### What problem does this PR solve? Feat: Create a conversation #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.1
| import message from '@/components/ui/message'; | import message from '@/components/ui/message'; | ||||
| import { ChatSearchParams } from '@/constants/chat'; | import { ChatSearchParams } from '@/constants/chat'; | ||||
| import { IDialog } from '@/interfaces/database/chat'; | import { IDialog } from '@/interfaces/database/chat'; | ||||
| import chatService from '@/services/chat-service'; | |||||
| import chatService from '@/services/next-chat-service '; | |||||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | ||||
| import { useDebounce } from 'ahooks'; | import { useDebounce } from 'ahooks'; | ||||
| import { useCallback, useMemo } from 'react'; | import { useCallback, useMemo } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { history, useSearchParams } from 'umi'; | |||||
| import { useParams, useSearchParams } from 'umi'; | |||||
| import { | import { | ||||
| useGetPaginationWithRouter, | useGetPaginationWithRouter, | ||||
| useHandleSearchChange, | useHandleSearchChange, | ||||
| FetchDialogList = 'fetchDialogList', | FetchDialogList = 'fetchDialogList', | ||||
| RemoveDialog = 'removeDialog', | RemoveDialog = 'removeDialog', | ||||
| SetDialog = 'setDialog', | SetDialog = 'setDialog', | ||||
| FetchDialog = 'fetchDialog', | |||||
| } | } | ||||
| export const useGetChatSearchParams = () => { | export const useGetChatSearchParams = () => { | ||||
| return { handleClickDialog }; | return { handleClickDialog }; | ||||
| }; | }; | ||||
| export const useFetchDialogList = (pureFetch = false) => { | |||||
| const { handleClickDialog } = useClickDialogCard(); | |||||
| const { dialogId } = useGetChatSearchParams(); | |||||
| export const useFetchDialogList = () => { | |||||
| const { searchString, handleInputChange } = useHandleSearchChange(); | const { searchString, handleInputChange } = useHandleSearchChange(); | ||||
| const { pagination, setPagination } = useGetPaginationWithRouter(); | const { pagination, setPagination } = useGetPaginationWithRouter(); | ||||
| const debouncedSearchString = useDebounce(searchString, { wait: 500 }); | const debouncedSearchString = useDebounce(searchString, { wait: 500 }); | ||||
| data, | data, | ||||
| isFetching: loading, | isFetching: loading, | ||||
| refetch, | refetch, | ||||
| } = useQuery<IDialog[]>({ | |||||
| } = useQuery<{ dialogs: IDialog[]; total: number }>({ | |||||
| queryKey: [ | queryKey: [ | ||||
| ChatApiAction.FetchDialogList, | ChatApiAction.FetchDialogList, | ||||
| { | { | ||||
| ...pagination, | ...pagination, | ||||
| }, | }, | ||||
| ], | ], | ||||
| initialData: [], | |||||
| initialData: { dialogs: [], total: 0 }, | |||||
| gcTime: 0, | gcTime: 0, | ||||
| refetchOnWindowFocus: false, | refetchOnWindowFocus: false, | ||||
| queryFn: async (...params) => { | |||||
| console.log('🚀 ~ queryFn: ~ params:', params); | |||||
| const { data } = await chatService.listDialog(); | |||||
| if (data.code === 0) { | |||||
| const list: IDialog[] = data.data; | |||||
| if (!pureFetch) { | |||||
| if (list.length > 0) { | |||||
| if (list.every((x) => x.id !== dialogId)) { | |||||
| handleClickDialog(data.data[0].id); | |||||
| } | |||||
| } else { | |||||
| history.push('/chat'); | |||||
| } | |||||
| } | |||||
| } | |||||
| return data?.data ?? []; | |||||
| queryFn: async () => { | |||||
| const { data } = await chatService.listDialog({ | |||||
| keywords: debouncedSearchString, | |||||
| page_size: pagination.pageSize, | |||||
| page: pagination.current, | |||||
| }); | |||||
| return data?.data ?? { dialogs: [], total: 0 }; | |||||
| }, | }, | ||||
| }); | }); | ||||
| mutateAsync, | mutateAsync, | ||||
| } = useMutation({ | } = useMutation({ | ||||
| mutationKey: [ChatApiAction.SetDialog], | mutationKey: [ChatApiAction.SetDialog], | ||||
| mutationFn: async (params: IDialog) => { | |||||
| mutationFn: async (params: Partial<IDialog>) => { | |||||
| const { data } = await chatService.setDialog(params); | const { data } = await chatService.setDialog(params); | ||||
| if (data.code === 0) { | if (data.code === 0) { | ||||
| queryClient.invalidateQueries({ | queryClient.invalidateQueries({ | ||||
| exact: false, | exact: false, | ||||
| queryKey: ['fetchDialogList'], | |||||
| queryKey: [ChatApiAction.FetchDialogList], | |||||
| }); | }); | ||||
| queryClient.invalidateQueries({ | |||||
| queryKey: ['fetchDialog'], | |||||
| }); | |||||
| message.success( | message.success( | ||||
| t(`message.${params.dialog_id ? 'modified' : 'created'}`), | t(`message.${params.dialog_id ? 'modified' : 'created'}`), | ||||
| ); | ); | ||||
| return { data, loading, setDialog: mutateAsync }; | return { data, loading, setDialog: mutateAsync }; | ||||
| }; | }; | ||||
| export const useFetchDialog = () => { | |||||
| const { id } = useParams(); | |||||
| const { | |||||
| data, | |||||
| isFetching: loading, | |||||
| refetch, | |||||
| } = useQuery<IDialog>({ | |||||
| queryKey: [ChatApiAction.FetchDialog, id], | |||||
| gcTime: 0, | |||||
| initialData: {} as IDialog, | |||||
| enabled: !!id, | |||||
| refetchOnWindowFocus: false, | |||||
| queryFn: async () => { | |||||
| const { data } = await chatService.getDialog( | |||||
| { params: { dialogId: id } }, | |||||
| true, | |||||
| ); | |||||
| return data?.data ?? ({} as IDialog); | |||||
| }, | |||||
| }); | |||||
| return { data, loading, refetch }; | |||||
| }; |
| import { ApplicationCard } from './application-card'; | import { ApplicationCard } from './application-card'; | ||||
| export function ChatList() { | export function ChatList() { | ||||
| const { data } = useFetchDialogList(true); | |||||
| const { data } = useFetchDialogList(); | |||||
| return data | |||||
| return data.dialogs | |||||
| .slice(0, 10) | .slice(0, 10) | ||||
| .map((x) => ( | .map((x) => ( | ||||
| <ApplicationCard | <ApplicationCard |
| import { PageHeader } from '@/components/page-header'; | import { PageHeader } from '@/components/page-header'; | ||||
| import { Button } from '@/components/ui/button'; | import { Button } from '@/components/ui/button'; | ||||
| import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | ||||
| import { useFetchDialog } from '@/hooks/use-chat-request'; | |||||
| import { EllipsisVertical } from 'lucide-react'; | import { EllipsisVertical } from 'lucide-react'; | ||||
| import { AppSettings } from './app-settings'; | import { AppSettings } from './app-settings'; | ||||
| import { ChatBox } from './chat-box'; | import { ChatBox } from './chat-box'; | ||||
| export default function Chat() { | export default function Chat() { | ||||
| const { navigateToChatList } = useNavigatePage(); | const { navigateToChatList } = useNavigatePage(); | ||||
| useFetchDialog(); | |||||
| return ( | return ( | ||||
| <section className="h-full flex flex-col"> | <section className="h-full flex flex-col"> | ||||
| <PageHeader back={navigateToChatList} title="Chat app 01"> | |||||
| <PageHeader> | |||||
| <div className="flex items-center gap-2"> | <div className="flex items-center gap-2"> | ||||
| <Button variant={'icon'} size={'icon'}> | <Button variant={'icon'} size={'icon'}> | ||||
| <EllipsisVertical /> | <EllipsisVertical /> |
| import { useSetModalState } from '@/hooks/common-hooks'; | import { useSetModalState } from '@/hooks/common-hooks'; | ||||
| import { useSetDialog } from '@/hooks/use-chat-request'; | import { useSetDialog } from '@/hooks/use-chat-request'; | ||||
| import { IDialog } from '@/interfaces/database/chat'; | import { IDialog } from '@/interfaces/database/chat'; | ||||
| import { isEmpty } from 'lodash'; | |||||
| import { useCallback, useState } from 'react'; | import { useCallback, useState } from 'react'; | ||||
| const InitialData = { | |||||
| name: '', | |||||
| icon: '', | |||||
| language: 'English', | |||||
| prompt_config: { | |||||
| empty_response: '', | |||||
| prologue: '你好! 我是你的助理,有什么可以帮到你的吗?', | |||||
| quote: true, | |||||
| keyword: false, | |||||
| tts: false, | |||||
| system: | |||||
| '你是一个智能助手,请总结知识库的内容来回答问题,请列举知识库中的数据详细回答。当所有知识库内容都与问题无关时,你的回答必须包括“知识库中未找到您要的答案!”这句话。回答需要考虑聊天历史。\n 以下是知识库:\n {knowledge}\n 以上是知识库。', | |||||
| refine_multiturn: false, | |||||
| use_kg: false, | |||||
| reasoning: false, | |||||
| parameters: [{ key: 'knowledge', optional: false }], | |||||
| }, | |||||
| llm_id: '', | |||||
| llm_setting: {}, | |||||
| similarity_threshold: 0.2, | |||||
| vector_similarity_weight: 0.30000000000000004, | |||||
| top_n: 8, | |||||
| }; | |||||
| export const useRenameChat = () => { | export const useRenameChat = () => { | ||||
| const [chat, setChat] = useState<IDialog>({} as IDialog); | const [chat, setChat] = useState<IDialog>({} as IDialog); | ||||
| const { | const { | ||||
| const onChatRenameOk = useCallback( | const onChatRenameOk = useCallback( | ||||
| async (name: string) => { | async (name: string) => { | ||||
| const ret = await setDialog({ | |||||
| ...chat, | |||||
| const nextChat = { | |||||
| ...(isEmpty(chat) ? InitialData : chat), | |||||
| name, | name, | ||||
| }); | |||||
| }; | |||||
| const ret = await setDialog(nextChat); | |||||
| if (ret === 0) { | if (ret === 0) { | ||||
| hideChatRenameModal(); | hideChatRenameModal(); | ||||
| ); | ); | ||||
| const handleShowChatRenameModal = useCallback( | const handleShowChatRenameModal = useCallback( | ||||
| async (record: IDialog) => { | |||||
| setChat(record); | |||||
| (record?: IDialog) => { | |||||
| if (record) { | |||||
| setChat(record); | |||||
| } | |||||
| showChatRenameModal(); | showChatRenameModal(); | ||||
| }, | }, | ||||
| [showChatRenameModal], | [showChatRenameModal], | ||||
| ); | ); | ||||
| const handleHideModal = useCallback(() => { | |||||
| hideChatRenameModal(); | |||||
| setChat({} as IDialog); | |||||
| }, [hideChatRenameModal]); | |||||
| return { | return { | ||||
| chatRenameLoading: loading, | chatRenameLoading: loading, | ||||
| initialChatName: chat?.name, | initialChatName: chat?.name, | ||||
| onChatRenameOk, | onChatRenameOk, | ||||
| chatRenameVisible, | chatRenameVisible, | ||||
| hideChatRenameModal, | |||||
| hideChatRenameModal: handleHideModal, | |||||
| showChatRenameModal: handleShowChatRenameModal, | showChatRenameModal: handleShowChatRenameModal, | ||||
| }; | }; | ||||
| }; | }; |
| import { useRenameChat } from './hooks/use-rename-chat'; | import { useRenameChat } from './hooks/use-rename-chat'; | ||||
| export default function ChatList() { | export default function ChatList() { | ||||
| const { data: chatList, setPagination, pagination } = useFetchDialogList(); | |||||
| const { data, setPagination, pagination } = useFetchDialogList(); | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const { | const { | ||||
| initialChatName, | initialChatName, | ||||
| [setPagination], | [setPagination], | ||||
| ); | ); | ||||
| const handleShowCreateModal = useCallback(() => { | |||||
| showChatRenameModal(); | |||||
| }, [showChatRenameModal]); | |||||
| return ( | return ( | ||||
| <section className="flex flex-col w-full flex-1"> | <section className="flex flex-col w-full flex-1"> | ||||
| <div className="px-8 pt-8"> | <div className="px-8 pt-8"> | ||||
| <ListFilterBar title="Chat apps"> | <ListFilterBar title="Chat apps"> | ||||
| <Button> | |||||
| <Button onClick={handleShowCreateModal}> | |||||
| <Plus className="size-2.5" /> | <Plus className="size-2.5" /> | ||||
| {t('chat.createChat')} | {t('chat.createChat')} | ||||
| </Button> | </Button> | ||||
| </div> | </div> | ||||
| <div className="flex-1 overflow-auto"> | <div className="flex-1 overflow-auto"> | ||||
| <div className="flex flex-wrap gap-4 px-8"> | <div className="flex flex-wrap gap-4 px-8"> | ||||
| {chatList.map((x) => { | |||||
| {data.dialogs.map((x) => { | |||||
| return ( | return ( | ||||
| <ChatCard | <ChatCard | ||||
| key={x.id} | key={x.id} | ||||
| onOk={onChatRenameOk} | onOk={onChatRenameOk} | ||||
| initialName={initialChatName} | initialName={initialChatName} | ||||
| loading={chatRenameLoading} | loading={chatRenameLoading} | ||||
| title={initialChatName || t('chat.createChat')} | |||||
| ></RenameDialog> | ></RenameDialog> | ||||
| )} | )} | ||||
| </section> | </section> |
| import api from '@/utils/api'; | |||||
| import { registerNextServer } from '@/utils/register-server'; | |||||
| const { | |||||
| getDialog, | |||||
| setDialog, | |||||
| listDialog, | |||||
| removeDialog, | |||||
| getConversation, | |||||
| getConversationSSE, | |||||
| setConversation, | |||||
| completeConversation, | |||||
| listConversation, | |||||
| removeConversation, | |||||
| createToken, | |||||
| listToken, | |||||
| removeToken, | |||||
| getStats, | |||||
| createExternalConversation, | |||||
| getExternalConversation, | |||||
| completeExternalConversation, | |||||
| uploadAndParseExternal, | |||||
| deleteMessage, | |||||
| thumbup, | |||||
| tts, | |||||
| ask, | |||||
| mindmap, | |||||
| getRelatedQuestions, | |||||
| listNextDialog, | |||||
| } = api; | |||||
| const methods = { | |||||
| getDialog: { | |||||
| url: getDialog, | |||||
| method: 'get', | |||||
| }, | |||||
| setDialog: { | |||||
| url: setDialog, | |||||
| method: 'post', | |||||
| }, | |||||
| removeDialog: { | |||||
| url: removeDialog, | |||||
| method: 'post', | |||||
| }, | |||||
| listDialog: { | |||||
| url: listNextDialog, | |||||
| method: 'post', | |||||
| }, | |||||
| listConversation: { | |||||
| url: listConversation, | |||||
| method: 'get', | |||||
| }, | |||||
| getConversation: { | |||||
| url: getConversation, | |||||
| method: 'get', | |||||
| }, | |||||
| getConversationSSE: { | |||||
| url: getConversationSSE, | |||||
| method: 'get', | |||||
| }, | |||||
| setConversation: { | |||||
| url: setConversation, | |||||
| method: 'post', | |||||
| }, | |||||
| completeConversation: { | |||||
| url: completeConversation, | |||||
| method: 'post', | |||||
| }, | |||||
| removeConversation: { | |||||
| url: removeConversation, | |||||
| method: 'post', | |||||
| }, | |||||
| createToken: { | |||||
| url: createToken, | |||||
| method: 'post', | |||||
| }, | |||||
| listToken: { | |||||
| url: listToken, | |||||
| method: 'get', | |||||
| }, | |||||
| removeToken: { | |||||
| url: removeToken, | |||||
| method: 'post', | |||||
| }, | |||||
| getStats: { | |||||
| url: getStats, | |||||
| method: 'get', | |||||
| }, | |||||
| createExternalConversation: { | |||||
| url: createExternalConversation, | |||||
| method: 'get', | |||||
| }, | |||||
| getExternalConversation: { | |||||
| url: getExternalConversation, | |||||
| method: 'get', | |||||
| }, | |||||
| completeExternalConversation: { | |||||
| url: completeExternalConversation, | |||||
| method: 'post', | |||||
| }, | |||||
| uploadAndParseExternal: { | |||||
| url: uploadAndParseExternal, | |||||
| method: 'post', | |||||
| }, | |||||
| deleteMessage: { | |||||
| url: deleteMessage, | |||||
| method: 'post', | |||||
| }, | |||||
| thumbup: { | |||||
| url: thumbup, | |||||
| method: 'post', | |||||
| }, | |||||
| tts: { | |||||
| url: tts, | |||||
| method: 'post', | |||||
| }, | |||||
| ask: { | |||||
| url: ask, | |||||
| method: 'post', | |||||
| }, | |||||
| getMindMap: { | |||||
| url: mindmap, | |||||
| method: 'post', | |||||
| }, | |||||
| getRelatedQuestions: { | |||||
| url: getRelatedQuestions, | |||||
| method: 'post', | |||||
| }, | |||||
| } as const; | |||||
| const chatService = registerNextServer<keyof typeof methods>(methods); | |||||
| export default chatService; |
| completeExternalConversation: `${api_host}/api/completion`, | completeExternalConversation: `${api_host}/api/completion`, | ||||
| uploadAndParseExternal: `${api_host}/api/document/upload_and_parse`, | uploadAndParseExternal: `${api_host}/api/document/upload_and_parse`, | ||||
| // next chat | |||||
| listNextDialog: `${api_host}/dialog/next`, | |||||
| // file manager | // file manager | ||||
| listFile: `${api_host}/file/list`, | listFile: `${api_host}/file/list`, | ||||
| uploadFile: `${api_host}/file/upload`, | uploadFile: `${api_host}/file/upload`, |