Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

logic-hooks.ts 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. import { Authorization } from '@/constants/authorization';
  2. import { MessageType } from '@/constants/chat';
  3. import { LanguageTranslationMap } from '@/constants/common';
  4. import { Pagination } from '@/interfaces/common';
  5. import { ResponseType } from '@/interfaces/database/base';
  6. import { IAnswer, Message } from '@/interfaces/database/chat';
  7. import { IKnowledgeFile } from '@/interfaces/database/knowledge';
  8. import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
  9. import { IClientConversation, IMessage } from '@/pages/chat/interface';
  10. import api from '@/utils/api';
  11. import { getAuthorization } from '@/utils/authorization-util';
  12. import { buildMessageUuid, getMessagePureId } from '@/utils/chat';
  13. import { PaginationProps } from 'antd';
  14. import { FormInstance } from 'antd/lib';
  15. import axios from 'axios';
  16. import { EventSourceParserStream } from 'eventsource-parser/stream';
  17. import {
  18. ChangeEventHandler,
  19. useCallback,
  20. useEffect,
  21. useMemo,
  22. useRef,
  23. useState,
  24. } from 'react';
  25. import { useTranslation } from 'react-i18next';
  26. import { useDispatch } from 'umi';
  27. import { v4 as uuid } from 'uuid';
  28. import { useSetModalState, useTranslate } from './common-hooks';
  29. import { useSetDocumentParser } from './document-hooks';
  30. import { useSetPaginationParams } from './route-hook';
  31. import { useOneNamespaceEffectsLoading } from './store-hooks';
  32. import { useFetchTenantInfo, useSaveSetting } from './user-setting-hooks';
  33. export const useChangeDocumentParser = (documentId: string) => {
  34. const setDocumentParser = useSetDocumentParser();
  35. const {
  36. visible: changeParserVisible,
  37. hideModal: hideChangeParserModal,
  38. showModal: showChangeParserModal,
  39. } = useSetModalState();
  40. const loading = useOneNamespaceEffectsLoading('kFModel', [
  41. 'document_change_parser',
  42. ]);
  43. const onChangeParserOk = useCallback(
  44. async (parserId: string, parserConfig: IChangeParserConfigRequestBody) => {
  45. const ret = await setDocumentParser(parserId, documentId, parserConfig);
  46. if (ret === 0) {
  47. hideChangeParserModal();
  48. }
  49. },
  50. [hideChangeParserModal, setDocumentParser, documentId],
  51. );
  52. return {
  53. changeParserLoading: loading,
  54. onChangeParserOk,
  55. changeParserVisible,
  56. hideChangeParserModal,
  57. showChangeParserModal,
  58. };
  59. };
  60. export const useSetSelectedRecord = <T = IKnowledgeFile>() => {
  61. const [currentRecord, setCurrentRecord] = useState<T>({} as T);
  62. const setRecord = (record: T) => {
  63. setCurrentRecord(record);
  64. };
  65. return { currentRecord, setRecord };
  66. };
  67. export const useHandleSearchChange = () => {
  68. const [searchString, setSearchString] = useState('');
  69. const handleInputChange = useCallback(
  70. (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
  71. const value = e.target.value;
  72. setSearchString(value);
  73. },
  74. [],
  75. );
  76. return { handleInputChange, searchString };
  77. };
  78. export const useChangeLanguage = () => {
  79. const { i18n } = useTranslation();
  80. const { saveSetting } = useSaveSetting();
  81. const changeLanguage = (lng: string) => {
  82. i18n.changeLanguage(
  83. LanguageTranslationMap[lng as keyof typeof LanguageTranslationMap],
  84. );
  85. saveSetting({ language: lng });
  86. };
  87. return changeLanguage;
  88. };
  89. export const useGetPaginationWithRouter = () => {
  90. const { t } = useTranslate('common');
  91. const {
  92. setPaginationParams,
  93. page,
  94. size: pageSize,
  95. } = useSetPaginationParams();
  96. const onPageChange: PaginationProps['onChange'] = useCallback(
  97. (pageNumber: number, pageSize: number) => {
  98. setPaginationParams(pageNumber, pageSize);
  99. },
  100. [setPaginationParams],
  101. );
  102. const setCurrentPagination = useCallback(
  103. (pagination: { page: number; pageSize?: number }) => {
  104. setPaginationParams(pagination.page, pagination.pageSize);
  105. },
  106. [setPaginationParams],
  107. );
  108. const pagination: PaginationProps = useMemo(() => {
  109. return {
  110. showQuickJumper: true,
  111. total: 0,
  112. showSizeChanger: true,
  113. current: page,
  114. pageSize: pageSize,
  115. pageSizeOptions: [1, 2, 10, 20, 50, 100],
  116. onChange: onPageChange,
  117. showTotal: (total) => `${t('total')} ${total}`,
  118. };
  119. }, [t, onPageChange, page, pageSize]);
  120. return {
  121. pagination,
  122. setPagination: setCurrentPagination,
  123. };
  124. };
  125. export const useGetPagination = () => {
  126. const [pagination, setPagination] = useState({ page: 1, pageSize: 10 });
  127. const { t } = useTranslate('common');
  128. const onPageChange: PaginationProps['onChange'] = useCallback(
  129. (pageNumber: number, pageSize: number) => {
  130. setPagination({ page: pageNumber, pageSize });
  131. },
  132. [],
  133. );
  134. const currentPagination: PaginationProps = useMemo(() => {
  135. return {
  136. showQuickJumper: true,
  137. total: 0,
  138. showSizeChanger: true,
  139. current: pagination.page,
  140. pageSize: pagination.pageSize,
  141. pageSizeOptions: [1, 2, 10, 20, 50, 100],
  142. onChange: onPageChange,
  143. showTotal: (total) => `${t('total')} ${total}`,
  144. };
  145. }, [t, onPageChange, pagination]);
  146. return {
  147. pagination: currentPagination,
  148. };
  149. };
  150. export const useSetPagination = (namespace: string) => {
  151. const dispatch = useDispatch();
  152. const setPagination = useCallback(
  153. (pageNumber = 1, pageSize?: number) => {
  154. const pagination: Pagination = {
  155. current: pageNumber,
  156. } as Pagination;
  157. if (pageSize) {
  158. pagination.pageSize = pageSize;
  159. }
  160. dispatch({
  161. type: `${namespace}/setPagination`,
  162. payload: pagination,
  163. });
  164. },
  165. [dispatch, namespace],
  166. );
  167. return setPagination;
  168. };
  169. export interface AppConf {
  170. appName: string;
  171. }
  172. export const useFetchAppConf = () => {
  173. const [appConf, setAppConf] = useState<AppConf>({} as AppConf);
  174. const fetchAppConf = useCallback(async () => {
  175. const ret = await axios.get('/conf.json');
  176. setAppConf(ret.data);
  177. }, []);
  178. useEffect(() => {
  179. fetchAppConf();
  180. }, [fetchAppConf]);
  181. return appConf;
  182. };
  183. export const useSendMessageWithSse = (
  184. url: string = api.completeConversation,
  185. ) => {
  186. const [answer, setAnswer] = useState<IAnswer>({} as IAnswer);
  187. const [done, setDone] = useState(true);
  188. const send = useCallback(
  189. async (
  190. body: any,
  191. ): Promise<{ response: Response; data: ResponseType } | undefined> => {
  192. try {
  193. setDone(false);
  194. const response = await fetch(url, {
  195. method: 'POST',
  196. headers: {
  197. [Authorization]: getAuthorization(),
  198. 'Content-Type': 'application/json',
  199. },
  200. body: JSON.stringify(body),
  201. });
  202. const res = response.clone().json();
  203. const reader = response?.body
  204. ?.pipeThrough(new TextDecoderStream())
  205. .pipeThrough(new EventSourceParserStream())
  206. .getReader();
  207. while (true) {
  208. const x = await reader?.read();
  209. if (x) {
  210. const { done, value } = x;
  211. try {
  212. const val = JSON.parse(value?.data || '');
  213. const d = val?.data;
  214. if (typeof d !== 'boolean') {
  215. // console.info('data:', d);
  216. setAnswer({
  217. ...d,
  218. conversationId: body?.conversation_id,
  219. });
  220. }
  221. } catch (e) {
  222. console.warn(e);
  223. }
  224. if (done) {
  225. console.info('done');
  226. break;
  227. }
  228. }
  229. }
  230. console.info('done?');
  231. setDone(true);
  232. setAnswer({} as IAnswer);
  233. return { data: await res, response };
  234. } catch (e) {
  235. setDone(true);
  236. setAnswer({} as IAnswer);
  237. console.warn(e);
  238. }
  239. },
  240. [url],
  241. );
  242. return { send, answer, done, setDone };
  243. };
  244. //#region chat hooks
  245. export const useScrollToBottom = (messages?: unknown) => {
  246. const ref = useRef<HTMLDivElement>(null);
  247. const scrollToBottom = useCallback(() => {
  248. if (messages) {
  249. ref.current?.scrollIntoView({ behavior: 'instant' });
  250. }
  251. }, [messages]); // If the message changes, scroll to the bottom
  252. useEffect(() => {
  253. scrollToBottom();
  254. }, [scrollToBottom]);
  255. return ref;
  256. };
  257. export const useHandleMessageInputChange = () => {
  258. const [value, setValue] = useState('');
  259. const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
  260. const value = e.target.value;
  261. const nextValue = value.replaceAll('\\n', '\n').replaceAll('\\t', '\t');
  262. setValue(nextValue);
  263. };
  264. return {
  265. handleInputChange,
  266. value,
  267. setValue,
  268. };
  269. };
  270. export const useSelectDerivedMessages = () => {
  271. const [derivedMessages, setDerivedMessages] = useState<IMessage[]>([]);
  272. const ref = useScrollToBottom(derivedMessages);
  273. const addNewestQuestion = useCallback(
  274. (message: Message, answer: string = '') => {
  275. setDerivedMessages((pre) => {
  276. return [
  277. ...pre,
  278. {
  279. ...message,
  280. id: buildMessageUuid(message),
  281. },
  282. {
  283. role: MessageType.Assistant,
  284. content: answer,
  285. id: buildMessageUuid({ ...message, role: MessageType.Assistant }),
  286. },
  287. ];
  288. });
  289. },
  290. [],
  291. );
  292. // Add the streaming message to the last item in the message list
  293. const addNewestAnswer = useCallback((answer: IAnswer) => {
  294. setDerivedMessages((pre) => {
  295. return [
  296. ...(pre?.slice(0, -1) ?? []),
  297. {
  298. role: MessageType.Assistant,
  299. content: answer.answer,
  300. reference: answer.reference,
  301. id: buildMessageUuid({
  302. id: answer.id,
  303. role: MessageType.Assistant,
  304. }),
  305. prompt: answer.prompt,
  306. },
  307. ];
  308. });
  309. }, []);
  310. const removeLatestMessage = useCallback(() => {
  311. setDerivedMessages((pre) => {
  312. const nextMessages = pre?.slice(0, -2) ?? [];
  313. return nextMessages;
  314. });
  315. }, []);
  316. const removeMessageById = useCallback(
  317. (messageId: string) => {
  318. setDerivedMessages((pre) => {
  319. const nextMessages =
  320. pre?.filter(
  321. (x) => getMessagePureId(x.id) !== getMessagePureId(messageId),
  322. ) ?? [];
  323. return nextMessages;
  324. });
  325. },
  326. [setDerivedMessages],
  327. );
  328. const removeMessagesAfterCurrentMessage = useCallback(
  329. (messageId: string) => {
  330. setDerivedMessages((pre) => {
  331. const index = pre.findIndex((x) => x.id === messageId);
  332. if (index !== -1) {
  333. let nextMessages = pre.slice(0, index + 2) ?? [];
  334. const latestMessage = nextMessages.at(-1);
  335. nextMessages = latestMessage
  336. ? [
  337. ...nextMessages.slice(0, -1),
  338. {
  339. ...latestMessage,
  340. content: '',
  341. reference: undefined,
  342. prompt: undefined,
  343. },
  344. ]
  345. : nextMessages;
  346. return nextMessages;
  347. }
  348. return pre;
  349. });
  350. },
  351. [setDerivedMessages],
  352. );
  353. return {
  354. ref,
  355. derivedMessages,
  356. setDerivedMessages,
  357. addNewestQuestion,
  358. addNewestAnswer,
  359. removeLatestMessage,
  360. removeMessageById,
  361. removeMessagesAfterCurrentMessage,
  362. };
  363. };
  364. export interface IRemoveMessageById {
  365. removeMessageById(messageId: string): void;
  366. }
  367. export const useRemoveMessageById = (
  368. setCurrentConversation: (
  369. callback: (state: IClientConversation) => IClientConversation,
  370. ) => void,
  371. ) => {
  372. const removeMessageById = useCallback(
  373. (messageId: string) => {
  374. setCurrentConversation((pre) => {
  375. const nextMessages =
  376. pre.message?.filter(
  377. (x) => getMessagePureId(x.id) !== getMessagePureId(messageId),
  378. ) ?? [];
  379. return {
  380. ...pre,
  381. message: nextMessages,
  382. };
  383. });
  384. },
  385. [setCurrentConversation],
  386. );
  387. return { removeMessageById };
  388. };
  389. export const useRemoveMessagesAfterCurrentMessage = (
  390. setCurrentConversation: (
  391. callback: (state: IClientConversation) => IClientConversation,
  392. ) => void,
  393. ) => {
  394. const removeMessagesAfterCurrentMessage = useCallback(
  395. (messageId: string) => {
  396. setCurrentConversation((pre) => {
  397. const index = pre.message?.findIndex((x) => x.id === messageId);
  398. if (index !== -1) {
  399. let nextMessages = pre.message?.slice(0, index + 2) ?? [];
  400. const latestMessage = nextMessages.at(-1);
  401. nextMessages = latestMessage
  402. ? [
  403. ...nextMessages.slice(0, -1),
  404. {
  405. ...latestMessage,
  406. content: '',
  407. reference: undefined,
  408. prompt: undefined,
  409. },
  410. ]
  411. : nextMessages;
  412. return {
  413. ...pre,
  414. message: nextMessages,
  415. };
  416. }
  417. return pre;
  418. });
  419. },
  420. [setCurrentConversation],
  421. );
  422. return { removeMessagesAfterCurrentMessage };
  423. };
  424. export interface IRegenerateMessage {
  425. regenerateMessage?: (message: Message) => void;
  426. }
  427. export const useRegenerateMessage = ({
  428. removeMessagesAfterCurrentMessage,
  429. sendMessage,
  430. messages,
  431. }: {
  432. removeMessagesAfterCurrentMessage(messageId: string): void;
  433. sendMessage({
  434. message,
  435. }: {
  436. message: Message;
  437. messages?: Message[];
  438. }): void | Promise<any>;
  439. messages: Message[];
  440. }) => {
  441. const regenerateMessage = useCallback(
  442. async (message: Message) => {
  443. if (message.id) {
  444. removeMessagesAfterCurrentMessage(message.id);
  445. const index = messages.findIndex((x) => x.id === message.id);
  446. let nextMessages;
  447. if (index !== -1) {
  448. nextMessages = messages.slice(0, index);
  449. }
  450. sendMessage({
  451. message: { ...message, id: uuid() },
  452. messages: nextMessages,
  453. });
  454. }
  455. },
  456. [removeMessagesAfterCurrentMessage, sendMessage, messages],
  457. );
  458. return { regenerateMessage };
  459. };
  460. // #endregion
  461. /**
  462. *
  463. * @param defaultId
  464. * used to switch between different items, similar to radio
  465. * @returns
  466. */
  467. export const useSelectItem = (defaultId?: string) => {
  468. const [selectedId, setSelectedId] = useState('');
  469. const handleItemClick = useCallback(
  470. (id: string) => () => {
  471. setSelectedId(id);
  472. },
  473. [],
  474. );
  475. useEffect(() => {
  476. if (defaultId) {
  477. setSelectedId(defaultId);
  478. }
  479. }, [defaultId]);
  480. return { selectedId, handleItemClick };
  481. };
  482. export const useFetchModelId = () => {
  483. const { data: tenantInfo } = useFetchTenantInfo();
  484. return tenantInfo?.llm_id ?? '';
  485. };
  486. const ChunkTokenNumMap = {
  487. naive: 128,
  488. knowledge_graph: 8192,
  489. };
  490. export const useHandleChunkMethodSelectChange = (form: FormInstance) => {
  491. // const form = Form.useFormInstance();
  492. const handleChange = useCallback(
  493. (value: string) => {
  494. if (value in ChunkTokenNumMap) {
  495. form.setFieldValue(
  496. ['parser_config', 'chunk_token_num'],
  497. ChunkTokenNumMap[value as keyof typeof ChunkTokenNumMap],
  498. );
  499. }
  500. },
  501. [form],
  502. );
  503. return handleChange;
  504. };