|
import { Authorization } from '@/constants/authorization'; |
|
import { MessageType } from '@/constants/chat'; |
|
import { LanguageTranslationMap } from '@/constants/common'; |
|
import { ResponseType } from '@/interfaces/database/base'; |
|
import { IAnswer, Message } from '@/interfaces/database/chat'; |
|
import { IKnowledgeFile } from '@/interfaces/database/knowledge'; |
|
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 { omit } from 'lodash'; |
|
import { |
|
ChangeEventHandler, |
|
useCallback, |
|
useEffect, |
|
useMemo, |
|
useRef, |
|
useState, |
|
} from 'react'; |
|
import { useTranslation } from 'react-i18next'; |
|
import { v4 as uuid } from 'uuid'; |
|
import { useTranslate } from './common-hooks'; |
|
import { useSetPaginationParams } from './route-hook'; |
|
import { useFetchTenantInfo, useSaveSetting } from './user-setting-hooks'; |
|
|
|
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 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 timer = useRef<any>(); |
|
|
|
const resetAnswer = useCallback(() => { |
|
if (timer.current) { |
|
clearTimeout(timer.current); |
|
} |
|
timer.current = setTimeout(() => { |
|
setAnswer({} as IAnswer); |
|
clearTimeout(timer.current); |
|
}, 1000); |
|
}, []); |
|
|
|
const send = useCallback( |
|
async ( |
|
body: any, |
|
controller?: AbortController, |
|
): 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), |
|
signal: controller?.signal, |
|
}); |
|
|
|
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'); |
|
resetAnswer(); |
|
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); |
|
resetAnswer(); |
|
return { data: await res, response }; |
|
} catch (e) { |
|
setDone(true); |
|
resetAnswer(); |
|
|
|
console.warn(e); |
|
} |
|
}, |
|
[url, resetAnswer], |
|
); |
|
|
|
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?.code !== 0) { |
|
message.error(res?.message); |
|
} |
|
} catch (error) { |
|
console.warn('🚀 ~ error:', error); |
|
} |
|
return response; |
|
}, |
|
[url], |
|
); |
|
|
|
return { read }; |
|
}; |
|
|
|
|
|
|
|
export const useScrollToBottom = (messages?: unknown) => { |
|
const ref = useRef<HTMLDivElement>(null); |
|
|
|
const scrollToBottom = useCallback(() => { |
|
if (messages) { |
|
ref.current?.scrollIntoView({ behavior: 'instant' }); |
|
} |
|
}, [messages]); |
|
|
|
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 }), |
|
}, |
|
]; |
|
}); |
|
}, |
|
[], |
|
); |
|
|
|
|
|
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, |
|
...omit(answer, 'reference'), |
|
}, |
|
]; |
|
}); |
|
}, []); |
|
|
|
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 }; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 handleChange = useCallback( |
|
(value: string) => { |
|
if (value in ChunkTokenNumMap) { |
|
form.setFieldValue( |
|
['parser_config', 'chunk_token_num'], |
|
ChunkTokenNumMap[value as keyof typeof ChunkTokenNumMap], |
|
); |
|
} |
|
}, |
|
[form], |
|
); |
|
|
|
return handleChange; |
|
}; |
|
|
|
|
|
export const useResetFormOnCloseModal = ({ |
|
form, |
|
visible, |
|
}: { |
|
form: FormInstance; |
|
visible?: boolean; |
|
}) => { |
|
const prevOpenRef = useRef<boolean>(); |
|
useEffect(() => { |
|
prevOpenRef.current = visible; |
|
}, [visible]); |
|
const prevOpen = prevOpenRef.current; |
|
|
|
useEffect(() => { |
|
if (!visible && prevOpen) { |
|
form.resetFields(); |
|
} |
|
}, [form, prevOpen, visible]); |
|
}; |
|
|