balibabu
feat: Click on the relevant question tag to continue searching for answers #2247 (#2320)
e68f488
| import { Authorization } from '@/constants/authorization'; | |
| import { MessageType } from '@/constants/chat'; | |
| import { LanguageTranslationMap } from '@/constants/common'; | |
| import { Pagination } from '@/interfaces/common'; | |
| import { ResponseType } from '@/interfaces/database/base'; | |
| import { IAnswer, Message } from '@/interfaces/database/chat'; | |
| import { IKnowledgeFile } from '@/interfaces/database/knowledge'; | |
| import { IChangeParserConfigRequestBody } from '@/interfaces/request/document'; | |
| import { IClientConversation, IMessage } from '@/pages/chat/interface'; | |
| import api from '@/utils/api'; | |
| import { getAuthorization } from '@/utils/authorization-util'; | |
| import { buildMessageUuid, getMessagePureId } from '@/utils/chat'; | |
| import { PaginationProps, message } from 'antd'; | |
| import { FormInstance } from 'antd/lib'; | |
| import axios from 'axios'; | |
| import { EventSourceParserStream } from 'eventsource-parser/stream'; | |
| import { | |
| ChangeEventHandler, | |
| useCallback, | |
| useEffect, | |
| useMemo, | |
| useRef, | |
| useState, | |
| } from 'react'; | |
| import { useTranslation } from 'react-i18next'; | |
| import { useDispatch } from 'umi'; | |
| import { v4 as uuid } from 'uuid'; | |
| import { useSetModalState, useTranslate } from './common-hooks'; | |
| import { useSetDocumentParser } from './document-hooks'; | |
| import { useSetPaginationParams } from './route-hook'; | |
| import { useOneNamespaceEffectsLoading } from './store-hooks'; | |
| import { useFetchTenantInfo, useSaveSetting } from './user-setting-hooks'; | |
| export const useChangeDocumentParser = (documentId: string) => { | |
| const setDocumentParser = useSetDocumentParser(); | |
| const { | |
| visible: changeParserVisible, | |
| hideModal: hideChangeParserModal, | |
| showModal: showChangeParserModal, | |
| } = useSetModalState(); | |
| const loading = useOneNamespaceEffectsLoading('kFModel', [ | |
| 'document_change_parser', | |
| ]); | |
| const onChangeParserOk = useCallback( | |
| async (parserId: string, parserConfig: IChangeParserConfigRequestBody) => { | |
| const ret = await setDocumentParser(parserId, documentId, parserConfig); | |
| if (ret === 0) { | |
| hideChangeParserModal(); | |
| } | |
| }, | |
| [hideChangeParserModal, setDocumentParser, documentId], | |
| ); | |
| return { | |
| changeParserLoading: loading, | |
| onChangeParserOk, | |
| changeParserVisible, | |
| hideChangeParserModal, | |
| showChangeParserModal, | |
| }; | |
| }; | |
| export const useSetSelectedRecord = <T = IKnowledgeFile>() => { | |
| const [currentRecord, setCurrentRecord] = useState<T>({} as T); | |
| const setRecord = (record: T) => { | |
| setCurrentRecord(record); | |
| }; | |
| return { currentRecord, setRecord }; | |
| }; | |
| export const useHandleSearchChange = () => { | |
| const [searchString, setSearchString] = useState(''); | |
| const handleInputChange = useCallback( | |
| (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { | |
| const value = e.target.value; | |
| setSearchString(value); | |
| }, | |
| [], | |
| ); | |
| return { handleInputChange, searchString }; | |
| }; | |
| export const useChangeLanguage = () => { | |
| const { i18n } = useTranslation(); | |
| const { saveSetting } = useSaveSetting(); | |
| const changeLanguage = (lng: string) => { | |
| i18n.changeLanguage( | |
| LanguageTranslationMap[lng as keyof typeof LanguageTranslationMap], | |
| ); | |
| saveSetting({ language: lng }); | |
| }; | |
| return changeLanguage; | |
| }; | |
| export const useGetPaginationWithRouter = () => { | |
| const { t } = useTranslate('common'); | |
| const { | |
| setPaginationParams, | |
| page, | |
| size: pageSize, | |
| } = useSetPaginationParams(); | |
| const onPageChange: PaginationProps['onChange'] = useCallback( | |
| (pageNumber: number, pageSize: number) => { | |
| setPaginationParams(pageNumber, pageSize); | |
| }, | |
| [setPaginationParams], | |
| ); | |
| const setCurrentPagination = useCallback( | |
| (pagination: { page: number; pageSize?: number }) => { | |
| setPaginationParams(pagination.page, pagination.pageSize); | |
| }, | |
| [setPaginationParams], | |
| ); | |
| const pagination: PaginationProps = useMemo(() => { | |
| return { | |
| showQuickJumper: true, | |
| total: 0, | |
| showSizeChanger: true, | |
| current: page, | |
| pageSize: pageSize, | |
| pageSizeOptions: [1, 2, 10, 20, 50, 100], | |
| onChange: onPageChange, | |
| showTotal: (total) => `${t('total')} ${total}`, | |
| }; | |
| }, [t, onPageChange, page, pageSize]); | |
| return { | |
| pagination, | |
| setPagination: setCurrentPagination, | |
| }; | |
| }; | |
| export const useGetPagination = () => { | |
| const [pagination, setPagination] = useState({ page: 1, pageSize: 10 }); | |
| const { t } = useTranslate('common'); | |
| const onPageChange: PaginationProps['onChange'] = useCallback( | |
| (pageNumber: number, pageSize: number) => { | |
| setPagination({ page: pageNumber, pageSize }); | |
| }, | |
| [], | |
| ); | |
| const currentPagination: PaginationProps = useMemo(() => { | |
| return { | |
| showQuickJumper: true, | |
| total: 0, | |
| showSizeChanger: true, | |
| current: pagination.page, | |
| pageSize: pagination.pageSize, | |
| pageSizeOptions: [1, 2, 10, 20, 50, 100], | |
| onChange: onPageChange, | |
| showTotal: (total) => `${t('total')} ${total}`, | |
| }; | |
| }, [t, onPageChange, pagination]); | |
| return { | |
| pagination: currentPagination, | |
| }; | |
| }; | |
| export const useSetPagination = (namespace: string) => { | |
| const dispatch = useDispatch(); | |
| const setPagination = useCallback( | |
| (pageNumber = 1, pageSize?: number) => { | |
| const pagination: Pagination = { | |
| current: pageNumber, | |
| } as Pagination; | |
| if (pageSize) { | |
| pagination.pageSize = pageSize; | |
| } | |
| dispatch({ | |
| type: `${namespace}/setPagination`, | |
| payload: pagination, | |
| }); | |
| }, | |
| [dispatch, namespace], | |
| ); | |
| return setPagination; | |
| }; | |
| export interface AppConf { | |
| appName: string; | |
| } | |
| export const useFetchAppConf = () => { | |
| const [appConf, setAppConf] = useState<AppConf>({} as AppConf); | |
| const fetchAppConf = useCallback(async () => { | |
| const ret = await axios.get('/conf.json'); | |
| setAppConf(ret.data); | |
| }, []); | |
| useEffect(() => { | |
| fetchAppConf(); | |
| }, [fetchAppConf]); | |
| return appConf; | |
| }; | |
| export const useSendMessageWithSse = ( | |
| url: string = api.completeConversation, | |
| ) => { | |
| const [answer, setAnswer] = useState<IAnswer>({} as IAnswer); | |
| const [done, setDone] = useState(true); | |
| const resetAnswer = useCallback(() => { | |
| setAnswer({} as IAnswer); | |
| }, []); | |
| const send = useCallback( | |
| async ( | |
| body: any, | |
| ): Promise<{ response: Response; data: ResponseType } | undefined> => { | |
| try { | |
| setDone(false); | |
| const response = await fetch(url, { | |
| method: 'POST', | |
| headers: { | |
| [Authorization]: getAuthorization(), | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(body), | |
| }); | |
| const res = response.clone().json(); | |
| const reader = response?.body | |
| ?.pipeThrough(new TextDecoderStream()) | |
| .pipeThrough(new EventSourceParserStream()) | |
| .getReader(); | |
| while (true) { | |
| const x = await reader?.read(); | |
| if (x) { | |
| const { done, value } = x; | |
| if (done) { | |
| console.info('done'); | |
| break; | |
| } | |
| try { | |
| const val = JSON.parse(value?.data || ''); | |
| const d = val?.data; | |
| if (typeof d !== 'boolean') { | |
| console.info('data:', d); | |
| setAnswer({ | |
| ...d, | |
| conversationId: body?.conversation_id, | |
| }); | |
| } | |
| } catch (e) { | |
| console.warn(e); | |
| } | |
| } | |
| } | |
| console.info('done?'); | |
| setDone(true); | |
| return { data: await res, response }; | |
| } catch (e) { | |
| setDone(true); | |
| console.warn(e); | |
| } | |
| }, | |
| [url], | |
| ); | |
| return { send, answer, done, setDone, resetAnswer }; | |
| }; | |
| export const useSpeechWithSse = (url: string = api.tts) => { | |
| const read = useCallback( | |
| async (body: any) => { | |
| const response = await fetch(url, { | |
| method: 'POST', | |
| headers: { | |
| [Authorization]: getAuthorization(), | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(body), | |
| }); | |
| try { | |
| const res = await response.clone().json(); | |
| if (res?.retcode !== 0) { | |
| message.error(res?.retmsg); | |
| } | |
| } catch (error) { | |
| console.warn('🚀 ~ error:', error); | |
| } | |
| return response; | |
| }, | |
| [url], | |
| ); | |
| return { read }; | |
| }; | |
| //#region chat hooks | |
| export const useScrollToBottom = (messages?: unknown) => { | |
| const ref = useRef<HTMLDivElement>(null); | |
| const scrollToBottom = useCallback(() => { | |
| if (messages) { | |
| ref.current?.scrollIntoView({ behavior: 'instant' }); | |
| } | |
| }, [messages]); // If the message changes, scroll to the bottom | |
| useEffect(() => { | |
| scrollToBottom(); | |
| }, [scrollToBottom]); | |
| return ref; | |
| }; | |
| export const useHandleMessageInputChange = () => { | |
| const [value, setValue] = useState(''); | |
| const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => { | |
| const value = e.target.value; | |
| const nextValue = value.replaceAll('\\n', '\n').replaceAll('\\t', '\t'); | |
| setValue(nextValue); | |
| }; | |
| return { | |
| handleInputChange, | |
| value, | |
| setValue, | |
| }; | |
| }; | |
| export const useSelectDerivedMessages = () => { | |
| const [derivedMessages, setDerivedMessages] = useState<IMessage[]>([]); | |
| const ref = useScrollToBottom(derivedMessages); | |
| const addNewestQuestion = useCallback( | |
| (message: Message, answer: string = '') => { | |
| setDerivedMessages((pre) => { | |
| return [ | |
| ...pre, | |
| { | |
| ...message, | |
| id: buildMessageUuid(message), | |
| }, | |
| { | |
| role: MessageType.Assistant, | |
| content: answer, | |
| id: buildMessageUuid({ ...message, role: MessageType.Assistant }), | |
| }, | |
| ]; | |
| }); | |
| }, | |
| [], | |
| ); | |
| // Add the streaming message to the last item in the message list | |
| const addNewestAnswer = useCallback((answer: IAnswer) => { | |
| setDerivedMessages((pre) => { | |
| return [ | |
| ...(pre?.slice(0, -1) ?? []), | |
| { | |
| role: MessageType.Assistant, | |
| content: answer.answer, | |
| reference: answer.reference, | |
| id: buildMessageUuid({ | |
| id: answer.id, | |
| role: MessageType.Assistant, | |
| }), | |
| prompt: answer.prompt, | |
| audio_binary: answer.audio_binary, | |
| }, | |
| ]; | |
| }); | |
| }, []); | |
| const removeLatestMessage = useCallback(() => { | |
| setDerivedMessages((pre) => { | |
| const nextMessages = pre?.slice(0, -2) ?? []; | |
| return nextMessages; | |
| }); | |
| }, []); | |
| const removeMessageById = useCallback( | |
| (messageId: string) => { | |
| setDerivedMessages((pre) => { | |
| const nextMessages = | |
| pre?.filter( | |
| (x) => getMessagePureId(x.id) !== getMessagePureId(messageId), | |
| ) ?? []; | |
| return nextMessages; | |
| }); | |
| }, | |
| [setDerivedMessages], | |
| ); | |
| const removeMessagesAfterCurrentMessage = useCallback( | |
| (messageId: string) => { | |
| setDerivedMessages((pre) => { | |
| const index = pre.findIndex((x) => x.id === messageId); | |
| if (index !== -1) { | |
| let nextMessages = pre.slice(0, index + 2) ?? []; | |
| const latestMessage = nextMessages.at(-1); | |
| nextMessages = latestMessage | |
| ? [ | |
| ...nextMessages.slice(0, -1), | |
| { | |
| ...latestMessage, | |
| content: '', | |
| reference: undefined, | |
| prompt: undefined, | |
| }, | |
| ] | |
| : nextMessages; | |
| return nextMessages; | |
| } | |
| return pre; | |
| }); | |
| }, | |
| [setDerivedMessages], | |
| ); | |
| return { | |
| ref, | |
| derivedMessages, | |
| setDerivedMessages, | |
| addNewestQuestion, | |
| addNewestAnswer, | |
| removeLatestMessage, | |
| removeMessageById, | |
| removeMessagesAfterCurrentMessage, | |
| }; | |
| }; | |
| export interface IRemoveMessageById { | |
| removeMessageById(messageId: string): void; | |
| } | |
| export const useRemoveMessageById = ( | |
| setCurrentConversation: ( | |
| callback: (state: IClientConversation) => IClientConversation, | |
| ) => void, | |
| ) => { | |
| const removeMessageById = useCallback( | |
| (messageId: string) => { | |
| setCurrentConversation((pre) => { | |
| const nextMessages = | |
| pre.message?.filter( | |
| (x) => getMessagePureId(x.id) !== getMessagePureId(messageId), | |
| ) ?? []; | |
| return { | |
| ...pre, | |
| message: nextMessages, | |
| }; | |
| }); | |
| }, | |
| [setCurrentConversation], | |
| ); | |
| return { removeMessageById }; | |
| }; | |
| export const useRemoveMessagesAfterCurrentMessage = ( | |
| setCurrentConversation: ( | |
| callback: (state: IClientConversation) => IClientConversation, | |
| ) => void, | |
| ) => { | |
| const removeMessagesAfterCurrentMessage = useCallback( | |
| (messageId: string) => { | |
| setCurrentConversation((pre) => { | |
| const index = pre.message?.findIndex((x) => x.id === messageId); | |
| if (index !== -1) { | |
| let nextMessages = pre.message?.slice(0, index + 2) ?? []; | |
| const latestMessage = nextMessages.at(-1); | |
| nextMessages = latestMessage | |
| ? [ | |
| ...nextMessages.slice(0, -1), | |
| { | |
| ...latestMessage, | |
| content: '', | |
| reference: undefined, | |
| prompt: undefined, | |
| }, | |
| ] | |
| : nextMessages; | |
| return { | |
| ...pre, | |
| message: nextMessages, | |
| }; | |
| } | |
| return pre; | |
| }); | |
| }, | |
| [setCurrentConversation], | |
| ); | |
| return { removeMessagesAfterCurrentMessage }; | |
| }; | |
| export interface IRegenerateMessage { | |
| regenerateMessage?: (message: Message) => void; | |
| } | |
| export const useRegenerateMessage = ({ | |
| removeMessagesAfterCurrentMessage, | |
| sendMessage, | |
| messages, | |
| }: { | |
| removeMessagesAfterCurrentMessage(messageId: string): void; | |
| sendMessage({ | |
| message, | |
| }: { | |
| message: Message; | |
| messages?: Message[]; | |
| }): void | Promise<any>; | |
| messages: Message[]; | |
| }) => { | |
| const regenerateMessage = useCallback( | |
| async (message: Message) => { | |
| if (message.id) { | |
| removeMessagesAfterCurrentMessage(message.id); | |
| const index = messages.findIndex((x) => x.id === message.id); | |
| let nextMessages; | |
| if (index !== -1) { | |
| nextMessages = messages.slice(0, index); | |
| } | |
| sendMessage({ | |
| message: { ...message, id: uuid() }, | |
| messages: nextMessages, | |
| }); | |
| } | |
| }, | |
| [removeMessagesAfterCurrentMessage, sendMessage, messages], | |
| ); | |
| return { regenerateMessage }; | |
| }; | |
| // #endregion | |
| /** | |
| * | |
| * @param defaultId | |
| * used to switch between different items, similar to radio | |
| * @returns | |
| */ | |
| export const useSelectItem = (defaultId?: string) => { | |
| const [selectedId, setSelectedId] = useState(''); | |
| const handleItemClick = useCallback( | |
| (id: string) => () => { | |
| setSelectedId(id); | |
| }, | |
| [], | |
| ); | |
| useEffect(() => { | |
| if (defaultId) { | |
| setSelectedId(defaultId); | |
| } | |
| }, [defaultId]); | |
| return { selectedId, handleItemClick }; | |
| }; | |
| export const useFetchModelId = () => { | |
| const { data: tenantInfo } = useFetchTenantInfo(); | |
| return tenantInfo?.llm_id ?? ''; | |
| }; | |
| const ChunkTokenNumMap = { | |
| naive: 128, | |
| knowledge_graph: 8192, | |
| }; | |
| export const useHandleChunkMethodSelectChange = (form: FormInstance) => { | |
| // const form = Form.useFormInstance(); | |
| const handleChange = useCallback( | |
| (value: string) => { | |
| if (value in ChunkTokenNumMap) { | |
| form.setFieldValue( | |
| ['parser_config', 'chunk_token_num'], | |
| ChunkTokenNumMap[value as keyof typeof ChunkTokenNumMap], | |
| ); | |
| } | |
| }, | |
| [form], | |
| ); | |
| return handleChange; | |
| }; | |