You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. import { MessageType } from '@/constants/chat';
  2. import { fileIconMap } from '@/constants/common';
  3. import {
  4. useFetchManualConversation,
  5. useFetchManualDialog,
  6. useFetchNextConversation,
  7. useFetchNextConversationList,
  8. useFetchNextDialog,
  9. useGetChatSearchParams,
  10. useRemoveNextConversation,
  11. useRemoveNextDialog,
  12. useSetNextDialog,
  13. useUpdateNextConversation,
  14. } from '@/hooks/chat-hooks';
  15. import {
  16. useSetModalState,
  17. useShowDeleteConfirm,
  18. useTranslate,
  19. } from '@/hooks/common-hooks';
  20. import {
  21. useRemoveMessageById,
  22. useSendMessageWithSse,
  23. } from '@/hooks/logic-hooks';
  24. import {
  25. IAnswer,
  26. IConversation,
  27. IDialog,
  28. Message,
  29. } from '@/interfaces/database/chat';
  30. import { IChunk } from '@/interfaces/database/knowledge';
  31. import { getFileExtension } from '@/utils';
  32. import { buildMessageUuid } from '@/utils/chat';
  33. import { useMutationState } from '@tanstack/react-query';
  34. import { get } from 'lodash';
  35. import trim from 'lodash/trim';
  36. import {
  37. ChangeEventHandler,
  38. useCallback,
  39. useEffect,
  40. useMemo,
  41. useRef,
  42. useState,
  43. } from 'react';
  44. import { useSearchParams } from 'umi';
  45. import { v4 as uuid } from 'uuid';
  46. import { ChatSearchParams } from './constants';
  47. import {
  48. IClientConversation,
  49. IMessage,
  50. VariableTableDataType,
  51. } from './interface';
  52. export const useSelectCurrentDialog = () => {
  53. const data = useMutationState({
  54. filters: { mutationKey: ['fetchDialog'] },
  55. select: (mutation) => {
  56. return get(mutation, 'state.data.data', {});
  57. },
  58. });
  59. return (data.at(-1) ?? {}) as IDialog;
  60. };
  61. export const useSelectPromptConfigParameters = (): VariableTableDataType[] => {
  62. const { data: currentDialog } = useFetchNextDialog();
  63. const finalParameters: VariableTableDataType[] = useMemo(() => {
  64. const parameters = currentDialog?.prompt_config?.parameters ?? [];
  65. if (!currentDialog.id) {
  66. // The newly created chat has a default parameter
  67. return [{ key: uuid(), variable: 'knowledge', optional: false }];
  68. }
  69. return parameters.map((x) => ({
  70. key: uuid(),
  71. variable: x.key,
  72. optional: x.optional,
  73. }));
  74. }, [currentDialog]);
  75. return finalParameters;
  76. };
  77. export const useDeleteDialog = () => {
  78. const showDeleteConfirm = useShowDeleteConfirm();
  79. const { removeDialog } = useRemoveNextDialog();
  80. const onRemoveDialog = (dialogIds: Array<string>) => {
  81. showDeleteConfirm({ onOk: () => removeDialog(dialogIds) });
  82. };
  83. return { onRemoveDialog };
  84. };
  85. export const useHandleItemHover = () => {
  86. const [activated, setActivated] = useState<string>('');
  87. const handleItemEnter = (id: string) => {
  88. setActivated(id);
  89. };
  90. const handleItemLeave = () => {
  91. setActivated('');
  92. };
  93. return {
  94. activated,
  95. handleItemEnter,
  96. handleItemLeave,
  97. };
  98. };
  99. export const useEditDialog = () => {
  100. const [dialog, setDialog] = useState<IDialog>({} as IDialog);
  101. const { fetchDialog } = useFetchManualDialog();
  102. const { setDialog: submitDialog, loading } = useSetNextDialog();
  103. const {
  104. visible: dialogEditVisible,
  105. hideModal: hideDialogEditModal,
  106. showModal: showDialogEditModal,
  107. } = useSetModalState();
  108. const hideModal = useCallback(() => {
  109. setDialog({} as IDialog);
  110. hideDialogEditModal();
  111. }, [hideDialogEditModal]);
  112. const onDialogEditOk = useCallback(
  113. async (dialog: IDialog) => {
  114. const ret = await submitDialog(dialog);
  115. if (ret === 0) {
  116. hideModal();
  117. }
  118. },
  119. [submitDialog, hideModal],
  120. );
  121. const handleShowDialogEditModal = useCallback(
  122. async (dialogId?: string) => {
  123. if (dialogId) {
  124. const ret = await fetchDialog(dialogId);
  125. if (ret.retcode === 0) {
  126. setDialog(ret.data);
  127. }
  128. }
  129. showDialogEditModal();
  130. },
  131. [showDialogEditModal, fetchDialog],
  132. );
  133. const clearDialog = useCallback(() => {
  134. setDialog({} as IDialog);
  135. }, []);
  136. return {
  137. dialogSettingLoading: loading,
  138. initialDialog: dialog,
  139. onDialogEditOk,
  140. dialogEditVisible,
  141. hideDialogEditModal: hideModal,
  142. showDialogEditModal: handleShowDialogEditModal,
  143. clearDialog,
  144. };
  145. };
  146. //#region conversation
  147. export const useSelectDerivedConversationList = () => {
  148. const { t } = useTranslate('chat');
  149. const [list, setList] = useState<Array<IConversation>>([]);
  150. const { data: currentDialog } = useFetchNextDialog();
  151. const { data: conversationList, loading } = useFetchNextConversationList();
  152. const { dialogId } = useGetChatSearchParams();
  153. const prologue = currentDialog?.prompt_config?.prologue ?? '';
  154. const addTemporaryConversation = useCallback(() => {
  155. setList((pre) => {
  156. if (dialogId) {
  157. const nextList = [
  158. {
  159. id: '',
  160. name: t('newConversation'),
  161. dialog_id: dialogId,
  162. message: [
  163. {
  164. content: prologue,
  165. role: MessageType.Assistant,
  166. },
  167. ],
  168. } as IConversation,
  169. ...conversationList,
  170. ];
  171. return nextList;
  172. }
  173. return pre;
  174. });
  175. }, [conversationList, dialogId, prologue, t]);
  176. useEffect(() => {
  177. addTemporaryConversation();
  178. }, [addTemporaryConversation]);
  179. return { list, addTemporaryConversation, loading };
  180. };
  181. export const useClickConversationCard = () => {
  182. const [currentQueryParameters, setSearchParams] = useSearchParams();
  183. const newQueryParameters: URLSearchParams = useMemo(
  184. () => new URLSearchParams(currentQueryParameters.toString()),
  185. [currentQueryParameters],
  186. );
  187. const handleClickConversation = useCallback(
  188. (conversationId: string) => {
  189. newQueryParameters.set(ChatSearchParams.ConversationId, conversationId);
  190. setSearchParams(newQueryParameters);
  191. },
  192. [newQueryParameters, setSearchParams],
  193. );
  194. return { handleClickConversation };
  195. };
  196. export const useSetConversation = () => {
  197. const { dialogId } = useGetChatSearchParams();
  198. const { updateConversation } = useUpdateNextConversation();
  199. const setConversation = useCallback(
  200. (message: string) => {
  201. return updateConversation({
  202. dialog_id: dialogId,
  203. name: message,
  204. message: [
  205. {
  206. role: MessageType.Assistant,
  207. content: message,
  208. },
  209. ],
  210. });
  211. },
  212. [updateConversation, dialogId],
  213. );
  214. return { setConversation };
  215. };
  216. export const useSelectCurrentConversation = () => {
  217. const [currentConversation, setCurrentConversation] =
  218. useState<IClientConversation>({} as IClientConversation);
  219. const { data: conversation, loading } = useFetchNextConversation();
  220. const { data: dialog } = useFetchNextDialog();
  221. const { conversationId, dialogId } = useGetChatSearchParams();
  222. const { removeMessageById } = useRemoveMessageById(setCurrentConversation);
  223. // Show the entered message in the conversation immediately after sending the message
  224. const addNewestConversation = useCallback(
  225. (message: Message, answer: string = '') => {
  226. setCurrentConversation((pre) => {
  227. return {
  228. ...pre,
  229. message: [
  230. ...pre.message,
  231. {
  232. ...message,
  233. id: buildMessageUuid(message),
  234. } as IMessage,
  235. {
  236. role: MessageType.Assistant,
  237. content: answer,
  238. id: buildMessageUuid({ ...message, role: MessageType.Assistant }),
  239. reference: {},
  240. } as IMessage,
  241. ],
  242. };
  243. });
  244. },
  245. [],
  246. );
  247. // Add the streaming message to the last item in the message list
  248. const addNewestAnswer = useCallback((answer: IAnswer) => {
  249. setCurrentConversation((pre) => {
  250. const latestMessage = pre.message?.at(-1);
  251. if (latestMessage) {
  252. return {
  253. ...pre,
  254. message: [
  255. ...pre.message.slice(0, -1),
  256. {
  257. ...latestMessage,
  258. content: answer.answer,
  259. reference: answer.reference,
  260. id: buildMessageUuid({
  261. id: answer.id,
  262. role: MessageType.Assistant,
  263. }),
  264. prompt: answer.prompt,
  265. } as IMessage,
  266. ],
  267. };
  268. }
  269. return pre;
  270. });
  271. }, []);
  272. const removeLatestMessage = useCallback(() => {
  273. setCurrentConversation((pre) => {
  274. const nextMessages = pre.message?.slice(0, -2) ?? [];
  275. return {
  276. ...pre,
  277. message: nextMessages,
  278. };
  279. });
  280. }, []);
  281. const addPrologue = useCallback(() => {
  282. if (dialogId !== '' && conversationId === '') {
  283. const prologue = dialog.prompt_config?.prologue;
  284. const nextMessage = {
  285. role: MessageType.Assistant,
  286. content: prologue,
  287. id: uuid(),
  288. } as IMessage;
  289. setCurrentConversation({
  290. id: '',
  291. dialog_id: dialogId,
  292. reference: [],
  293. message: [nextMessage],
  294. } as any);
  295. }
  296. }, [conversationId, dialog, dialogId]);
  297. useEffect(() => {
  298. addPrologue();
  299. }, [addPrologue]);
  300. useEffect(() => {
  301. if (conversationId) {
  302. setCurrentConversation(conversation);
  303. }
  304. }, [conversation, conversationId]);
  305. return {
  306. currentConversation,
  307. addNewestConversation,
  308. removeLatestMessage,
  309. addNewestAnswer,
  310. removeMessageById,
  311. loading,
  312. };
  313. };
  314. export const useScrollToBottom = (currentConversation: IClientConversation) => {
  315. const ref = useRef<HTMLDivElement>(null);
  316. const scrollToBottom = useCallback(() => {
  317. if (currentConversation.id) {
  318. ref.current?.scrollIntoView({ behavior: 'instant' });
  319. }
  320. }, [currentConversation]);
  321. useEffect(() => {
  322. scrollToBottom();
  323. }, [scrollToBottom]);
  324. return ref;
  325. };
  326. export const useFetchConversationOnMount = () => {
  327. const { conversationId } = useGetChatSearchParams();
  328. const {
  329. currentConversation,
  330. addNewestConversation,
  331. removeLatestMessage,
  332. addNewestAnswer,
  333. loading,
  334. removeMessageById,
  335. } = useSelectCurrentConversation();
  336. const ref = useScrollToBottom(currentConversation);
  337. return {
  338. currentConversation,
  339. addNewestConversation,
  340. ref,
  341. removeLatestMessage,
  342. addNewestAnswer,
  343. conversationId,
  344. loading,
  345. removeMessageById,
  346. };
  347. };
  348. export const useHandleMessageInputChange = () => {
  349. const [value, setValue] = useState('');
  350. const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
  351. const value = e.target.value;
  352. const nextValue = value.replaceAll('\\n', '\n').replaceAll('\\t', '\t');
  353. setValue(nextValue);
  354. };
  355. return {
  356. handleInputChange,
  357. value,
  358. setValue,
  359. };
  360. };
  361. export const useSendMessage = (
  362. conversation: IClientConversation,
  363. addNewestConversation: (message: Message, answer?: string) => void,
  364. removeLatestMessage: () => void,
  365. addNewestAnswer: (answer: IAnswer) => void,
  366. ) => {
  367. const { setConversation } = useSetConversation();
  368. const { conversationId } = useGetChatSearchParams();
  369. const { handleInputChange, value, setValue } = useHandleMessageInputChange();
  370. const { handleClickConversation } = useClickConversationCard();
  371. const { send, answer, done, setDone } = useSendMessageWithSse();
  372. const sendMessage = useCallback(
  373. async (message: Message, documentIds: string[], id?: string) => {
  374. const res = await send({
  375. conversation_id: id ?? conversationId,
  376. messages: [
  377. ...(conversation?.message ?? []),
  378. {
  379. ...message,
  380. doc_ids: documentIds,
  381. },
  382. ],
  383. });
  384. if (res && (res?.response.status !== 200 || res?.data?.retcode !== 0)) {
  385. // cancel loading
  386. setValue(message.content);
  387. console.info('removeLatestMessage111');
  388. removeLatestMessage();
  389. } else {
  390. if (id) {
  391. console.info('111');
  392. // new conversation
  393. handleClickConversation(id);
  394. } else {
  395. console.info('222');
  396. // fetchConversation(conversationId);
  397. }
  398. }
  399. },
  400. [
  401. conversation?.message,
  402. conversationId,
  403. handleClickConversation,
  404. removeLatestMessage,
  405. setValue,
  406. send,
  407. ],
  408. );
  409. const handleSendMessage = useCallback(
  410. async (message: Message, documentIds: string[]) => {
  411. if (conversationId !== '') {
  412. sendMessage(message, documentIds);
  413. } else {
  414. const data = await setConversation(message.content);
  415. if (data.retcode === 0) {
  416. const id = data.data.id;
  417. sendMessage(message, documentIds, id);
  418. }
  419. }
  420. },
  421. [conversationId, setConversation, sendMessage],
  422. );
  423. useEffect(() => {
  424. // #1289
  425. if (answer.answer && answer?.conversationId === conversationId) {
  426. addNewestAnswer(answer);
  427. }
  428. }, [answer, addNewestAnswer, conversationId]);
  429. useEffect(() => {
  430. // #1289 switch to another conversion window when the last conversion answer doesn't finish.
  431. if (conversationId) {
  432. setDone(true);
  433. }
  434. }, [setDone, conversationId]);
  435. const handlePressEnter = useCallback(
  436. (documentIds: string[]) => {
  437. if (trim(value) === '') return;
  438. const id = uuid();
  439. addNewestConversation({
  440. content: value,
  441. doc_ids: documentIds,
  442. id,
  443. role: MessageType.User,
  444. });
  445. if (done) {
  446. setValue('');
  447. handleSendMessage(
  448. { id, content: value.trim(), role: MessageType.User },
  449. documentIds,
  450. );
  451. }
  452. },
  453. [addNewestConversation, handleSendMessage, done, setValue, value],
  454. );
  455. return {
  456. handlePressEnter,
  457. handleInputChange,
  458. value,
  459. setValue,
  460. loading: !done,
  461. };
  462. };
  463. export const useGetFileIcon = () => {
  464. const getFileIcon = (filename: string) => {
  465. const ext: string = getFileExtension(filename);
  466. const iconPath = fileIconMap[ext as keyof typeof fileIconMap];
  467. return `@/assets/svg/file-icon/${iconPath}`;
  468. };
  469. return getFileIcon;
  470. };
  471. export const useDeleteConversation = () => {
  472. const { handleClickConversation } = useClickConversationCard();
  473. const showDeleteConfirm = useShowDeleteConfirm();
  474. const { removeConversation } = useRemoveNextConversation();
  475. const deleteConversation = (conversationIds: Array<string>) => async () => {
  476. const ret = await removeConversation(conversationIds);
  477. if (ret === 0) {
  478. handleClickConversation('');
  479. }
  480. return ret;
  481. };
  482. const onRemoveConversation = (conversationIds: Array<string>) => {
  483. showDeleteConfirm({ onOk: deleteConversation(conversationIds) });
  484. };
  485. return { onRemoveConversation };
  486. };
  487. export const useRenameConversation = () => {
  488. const [conversation, setConversation] = useState<IClientConversation>(
  489. {} as IClientConversation,
  490. );
  491. const { fetchConversation } = useFetchManualConversation();
  492. const {
  493. visible: conversationRenameVisible,
  494. hideModal: hideConversationRenameModal,
  495. showModal: showConversationRenameModal,
  496. } = useSetModalState();
  497. const { updateConversation, loading } = useUpdateNextConversation();
  498. const onConversationRenameOk = useCallback(
  499. async (name: string) => {
  500. const ret = await updateConversation({
  501. ...conversation,
  502. conversation_id: conversation.id,
  503. name,
  504. });
  505. if (ret.retcode === 0) {
  506. hideConversationRenameModal();
  507. }
  508. },
  509. [updateConversation, conversation, hideConversationRenameModal],
  510. );
  511. const handleShowConversationRenameModal = useCallback(
  512. async (conversationId: string) => {
  513. const ret = await fetchConversation(conversationId);
  514. if (ret.retcode === 0) {
  515. setConversation(ret.data);
  516. }
  517. showConversationRenameModal();
  518. },
  519. [showConversationRenameModal, fetchConversation],
  520. );
  521. return {
  522. conversationRenameLoading: loading,
  523. initialConversationName: conversation.name,
  524. onConversationRenameOk,
  525. conversationRenameVisible,
  526. hideConversationRenameModal,
  527. showConversationRenameModal: handleShowConversationRenameModal,
  528. };
  529. };
  530. export const useClickDrawer = () => {
  531. const { visible, showModal, hideModal } = useSetModalState();
  532. const [selectedChunk, setSelectedChunk] = useState<IChunk>({} as IChunk);
  533. const [documentId, setDocumentId] = useState<string>('');
  534. const clickDocumentButton = useCallback(
  535. (documentId: string, chunk: IChunk) => {
  536. showModal();
  537. setSelectedChunk(chunk);
  538. setDocumentId(documentId);
  539. },
  540. [showModal],
  541. );
  542. return {
  543. clickDocumentButton,
  544. visible,
  545. showModal,
  546. hideModal,
  547. selectedChunk,
  548. documentId,
  549. };
  550. };
  551. export const useGetSendButtonDisabled = () => {
  552. const { dialogId, conversationId } = useGetChatSearchParams();
  553. return dialogId === '' && conversationId === '';
  554. };
  555. export const useSendButtonDisabled = (value: string) => {
  556. return trim(value) === '';
  557. };
  558. export const useCreateConversationBeforeUploadDocument = () => {
  559. const { setConversation } = useSetConversation();
  560. const { dialogId } = useGetChatSearchParams();
  561. const { handleClickConversation } = useClickConversationCard();
  562. const createConversationBeforeUploadDocument = useCallback(
  563. async (message: string) => {
  564. const data = await setConversation(message);
  565. if (data.retcode === 0) {
  566. const id = data.data.id;
  567. handleClickConversation(id);
  568. }
  569. return data;
  570. },
  571. [setConversation, handleClickConversation],
  572. );
  573. return {
  574. createConversationBeforeUploadDocument,
  575. dialogId,
  576. };
  577. };
  578. //#endregion