From 5a0f1d2b844510c8f41001e1bb96de4f78126587 Mon Sep 17 00:00:00 2001 From: balibabu <cike8899@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:14:25 +0800 Subject: [PATCH] feat: fetch conversation and delete chat dialog (#69) * feat: set chat configuration to backend * feat: exclude unEnabled variables * feat: delete chat dialog * feat: fetch conversation --- web/src/constants/chat.ts | 4 + web/src/hooks/commonHooks.ts | 34 ++ web/src/hooks/knowledgeHook.ts | 11 +- web/src/interfaces/database/chat.ts | 19 + .../parsing-status-cell/index.tsx | 4 +- .../assistant-setting.tsx | 6 +- .../chat/chat-configuration-modal/index.tsx | 49 ++- .../chat-configuration-modal/interface.ts | 14 - .../model-setting.tsx | 4 +- .../prompt-engine.tsx | 32 +- web/src/pages/chat/chat-container/index.less | 15 + web/src/pages/chat/chat-container/index.tsx | 36 +- .../constants.ts | 7 + web/src/pages/chat/hooks.ts | 342 +++++++++++++++++- web/src/pages/chat/index.less | 23 ++ web/src/pages/chat/index.tsx | 164 ++++++--- web/src/pages/chat/interface.ts | 31 ++ web/src/pages/chat/model.ts | 83 ++++- web/src/pages/chat/utils.ts | 12 + web/src/pages/knowledge/model.ts | 2 +- web/src/services/chatService.ts | 5 + web/src/utils/api.ts | 1 + 22 files changed, 783 insertions(+), 115 deletions(-) create mode 100644 web/src/constants/chat.ts create mode 100644 web/src/hooks/commonHooks.ts delete mode 100644 web/src/pages/chat/chat-configuration-modal/interface.ts rename web/src/pages/chat/{chat-configuration-modal => }/constants.ts (62%) create mode 100644 web/src/pages/chat/interface.ts create mode 100644 web/src/pages/chat/utils.ts diff --git a/web/src/constants/chat.ts b/web/src/constants/chat.ts new file mode 100644 index 0000000..2ce95b5 --- /dev/null +++ b/web/src/constants/chat.ts @@ -0,0 +1,4 @@ +export enum MessageType { + Assistant = 'assistant', + User = 'user', +} diff --git a/web/src/hooks/commonHooks.ts b/web/src/hooks/commonHooks.ts new file mode 100644 index 0000000..e4361cd --- /dev/null +++ b/web/src/hooks/commonHooks.ts @@ -0,0 +1,34 @@ +import isEqual from 'lodash/isEqual'; +import { useEffect, useRef, useState } from 'react'; + +export const useSetModalState = () => { + const [visible, setVisible] = useState(false); + + const showModal = () => { + setVisible(true); + }; + const hideModal = () => { + setVisible(false); + }; + + return { visible, showModal, hideModal }; +}; + +export const useDeepCompareEffect = ( + effect: React.EffectCallback, + deps: React.DependencyList, +) => { + const ref = useRef<React.DependencyList>(); + let callback: ReturnType<React.EffectCallback> = () => {}; + if (!isEqual(deps, ref.current)) { + callback = effect(); + ref.current = deps; + } + useEffect(() => { + return () => { + if (callback) { + callback(); + } + }; + }, []); +}; diff --git a/web/src/hooks/knowledgeHook.ts b/web/src/hooks/knowledgeHook.ts index ba090d9..9ad41f5 100644 --- a/web/src/hooks/knowledgeHook.ts +++ b/web/src/hooks/knowledgeHook.ts @@ -125,11 +125,18 @@ export const useFetchKnowledgeBaseConfiguration = () => { }, [fetchKnowledgeBaseConfiguration]); }; -export const useFetchKnowledgeList = (): IKnowledge[] => { +export const useFetchKnowledgeList = ( + shouldFilterListWithoutDocument: boolean = false, +): IKnowledge[] => { const dispatch = useDispatch(); const knowledgeModel = useSelector((state: any) => state.knowledgeModel); const { data = [] } = knowledgeModel; + const list = useMemo(() => { + return shouldFilterListWithoutDocument + ? data.filter((x: IKnowledge) => x.doc_num > 0) + : data; + }, [data, shouldFilterListWithoutDocument]); const fetchList = useCallback(() => { dispatch({ @@ -141,5 +148,5 @@ export const useFetchKnowledgeList = (): IKnowledge[] => { fetchList(); }, [fetchList]); - return data; + return list; }; diff --git a/web/src/interfaces/database/chat.ts b/web/src/interfaces/database/chat.ts index 6aec10a..f7c4a23 100644 --- a/web/src/interfaces/database/chat.ts +++ b/web/src/interfaces/database/chat.ts @@ -1,3 +1,5 @@ +import { MessageType } from '@/constants/chat'; + export interface PromptConfig { empty_response: string; parameters: Parameter[]; @@ -45,3 +47,20 @@ export interface IDialog { update_date: string; update_time: number; } + +export interface IConversation { + create_date: string; + create_time: number; + dialog_id: string; + id: string; + message: Message[]; + reference: any[]; + name: string; + update_date: string; + update_time: number; +} + +export interface Message { + content: string; + role: MessageType; +} diff --git a/web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.tsx b/web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.tsx index 5c0711a..22f840b 100644 --- a/web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-file/parsing-status-cell/index.tsx @@ -75,9 +75,7 @@ export const ParsingStatusCell = ({ record }: IProps) => { return ( <Flex justify={'space-between'}> - <Popover - content={isRunning && <PopoverContent record={record}></PopoverContent>} - > + <Popover content={<PopoverContent record={record}></PopoverContent>}> <Tag color={runningStatus.color}> {isRunning ? ( <Space> diff --git a/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx b/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx index 968d277..0ce8af7 100644 --- a/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx +++ b/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx @@ -1,15 +1,13 @@ import { Form, Input, Select } from 'antd'; import classNames from 'classnames'; -import { ISegmentedContentProps } from './interface'; +import { ISegmentedContentProps } from '../interface'; import { useFetchKnowledgeList } from '@/hooks/knowledgeHook'; import styles from './index.less'; -const { Option } = Select; - const AssistantSetting = ({ show }: ISegmentedContentProps) => { - const knowledgeList = useFetchKnowledgeList(); + const knowledgeList = useFetchKnowledgeList(true); const knowledgeOptions = knowledgeList.map((x) => ({ label: x.name, value: x.id, diff --git a/web/src/pages/chat/chat-configuration-modal/index.tsx b/web/src/pages/chat/chat-configuration-modal/index.tsx index 64943c9..3978961 100644 --- a/web/src/pages/chat/chat-configuration-modal/index.tsx +++ b/web/src/pages/chat/chat-configuration-modal/index.tsx @@ -3,13 +3,16 @@ import { IModalManagerChildrenProps } from '@/components/modal-manager'; import { Divider, Flex, Form, Modal, Segmented } from 'antd'; import { SegmentedValue } from 'antd/es/segmented'; import omit from 'lodash/omit'; -import { useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import AssistantSetting from './assistant-setting'; import ModelSetting from './model-setting'; import PromptEngine from './prompt-engine'; -import { useSetDialog } from '../hooks'; -import { variableEnabledFieldMap } from './constants'; +import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; +import { variableEnabledFieldMap } from '../constants'; +import { useFetchDialog, useResetCurrentDialog, useSetDialog } from '../hooks'; +import { IPromptConfigParameters } from '../interface'; +import { excludeUnEnabledVariables } from '../utils'; import styles from './index.less'; enum ConfigurationSegmented { @@ -40,32 +43,46 @@ const validateMessages = { }, }; -const ChatConfigurationModal = ({ - visible, - hideModal, -}: IModalManagerChildrenProps) => { +interface IProps extends IModalManagerChildrenProps { + id: string; +} + +const ChatConfigurationModal = ({ visible, hideModal, id }: IProps) => { const [form] = Form.useForm(); const [value, setValue] = useState<ConfigurationSegmented>( ConfigurationSegmented.AssistantSetting, ); - const promptEngineRef = useRef(null); + const promptEngineRef = useRef<Array<IPromptConfigParameters>>([]); + const loading = useOneNamespaceEffectsLoading('chatModel', ['setDialog']); const setDialog = useSetDialog(); + const currentDialog = useFetchDialog(id, visible); + const { resetCurrentDialog } = useResetCurrentDialog(); const handleOk = async () => { const values = await form.validateFields(); - const nextValues: any = omit(values, Object.keys(variableEnabledFieldMap)); + const nextValues: any = omit(values, [ + ...Object.keys(variableEnabledFieldMap), + 'parameters', + ...excludeUnEnabledVariables(values), + ]); + const emptyResponse = nextValues.prompt_config?.empty_response ?? ''; const finalValues = { + dialog_id: id, ...nextValues, prompt_config: { ...nextValues.prompt_config, parameters: promptEngineRef.current, + empty_response: emptyResponse, }, }; console.info(promptEngineRef.current); console.info(nextValues); console.info(finalValues); - setDialog(finalValues); + const retcode: number = await setDialog(finalValues); + if (retcode === 0) { + hideModal(); + } }; const handleCancel = () => { @@ -76,6 +93,11 @@ const ChatConfigurationModal = ({ setValue(val as ConfigurationSegmented); }; + const handleModalAfterClose = () => { + resetCurrentDialog(); + form.resetFields(); + }; + const title = ( <Flex gap={16}> <ChatConfigurationAtom></ChatConfigurationAtom> @@ -89,6 +111,10 @@ const ChatConfigurationModal = ({ </Flex> ); + useEffect(() => { + form.setFieldsValue(currentDialog); + }, [currentDialog, form]); + return ( <Modal title={title} @@ -96,6 +122,9 @@ const ChatConfigurationModal = ({ open={visible} onOk={handleOk} onCancel={handleCancel} + confirmLoading={loading} + destroyOnClose + afterClose={handleModalAfterClose} > <Segmented size={'large'} diff --git a/web/src/pages/chat/chat-configuration-modal/interface.ts b/web/src/pages/chat/chat-configuration-modal/interface.ts deleted file mode 100644 index 55b67c3..0000000 --- a/web/src/pages/chat/chat-configuration-modal/interface.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { FormInstance } from 'antd'; - -export interface ISegmentedContentProps { - show: boolean; - form: FormInstance; -} - -export interface IVariable { - temperature: number; - top_p: number; - frequency_penalty: number; - presence_penalty: number; - max_tokens: number; -} diff --git a/web/src/pages/chat/chat-configuration-modal/model-setting.tsx b/web/src/pages/chat/chat-configuration-modal/model-setting.tsx index 1c421fc..5f68a6d 100644 --- a/web/src/pages/chat/chat-configuration-modal/model-setting.tsx +++ b/web/src/pages/chat/chat-configuration-modal/model-setting.tsx @@ -6,10 +6,10 @@ import { import { Divider, Flex, Form, InputNumber, Select, Slider, Switch } from 'antd'; import classNames from 'classnames'; import { useEffect } from 'react'; -import { ISegmentedContentProps } from './interface'; +import { ISegmentedContentProps } from '../interface'; import { useFetchLlmList, useSelectLlmOptions } from '@/hooks/llmHooks'; -import { variableEnabledFieldMap } from './constants'; +import { variableEnabledFieldMap } from '../constants'; import styles from './index.less'; const ModelSetting = ({ show, form }: ISegmentedContentProps) => { diff --git a/web/src/pages/chat/chat-configuration-modal/prompt-engine.tsx b/web/src/pages/chat/chat-configuration-modal/prompt-engine.tsx index 3eaedfa..f6bd1c5 100644 --- a/web/src/pages/chat/chat-configuration-modal/prompt-engine.tsx +++ b/web/src/pages/chat/chat-configuration-modal/prompt-engine.tsx @@ -21,17 +21,16 @@ import { useState, } from 'react'; import { v4 as uuid } from 'uuid'; +import { + VariableTableDataType as DataType, + IPromptConfigParameters, + ISegmentedContentProps, +} from '../interface'; import { EditableCell, EditableRow } from './editable-cell'; -import { ISegmentedContentProps } from './interface'; +import { useSelectPromptConfigParameters } from '../hooks'; import styles from './index.less'; -interface DataType { - key: string; - variable: string; - optional: boolean; -} - type FieldType = { similarity_threshold?: number; vector_similarity_weight?: number; @@ -39,10 +38,11 @@ type FieldType = { }; const PromptEngine = ( - { show, form }: ISegmentedContentProps, - ref: ForwardedRef<Array<Omit<DataType, 'variable'>>>, + { show }: ISegmentedContentProps, + ref: ForwardedRef<Array<IPromptConfigParameters>>, ) => { const [dataSource, setDataSource] = useState<DataType[]>([]); + const parameters = useSelectPromptConfigParameters(); const components = { body: { @@ -99,12 +99,6 @@ const PromptEngine = ( [dataSource], ); - useEffect(() => { - form.setFieldValue(['prompt_config', 'parameters'], dataSource); - const x = form.getFieldValue(['prompt_config', 'parameters']); - console.info(x); - }, [dataSource, form]); - const columns: TableProps<DataType>['columns'] = [ { title: 'key', @@ -146,6 +140,10 @@ const PromptEngine = ( }, ]; + useEffect(() => { + setDataSource(parameters); + }, [parameters]); + return ( <section className={classNames({ @@ -153,7 +151,7 @@ const PromptEngine = ( })} > <Form.Item - label="Orchestrate" + label="System" rules={[{ required: true, message: 'Please input!' }]} name={['prompt_config', 'system']} initialValue={`ä˝ ćŻä¸€ä¸Şć™şč˝ĺŠ©ć‰‹ďĽŚčŻ·ć€»ç»“知识库的内容来回ç”é—®é˘ďĽŚčŻ·ĺ—举知识库ä¸çš„数据详细回ç”。当所有知识库内容é˝ä¸Žé—®é˘ć— ĺ…łć—¶ďĽŚä˝ çš„ĺ›žç”必须包括“知识库ä¸ćśŞć‰ľĺ°ć‚¨č¦çš„ç”ćˇďĽâ€ťčż™ĺŹĄčŻťă€‚回ç”需č¦č€č™‘čŠĺ¤©ĺŽ†ĺŹ˛ă€‚ @@ -161,7 +159,7 @@ const PromptEngine = ( {knowledge} 以上ćŻçźĄčŻ†ĺş“。`} > - <Input.TextArea autoSize={{ maxRows: 5, minRows: 5 }} /> + <Input.TextArea autoSize={{ maxRows: 8, minRows: 5 }} /> </Form.Item> <Divider></Divider> <SimilaritySlider></SimilaritySlider> diff --git a/web/src/pages/chat/chat-container/index.less b/web/src/pages/chat/chat-container/index.less index 147213c..ff6eec7 100644 --- a/web/src/pages/chat/chat-container/index.less +++ b/web/src/pages/chat/chat-container/index.less @@ -1,3 +1,18 @@ .chatContainer { padding: 0 24px 24px; } + +.messageItem { + .messageItemContent { + display: inline-block; + width: 300px; + } +} + +.messageItemLeft { + text-align: left; +} + +.messageItemRight { + text-align: right; +} diff --git a/web/src/pages/chat/chat-container/index.tsx b/web/src/pages/chat/chat-container/index.tsx index bd8d837..b58b52f 100644 --- a/web/src/pages/chat/chat-container/index.tsx +++ b/web/src/pages/chat/chat-container/index.tsx @@ -1,13 +1,41 @@ -import { Button, Flex, Input } from 'antd'; +import { Button, Flex, Input, Typography } from 'antd'; import { ChangeEventHandler, useState } from 'react'; +import { Message } from '@/interfaces/database/chat'; +import classNames from 'classnames'; +import { useFetchConversation, useSendMessage } from '../hooks'; + +import { MessageType } from '@/constants/chat'; +import { IClientConversation } from '../interface'; import styles from './index.less'; +const { Paragraph } = Typography; + +const MessageItem = ({ item }: { item: Message }) => { + return ( + <div + className={classNames(styles.messageItem, { + [styles.messageItemLeft]: item.role === MessageType.Assistant, + [styles.messageItemRight]: item.role === MessageType.User, + })} + > + <span className={styles.messageItemContent}> + <Paragraph ellipsis={{ tooltip: item.content, rows: 3 }}> + {item.content} + </Paragraph> + </span> + </div> + ); +}; + const ChatContainer = () => { const [value, setValue] = useState(''); + const conversation: IClientConversation = useFetchConversation(); + const { sendMessage } = useSendMessage(); const handlePressEnter = () => { console.info(value); + sendMessage(value); }; const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => { @@ -16,7 +44,11 @@ const ChatContainer = () => { return ( <Flex flex={1} className={styles.chatContainer} vertical> - <Flex flex={1}>xx</Flex> + <Flex flex={1} vertical> + {conversation?.message?.map((message) => ( + <MessageItem key={message.id} item={message}></MessageItem> + ))} + </Flex> <Input size="large" placeholder="Message Resume Assistant..." diff --git a/web/src/pages/chat/chat-configuration-modal/constants.ts b/web/src/pages/chat/constants.ts similarity index 62% rename from web/src/pages/chat/chat-configuration-modal/constants.ts rename to web/src/pages/chat/constants.ts index 640ad6f..4c2c5ed 100644 --- a/web/src/pages/chat/chat-configuration-modal/constants.ts +++ b/web/src/pages/chat/constants.ts @@ -5,3 +5,10 @@ export const variableEnabledFieldMap = { frequencyPenaltyEnabled: 'frequency_penalty', maxTokensEnabled: 'max_tokens', }; + +export enum ChatSearchParams { + DialogId = 'dialogId', + ConversationId = 'conversationId', +} + +export const EmptyConversationId = 'empty'; diff --git a/web/src/pages/chat/hooks.ts b/web/src/pages/chat/hooks.ts index 588f63a..dd8e04a 100644 --- a/web/src/pages/chat/hooks.ts +++ b/web/src/pages/chat/hooks.ts @@ -1,6 +1,16 @@ +import showDeleteConfirm from '@/components/deleting-confirm'; +import { MessageType } from '@/constants/chat'; import { IDialog } from '@/interfaces/database/chat'; -import { useCallback, useEffect } from 'react'; -import { useDispatch, useSelector } from 'umi'; +import omit from 'lodash/omit'; +import { useCallback, useEffect, useMemo } from 'react'; +import { useDispatch, useSearchParams, useSelector } from 'umi'; +import { v4 as uuid } from 'uuid'; +import { ChatSearchParams, EmptyConversationId } from './constants'; +import { + IClientConversation, + IMessage, + VariableTableDataType, +} from './interface'; export const useFetchDialogList = () => { const dispatch = useDispatch(); @@ -20,10 +30,336 @@ export const useSetDialog = () => { const setDialog = useCallback( (payload: IDialog) => { - dispatch({ type: 'chatModel/setDialog', payload }); + return dispatch<any>({ type: 'chatModel/setDialog', payload }); }, [dispatch], ); return setDialog; }; + +export const useFetchDialog = (dialogId: string, visible: boolean): IDialog => { + const dispatch = useDispatch(); + const currentDialog: IDialog = useSelector( + (state: any) => state.chatModel.currentDialog, + ); + + const fetchDialog = useCallback(() => { + if (dialogId) { + dispatch({ + type: 'chatModel/getDialog', + payload: { dialog_id: dialogId }, + }); + } + }, [dispatch, dialogId]); + + useEffect(() => { + if (dialogId && visible) { + fetchDialog(); + } + }, [dialogId, fetchDialog, visible]); + + return currentDialog; +}; + +export const useSetCurrentDialog = () => { + const dispatch = useDispatch(); + + const currentDialog: IDialog = useSelector( + (state: any) => state.chatModel.currentDialog, + ); + + const setCurrentDialog = useCallback( + (dialogId: string) => { + if (dialogId) { + dispatch({ + type: 'chatModel/setCurrentDialog', + payload: { id: dialogId }, + }); + } + }, + [dispatch], + ); + + return { currentDialog, setCurrentDialog }; +}; + +export const useResetCurrentDialog = () => { + const dispatch = useDispatch(); + + const resetCurrentDialog = useCallback(() => { + dispatch({ + type: 'chatModel/setCurrentDialog', + payload: {}, + }); + }, [dispatch]); + + return { resetCurrentDialog }; +}; + +export const useSelectPromptConfigParameters = (): VariableTableDataType[] => { + const currentDialog: IDialog = useSelector( + (state: any) => state.chatModel.currentDialog, + ); + + const finalParameters: VariableTableDataType[] = useMemo(() => { + const parameters = currentDialog?.prompt_config?.parameters ?? []; + if (!currentDialog.id) { + // The newly created chat has a default parameter + return [{ key: uuid(), variable: 'knowledge', optional: false }]; + } + return parameters.map((x) => ({ + key: uuid(), + variable: x.key, + optional: x.optional, + })); + }, [currentDialog]); + + return finalParameters; +}; + +export const useRemoveDialog = () => { + const dispatch = useDispatch(); + + const removeDocument = (dialogIds: Array<string>) => () => { + return dispatch({ + type: 'chatModel/removeDialog', + payload: { + dialog_ids: dialogIds, + }, + }); + }; + + const onRemoveDialog = (dialogIds: Array<string>) => { + showDeleteConfirm({ onOk: removeDocument(dialogIds) }); + }; + + 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 = () => { + const [currentQueryParameters] = useSearchParams(); + + return { + dialogId: currentQueryParameters.get(ChatSearchParams.DialogId) || '', + conversationId: + currentQueryParameters.get(ChatSearchParams.ConversationId) || '', + }; +}; + +export const useSelectFirstDialogOnMount = () => { + const dialogList = useFetchDialogList(); + const { dialogId } = useGetChatSearchParams(); + + const { handleClickDialog } = useClickDialogCard(); + + useEffect(() => { + if (dialogList.length > 0 && !dialogId) { + handleClickDialog(dialogList[0].id); + } + }, [dialogList, handleClickDialog, dialogId]); + + return dialogList; +}; + +//#region conversation + +export const useFetchConversationList = (dialogId?: string) => { + const dispatch = useDispatch(); + const conversationList: any[] = useSelector( + (state: any) => state.chatModel.conversationList, + ); + + const fetchConversationList = useCallback(() => { + if (dialogId) { + dispatch({ + type: 'chatModel/listConversation', + payload: { dialog_id: dialogId }, + }); + } + }, [dispatch, dialogId]); + + useEffect(() => { + fetchConversationList(); + }, [fetchConversationList]); + + return conversationList; +}; + +export const useClickConversationCard = () => { + const [currentQueryParameters, setSearchParams] = useSearchParams(); + const newQueryParameters: URLSearchParams = new URLSearchParams( + currentQueryParameters.toString(), + ); + + const handleClickConversation = (conversationId: string) => { + newQueryParameters.set(ChatSearchParams.ConversationId, conversationId); + setSearchParams(newQueryParameters); + }; + + return { handleClickConversation }; +}; + +export const useCreateTemporaryConversation = () => { + const dispatch = useDispatch(); + const { dialogId } = useGetChatSearchParams(); + const { handleClickConversation } = useClickConversationCard(); + let chatModel = useSelector((state: any) => state.chatModel); + let currentConversation: Pick< + IClientConversation, + 'id' | 'message' | 'name' | 'dialog_id' + > = chatModel.currentConversation; + let conversationList: IClientConversation[] = chatModel.conversationList; + + const createTemporaryConversation = (message: string) => { + const messages = [...(currentConversation?.message ?? [])]; + if (messages.some((x) => x.id === EmptyConversationId)) { + return; + } + messages.unshift({ + id: EmptyConversationId, + content: message, + role: MessageType.Assistant, + }); + + // It’s the back-end data. + 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 = () => { + const dispatch = useDispatch(); + const { dialogId } = useGetChatSearchParams(); + + const setConversation = (message: string) => { + return dispatch<any>({ + type: 'chatModel/setConversation', + payload: { + // conversation_id: '', + dialog_id: dialogId, + name: message, + message: [ + { + role: MessageType.Assistant, + content: message, + }, + ], + }, + }); + }; + + return { setConversation }; +}; + +export const useFetchConversation = () => { + const dispatch = useDispatch(); + const { conversationId } = useGetChatSearchParams(); + const conversation = useSelector( + (state: any) => state.chatModel.currentConversation, + ); + + const fetchConversation = useCallback(() => { + if (conversationId !== EmptyConversationId && conversationId !== '') { + dispatch({ + type: 'chatModel/getConversation', + payload: { + conversation_id: conversationId, + }, + }); + } + }, [dispatch, conversationId]); + + useEffect(() => { + fetchConversation(); + }, [fetchConversation]); + + return conversation; +}; + +export const useSendMessage = () => { + const dispatch = useDispatch(); + const { setConversation } = useSetConversation(); + const { conversationId } = useGetChatSearchParams(); + const conversation = useSelector( + (state: any) => state.chatModel.currentConversation, + ); + const { handleClickConversation } = useClickConversationCard(); + + const sendMessage = (message: string, id?: string) => { + dispatch({ + type: 'chatModel/completeConversation', + payload: { + conversation_id: id ?? conversationId, + messages: [ + ...(conversation?.message ?? []).map((x: IMessage) => omit(x, 'id')), + { + role: MessageType.User, + content: message, + }, + ], + }, + }); + }; + + const handleSendMessage = async (message: string) => { + if (conversationId !== EmptyConversationId) { + sendMessage(message); + } else { + const data = await setConversation(message); + if (data.retcode === 0) { + const id = data.data.id; + handleClickConversation(id); + sendMessage(message, id); + } + } + }; + + return { sendMessage: handleSendMessage }; +}; + +//#endregion diff --git a/web/src/pages/chat/index.less b/web/src/pages/chat/index.less index 4400e7e..e3b5f89 100644 --- a/web/src/pages/chat/index.less +++ b/web/src/pages/chat/index.less @@ -5,6 +5,10 @@ width: 288px; padding: 26px; + .chatAppContent { + overflow-y: auto; + } + .chatAppCard { :global(.ant-card-body) { padding: 10px; @@ -15,6 +19,12 @@ } } } + .chatAppCardSelected { + :global(.ant-card-body) { + background-color: @gray11; + border-radius: 8px; + } + } } .chatTitleWrapper { width: 220px; @@ -29,6 +39,19 @@ padding: 5px 10px; } + .chatTitleCard { + :global(.ant-card-body) { + padding: 8px; + } + } + + .chatTitleCardSelected { + :global(.ant-card-body) { + background-color: @gray11; + border-radius: 8px; + } + } + .divider { margin: 0; height: 100%; diff --git a/web/src/pages/chat/index.tsx b/web/src/pages/chat/index.tsx index f33bbd3..33b20e4 100644 --- a/web/src/pages/chat/index.tsx +++ b/web/src/pages/chat/index.tsx @@ -1,3 +1,5 @@ +import { ReactComponent as ChatAppCube } from '@/assets/svg/chat-app-cube.svg'; +import { useSetModalState } from '@/hooks/commonHooks'; import { DeleteOutlined, EditOutlined, FormOutlined } from '@ant-design/icons'; import { Button, @@ -9,20 +11,39 @@ import { Space, Tag, } from 'antd'; -import ChatContainer from './chat-container'; - -import { ReactComponent as ChatAppCube } from '@/assets/svg/chat-app-cube.svg'; -import ModalManager from '@/components/modal-manager'; import classNames from 'classnames'; +import { useCallback, useState } from 'react'; import ChatConfigurationModal from './chat-configuration-modal'; -import { useFetchDialogList } from './hooks'; +import ChatContainer from './chat-container'; +import { + useClickConversationCard, + useClickDialogCard, + useCreateTemporaryConversation, + useFetchConversationList, + useFetchDialog, + useGetChatSearchParams, + useRemoveDialog, + useSelectFirstDialogOnMount, + useSetCurrentDialog, +} from './hooks'; -import { useState } from 'react'; import styles from './index.less'; const Chat = () => { - const dialogList = useFetchDialogList(); + const dialogList = useSelectFirstDialogOnMount(); const [activated, setActivated] = useState<string>(''); + const { visible, hideModal, showModal } = useSetModalState(); + const { setCurrentDialog, currentDialog } = useSetCurrentDialog(); + const { onRemoveDialog } = useRemoveDialog(); + const { handleClickDialog } = useClickDialogCard(); + const { handleClickConversation } = useClickConversationCard(); + const { dialogId, conversationId } = useGetChatSearchParams(); + const list = useFetchConversationList(dialogId); + const { createTemporaryConversation } = useCreateTemporaryConversation(); + + const selectedDialog = useFetchDialog(dialogId, true); + + const prologue = selectedDialog?.prompt_config?.prologue || ''; const handleAppCardEnter = (id: string) => () => { setActivated(id); @@ -32,72 +53,84 @@ const Chat = () => { setActivated(''); }; - const items: MenuProps['items'] = [ - { - key: '1', - label: ( - <a - target="_blank" - rel="noopener noreferrer" - href="https://www.antgroup.com" - > - 1st menu item - </a> - ), - }, - ]; + const handleShowChatConfigurationModal = (dialogId?: string) => () => { + if (dialogId) { + setCurrentDialog(dialogId); + } + showModal(); + }; + + const handleDialogCardClick = (dialogId: string) => () => { + handleClickDialog(dialogId); + }; + + const handleConversationCardClick = (dialogId: string) => () => { + handleClickConversation(dialogId); + }; + + const handleCreateTemporaryConversation = useCallback(() => { + createTemporaryConversation(prologue); + }, [createTemporaryConversation, prologue]); - const appItems: MenuProps['items'] = [ + const items: MenuProps['items'] = [ { key: '1', + onClick: handleCreateTemporaryConversation, label: ( <Space> - <EditOutlined /> - Edit - </Space> - ), - }, - { type: 'divider' }, - { - key: '2', - label: ( - <Space> - <DeleteOutlined /> - Delete chat + <EditOutlined /> New chat </Space> ), }, ]; + const buildAppItems = (dialogId: string) => { + const appItems: MenuProps['items'] = [ + { + key: '1', + onClick: handleShowChatConfigurationModal(dialogId), + label: ( + <Space> + <EditOutlined /> + Edit + </Space> + ), + }, + { type: 'divider' }, + { + key: '2', + onClick: () => onRemoveDialog([dialogId]), + label: ( + <Space> + <DeleteOutlined /> + Delete chat + </Space> + ), + }, + ]; + + return appItems; + }; + return ( <Flex className={styles.chatWrapper}> <Flex className={styles.chatAppWrapper}> <Flex flex={1} vertical> - <ModalManager> - {({ visible, showModal, hideModal }) => { - return ( - <> - <Button type="primary" onClick={() => showModal()}> - Create an Assistant - </Button> - <ChatConfigurationModal - visible={visible} - showModal={showModal} - hideModal={hideModal} - ></ChatConfigurationModal> - </> - ); - }} - </ModalManager> - + <Button type="primary" onClick={handleShowChatConfigurationModal()}> + Create an Assistant + </Button> <Divider></Divider> - <Space direction={'vertical'} size={'middle'}> + <Flex className={styles.chatAppContent} vertical gap={10}> {dialogList.map((x) => ( <Card key={x.id} - className={classNames(styles.chatAppCard)} + hoverable + className={classNames(styles.chatAppCard, { + [styles.chatAppCardSelected]: dialogId === x.id, + })} onMouseEnter={handleAppCardEnter(x.id)} onMouseLeave={handleAppCardLeave} + onClick={handleDialogCardClick(x.id)} > <Flex justify="space-between" align="center"> <Space> @@ -109,7 +142,7 @@ const Chat = () => { </Space> {activated === x.id && ( <section> - <Dropdown menu={{ items: appItems }}> + <Dropdown menu={{ items: buildAppItems(x.id) }}> <ChatAppCube className={styles.cubeIcon}></ChatAppCube> </Dropdown> </section> @@ -117,7 +150,7 @@ const Chat = () => { </Flex> </Card> ))} - </Space> + </Flex> </Flex> </Flex> <Divider type={'vertical'} className={styles.divider}></Divider> @@ -137,11 +170,30 @@ const Chat = () => { </Dropdown> </Flex> <Divider></Divider> - <section className={styles.chatTitleContent}>today</section> + <Flex vertical gap={10} className={styles.chatTitleContent}> + {list.map((x) => ( + <Card + key={x.id} + hoverable + onClick={handleConversationCardClick(x.id)} + className={classNames(styles.chatTitleCard, { + [styles.chatTitleCardSelected]: x.id === conversationId, + })} + > + <div>{x.name}</div> + </Card> + ))} + </Flex> </Flex> </Flex> <Divider type={'vertical'} className={styles.divider}></Divider> <ChatContainer></ChatContainer> + <ChatConfigurationModal + visible={visible} + showModal={showModal} + hideModal={hideModal} + id={currentDialog.id} + ></ChatConfigurationModal> </Flex> ); }; diff --git a/web/src/pages/chat/interface.ts b/web/src/pages/chat/interface.ts new file mode 100644 index 0000000..c45da81 --- /dev/null +++ b/web/src/pages/chat/interface.ts @@ -0,0 +1,31 @@ +import { IConversation, Message } from '@/interfaces/database/chat'; +import { FormInstance } from 'antd'; + +export interface ISegmentedContentProps { + show: boolean; + form: FormInstance; +} + +export interface IVariable { + temperature: number; + top_p: number; + frequency_penalty: number; + presence_penalty: number; + max_tokens: number; +} + +export interface VariableTableDataType { + key: string; + variable: string; + optional: boolean; +} + +export type IPromptConfigParameters = Omit<VariableTableDataType, 'variable'>; + +export interface IMessage extends Message { + id: string; +} + +export interface IClientConversation extends IConversation { + message: IMessage[]; +} diff --git a/web/src/pages/chat/model.ts b/web/src/pages/chat/model.ts index dd5c5d3..ff7f4ff 100644 --- a/web/src/pages/chat/model.ts +++ b/web/src/pages/chat/model.ts @@ -1,11 +1,16 @@ -import { IDialog } from '@/interfaces/database/chat'; +import { IConversation, IDialog, Message } from '@/interfaces/database/chat'; import chatService from '@/services/chatService'; import { message } from 'antd'; import { DvaModel } from 'umi'; +import { v4 as uuid } from 'uuid'; +import { IClientConversation, IMessage } from './interface'; export interface ChatModelState { name: string; dialogList: IDialog[]; + currentDialog: IDialog; + conversationList: IConversation[]; + currentConversation: IClientConversation; } const model: DvaModel<ChatModelState> = { @@ -13,6 +18,9 @@ const model: DvaModel<ChatModelState> = { state: { name: 'kate', dialogList: [], + currentDialog: <IDialog>{}, + conversationList: [], + currentConversation: {} as IClientConversation, }, reducers: { save(state, action) { @@ -27,11 +35,50 @@ const model: DvaModel<ChatModelState> = { dialogList: payload, }; }, + setCurrentDialog(state, { payload }) { + return { + ...state, + currentDialog: payload, + }; + }, + setConversationList(state, { payload }) { + return { + ...state, + conversationList: payload, + }; + }, + setCurrentConversation(state, { payload }) { + const messageList = payload?.message.map((x: Message | IMessage) => ({ + ...x, + id: 'id' in x ? x.id : uuid(), + })); + return { + ...state, + currentConversation: { ...payload, message: messageList }, + }; + }, + addEmptyConversationToList(state, {}) { + const list = [...state.conversationList]; + // if (list.every((x) => x.id !== 'empty')) { + // list.push({ + // id: 'empty', + // name: 'New conversation', + // message: [], + // }); + // } + return { + ...state, + conversationList: list, + }; + }, }, effects: { *getDialog({ payload }, { call, put }) { const { data } = yield call(chatService.getDialog, payload); + if (data.retcode === 0) { + yield put({ type: 'setCurrentDialog', payload: data.data }); + } }, *setDialog({ payload }, { call, put }) { const { data } = yield call(chatService.setDialog, payload); @@ -39,6 +86,15 @@ const model: DvaModel<ChatModelState> = { yield put({ type: 'listDialog' }); message.success('Created successfully !'); } + return data.retcode; + }, + *removeDialog({ payload }, { call, put }) { + const { data } = yield call(chatService.removeDialog, payload); + if (data.retcode === 0) { + yield put({ type: 'listDialog' }); + message.success('Deleted successfully !'); + } + return data.retcode; }, *listDialog({ payload }, { call, put }) { const { data } = yield call(chatService.listDialog, payload); @@ -46,15 +102,40 @@ const model: DvaModel<ChatModelState> = { }, *listConversation({ payload }, { call, put }) { const { data } = yield call(chatService.listConversation, payload); + if (data.retcode === 0) { + yield put({ type: 'setConversationList', payload: data.data }); + } + return data.retcode; }, *getConversation({ payload }, { call, put }) { const { data } = yield call(chatService.getConversation, payload); + if (data.retcode === 0) { + yield put({ type: 'setCurrentConversation', payload: data.data }); + } + return data.retcode; }, *setConversation({ payload }, { call, put }) { const { data } = yield call(chatService.setConversation, payload); + if (data.retcode === 0) { + yield put({ + type: 'listConversation', + payload: { + dialog_id: data.data.dialog_id, + }, + }); + } + return data; }, *completeConversation({ payload }, { call, put }) { const { data } = yield call(chatService.completeConversation, payload); + if (data.retcode === 0) { + yield put({ + type: 'getConversation', + payload: { + conversation_id: payload.conversation_id, + }, + }); + } }, }, }; diff --git a/web/src/pages/chat/utils.ts b/web/src/pages/chat/utils.ts new file mode 100644 index 0000000..997dc37 --- /dev/null +++ b/web/src/pages/chat/utils.ts @@ -0,0 +1,12 @@ +import { variableEnabledFieldMap } from './constants'; + +export const excludeUnEnabledVariables = (values: any) => { + const unEnabledFields: Array<keyof typeof variableEnabledFieldMap> = + Object.keys(variableEnabledFieldMap).filter((key) => !values[key]) as Array< + keyof typeof variableEnabledFieldMap + >; + + return unEnabledFields.map( + (key) => `llm_setting.${variableEnabledFieldMap[key]}`, + ); +}; diff --git a/web/src/pages/knowledge/model.ts b/web/src/pages/knowledge/model.ts index 51ab284..04357c9 100644 --- a/web/src/pages/knowledge/model.ts +++ b/web/src/pages/knowledge/model.ts @@ -31,7 +31,7 @@ const model: DvaModel<KnowledgeModelState> = { }, *getList({ payload = {} }, { call, put }) { const { data } = yield call(kbService.getList, payload); - const { retcode, data: res, retmsg } = data; + const { retcode, data: res } = data; if (retcode === 0) { yield put({ diff --git a/web/src/services/chatService.ts b/web/src/services/chatService.ts index 5fbaf1a..946de86 100644 --- a/web/src/services/chatService.ts +++ b/web/src/services/chatService.ts @@ -6,6 +6,7 @@ const { getDialog, setDialog, listDialog, + removeDialog, getConversation, setConversation, completeConversation, @@ -21,6 +22,10 @@ const methods = { url: setDialog, method: 'post', }, + removeDialog: { + url: removeDialog, + method: 'post', + }, listDialog: { url: listDialog, method: 'get', diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index f813071..777c413 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -45,6 +45,7 @@ export default { setDialog: `${api_host}/dialog/set`, getDialog: `${api_host}/dialog/get`, + removeDialog: `${api_host}/dialog/rm`, listDialog: `${api_host}/dialog/list`, setConversation: `${api_host}/conversation/set`, -- GitLab