index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. import { ChatContext } from '@/app/chat-context';
  2. import { apiInterceptors, getAppInfo, getChatHistory, getDialogueList } from '@/client/api';
  3. import useChat from '@/hooks/use-chat';
  4. import ChatContentContainer from '@/new-components/chat/ChatContentContainer';
  5. import ChatDefault from '@/new-components/chat/content/ChatDefault';
  6. import ChatInputPanel from '@/new-components/chat/input/ChatInputPanel';
  7. import ChatSider from '@/new-components/chat/sider/ChatSider';
  8. import { IApp } from '@/types/app';
  9. import { ChartData, ChatHistoryResponse, IChatDialogueSchema } from '@/types/chat';
  10. import { getInitMessage } from '@/utils';
  11. import { useAsyncEffect, useRequest } from 'ahooks';
  12. import { Flex, Layout, Spin } from 'antd';
  13. import dynamic from 'next/dynamic';
  14. import { useSearchParams } from 'next/navigation';
  15. import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
  16. const DbEditor = dynamic(() => import('@/components/chat/db-editor'), {
  17. ssr: false,
  18. });
  19. const ChatContainer = dynamic(() => import('@/components/chat/chat-container'), { ssr: false });
  20. const { Content } = Layout;
  21. interface ChatContentProps {
  22. history: ChatHistoryResponse; // 会话记录列表
  23. replyLoading: boolean; // 对话回复loading
  24. scrollRef: React.RefObject<HTMLDivElement>; // 会话内容可滚动dom
  25. canAbort: boolean; // 是否能中断回复
  26. chartsData: ChartData[];
  27. agent: string;
  28. currentDialogue: IChatDialogueSchema; // 当前选择的会话
  29. appInfo: IApp;
  30. temperatureValue: any;
  31. resourceValue: any;
  32. modelValue: string;
  33. setModelValue: React.Dispatch<React.SetStateAction<string>>;
  34. setTemperatureValue: React.Dispatch<React.SetStateAction<any>>;
  35. setResourceValue: React.Dispatch<React.SetStateAction<any>>;
  36. setAppInfo: React.Dispatch<React.SetStateAction<IApp>>;
  37. setAgent: React.Dispatch<React.SetStateAction<string>>;
  38. setCanAbort: React.Dispatch<React.SetStateAction<boolean>>;
  39. setReplyLoading: React.Dispatch<React.SetStateAction<boolean>>;
  40. handleChat: (content: string, data?: Record<string, any>) => Promise<void>; // 处理会话请求逻辑函数
  41. refreshDialogList: () => void;
  42. refreshHistory: () => void;
  43. refreshAppInfo: () => void;
  44. setHistory: React.Dispatch<React.SetStateAction<ChatHistoryResponse>>;
  45. }
  46. export const ChatContentContext = createContext<ChatContentProps>({
  47. history: [],
  48. replyLoading: false,
  49. scrollRef: { current: null },
  50. canAbort: false,
  51. chartsData: [],
  52. agent: '',
  53. currentDialogue: {} as any,
  54. appInfo: {} as any,
  55. temperatureValue: 0.5,
  56. resourceValue: {},
  57. modelValue: '',
  58. setModelValue: () => {},
  59. setResourceValue: () => {},
  60. setTemperatureValue: () => {},
  61. setAppInfo: () => {},
  62. setAgent: () => {},
  63. setCanAbort: () => {},
  64. setReplyLoading: () => {},
  65. refreshDialogList: () => {},
  66. refreshHistory: () => {},
  67. refreshAppInfo: () => {},
  68. setHistory: () => {},
  69. handleChat: () => Promise.resolve(),
  70. });
  71. const Chat: React.FC = () => {
  72. const { model, currentDialogInfo } = useContext(ChatContext);
  73. const { isContract, setIsContract, setIsMenuExpand } = useContext(ChatContext);
  74. const { chat, ctrl } = useChat({
  75. app_code: currentDialogInfo.app_code || '',
  76. });
  77. const searchParams = useSearchParams();
  78. const chatId = searchParams?.get('id') ?? '';
  79. const scene = searchParams?.get('scene') ?? '';
  80. const knowledgeId = searchParams?.get('knowledge_id') ?? '';
  81. const dbName = searchParams?.get('db_name') ?? '';
  82. const scrollRef = useRef<HTMLDivElement>(null);
  83. const order = useRef<number>(1);
  84. const [history, setHistory] = useState<ChatHistoryResponse>([]);
  85. const [chartsData] = useState<Array<ChartData>>();
  86. const [replyLoading, setReplyLoading] = useState<boolean>(false);
  87. const [canAbort, setCanAbort] = useState<boolean>(false);
  88. const [agent, setAgent] = useState<string>('');
  89. const [appInfo, setAppInfo] = useState<IApp>({} as IApp);
  90. const [temperatureValue, setTemperatureValue] = useState();
  91. const [resourceValue, setResourceValue] = useState<any>();
  92. const [modelValue, setModelValue] = useState<string>('');
  93. useEffect(() => {
  94. setTemperatureValue(appInfo?.param_need?.filter(item => item.type === 'temperature')[0]?.value || 0.5);
  95. setModelValue(appInfo?.param_need?.filter(item => item.type === 'model')[0]?.value || model);
  96. setResourceValue(
  97. knowledgeId || dbName || appInfo?.param_need?.filter(item => item.type === 'resource')[0]?.bind_value,
  98. );
  99. }, [appInfo, dbName, knowledgeId, model]);
  100. useEffect(() => {
  101. // 仅初始化执行,防止dashboard页面无法切换状态
  102. setIsMenuExpand(scene !== 'chat_dashboard');
  103. // 路由变了要取消Editor模式,再进来是默认的Preview模式
  104. if (chatId && scene) {
  105. setIsContract(false);
  106. }
  107. }, [chatId, scene]);
  108. // 是否是默认小助手
  109. const isChatDefault = useMemo(() => {
  110. return !chatId && !scene;
  111. }, [chatId, scene]);
  112. // 获取会话列表
  113. const {
  114. data: dialogueList = [],
  115. refresh: refreshDialogList,
  116. loading: listLoading,
  117. } = useRequest(async () => {
  118. return await apiInterceptors(getDialogueList());
  119. });
  120. // 获取应用详情
  121. const { run: queryAppInfo, refresh: refreshAppInfo } = useRequest(
  122. async () =>
  123. await apiInterceptors(
  124. getAppInfo({
  125. ...currentDialogInfo,
  126. }),
  127. ),
  128. {
  129. manual: true,
  130. onSuccess: data => {
  131. const [, res] = data;
  132. setAppInfo(res || ({} as IApp));
  133. },
  134. },
  135. );
  136. // 列表当前活跃对话
  137. const currentDialogue = useMemo(() => {
  138. const [, list] = dialogueList;
  139. return list?.find(item => item.conv_uid === chatId) || ({} as IChatDialogueSchema);
  140. }, [chatId, dialogueList]);
  141. useEffect(() => {
  142. const initMessage = getInitMessage();
  143. if (currentDialogInfo.chat_scene === scene && !isChatDefault && !(initMessage && initMessage.message)) {
  144. queryAppInfo();
  145. }
  146. }, [chatId, currentDialogInfo, isChatDefault, queryAppInfo, scene]);
  147. // 获取会话历史记录
  148. const {
  149. run: getHistory,
  150. loading: historyLoading,
  151. refresh: refreshHistory,
  152. } = useRequest(async () => await apiInterceptors(getChatHistory(chatId)), {
  153. manual: true,
  154. onSuccess: data => {
  155. const [, res] = data;
  156. const viewList = res?.filter(item => item.role === 'view');
  157. if (viewList && viewList.length > 0) {
  158. order.current = viewList[viewList.length - 1].order + 1;
  159. }
  160. setHistory(res || []);
  161. },
  162. });
  163. // 会话提问
  164. const handleChat = useCallback(
  165. (content: string, data?: Record<string, any>) => {
  166. return new Promise<void>(resolve => {
  167. const initMessage = getInitMessage();
  168. const ctrl = new AbortController();
  169. setReplyLoading(true);
  170. if (history && history.length > 0) {
  171. const viewList = history?.filter(item => item.role === 'view');
  172. const humanList = history?.filter(item => item.role === 'human');
  173. order.current = (viewList[viewList.length - 1]?.order || humanList[humanList.length - 1]?.order) + 1;
  174. }
  175. const tempHistory: ChatHistoryResponse = [
  176. ...(initMessage && initMessage.id === chatId ? [] : history),
  177. {
  178. role: 'human',
  179. context: content,
  180. model_name: data?.model_name || modelValue,
  181. order: order.current,
  182. time_stamp: 0,
  183. },
  184. {
  185. role: 'view',
  186. context: '',
  187. model_name: data?.model_name || modelValue,
  188. order: order.current,
  189. time_stamp: 0,
  190. thinking: true,
  191. },
  192. ];
  193. const index = tempHistory.length - 1;
  194. setHistory([...tempHistory]);
  195. chat({
  196. data: {
  197. chat_mode: scene,
  198. model_name: modelValue,
  199. user_input: content,
  200. ...data,
  201. },
  202. ctrl,
  203. chatId,
  204. onMessage: message => {
  205. setCanAbort(true);
  206. if (data?.incremental) {
  207. tempHistory[index].context += message;
  208. tempHistory[index].thinking = false;
  209. } else {
  210. tempHistory[index].context = message;
  211. tempHistory[index].thinking = false;
  212. }
  213. setHistory([...tempHistory]);
  214. },
  215. onDone: () => {
  216. setReplyLoading(false);
  217. setCanAbort(false);
  218. resolve();
  219. },
  220. onClose: () => {
  221. setReplyLoading(false);
  222. setCanAbort(false);
  223. resolve();
  224. },
  225. onError: message => {
  226. setReplyLoading(false);
  227. setCanAbort(false);
  228. tempHistory[index].context = message;
  229. tempHistory[index].thinking = false;
  230. setHistory([...tempHistory]);
  231. resolve();
  232. },
  233. });
  234. });
  235. },
  236. [chatId, history, modelValue, chat, scene],
  237. );
  238. useAsyncEffect(async () => {
  239. // 如果是默认小助手,不获取历史记录
  240. if (isChatDefault) {
  241. return;
  242. }
  243. const initMessage = getInitMessage();
  244. if (initMessage && initMessage.id === chatId) {
  245. return;
  246. }
  247. await getHistory();
  248. }, [chatId, scene, getHistory]);
  249. useEffect(() => {
  250. if (isChatDefault) {
  251. order.current = 1;
  252. setHistory([]);
  253. }
  254. }, [isChatDefault]);
  255. const contentRender = () => {
  256. if (scene === 'chat_dashboard') {
  257. return isContract ? <DbEditor /> : <ChatContainer />;
  258. } else {
  259. return isChatDefault ? (
  260. <Content>
  261. <ChatDefault />
  262. </Content>
  263. ) : (
  264. <Spin spinning={historyLoading} className='w-full h-full m-auto'>
  265. <Content className='flex flex-col h-screen'>
  266. <ChatContentContainer ref={scrollRef} />
  267. <ChatInputPanel ctrl={ctrl} />
  268. </Content>
  269. </Spin>
  270. );
  271. }
  272. };
  273. return (
  274. <ChatContentContext.Provider
  275. value={{
  276. history,
  277. replyLoading,
  278. scrollRef,
  279. canAbort,
  280. chartsData: chartsData || [],
  281. agent,
  282. currentDialogue,
  283. appInfo,
  284. temperatureValue,
  285. resourceValue,
  286. modelValue,
  287. setModelValue,
  288. setResourceValue,
  289. setTemperatureValue,
  290. setAppInfo,
  291. setAgent,
  292. setCanAbort,
  293. setReplyLoading,
  294. handleChat,
  295. refreshDialogList,
  296. refreshHistory,
  297. refreshAppInfo,
  298. setHistory,
  299. }}
  300. >
  301. <Flex flex={1}>
  302. <Layout className='bg-gradient-light bg-cover bg-center dark:bg-gradient-dark'>
  303. <ChatSider
  304. refresh={refreshDialogList}
  305. dialogueList={dialogueList}
  306. listLoading={listLoading}
  307. historyLoading={historyLoading}
  308. order={order}
  309. />
  310. <Layout className='bg-transparent'>{contentRender()}</Layout>
  311. </Layout>
  312. </Flex>
  313. </ChatContentContext.Provider>
  314. );
  315. };
  316. export default Chat;