index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. import { ChatContext } from '@/app/chat-context';
  2. import { apiInterceptors, getAppInfo, getChatHistory, getDialogueList, postChatModeParamsList } from '@/client/api';
  3. import useUser from '@/hooks/use-user';
  4. import { IApp } from '@/types/app';
  5. import { ChatHistoryResponse } from '@/types/chat';
  6. import { getUserId } from '@/utils';
  7. import { HEADER_USER_ID_KEY } from '@/utils/constants/index';
  8. import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source';
  9. import { useRequest } from 'ahooks';
  10. import { Spin } from 'antd';
  11. import dynamic from 'next/dynamic';
  12. import { useSearchParams } from 'next/navigation';
  13. import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
  14. import Header from './components/Header';
  15. import InputContainer from './components/InputContainer';
  16. const Content = dynamic(() => import('@/pages/mobile/chat/components/Content'), { ssr: false });
  17. interface MobileChatProps {
  18. model: string;
  19. temperature: number;
  20. resource: any;
  21. setResource: React.Dispatch<React.SetStateAction<any>>;
  22. setTemperature: React.Dispatch<React.SetStateAction<number>>;
  23. setModel: React.Dispatch<React.SetStateAction<string>>;
  24. scene: string;
  25. history: ChatHistoryResponse; // 会话内容
  26. setHistory: React.Dispatch<React.SetStateAction<ChatHistoryResponse>>;
  27. scrollViewRef: React.RefObject<HTMLDivElement>; // 会话可滚动区域
  28. appInfo: IApp;
  29. conv_uid: string;
  30. resourceList?: Record<string, any>[];
  31. order: React.MutableRefObject<number>;
  32. handleChat: (_content?: string) => Promise<void>;
  33. canAbort: boolean;
  34. setCarAbort: React.Dispatch<React.SetStateAction<boolean>>;
  35. canNewChat: boolean;
  36. setCanNewChat: React.Dispatch<React.SetStateAction<boolean>>;
  37. ctrl: React.MutableRefObject<AbortController | undefined>;
  38. userInput: string;
  39. setUserInput: React.Dispatch<React.SetStateAction<string>>;
  40. getChatHistoryRun: () => void;
  41. }
  42. export const MobileChatContext = createContext<MobileChatProps>({
  43. model: '',
  44. temperature: 0.5,
  45. resource: null,
  46. setModel: () => {},
  47. setTemperature: () => {},
  48. setResource: () => {},
  49. scene: '',
  50. history: [],
  51. setHistory: () => {},
  52. scrollViewRef: { current: null },
  53. appInfo: {} as IApp,
  54. conv_uid: '',
  55. resourceList: [],
  56. order: { current: 1 },
  57. handleChat: () => Promise.resolve(),
  58. canAbort: false,
  59. setCarAbort: () => {},
  60. canNewChat: false,
  61. setCanNewChat: () => {},
  62. ctrl: { current: undefined },
  63. userInput: '',
  64. setUserInput: () => {},
  65. getChatHistoryRun: () => {},
  66. });
  67. const MobileChat: React.FC = () => {
  68. // 从url上获取基本参数
  69. const searchParams = useSearchParams();
  70. const chatScene = searchParams?.get('chat_scene') ?? '';
  71. const appCode = searchParams?.get('app_code') ?? '';
  72. // 模型列表
  73. const { modelList } = useContext(ChatContext);
  74. const [history, setHistory] = useState<ChatHistoryResponse>([]);
  75. const [model, setModel] = useState<string>('');
  76. const [temperature, setTemperature] = useState<number>(0.5);
  77. const [resource, setResource] = useState<any>(null);
  78. const scrollViewRef = useRef<HTMLDivElement>(null);
  79. // 用户输入
  80. const [userInput, setUserInput] = useState<string>('');
  81. // 回复可以终止
  82. const [canAbort, setCarAbort] = useState<boolean>(false);
  83. // 是否可以开始新的提问,上一次回答结束或者暂停才可以开始新的提问
  84. const [canNewChat, setCanNewChat] = useState<boolean>(true);
  85. const ctrl = useRef<AbortController>();
  86. const order = useRef<number>(1);
  87. // 用户信息
  88. const userInfo = useUser();
  89. // 会话id
  90. const conv_uid = useMemo(() => `${userInfo?.user_no}_${appCode}`, [appCode, userInfo]);
  91. // 获取历史会话记录
  92. const { run: getChatHistoryRun, loading: historyLoading } = useRequest(
  93. async () => await apiInterceptors(getChatHistory(`${userInfo?.user_no}_${appCode}`)),
  94. {
  95. manual: true,
  96. onSuccess: data => {
  97. const [, res] = data;
  98. const viewList = res?.filter(item => item.role === 'view');
  99. if (viewList && viewList.length > 0) {
  100. order.current = viewList[viewList.length - 1].order + 1;
  101. }
  102. setHistory(res || []);
  103. },
  104. },
  105. );
  106. // 获取应用信息
  107. const {
  108. data: appInfo,
  109. run: getAppInfoRun,
  110. loading: appInfoLoading,
  111. } = useRequest(
  112. async (params: { chat_scene: string; app_code: string }) => {
  113. const [, res] = await apiInterceptors(getAppInfo(params));
  114. return res ?? ({} as IApp);
  115. },
  116. {
  117. manual: true,
  118. },
  119. );
  120. // 获取可选择的资源类型列表
  121. const {
  122. run,
  123. data,
  124. loading: resourceLoading,
  125. } = useRequest(
  126. async () => {
  127. const [, res] = await apiInterceptors(postChatModeParamsList(chatScene));
  128. setResource(res?.[0]?.space_id || res?.[0]?.param);
  129. return res ?? [];
  130. },
  131. {
  132. manual: true,
  133. },
  134. );
  135. // 获取会话列表
  136. const { run: getDialogueListRun, loading: dialogueListLoading } = useRequest(
  137. async () => {
  138. const [, res] = await apiInterceptors(getDialogueList());
  139. return res ?? [];
  140. },
  141. {
  142. manual: true,
  143. onSuccess: data => {
  144. const filterDialogue = data?.filter(item => item.conv_uid === conv_uid)?.[0];
  145. filterDialogue?.select_param && setResource(JSON.parse(filterDialogue?.select_param));
  146. },
  147. },
  148. );
  149. // 获取应用信息
  150. useEffect(() => {
  151. if (chatScene && appCode && modelList.length) {
  152. getAppInfoRun({ chat_scene: chatScene, app_code: appCode });
  153. }
  154. }, [appCode, chatScene, getAppInfoRun, modelList]);
  155. // 设置历史会话记录
  156. useEffect(() => {
  157. appCode && getChatHistoryRun();
  158. // eslint-disable-next-line react-hooks/exhaustive-deps
  159. }, [appCode]);
  160. // 设置默认模型
  161. useEffect(() => {
  162. if (modelList.length > 0) {
  163. // 获取应用信息中的model值
  164. const infoModel = appInfo?.param_need?.filter(item => item.type === 'model')?.[0]?.value;
  165. setModel(infoModel || modelList[0]);
  166. }
  167. }, [modelList, appInfo]);
  168. // 设置默认温度;
  169. useEffect(() => {
  170. // 获取应用信息中的model值
  171. const infoTemperature = appInfo?.param_need?.filter(item => item.type === 'temperature')?.[0]?.value;
  172. setTemperature(infoTemperature || 0.5);
  173. }, [appInfo]);
  174. // 获取可选择资源列表
  175. useEffect(() => {
  176. if (chatScene && appInfo?.app_code) {
  177. const resourceVal = appInfo?.param_need?.filter(item => item.type === 'resource')?.[0]?.value;
  178. const bindResourceVal = appInfo?.param_need?.filter(item => item.type === 'resource')?.[0]?.bind_value;
  179. bindResourceVal && setResource(bindResourceVal);
  180. ['database', 'knowledge', 'plugin', 'awel_flow'].includes(resourceVal) && !bindResourceVal && run();
  181. }
  182. }, [appInfo, chatScene, run]);
  183. // 处理会话
  184. const handleChat = async (content?: string) => {
  185. setUserInput('');
  186. ctrl.current = new AbortController();
  187. const params = {
  188. chat_mode: chatScene,
  189. model_name: model,
  190. user_input: content || userInput,
  191. conv_uid,
  192. temperature,
  193. app_code: appInfo?.app_code,
  194. ...(resource && { select_param: resource }),
  195. };
  196. if (history && history.length > 0) {
  197. const viewList = history?.filter(item => item.role === 'view');
  198. order.current = viewList[viewList.length - 1].order + 1;
  199. }
  200. const tempHistory: ChatHistoryResponse = [
  201. {
  202. role: 'human',
  203. context: content || userInput,
  204. model_name: model,
  205. order: order.current,
  206. time_stamp: 0,
  207. },
  208. {
  209. role: 'view',
  210. context: '',
  211. model_name: model,
  212. order: order.current,
  213. time_stamp: 0,
  214. thinking: true,
  215. },
  216. ];
  217. const index = tempHistory.length - 1;
  218. setHistory([...history, ...tempHistory]);
  219. setCanNewChat(false);
  220. try {
  221. await fetchEventSource(`${process.env.API_BASE_URL ?? ''}/api/v1/chat/completions`, {
  222. method: 'POST',
  223. headers: {
  224. 'Content-Type': 'application/json',
  225. [HEADER_USER_ID_KEY]: getUserId() ?? '',
  226. },
  227. signal: ctrl.current.signal,
  228. body: JSON.stringify(params),
  229. openWhenHidden: true,
  230. async onopen(response) {
  231. if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
  232. return;
  233. }
  234. },
  235. onclose() {
  236. ctrl.current?.abort();
  237. setCanNewChat(true);
  238. setCarAbort(false);
  239. },
  240. onerror(err) {
  241. throw new Error(err);
  242. },
  243. onmessage: event => {
  244. let message = event.data;
  245. try {
  246. message = JSON.parse(message).vis;
  247. } catch {
  248. message.replaceAll('\\n', '\n');
  249. }
  250. if (message === '[DONE]') {
  251. setCanNewChat(true);
  252. setCarAbort(false);
  253. } else if (message?.startsWith('[ERROR]')) {
  254. tempHistory[index].context = message?.replace('[ERROR]', '');
  255. tempHistory[index].thinking = false;
  256. setHistory([...history, ...tempHistory]);
  257. setCanNewChat(true);
  258. setCarAbort(false);
  259. } else {
  260. setCarAbort(true);
  261. tempHistory[index].context = message;
  262. tempHistory[index].thinking = false;
  263. setHistory([...history, ...tempHistory]);
  264. }
  265. },
  266. });
  267. } catch {
  268. ctrl.current?.abort();
  269. tempHistory[index].context = 'Sorry, we meet some error, please try again later.';
  270. tempHistory[index].thinking = false;
  271. setHistory([...tempHistory]);
  272. setCanNewChat(true);
  273. setCarAbort(false);
  274. }
  275. };
  276. // 如果是原生应用,拉取会话列表获取资源参数
  277. useEffect(() => {
  278. if (chatScene && chatScene !== 'chat_agent') {
  279. getDialogueListRun();
  280. }
  281. }, [chatScene, getDialogueListRun]);
  282. return (
  283. <MobileChatContext.Provider
  284. value={{
  285. model,
  286. resource,
  287. setModel,
  288. setTemperature,
  289. setResource,
  290. temperature,
  291. appInfo: appInfo as IApp,
  292. conv_uid,
  293. scene: chatScene,
  294. history,
  295. scrollViewRef,
  296. setHistory,
  297. resourceList: data,
  298. order,
  299. handleChat,
  300. setCanNewChat,
  301. ctrl,
  302. canAbort,
  303. setCarAbort,
  304. canNewChat,
  305. userInput,
  306. setUserInput,
  307. getChatHistoryRun,
  308. }}
  309. >
  310. <Spin
  311. size='large'
  312. className='flex h-screen w-screen justify-center items-center max-h-screen'
  313. spinning={historyLoading || appInfoLoading || resourceLoading || dialogueListLoading}
  314. >
  315. <div className='flex flex-col h-screen bg-gradient-light dark:bg-gradient-dark p-4 pt-0'>
  316. <div ref={scrollViewRef} className='flex flex-col flex-1 overflow-y-auto mb-3'>
  317. <Header />
  318. <Content />
  319. </div>
  320. {appInfo?.app_code && <InputContainer />}
  321. </div>
  322. </Spin>
  323. </MobileChatContext.Provider>
  324. );
  325. };
  326. export default MobileChat;