From 109548a28e06c4961f9aefddea37f366446dc05e Mon Sep 17 00:00:00 2001 From: billchen Date: Wed, 21 Feb 2024 13:44:39 +0800 Subject: [PATCH 1/4] feat: set chat configuration to backend --- web/src/hooks/commonHooks.ts | 14 +++ .../assistant-setting.tsx | 2 - .../chat/chat-configuration-modal/index.tsx | 43 +++++++-- .../chat-configuration-modal/interface.ts | 8 ++ .../prompt-engine.tsx | 14 ++- web/src/pages/chat/hooks.ts | 61 ++++++++++++- web/src/pages/chat/index.tsx | 88 ++++++++++--------- web/src/pages/chat/model.ts | 12 +++ 8 files changed, 181 insertions(+), 61 deletions(-) create mode 100644 web/src/hooks/commonHooks.ts diff --git a/web/src/hooks/commonHooks.ts b/web/src/hooks/commonHooks.ts new file mode 100644 index 0000000000..651b7b7ae5 --- /dev/null +++ b/web/src/hooks/commonHooks.ts @@ -0,0 +1,14 @@ +import { useState } from 'react'; + +export const useSetModalState = () => { + const [visible, setVisible] = useState(false); + + const showModal = () => { + setVisible(true); + }; + const hideModal = () => { + setVisible(false); + }; + + return { visible, showModal, hideModal }; +}; 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 968d277b26..83bd8082bf 100644 --- a/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx +++ b/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx @@ -6,8 +6,6 @@ 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 knowledgeOptions = knowledgeList.map((x) => ({ diff --git a/web/src/pages/chat/chat-configuration-modal/index.tsx b/web/src/pages/chat/chat-configuration-modal/index.tsx index 64943c9537..db0bbf5ef2 100644 --- a/web/src/pages/chat/chat-configuration-modal/index.tsx +++ b/web/src/pages/chat/chat-configuration-modal/index.tsx @@ -3,14 +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 { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; +import { useFetchDialog, useResetCurrentDialog, useSetDialog } from '../hooks'; import { variableEnabledFieldMap } from './constants'; import styles from './index.less'; +import { IPromptConfigParameters } from './interface'; enum ConfigurationSegmented { AssistantSetting = 'Assistant Setting', @@ -40,22 +42,30 @@ 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.AssistantSetting, ); - const promptEngineRef = useRef(null); + const promptEngineRef = useRef>([]); + 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', + ]); const finalValues = { + dialog_id: id, ...nextValues, prompt_config: { ...nextValues.prompt_config, @@ -65,7 +75,10 @@ const ChatConfigurationModal = ({ 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 +89,11 @@ const ChatConfigurationModal = ({ setValue(val as ConfigurationSegmented); }; + const handleModalAfterClose = () => { + resetCurrentDialog(); + form.resetFields(); + }; + const title = ( @@ -89,6 +107,10 @@ const ChatConfigurationModal = ({ ); + useEffect(() => { + form.setFieldsValue(currentDialog); + }, [currentDialog, form]); + return ( ; 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 3eaedfa7a8..aa860a51a8 100644 --- a/web/src/pages/chat/chat-configuration-modal/prompt-engine.tsx +++ b/web/src/pages/chat/chat-configuration-modal/prompt-engine.tsx @@ -22,16 +22,14 @@ import { } from 'react'; import { v4 as uuid } from 'uuid'; import { EditableCell, EditableRow } from './editable-cell'; -import { ISegmentedContentProps } from './interface'; +import { + VariableTableDataType as DataType, + IPromptConfigParameters, + ISegmentedContentProps, +} from './interface'; import styles from './index.less'; -interface DataType { - key: string; - variable: string; - optional: boolean; -} - type FieldType = { similarity_threshold?: number; vector_similarity_weight?: number; @@ -40,7 +38,7 @@ type FieldType = { const PromptEngine = ( { show, form }: ISegmentedContentProps, - ref: ForwardedRef>>, + ref: ForwardedRef>, ) => { const [dataSource, setDataSource] = useState([]); diff --git a/web/src/pages/chat/hooks.ts b/web/src/pages/chat/hooks.ts index 588f63a92b..5959f7031a 100644 --- a/web/src/pages/chat/hooks.ts +++ b/web/src/pages/chat/hooks.ts @@ -20,10 +20,69 @@ export const useSetDialog = () => { const setDialog = useCallback( (payload: IDialog) => { - dispatch({ type: 'chatModel/setDialog', payload }); + return dispatch({ 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 }; +}; diff --git a/web/src/pages/chat/index.tsx b/web/src/pages/chat/index.tsx index f33bbd338b..c47c3700b3 100644 --- a/web/src/pages/chat/index.tsx +++ b/web/src/pages/chat/index.tsx @@ -12,17 +12,19 @@ import { 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 ChatConfigurationModal from './chat-configuration-modal'; -import { useFetchDialogList } from './hooks'; +import { useFetchDialogList, useSetCurrentDialog } from './hooks'; +import { useSetModalState } from '@/hooks/commonHooks'; import { useState } from 'react'; import styles from './index.less'; const Chat = () => { const dialogList = useFetchDialogList(); const [activated, setActivated] = useState(''); + const { visible, hideModal, showModal } = useSetModalState(); + const { setCurrentDialog, currentDialog } = useSetCurrentDialog(); const handleAppCardEnter = (id: string) => () => { setActivated(id); @@ -32,6 +34,13 @@ const Chat = () => { setActivated(''); }; + const handleShowChatConfigurationModal = (dialogId?: string) => () => { + if (dialogId) { + setCurrentDialog(dialogId); + } + showModal(); + }; + const items: MenuProps['items'] = [ { key: '1', @@ -47,49 +56,40 @@ const Chat = () => { }, ]; - const appItems: MenuProps['items'] = [ - { - key: '1', - label: ( - - - Edit - - ), - }, - { type: 'divider' }, - { - key: '2', - label: ( - - - Delete chat - - ), - }, - ]; + const buildAppItems = (dialogId: string) => { + const appItems: MenuProps['items'] = [ + { + key: '1', + onClick: handleShowChatConfigurationModal(dialogId), + label: ( + + + Edit + + ), + }, + { type: 'divider' }, + { + key: '2', + label: ( + + + Delete chat + + ), + }, + ]; + + return appItems; + }; return ( - - {({ visible, showModal, hideModal }) => { - return ( - <> - - - - ); - }} - - + {dialogList.map((x) => ( @@ -109,7 +109,7 @@ const Chat = () => { {activated === x.id && (
- +
@@ -142,6 +142,12 @@ const Chat = () => {
+
); }; diff --git a/web/src/pages/chat/model.ts b/web/src/pages/chat/model.ts index dd5c5d322a..66d30a87a1 100644 --- a/web/src/pages/chat/model.ts +++ b/web/src/pages/chat/model.ts @@ -6,6 +6,7 @@ import { DvaModel } from 'umi'; export interface ChatModelState { name: string; dialogList: IDialog[]; + currentDialog: IDialog; } const model: DvaModel = { @@ -13,6 +14,7 @@ const model: DvaModel = { state: { name: 'kate', dialogList: [], + currentDialog: {}, }, reducers: { save(state, action) { @@ -27,11 +29,20 @@ const model: DvaModel = { dialogList: payload, }; }, + setCurrentDialog(state, { payload }) { + return { + ...state, + currentDialog: payload, + }; + }, }, 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 +50,7 @@ const model: DvaModel = { yield put({ type: 'listDialog' }); message.success('Created successfully !'); } + return data.retcode; }, *listDialog({ payload }, { call, put }) { const { data } = yield call(chatService.listDialog, payload); From 33ce17ea898eadfa24328c75a999503b30b95fd2 Mon Sep 17 00:00:00 2001 From: billchen Date: Wed, 21 Feb 2024 15:32:01 +0800 Subject: [PATCH 2/4] feat: exclude unEnabled variables --- web/src/hooks/commonHooks.ts | 22 +++++++++++++++- .../assistant-setting.tsx | 2 +- .../chat/chat-configuration-modal/index.tsx | 6 +++-- .../model-setting.tsx | 4 +-- .../prompt-engine.tsx | 22 ++++++++-------- .../constants.ts | 0 web/src/pages/chat/hooks.ts | 25 ++++++++++++++++++- .../interface.ts | 0 web/src/pages/chat/utils.ts | 12 +++++++++ 9 files changed, 75 insertions(+), 18 deletions(-) rename web/src/pages/chat/{chat-configuration-modal => }/constants.ts (100%) rename web/src/pages/chat/{chat-configuration-modal => }/interface.ts (100%) create mode 100644 web/src/pages/chat/utils.ts diff --git a/web/src/hooks/commonHooks.ts b/web/src/hooks/commonHooks.ts index 651b7b7ae5..e4361cd29f 100644 --- a/web/src/hooks/commonHooks.ts +++ b/web/src/hooks/commonHooks.ts @@ -1,4 +1,5 @@ -import { useState } from 'react'; +import isEqual from 'lodash/isEqual'; +import { useEffect, useRef, useState } from 'react'; export const useSetModalState = () => { const [visible, setVisible] = useState(false); @@ -12,3 +13,22 @@ export const useSetModalState = () => { return { visible, showModal, hideModal }; }; + +export const useDeepCompareEffect = ( + effect: React.EffectCallback, + deps: React.DependencyList, +) => { + const ref = useRef(); + let callback: ReturnType = () => {}; + if (!isEqual(deps, ref.current)) { + callback = effect(); + ref.current = deps; + } + useEffect(() => { + return () => { + if (callback) { + callback(); + } + }; + }, []); +}; 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 83bd8082bf..24463b6690 100644 --- a/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx +++ b/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx @@ -1,7 +1,7 @@ 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'; diff --git a/web/src/pages/chat/chat-configuration-modal/index.tsx b/web/src/pages/chat/chat-configuration-modal/index.tsx index db0bbf5ef2..83ce13ec80 100644 --- a/web/src/pages/chat/chat-configuration-modal/index.tsx +++ b/web/src/pages/chat/chat-configuration-modal/index.tsx @@ -9,10 +9,11 @@ import ModelSetting from './model-setting'; import PromptEngine from './prompt-engine'; import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; +import { variableEnabledFieldMap } from '../constants'; import { useFetchDialog, useResetCurrentDialog, useSetDialog } from '../hooks'; -import { variableEnabledFieldMap } from './constants'; +import { IPromptConfigParameters } from '../interface'; +import { excludeUnEnabledVariables } from '../utils'; import styles from './index.less'; -import { IPromptConfigParameters } from './interface'; enum ConfigurationSegmented { AssistantSetting = 'Assistant Setting', @@ -63,6 +64,7 @@ const ChatConfigurationModal = ({ visible, hideModal, id }: IProps) => { const nextValues: any = omit(values, [ ...Object.keys(variableEnabledFieldMap), 'parameters', + ...excludeUnEnabledVariables(values), ]); const finalValues = { dialog_id: id, 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 1c421fcd02..5f68a6dee4 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 aa860a51a8..f6bd1c5375 100644 --- a/web/src/pages/chat/chat-configuration-modal/prompt-engine.tsx +++ b/web/src/pages/chat/chat-configuration-modal/prompt-engine.tsx @@ -21,13 +21,14 @@ import { useState, } from 'react'; import { v4 as uuid } from 'uuid'; -import { EditableCell, EditableRow } from './editable-cell'; import { VariableTableDataType as DataType, IPromptConfigParameters, ISegmentedContentProps, -} from './interface'; +} from '../interface'; +import { EditableCell, EditableRow } from './editable-cell'; +import { useSelectPromptConfigParameters } from '../hooks'; import styles from './index.less'; type FieldType = { @@ -37,10 +38,11 @@ type FieldType = { }; const PromptEngine = ( - { show, form }: ISegmentedContentProps, + { show }: ISegmentedContentProps, ref: ForwardedRef>, ) => { const [dataSource, setDataSource] = useState([]); + const parameters = useSelectPromptConfigParameters(); const components = { body: { @@ -97,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['columns'] = [ { title: 'key', @@ -144,6 +140,10 @@ const PromptEngine = ( }, ]; + useEffect(() => { + setDataSource(parameters); + }, [parameters]); + return (
- + diff --git a/web/src/pages/chat/chat-configuration-modal/constants.ts b/web/src/pages/chat/constants.ts similarity index 100% rename from web/src/pages/chat/chat-configuration-modal/constants.ts rename to web/src/pages/chat/constants.ts diff --git a/web/src/pages/chat/hooks.ts b/web/src/pages/chat/hooks.ts index 5959f7031a..bd9ee3f8ef 100644 --- a/web/src/pages/chat/hooks.ts +++ b/web/src/pages/chat/hooks.ts @@ -1,6 +1,8 @@ import { IDialog } from '@/interfaces/database/chat'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'umi'; +import { v4 as uuid } from 'uuid'; +import { VariableTableDataType } from './interface'; export const useFetchDialogList = () => { const dispatch = useDispatch(); @@ -86,3 +88,24 @@ export const useResetCurrentDialog = () => { 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; +}; diff --git a/web/src/pages/chat/chat-configuration-modal/interface.ts b/web/src/pages/chat/interface.ts similarity index 100% rename from web/src/pages/chat/chat-configuration-modal/interface.ts rename to web/src/pages/chat/interface.ts diff --git a/web/src/pages/chat/utils.ts b/web/src/pages/chat/utils.ts new file mode 100644 index 0000000000..997dc3755c --- /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 = + Object.keys(variableEnabledFieldMap).filter((key) => !values[key]) as Array< + keyof typeof variableEnabledFieldMap + >; + + return unEnabledFields.map( + (key) => `llm_setting.${variableEnabledFieldMap[key]}`, + ); +}; From ee28f1c20010294eb9a049b49cd1d124b97ca6c9 Mon Sep 17 00:00:00 2001 From: billchen Date: Wed, 21 Feb 2024 17:01:39 +0800 Subject: [PATCH 3/4] feat: delete chat dialog --- web/src/pages/chat/hooks.ts | 20 ++++++++++++++++++++ web/src/pages/chat/index.less | 4 ++++ web/src/pages/chat/index.tsx | 12 +++++++++--- web/src/pages/chat/model.ts | 8 ++++++++ web/src/services/chatService.ts | 5 +++++ web/src/utils/api.ts | 1 + 6 files changed, 47 insertions(+), 3 deletions(-) diff --git a/web/src/pages/chat/hooks.ts b/web/src/pages/chat/hooks.ts index bd9ee3f8ef..ea5d3bcfcb 100644 --- a/web/src/pages/chat/hooks.ts +++ b/web/src/pages/chat/hooks.ts @@ -1,3 +1,4 @@ +import showDeleteConfirm from '@/components/deleting-confirm'; import { IDialog } from '@/interfaces/database/chat'; import { useCallback, useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'umi'; @@ -109,3 +110,22 @@ export const useSelectPromptConfigParameters = (): VariableTableDataType[] => { return finalParameters; }; + +export const useRemoveDialog = () => { + const dispatch = useDispatch(); + + const removeDocument = (dialogIds: Array) => () => { + return dispatch({ + type: 'chatModel/removeDialog', + payload: { + dialog_ids: dialogIds, + }, + }); + }; + + const onRemoveDialog = (dialogIds: Array) => { + showDeleteConfirm({ onOk: removeDocument(dialogIds) }); + }; + + return { onRemoveDialog }; +}; diff --git a/web/src/pages/chat/index.less b/web/src/pages/chat/index.less index 4400e7eeee..c98f8bdf73 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; diff --git a/web/src/pages/chat/index.tsx b/web/src/pages/chat/index.tsx index c47c3700b3..bde6f4dbfb 100644 --- a/web/src/pages/chat/index.tsx +++ b/web/src/pages/chat/index.tsx @@ -14,7 +14,11 @@ import ChatContainer from './chat-container'; import { ReactComponent as ChatAppCube } from '@/assets/svg/chat-app-cube.svg'; import classNames from 'classnames'; import ChatConfigurationModal from './chat-configuration-modal'; -import { useFetchDialogList, useSetCurrentDialog } from './hooks'; +import { + useFetchDialogList, + useRemoveDialog, + useSetCurrentDialog, +} from './hooks'; import { useSetModalState } from '@/hooks/commonHooks'; import { useState } from 'react'; @@ -25,6 +29,7 @@ const Chat = () => { const [activated, setActivated] = useState(''); const { visible, hideModal, showModal } = useSetModalState(); const { setCurrentDialog, currentDialog } = useSetCurrentDialog(); + const { onRemoveDialog } = useRemoveDialog(); const handleAppCardEnter = (id: string) => () => { setActivated(id); @@ -71,6 +76,7 @@ const Chat = () => { { type: 'divider' }, { key: '2', + onClick: () => onRemoveDialog([dialogId]), label: ( @@ -91,7 +97,7 @@ const Chat = () => { Create an Assistant - + {dialogList.map((x) => ( { ))} - + diff --git a/web/src/pages/chat/model.ts b/web/src/pages/chat/model.ts index 66d30a87a1..de279c079f 100644 --- a/web/src/pages/chat/model.ts +++ b/web/src/pages/chat/model.ts @@ -52,6 +52,14 @@ const model: DvaModel = { } 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); yield put({ type: 'setDialogList', payload: data.data }); diff --git a/web/src/services/chatService.ts b/web/src/services/chatService.ts index 5fbaf1a1af..946de86ae8 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 f813071cfd..777c413fe5 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`, From 2249251700c7e1edeca1ea508c58bf9f8a3c6acb Mon Sep 17 00:00:00 2001 From: billchen Date: Thu, 22 Feb 2024 17:10:57 +0800 Subject: [PATCH 4/4] feat: fetch conversation --- web/src/constants/chat.ts | 4 + web/src/hooks/knowledgeHook.ts | 11 +- web/src/interfaces/database/chat.ts | 19 ++ .../parsing-status-cell/index.tsx | 4 +- .../assistant-setting.tsx | 2 +- .../chat/chat-configuration-modal/index.tsx | 2 + web/src/pages/chat/chat-container/index.less | 15 ++ web/src/pages/chat/chat-container/index.tsx | 36 ++- web/src/pages/chat/constants.ts | 7 + web/src/pages/chat/hooks.ts | 238 +++++++++++++++++- web/src/pages/chat/index.less | 19 ++ web/src/pages/chat/index.tsx | 72 ++++-- web/src/pages/chat/interface.ts | 9 + web/src/pages/chat/model.ts | 63 ++++- web/src/pages/knowledge/model.ts | 2 +- 15 files changed, 475 insertions(+), 28 deletions(-) create mode 100644 web/src/constants/chat.ts diff --git a/web/src/constants/chat.ts b/web/src/constants/chat.ts new file mode 100644 index 0000000000..2ce95b56a0 --- /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/knowledgeHook.ts b/web/src/hooks/knowledgeHook.ts index ba090d976e..9ad41f5a78 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 6aec10a5d8..f7c4a23cbf 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 5c0711a451..22f840b69a 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 ( - } - > + }> {isRunning ? ( 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 24463b6690..0ce8af7c74 100644 --- a/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx +++ b/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx @@ -7,7 +7,7 @@ import { useFetchKnowledgeList } from '@/hooks/knowledgeHook'; import styles from './index.less'; 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 83ce13ec80..3978961be3 100644 --- a/web/src/pages/chat/chat-configuration-modal/index.tsx +++ b/web/src/pages/chat/chat-configuration-modal/index.tsx @@ -66,12 +66,14 @@ const ChatConfigurationModal = ({ visible, hideModal, id }: IProps) => { '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); diff --git a/web/src/pages/chat/chat-container/index.less b/web/src/pages/chat/chat-container/index.less index 147213cff9..ff6eec79c4 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 bd8d837ba7..b58b52f65c 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 ( +
+ + + {item.content} + + +
+ ); +}; + const ChatContainer = () => { const [value, setValue] = useState(''); + const conversation: IClientConversation = useFetchConversation(); + const { sendMessage } = useSendMessage(); const handlePressEnter = () => { console.info(value); + sendMessage(value); }; const handleInputChange: ChangeEventHandler = (e) => { @@ -16,7 +44,11 @@ const ChatContainer = () => { return ( - xx + + {conversation?.message?.map((message) => ( + + ))} + { const dispatch = useDispatch(); @@ -129,3 +136,230 @@ export const useRemoveDialog = () => { 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({ + 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 c98f8bdf73..e3b5f89949 100644 --- a/web/src/pages/chat/index.less +++ b/web/src/pages/chat/index.less @@ -19,6 +19,12 @@ } } } + .chatAppCardSelected { + :global(.ant-card-body) { + background-color: @gray11; + border-radius: 8px; + } + } } .chatTitleWrapper { width: 220px; @@ -33,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 bde6f4dbfb..33b20e44c3 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,27 +11,39 @@ import { Space, Tag, } from 'antd'; -import ChatContainer from './chat-container'; - -import { ReactComponent as ChatAppCube } from '@/assets/svg/chat-app-cube.svg'; import classNames from 'classnames'; +import { useCallback, useState } from 'react'; import ChatConfigurationModal from './chat-configuration-modal'; +import ChatContainer from './chat-container'; import { - useFetchDialogList, + useClickConversationCard, + useClickDialogCard, + useCreateTemporaryConversation, + useFetchConversationList, + useFetchDialog, + useGetChatSearchParams, useRemoveDialog, + useSelectFirstDialogOnMount, useSetCurrentDialog, } from './hooks'; -import { useSetModalState } from '@/hooks/commonHooks'; -import { useState } from 'react'; import styles from './index.less'; const Chat = () => { - const dialogList = useFetchDialogList(); + const dialogList = useSelectFirstDialogOnMount(); const [activated, setActivated] = useState(''); 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); @@ -46,17 +60,26 @@ const Chat = () => { showModal(); }; + const handleDialogCardClick = (dialogId: string) => () => { + handleClickDialog(dialogId); + }; + + const handleConversationCardClick = (dialogId: string) => () => { + handleClickConversation(dialogId); + }; + + const handleCreateTemporaryConversation = useCallback(() => { + createTemporaryConversation(prologue); + }, [createTemporaryConversation, prologue]); + const items: MenuProps['items'] = [ { key: '1', + onClick: handleCreateTemporaryConversation, label: ( - - 1st menu item - + + New chat + ), }, ]; @@ -101,9 +124,13 @@ const Chat = () => { {dialogList.map((x) => ( @@ -143,7 +170,20 @@ const Chat = () => { -
today
+ + {list.map((x) => ( + +
{x.name}
+
+ ))} +
diff --git a/web/src/pages/chat/interface.ts b/web/src/pages/chat/interface.ts index 9d734dc003..c45da819ed 100644 --- a/web/src/pages/chat/interface.ts +++ b/web/src/pages/chat/interface.ts @@ -1,3 +1,4 @@ +import { IConversation, Message } from '@/interfaces/database/chat'; import { FormInstance } from 'antd'; export interface ISegmentedContentProps { @@ -20,3 +21,11 @@ export interface VariableTableDataType { } export type IPromptConfigParameters = Omit; + +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 de279c079f..ff7f4ffb76 100644 --- a/web/src/pages/chat/model.ts +++ b/web/src/pages/chat/model.ts @@ -1,12 +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 = { @@ -15,6 +19,8 @@ const model: DvaModel = { name: 'kate', dialogList: [], currentDialog: {}, + conversationList: [], + currentConversation: {} as IClientConversation, }, reducers: { save(state, action) { @@ -35,6 +41,36 @@ const model: DvaModel = { 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: { @@ -66,15 +102,40 @@ const model: DvaModel = { }, *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/knowledge/model.ts b/web/src/pages/knowledge/model.ts index 51ab284bc9..04357c9dde 100644 --- a/web/src/pages/knowledge/model.ts +++ b/web/src/pages/knowledge/model.ts @@ -31,7 +31,7 @@ const model: DvaModel = { }, *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({