ChatSider.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import { ChatContext } from '@/app/chat-context';
  2. import { apiInterceptors, delDialogue } from '@/client/api';
  3. import { IChatDialogueSchema } from '@/types/chat';
  4. import { CaretLeftOutlined, CaretRightOutlined, DeleteOutlined, ShareAltOutlined } from '@ant-design/icons';
  5. import type { MenuProps } from 'antd';
  6. import { Flex, Layout, Modal, Spin, Tooltip, Typography, message } from 'antd';
  7. import copy from 'copy-to-clipboard';
  8. import Image from 'next/image';
  9. import { useRouter, useSearchParams } from 'next/navigation';
  10. import React, { useContext, useMemo, useState } from 'react';
  11. import { useTranslation } from 'react-i18next';
  12. import AppDefaultIcon from '../../common/AppDefaultIcon';
  13. const { Sider } = Layout;
  14. const zeroWidthTriggerDefaultStyle: React.CSSProperties = {
  15. display: 'flex',
  16. alignItems: 'center',
  17. justifyContent: 'center',
  18. width: 16,
  19. height: 48,
  20. position: 'absolute',
  21. top: '50%',
  22. transform: 'translateY(-50%)',
  23. border: '1px solid #d6d8da',
  24. borderRadius: 8,
  25. right: -8,
  26. };
  27. /**
  28. * 会话项
  29. */
  30. const MenuItem: React.FC<{
  31. item: any;
  32. refresh?: any;
  33. order: React.MutableRefObject<number>;
  34. historyLoading?: boolean;
  35. }> = ({ item, refresh, historyLoading }) => {
  36. const { t } = useTranslation();
  37. const router = useRouter();
  38. const searchParams = useSearchParams();
  39. const chatId = searchParams?.get('id') ?? '';
  40. const scene = searchParams?.get('scene') ?? '';
  41. const { setCurrentDialogInfo } = useContext(ChatContext);
  42. // 当前活跃会话
  43. const active = useMemo(() => {
  44. if (item.default) {
  45. return item.default && !chatId && !scene;
  46. } else {
  47. return item.conv_uid === chatId && item.chat_mode === scene;
  48. }
  49. }, [chatId, scene, item]);
  50. // 删除会话
  51. const handleDelChat = () => {
  52. Modal.confirm({
  53. title: t('delete_chat'),
  54. content: t('delete_chat_confirm'),
  55. centered: true,
  56. onOk: async () => {
  57. const [err] = await apiInterceptors(delDialogue(item.conv_uid));
  58. if (err) {
  59. return;
  60. }
  61. await refresh?.();
  62. if (item.conv_uid === chatId) {
  63. router.push(`/chat`);
  64. }
  65. },
  66. });
  67. };
  68. return (
  69. <Flex
  70. align='center'
  71. className={`group/item w-full h-12 p-3 rounded-lg dark:hover:bg-theme-dark cursor-pointer mb-2 relative ${
  72. active ? 'bg-[#7288FA] dark:bg-theme-dark bg-opacity-100' : ''
  73. } ${ active ?'': 'hover:text-[#3d8dfc]'} ${ active ?'': 'hover:bg-[#d8d8d8]'}`}
  74. onClick={() => {
  75. if (historyLoading) {
  76. return;
  77. }
  78. !item.default &&
  79. setCurrentDialogInfo?.({
  80. chat_scene: item.chat_mode,
  81. app_code: item.app_code,
  82. });
  83. localStorage.setItem(
  84. 'cur_dialog_info',
  85. JSON.stringify({
  86. chat_scene: item.chat_mode,
  87. app_code: item.app_code,
  88. }),
  89. );
  90. router.push(item.default ? '/chat' : `?scene=${item.chat_mode}&id=${item.conv_uid}`);
  91. }}
  92. >
  93. <Tooltip title={item.chat_mode}>
  94. <div className='flex items-center justify-center w-8 h-8 rounded-lg mr-3 bg-white'>{item.icon}</div>
  95. </Tooltip>
  96. <div className='flex flex-1 line-clamp-1'>
  97. <Typography.Text
  98. ellipsis={{
  99. tooltip: true,
  100. }}
  101. >
  102. {item.label}
  103. </Typography.Text>
  104. </div>
  105. {!item.default && (
  106. <div className='flex gap-1 ml-1'>
  107. <div
  108. className='group-hover/item:opacity-100 cursor-pointer opacity-0'
  109. onClick={e => {
  110. e.stopPropagation();
  111. }}
  112. >
  113. <ShareAltOutlined
  114. style={{ fontSize: 16 }}
  115. onClick={() => {
  116. const success = copy(`${location.origin}/chat?scene=${item.chat_mode}&id=${item.conv_uid}`);
  117. message[success ? 'success' : 'error'](success ? t('copy_success') : t('copy_failed'));
  118. }}
  119. />
  120. </div>
  121. <div
  122. className='group-hover/item:opacity-100 cursor-pointer opacity-0'
  123. onClick={e => {
  124. e.stopPropagation();
  125. handleDelChat();
  126. }}
  127. >
  128. <DeleteOutlined style={{ fontSize: 16 }} />
  129. </div>
  130. </div>
  131. )}
  132. <div
  133. className={` w-1 rounded-sm bg-[#0c75fc] absolute top-1/2 left-0 -translate-y-1/2 transition-all duration-500 ease-in-out ${
  134. active ? 'h-5' : 'w-0 h-0'
  135. }`}
  136. />
  137. </Flex>
  138. );
  139. };
  140. const ChatSider: React.FC<{
  141. dialogueList: any;
  142. refresh: () => void;
  143. historyLoading: boolean;
  144. listLoading: boolean;
  145. order: React.MutableRefObject<number>;
  146. }> = ({ dialogueList = [], refresh, historyLoading, listLoading, order }) => {
  147. const searchParams = useSearchParams();
  148. const scene = searchParams?.get('scene') ?? '';
  149. const { t } = useTranslation();
  150. const { mode } = useContext(ChatContext);
  151. const [collapsed, setCollapsed] = useState<boolean>(scene === 'chat_dashboard');
  152. // 展开或收起列表按钮样式
  153. const triggerStyle: React.CSSProperties = useMemo(() => {
  154. if (collapsed) {
  155. return {
  156. ...zeroWidthTriggerDefaultStyle,
  157. right: -16,
  158. borderRadius: '0px 8px 8px 0',
  159. borderLeft: '1px solid #d5e5f6',
  160. };
  161. }
  162. return {
  163. ...zeroWidthTriggerDefaultStyle,
  164. borderLeft: '1px solid #d6d8da',
  165. };
  166. }, [collapsed]);
  167. // 会话列表配置项
  168. const items: MenuProps['items'] = useMemo(() => {
  169. const list = dialogueList[1] || [];
  170. if (list?.length > 0) {
  171. return list.map((item: IChatDialogueSchema) => ({
  172. ...item,
  173. label: item.user_input || item.select_param,
  174. key: item.conv_uid,
  175. icon: <AppDefaultIcon scene={item.chat_mode} />,
  176. default: false,
  177. }));
  178. }
  179. return [];
  180. }, [dialogueList]);
  181. return (
  182. <Sider
  183. className='bg-[#ffffff80] border-r border-[#d5e5f6] dark:bg-[#ffffff29] dark:border-[#ffffff66]'
  184. theme={mode}
  185. width={280}
  186. collapsible={true}
  187. collapsed={collapsed}
  188. collapsedWidth={0}
  189. trigger={collapsed ? <CaretRightOutlined className='text-base' /> : <CaretLeftOutlined className='text-base' />}
  190. zeroWidthTriggerStyle={triggerStyle}
  191. onCollapse={collapsed => setCollapsed(collapsed)}
  192. >
  193. <div className='flex flex-col h-full w-full bg-transparent px-4 pt-6 '>
  194. <div className='w-full text-base font-semibold text-[#1c2533] dark:text-[rgba(255,255,255,0.85)] mb-4 line-clamp-1'>
  195. {t('dialog_list')}
  196. </div>
  197. <Flex flex={1} vertical={true} className='overflow-y-auto'>
  198. <MenuItem
  199. item={{
  200. label: t('assistant'),
  201. key: 'default',
  202. icon: <Image src='/LOGO_SMALL.png' alt='default' width={24} height={24} className='flex-1' />,
  203. default: true,
  204. }}
  205. order={order}
  206. />
  207. <Spin spinning={listLoading} className='mt-2'>
  208. {!!items?.length &&
  209. items.map(item => (
  210. <MenuItem key={item?.key} item={item} refresh={refresh} historyLoading={historyLoading} order={order} />
  211. ))}
  212. </Spin>
  213. </Flex>
  214. </div>
  215. </Sider>
  216. );
  217. };
  218. export default ChatSider;