feat: fetch conversation and delete chat dialog (#69)

* feat: set chat configuration to backend

* feat: exclude unEnabled variables

* feat: delete chat dialog

* feat: fetch conversation
This commit is contained in:
balibabu 2024-02-22 17:14:25 +08:00 committed by GitHub
parent cacd36c5e1
commit 5a0f1d2b84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 784 additions and 116 deletions

View File

@ -0,0 +1,4 @@
export enum MessageType {
Assistant = 'assistant',
User = 'user',
}

View File

@ -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();
}
};
}, []);
};

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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>

View File

@ -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,

View File

@ -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'}

View File

@ -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;
}

View File

@ -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) => {

View File

@ -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>

View File

@ -1,3 +1,18 @@
.chatContainer {
padding: 0 24px 24px;
}
.messageItem {
.messageItemContent {
display: inline-block;
width: 300px;
}
}
.messageItemLeft {
text-align: left;
}
.messageItemRight {
text-align: right;
}

View File

@ -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..."

View File

@ -5,3 +5,10 @@ export const variableEnabledFieldMap = {
frequencyPenaltyEnabled: 'frequency_penalty',
maxTokensEnabled: 'max_tokens',
};
export enum ChatSearchParams {
DialogId = 'dialogId',
ConversationId = 'conversationId',
}
export const EmptyConversationId = 'empty';

View File

@ -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,
});
// Its 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

View File

@ -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%;

View File

@ -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,24 +53,42 @@ const Chat = () => {
setActivated('');
};
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 items: MenuProps['items'] = [
{
key: '1',
onClick: handleCreateTemporaryConversation,
label: (
<a
target="_blank"
rel="noopener noreferrer"
href="https://www.antgroup.com"
>
1st menu item
</a>
<Space>
<EditOutlined /> New chat
</Space>
),
},
];
const buildAppItems = (dialogId: string) => {
const appItems: MenuProps['items'] = [
{
key: '1',
onClick: handleShowChatConfigurationModal(dialogId),
label: (
<Space>
<EditOutlined />
@ -60,6 +99,7 @@ const Chat = () => {
{ type: 'divider' },
{
key: '2',
onClick: () => onRemoveDialog([dialogId]),
label: (
<Space>
<DeleteOutlined />
@ -69,35 +109,28 @@ const Chat = () => {
},
];
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()}>
<Button type="primary" onClick={handleShowChatConfigurationModal()}>
Create an Assistant
</Button>
<ChatConfigurationModal
visible={visible}
showModal={showModal}
hideModal={hideModal}
></ChatConfigurationModal>
</>
);
}}
</ModalManager>
<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>
);
};

View File

@ -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[];
}

View File

@ -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,
},
});
}
},
},
};

View File

@ -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]}`,
);
};

View File

@ -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({

View File

@ -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',

View File

@ -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`,