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/commonHooks.ts b/web/src/hooks/commonHooks.ts new file mode 100644 index 0000000000..e4361cd29f --- /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(); + let callback: ReturnType = () => {}; + 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 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 968d277b26..0ce8af7c74 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 64943c9537..3978961be3 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.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', + ...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 = ( @@ -89,6 +111,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..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,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>>, + { show }: ISegmentedContentProps, + ref: ForwardedRef>, ) => { const [dataSource, setDataSource] = useState([]); + 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['columns'] = [ { title: 'key', @@ -146,6 +140,10 @@ const PromptEngine = ( }, ]; + useEffect(() => { + setDataSource(parameters); + }, [parameters]); + return (
- + 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(); @@ -20,10 +30,336 @@ 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 }; +}; + +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) => () => { + return dispatch({ + type: 'chatModel/removeDialog', + payload: { + dialog_ids: dialogIds, + }, + }); + }; + + const onRemoveDialog = (dialogIds: Array) => { + 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({ + 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 4400e7eeee..e3b5f89949 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 f33bbd338b..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,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(''); + 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: ( - - 1st menu item - - ), - }, - ]; + 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: ( - - Edit - - ), - }, - { type: 'divider' }, - { - key: '2', - label: ( - - - Delete chat + New chat ), }, ]; + const buildAppItems = (dialogId: string) => { + const appItems: MenuProps['items'] = [ + { + key: '1', + onClick: handleShowChatConfigurationModal(dialogId), + label: ( + + + Edit + + ), + }, + { type: 'divider' }, + { + key: '2', + onClick: () => onRemoveDialog([dialogId]), + label: ( + + + Delete chat + + ), + }, + ]; + + return appItems; + }; + return ( - - {({ visible, showModal, hideModal }) => { - return ( - <> - - - - ); - }} - - + - + {dialogList.map((x) => ( @@ -109,7 +142,7 @@ const Chat = () => { {activated === x.id && (
- +
@@ -117,7 +150,7 @@ const Chat = () => {
))} -
+
@@ -137,11 +170,30 @@ const Chat = () => {
-
today
+ + {list.map((x) => ( + +
{x.name}
+
+ ))} +
+ ); }; diff --git a/web/src/pages/chat/interface.ts b/web/src/pages/chat/interface.ts new file mode 100644 index 0000000000..c45da819ed --- /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; + +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 dd5c5d322a..ff7f4ffb76 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 = { @@ -13,6 +18,9 @@ const model: DvaModel = { state: { name: 'kate', dialogList: [], + currentDialog: {}, + conversationList: [], + currentConversation: {} as IClientConversation, }, reducers: { save(state, action) { @@ -27,11 +35,50 @@ const model: DvaModel = { 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 = { 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 = { }, *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 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]}`, + ); +}; 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({ 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`,