feat: add overview (#391)
### What problem does this PR solve? feat: render stats charts feat: create api token feat: delete api token feat: add ChatApiKeyModal feat: add RagLineChart Issue link: #345 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@@ -26,6 +26,7 @@ import ParsingActionCell from './parsing-action-cell';
|
||||
import ParsingStatusCell from './parsing-status-cell';
|
||||
import RenameModal from './rename-modal';
|
||||
|
||||
import { formatDate } from '@/utils/date';
|
||||
import styles from './index.less';
|
||||
|
||||
const KnowledgeFile = () => {
|
||||
@@ -94,6 +95,9 @@ const KnowledgeFile = () => {
|
||||
title: t('uploadDate'),
|
||||
dataIndex: 'create_date',
|
||||
key: 'create_date',
|
||||
render(value) {
|
||||
return formatDate(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('chunkMethod'),
|
||||
|
||||
70
web/src/pages/chat/chat-api-key-modal/index.tsx
Normal file
70
web/src/pages/chat/chat-api-key-modal/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import CopyToClipboard from '@/components/copy-to-clipboard';
|
||||
import { useTranslate } from '@/hooks/commonHooks';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IToken } from '@/interfaces/database/chat';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import type { TableProps } from 'antd';
|
||||
import { Button, Modal, Space, Table } from 'antd';
|
||||
import { useOperateApiKey } from '../hooks';
|
||||
|
||||
const ChatApiKeyModal = ({
|
||||
visible,
|
||||
dialogId,
|
||||
hideModal,
|
||||
}: IModalProps<any> & { dialogId: string }) => {
|
||||
const { createToken, removeToken, tokenList, listLoading, creatingLoading } =
|
||||
useOperateApiKey(visible, dialogId);
|
||||
const { t } = useTranslate('chat');
|
||||
|
||||
const columns: TableProps<IToken>['columns'] = [
|
||||
{
|
||||
title: 'Token',
|
||||
dataIndex: 'token',
|
||||
key: 'token',
|
||||
render: (text) => <a>{text}</a>,
|
||||
},
|
||||
{
|
||||
title: t('created'),
|
||||
dataIndex: 'create_date',
|
||||
key: 'create_date',
|
||||
render: (text) => formatDate(text),
|
||||
},
|
||||
{
|
||||
title: t('action'),
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
<CopyToClipboard text={record.token}></CopyToClipboard>
|
||||
<DeleteOutlined
|
||||
onClick={() => removeToken(record.token, record.tenant_id)}
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title={t('apiKey')}
|
||||
open={visible}
|
||||
onCancel={hideModal}
|
||||
style={{ top: 300 }}
|
||||
width={'50vw'}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tokenList}
|
||||
rowKey={'token'}
|
||||
loading={listLoading}
|
||||
/>
|
||||
<Button onClick={createToken} loading={creatingLoading}>
|
||||
{t('createNewKey')}
|
||||
</Button>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatApiKeyModal;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Form, Input, Select, Upload } from 'antd';
|
||||
import { Form, Input, Select, Switch, Upload } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { ISegmentedContentProps } from '../interface';
|
||||
|
||||
@@ -83,6 +83,15 @@ const AssistantSetting = ({ show }: ISegmentedContentProps) => {
|
||||
>
|
||||
<Input.TextArea autoSize={{ minRows: 5 }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('quote')}
|
||||
valuePropName="checked"
|
||||
name={['prompt_config', 'quote']}
|
||||
tooltip={t('quoteTip')}
|
||||
initialValue={true}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('knowledgeBases')}
|
||||
name="kb_ids"
|
||||
|
||||
@@ -172,15 +172,7 @@ const PromptEngine = (
|
||||
>
|
||||
<Slider max={30} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('quote')}
|
||||
valuePropName="checked"
|
||||
name={['prompt_config', 'quote']}
|
||||
tooltip={t('quoteTip')}
|
||||
initialValue={true}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<section className={classNames(styles.variableContainer)}>
|
||||
<Row align={'middle'} justify="end">
|
||||
<Col span={7} className={styles.variableAlign}>
|
||||
|
||||
21
web/src/pages/chat/chat-overview-modal/index.less
Normal file
21
web/src/pages/chat/chat-overview-modal/index.less
Normal file
@@ -0,0 +1,21 @@
|
||||
.chartWrapper {
|
||||
height: 40vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.chartItem {
|
||||
height: 300px;
|
||||
padding: 10px 0 30px;
|
||||
}
|
||||
|
||||
.chartLabel {
|
||||
display: inline-block;
|
||||
padding-left: 60px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.linkText {
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
background-color: #eff8ff;
|
||||
border: 1px;
|
||||
}
|
||||
97
web/src/pages/chat/chat-overview-modal/index.tsx
Normal file
97
web/src/pages/chat/chat-overview-modal/index.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import LineChart from '@/components/line-chart';
|
||||
import { useSetModalState, useTranslate } from '@/hooks/commonHooks';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IDialog, IStats } from '@/interfaces/database/chat';
|
||||
import { Button, Card, DatePicker, Flex, Modal, Space, Typography } from 'antd';
|
||||
import { RangePickerProps } from 'antd/es/date-picker';
|
||||
import dayjs from 'dayjs';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import ChatApiKeyModal from '../chat-api-key-modal';
|
||||
import { useFetchStatsOnMount, useSelectChartStatsList } from '../hooks';
|
||||
import styles from './index.less';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
const ChatOverviewModal = ({
|
||||
visible,
|
||||
hideModal,
|
||||
dialog,
|
||||
}: IModalProps<any> & { dialog: IDialog }) => {
|
||||
const { t } = useTranslate('chat');
|
||||
const chartList = useSelectChartStatsList();
|
||||
|
||||
const {
|
||||
visible: apiKeyVisible,
|
||||
hideModal: hideApiKeyModal,
|
||||
showModal: showApiKeyModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const { pickerValue, setPickerValue } = useFetchStatsOnMount(visible);
|
||||
|
||||
const disabledDate: RangePickerProps['disabledDate'] = (current) => {
|
||||
return current && current > dayjs().endOf('day');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title={t('overview')}
|
||||
open={visible}
|
||||
onCancel={hideModal}
|
||||
width={'100vw'}
|
||||
>
|
||||
<Flex vertical gap={'middle'}>
|
||||
<Card title={dialog.name}>
|
||||
<Flex gap={8} vertical>
|
||||
{t('publicUrl')}
|
||||
<Paragraph copyable className={styles.linkText}>
|
||||
This is a copyable text.
|
||||
</Paragraph>
|
||||
</Flex>
|
||||
<Space size={'middle'}>
|
||||
<Button>{t('preview')}</Button>
|
||||
<Button>{t('embedded')}</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
<Card title={t('backendServiceApi')}>
|
||||
<Flex gap={8} vertical>
|
||||
{t('serviceApiEndpoint')}
|
||||
<Paragraph copyable className={styles.linkText}>
|
||||
This is a copyable text.
|
||||
</Paragraph>
|
||||
</Flex>
|
||||
<Space size={'middle'}>
|
||||
<Button onClick={showApiKeyModal}>{t('apiKey')}</Button>
|
||||
<Button>{t('apiReference')}</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
<Space>
|
||||
<b>{t('dateRange')}</b>
|
||||
<RangePicker
|
||||
disabledDate={disabledDate}
|
||||
value={pickerValue}
|
||||
onChange={setPickerValue}
|
||||
allowClear={false}
|
||||
/>
|
||||
</Space>
|
||||
<div className={styles.chartWrapper}>
|
||||
{Object.keys(chartList).map((x) => (
|
||||
<div key={x} className={styles.chartItem}>
|
||||
<b className={styles.chartLabel}>{t(camelCase(x))}</b>
|
||||
<LineChart data={chartList[x as keyof IStats]}></LineChart>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Flex>
|
||||
<ChatApiKeyModal
|
||||
visible={apiKeyVisible}
|
||||
hideModal={hideApiKeyModal}
|
||||
dialogId={dialog.id}
|
||||
></ChatApiKeyModal>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatOverviewModal;
|
||||
@@ -2,22 +2,28 @@ import { MessageType } from '@/constants/chat';
|
||||
import { fileIconMap } from '@/constants/common';
|
||||
import {
|
||||
useCompleteConversation,
|
||||
useCreateToken,
|
||||
useFetchConversation,
|
||||
useFetchConversationList,
|
||||
useFetchDialog,
|
||||
useFetchDialogList,
|
||||
useFetchStats,
|
||||
useListToken,
|
||||
useRemoveConversation,
|
||||
useRemoveDialog,
|
||||
useRemoveToken,
|
||||
useSelectConversationList,
|
||||
useSelectDialogList,
|
||||
useSelectTokenList,
|
||||
useSetDialog,
|
||||
useUpdateConversation,
|
||||
} from '@/hooks/chatHooks';
|
||||
import { useSetModalState, useShowDeleteConfirm } from '@/hooks/commonHooks';
|
||||
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
|
||||
import { IConversation, IDialog } from '@/interfaces/database/chat';
|
||||
import { IConversation, IDialog, IStats } from '@/interfaces/database/chat';
|
||||
import { IChunk } from '@/interfaces/database/knowledge';
|
||||
import { getFileExtension } from '@/utils';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import omit from 'lodash/omit';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
@@ -704,3 +710,108 @@ export const useGetSendButtonDisabled = () => {
|
||||
return dialogId === '' && conversationId === '';
|
||||
};
|
||||
//#endregion
|
||||
|
||||
//#region API provided for external calls
|
||||
|
||||
type RangeValue = [Dayjs | null, Dayjs | null] | null;
|
||||
|
||||
export const useFetchStatsOnMount = (visible: boolean) => {
|
||||
const fetchStats = useFetchStats();
|
||||
const [pickerValue, setPickerValue] = useState<RangeValue>([
|
||||
dayjs(),
|
||||
dayjs().subtract(7, 'day'),
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && Array.isArray(pickerValue) && pickerValue[0]) {
|
||||
fetchStats({ fromDate: pickerValue[0], toDate: pickerValue[1] });
|
||||
}
|
||||
}, [fetchStats, pickerValue, visible]);
|
||||
|
||||
return {
|
||||
pickerValue,
|
||||
setPickerValue,
|
||||
};
|
||||
};
|
||||
|
||||
export const useOperateApiKey = (visible: boolean, dialogId: string) => {
|
||||
const removeToken = useRemoveToken();
|
||||
const createToken = useCreateToken(dialogId);
|
||||
const listToken = useListToken();
|
||||
const tokenList = useSelectTokenList();
|
||||
const creatingLoading = useOneNamespaceEffectsLoading('chatModel', [
|
||||
'createToken',
|
||||
]);
|
||||
const listLoading = useOneNamespaceEffectsLoading('chatModel', ['list']);
|
||||
|
||||
const showDeleteConfirm = useShowDeleteConfirm();
|
||||
|
||||
const onRemoveToken = (token: string, tenantId: string) => {
|
||||
showDeleteConfirm({
|
||||
onOk: () => removeToken({ dialogId, tokens: [token], tenantId }),
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && dialogId) {
|
||||
listToken(dialogId);
|
||||
}
|
||||
}, [listToken, dialogId, visible]);
|
||||
|
||||
return {
|
||||
removeToken: onRemoveToken,
|
||||
createToken,
|
||||
tokenList,
|
||||
creatingLoading,
|
||||
listLoading,
|
||||
};
|
||||
};
|
||||
|
||||
type ChartStatsType = {
|
||||
[k in keyof IStats]: Array<{ xAxis: string; yAxis: number }>;
|
||||
};
|
||||
|
||||
export const useSelectChartStatsList = (): ChartStatsType => {
|
||||
// const stats: IStats = useSelectStats();
|
||||
const stats = {
|
||||
pv: [
|
||||
['2024-06-01', 1],
|
||||
['2024-07-24', 3],
|
||||
['2024-09-01', 10],
|
||||
],
|
||||
uv: [
|
||||
['2024-02-01', 0],
|
||||
['2024-03-01', 99],
|
||||
['2024-05-01', 3],
|
||||
],
|
||||
speed: [
|
||||
['2024-09-01', 2],
|
||||
['2024-09-01', 3],
|
||||
],
|
||||
tokens: [
|
||||
['2024-09-01', 1],
|
||||
['2024-09-01', 3],
|
||||
],
|
||||
round: [
|
||||
['2024-09-01', 0],
|
||||
['2024-09-01', 3],
|
||||
],
|
||||
thumb_up: [
|
||||
['2024-09-01', 3],
|
||||
['2024-09-01', 9],
|
||||
],
|
||||
};
|
||||
|
||||
return Object.keys(stats).reduce((pre, cur) => {
|
||||
const item = stats[cur as keyof IStats];
|
||||
if (item.length > 0) {
|
||||
pre[cur as keyof IStats] = item.map((x) => ({
|
||||
xAxis: x[0] as string,
|
||||
yAxis: x[1] as number,
|
||||
}));
|
||||
}
|
||||
return pre;
|
||||
}, {} as ChartStatsType);
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -35,7 +35,10 @@ import {
|
||||
useSelectFirstDialogOnMount,
|
||||
} from './hooks';
|
||||
|
||||
import { useTranslate } from '@/hooks/commonHooks';
|
||||
import { useSetModalState, useTranslate } from '@/hooks/commonHooks';
|
||||
import { useSetSelectedRecord } from '@/hooks/logicHooks';
|
||||
import { IDialog } from '@/interfaces/database/chat';
|
||||
import ChatOverviewModal from './chat-overview-modal';
|
||||
import styles from './index.less';
|
||||
|
||||
const Chat = () => {
|
||||
@@ -73,6 +76,12 @@ const Chat = () => {
|
||||
const dialogLoading = useSelectDialogListLoading();
|
||||
const conversationLoading = useSelectConversationListLoading();
|
||||
const { t } = useTranslate('chat');
|
||||
const {
|
||||
visible: overviewVisible,
|
||||
hideModal: hideOverviewModal,
|
||||
showModal: showOverviewModal,
|
||||
} = useSetModalState();
|
||||
const { currentRecord, setRecord } = useSetSelectedRecord<IDialog>();
|
||||
|
||||
useFetchDialogOnMount(dialogId, true);
|
||||
|
||||
@@ -100,6 +109,15 @@ const Chat = () => {
|
||||
onRemoveDialog([dialogId]);
|
||||
};
|
||||
|
||||
const handleShowOverviewModal =
|
||||
(dialog: IDialog): any =>
|
||||
(info: any) => {
|
||||
info?.domEvent?.preventDefault();
|
||||
info?.domEvent?.stopPropagation();
|
||||
setRecord(dialog);
|
||||
showOverviewModal();
|
||||
};
|
||||
|
||||
const handleRemoveConversation =
|
||||
(conversationId: string): MenuItemProps['onClick'] =>
|
||||
({ domEvent }) => {
|
||||
@@ -141,7 +159,9 @@ const Chat = () => {
|
||||
},
|
||||
];
|
||||
|
||||
const buildAppItems = (dialogId: string) => {
|
||||
const buildAppItems = (dialog: IDialog) => {
|
||||
const dialogId = dialog.id;
|
||||
|
||||
const appItems: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
@@ -164,6 +184,17 @@ const Chat = () => {
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
// {
|
||||
// key: '3',
|
||||
// onClick: handleShowOverviewModal(dialog),
|
||||
// label: (
|
||||
// <Space>
|
||||
// <ProfileOutlined />
|
||||
// {t('overview')}
|
||||
// </Space>
|
||||
// ),
|
||||
// },
|
||||
];
|
||||
|
||||
return appItems;
|
||||
@@ -230,7 +261,7 @@ const Chat = () => {
|
||||
</Space>
|
||||
{activated === x.id && (
|
||||
<section>
|
||||
<Dropdown menu={{ items: buildAppItems(x.id) }}>
|
||||
<Dropdown menu={{ items: buildAppItems(x) }}>
|
||||
<ChatAppCube
|
||||
className={styles.cubeIcon}
|
||||
></ChatAppCube>
|
||||
@@ -315,6 +346,11 @@ const Chat = () => {
|
||||
initialName={initialConversationName}
|
||||
loading={conversationRenameLoading}
|
||||
></RenameModal>
|
||||
<ChatOverviewModal
|
||||
visible={overviewVisible}
|
||||
hideModal={hideOverviewModal}
|
||||
dialog={currentRecord}
|
||||
></ChatOverviewModal>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { IConversation, IDialog, Message } from '@/interfaces/database/chat';
|
||||
import {
|
||||
IConversation,
|
||||
IDialog,
|
||||
IStats,
|
||||
IToken,
|
||||
Message,
|
||||
} from '@/interfaces/database/chat';
|
||||
import i18n from '@/locales/config';
|
||||
import chatService from '@/services/chatService';
|
||||
import { message } from 'antd';
|
||||
import omit from 'lodash/omit';
|
||||
import { DvaModel } from 'umi';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { IClientConversation, IMessage } from './interface';
|
||||
@@ -13,6 +20,8 @@ export interface ChatModelState {
|
||||
currentDialog: IDialog;
|
||||
conversationList: IConversation[];
|
||||
currentConversation: IClientConversation;
|
||||
tokenList: IToken[];
|
||||
stats: IStats;
|
||||
}
|
||||
|
||||
const model: DvaModel<ChatModelState> = {
|
||||
@@ -23,6 +32,8 @@ const model: DvaModel<ChatModelState> = {
|
||||
currentDialog: <IDialog>{},
|
||||
conversationList: [],
|
||||
currentConversation: {} as IClientConversation,
|
||||
tokenList: [],
|
||||
stats: {} as IStats,
|
||||
},
|
||||
reducers: {
|
||||
save(state, action) {
|
||||
@@ -60,6 +71,18 @@ const model: DvaModel<ChatModelState> = {
|
||||
currentConversation: { ...payload, message: messageList },
|
||||
};
|
||||
},
|
||||
setTokenList(state, { payload }) {
|
||||
return {
|
||||
...state,
|
||||
tokenList: payload,
|
||||
};
|
||||
},
|
||||
setStats(state, { payload }) {
|
||||
return {
|
||||
...state,
|
||||
stats: payload,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
effects: {
|
||||
@@ -160,6 +183,78 @@ const model: DvaModel<ChatModelState> = {
|
||||
}
|
||||
return data.retcode;
|
||||
},
|
||||
*createToken({ payload }, { call, put }) {
|
||||
const { data } = yield call(chatService.createToken, payload);
|
||||
if (data.retcode === 0) {
|
||||
yield put({
|
||||
type: 'listToken',
|
||||
payload: payload,
|
||||
});
|
||||
message.success(i18n.t('message.created'));
|
||||
}
|
||||
return data.retcode;
|
||||
},
|
||||
*listToken({ payload }, { call, put }) {
|
||||
const { data } = yield call(chatService.listToken, payload);
|
||||
if (data.retcode === 0) {
|
||||
yield put({
|
||||
type: 'setTokenList',
|
||||
payload: data.data,
|
||||
});
|
||||
}
|
||||
return data.retcode;
|
||||
},
|
||||
*removeToken({ payload }, { call, put }) {
|
||||
const { data } = yield call(
|
||||
chatService.removeToken,
|
||||
omit(payload, ['dialogId']),
|
||||
);
|
||||
if (data.retcode === 0) {
|
||||
yield put({
|
||||
type: 'listToken',
|
||||
payload: { dialog_id: payload.dialogId },
|
||||
});
|
||||
}
|
||||
return data.retcode;
|
||||
},
|
||||
*getStats({ payload }, { call, put }) {
|
||||
const { data } = yield call(chatService.getStats, payload);
|
||||
if (data.retcode === 0) {
|
||||
yield put({
|
||||
type: 'setStats',
|
||||
payload: data.data,
|
||||
});
|
||||
}
|
||||
return data.retcode;
|
||||
},
|
||||
*createExternalConversation({ payload }, { call, put }) {
|
||||
const { data } = yield call(
|
||||
chatService.createExternalConversation,
|
||||
payload,
|
||||
);
|
||||
if (data.retcode === 0) {
|
||||
yield put({
|
||||
type: 'getExternalConversation',
|
||||
payload: { conversation_id: payload.conversationId },
|
||||
});
|
||||
}
|
||||
return data.retcode;
|
||||
},
|
||||
*getExternalConversation({ payload }, { call }) {
|
||||
const { data } = yield call(
|
||||
chatService.getExternalConversation,
|
||||
null,
|
||||
payload,
|
||||
);
|
||||
return data.retcode;
|
||||
},
|
||||
*completeExternalConversation({ payload }, { call }) {
|
||||
const { data } = yield call(
|
||||
chatService.completeExternalConversation,
|
||||
payload,
|
||||
);
|
||||
return data.retcode;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user