### What problem does this PR solve? fix: Fixed an issue where the first message would be displayed when sending the second message #2625 ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [ ] New Feature (non-breaking change which adds functionality) - [ ] Documentation Update - [ ] Refactoring - [ ] Performance Improvement - [ ] Other (please describe):tags/v0.12.0
| def set_conversation(): | def set_conversation(): | ||||
| req = request.json | req = request.json | ||||
| conv_id = req.get("conversation_id") | conv_id = req.get("conversation_id") | ||||
| if conv_id: | |||||
| is_new = req.get("is_new") | |||||
| del req["is_new"] | |||||
| if not is_new: | |||||
| del req["conversation_id"] | del req["conversation_id"] | ||||
| try: | try: | ||||
| if not ConversationService.update_by_id(conv_id, req): | if not ConversationService.update_by_id(conv_id, req): | ||||
| if not e: | if not e: | ||||
| return get_data_error_result(retmsg="Dialog not found") | return get_data_error_result(retmsg="Dialog not found") | ||||
| conv = { | conv = { | ||||
| "id": get_uuid(), | |||||
| "id": conv_id, | |||||
| "dialog_id": req["dialog_id"], | "dialog_id": req["dialog_id"], | ||||
| "name": req.get("name", "New conversation"), | "name": req.get("name", "New conversation"), | ||||
| "message": [{"role": "assistant", "content": dia.prompt_config["prologue"]}] | "message": [{"role": "assistant", "content": dia.prompt_config["prologue"]}] | 
| copy: ['src/conf.json'], | copy: ['src/conf.json'], | ||||
| proxy: { | proxy: { | ||||
| '/v1': { | '/v1': { | ||||
| target: 'http://127.0.0.1:9456/', | |||||
| target: 'http://127.0.0.1:9380/', | |||||
| changeOrigin: true, | changeOrigin: true, | ||||
| ws: true, | ws: true, | ||||
| logger: console, | logger: console, | 
| file, | file, | ||||
| }) => { | }) => { | ||||
| let nextConversationId: string = conversationId; | let nextConversationId: string = conversationId; | ||||
| if (createConversationBeforeUploadDocument && !conversationId) { | |||||
| if (createConversationBeforeUploadDocument) { | |||||
| const creatingRet = await createConversationBeforeUploadDocument( | const creatingRet = await createConversationBeforeUploadDocument( | ||||
| file.name, | file.name, | ||||
| ); | ); | ||||
| > | > | ||||
| <Button | <Button | ||||
| type={'text'} | type={'text'} | ||||
| disabled={disabled} | |||||
| icon={ | icon={ | ||||
| <SvgIcon name="paper-clip" width={18} height={22}></SvgIcon> | |||||
| <SvgIcon | |||||
| name="paper-clip" | |||||
| width={18} | |||||
| height={22} | |||||
| disabled={disabled} | |||||
| ></SvgIcon> | |||||
| } | } | ||||
| ></Button> | ></Button> | ||||
| </Upload> | </Upload> | 
| import { useSetModalState } from '@/hooks/common-hooks'; | import { useSetModalState } from '@/hooks/common-hooks'; | ||||
| import { IRemoveMessageById, useSpeechWithSse } from '@/hooks/logic-hooks'; | import { IRemoveMessageById, useSpeechWithSse } from '@/hooks/logic-hooks'; | ||||
| import { IFeedbackRequestBody } from '@/interfaces/request/chat'; | import { IFeedbackRequestBody } from '@/interfaces/request/chat'; | ||||
| import { ConversationContext } from '@/pages/chat/context'; | |||||
| import { getMessagePureId } from '@/utils/chat'; | import { getMessagePureId } from '@/utils/chat'; | ||||
| import { hexStringToUint8Array } from '@/utils/common-util'; | import { hexStringToUint8Array } from '@/utils/common-util'; | ||||
| import { SpeechPlayer } from 'openai-speech-stream-player'; | import { SpeechPlayer } from 'openai-speech-stream-player'; | ||||
| import { useCallback, useContext, useEffect, useRef, useState } from 'react'; | |||||
| import { useCallback, useEffect, useRef, useState } from 'react'; | |||||
| export const useSendFeedback = (messageId: string) => { | export const useSendFeedback = (messageId: string) => { | ||||
| const { visible, hideModal, showModal } = useSetModalState(); | const { visible, hideModal, showModal } = useSetModalState(); | ||||
| const { read } = useSpeechWithSse(); | const { read } = useSpeechWithSse(); | ||||
| const player = useRef<SpeechPlayer>(); | const player = useRef<SpeechPlayer>(); | ||||
| const [isPlaying, setIsPlaying] = useState<boolean>(false); | const [isPlaying, setIsPlaying] = useState<boolean>(false); | ||||
| const callback = useContext(ConversationContext); | |||||
| const initialize = useCallback(async () => { | const initialize = useCallback(async () => { | ||||
| player.current = new SpeechPlayer({ | player.current = new SpeechPlayer({ | ||||
| audio: ref.current!, | audio: ref.current!, | ||||
| onPlaying: () => { | onPlaying: () => { | ||||
| setIsPlaying(true); | setIsPlaying(true); | ||||
| callback?.(true); | |||||
| }, | }, | ||||
| onPause: () => { | onPause: () => { | ||||
| setIsPlaying(false); | setIsPlaying(false); | ||||
| callback?.(false); | |||||
| }, | }, | ||||
| onChunkEnd: () => {}, | onChunkEnd: () => {}, | ||||
| mimeType: 'audio/mpeg', | mimeType: 'audio/mpeg', | ||||
| }); | }); | ||||
| await player.current.init(); | await player.current.init(); | ||||
| }, [callback]); | |||||
| }, []); | |||||
| const pause = useCallback(() => { | const pause = useCallback(() => { | ||||
| player.current?.pause(); | player.current?.pause(); | ||||
| if (audioBinary) { | if (audioBinary) { | ||||
| const units = hexStringToUint8Array(audioBinary); | const units = hexStringToUint8Array(audioBinary); | ||||
| if (units) { | if (units) { | ||||
| player.current?.feed(units); | |||||
| try { | |||||
| player.current?.feed(units); | |||||
| } catch (error) { | |||||
| console.warn(error); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| }, [audioBinary]); | }, [audioBinary]); | 
| export enum ChatSearchParams { | export enum ChatSearchParams { | ||||
| DialogId = 'dialogId', | DialogId = 'dialogId', | ||||
| ConversationId = 'conversationId', | ConversationId = 'conversationId', | ||||
| isNew = 'isNew', | |||||
| } | } | ||||
| export const EmptyConversationId = 'empty'; | export const EmptyConversationId = 'empty'; | 
| import i18n from '@/locales/config'; | import i18n from '@/locales/config'; | ||||
| import { IClientConversation } from '@/pages/chat/interface'; | import { IClientConversation } from '@/pages/chat/interface'; | ||||
| import chatService from '@/services/chat-service'; | import chatService from '@/services/chat-service'; | ||||
| import { buildMessageListWithUuid, isConversationIdExist } from '@/utils/chat'; | |||||
| import { | |||||
| buildMessageListWithUuid, | |||||
| getConversationId, | |||||
| isConversationIdExist, | |||||
| } from '@/utils/chat'; | |||||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | ||||
| import { message } from 'antd'; | import { message } from 'antd'; | ||||
| import dayjs, { Dayjs } from 'dayjs'; | import dayjs, { Dayjs } from 'dayjs'; | ||||
| import { has, set } from 'lodash'; | import { has, set } from 'lodash'; | ||||
| import { useCallback, useMemo, useState } from 'react'; | import { useCallback, useMemo, useState } from 'react'; | ||||
| import { useSearchParams } from 'umi'; | |||||
| import { history, useSearchParams } from 'umi'; | |||||
| //#region logic | //#region logic | ||||
| export const useClickDialogCard = () => { | export const useClickDialogCard = () => { | ||||
| const [, setSearchParams] = useSearchParams(); | |||||
| const [_, setSearchParams] = useSearchParams(); | |||||
| const newQueryParameters: URLSearchParams = useMemo(() => { | const newQueryParameters: URLSearchParams = useMemo(() => { | ||||
| return new URLSearchParams(); | return new URLSearchParams(); | ||||
| return { handleClickDialog }; | return { handleClickDialog }; | ||||
| }; | }; | ||||
| export const useClickConversationCard = () => { | |||||
| const [currentQueryParameters, setSearchParams] = useSearchParams(); | |||||
| const newQueryParameters: URLSearchParams = useMemo( | |||||
| () => new URLSearchParams(currentQueryParameters.toString()), | |||||
| [currentQueryParameters], | |||||
| ); | |||||
| const handleClickConversation = useCallback( | |||||
| (conversationId: string, isNew: string) => { | |||||
| newQueryParameters.set(ChatSearchParams.ConversationId, conversationId); | |||||
| newQueryParameters.set(ChatSearchParams.isNew, isNew); | |||||
| setSearchParams(newQueryParameters); | |||||
| }, | |||||
| [setSearchParams, newQueryParameters], | |||||
| ); | |||||
| return { handleClickConversation }; | |||||
| }; | |||||
| export const useGetChatSearchParams = () => { | export const useGetChatSearchParams = () => { | ||||
| const [currentQueryParameters] = useSearchParams(); | const [currentQueryParameters] = useSearchParams(); | ||||
| dialogId: currentQueryParameters.get(ChatSearchParams.DialogId) || '', | dialogId: currentQueryParameters.get(ChatSearchParams.DialogId) || '', | ||||
| conversationId: | conversationId: | ||||
| currentQueryParameters.get(ChatSearchParams.ConversationId) || '', | currentQueryParameters.get(ChatSearchParams.ConversationId) || '', | ||||
| isNew: currentQueryParameters.get(ChatSearchParams.isNew) || '', | |||||
| }; | }; | ||||
| }; | }; | ||||
| export const useFetchNextDialogList = () => { | export const useFetchNextDialogList = () => { | ||||
| const { handleClickDialog } = useClickDialogCard(); | const { handleClickDialog } = useClickDialogCard(); | ||||
| const { dialogId } = useGetChatSearchParams(); | |||||
| const { | const { | ||||
| data, | data, | ||||
| initialData: [], | initialData: [], | ||||
| gcTime: 0, | gcTime: 0, | ||||
| refetchOnWindowFocus: false, | refetchOnWindowFocus: false, | ||||
| queryFn: async () => { | |||||
| refetchOnMount: false, | |||||
| queryFn: async (...params) => { | |||||
| console.log('🚀 ~ queryFn: ~ params:', params); | |||||
| const { data } = await chatService.listDialog(); | const { data } = await chatService.listDialog(); | ||||
| if (data.retcode === 0 && data.data.length > 0) { | |||||
| handleClickDialog(data.data[0].id); | |||||
| if (data.retcode === 0) { | |||||
| const list: IDialog[] = data.data; | |||||
| if (list.length > 0) { | |||||
| if (list.every((x) => x.id !== dialogId)) { | |||||
| handleClickDialog(data.data[0].id); | |||||
| } | |||||
| } else { | |||||
| history.push('/chat'); | |||||
| } | |||||
| } | } | ||||
| return data?.data ?? []; | return data?.data ?? []; | ||||
| export const useSetNextDialog = () => { | export const useSetNextDialog = () => { | ||||
| const queryClient = useQueryClient(); | const queryClient = useQueryClient(); | ||||
| const { | const { | ||||
| data, | data, | ||||
| isPending: loading, | isPending: loading, | ||||
| const { data } = await chatService.setDialog(params); | const { data } = await chatService.setDialog(params); | ||||
| if (data.retcode === 0) { | if (data.retcode === 0) { | ||||
| queryClient.invalidateQueries({ | queryClient.invalidateQueries({ | ||||
| exact: false, | |||||
| queryKey: ['fetchDialogList'], | queryKey: ['fetchDialogList'], | ||||
| }); | }); | ||||
| queryClient.invalidateQueries({ | queryClient.invalidateQueries({ | ||||
| queryKey: ['fetchDialog'], | queryKey: ['fetchDialog'], | ||||
| }); | }); | ||||
| const { data } = await chatService.removeDialog({ dialogIds }); | const { data } = await chatService.removeDialog({ dialogIds }); | ||||
| if (data.retcode === 0) { | if (data.retcode === 0) { | ||||
| queryClient.invalidateQueries({ queryKey: ['fetchDialogList'] }); | queryClient.invalidateQueries({ queryKey: ['fetchDialogList'] }); | ||||
| message.success(i18n.t('message.deleted')); | message.success(i18n.t('message.deleted')); | ||||
| } | } | ||||
| return data.retcode; | return data.retcode; | ||||
| export const useFetchNextConversationList = () => { | export const useFetchNextConversationList = () => { | ||||
| const { dialogId } = useGetChatSearchParams(); | const { dialogId } = useGetChatSearchParams(); | ||||
| const { handleClickConversation } = useClickConversationCard(); | |||||
| const { | const { | ||||
| data, | data, | ||||
| isFetching: loading, | isFetching: loading, | ||||
| enabled: !!dialogId, | enabled: !!dialogId, | ||||
| queryFn: async () => { | queryFn: async () => { | ||||
| const { data } = await chatService.listConversation({ dialogId }); | const { data } = await chatService.listConversation({ dialogId }); | ||||
| if (data.retcode === 0 && data.data.length > 0) { | |||||
| handleClickConversation(data.data[0].id, ''); | |||||
| } | |||||
| return data?.data; | return data?.data; | ||||
| }, | }, | ||||
| }); | }); | ||||
| }; | }; | ||||
| export const useFetchNextConversation = () => { | export const useFetchNextConversation = () => { | ||||
| const { conversationId } = useGetChatSearchParams(); | |||||
| const { isNew, conversationId } = useGetChatSearchParams(); | |||||
| const { | const { | ||||
| data, | data, | ||||
| isFetching: loading, | isFetching: loading, | ||||
| gcTime: 0, | gcTime: 0, | ||||
| refetchOnWindowFocus: false, | refetchOnWindowFocus: false, | ||||
| queryFn: async () => { | queryFn: async () => { | ||||
| if (isConversationIdExist(conversationId)) { | |||||
| if (isNew !== 'true' && isConversationIdExist(conversationId)) { | |||||
| const { data } = await chatService.getConversation({ conversationId }); | const { data } = await chatService.getConversation({ conversationId }); | ||||
| // if (data.retcode === 0 && needToBeSaved) { | |||||
| // yield put({ | |||||
| // type: 'kFModel/fetch_document_thumbnails', | |||||
| // payload: { | |||||
| // doc_ids: getDocumentIdsFromConversionReference(data.data), | |||||
| // }, | |||||
| // }); | |||||
| // yield put({ type: 'setCurrentConversation', payload: data.data }); | |||||
| // } | |||||
| const conversation = data?.data ?? {}; | const conversation = data?.data ?? {}; | ||||
| const messageList = buildMessageListWithUuid(conversation?.message); | const messageList = buildMessageListWithUuid(conversation?.message); | ||||
| } = useMutation({ | } = useMutation({ | ||||
| mutationKey: ['updateConversation'], | mutationKey: ['updateConversation'], | ||||
| mutationFn: async (params: Record<string, any>) => { | mutationFn: async (params: Record<string, any>) => { | ||||
| const { data } = await chatService.setConversation(params); | |||||
| const { data } = await chatService.setConversation({ | |||||
| ...params, | |||||
| conversation_id: params.conversation_id | |||||
| ? params.conversation_id | |||||
| : getConversationId(), | |||||
| }); | |||||
| if (data.retcode === 0) { | if (data.retcode === 0) { | ||||
| queryClient.invalidateQueries({ queryKey: ['fetchConversationList'] }); | queryClient.invalidateQueries({ queryKey: ['fetchConversationList'] }); | ||||
| } | } | 
| const send = useCallback( | const send = useCallback( | ||||
| async ( | async ( | ||||
| body: any, | body: any, | ||||
| controller?: AbortController, | |||||
| ): Promise<{ response: Response; data: ResponseType } | undefined> => { | ): Promise<{ response: Response; data: ResponseType } | undefined> => { | ||||
| try { | try { | ||||
| setDone(false); | setDone(false); | ||||
| 'Content-Type': 'application/json', | 'Content-Type': 'application/json', | ||||
| }, | }, | ||||
| body: JSON.stringify(body), | body: JSON.stringify(body), | ||||
| signal: controller?.signal, | |||||
| }); | }); | ||||
| const res = response.clone().json(); | const res = response.clone().json(); | ||||
| const { done, value } = x; | const { done, value } = x; | ||||
| if (done) { | if (done) { | ||||
| console.info('done'); | console.info('done'); | ||||
| setAnswer({} as IAnswer); | |||||
| break; | break; | ||||
| } | } | ||||
| try { | try { | ||||
| } | } | ||||
| console.info('done?'); | console.info('done?'); | ||||
| setDone(true); | setDone(true); | ||||
| setAnswer({} as IAnswer); | |||||
| return { data: await res, response }; | return { data: await res, response }; | ||||
| } catch (e) { | } catch (e) { | ||||
| setDone(true); | setDone(true); | ||||
| setAnswer({} as IAnswer); | |||||
| console.warn(e); | console.warn(e); | ||||
| } | } | ||||
| }, | }, | 
| name: string; | name: string; | ||||
| update_date: string; | update_date: string; | ||||
| update_time: number; | update_time: number; | ||||
| is_new: true; | |||||
| } | } | ||||
| export interface Message { | export interface Message { | 
| addGoogleRegion: 'Google Cloud Region', | addGoogleRegion: 'Google Cloud Region', | ||||
| GoogleRegionMessage: 'Please input Google Cloud Region', | GoogleRegionMessage: 'Please input Google Cloud Region', | ||||
| modelProvidersWarn: | modelProvidersWarn: | ||||
| 'Please add both embedding model and LLM in <b>Settings > Model</b> providers firstly.', | |||||
| 'Please add both embedding model and LLM in <b>Settings > Model providers</b> firstly.', | |||||
| }, | }, | ||||
| message: { | message: { | ||||
| registered: 'Registered!', | registered: 'Registered!', | 
| } from '@/hooks/chat-hooks'; | } from '@/hooks/chat-hooks'; | ||||
| import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | ||||
| import { memo } from 'react'; | import { memo } from 'react'; | ||||
| import { ConversationContext } from '../context'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| const ChatContainer = () => { | |||||
| interface IProps { | |||||
| controller: AbortController; | |||||
| } | |||||
| const ChatContainer = ({ controller }: IProps) => { | |||||
| const { conversationId } = useGetChatSearchParams(); | const { conversationId } = useGetChatSearchParams(); | ||||
| const { data: conversation } = useFetchNextConversation(); | const { data: conversation } = useFetchNextConversation(); | ||||
| handlePressEnter, | handlePressEnter, | ||||
| regenerateMessage, | regenerateMessage, | ||||
| removeMessageById, | removeMessageById, | ||||
| redirectToNewConversation, | |||||
| } = useSendNextMessage(); | |||||
| } = useSendNextMessage(controller); | |||||
| const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | ||||
| useClickDrawer(); | useClickDrawer(); | ||||
| <Flex flex={1} vertical className={styles.messageContainer}> | <Flex flex={1} vertical className={styles.messageContainer}> | ||||
| <div> | <div> | ||||
| <Spin spinning={loading}> | <Spin spinning={loading}> | ||||
| <ConversationContext.Provider value={redirectToNewConversation}> | |||||
| {derivedMessages?.map((message, i) => { | |||||
| return ( | |||||
| <MessageItem | |||||
| loading={ | |||||
| message.role === MessageType.Assistant && | |||||
| sendLoading && | |||||
| derivedMessages.length - 1 === i | |||||
| } | |||||
| key={message.id} | |||||
| item={message} | |||||
| nickname={userInfo.nickname} | |||||
| avatar={userInfo.avatar} | |||||
| reference={buildMessageItemReference( | |||||
| { | |||||
| message: derivedMessages, | |||||
| reference: conversation.reference, | |||||
| }, | |||||
| message, | |||||
| )} | |||||
| clickDocumentButton={clickDocumentButton} | |||||
| index={i} | |||||
| removeMessageById={removeMessageById} | |||||
| regenerateMessage={regenerateMessage} | |||||
| sendLoading={sendLoading} | |||||
| ></MessageItem> | |||||
| ); | |||||
| })} | |||||
| </ConversationContext.Provider> | |||||
| {derivedMessages?.map((message, i) => { | |||||
| return ( | |||||
| <MessageItem | |||||
| loading={ | |||||
| message.role === MessageType.Assistant && | |||||
| sendLoading && | |||||
| derivedMessages.length - 1 === i | |||||
| } | |||||
| key={message.id} | |||||
| item={message} | |||||
| nickname={userInfo.nickname} | |||||
| avatar={userInfo.avatar} | |||||
| reference={buildMessageItemReference( | |||||
| { | |||||
| message: derivedMessages, | |||||
| reference: conversation.reference, | |||||
| }, | |||||
| message, | |||||
| )} | |||||
| clickDocumentButton={clickDocumentButton} | |||||
| index={i} | |||||
| removeMessageById={removeMessageById} | |||||
| regenerateMessage={regenerateMessage} | |||||
| sendLoading={sendLoading} | |||||
| ></MessageItem> | |||||
| ); | |||||
| })} | |||||
| </Spin> | </Spin> | ||||
| </div> | </div> | ||||
| <div ref={ref} /> | <div ref={ref} /> | 
| export enum ChatSearchParams { | |||||
| DialogId = 'dialogId', | |||||
| ConversationId = 'conversationId', | |||||
| } | |||||
| export const EmptyConversationId = 'empty'; | export const EmptyConversationId = 'empty'; | 
| import { MessageType } from '@/constants/chat'; | |||||
| import { ChatSearchParams, MessageType } from '@/constants/chat'; | |||||
| import { fileIconMap } from '@/constants/common'; | import { fileIconMap } from '@/constants/common'; | ||||
| import { | import { | ||||
| useFetchManualConversation, | useFetchManualConversation, | ||||
| } from '@/hooks/logic-hooks'; | } from '@/hooks/logic-hooks'; | ||||
| import { IConversation, IDialog, Message } from '@/interfaces/database/chat'; | import { IConversation, IDialog, Message } from '@/interfaces/database/chat'; | ||||
| import { getFileExtension } from '@/utils'; | import { getFileExtension } from '@/utils'; | ||||
| import api from '@/utils/api'; | |||||
| import { getConversationId } from '@/utils/chat'; | |||||
| import { useMutationState } from '@tanstack/react-query'; | import { useMutationState } from '@tanstack/react-query'; | ||||
| import { get } from 'lodash'; | import { get } from 'lodash'; | ||||
| import trim from 'lodash/trim'; | import trim from 'lodash/trim'; | ||||
| useCallback, | useCallback, | ||||
| useEffect, | useEffect, | ||||
| useMemo, | useMemo, | ||||
| useRef, | |||||
| useState, | useState, | ||||
| } from 'react'; | } from 'react'; | ||||
| import { useSearchParams } from 'umi'; | import { useSearchParams } from 'umi'; | ||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||
| import { ChatSearchParams } from './constants'; | |||||
| import { | import { | ||||
| IClientConversation, | IClientConversation, | ||||
| IMessage, | IMessage, | ||||
| VariableTableDataType, | VariableTableDataType, | ||||
| } from './interface'; | } from './interface'; | ||||
| export const useSetChatRouteParams = () => { | |||||
| const [currentQueryParameters, setSearchParams] = useSearchParams(); | |||||
| const newQueryParameters: URLSearchParams = useMemo( | |||||
| () => new URLSearchParams(currentQueryParameters.toString()), | |||||
| [currentQueryParameters], | |||||
| ); | |||||
| const setConversationIsNew = useCallback( | |||||
| (value: string) => { | |||||
| newQueryParameters.set(ChatSearchParams.isNew, value); | |||||
| setSearchParams(newQueryParameters); | |||||
| }, | |||||
| [newQueryParameters, setSearchParams], | |||||
| ); | |||||
| const getConversationIsNew = useCallback(() => { | |||||
| return newQueryParameters.get(ChatSearchParams.isNew); | |||||
| }, [newQueryParameters]); | |||||
| return { setConversationIsNew, getConversationIsNew }; | |||||
| }; | |||||
| export const useSetNewConversationRouteParams = () => { | |||||
| const [currentQueryParameters, setSearchParams] = useSearchParams(); | |||||
| const newQueryParameters: URLSearchParams = useMemo( | |||||
| () => new URLSearchParams(currentQueryParameters.toString()), | |||||
| [currentQueryParameters], | |||||
| ); | |||||
| const setNewConversationRouteParams = useCallback( | |||||
| (conversationId: string, isNew: string) => { | |||||
| newQueryParameters.set(ChatSearchParams.ConversationId, conversationId); | |||||
| newQueryParameters.set(ChatSearchParams.isNew, isNew); | |||||
| setSearchParams(newQueryParameters); | |||||
| }, | |||||
| [newQueryParameters, setSearchParams], | |||||
| ); | |||||
| return { setNewConversationRouteParams }; | |||||
| }; | |||||
| export const useSelectCurrentDialog = () => { | export const useSelectCurrentDialog = () => { | ||||
| const data = useMutationState({ | const data = useMutationState({ | ||||
| filters: { mutationKey: ['fetchDialog'] }, | filters: { mutationKey: ['fetchDialog'] }, | ||||
| const { data: conversationList, loading } = useFetchNextConversationList(); | const { data: conversationList, loading } = useFetchNextConversationList(); | ||||
| const { dialogId } = useGetChatSearchParams(); | const { dialogId } = useGetChatSearchParams(); | ||||
| const prologue = currentDialog?.prompt_config?.prologue ?? ''; | const prologue = currentDialog?.prompt_config?.prologue ?? ''; | ||||
| const { setNewConversationRouteParams } = useSetNewConversationRouteParams(); | |||||
| const addTemporaryConversation = useCallback(() => { | const addTemporaryConversation = useCallback(() => { | ||||
| const conversationId = getConversationId(); | |||||
| setList((pre) => { | setList((pre) => { | ||||
| if (dialogId) { | if (dialogId) { | ||||
| setNewConversationRouteParams(conversationId, 'true'); | |||||
| const nextList = [ | const nextList = [ | ||||
| { | { | ||||
| id: '', | |||||
| id: conversationId, | |||||
| name: t('newConversation'), | name: t('newConversation'), | ||||
| dialog_id: dialogId, | dialog_id: dialogId, | ||||
| is_new: true, | |||||
| message: [ | message: [ | ||||
| { | { | ||||
| content: prologue, | content: prologue, | ||||
| role: MessageType.Assistant, | role: MessageType.Assistant, | ||||
| }, | }, | ||||
| ], | ], | ||||
| } as IConversation, | |||||
| } as any, | |||||
| ...conversationList, | ...conversationList, | ||||
| ]; | ]; | ||||
| return nextList; | return nextList; | ||||
| return pre; | return pre; | ||||
| }); | }); | ||||
| }, [conversationList, dialogId, prologue, t]); | |||||
| }, [conversationList, dialogId, prologue, t, setNewConversationRouteParams]); | |||||
| // When you first enter the page, select the top conversation card | |||||
| useEffect(() => { | useEffect(() => { | ||||
| addTemporaryConversation(); | |||||
| }, [addTemporaryConversation]); | |||||
| setList([...conversationList]); | |||||
| }, [conversationList]); | |||||
| return { list, addTemporaryConversation, loading }; | return { list, addTemporaryConversation, loading }; | ||||
| }; | }; | ||||
| export const useClickConversationCard = () => { | |||||
| const [currentQueryParameters, setSearchParams] = useSearchParams(); | |||||
| const newQueryParameters: URLSearchParams = useMemo( | |||||
| () => new URLSearchParams(currentQueryParameters.toString()), | |||||
| [currentQueryParameters], | |||||
| ); | |||||
| const handleClickConversation = useCallback( | |||||
| (conversationId: string) => { | |||||
| newQueryParameters.set(ChatSearchParams.ConversationId, conversationId); | |||||
| setSearchParams(newQueryParameters); | |||||
| }, | |||||
| [newQueryParameters, setSearchParams], | |||||
| ); | |||||
| return { handleClickConversation }; | |||||
| }; | |||||
| export const useSetConversation = () => { | export const useSetConversation = () => { | ||||
| const { dialogId } = useGetChatSearchParams(); | const { dialogId } = useGetChatSearchParams(); | ||||
| const { updateConversation } = useUpdateNextConversation(); | const { updateConversation } = useUpdateNextConversation(); | ||||
| const setConversation = useCallback( | const setConversation = useCallback( | ||||
| (message: string) => { | |||||
| return updateConversation({ | |||||
| async ( | |||||
| message: string, | |||||
| isNew: boolean = false, | |||||
| conversationId?: string, | |||||
| ) => { | |||||
| const data = await updateConversation({ | |||||
| dialog_id: dialogId, | dialog_id: dialogId, | ||||
| name: message, | name: message, | ||||
| is_new: isNew, | |||||
| conversation_id: conversationId, | |||||
| message: [ | message: [ | ||||
| { | { | ||||
| role: MessageType.Assistant, | role: MessageType.Assistant, | ||||
| }, | }, | ||||
| ], | ], | ||||
| }); | }); | ||||
| return data; | |||||
| }, | }, | ||||
| [updateConversation, dialogId], | [updateConversation, dialogId], | ||||
| ); | ); | ||||
| return { setConversation }; | return { setConversation }; | ||||
| }; | }; | ||||
| // export const useScrollToBottom = (currentConversation: IClientConversation) => { | |||||
| // const ref = useRef<HTMLDivElement>(null); | |||||
| // const scrollToBottom = useCallback(() => { | |||||
| // if (currentConversation.id) { | |||||
| // ref.current?.scrollIntoView({ behavior: 'instant' }); | |||||
| // } | |||||
| // }, [currentConversation]); | |||||
| // useEffect(() => { | |||||
| // scrollToBottom(); | |||||
| // }, [scrollToBottom]); | |||||
| // return ref; | |||||
| // }; | |||||
| export const useSelectNextMessages = () => { | export const useSelectNextMessages = () => { | ||||
| const { | const { | ||||
| ref, | ref, | ||||
| } = useSelectDerivedMessages(); | } = useSelectDerivedMessages(); | ||||
| const { data: conversation, loading } = useFetchNextConversation(); | const { data: conversation, loading } = useFetchNextConversation(); | ||||
| const { data: dialog } = useFetchNextDialog(); | const { data: dialog } = useFetchNextDialog(); | ||||
| const { conversationId, dialogId } = useGetChatSearchParams(); | |||||
| const { conversationId, dialogId, isNew } = useGetChatSearchParams(); | |||||
| const addPrologue = useCallback(() => { | const addPrologue = useCallback(() => { | ||||
| if (dialogId !== '' && conversationId === '') { | |||||
| if (dialogId !== '' && isNew === 'true') { | |||||
| const prologue = dialog.prompt_config?.prologue; | const prologue = dialog.prompt_config?.prologue; | ||||
| const nextMessage = { | const nextMessage = { | ||||
| setDerivedMessages([nextMessage]); | setDerivedMessages([nextMessage]); | ||||
| } | } | ||||
| }, [conversationId, dialog, dialogId, setDerivedMessages]); | |||||
| }, [isNew, dialog, dialogId, setDerivedMessages]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| addPrologue(); | addPrologue(); | ||||
| }, [addPrologue]); | }, [addPrologue]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (conversationId) { | |||||
| if ( | |||||
| conversationId && | |||||
| isNew !== 'true' && | |||||
| conversation.message?.length > 0 | |||||
| ) { | |||||
| setDerivedMessages(conversation.message); | setDerivedMessages(conversation.message); | ||||
| } | } | ||||
| }, [conversation.message, conversationId, setDerivedMessages]); | |||||
| if (!conversationId) { | |||||
| setDerivedMessages([]); | |||||
| } | |||||
| }, [conversation.message, conversationId, setDerivedMessages, isNew]); | |||||
| return { | return { | ||||
| ref, | ref, | ||||
| }; | }; | ||||
| }; | }; | ||||
| export const useSendNextMessage = () => { | |||||
| export const useSendNextMessage = (controller: AbortController) => { | |||||
| const { setConversation } = useSetConversation(); | const { setConversation } = useSetConversation(); | ||||
| const { conversationId } = useGetChatSearchParams(); | |||||
| const { conversationId, isNew } = useGetChatSearchParams(); | |||||
| const { handleInputChange, value, setValue } = useHandleMessageInputChange(); | const { handleInputChange, value, setValue } = useHandleMessageInputChange(); | ||||
| const { handleClickConversation } = useClickConversationCard(); | |||||
| const { send, answer, done, setDone, resetAnswer } = useSendMessageWithSse(); | |||||
| const { send, answer, done } = useSendMessageWithSse( | |||||
| api.completeConversation, | |||||
| ); | |||||
| const { | const { | ||||
| ref, | ref, | ||||
| derivedMessages, | derivedMessages, | ||||
| removeMessageById, | removeMessageById, | ||||
| removeMessagesAfterCurrentMessage, | removeMessagesAfterCurrentMessage, | ||||
| } = useSelectNextMessages(); | } = useSelectNextMessages(); | ||||
| const { data: dialog } = useFetchNextDialog(); | |||||
| const currentConversationIdRef = useRef<string>(''); | |||||
| const redirectToNewConversation = useCallback( | |||||
| (isPlaying: boolean) => { | |||||
| if (!conversationId && dialog?.prompt_config?.tts && !isPlaying) { | |||||
| handleClickConversation(currentConversationIdRef.current); | |||||
| } | |||||
| }, | |||||
| [dialog, handleClickConversation, conversationId], | |||||
| ); | |||||
| const { setConversationIsNew, getConversationIsNew } = | |||||
| useSetChatRouteParams(); | |||||
| const sendMessage = useCallback( | const sendMessage = useCallback( | ||||
| async ({ | async ({ | ||||
| currentConversationId?: string; | currentConversationId?: string; | ||||
| messages?: Message[]; | messages?: Message[]; | ||||
| }) => { | }) => { | ||||
| const res = await send({ | |||||
| conversation_id: currentConversationId ?? conversationId, | |||||
| messages: [...(messages ?? derivedMessages ?? []), message], | |||||
| }); | |||||
| const res = await send( | |||||
| { | |||||
| conversation_id: currentConversationId ?? conversationId, | |||||
| messages: [...(messages ?? derivedMessages ?? []), message], | |||||
| }, | |||||
| controller, | |||||
| ); | |||||
| if (res && (res?.response.status !== 200 || res?.data?.retcode !== 0)) { | if (res && (res?.response.status !== 200 || res?.data?.retcode !== 0)) { | ||||
| // cancel loading | // cancel loading | ||||
| setValue(message.content); | setValue(message.content); | ||||
| console.info('removeLatestMessage111'); | console.info('removeLatestMessage111'); | ||||
| removeLatestMessage(); | removeLatestMessage(); | ||||
| } else { | |||||
| if (currentConversationId) { | |||||
| console.info('111'); | |||||
| // new conversation | |||||
| if (!dialog?.prompt_config?.tts) { | |||||
| handleClickConversation(currentConversationId); | |||||
| } | |||||
| } else { | |||||
| console.info('222'); | |||||
| // fetchConversation(conversationId); | |||||
| } | |||||
| } | } | ||||
| }, | }, | ||||
| [ | [ | ||||
| dialog, | |||||
| derivedMessages, | derivedMessages, | ||||
| conversationId, | conversationId, | ||||
| handleClickConversation, | |||||
| removeLatestMessage, | removeLatestMessage, | ||||
| setValue, | setValue, | ||||
| send, | send, | ||||
| controller, | |||||
| ], | ], | ||||
| ); | ); | ||||
| const handleSendMessage = useCallback( | const handleSendMessage = useCallback( | ||||
| async (message: Message) => { | async (message: Message) => { | ||||
| if (conversationId !== '') { | |||||
| const isNew = getConversationIsNew(); | |||||
| if (isNew !== 'true') { | |||||
| sendMessage({ message }); | sendMessage({ message }); | ||||
| } else { | } else { | ||||
| const data = await setConversation(message.content); | |||||
| const data = await setConversation( | |||||
| message.content, | |||||
| true, | |||||
| conversationId, | |||||
| ); | |||||
| if (data.retcode === 0) { | if (data.retcode === 0) { | ||||
| setConversationIsNew(''); | |||||
| const id = data.data.id; | const id = data.data.id; | ||||
| currentConversationIdRef.current = id; | |||||
| // currentConversationIdRef.current = id; | |||||
| sendMessage({ | sendMessage({ | ||||
| message, | message, | ||||
| currentConversationId: id, | currentConversationId: id, | ||||
| } | } | ||||
| } | } | ||||
| }, | }, | ||||
| [conversationId, setConversation, sendMessage], | |||||
| [ | |||||
| setConversation, | |||||
| sendMessage, | |||||
| setConversationIsNew, | |||||
| getConversationIsNew, | |||||
| conversationId, | |||||
| ], | |||||
| ); | ); | ||||
| const { regenerateMessage } = useRegenerateMessage({ | const { regenerateMessage } = useRegenerateMessage({ | ||||
| useEffect(() => { | useEffect(() => { | ||||
| // #1289 | // #1289 | ||||
| console.log('🚀 ~ useEffect ~ answer:', answer, done); | |||||
| if ( | |||||
| answer.answer && | |||||
| (answer?.conversationId === conversationId || | |||||
| ((!done || (done && answer.audio_binary)) && conversationId === '')) | |||||
| ) { | |||||
| if (answer.answer && conversationId && isNew !== 'true') { | |||||
| addNewestAnswer(answer); | addNewestAnswer(answer); | ||||
| } | } | ||||
| }, [answer, addNewestAnswer, conversationId, done]); | |||||
| useEffect(() => { | |||||
| // #1289 switch to another conversion window when the last conversion answer doesn't finish. | |||||
| if (conversationId) { | |||||
| setDone(true); | |||||
| } else { | |||||
| resetAnswer(); | |||||
| } | |||||
| }, [setDone, conversationId, resetAnswer]); | |||||
| }, [answer, addNewestAnswer, conversationId, isNew]); | |||||
| const handlePressEnter = useCallback( | const handlePressEnter = useCallback( | ||||
| (documentIds: string[]) => { | (documentIds: string[]) => { | ||||
| ref, | ref, | ||||
| derivedMessages, | derivedMessages, | ||||
| removeMessageById, | removeMessageById, | ||||
| redirectToNewConversation, | |||||
| }; | }; | ||||
| }; | }; | ||||
| }; | }; | ||||
| export const useDeleteConversation = () => { | export const useDeleteConversation = () => { | ||||
| const { handleClickConversation } = useClickConversationCard(); | |||||
| const showDeleteConfirm = useShowDeleteConfirm(); | const showDeleteConfirm = useShowDeleteConfirm(); | ||||
| const { removeConversation } = useRemoveNextConversation(); | const { removeConversation } = useRemoveNextConversation(); | ||||
| const deleteConversation = (conversationIds: Array<string>) => async () => { | const deleteConversation = (conversationIds: Array<string>) => async () => { | ||||
| const ret = await removeConversation(conversationIds); | const ret = await removeConversation(conversationIds); | ||||
| if (ret === 0) { | |||||
| handleClickConversation(''); | |||||
| } | |||||
| return ret; | return ret; | ||||
| }; | }; | ||||
| ...conversation, | ...conversation, | ||||
| conversation_id: conversation.id, | conversation_id: conversation.id, | ||||
| name, | name, | ||||
| is_new: false, | |||||
| }); | }); | ||||
| if (ret.retcode === 0) { | if (ret.retcode === 0) { | ||||
| export const useGetSendButtonDisabled = () => { | export const useGetSendButtonDisabled = () => { | ||||
| const { dialogId, conversationId } = useGetChatSearchParams(); | const { dialogId, conversationId } = useGetChatSearchParams(); | ||||
| return dialogId === '' && conversationId === ''; | |||||
| return dialogId === '' || conversationId === ''; | |||||
| }; | }; | ||||
| export const useSendButtonDisabled = (value: string) => { | export const useSendButtonDisabled = (value: string) => { | ||||
| const { setConversation } = useSetConversation(); | const { setConversation } = useSetConversation(); | ||||
| const { dialogId } = useGetChatSearchParams(); | const { dialogId } = useGetChatSearchParams(); | ||||
| const { handleClickConversation } = useClickConversationCard(); | |||||
| const createConversationBeforeUploadDocument = useCallback( | const createConversationBeforeUploadDocument = useCallback( | ||||
| async (message: string) => { | async (message: string) => { | ||||
| const data = await setConversation(message); | |||||
| if (data.retcode === 0) { | |||||
| const id = data.data.id; | |||||
| handleClickConversation(id); | |||||
| } | |||||
| const data = await setConversation(message, true); | |||||
| return data; | return data; | ||||
| }, | }, | ||||
| [setConversation, handleClickConversation], | |||||
| [setConversation], | |||||
| ); | ); | ||||
| return { | return { | 
| Space, | Space, | ||||
| Spin, | Spin, | ||||
| Tag, | Tag, | ||||
| Tooltip, | |||||
| Typography, | Typography, | ||||
| } from 'antd'; | } from 'antd'; | ||||
| import { MenuItemProps } from 'antd/lib/menu/MenuItem'; | import { MenuItemProps } from 'antd/lib/menu/MenuItem'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useCallback } from 'react'; | |||||
| import { useCallback, useState } from 'react'; | |||||
| import ChatConfigurationModal from './chat-configuration-modal'; | import ChatConfigurationModal from './chat-configuration-modal'; | ||||
| import ChatContainer from './chat-container'; | import ChatContainer from './chat-container'; | ||||
| import { | import { | ||||
| useClickConversationCard, | |||||
| useDeleteConversation, | useDeleteConversation, | ||||
| useDeleteDialog, | useDeleteDialog, | ||||
| useEditDialog, | useEditDialog, | ||||
| import ChatOverviewModal from '@/components/api-service/chat-overview-modal'; | import ChatOverviewModal from '@/components/api-service/chat-overview-modal'; | ||||
| import { | import { | ||||
| useClickConversationCard, | |||||
| useClickDialogCard, | useClickDialogCard, | ||||
| useFetchNextDialogList, | useFetchNextDialogList, | ||||
| useGetChatSearchParams, | useGetChatSearchParams, | ||||
| showModal: showOverviewModal, | showModal: showOverviewModal, | ||||
| } = useSetModalState(); | } = useSetModalState(); | ||||
| const { currentRecord, setRecord } = useSetSelectedRecord<IDialog>(); | const { currentRecord, setRecord } = useSetSelectedRecord<IDialog>(); | ||||
| const [controller, setController] = useState(new AbortController()); | |||||
| const handleAppCardEnter = (id: string) => () => { | const handleAppCardEnter = (id: string) => () => { | ||||
| handleItemEnter(id); | handleItemEnter(id); | ||||
| showConversationRenameModal(conversationId); | showConversationRenameModal(conversationId); | ||||
| }; | }; | ||||
| const handleDialogCardClick = (dialogId: string) => () => { | |||||
| handleClickDialog(dialogId); | |||||
| }; | |||||
| const handleDialogCardClick = useCallback( | |||||
| (dialogId: string) => () => { | |||||
| handleClickDialog(dialogId); | |||||
| }, | |||||
| [handleClickDialog], | |||||
| ); | |||||
| const handleConversationCardClick = (dialogId: string) => () => { | |||||
| handleClickConversation(dialogId); | |||||
| }; | |||||
| const handleConversationCardClick = useCallback( | |||||
| (conversationId: string, isNew: boolean) => () => { | |||||
| handleClickConversation(conversationId, isNew ? 'true' : ''); | |||||
| setController((pre) => { | |||||
| pre.abort(); | |||||
| return new AbortController(); | |||||
| }); | |||||
| }, | |||||
| [handleClickConversation], | |||||
| ); | |||||
| const handleCreateTemporaryConversation = useCallback(() => { | const handleCreateTemporaryConversation = useCallback(() => { | ||||
| addTemporaryConversation(); | addTemporaryConversation(); | ||||
| }, [addTemporaryConversation]); | }, [addTemporaryConversation]); | ||||
| const items: MenuProps['items'] = [ | |||||
| { | |||||
| key: '1', | |||||
| onClick: handleCreateTemporaryConversation, | |||||
| label: ( | |||||
| <Space> | |||||
| <PlusOutlined /> | |||||
| {t('newChat')} | |||||
| </Space> | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| const buildAppItems = (dialog: IDialog) => { | const buildAppItems = (dialog: IDialog) => { | ||||
| const dialogId = dialog.id; | const dialogId = dialog.id; | ||||
| <b>{t('chat')}</b> | <b>{t('chat')}</b> | ||||
| <Tag>{conversationList.length}</Tag> | <Tag>{conversationList.length}</Tag> | ||||
| </Space> | </Space> | ||||
| <Dropdown menu={{ items }}> | |||||
| {/* <FormOutlined /> */} | |||||
| <PlusOutlined /> | |||||
| </Dropdown> | |||||
| <Tooltip title={t('newChat')}> | |||||
| <PlusOutlined onClick={handleCreateTemporaryConversation} /> | |||||
| </Tooltip> | |||||
| </Flex> | </Flex> | ||||
| <Divider></Divider> | <Divider></Divider> | ||||
| <Flex vertical gap={10} className={styles.chatTitleContent}> | <Flex vertical gap={10} className={styles.chatTitleContent}> | ||||
| <Card | <Card | ||||
| key={x.id} | key={x.id} | ||||
| hoverable | hoverable | ||||
| onClick={handleConversationCardClick(x.id)} | |||||
| onClick={handleConversationCardClick(x.id, x.is_new)} | |||||
| onMouseEnter={handleConversationCardEnter(x.id)} | onMouseEnter={handleConversationCardEnter(x.id)} | ||||
| onMouseLeave={handleConversationItemLeave} | onMouseLeave={handleConversationItemLeave} | ||||
| className={classNames(styles.chatTitleCard, { | className={classNames(styles.chatTitleCard, { | ||||
| </Flex> | </Flex> | ||||
| </Flex> | </Flex> | ||||
| <Divider type={'vertical'} className={styles.divider}></Divider> | <Divider type={'vertical'} className={styles.divider}></Divider> | ||||
| <ChatContainer></ChatContainer> | |||||
| <ChatContainer controller={controller}></ChatContainer> | |||||
| {dialogEditVisible && ( | {dialogEditVisible && ( | ||||
| <ChatConfigurationModal | <ChatConfigurationModal | ||||
| visible={dialogEditVisible} | visible={dialogEditVisible} | 
| <Form.Item<FieldType> | <Form.Item<FieldType> | ||||
| label={t('addFishAudioRefID')} | label={t('addFishAudioRefID')} | ||||
| name="fish_audio_refid" | name="fish_audio_refid" | ||||
| rules={[{ required: false, message: t('FishAudioRefIDMessage') }]} | |||||
| rules={[{ required: true, message: t('FishAudioRefIDMessage') }]} | |||||
| > | > | ||||
| <Input placeholder={t('FishAudioRefIDMessage')} /> | <Input placeholder={t('FishAudioRefIDMessage')} /> | ||||
| </Form.Item> | </Form.Item> | 
| })) ?? [] | })) ?? [] | ||||
| ); | ); | ||||
| }; | }; | ||||
| export const getConversationId = () => { | |||||
| return uuid().replace(/-/g, ''); | |||||
| }; |