mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
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:
@ -6,6 +6,21 @@ import zh_HK from 'antd/locale/zh_HK';
|
||||
import React, { ReactNode, useEffect, useState } from 'react';
|
||||
import storage from './utils/authorizationUtil';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import advancedFormat from 'dayjs/plugin/advancedFormat';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
import weekday from 'dayjs/plugin/weekday';
|
||||
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
||||
import weekYear from 'dayjs/plugin/weekYear';
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
dayjs.extend(advancedFormat);
|
||||
dayjs.extend(weekday);
|
||||
dayjs.extend(localeData);
|
||||
dayjs.extend(weekOfYear);
|
||||
dayjs.extend(weekYear);
|
||||
|
||||
const AntLanguageMap = {
|
||||
en: enUS,
|
||||
zh: zhCN,
|
||||
|
||||
27
web/src/components/copy-to-clipboard.tsx
Normal file
27
web/src/components/copy-to-clipboard.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { useTranslate } from '@/hooks/commonHooks';
|
||||
import { CheckOutlined, CopyOutlined } from '@ant-design/icons';
|
||||
import { Tooltip } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { CopyToClipboard as Clipboard, Props } from 'react-copy-to-clipboard';
|
||||
|
||||
const CopyToClipboard = ({ text }: Props) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const { t } = useTranslate('common');
|
||||
|
||||
const handleCopy = () => {
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip title={copied ? t('copied') : t('copy')}>
|
||||
<Clipboard text={text} onCopy={handleCopy}>
|
||||
{copied ? <CheckOutlined /> : <CopyOutlined />}
|
||||
</Clipboard>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyToClipboard;
|
||||
88
web/src/components/line-chart/index.tsx
Normal file
88
web/src/components/line-chart/index.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import {
|
||||
CartesianGrid,
|
||||
Legend,
|
||||
Line,
|
||||
LineChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
import { CategoricalChartProps } from 'recharts/types/chart/generateCategoricalChart';
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: 'Page A',
|
||||
uv: 4000,
|
||||
pv: 2400,
|
||||
},
|
||||
{
|
||||
name: 'Page B',
|
||||
uv: 3000,
|
||||
pv: 1398,
|
||||
},
|
||||
{
|
||||
name: 'Page C',
|
||||
uv: 2000,
|
||||
pv: 9800,
|
||||
},
|
||||
{
|
||||
name: 'Page D',
|
||||
uv: 2780,
|
||||
pv: 3908,
|
||||
},
|
||||
{
|
||||
name: 'Page E',
|
||||
uv: 1890,
|
||||
pv: 4800,
|
||||
},
|
||||
{
|
||||
name: 'Page F',
|
||||
uv: 2390,
|
||||
pv: 3800,
|
||||
},
|
||||
{
|
||||
name: 'Page G',
|
||||
uv: 3490,
|
||||
pv: 4300,
|
||||
},
|
||||
];
|
||||
|
||||
interface IProps extends CategoricalChartProps {
|
||||
data?: Array<{ xAxis: string; yAxis: number }>;
|
||||
}
|
||||
|
||||
const RagLineChart = ({ data }: IProps) => {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart
|
||||
// width={500}
|
||||
// height={300}
|
||||
data={data}
|
||||
margin={
|
||||
{
|
||||
// top: 5,
|
||||
// right: 30,
|
||||
// left: 20,
|
||||
// bottom: 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="xAxis" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="yAxis"
|
||||
stroke="#8884d8"
|
||||
activeDot={{ r: 8 }}
|
||||
/>
|
||||
{/* <Line type="monotone" dataKey="uv" stroke="#82ca9d" /> */}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default RagLineChart;
|
||||
@ -1,4 +1,9 @@
|
||||
import { IConversation, IDialog } from '@/interfaces/database/chat';
|
||||
import {
|
||||
IConversation,
|
||||
IDialog,
|
||||
IStats,
|
||||
IToken,
|
||||
} from '@/interfaces/database/chat';
|
||||
import { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'umi';
|
||||
|
||||
@ -164,3 +169,82 @@ export const useCompleteConversation = () => {
|
||||
|
||||
return completeConversation;
|
||||
};
|
||||
|
||||
// #region API provided for external calls
|
||||
|
||||
export const useCreateToken = (dialogId: string) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const createToken = useCallback(() => {
|
||||
return dispatch<any>({
|
||||
type: 'chatModel/createToken',
|
||||
payload: { dialogId },
|
||||
});
|
||||
}, [dispatch, dialogId]);
|
||||
|
||||
return createToken;
|
||||
};
|
||||
|
||||
export const useListToken = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const listToken = useCallback(
|
||||
(dialogId: string) => {
|
||||
return dispatch<any>({
|
||||
type: 'chatModel/listToken',
|
||||
payload: { dialogId },
|
||||
});
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
return listToken;
|
||||
};
|
||||
|
||||
export const useSelectTokenList = () => {
|
||||
const tokenList: IToken[] = useSelector(
|
||||
(state: any) => state.chatModel.tokenList,
|
||||
);
|
||||
|
||||
return tokenList;
|
||||
};
|
||||
|
||||
export const useRemoveToken = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const removeToken = useCallback(
|
||||
(payload: { tenantId: string; dialogId: string; tokens: string[] }) => {
|
||||
return dispatch<any>({
|
||||
type: 'chatModel/removeToken',
|
||||
payload: payload,
|
||||
});
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
return removeToken;
|
||||
};
|
||||
|
||||
export const useFetchStats = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const fetchStats = useCallback(
|
||||
(payload: any) => {
|
||||
return dispatch<any>({
|
||||
type: 'chatModel/getStats',
|
||||
payload,
|
||||
});
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
return fetchStats;
|
||||
};
|
||||
|
||||
export const useSelectStats = () => {
|
||||
const stats: IStats = useSelector((state: any) => state.chatModel.stats);
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
//#endregion
|
||||
|
||||
@ -91,3 +91,21 @@ export interface Docagg {
|
||||
// term_similarity: number;
|
||||
// vector_similarity: number;
|
||||
// }
|
||||
|
||||
export interface IToken {
|
||||
create_date: string;
|
||||
create_time: number;
|
||||
tenant_id: string;
|
||||
token: string;
|
||||
update_date?: any;
|
||||
update_time?: any;
|
||||
}
|
||||
|
||||
export interface IStats {
|
||||
pv: [string, number][];
|
||||
uv: [string, number][];
|
||||
speed: [string, number][];
|
||||
tokens: [string, number][];
|
||||
round: [string, number][];
|
||||
thumb_up: [string, number][];
|
||||
}
|
||||
|
||||
@ -20,6 +20,8 @@ export default {
|
||||
language: 'Language',
|
||||
languageMessage: 'Please input your language!',
|
||||
languagePlaceholder: 'select your language',
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
},
|
||||
login: {
|
||||
login: 'Sign in',
|
||||
@ -335,6 +337,24 @@ export default {
|
||||
'This sets the maximum length of the model’s output, measured in the number of tokens (words or pieces of words).',
|
||||
quote: 'Show Quote',
|
||||
quoteTip: 'Should the source of the original text be displayed?',
|
||||
overview: 'Overview',
|
||||
pv: 'Number of messages',
|
||||
uv: 'Active user number',
|
||||
speed: 'Token output speed',
|
||||
tokens: 'Consume the token number',
|
||||
round: 'Session Interaction Number',
|
||||
thumbUp: 'customer satisfaction',
|
||||
publicUrl: 'Public URL',
|
||||
preview: 'Preview',
|
||||
embedded: 'Embedded',
|
||||
serviceApiEndpoint: 'Service API Endpoint',
|
||||
apiKey: 'Api Key',
|
||||
apiReference: 'Api Reference',
|
||||
dateRange: 'Date Range:',
|
||||
backendServiceApi: 'Backend service API',
|
||||
createNewKey: 'Create new key',
|
||||
created: 'Created',
|
||||
action: 'Action',
|
||||
},
|
||||
setting: {
|
||||
profile: 'Profile',
|
||||
|
||||
@ -15,11 +15,13 @@ export default {
|
||||
edit: '編輯',
|
||||
upload: '上傳',
|
||||
english: '英語',
|
||||
chinese: '中文簡體',
|
||||
traditionalChinese: '中文繁體',
|
||||
chinese: '簡體中文',
|
||||
traditionalChinese: '繁體中文',
|
||||
language: '語言',
|
||||
languageMessage: '請輸入語言',
|
||||
languagePlaceholder: '請選擇語言',
|
||||
copy: '複製',
|
||||
copied: '複製成功',
|
||||
},
|
||||
login: {
|
||||
login: '登入',
|
||||
@ -269,7 +271,7 @@ export default {
|
||||
systemMessage: '請輸入',
|
||||
systemTip:
|
||||
'當LLM回答問題時,你需要LLM遵循的說明,比如角色設計、答案長度和答案語言等。',
|
||||
topN: 'top n',
|
||||
topN: 'Top N',
|
||||
topNTip: `並非所有相似度得分高於“相似度閾值”的塊都會被提供給法學碩士。LLM 只能看到這些“Top N”塊。`,
|
||||
variable: '變量',
|
||||
variableTip: `如果您使用对话 API,变量可能会帮助您使用不同的策略与客户聊天。
|
||||
@ -310,6 +312,24 @@ export default {
|
||||
'這設置了模型輸出的最大長度,以標記(單詞或單詞片段)的數量來衡量。',
|
||||
quote: '顯示引文',
|
||||
quoteTip: '是否應該顯示原文出處?',
|
||||
overview: '概覽',
|
||||
pv: '消息數',
|
||||
uv: '活躍用戶數',
|
||||
speed: 'Token 輸出速度',
|
||||
tokens: '消耗Token數',
|
||||
round: '會話互動數',
|
||||
thumbUp: '用戶滿意度',
|
||||
publicUrl: '公共url',
|
||||
preview: '預覽',
|
||||
embedded: '嵌入',
|
||||
serviceApiEndpoint: '服務API端點',
|
||||
apiKey: 'API鍵',
|
||||
apiReference: 'API參考',
|
||||
dateRange: '日期範圍:',
|
||||
backendServiceApi: '後端服務API',
|
||||
createNewKey: '創建新密鑰',
|
||||
created: '創建於',
|
||||
action: '操作',
|
||||
},
|
||||
setting: {
|
||||
profile: '概述',
|
||||
|
||||
@ -15,11 +15,13 @@ export default {
|
||||
edit: '编辑',
|
||||
upload: '上传',
|
||||
english: '英文',
|
||||
chinese: '中文简体',
|
||||
traditionalChinese: '中文繁体',
|
||||
chinese: '简体中文',
|
||||
traditionalChinese: '繁体中文',
|
||||
language: '语言',
|
||||
languageMessage: '请输入语言',
|
||||
languagePlaceholder: '请选择语言',
|
||||
copy: '复制',
|
||||
copied: '复制成功',
|
||||
},
|
||||
login: {
|
||||
login: '登录',
|
||||
@ -326,6 +328,24 @@ export default {
|
||||
'这设置了模型输出的最大长度,以标记(单词或单词片段)的数量来衡量。',
|
||||
quote: '显示引文',
|
||||
quoteTip: '是否应该显示原文出处?',
|
||||
overview: '概览',
|
||||
pv: '消息数',
|
||||
uv: '活跃用户数',
|
||||
speed: 'Token 输出速度',
|
||||
tokens: '消耗Token数',
|
||||
round: '会话互动数',
|
||||
thumbUp: '用户满意度',
|
||||
publicUrl: '公共Url',
|
||||
preview: '预览',
|
||||
embedded: '嵌入',
|
||||
serviceApiEndpoint: '服务API端点',
|
||||
apiKey: 'API键',
|
||||
apiReference: 'API参考',
|
||||
dateRange: '日期范围:',
|
||||
backendServiceApi: '后端服务API',
|
||||
createNewKey: '创建新密钥',
|
||||
created: '创建于',
|
||||
action: '操作',
|
||||
},
|
||||
setting: {
|
||||
profile: '概要',
|
||||
|
||||
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -12,6 +12,13 @@ const {
|
||||
completeConversation,
|
||||
listConversation,
|
||||
removeConversation,
|
||||
createToken,
|
||||
listToken,
|
||||
removeToken,
|
||||
getStats,
|
||||
createExternalConversation,
|
||||
getExternalConversation,
|
||||
completeExternalConversation,
|
||||
} = api;
|
||||
|
||||
const methods = {
|
||||
@ -51,6 +58,34 @@ const methods = {
|
||||
url: removeConversation,
|
||||
method: 'post',
|
||||
},
|
||||
createToken: {
|
||||
url: createToken,
|
||||
method: 'post',
|
||||
},
|
||||
listToken: {
|
||||
url: listToken,
|
||||
method: 'get',
|
||||
},
|
||||
removeToken: {
|
||||
url: removeToken,
|
||||
method: 'post',
|
||||
},
|
||||
getStats: {
|
||||
url: getStats,
|
||||
method: 'get',
|
||||
},
|
||||
createExternalConversation: {
|
||||
url: createExternalConversation,
|
||||
method: 'post',
|
||||
},
|
||||
getExternalConversation: {
|
||||
url: getExternalConversation,
|
||||
method: 'get',
|
||||
},
|
||||
completeExternalConversation: {
|
||||
url: completeExternalConversation,
|
||||
method: 'post',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const chatService = registerServer<keyof typeof methods>(methods, request);
|
||||
|
||||
@ -3,7 +3,7 @@ let api_host = `/v1`;
|
||||
export { api_host };
|
||||
|
||||
export default {
|
||||
// 用户
|
||||
// user
|
||||
login: `${api_host}/user/login`,
|
||||
logout: `${api_host}/user/logout`,
|
||||
register: `${api_host}/user/register`,
|
||||
@ -12,21 +12,21 @@ export default {
|
||||
tenant_info: `${api_host}/user/tenant_info`,
|
||||
set_tenant_info: `${api_host}/user/set_tenant_info`,
|
||||
|
||||
// 模型管理
|
||||
// llm model
|
||||
factories_list: `${api_host}/llm/factories`,
|
||||
llm_list: `${api_host}/llm/list`,
|
||||
my_llm: `${api_host}/llm/my_llms`,
|
||||
set_api_key: `${api_host}/llm/set_api_key`,
|
||||
add_llm: `${api_host}/llm/add_llm`,
|
||||
|
||||
//知识库管理
|
||||
// knowledge base
|
||||
kb_list: `${api_host}/kb/list`,
|
||||
create_kb: `${api_host}/kb/create`,
|
||||
update_kb: `${api_host}/kb/update`,
|
||||
rm_kb: `${api_host}/kb/rm`,
|
||||
get_kb_detail: `${api_host}/kb/detail`,
|
||||
|
||||
// chunk管理
|
||||
// chunk
|
||||
chunk_list: `${api_host}/chunk/list`,
|
||||
create_chunk: `${api_host}/chunk/create`,
|
||||
set_chunk: `${api_host}/chunk/set`,
|
||||
@ -35,7 +35,7 @@ export default {
|
||||
rm_chunk: `${api_host}/chunk/rm`,
|
||||
retrieval_test: `${api_host}/chunk/retrieval_test`,
|
||||
|
||||
// 文件管理
|
||||
// document
|
||||
upload: `${api_host}/document/upload`,
|
||||
get_document_list: `${api_host}/document/list`,
|
||||
document_change_status: `${api_host}/document/change_status`,
|
||||
@ -48,14 +48,22 @@ export default {
|
||||
get_document_file: `${api_host}/document/get`,
|
||||
document_upload: `${api_host}/document/upload`,
|
||||
|
||||
// chat
|
||||
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`,
|
||||
getConversation: `${api_host}/conversation/get`,
|
||||
listConversation: `${api_host}/conversation/list`,
|
||||
removeConversation: `${api_host}/conversation/rm`,
|
||||
completeConversation: `${api_host}/conversation/completion`,
|
||||
// chat for external
|
||||
createToken: `${api_host}/api/new_token`,
|
||||
listToken: `${api_host}/api/token_list`,
|
||||
removeToken: `${api_host}/api/rm`,
|
||||
getStats: `${api_host}/api/stats`,
|
||||
createExternalConversation: `${api_host}/api/new_conversation`,
|
||||
getExternalConversation: `${api_host}/api/conversation`,
|
||||
completeExternalConversation: `${api_host}/api/completion`,
|
||||
};
|
||||
|
||||
17
web/src/utils/commonUtil.ts
Normal file
17
web/src/utils/commonUtil.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import isObject from 'lodash/isObject';
|
||||
import snakeCase from 'lodash/snakeCase';
|
||||
|
||||
export const isFormData = (data: unknown): data is FormData => {
|
||||
return data instanceof FormData;
|
||||
};
|
||||
|
||||
export const convertTheKeysOfTheObjectToSnake = (data: unknown) => {
|
||||
if (isObject(data) && !isFormData(data)) {
|
||||
return Object.keys(data).reduce<Record<string, any>>((pre, cur) => {
|
||||
const value = (data as Record<string, any>)[cur];
|
||||
pre[isFormData(value) ? cur : snakeCase(cur)] = value;
|
||||
return pre;
|
||||
}, {});
|
||||
}
|
||||
return data;
|
||||
};
|
||||
@ -1,20 +1,20 @@
|
||||
import moment from 'moment';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export function today() {
|
||||
return formatDate(moment());
|
||||
return formatDate(dayjs());
|
||||
}
|
||||
|
||||
export function lastDay() {
|
||||
return formatDate(moment().subtract(1, 'days'));
|
||||
return formatDate(dayjs().subtract(1, 'days'));
|
||||
}
|
||||
|
||||
export function lastWeek() {
|
||||
return formatDate(moment().subtract(1, 'weeks'));
|
||||
return formatDate(dayjs().subtract(1, 'weeks'));
|
||||
}
|
||||
|
||||
export function formatDate(date: any) {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
return moment(date).format('DD/MM/YYYY');
|
||||
return dayjs(date).format('DD/MM/YYYY');
|
||||
}
|
||||
|
||||
@ -8,16 +8,20 @@ const registerServer = <T extends string>(
|
||||
) => {
|
||||
const server: Service<T> = {} as Service<T>;
|
||||
for (let key in opt) {
|
||||
server[key] = (params) => {
|
||||
server[key] = (params: any, urlAppendix?: string) => {
|
||||
let url = opt[key].url;
|
||||
if (urlAppendix) {
|
||||
url = url + '/' + urlAppendix;
|
||||
}
|
||||
if (opt[key].method === 'post' || opt[key].method === 'POST') {
|
||||
return request(opt[key].url, {
|
||||
return request(url, {
|
||||
method: opt[key].method,
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
if (opt[key].method === 'get' || opt[key].method === 'GET') {
|
||||
return request.get(opt[key].url, {
|
||||
return request.get(url, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import authorizationUtil from '@/utils/authorizationUtil';
|
||||
import { message, notification } from 'antd';
|
||||
import { history } from 'umi';
|
||||
import { RequestMethod, extend } from 'umi-request';
|
||||
import { convertTheKeysOfTheObjectToSnake } from './commonUtil';
|
||||
|
||||
const ABORT_REQUEST_ERR_MESSAGE = 'The user aborted a request.'; // 手动中断请求。errorHandler 抛出的error message
|
||||
|
||||
@ -87,10 +88,15 @@ const request: RequestMethod = extend({
|
||||
|
||||
request.interceptors.request.use((url: string, options: any) => {
|
||||
const authorization = authorizationUtil.getAuthorization();
|
||||
const data = convertTheKeysOfTheObjectToSnake(options.data);
|
||||
const params = convertTheKeysOfTheObjectToSnake(options.params);
|
||||
|
||||
return {
|
||||
url,
|
||||
options: {
|
||||
...options,
|
||||
// data,
|
||||
// params,
|
||||
headers: {
|
||||
...(options.skipToken ? undefined : { [Authorization]: authorization }),
|
||||
...options.headers,
|
||||
|
||||
Reference in New Issue
Block a user