瀏覽代碼

Feat: Render chat page #3221 (#9298)

### What problem does this PR solve?

Feat: Render chat page #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
tags/v0.20.1
balibabu 2 月之前
父節點
當前提交
f0c34d4454
No account linked to committer's email address

+ 8
- 3
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 ?? {};


+ 1
- 1
web/src/pages/next-chats/chat-card.tsx 查看文件

@@ -31,7 +31,7 @@ export function ChatCard({ data, showChatRenameModal }: IProps) {
</section>
<div className="flex justify-between items-end">
<div className="w-full">
<h3 className="text-lg font-semibold mb-2 line-clamp-1">
<h3 className="text-lg font-semibold mb-2 line-clamp-1 truncate">
{data.name}
</h3>
<p className="text-xs text-text-sub-title">{data.description}</p>

+ 23
- 0
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 (
<Sheet>
<SheetTrigger asChild>{children}</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Chat Settings</SheetTitle>
</SheetHeader>
<ChatSettings></ChatSettings>
</SheetContent>
</Sheet>
);
}

web/src/pages/next-chats/chat/app-settings/index.tsx → 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<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
@@ -32,27 +33,19 @@ export function AppSettings() {
}

return (
<section className="py-6 w-[500px] max-w-[25%] ">
<div className="text-2xl font-bold mb-4 text-colors-text-neutral-strong px-6">
App settings
</div>
<div className="overflow-auto max-h-[81vh] px-6 ">
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<ChatBasicSetting></ChatBasicSetting>
<ChatPromptEngine></ChatPromptEngine>
<ChatModelSettings></ChatModelSettings>
</form>
</FormProvider>
</div>
<div className="p-6 text-center">
<p className="text-colors-text-neutral-weak mb-1">
There are unsaved changes
</p>
<Button variant={'tertiary'} className="w-full">
Update
</Button>
</div>
<section className="py-6">
<FormProvider {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 overflow-auto max-h-[88vh] pr-4"
>
<ChatBasicSetting></ChatBasicSetting>
<ChatPromptEngine></ChatPromptEngine>
<ChatModelSettings></ChatModelSettings>
</form>
</FormProvider>

<Button className="w-full my-4">Update</Button>
</section>
);
}

+ 90
- 4
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 (
<section className="border-x flex-1">
<ChatInput></ChatInput>
<section className="border-x flex flex-col p-5 w-full">
<div ref={messageContainerRef} className="flex-1 overflow-auto">
<div className="w-full">
{derivedMessages?.map((message, i) => {
return (
<MessageItem
loading={
message.role === MessageType.Assistant &&
sendLoading &&
derivedMessages.length - 1 === i
}
key={buildMessageUuidWithRole(message)}
item={message}
nickname={userInfo.nickname}
avatar={userInfo.avatar}
avatarDialog={currentDialog.icon}
reference={buildMessageItemReference(
{
message: derivedMessages,
reference: conversation.reference,
},
message,
)}
// clickDocumentButton={clickDocumentButton}
index={i}
removeMessageById={removeMessageById}
regenerateMessage={regenerateMessage}
sendLoading={sendLoading}
></MessageItem>
);
})}
</div>
<div ref={scrollRef} />
</div>
<NextMessageInput
disabled={disabled}
sendDisabled={sendDisabled}
sendLoading={sendLoading}
value={value}
onInputChange={handleInputChange}
onPressEnter={handlePressEnter}
conversationId={conversationId}
createConversationBeforeUploadDocument={
createConversationBeforeUploadDocument
}
stopOutputMessage={stopOutputMessage}
/>
</section>
);
}

+ 8
- 5
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 (
<section className="h-full flex flex-col">
@@ -36,10 +38,11 @@ export default function Chat() {
</BreadcrumbList>
</Breadcrumb>
</PageHeader>
<div className="flex flex-1">
<Sessions></Sessions>
<ChatBox></ChatBox>
<AppSettings></AppSettings>
<div className="flex flex-1 min-h-0">
<Sessions
handleConversationCardClick={handleConversationCardClick}
></Sessions>
<ChatBox controller={controller}></ChatBox>
</div>
</section>
);

+ 41
- 18
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 (
<Card>
<CardContent className="px-3 py-2 flex justify-between items-center group">
xxx
<MoreButton></MoreButton>
</CardContent>
</Card>
type SessionProps = Pick<
ReturnType<typeof useHandleClickConversationCard>,
'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 (
<section className="p-6 w-[400px] max-w-[20%]">
<section className="p-6 w-[400px] max-w-[20%] flex flex-col">
<div className="flex justify-between items-center mb-4">
<span className="text-xl font-bold">Conversations</span>
<Button variant={'ghost'}>
<Button variant={'ghost'} onClick={addTemporaryConversation}>
<Plus></Plus>
</Button>
</div>
<div className="space-y-4">
{sessionList.map((x) => (
<SessionCard key={x}></SessionCard>
<div className="space-y-4 flex-1 overflow-auto">
{conversationList.map((x) => (
<Card
key={x.id}
onClick={handleCardClick(x.id, x.is_new)}
className={cn('cursor-pointer bg-transparent', {
'bg-background-card': conversationId === x.id,
})}
>
<CardContent className="px-3 py-2 flex justify-between items-center group">
{x.name}
<MoreButton></MoreButton>
</CardContent>
</Card>
))}
</div>
<div className="py-2">
<ChatSettingSheet>
<Button className="w-full">Chat Settings</Button>
</ChatSettingSheet>
</div>
</section>
);
}

+ 14
- 0
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) === '';
};

+ 20
- 0
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 };
}

+ 29
- 0
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,
};
};

+ 85
- 0
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<Array<IConversation>>([]);
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 };
};

+ 279
- 0
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,
};
};

Loading…
取消
儲存