feat: add temporary conversation

This commit is contained in:
billchen 2024-02-26 11:51:35 +08:00
parent 17d751d2d1
commit bc16879a10
7 changed files with 220 additions and 110 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -1,11 +1,14 @@
.chatContainer { .chatContainer {
padding: 0 24px 24px; padding: 0 24px 24px;
.messageContainer {
overflow-y: auto;
}
} }
.messageItem { .messageItem {
.messageItemContent { .messageItemContent {
display: inline-block; display: inline-block;
width: 300px; width: 400px;
} }
} }

View file

@ -1,4 +1,4 @@
import { Button, Flex, Input, Typography } from 'antd'; import { Button, Flex, Input } from 'antd';
import { ChangeEventHandler, useState } from 'react'; import { ChangeEventHandler, useState } from 'react';
import { Message } from '@/interfaces/database/chat'; import { Message } from '@/interfaces/database/chat';
@ -9,8 +9,6 @@ import { MessageType } from '@/constants/chat';
import { IClientConversation } from '../interface'; import { IClientConversation } from '../interface';
import styles from './index.less'; import styles from './index.less';
const { Paragraph } = Typography;
const MessageItem = ({ item }: { item: Message }) => { const MessageItem = ({ item }: { item: Message }) => {
return ( return (
<div <div
@ -20,9 +18,7 @@ const MessageItem = ({ item }: { item: Message }) => {
})} })}
> >
<span className={styles.messageItemContent}> <span className={styles.messageItemContent}>
<Paragraph ellipsis={{ tooltip: item.content, rows: 3 }}> <div>{item.content}</div>
{item.content}
</Paragraph>
</span> </span>
</div> </div>
); );
@ -44,10 +40,12 @@ const ChatContainer = () => {
return ( return (
<Flex flex={1} className={styles.chatContainer} vertical> <Flex flex={1} className={styles.chatContainer} vertical>
<Flex flex={1} vertical> <Flex flex={1} vertical className={styles.messageContainer}>
{conversation?.message?.map((message) => ( <div>
<MessageItem key={message.id} item={message}></MessageItem> {conversation?.message?.map((message) => (
))} <MessageItem key={message.id} item={message}></MessageItem>
))}
</div>
</Flex> </Flex>
<Input <Input
size="large" size="large"

View file

@ -1,8 +1,8 @@
import showDeleteConfirm from '@/components/deleting-confirm'; import showDeleteConfirm from '@/components/deleting-confirm';
import { MessageType } from '@/constants/chat'; import { MessageType } from '@/constants/chat';
import { IDialog } from '@/interfaces/database/chat'; import { IConversation, IDialog } from '@/interfaces/database/chat';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSearchParams, useSelector } from 'umi'; import { useDispatch, useSearchParams, useSelector } from 'umi';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { ChatSearchParams, EmptyConversationId } from './constants'; import { ChatSearchParams, EmptyConversationId } from './constants';
@ -11,6 +11,8 @@ import {
IMessage, IMessage,
VariableTableDataType, VariableTableDataType,
} from './interface'; } from './interface';
import { ChatModelState } from './model';
import { isConversationIdNotExist } from './utils';
export const useFetchDialogList = () => { export const useFetchDialogList = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -137,24 +139,6 @@ export const useRemoveDialog = () => {
return { onRemoveDialog }; return { onRemoveDialog };
}; };
export const useClickDialogCard = () => {
const [currentQueryParameters, setSearchParams] = useSearchParams();
const newQueryParameters: URLSearchParams = useMemo(() => {
return new URLSearchParams(currentQueryParameters.toString());
}, [currentQueryParameters]);
const handleClickDialog = useCallback(
(dialogId: string) => {
newQueryParameters.set(ChatSearchParams.DialogId, dialogId);
setSearchParams(newQueryParameters);
},
[newQueryParameters, setSearchParams],
);
return { handleClickDialog };
};
export const useGetChatSearchParams = () => { export const useGetChatSearchParams = () => {
const [currentQueryParameters] = useSearchParams(); const [currentQueryParameters] = useSearchParams();
@ -165,6 +149,44 @@ export const useGetChatSearchParams = () => {
}; };
}; };
export const useSetCurrentConversation = () => {
const dispatch = useDispatch();
const setCurrentConversation = useCallback(
(currentConversation: IClientConversation) => {
dispatch({
type: 'chatModel/setCurrentConversation',
payload: currentConversation,
});
},
[dispatch],
);
return setCurrentConversation;
};
export const useClickDialogCard = () => {
const [currentQueryParameters, setSearchParams] = useSearchParams();
const newQueryParameters: URLSearchParams = useMemo(() => {
return new URLSearchParams();
}, []);
const handleClickDialog = useCallback(
(dialogId: string) => {
newQueryParameters.set(ChatSearchParams.DialogId, dialogId);
// newQueryParameters.set(
// ChatSearchParams.ConversationId,
// EmptyConversationId,
// );
setSearchParams(newQueryParameters);
},
[newQueryParameters, setSearchParams],
);
return { handleClickDialog };
};
export const useSelectFirstDialogOnMount = () => { export const useSelectFirstDialogOnMount = () => {
const dialogList = useFetchDialogList(); const dialogList = useFetchDialogList();
const { dialogId } = useGetChatSearchParams(); const { dialogId } = useGetChatSearchParams();
@ -182,13 +204,83 @@ export const useSelectFirstDialogOnMount = () => {
//#region conversation //#region conversation
export const useFetchConversationList = (dialogId?: string) => { export const useCreateTemporaryConversation = () => {
const dispatch = useDispatch();
const { dialogId } = useGetChatSearchParams();
const { handleClickConversation } = useClickConversationCard();
let chatModel = useSelector((state: any) => state.chatModel);
const currentConversation: Pick<
IClientConversation,
'id' | 'message' | 'name' | 'dialog_id'
> = chatModel.currentConversation;
const conversationList: IClientConversation[] = chatModel.conversationList;
const currentDialog: IDialog = chatModel.currentDialog;
const setCurrentConversation = useSetCurrentConversation();
const createTemporaryConversation = useCallback(() => {
const firstConversation = conversationList[0];
const messages = [...(firstConversation?.message ?? [])];
if (messages.some((x) => x.id === EmptyConversationId)) {
return;
}
messages.push({
id: EmptyConversationId,
content: currentDialog?.prompt_config?.prologue ?? '',
role: MessageType.Assistant,
});
let nextCurrentConversation = currentConversation;
// Its the back-end data.
if ('id' in currentConversation) {
nextCurrentConversation = { ...currentConversation, message: messages };
} else {
// client data
nextCurrentConversation = {
id: EmptyConversationId,
name: 'New conversation',
dialog_id: dialogId,
message: messages,
};
}
const nextConversationList = [...conversationList];
nextConversationList.unshift(
nextCurrentConversation as IClientConversation,
);
setCurrentConversation(nextCurrentConversation as IClientConversation);
dispatch({
type: 'chatModel/setConversationList',
payload: nextConversationList,
});
handleClickConversation(EmptyConversationId);
}, [
dispatch,
currentConversation,
dialogId,
setCurrentConversation,
handleClickConversation,
conversationList,
currentDialog,
]);
return { createTemporaryConversation };
};
export const useFetchConversationList = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const conversationList: any[] = useSelector( const conversationList: any[] = useSelector(
(state: any) => state.chatModel.conversationList, (state: any) => state.chatModel.conversationList,
); );
const { dialogId } = useGetChatSearchParams();
const fetchConversationList = useCallback(() => { const fetchConversationList = useCallback(async () => {
if (dialogId) { if (dialogId) {
dispatch({ dispatch({
type: 'chatModel/listConversation', type: 'chatModel/listConversation',
@ -204,72 +296,56 @@ export const useFetchConversationList = (dialogId?: string) => {
return conversationList; return conversationList;
}; };
export const useClickConversationCard = () => { export const useSelectConversationList = () => {
const [currentQueryParameters, setSearchParams] = useSearchParams(); const [list, setList] = useState<Array<IConversation>>([]);
const newQueryParameters: URLSearchParams = new URLSearchParams( let chatModel: ChatModelState = useSelector((state: any) => state.chatModel);
currentQueryParameters.toString(), const { conversationList, currentDialog } = chatModel;
); const { dialogId } = useGetChatSearchParams();
const prologue = currentDialog?.prompt_config?.prologue ?? '';
const handleClickConversation = (conversationId: string) => { const addTemporaryConversation = useCallback(() => {
newQueryParameters.set(ChatSearchParams.ConversationId, conversationId); setList(() => {
setSearchParams(newQueryParameters); const nextList = [
}; {
id: '',
name: 'New conversation',
dialog_id: dialogId,
message: [
{
content: prologue,
role: MessageType.Assistant,
},
],
} as IConversation,
...conversationList,
];
return nextList;
});
}, [conversationList, dialogId, prologue]);
return { handleClickConversation }; useEffect(() => {
addTemporaryConversation();
}, [addTemporaryConversation]);
return { list, addTemporaryConversation };
}; };
export const useCreateTemporaryConversation = () => { export const useClickConversationCard = () => {
const dispatch = useDispatch(); const [currentQueryParameters, setSearchParams] = useSearchParams();
const { dialogId } = useGetChatSearchParams(); const newQueryParameters: URLSearchParams = useMemo(
const { handleClickConversation } = useClickConversationCard(); () => new URLSearchParams(currentQueryParameters.toString()),
let chatModel = useSelector((state: any) => state.chatModel); [currentQueryParameters],
let currentConversation: Pick< );
IClientConversation,
'id' | 'message' | 'name' | 'dialog_id'
> = chatModel.currentConversation;
let conversationList: IClientConversation[] = chatModel.conversationList;
const createTemporaryConversation = (message: string) => { const handleClickConversation = useCallback(
const messages = [...(currentConversation?.message ?? [])]; (conversationId: string) => {
if (messages.some((x) => x.id === EmptyConversationId)) { newQueryParameters.set(ChatSearchParams.ConversationId, conversationId);
return; setSearchParams(newQueryParameters);
} },
messages.unshift({ [newQueryParameters, setSearchParams],
id: EmptyConversationId, );
content: message,
role: MessageType.Assistant,
});
// Its the back-end data. return { handleClickConversation };
if ('id' in currentConversation) {
currentConversation = { ...currentConversation, message: messages };
} else {
// client data
currentConversation = {
id: EmptyConversationId,
name: 'New conversation',
dialog_id: dialogId,
message: messages,
};
}
const nextConversationList = [...conversationList];
nextConversationList.push(currentConversation as IClientConversation);
dispatch({
type: 'chatModel/setCurrentConversation',
payload: currentConversation,
});
dispatch({
type: 'chatModel/setConversationList',
payload: nextConversationList,
});
handleClickConversation(EmptyConversationId);
};
return { createTemporaryConversation };
}; };
export const useSetConversation = () => { export const useSetConversation = () => {
@ -302,17 +378,20 @@ export const useFetchConversation = () => {
const conversation = useSelector( const conversation = useSelector(
(state: any) => state.chatModel.currentConversation, (state: any) => state.chatModel.currentConversation,
); );
const setCurrentConversation = useSetCurrentConversation();
const fetchConversation = useCallback(() => { const fetchConversation = useCallback(() => {
if (conversationId !== EmptyConversationId && conversationId !== '') { if (isConversationIdNotExist(conversationId)) {
dispatch({ dispatch<any>({
type: 'chatModel/getConversation', type: 'chatModel/getConversation',
payload: { payload: {
conversation_id: conversationId, conversation_id: conversationId,
}, },
}); });
} else {
setCurrentConversation({} as IClientConversation);
} }
}, [dispatch, conversationId]); }, [dispatch, conversationId, setCurrentConversation]);
useEffect(() => { useEffect(() => {
fetchConversation(); fetchConversation();

View file

@ -18,11 +18,11 @@ import ChatContainer from './chat-container';
import { import {
useClickConversationCard, useClickConversationCard,
useClickDialogCard, useClickDialogCard,
useCreateTemporaryConversation,
useFetchConversationList, useFetchConversationList,
useFetchDialog, useFetchDialog,
useGetChatSearchParams, useGetChatSearchParams,
useRemoveDialog, useRemoveDialog,
useSelectConversationList,
useSelectFirstDialogOnMount, useSelectFirstDialogOnMount,
useSetCurrentDialog, useSetCurrentDialog,
} from './hooks'; } from './hooks';
@ -38,12 +38,10 @@ const Chat = () => {
const { handleClickDialog } = useClickDialogCard(); const { handleClickDialog } = useClickDialogCard();
const { handleClickConversation } = useClickConversationCard(); const { handleClickConversation } = useClickConversationCard();
const { dialogId, conversationId } = useGetChatSearchParams(); const { dialogId, conversationId } = useGetChatSearchParams();
const list = useFetchConversationList(dialogId); const { list: conversationList, addTemporaryConversation } =
const { createTemporaryConversation } = useCreateTemporaryConversation(); useSelectConversationList();
const selectedDialog = useFetchDialog(dialogId, true); useFetchDialog(dialogId, true);
const prologue = selectedDialog?.prompt_config?.prologue || '';
const handleAppCardEnter = (id: string) => () => { const handleAppCardEnter = (id: string) => () => {
setActivated(id); setActivated(id);
@ -69,8 +67,8 @@ const Chat = () => {
}; };
const handleCreateTemporaryConversation = useCallback(() => { const handleCreateTemporaryConversation = useCallback(() => {
createTemporaryConversation(prologue); addTemporaryConversation();
}, [createTemporaryConversation, prologue]); }, [addTemporaryConversation]);
const items: MenuProps['items'] = [ const items: MenuProps['items'] = [
{ {
@ -112,6 +110,8 @@ const Chat = () => {
return appItems; return appItems;
}; };
useFetchConversationList();
return ( return (
<Flex className={styles.chatWrapper}> <Flex className={styles.chatWrapper}>
<Flex className={styles.chatAppWrapper}> <Flex className={styles.chatAppWrapper}>
@ -171,7 +171,7 @@ const Chat = () => {
</Flex> </Flex>
<Divider></Divider> <Divider></Divider>
<Flex vertical gap={10} className={styles.chatTitleContent}> <Flex vertical gap={10} className={styles.chatTitleContent}>
{list.map((x) => ( {conversationList.map((x) => (
<Card <Card
key={x.id} key={x.id}
hoverable hoverable

View file

@ -48,10 +48,11 @@ const model: DvaModel<ChatModelState> = {
}; };
}, },
setCurrentConversation(state, { payload }) { setCurrentConversation(state, { payload }) {
const messageList = payload?.message.map((x: Message | IMessage) => ({ const messageList =
...x, payload?.message?.map((x: Message | IMessage) => ({
id: 'id' in x ? x.id : uuid(), ...x,
})); id: 'id' in x ? x.id : uuid(),
})) ?? [];
return { return {
...state, ...state,
currentConversation: { ...payload, message: messageList }, currentConversation: { ...payload, message: messageList },

View file

@ -1,4 +1,4 @@
import { variableEnabledFieldMap } from './constants'; import { EmptyConversationId, variableEnabledFieldMap } from './constants';
export const excludeUnEnabledVariables = (values: any) => { export const excludeUnEnabledVariables = (values: any) => {
const unEnabledFields: Array<keyof typeof variableEnabledFieldMap> = const unEnabledFields: Array<keyof typeof variableEnabledFieldMap> =
@ -10,3 +10,7 @@ export const excludeUnEnabledVariables = (values: any) => {
(key) => `llm_setting.${variableEnabledFieldMap[key]}`, (key) => `llm_setting.${variableEnabledFieldMap[key]}`,
); );
}; };
export const isConversationIdNotExist = (conversationId: string) => {
return conversationId !== EmptyConversationId && conversationId !== '';
};