* feat: set chat configuration to backend * feat: exclude unEnabled variables * feat: delete chat dialog * feat: fetch conversationtags/v0.1.0
| @@ -0,0 +1,4 @@ | |||
| export enum MessageType { | |||
| Assistant = 'assistant', | |||
| User = 'user', | |||
| } | |||
| @@ -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(); | |||
| } | |||
| }; | |||
| }, []); | |||
| }; | |||
| @@ -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; | |||
| }; | |||
| @@ -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; | |||
| } | |||
| @@ -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> | |||
| @@ -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, | |||
| @@ -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'} | |||
| @@ -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; | |||
| } | |||
| @@ -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) => { | |||
| @@ -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> | |||
| @@ -1,3 +1,18 @@ | |||
| .chatContainer { | |||
| padding: 0 24px 24px; | |||
| } | |||
| .messageItem { | |||
| .messageItemContent { | |||
| display: inline-block; | |||
| width: 300px; | |||
| } | |||
| } | |||
| .messageItemLeft { | |||
| text-align: left; | |||
| } | |||
| .messageItemRight { | |||
| text-align: right; | |||
| } | |||
| @@ -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..." | |||
| @@ -5,3 +5,10 @@ export const variableEnabledFieldMap = { | |||
| frequencyPenaltyEnabled: 'frequency_penalty', | |||
| maxTokensEnabled: 'max_tokens', | |||
| }; | |||
| export enum ChatSearchParams { | |||
| DialogId = 'dialogId', | |||
| ConversationId = 'conversationId', | |||
| } | |||
| export const EmptyConversationId = 'empty'; | |||
| @@ -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 | |||
| @@ -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%; | |||
| @@ -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> | |||
| ); | |||
| }; | |||
| @@ -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[]; | |||
| } | |||
| @@ -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, | |||
| }, | |||
| }); | |||
| } | |||
| }, | |||
| }, | |||
| }; | |||
| @@ -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]}`, | |||
| ); | |||
| }; | |||
| @@ -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({ | |||
| @@ -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', | |||
| @@ -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`, | |||