diff --git a/web/src/hooks/use-chat-request.ts b/web/src/hooks/use-chat-request.ts index d7e5a3642..a0147fc16 100644 --- a/web/src/hooks/use-chat-request.ts +++ b/web/src/hooks/use-chat-request.ts @@ -271,9 +271,14 @@ export const useFetchConversation = () => { isNew !== 'true' && isConversationIdExist(sharedId || conversationId) ) { - const { data } = await chatService.getConversation({ - conversationId: conversationId || sharedId, - }); + const { data } = await chatService.getConversation( + { + params: { + conversationId: conversationId || sharedId, + }, + }, + true, + ); const conversation = data?.data ?? {}; diff --git a/web/src/pages/next-chats/chat-card.tsx b/web/src/pages/next-chats/chat-card.tsx index 733b77308..82ad69345 100644 --- a/web/src/pages/next-chats/chat-card.tsx +++ b/web/src/pages/next-chats/chat-card.tsx @@ -31,7 +31,7 @@ export function ChatCard({ data, showChatRenameModal }: IProps) {
-

+

{data.name}

{data.description}

diff --git a/web/src/pages/next-chats/chat/app-settings/chat-settings-sheet.tsx b/web/src/pages/next-chats/chat/app-settings/chat-settings-sheet.tsx new file mode 100644 index 000000000..e5343e75a --- /dev/null +++ b/web/src/pages/next-chats/chat/app-settings/chat-settings-sheet.tsx @@ -0,0 +1,23 @@ +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from '@/components/ui/sheet'; +import { PropsWithChildren } from 'react'; +import { ChatSettings } from './chat-settings'; + +export function ChatSettingSheet({ children }: PropsWithChildren) { + return ( + + {children} + + + Chat Settings + + + + + ); +} diff --git a/web/src/pages/next-chats/chat/app-settings/index.tsx b/web/src/pages/next-chats/chat/app-settings/chat-settings.tsx similarity index 54% rename from web/src/pages/next-chats/chat/app-settings/index.tsx rename to web/src/pages/next-chats/chat/app-settings/chat-settings.tsx index 4636b05db..143674e9c 100644 --- a/web/src/pages/next-chats/chat/app-settings/index.tsx +++ b/web/src/pages/next-chats/chat/app-settings/chat-settings.tsx @@ -7,8 +7,9 @@ import { ChatModelSettings } from './chat-model-settings'; import { ChatPromptEngine } from './chat-prompt-engine'; import { useChatSettingSchema } from './use-chat-setting-schema'; -export function AppSettings() { +export function ChatSettings() { const formSchema = useChatSettingSchema(); + const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { @@ -32,27 +33,19 @@ export function AppSettings() { } return ( -
-
- App settings -
-
- -
- - - -
-
-
-
-

- There are unsaved changes -

- -
+
+ +
+ + + +
+
+ +
); } diff --git a/web/src/pages/next-chats/chat/chat-box.tsx b/web/src/pages/next-chats/chat/chat-box.tsx index 3b1cdf181..897c9e66a 100644 --- a/web/src/pages/next-chats/chat/chat-box.tsx +++ b/web/src/pages/next-chats/chat/chat-box.tsx @@ -1,9 +1,95 @@ -import { ChatInput } from '@/components/chat-input'; +import { NextMessageInput } from '@/components/message-input/next'; +import MessageItem from '@/components/message-item'; +import { MessageType } from '@/constants/chat'; +import { + useFetchConversation, + useFetchDialog, + useGetChatSearchParams, +} from '@/hooks/use-chat-request'; +import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; +import { buildMessageUuidWithRole } from '@/utils/chat'; +import { + useGetSendButtonDisabled, + useSendButtonDisabled, +} from '../hooks/use-button-disabled'; +import { useCreateConversationBeforeUploadDocument } from '../hooks/use-create-conversation'; +import { useSendMessage } from '../hooks/use-send-chat-message'; +import { buildMessageItemReference } from '../utils'; + +interface IProps { + controller: AbortController; +} + +export function ChatBox({ controller }: IProps) { + const { + value, + scrollRef, + messageContainerRef, + sendLoading, + derivedMessages, + handleInputChange, + handlePressEnter, + regenerateMessage, + removeMessageById, + stopOutputMessage, + } = useSendMessage(controller); + const { data: userInfo } = useFetchUserInfo(); + const { data: currentDialog } = useFetchDialog(); + const { createConversationBeforeUploadDocument } = + useCreateConversationBeforeUploadDocument(); + const { conversationId } = useGetChatSearchParams(); + const { data: conversation } = useFetchConversation(); + const disabled = useGetSendButtonDisabled(); + const sendDisabled = useSendButtonDisabled(value); -export function ChatBox() { return ( -
- +
+
+
+ {derivedMessages?.map((message, i) => { + return ( + + ); + })} +
+
+
+
); } diff --git a/web/src/pages/next-chats/chat/index.tsx b/web/src/pages/next-chats/chat/index.tsx index 7211d30db..130cf4301 100644 --- a/web/src/pages/next-chats/chat/index.tsx +++ b/web/src/pages/next-chats/chat/index.tsx @@ -10,7 +10,7 @@ import { import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useFetchDialog } from '@/hooks/use-chat-request'; import { useTranslation } from 'react-i18next'; -import { AppSettings } from './app-settings'; +import { useHandleClickConversationCard } from '../hooks/use-click-card'; import { ChatBox } from './chat-box'; import { Sessions } from './sessions'; @@ -18,6 +18,8 @@ export default function Chat() { const { navigateToChatList } = useNavigatePage(); const { data } = useFetchDialog(); const { t } = useTranslation(); + const { handleConversationCardClick, controller } = + useHandleClickConversationCard(); return (
@@ -36,10 +38,11 @@ export default function Chat() { -
- - - +
+ +
); diff --git a/web/src/pages/next-chats/chat/sessions.tsx b/web/src/pages/next-chats/chat/sessions.tsx index 6e7013dfa..1be31e771 100644 --- a/web/src/pages/next-chats/chat/sessions.tsx +++ b/web/src/pages/next-chats/chat/sessions.tsx @@ -1,37 +1,60 @@ import { MoreButton } from '@/components/more-button'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; -import { useFetchConversationList } from '@/hooks/use-chat-request'; +import { useGetChatSearchParams } from '@/hooks/use-chat-request'; +import { cn } from '@/lib/utils'; import { Plus } from 'lucide-react'; +import { useCallback } from 'react'; +import { useHandleClickConversationCard } from '../hooks/use-click-card'; +import { useSelectDerivedConversationList } from '../hooks/use-select-conversation-list'; +import { ChatSettingSheet } from './app-settings/chat-settings-sheet'; -function SessionCard() { - return ( - - - xxx - - - +type SessionProps = Pick< + ReturnType, + 'handleConversationCardClick' +>; +export function Sessions({ handleConversationCardClick }: SessionProps) { + const { list: conversationList, addTemporaryConversation } = + useSelectDerivedConversationList(); + + const handleCardClick = useCallback( + (conversationId: string, isNew: boolean) => () => { + handleConversationCardClick(conversationId, isNew); + }, + [handleConversationCardClick], ); -} -export function Sessions() { - const sessionList = new Array(10).fill(1); - const {} = useFetchConversationList(); + const { conversationId } = useGetChatSearchParams(); return ( -
+
Conversations -
-
- {sessionList.map((x) => ( - +
+ {conversationList.map((x) => ( + + + {x.name} + + + ))}
+
+ + + +
); } diff --git a/web/src/pages/next-chats/hooks/use-button-disabled.tsx b/web/src/pages/next-chats/hooks/use-button-disabled.tsx new file mode 100644 index 000000000..50bad8867 --- /dev/null +++ b/web/src/pages/next-chats/hooks/use-button-disabled.tsx @@ -0,0 +1,14 @@ +import { useGetChatSearchParams } from '@/hooks/use-chat-request'; +import { trim } from 'lodash'; +import { useParams } from 'umi'; + +export const useGetSendButtonDisabled = () => { + const { conversationId } = useGetChatSearchParams(); + const { id: dialogId } = useParams(); + + return dialogId === '' || conversationId === ''; +}; + +export const useSendButtonDisabled = (value: string) => { + return trim(value) === ''; +}; diff --git a/web/src/pages/next-chats/hooks/use-click-card.ts b/web/src/pages/next-chats/hooks/use-click-card.ts new file mode 100644 index 000000000..9d7a65f7c --- /dev/null +++ b/web/src/pages/next-chats/hooks/use-click-card.ts @@ -0,0 +1,20 @@ +import { useClickConversationCard } from '@/hooks/use-chat-request'; +import { useCallback, useState } from 'react'; + +export function useHandleClickConversationCard() { + const [controller, setController] = useState(new AbortController()); + const { handleClickConversation } = useClickConversationCard(); + + const handleConversationCardClick = useCallback( + (conversationId: string, isNew: boolean) => { + handleClickConversation(conversationId, isNew ? 'true' : ''); + setController((pre) => { + pre.abort(); + return new AbortController(); + }); + }, + [handleClickConversation], + ); + + return { controller, handleConversationCardClick }; +} diff --git a/web/src/pages/next-chats/hooks/use-create-conversation.ts b/web/src/pages/next-chats/hooks/use-create-conversation.ts new file mode 100644 index 000000000..b27ec486d --- /dev/null +++ b/web/src/pages/next-chats/hooks/use-create-conversation.ts @@ -0,0 +1,29 @@ +import { useGetChatSearchParams } from '@/hooks/use-chat-request'; +import { useCallback } from 'react'; +import { + useSetChatRouteParams, + useSetConversation, +} from './use-send-chat-message'; + +export const useCreateConversationBeforeUploadDocument = () => { + const { setConversation } = useSetConversation(); + const { dialogId } = useGetChatSearchParams(); + const { getConversationIsNew } = useSetChatRouteParams(); + + const createConversationBeforeUploadDocument = useCallback( + async (message: string) => { + const isNew = getConversationIsNew(); + if (isNew === 'true') { + const data = await setConversation(message, true); + + return data; + } + }, + [setConversation, getConversationIsNew], + ); + + return { + createConversationBeforeUploadDocument, + dialogId, + }; +}; diff --git a/web/src/pages/next-chats/hooks/use-select-conversation-list.ts b/web/src/pages/next-chats/hooks/use-select-conversation-list.ts new file mode 100644 index 000000000..26b165d52 --- /dev/null +++ b/web/src/pages/next-chats/hooks/use-select-conversation-list.ts @@ -0,0 +1,85 @@ +import { ChatSearchParams, MessageType } from '@/constants/chat'; +import { useTranslate } from '@/hooks/common-hooks'; +import { + useFetchConversationList, + useFetchDialogList, +} from '@/hooks/use-chat-request'; +import { IConversation } from '@/interfaces/database/chat'; +import { getConversationId } from '@/utils/chat'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useParams, useSearchParams } from 'umi'; + +export const useFindPrologueFromDialogList = () => { + const { id: dialogId } = useParams(); + const { data } = useFetchDialogList(); + + const prologue = useMemo(() => { + return data.dialogs.find((x) => x.id === dialogId)?.prompt_config.prologue; + }, [dialogId, data]); + + return prologue; +}; + +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 useSelectDerivedConversationList = () => { + const { t } = useTranslate('chat'); + + const [list, setList] = useState>([]); + const { data: conversationList, loading } = useFetchConversationList(); + const { id: dialogId } = useParams(); + const { setNewConversationRouteParams } = useSetNewConversationRouteParams(); + const prologue = useFindPrologueFromDialogList(); + + const addTemporaryConversation = useCallback(() => { + const conversationId = getConversationId(); + setList((pre) => { + if (dialogId) { + setNewConversationRouteParams(conversationId, 'true'); + const nextList = [ + { + id: conversationId, + name: t('newConversation'), + dialog_id: dialogId, + is_new: true, + message: [ + { + content: prologue, + role: MessageType.Assistant, + }, + ], + } as any, + ...conversationList, + ]; + return nextList; + } + + return pre; + }); + }, [conversationList, dialogId, prologue, t, setNewConversationRouteParams]); + + // When you first enter the page, select the top conversation card + + useEffect(() => { + setList([...conversationList]); + }, [conversationList]); + + return { list, addTemporaryConversation, loading }; +}; diff --git a/web/src/pages/next-chats/hooks/use-send-chat-message.ts b/web/src/pages/next-chats/hooks/use-send-chat-message.ts new file mode 100644 index 000000000..534120b6d --- /dev/null +++ b/web/src/pages/next-chats/hooks/use-send-chat-message.ts @@ -0,0 +1,279 @@ +import { ChatSearchParams, MessageType } from '@/constants/chat'; +import { + useHandleMessageInputChange, + useRegenerateMessage, + useSelectDerivedMessages, + useSendMessageWithSse, +} from '@/hooks/logic-hooks'; +import { + useFetchConversation, + useGetChatSearchParams, + useUpdateConversation, +} from '@/hooks/use-chat-request'; +import { Message } from '@/interfaces/database/chat'; +import api from '@/utils/api'; +import { trim } from 'lodash'; +import { useCallback, useEffect, useMemo } from 'react'; +import { useParams, useSearchParams } from 'umi'; +import { v4 as uuid } from 'uuid'; +import { IMessage } from '../chat/interface'; +import { useFindPrologueFromDialogList } from './use-select-conversation-list'; + +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 useSelectNextMessages = () => { + const { + scrollRef, + messageContainerRef, + setDerivedMessages, + derivedMessages, + addNewestAnswer, + addNewestQuestion, + removeLatestMessage, + removeMessageById, + removeMessagesAfterCurrentMessage, + } = useSelectDerivedMessages(); + const { data: conversation, loading } = useFetchConversation(); + const { conversationId, isNew } = useGetChatSearchParams(); + const { id: dialogId } = useParams(); + const prologue = useFindPrologueFromDialogList(); + + const addPrologue = useCallback(() => { + if (dialogId !== '' && isNew === 'true') { + const nextMessage = { + role: MessageType.Assistant, + content: prologue, + id: uuid(), + } as IMessage; + + setDerivedMessages([nextMessage]); + } + }, [dialogId, isNew, prologue, setDerivedMessages]); + + useEffect(() => { + addPrologue(); + }, [addPrologue]); + + useEffect(() => { + if ( + conversationId && + isNew !== 'true' && + conversation.message?.length > 0 + ) { + setDerivedMessages(conversation.message); + } + + if (!conversationId) { + setDerivedMessages([]); + } + }, [conversation.message, conversationId, setDerivedMessages, isNew]); + + return { + scrollRef, + messageContainerRef, + derivedMessages, + loading, + addNewestAnswer, + addNewestQuestion, + removeLatestMessage, + removeMessageById, + removeMessagesAfterCurrentMessage, + }; +}; + +export const useSetConversation = () => { + const { id: dialogId } = useParams(); + const { updateConversation } = useUpdateConversation(); + + const setConversation = useCallback( + async ( + message: string, + isNew: boolean = false, + conversationId?: string, + ) => { + const data = await updateConversation({ + dialog_id: dialogId, + name: message, + is_new: isNew, + conversation_id: conversationId, + message: [ + { + role: MessageType.Assistant, + content: message, + }, + ], + }); + + return data; + }, + [updateConversation, dialogId], + ); + + return { setConversation }; +}; + +export const useSendMessage = (controller: AbortController) => { + const { setConversation } = useSetConversation(); + const { conversationId, isNew } = useGetChatSearchParams(); + const { handleInputChange, value, setValue } = useHandleMessageInputChange(); + + const { send, answer, done } = useSendMessageWithSse( + api.completeConversation, + ); + const { + scrollRef, + messageContainerRef, + derivedMessages, + loading, + addNewestAnswer, + addNewestQuestion, + removeLatestMessage, + removeMessageById, + removeMessagesAfterCurrentMessage, + } = useSelectNextMessages(); + const { setConversationIsNew, getConversationIsNew } = + useSetChatRouteParams(); + + const stopOutputMessage = useCallback(() => { + controller.abort(); + }, [controller]); + + const sendMessage = useCallback( + async ({ + message, + currentConversationId, + messages, + }: { + message: Message; + currentConversationId?: string; + messages?: Message[]; + }) => { + const res = await send( + { + conversation_id: currentConversationId ?? conversationId, + messages: [...(messages ?? derivedMessages ?? []), message], + }, + controller, + ); + + if (res && (res?.response.status !== 200 || res?.data?.code !== 0)) { + // cancel loading + setValue(message.content); + console.info('removeLatestMessage111'); + removeLatestMessage(); + } + }, + [ + derivedMessages, + conversationId, + removeLatestMessage, + setValue, + send, + controller, + ], + ); + + const handleSendMessage = useCallback( + async (message: Message) => { + const isNew = getConversationIsNew(); + if (isNew !== 'true') { + sendMessage({ message }); + } else { + const data = await setConversation( + message.content, + true, + conversationId, + ); + if (data.code === 0) { + setConversationIsNew(''); + const id = data.data.id; + // currentConversationIdRef.current = id; + sendMessage({ + message, + currentConversationId: id, + messages: data.data.message, + }); + } + } + }, + [ + setConversation, + sendMessage, + setConversationIsNew, + getConversationIsNew, + conversationId, + ], + ); + + const { regenerateMessage } = useRegenerateMessage({ + removeMessagesAfterCurrentMessage, + sendMessage, + messages: derivedMessages, + }); + + useEffect(() => { + // #1289 + if (answer.answer && conversationId && isNew !== 'true') { + addNewestAnswer(answer); + } + }, [answer, addNewestAnswer, conversationId, isNew]); + + const handlePressEnter = useCallback( + (documentIds: string[]) => { + if (trim(value) === '') return; + const id = uuid(); + + addNewestQuestion({ + content: value, + doc_ids: documentIds, + id, + role: MessageType.User, + }); + if (done) { + setValue(''); + handleSendMessage({ + id, + content: value.trim(), + role: MessageType.User, + doc_ids: documentIds, + }); + } + }, + [addNewestQuestion, handleSendMessage, done, setValue, value], + ); + + return { + handlePressEnter, + handleInputChange, + value, + setValue, + regenerateMessage, + sendLoading: !done, + loading, + scrollRef, + messageContainerRef, + derivedMessages, + removeMessageById, + stopOutputMessage, + }; +};