Merge #5 into develop_web from feature/20251219_zy
optimize: check en.ts * feature/20251219_zy: (6 commits) optimize: UI update components: Add Chat component optimize: 1. stream request optimize; 2. replace Chat component feature: memory extraction engine debug switch to streaming output feature: add api key optimize: check en.ts Signed-off-by: zhaoying <zhaoying@redbearai.com> Merged-by: zhaoying <zhaoying@redbearai.com> CR-link: https://codeup.aliyun.com/redbearai/python/redbear-mem-open/change/5
This commit is contained in:
102
web/src/views/ApiKeyManagement/components/ApiKeyDetailModal.tsx
Normal file
102
web/src/views/ApiKeyManagement/components/ApiKeyDetailModal.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Switch, Button } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ApiKey, ApiKeyModalRef } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { getApiKey } from '@/api/apiKey';
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import Tag from '@/components/Tag'
|
||||
import { maskApiKeys } from '@/utils/apiKeyReplacer';
|
||||
|
||||
const ApiKeyDetailModal = forwardRef<ApiKeyModalRef, { handleCopy: (content: string) => void }>(({ handleCopy }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [data, setData] = useState<ApiKey>({} as ApiKey)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handleOpen = (apiKey?: ApiKey) => {
|
||||
if (apiKey?.id) {
|
||||
getApiKey(apiKey.id)
|
||||
.then((res) => {
|
||||
setVisible(true);
|
||||
setData(res as ApiKey)
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('apiKey.viewDetail')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
>
|
||||
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:mb-4">{t('apiKey.baseInfo')}</div>
|
||||
{['id', 'name', 'is_expired', 'created_at'].map((key, index) => (
|
||||
<div key={key} className={clsx("rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px]", {
|
||||
'rb:mt-3': index !== 0
|
||||
})}>
|
||||
<span className="rb:text-[#5B6167]">{t(`apiKey.${key}`)}</span>
|
||||
<span className="rb:text-right rb:flex-1 rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">
|
||||
{ key === 'created_at'
|
||||
? formatDateTime(data[key], 'YYYY-MM-DD HH:mm:ss')
|
||||
: key === 'is_expired'
|
||||
? <Tag>{data[key] ? t('apiKey.inactive') : t('apiKey.active')}</Tag>
|
||||
: String(data[key as keyof ApiKey])
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-5 rb:p-[8px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:leading-5">
|
||||
{maskApiKeys(data.api_key)}
|
||||
|
||||
<Button className="rb:px-2! rb:h-7! rb:group" onClick={() => handleCopy(data.api_key)}>
|
||||
<div
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
></div>
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:my-4">{t('apiKey.permissionInfo')}</div>
|
||||
|
||||
<div className="rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px] rb:mt-3">
|
||||
<span className="rb:text-[#5B6167]">{t(`apiKey.memoryEngine`)}</span>
|
||||
<span>
|
||||
<Switch checked={data.scopes?.includes('memory')} disabled />
|
||||
</span>
|
||||
</div>
|
||||
<div className="rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px] rb:mt-3">
|
||||
<span className="rb:text-[#5B6167]">{t(`apiKey.knowledgeBase`)}</span>
|
||||
<span>
|
||||
<Switch checked={data.scopes?.includes('rag')} disabled />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 高级设置 */}
|
||||
{data.expires_at && <>
|
||||
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:my-4">{t('apiKey.advancedSettings')}</div>
|
||||
|
||||
<div className="rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px] rb:mt-3">
|
||||
<span className="rb:text-[#5B6167]">{t(`apiKey.expires_at`)}</span>
|
||||
<span>
|
||||
{data.expires_at ? formatDateTime(data.expires_at as number, 'yyyy-MM-DD') : '-'}
|
||||
</span>
|
||||
</div>
|
||||
</>}
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ApiKeyDetailModal;
|
||||
153
web/src/views/ApiKeyManagement/components/ApiKeyModal.tsx
Normal file
153
web/src/views/ApiKeyManagement/components/ApiKeyModal.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, Switch, App, DatePicker } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ApiKey, ApiKeyModalRef } from '../types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
import dayjs from 'dayjs'
|
||||
import { createApiKey, updateApiKey } from '@/api/apiKey';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface CreateModalProps {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const ApiKeyModal = forwardRef<ApiKeyModalRef, CreateModalProps>(({
|
||||
refresh,
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<ApiKey>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [editVo, setEditVo] = useState<ApiKey | null>(null);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false);
|
||||
setEditVo(null);
|
||||
};
|
||||
|
||||
const handleOpen = (apiKey?: ApiKey) => {
|
||||
if (apiKey?.id) {
|
||||
const { scopes = [], expires_at } = apiKey
|
||||
// 编辑模式,填充表单
|
||||
form.setFieldsValue({
|
||||
name: apiKey.name,
|
||||
description: apiKey.description,
|
||||
memory: scopes.includes('memory'),
|
||||
rag: scopes.includes('rag'),
|
||||
expires_at: expires_at ? dayjs(expires_at) : undefined
|
||||
});
|
||||
setEditVo(apiKey);
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = async () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
const { memory, rag, expires_at, ...rest } = values
|
||||
let scopes = []
|
||||
|
||||
if (memory) {
|
||||
scopes.push('memory')
|
||||
}
|
||||
if (rag) {
|
||||
scopes.push('rag')
|
||||
}
|
||||
// 准备新的/更新的API Key数据
|
||||
const apiKeyData = {
|
||||
...rest,
|
||||
scopes,
|
||||
expires_at: expires_at ? dayjs(expires_at.valueOf()).endOf('day').valueOf() : null,
|
||||
type: 'service'
|
||||
};
|
||||
setLoading(true)
|
||||
const req = editVo?.id ? updateApiKey(editVo.id, apiKeyData as ApiKey) : createApiKey(apiKeyData as ApiKey)
|
||||
|
||||
req.then(() => {
|
||||
refresh();
|
||||
handleClose();
|
||||
message.success(t(editVo ? 'common.updateSuccess' : 'common.createSuccess'));
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={editVo ? t('apiKey.updateApiKey') : t('apiKey.createApiKey')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:mb-4">{t('apiKey.baseInfo')}</div>
|
||||
<FormItem
|
||||
name="name"
|
||||
label={t('apiKey.name')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="description"
|
||||
label={t('apiKey.description')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} rows={3} />
|
||||
</FormItem>
|
||||
|
||||
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:mb-4">{t('apiKey.permissionInfo')}</div>
|
||||
|
||||
<FormItem
|
||||
name="memory"
|
||||
label={t('apiKey.memoryEngine')}
|
||||
layout="horizontal"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="rag"
|
||||
label={t('apiKey.knowledgeBase')}
|
||||
layout="horizontal"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</FormItem>
|
||||
|
||||
{/* 高级设置 */}
|
||||
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:mb-4">{t('apiKey.advancedSettings')}</div>
|
||||
|
||||
<FormItem
|
||||
name="expires_at"
|
||||
label={t('apiKey.expires_at')}
|
||||
>
|
||||
<DatePicker
|
||||
className="rb:w-full"
|
||||
disabledDate={(current) => current && current < dayjs().subtract(1, 'day').endOf('day')}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ApiKeyModal;
|
||||
125
web/src/views/ApiKeyManagement/index.tsx
Normal file
125
web/src/views/ApiKeyManagement/index.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, App, Space } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import { DeleteOutlined, EditOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import type { ApiKey, ApiKeyModalRef } from './types';
|
||||
import ApiKeyModal from './components/ApiKeyModal';
|
||||
import ApiKeyDetailModal from './components/ApiKeyDetailModal';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import { getApiKeyListUrl, deleteApiKey } from '@/api/apiKey';
|
||||
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
import Tag from '@/components/Tag'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { maskApiKeys } from '@/utils/apiKeyReplacer';
|
||||
|
||||
const ApiKeyManagement: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { modal, message } = App.useApp();
|
||||
const apiKeyModalRef = useRef<ApiKeyModalRef>(null);
|
||||
const apiKeyDetailModalRef = useRef<ApiKeyModalRef>(null)
|
||||
const scrollListRef = useRef<PageScrollListRef>(null)
|
||||
|
||||
const refresh = () => {
|
||||
scrollListRef.current?.refresh();
|
||||
}
|
||||
|
||||
const handleEdit = (item?: ApiKey) => {
|
||||
apiKeyModalRef.current?.handleOpen(item);
|
||||
}
|
||||
const handleView = (item: ApiKey) => {
|
||||
apiKeyDetailModalRef.current?.handleOpen(item);
|
||||
}
|
||||
const handleDelete = (item: ApiKey) => {
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: item.name }),
|
||||
okText: t('common.delete'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
deleteApiKey(item.id)
|
||||
.then(() => {
|
||||
refresh();
|
||||
message.success(t('common.deleteSuccess'))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleCopy = (content: string) => {
|
||||
copy(content)
|
||||
message.success(t('common.copySuccess'))
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="rb:flex rb:justify-end rb:mb-3 rb:p-4">
|
||||
<Button type="primary" onClick={() => handleEdit()}>
|
||||
{t('apiKey.createApiKey')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<PageScrollList
|
||||
ref={scrollListRef}
|
||||
url={getApiKeyListUrl}
|
||||
query={{ is_active: true, type: 'service' }}
|
||||
column={2}
|
||||
renderItem={(item: Record<string, unknown>) => {
|
||||
let apiKeyItem = item as unknown as ApiKey
|
||||
return (
|
||||
<RbCard
|
||||
title={apiKeyItem.name}
|
||||
>
|
||||
{['id', 'is_expired', 'created_at'].map((key, index) => (
|
||||
<div key={key} className={clsx("rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px]", {
|
||||
'rb:mt-3': index !== 0
|
||||
})}>
|
||||
<span className="rb:text-[#5B6167]">{t(`apiKey.${key}`)}</span>
|
||||
<span>
|
||||
{ key === 'created_at'
|
||||
? formatDateTime(apiKeyItem[key], 'YYYY-MM-DD HH:mm:ss')
|
||||
: key === 'is_expired'
|
||||
? <Tag>{apiKeyItem[key] ? t('apiKey.inactive') : t('apiKey.active')}</Tag>
|
||||
: String(apiKeyItem[key as keyof ApiKey])
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-5 rb:p-[8px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:leading-5">
|
||||
{maskApiKeys(apiKeyItem.api_key)}
|
||||
|
||||
<Button className="rb:px-2! rb:h-7! rb:group" onClick={() => handleCopy(apiKeyItem.api_key)}>
|
||||
<div
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
></div>
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Space className="rb:pt-2 rb:min-h-6.25">
|
||||
{apiKeyItem.scopes?.includes('memory') && <Tag>{t('apiKey.memoryEngine')}</Tag>}
|
||||
{apiKeyItem.scopes?.includes('rag') && <Tag color="success">{t('apiKey.knowledgeBase')}</Tag>}
|
||||
</Space>
|
||||
|
||||
<div className="rb:mt-5 rb:flex rb:justify-end rb:gap-2.5">
|
||||
<Button icon={<EyeOutlined />} onClick={() => handleView(apiKeyItem)}></Button>
|
||||
<Button icon={<EditOutlined />} onClick={() => handleEdit(apiKeyItem)}></Button>
|
||||
<Button icon={<DeleteOutlined />} onClick={() => handleDelete(apiKeyItem)}></Button>
|
||||
</div>
|
||||
</RbCard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<ApiKeyModal
|
||||
ref={apiKeyModalRef}
|
||||
refresh={refresh}
|
||||
/>
|
||||
<ApiKeyDetailModal
|
||||
ref={apiKeyDetailModalRef}
|
||||
handleCopy={handleCopy}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeyManagement;
|
||||
40
web/src/views/ApiKeyManagement/types.ts
Normal file
40
web/src/views/ApiKeyManagement/types.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { maskApiKeys } from '@/utils/apiKeyReplacer'
|
||||
|
||||
export interface ApiKey {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
type: 'agent' | 'multi_agent' | 'workflow' | 'service';
|
||||
scopes?: string[]; // 'memory' | 'rag' | 'app'
|
||||
|
||||
api_key: string;
|
||||
is_active: boolean;
|
||||
is_expired: boolean;
|
||||
created_at: number;
|
||||
expires_at?: number | Dayjs;
|
||||
memory?: boolean;
|
||||
rag?: boolean;
|
||||
|
||||
|
||||
updated_at: string;
|
||||
qps_limit?: number;
|
||||
daily_request_limit?: number;
|
||||
|
||||
rate_limit?: number;
|
||||
total_requests: number;
|
||||
quota_used: number;
|
||||
quota_limit: number;
|
||||
}
|
||||
|
||||
export interface ApiKeyModalRef {
|
||||
handleOpen: (apiKey?: ApiKey) => void;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取掩码后的API密钥
|
||||
*/
|
||||
export const getMaskedApiKey = (apiKey: string): string => {
|
||||
return maskApiKeys(apiKey)
|
||||
}
|
||||
@@ -239,6 +239,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
return [
|
||||
...(prev || []).map(item => ({
|
||||
...item,
|
||||
conversation_id: undefined,
|
||||
list: []
|
||||
})),
|
||||
newChatItem
|
||||
|
||||
@@ -1,153 +1,189 @@
|
||||
import { type FC, useState } from 'react';
|
||||
import { type FC, useState, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Space, App
|
||||
// Slider, Input,
|
||||
// Form,
|
||||
// Checkbox
|
||||
} from 'antd';
|
||||
import { Button, Space, App, Statistic, Row, Col, Divider } from 'antd';
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
import Card from './components/Card';
|
||||
// import qpsRestrictions from '@/assets/images/application/qpsRestrictions.svg'
|
||||
// import dailyAdjustmentDosage from '@/assets/images/application/dailyAdjustmentDosage.svg'
|
||||
// import tokenCap from '@/assets/images/application/tokenCap.svg'
|
||||
import type { Application } from '@/views/ApplicationManagement/types'
|
||||
import type { ApiKeyModalRef, ApiKeyConfigModalRef } from './types'
|
||||
import type { ApiKey } from '@/views/ApiKeyManagement/types'
|
||||
import ApiKeyModal from './components/ApiKeyModal';
|
||||
import ApiKeyConfigModal from './components/ApiKeyConfigModal';
|
||||
import Tag from '@/components/Tag'
|
||||
import { getApiKeyList, getApiKeyStats } from '@/api/apiKey';
|
||||
import { maskApiKeys } from '@/utils/apiKeyReplacer'
|
||||
|
||||
// const limitList = [
|
||||
// { key: 'qpsRestrictions', value: '10', icon: qpsRestrictions, unit: ' times/second' },
|
||||
// { key: 'dailyAdjustmentDosage', value: '1000', icon: dailyAdjustmentDosage, unit: ' times/day' },
|
||||
// { key: 'tokenCap', value: '10', icon: tokenCap, unit: 'M Tokens/day' },
|
||||
// ]
|
||||
// const sdkList = ['pythonSDK', 'nodejsSDK', 'goSDK', 'curlExample']
|
||||
|
||||
const Api: FC<{apiKeyList?: string[]}> = ({apiKeyList = []}) => {
|
||||
const Api: FC<{ application: Application | null }> = ({ application }) => {
|
||||
const { t } = useTranslation();
|
||||
const [activeMethods, setActiveMethod] = useState(['GET']);
|
||||
const { message } = App.useApp()
|
||||
// const [form] = Form.useForm();
|
||||
const { message, modal } = App.useApp()
|
||||
const copyContent = window.location.origin + '/v1/chat'
|
||||
const apiKeyModalRef = useRef<ApiKeyModalRef>(null);
|
||||
const apiKeyConfigModalRef = useRef<ApiKeyConfigModalRef>(null);
|
||||
const [apiKeyList, setApiKeyList] = useState<ApiKey[]>([])
|
||||
|
||||
const handleCopy = (content: string) => {
|
||||
copy(content)
|
||||
message.success(t('common.copySuccess'))
|
||||
}
|
||||
return (
|
||||
<div className="rb:w-[1000px] rb:mt-[20px] rb:pb-[20px] rb:mx-auto">
|
||||
{/* <Form form={form} layout="vertical"> */}
|
||||
<Space size={20} direction="vertical" style={{width: '100%'}}>
|
||||
<Card title={t('application.endpointConfiguration')}>
|
||||
<div className="rb:p-[20px_20px_24px_20px] rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-[8px]">
|
||||
<Space size={8}>
|
||||
{['GET', 'POST', 'PUT', 'DELETE'].map((method) => (
|
||||
<Button key={method} type={activeMethods.includes(method) ? 'primary' : 'default'} onClick={() => setActiveMethod(prev => activeMethods.includes(method) ? prev.filter(m => m !== method) : [...prev, method])}>
|
||||
{method}
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-[20px] rb:p-[20px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-[8px] rb:leading-[20px]">
|
||||
{copyContent}
|
||||
|
||||
<Button className="rb:px-[8px]! rb:h-[28px]! rb:group" onClick={() => handleCopy(copyContent)}>
|
||||
useEffect(() => {
|
||||
getApiList()
|
||||
}, [])
|
||||
const getApiList = () => {
|
||||
if (!application) {
|
||||
return
|
||||
}
|
||||
setApiKeyList([])
|
||||
getApiKeyList({
|
||||
type: application.type,
|
||||
is_active: true,
|
||||
resource_id: application.id,
|
||||
page: 1,
|
||||
pagesize: 10,
|
||||
}).then(res => {
|
||||
const response = res as { items: ApiKey[] }
|
||||
const list = response.items ?? []
|
||||
getAllStats(list)
|
||||
})
|
||||
}
|
||||
const getAllStats = (list: ApiKey[]) => {
|
||||
const allList: ApiKey[] = []
|
||||
list.forEach(async item => {
|
||||
await getApiKeyStats(item.id)
|
||||
.then(res => {
|
||||
const response = res as { requests_today: number; total_requests: number; quota_limit: number; quota_used: number; }
|
||||
allList.push({
|
||||
...item,
|
||||
...response,
|
||||
})
|
||||
setApiKeyList(prev => [...prev, {
|
||||
...item,
|
||||
...response,
|
||||
}])
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
const handleAdd = () => {
|
||||
apiKeyModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleEdit = (vo: ApiKey) => {
|
||||
apiKeyConfigModalRef.current?.handleOpen(vo)
|
||||
}
|
||||
const handleDelete = (vo: ApiKey) => {
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: vo.name }),
|
||||
content: t('application.apiKeyDeleteContent'),
|
||||
okText: t('common.delete'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 计算total_requests总数
|
||||
const totalRequests = apiKeyList.reduce((total, item) => total + item.total_requests, 0);
|
||||
return (
|
||||
<div className="rb:w-[1000px] rb:mt-5 rb:pb-5 rb:mx-auto">
|
||||
<Space size={20} direction="vertical" style={{width: '100%'}}>
|
||||
<Card
|
||||
title={t('application.endpointConfiguration')}
|
||||
>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mb-2">{t('application.endpointConfigurationSubTitle')}</div>
|
||||
<div className="rb:p-[20px_20px_24px_20px] rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<Space size={8}>
|
||||
{['GET', 'POST', 'PUT', 'DELETE'].map((method) => (
|
||||
<Button key={method} type={activeMethods.includes(method) ? 'primary' : 'default'} onClick={() => setActiveMethod(prev => activeMethods.includes(method) ? prev.filter(m => m !== method) : [...prev, method])}>
|
||||
{method}
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-5 rb:p-[20px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:leading-5">
|
||||
{copyContent}
|
||||
|
||||
<Button className="rb:px-2! rb:h-7! rb:group" onClick={() => handleCopy(copyContent)}>
|
||||
<div
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
></div>
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title={t('application.apiKeys')}
|
||||
extra={
|
||||
<Button style={{padding: '0 8px', height: '24px'}} onClick={handleAdd}>+ {t('application.addApiKey')}</Button>
|
||||
}
|
||||
>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mb-2">{t('application.apiKeySubTitle')}</div>
|
||||
{/* 总览数据 */}
|
||||
<Row>
|
||||
<Col span={6}>
|
||||
<Statistic title={t('application.apiKeyTotal')} value={apiKeyList.length} />
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic title={t('application.apiKeyRequestTotal')} value={totalRequests} />
|
||||
</Col>
|
||||
</Row>
|
||||
{/* API Key 列表 */}
|
||||
{apiKeyList.sort((a, b) => b.created_at - a.created_at).map(item => (
|
||||
<div key={item.id} className="rb:mt-4 rb:p-[10px_12px] rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div className="rb:flex rb:items-center rb:max-w-[calc(100%-92px)]">
|
||||
<div className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:flex-1">{item.name}</div>
|
||||
<Tag className="rb:ml-2">ID: {item.id}</Tag>
|
||||
</div>
|
||||
<Space>
|
||||
<div
|
||||
className="rb:w-[16px] rb:h-[16px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
onClick={() => handleEdit(item)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDelete(item)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
<div className="rb:mb-3 rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-5 rb:p-[8px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:leading-5">
|
||||
{maskApiKeys(item.api_key)}
|
||||
|
||||
<Button className="rb:px-2! rb:h-7! rb:group" onClick={() => handleCopy(item.api_key)}>
|
||||
<div
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
></div>
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={8}>
|
||||
<Statistic valueStyle={{ fontSize: '18px' }} title={t('application.apiKeyRequestTotal')} value={item.total_requests} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic valueStyle={{ fontSize: '18px' }} title={t('application.qps')} value={item.quota_used} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic valueStyle={{ fontSize: '18px' }} title={t('application.qpsLimit')} value={item.rate_limit} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title={t('application.authenticationMethod')}
|
||||
// extra={
|
||||
// <Button style={{padding: '0 8px', height: '24px'}} onClick={handleAdd}>+ {t('application.addApiKey')}</Button>
|
||||
// }
|
||||
>
|
||||
<div className="rb:p-[10px_20px] rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-[8px] rb:font-medium rb:text-center">
|
||||
{t('application.apiKeyTitle')}
|
||||
<p className="rb:mt-[6px] rb:text-[#5B6167] rb:text-[12px] rb:font-regular">{t('application.apiKeyDesc')}</p>
|
||||
</div>
|
||||
{apiKeyList.map((item, index) => (
|
||||
<div key={index} className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-[20px] rb:p-[12px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-[8px] rb:leading-[20px]">
|
||||
{item}
|
||||
))}
|
||||
</Card>
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
<Button className="rb:px-[8px]! rb:h-[28px]! rb:group" onClick={() => handleCopy(item)}>
|
||||
<div
|
||||
className="rb:w-[16px] rb:h-[16px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
|
||||
></div>
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
{/* <div
|
||||
className="rb:w-[24px] rb:h-[24px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
onClick={() => handleDelete(index)}
|
||||
></div> */}
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
{/* <Card title={t('application.requestResponseExample')}>
|
||||
<div className="rb:mb-[12px] rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:font-regular">
|
||||
{t('application.requestExample')}
|
||||
<Button>{t('application.downloadPostmanCollection')}</Button>
|
||||
</div>
|
||||
<div className="rb:p-[16px_20px] rb:bg-[#F0F3F8] rb:rounded-[8px] rb:text-[#5B6167] rb:leading-[18px]">
|
||||
curl -X POST https://api.example.com/v1/agent/execute \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d
|
||||
</div>
|
||||
|
||||
<div className="rb:mb-[12px] rb:mt-[24px] rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:font-regular">
|
||||
{t('application.responseExample')}
|
||||
</div>
|
||||
<div className="rb:p-[16px_20px] rb:bg-[#F0F3F8] rb:rounded-[8px] rb:text-[#5B6167] rb:leading-[18px]">
|
||||
curl -X POST https://api.example.com/v1/agent/execute \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d
|
||||
</div>
|
||||
</Card>
|
||||
<Card title={t('application.rateLimitingStrategy')}>
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-[18px]">
|
||||
{limitList.map(item => (
|
||||
<div key={item.key} className="rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded-[8px] rb:p-[16px_20px]">
|
||||
<div className="rb:flex rb:justify-between">
|
||||
<div className="rb:leading-[20px]">
|
||||
{t(`application.${item.key}`)}
|
||||
<div className="rb:text-[14px] rb:font-medium rb:text-[#155EEF] rb:mt-[8px]">{item.value}{item.unit}</div>
|
||||
</div>
|
||||
<img src={item.icon} className="rb:w-[24px] rb:h-[24px]" />
|
||||
</div>
|
||||
<Slider style={{ margin: '24px 0 0 0' }} value={item.value} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card title={t('application.sdkDownload')}>
|
||||
<div className="rb:grid rb:grid-cols-4 rb:gap-[16px]">
|
||||
{sdkList.map(item => (
|
||||
<div key={item} className="rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded-[8px] rb:p-[24px_20px] rb:text-center">
|
||||
{t(`application.${item}`)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card title={t('application.advancedSettings')}>
|
||||
<Form.Item
|
||||
name="WebhookReturnsTimeout"
|
||||
label={<>{t('application.WebhookReturnsTimeout')}<span className="rb:text-[#5B6167] rb:text-[12px] rb:font-regular"> ({t('application.WebhookReturnsTimeoutDesc')})</span></>}
|
||||
>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="whitelistIP"
|
||||
label={<>{t('application.whitelistIP')}<span className="rb:text-[#5B6167] rb:text-[12px] rb:font-regular"> ({t('application.whitelistIPDesc')})</span></>}
|
||||
>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="whitelistIP"
|
||||
className="rb:mb-[0]!"
|
||||
>
|
||||
<Checkbox>{t('application.publicAPIDocumentation')}</Checkbox>
|
||||
</Form.Item>
|
||||
</Card> */}
|
||||
</Space>
|
||||
{/* </Form> */}
|
||||
<ApiKeyModal
|
||||
ref={apiKeyModalRef}
|
||||
application={application}
|
||||
refresh={getApiList}
|
||||
/>
|
||||
<ApiKeyConfigModal
|
||||
ref={apiKeyConfigModalRef}
|
||||
refresh={getApiList}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => {
|
||||
chatList={chatList}
|
||||
updateChatList={setChatList}
|
||||
handleSave={handleSave}
|
||||
source="cluster"
|
||||
source="multi_agent"
|
||||
/>
|
||||
</RbCard>
|
||||
</Col>
|
||||
|
||||
127
web/src/views/ApplicationConfig/components/ApiKeyConfigModal.tsx
Normal file
127
web/src/views/ApplicationConfig/components/ApiKeyConfigModal.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Slider } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ApiKeyConfigModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { updateApiKey } from '@/api/apiKey';
|
||||
import type { ApiKey } from '@/views/ApiKeyManagement/types'
|
||||
|
||||
interface ApiKeyConfigModalProps {
|
||||
refresh: () => void;
|
||||
}
|
||||
const ApiKeyConfigModal = forwardRef<ApiKeyConfigModalRef, ApiKeyConfigModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<ApiKey>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const values = Form.useWatch<ApiKey>([], form)
|
||||
const [editVo, setEditVo] = useState<ApiKey>({} as ApiKey)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = (apiKey: ApiKey) => {
|
||||
setVisible(true);
|
||||
setEditVo(apiKey)
|
||||
form.setFieldsValue({
|
||||
daily_request_limit: apiKey.daily_request_limit,
|
||||
rate_limit: apiKey.rate_limit
|
||||
});
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
updateApiKey(editVo.id, {
|
||||
...editVo,
|
||||
...values
|
||||
})
|
||||
handleClose()
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('application.apiLimitConfig')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
className="rb:px-2.5!"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
>
|
||||
{/* QPS 限制(每秒请求数) */}
|
||||
<>
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mb-2">
|
||||
{t(`application.qpsLimit`)}({t('application.qpsLimitTip')})
|
||||
</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:mb-2">
|
||||
{t('application.qpsLimitDesc')}
|
||||
</div>
|
||||
<div className="rb:pl-2">
|
||||
<Form.Item
|
||||
name="rate_limit"
|
||||
>
|
||||
<Slider
|
||||
style={{ margin: '0' }}
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5 rb:mt-[-26px]">
|
||||
1
|
||||
<span>{t('application.currentValue')}: {values?.rate_limit}{t('application.qpsLimitUnit')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
{/* 日调用量限制 */}
|
||||
<>
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mt-6 rb:mb-2">
|
||||
{t(`application.dailyUsageLimit`)}
|
||||
</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:mb-2">
|
||||
{t('application.dailyUsageLimitDesc')}
|
||||
</div>
|
||||
<div className="rb:pl-2">
|
||||
<Form.Item
|
||||
name="daily_request_limit"
|
||||
>
|
||||
<Slider
|
||||
style={{ margin: '0' }}
|
||||
min={100}
|
||||
max={100000}
|
||||
step={100}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5 rb:mt-[-26px]">
|
||||
100
|
||||
<span>{t('application.currentValue')}: {values?.daily_request_limit}{t('application.dailyUsageLimitUnit')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ApiKeyConfigModal;
|
||||
103
web/src/views/ApplicationConfig/components/ApiKeyModal.tsx
Normal file
103
web/src/views/ApplicationConfig/components/ApiKeyModal.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { Application } from '@/views/ApplicationManagement/types'
|
||||
import type { ApiKeyModalRef } from '../types'
|
||||
import { createApiKey } from '@/api/apiKey';
|
||||
import RbModal from '@/components/RbModal'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface ApiKeyModalProps {
|
||||
refresh: () => void;
|
||||
application?: Application | null;
|
||||
}
|
||||
|
||||
const ApiKeyModal = forwardRef<ApiKeyModalRef, ApiKeyModalProps>(({
|
||||
refresh,
|
||||
application
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setVisible(true);
|
||||
form.resetFields();
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
if (!application) return
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
setLoading(true)
|
||||
createApiKey({
|
||||
...values,
|
||||
type: application.type,
|
||||
resource_id: application.id,
|
||||
})
|
||||
.then(() => {
|
||||
handleClose()
|
||||
refresh()
|
||||
message.success(t('common.createSuccess'))
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('application.addApiKey')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.create')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
>
|
||||
{/* Key 名称 */}
|
||||
<FormItem
|
||||
name="name"
|
||||
label={t('application.apiKeyName')}
|
||||
rules={[
|
||||
{ required: true, message: t('common.pleaseEnter') },
|
||||
{ pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, message: t('application.invalidVariableName') },
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('application.apiKeyNamePlaceholder')} />
|
||||
</FormItem>
|
||||
{/* 描述 */}
|
||||
<FormItem
|
||||
name="description"
|
||||
label={t('application.description')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('application.apiKeyDescPlaceholder')} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ApiKeyModal;
|
||||
@@ -1,46 +1,125 @@
|
||||
import { type FC, useRef, useEffect, useState } from 'react';
|
||||
import { type FC, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx'
|
||||
import { Input, Form } from 'antd'
|
||||
import ChatIcon from '@/assets/images/application/chat.png'
|
||||
import ChatSendIcon from '@/assets/images/application/chatSend.svg'
|
||||
import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png'
|
||||
import type { ChatItem, ChatData, Config } from '../types'
|
||||
import type { ChatData, Config } from '../types'
|
||||
import { runCompare, draftRun } from '@/api/application'
|
||||
import Empty from '@/components/Empty'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import ChatContent from '@/components/Chat/ChatContent'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import { type SSEMessage } from '@/utils/stream'
|
||||
|
||||
interface ChatProps {
|
||||
chatList: ChatData[];
|
||||
data: Config;
|
||||
updateChatList: (list: ChatData[]) => void;
|
||||
updateChatList: React.Dispatch<React.SetStateAction<ChatData[]>>;
|
||||
handleSave: (flag?: boolean) => Promise<any>;
|
||||
source?: 'cluster' | 'agent';
|
||||
source?: 'multi_agent' | 'agent';
|
||||
}
|
||||
const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, source = 'agent' }) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm<{ message: string }>()
|
||||
const scrollContainerRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isCluster, setIsCluster] = useState(source === 'cluster')
|
||||
const [isCluster, setIsCluster] = useState(source === 'multi_agent')
|
||||
const [conversationId, setConversationId] = useState<string | null>(null)
|
||||
const [compareLoading, setCompareLoading] = useState(false)
|
||||
|
||||
// 当聊天列表更新时,自动滚动到底部
|
||||
|
||||
useEffect(() => {
|
||||
// 延迟一下,确保DOM已经更新
|
||||
setTimeout(() => {
|
||||
scrollContainerRefs.current.forEach(container => {
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
}, [chatList]);
|
||||
useEffect(() => {
|
||||
setIsCluster(source === 'cluster')
|
||||
setIsCluster(source === 'multi_agent')
|
||||
}, [source])
|
||||
|
||||
const addUserMessage = (message: string) => {
|
||||
const newUserMessage: ChatItem = {
|
||||
role: 'user',
|
||||
content: message,
|
||||
created_at: Date.now(),
|
||||
};
|
||||
updateChatList(prev => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), newUserMessage]
|
||||
})))
|
||||
}
|
||||
const addAssistantMessage = () => {
|
||||
const assistantMessage: ChatItem = {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
created_at: Date.now(),
|
||||
};
|
||||
|
||||
if (isCluster) {
|
||||
updateChatList(prev => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessage]
|
||||
})))
|
||||
} else {
|
||||
const assistantMessages: Record<string, ChatItem> = {}
|
||||
chatList.forEach(item => {
|
||||
assistantMessages[item.model_config_id as string] = assistantMessage
|
||||
})
|
||||
updateChatList(prev => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessages[item.model_config_id as string]]
|
||||
})))
|
||||
}
|
||||
}
|
||||
const updateAssistantMessage = (content?: string, model_config_id?: string, conversation_id?: string) => {
|
||||
if (!content || !model_config_id) return
|
||||
updateChatList(prev => {
|
||||
const targetIndex = prev.findIndex(item => item.model_config_id === model_config_id);
|
||||
if (targetIndex !== -1) {
|
||||
const modelChatList = [...prev]
|
||||
const curModelChat = modelChatList[targetIndex]
|
||||
const curChatMsgList = curModelChat.list || []
|
||||
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
modelChatList[targetIndex] = {
|
||||
...modelChatList[targetIndex],
|
||||
conversation_id: conversation_id,
|
||||
list: [
|
||||
...curChatMsgList.slice(0, curChatMsgList.length - 1),
|
||||
{
|
||||
...lastMsg,
|
||||
content: lastMsg.content + content
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return [...modelChatList]
|
||||
}
|
||||
return prev;
|
||||
})
|
||||
}
|
||||
const updateErrorAssistantMessage = (message_length: number, model_config_id?: string) => {
|
||||
if (message_length > 0 || !model_config_id) return
|
||||
|
||||
updateChatList(prev => {
|
||||
const targetIndex = prev.findIndex(item => item.model_config_id === model_config_id);
|
||||
if (targetIndex > -1) {
|
||||
const modelChatList = [...prev]
|
||||
const curModelChat = modelChatList[targetIndex]
|
||||
const curChatMsgList = curModelChat.list || []
|
||||
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
modelChatList[targetIndex] = {
|
||||
...modelChatList[targetIndex],
|
||||
list: [
|
||||
...curChatMsgList.slice(0, curChatMsgList.length - 1),
|
||||
{
|
||||
...lastMsg,
|
||||
content: null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return [...modelChatList]
|
||||
}
|
||||
|
||||
return prev
|
||||
})
|
||||
}
|
||||
const handleSend = () => {
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
@@ -48,182 +127,47 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
handleSave(false)
|
||||
.then(() => {
|
||||
const message = form.getFieldValue('message')
|
||||
if (!message || message.trim() === '') return
|
||||
const newUserMessage: ChatItem = {
|
||||
role: 'question',
|
||||
content: message,
|
||||
time: Date.now(),
|
||||
};
|
||||
updateChatList((prev: ChatData[]) => {
|
||||
return prev.map(item => ({
|
||||
...item,
|
||||
list: [
|
||||
...(item.list || []),
|
||||
newUserMessage
|
||||
]
|
||||
}))
|
||||
})
|
||||
if (!message?.trim()) return
|
||||
|
||||
addUserMessage(message)
|
||||
form.setFieldsValue({ message: undefined })
|
||||
// 添加空的助手消息用于流式更新
|
||||
const assistantMessages: Record<string, ChatItem> = {};
|
||||
if (isCluster) {
|
||||
const assistantMessage: ChatItem = {
|
||||
role: 'answer',
|
||||
content: '',
|
||||
time: Date.now(),
|
||||
};
|
||||
assistantMessages['cluster'] = assistantMessage;
|
||||
updateChatList((prev: ChatData[]) => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessage]
|
||||
})))
|
||||
} else {
|
||||
chatList.forEach(item => {
|
||||
const assistantMessage: ChatItem = {
|
||||
role: 'answer',
|
||||
content: '',
|
||||
time: Date.now(),
|
||||
};
|
||||
assistantMessages[item.model_config_id] = assistantMessage;
|
||||
});
|
||||
updateChatList((prev: ChatData[]) => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessages[item.model_config_id]]
|
||||
})))
|
||||
}
|
||||
addAssistantMessage()
|
||||
|
||||
const handleStreamMessage = (data: string) => {
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
setCompareLoading(false)
|
||||
try {
|
||||
const lines = data.split('\n');
|
||||
let currentEvent = '';
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
if (line.startsWith('event:')) {
|
||||
currentEvent = line.substring(6).trim();
|
||||
} else if (line.startsWith('data:') && (!isCluster && currentEvent === 'model_message')) {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
|
||||
if (parsed.content && parsed.model_config_id) {
|
||||
const targetIndex = chatList.findIndex(item => item.model_config_id === parsed.model_config_id);
|
||||
if (targetIndex !== -1) {
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === targetIndex) {
|
||||
return {
|
||||
...item,
|
||||
conversation_id: parsed.conversation_id,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
|
||||
return { ...msg, content: msg.content + parsed.content };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('data:') && (isCluster && currentEvent === 'message')) {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
if (parsed.content) {
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === 0) {
|
||||
return {
|
||||
...item,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
|
||||
return { ...msg, content: (msg.content || '') + parsed.content };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
if (parsed.conversation_id) {
|
||||
setConversationId(parsed.conversation_id);
|
||||
}
|
||||
} else if (line.startsWith('data:') && (!isCluster && currentEvent === 'model_end')) {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
|
||||
if (parsed.message_length === 0 && parsed.model_config_id) {
|
||||
const targetIndex = chatList.findIndex(item => item.model_config_id === parsed.model_config_id);
|
||||
if (targetIndex !== -1) {
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === targetIndex) {
|
||||
return {
|
||||
...item,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
|
||||
return { ...msg, content: null };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('data:') && (isCluster && currentEvent === 'model_end')) {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
if (parsed.message_length === 0) {
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === 0) {
|
||||
return {
|
||||
...item,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
|
||||
return { ...msg, content: null };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
|
||||
if (parsed.conversation_id) {
|
||||
setConversationId(parsed.conversation_id);
|
||||
}
|
||||
} else if (currentEvent === 'compare_end') {
|
||||
data.map(item => {
|
||||
const { model_config_id, conversation_id, content, message_length } = item.data as { model_config_id: string; conversation_id: string; content: string; message_length: number };
|
||||
|
||||
switch(item.event) {
|
||||
case 'model_message':
|
||||
updateAssistantMessage(content, model_config_id, conversation_id)
|
||||
break;
|
||||
case 'model_end':
|
||||
updateErrorAssistantMessage(message_length, model_config_id)
|
||||
break;
|
||||
case 'compare_end':
|
||||
setLoading(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Parse stream data error:', e);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
if (isCluster) {
|
||||
draftRun(data.app_id, { message, conversation_id: conversationId, stream: true }, handleStreamMessage)
|
||||
.finally(() => setLoading(false))
|
||||
} else {
|
||||
runCompare(data.app_id, {
|
||||
message,
|
||||
models: chatList.map(item => ({
|
||||
model_config_id: item.model_config_id,
|
||||
label: item.label,
|
||||
model_parameters: item.model_parameters,
|
||||
conversation_id: item.conversation_id
|
||||
})),
|
||||
variables: {},
|
||||
"parallel": true,
|
||||
"stream": true,
|
||||
"timeout": 60,
|
||||
}, handleStreamMessage)
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
runCompare(data.app_id, {
|
||||
message,
|
||||
models: chatList.map(item => ({
|
||||
model_config_id: item.model_config_id,
|
||||
label: item.label,
|
||||
model_parameters: item.model_parameters,
|
||||
conversation_id: item.conversation_id
|
||||
})),
|
||||
variables: {},
|
||||
"parallel": true,
|
||||
"stream": true,
|
||||
"timeout": 60,
|
||||
}, handleStreamMessage)
|
||||
.finally(() => setLoading(false));
|
||||
}, 0)
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -231,6 +175,136 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
setCompareLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const addClusterAssistantMessage = () => {
|
||||
const assistantMessage: ChatItem = {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
created_at: Date.now(),
|
||||
};
|
||||
updateChatList(prev => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessage]
|
||||
})))
|
||||
}
|
||||
const updateClusterAssistantMessage = (content?: string) => {
|
||||
if (!content) return
|
||||
updateChatList(prev => {
|
||||
const modelChatList = [...prev]
|
||||
const curModelChat = modelChatList[0]
|
||||
const curChatMsgList = curModelChat.list || []
|
||||
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
modelChatList[0] = {
|
||||
...modelChatList[0],
|
||||
list: [
|
||||
...curChatMsgList.slice(0, curChatMsgList.length - 1),
|
||||
{
|
||||
...lastMsg,
|
||||
content: lastMsg.content + content
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return [...modelChatList]
|
||||
})
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === 0) {
|
||||
return {
|
||||
...item,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'assistant') {
|
||||
return { ...msg, content: (msg.content || '') + content };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
const updateClusterErrorAssistantMessage = (message_length: number) => {
|
||||
if (message_length > 0) return
|
||||
|
||||
updateChatList(prev => {
|
||||
const modelChatList = [...prev]
|
||||
const curModelChat = modelChatList[0]
|
||||
const curChatMsgList = curModelChat.list || []
|
||||
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
modelChatList[0] = {
|
||||
...modelChatList[0],
|
||||
list: [
|
||||
...curChatMsgList.slice(0, curChatMsgList.length - 1),
|
||||
{
|
||||
...lastMsg,
|
||||
content: null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return [...modelChatList]
|
||||
})
|
||||
}
|
||||
const handleClusterSend = () => {
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
setCompareLoading(true)
|
||||
handleSave(false)
|
||||
.then(() => {
|
||||
const message = form.getFieldValue('message')
|
||||
if (!message || message.trim() === '') return
|
||||
addUserMessage(message)
|
||||
form.setFieldsValue({ message: undefined })
|
||||
addClusterAssistantMessage()
|
||||
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
setCompareLoading(false)
|
||||
|
||||
data.map(item => {
|
||||
const { conversation_id, content, message_length } = item.data as { conversation_id: string, content: string, message_length: number };
|
||||
|
||||
switch(item.event) {
|
||||
case 'start':
|
||||
if (conversation_id && conversationId !== conversation_id) {
|
||||
setConversationId(conversation_id);
|
||||
}
|
||||
break
|
||||
case 'message':
|
||||
updateClusterAssistantMessage(content)
|
||||
if (conversation_id && conversationId !== conversation_id) {
|
||||
setConversationId(conversation_id);
|
||||
}
|
||||
break;
|
||||
case 'model_end':
|
||||
updateClusterErrorAssistantMessage(message_length)
|
||||
break;
|
||||
case 'compare_end':
|
||||
setLoading(false);
|
||||
break;
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
draftRun(
|
||||
data.app_id,
|
||||
{
|
||||
message,
|
||||
conversation_id: conversationId,
|
||||
stream: true
|
||||
},
|
||||
handleStreamMessage
|
||||
)
|
||||
.finally(() => setLoading(false))
|
||||
}, 0)
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
setCompareLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
updateChatList(chatList.filter((_, voIndex) => voIndex !== index))
|
||||
}
|
||||
@@ -258,69 +332,55 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
<div className={clsx(
|
||||
"rb:grid rb:bg-[#F0F3F8] rb:text-center rb:flex-[0_0_auto]",
|
||||
{
|
||||
'rb:rounded-tr-[12px]': index === chatList.length - 1,
|
||||
'rb:rounded-tl-[12px]': index === 0,
|
||||
'rb:rounded-tr-xl': index === chatList.length - 1,
|
||||
'rb:rounded-tl-xl': index === 0,
|
||||
}
|
||||
)}>
|
||||
<div className='rb:relative rb:p-[10px_12px] rb:overflow-hidden'>
|
||||
<div className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:w-[calc(100%-24px)]">{chat.label}</div>
|
||||
<div
|
||||
className="rb:w-[16px] rb:h-[16px] rb:cursor-pointer rb:absolute rb:top-[12px] rb:right-[12px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:absolute rb:top-3 rb:right-3 rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
|
||||
onClick={() => handleDelete(index)}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{!chat.list || chat.list.length === 0
|
||||
? <Empty url={ChatIcon} title={t('application.chatEmpty')} isNeedSubTitle={false} className="rb:h-full" size={[240, 200]} />
|
||||
: (
|
||||
<div ref={el => scrollContainerRefs.current[index] = el} className={clsx(`rb:relative rb:overflow-y-auto rb:overflow-x-hidden`, {
|
||||
'rb:h-[calc(100vh-186px)]': isCluster,
|
||||
'rb:h-[calc(100vh-286px)]': !isCluster,
|
||||
})}>
|
||||
{chat.list?.map((vo, voIndex) => {
|
||||
if (compareLoading && voIndex === chat.list?.length - 1) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div key={voIndex} className={clsx("rb:relative rb:mt-[24px]", {
|
||||
'rb:right-[16px] rb:text-right': vo.role === 'question',
|
||||
'rb:left-[16px] rb:text-left': vo.role !== 'question',
|
||||
})}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-[16px] rb:font-regular">{vo.role === 'question' ? 'You' : chat.label}</div>
|
||||
<div className={clsx('rb:border rb:text-left rb:rounded-[8px] rb:mt-[6px] rb:leading-[18px] rb:p-[10px_12px_2px_12px] rb:inline-block', {
|
||||
'rb:border-[rgba(255,93,52,0.30)] rb:bg-[rgba(255,93,52,0.08)] rb:text-[#FF5D34]': vo.role !== 'question' && vo.content === null,
|
||||
'rb:bg-[rgba(21,94,239,0.08)] rb:border-[rgba(21,94,239,0.30)]': vo.role === 'question' && vo.content,
|
||||
'rb:bg-[#ffffff] rb:border-[rgba(235,235,235,1)]': vo.role !== 'question' && (vo.content || vo.content === ''),
|
||||
'rb:max-w-[400px]': chatList.length === 1,
|
||||
'rb:max-w-[260px]': chatList.length === 2,
|
||||
'rb:max-w-[150px]': chatList.length === 3,
|
||||
'rb:max-w-[108px]': chatList.length === 4,
|
||||
})}>
|
||||
<Markdown content={vo.content === null ? t('application.ReplyException') : vo.content} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<ChatContent
|
||||
classNames={{
|
||||
'rb:mx-[16px] rb:pt-[24px]': true,
|
||||
'rb:h-[calc(100vh-186px)]': isCluster,
|
||||
'rb:h-[calc(100vh-286px)]': !isCluster,
|
||||
}}
|
||||
contentClassNames={{
|
||||
'rb:max-w-[400px]!': chatList.length === 1,
|
||||
'rb:max-w-[260px]!': chatList.length === 2,
|
||||
'rb:max-w-[150px]!': chatList.length === 3,
|
||||
'rb:max-w-[108px]!': chatList.length === 4,
|
||||
}}
|
||||
empty={<Empty url={ChatIcon} title={t('application.chatEmpty')} isNeedSubTitle={false} size={[240, 200]} className="rb:h-full" />}
|
||||
data={chat.list || []}
|
||||
streamLoading={compareLoading}
|
||||
labelPosition="top"
|
||||
labelFormat={(item) => item.role === 'user' ? 'You' : chat.label}
|
||||
errorDesc={t('application.ReplyException')}
|
||||
/>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="rb:flex rb:items-center rb:gap-[10px] rb:p-[16px]">
|
||||
<div className="rb:flex rb:items-center rb:gap-2.5 rb:p-4">
|
||||
<Form form={form} style={{width: 'calc(100% - 54px)'}}>
|
||||
<Form.Item name="message" className="rb:mb-[0]!">
|
||||
<Form.Item name="message" className="rb:mb-0!">
|
||||
<Input
|
||||
className="rb:h-[44px] rb:shadow-[0px_2px_8px_0px_rgba(33,35,50,0.1)]"
|
||||
className="rb:h-11 rb:shadow-[0px_2px_8px_0px_rgba(33,35,50,0.1)]"
|
||||
placeholder={t('application.chatPlaceholder')}
|
||||
onPressEnter={handleSend}
|
||||
onPressEnter={isCluster ? handleClusterSend : handleSend}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<img src={ChatSendIcon} className={clsx("rb:w-[44px] rb:h-[44px] rb:cursor-pointer", {
|
||||
<img src={ChatSendIcon} className={clsx("rb:w-11 rb:h-11 rb:cursor-pointer", {
|
||||
'rb:opacity-50': loading,
|
||||
})} onClick={handleSend} />
|
||||
})} onClick={isCluster ? handleClusterSend : handleSend} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { useEffect, useState, type FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Cascader } from 'antd'
|
||||
import type { CascaderProps } from 'antd';
|
||||
import { getModelProviderList } from '@/api/models'
|
||||
|
||||
interface Option {
|
||||
value?: string | number | null;
|
||||
label: React.ReactNode;
|
||||
children?: Option[];
|
||||
isLeaf?: boolean;
|
||||
}
|
||||
const CustomSelect: FC<CascaderProps> = () => {
|
||||
const { t } = useTranslation();
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
useEffect(() => {
|
||||
getProviderList()
|
||||
}, []);
|
||||
|
||||
const getProviderList = () => {
|
||||
getModelProviderList().then(res => {
|
||||
const response = res as string[]
|
||||
setOptions(response.map((key: string) => ({
|
||||
value: key,
|
||||
label: t(`model.${key}`),
|
||||
children: [],
|
||||
isLeaf: false,
|
||||
})))
|
||||
})
|
||||
}
|
||||
const loadData = (selectedOptions: Option[]) => {
|
||||
const targetOption = selectedOptions[selectedOptions.length - 1];
|
||||
console.log(targetOption)
|
||||
}
|
||||
return (
|
||||
<Cascader
|
||||
options={options}
|
||||
loadData={loadData}
|
||||
changeOnSelect
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default CustomSelect;
|
||||
@@ -8,9 +8,7 @@ import Api from './Api'
|
||||
import ReleasePage from './ReleasePage'
|
||||
import Cluster from './Cluster'
|
||||
import { getApplication } from '@/api/application'
|
||||
import { randomString } from '@/utils/common'
|
||||
|
||||
const apiKeyList = [`app-${randomString(24, false)}`]
|
||||
const ApplicationConfig: React.FC = () => {
|
||||
const { id } = useParams();
|
||||
const agentRef = useRef<AgentRef>(null)
|
||||
@@ -52,7 +50,7 @@ const ApplicationConfig: React.FC = () => {
|
||||
/>
|
||||
{activeTab === 'arrangement' && application?.type === 'agent' && <Agent ref={agentRef} />}
|
||||
{activeTab === 'arrangement' && application?.type === 'multi_agent' && <Cluster application={application as Application} />}
|
||||
{activeTab === 'api' && <Api apiKeyList={apiKeyList} />}
|
||||
{activeTab === 'api' && <Api application={application} />}
|
||||
{activeTab === 'release' && <ReleasePage data={application as Application} refresh={getApplicationInfo} />}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { KnowledgeBaseListItem } from '@/views/KnowledgeBase/types'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
|
||||
export interface ModelConfig {
|
||||
label?: string;
|
||||
@@ -139,11 +140,6 @@ export interface ApiExtensionModalData {
|
||||
export interface ApiExtensionModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
export interface ChatItem {
|
||||
role: 'answer' | 'question';
|
||||
content?: string;
|
||||
time: number;
|
||||
}
|
||||
export interface ChatData {
|
||||
label?: string;
|
||||
model_config_id?: string;
|
||||
@@ -191,4 +187,10 @@ export interface SubAgentItem {
|
||||
}
|
||||
export interface SubAgentModalRef {
|
||||
handleOpen: (agent?: SubAgentItem) => void;
|
||||
}
|
||||
export interface ApiKeyModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
export interface ApiKeyConfigModalRef {
|
||||
handleOpen: (apiKey: ApiKey) => void;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export interface Application {
|
||||
description?: string;
|
||||
icon?: string;
|
||||
icon_type?: string;
|
||||
type: string;
|
||||
type: 'agent' | 'multi_agent' | 'workflow';
|
||||
visibility: string;
|
||||
status: string;
|
||||
tags: string[];
|
||||
|
||||
@@ -2,16 +2,24 @@ import { type FC, useState, useEffect, useRef } from 'react'
|
||||
import { useParams, useLocation } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||
import { Flex, Skeleton } from 'antd'
|
||||
import { Flex, Skeleton, Form } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
import Chat, { type ChatItem } from '@/views/MemoryConversation/components/Chat'
|
||||
import AnalysisEmptyIcon from '@/assets/images/conversation/analysisEmpty.svg'
|
||||
import { getConversationHistory, sendConversation, getConversationDetail, getShareToken } from '@/api/application'
|
||||
import type { HistoryItem } from './types'
|
||||
import type { HistoryItem, QueryParams } from './types'
|
||||
import Empty from '@/components/Empty'
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
import { randomString } from '@/utils/common'
|
||||
import BgImg from '@/assets/images/conversation/bg.png'
|
||||
import Chat from '@/components/Chat'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import ButtonCheckbox from '@/components/ButtonCheckbox'
|
||||
import MemoryFunctionIcon from '@/assets/images/conversation/memoryFunction.svg'
|
||||
import OnlineIcon from '@/assets/images/conversation/online.svg'
|
||||
import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg'
|
||||
import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg'
|
||||
import dayjs from 'dayjs'
|
||||
import { type SSEMessage } from '@/utils/stream'
|
||||
|
||||
const Conversation: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -20,13 +28,8 @@ const Conversation: FC = () => {
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
const userId = searchParams.get('user_id')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [chatLoading, setChatLoading] = useState(false)
|
||||
const [query, setQuery] = useState<{
|
||||
message?: string;
|
||||
web_search?: boolean;
|
||||
memory?: boolean;
|
||||
conversation_id?: string;
|
||||
}>({})
|
||||
const [streamLoading, setStreamLoading] = useState(false)
|
||||
const [message, setMessage] = useState<string>('')
|
||||
const [conversation_id, setConversationId] = useState<string | null>(null)
|
||||
const [historyList, setHistoryList] = useState<HistoryItem[]>([])
|
||||
const [groupHistoryList, setGroupHistoryList] = useState<Record<string, HistoryItem[]>>({})
|
||||
@@ -36,14 +39,18 @@ const Conversation: FC = () => {
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [shareToken, setShareToken] = useState<string | null>(localStorage.getItem(`shareToken_${token}`))
|
||||
|
||||
const [form] = Form.useForm<QueryParams>()
|
||||
const queryValues = Form.useWatch<QueryParams>([], form)
|
||||
useEffect(() => {
|
||||
const shareToken = localStorage.getItem(`shareToken_${token}`)
|
||||
setShareToken(shareToken)
|
||||
if (shareToken && shareToken !== '') return
|
||||
getShareToken(token as string, userId || randomString(12, false))
|
||||
.then(res => {
|
||||
localStorage.setItem(`shareToken_${token}`, res?.access_token || '')
|
||||
setShareToken(res?.access_token || '')
|
||||
const response = res as { access_token: string } || {}
|
||||
localStorage.setItem(`shareToken_${token}`, response.access_token ?? '')
|
||||
setShareToken(response.access_token ?? '')
|
||||
})
|
||||
}, [token])
|
||||
|
||||
@@ -73,7 +80,7 @@ const Conversation: FC = () => {
|
||||
setPageLoading(true);
|
||||
getConversationHistory(token, { page: flag ? 1 : page, pagesize: 20 })
|
||||
.then(res => {
|
||||
const response = res as { items: HistoryItem[], page: { hasnext: boolean } }
|
||||
const response = res as { items: HistoryItem[], page: { hasnext: boolean; page: number; pagesize: number; total: number } }
|
||||
const results = response?.items || []
|
||||
let list = []
|
||||
if (flag) {
|
||||
@@ -101,7 +108,7 @@ const Conversation: FC = () => {
|
||||
setConversationId(id)
|
||||
}
|
||||
if (!id) {
|
||||
setQuery({})
|
||||
setMessage('')
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
@@ -116,72 +123,81 @@ const Conversation: FC = () => {
|
||||
}
|
||||
}, [conversation_id])
|
||||
|
||||
const addUserMessage = (message: string = '') => {
|
||||
const newUserMessage: ChatItem = {
|
||||
conversation_id,
|
||||
role: 'user',
|
||||
content: message,
|
||||
created_at: Date.now()
|
||||
};
|
||||
setChatList(prev => [...prev, newUserMessage])
|
||||
}
|
||||
const addAssistantMessage = () => {
|
||||
const newAssistantMessage: ChatItem = {
|
||||
created_at: Date.now(),
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
}
|
||||
setChatList(prev => [...prev, newAssistantMessage])
|
||||
}
|
||||
const updateAssistantMessage = (content: string = '') => {
|
||||
if (!content) return
|
||||
if (streamLoading) {
|
||||
setStreamLoading(false)
|
||||
}
|
||||
|
||||
setChatList(prev => {
|
||||
const lastList = [...prev]
|
||||
const lastIndex = lastList.length - 1
|
||||
const lastMsg = lastList[lastIndex]
|
||||
if (lastMsg?.role === 'assistant') {
|
||||
return [
|
||||
...lastList.slice(0, lastList.length - 1),
|
||||
{
|
||||
...lastMsg,
|
||||
content: lastMsg.content + content
|
||||
}
|
||||
]
|
||||
}
|
||||
return prev
|
||||
})
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
if (!token || !shareToken) {
|
||||
return
|
||||
}
|
||||
// 添加必需的id和conversation_id属性
|
||||
const newUserMessage: ChatItem = {
|
||||
conversation_id,
|
||||
role: 'user',
|
||||
content: query?.message || '',
|
||||
created_at: Date.now()
|
||||
};
|
||||
setChatList(prev => [...prev, newUserMessage])
|
||||
|
||||
setLoading(true)
|
||||
setChatLoading(true)
|
||||
setChatList(prev => [...prev, {
|
||||
created_at: Date.now(),
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
}])
|
||||
let currentConversationId: string | null = null
|
||||
const handleStreamMessage = (data: string) => {
|
||||
setChatLoading(false)
|
||||
try {
|
||||
const lines = data.split('\n');
|
||||
let currentEvent = '';
|
||||
setStreamLoading(true)
|
||||
addUserMessage(message)
|
||||
addAssistantMessage()
|
||||
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
if (line.startsWith('event:')) {
|
||||
currentEvent = line.substring(6).trim();
|
||||
} else if (line.startsWith('data:') && currentEvent === 'message') {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
|
||||
if (parsed.content) {
|
||||
setChatList(prev => prev.map((msg, msgIndex) => {
|
||||
if (msgIndex === prev!.length - 1 && msg.role === 'assistant') {
|
||||
return { ...msg, content: msg.content + parsed.content };
|
||||
}
|
||||
return msg;
|
||||
}))
|
||||
}
|
||||
} else if (line.startsWith('data:') && currentEvent === 'start') {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
currentConversationId = parsed.conversation_id
|
||||
} else if (currentEvent === 'end') {
|
||||
setLoading(false);
|
||||
let currentConversationId: string | null = null
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
data.forEach((item) => {
|
||||
switch(item.event) {
|
||||
case 'start':
|
||||
const { conversation_id: newId } = item.data as { conversation_id: string }
|
||||
currentConversationId = newId
|
||||
break
|
||||
case 'message':
|
||||
const { content } = item.data as { content: string }
|
||||
updateAssistantMessage(content)
|
||||
break
|
||||
case 'end':
|
||||
setLoading(false)
|
||||
if (currentConversationId && currentConversationId !== conversation_id) {
|
||||
setConversationId(currentConversationId)
|
||||
getHistory(true)
|
||||
}
|
||||
}
|
||||
getHistory(true)
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Parse stream data error:', e);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
sendConversation(token as string, {
|
||||
message: query?.message || '',
|
||||
web_search: query?.web_search || false,
|
||||
memory: query?.memory || false,
|
||||
sendConversation({
|
||||
...queryValues,
|
||||
message: message || '',
|
||||
stream: true,
|
||||
conversation_id: conversation_id || null,
|
||||
}, handleStreamMessage, shareToken)
|
||||
@@ -192,12 +208,12 @@ const Conversation: FC = () => {
|
||||
|
||||
return (
|
||||
<Flex className="rb:w-full rb:p-[-16px]!">
|
||||
<div className="rb:w-[345px] rb:h-[100vh] rb:overflow-hidden rb:border-r rb:border-[#EAECEE] rb:p-[12px]">
|
||||
<div className="rb:group rb:flex rb:items-center rb:justify-center rb:font-regular rb:cursor-pointer rb:mb-[20px] rb:border rb:border-[#DFE4ED] rb:hover:border-[#155EEF] rb:hover:text-[#155EEF] rb:rounded-[8px] rb:py-[10px]"
|
||||
<div className="rb:w-[345px] rb:h-screen rb:overflow-hidden rb:border-r rb:border-[#EAECEE] rb:p-3">
|
||||
<div className="rb:group rb:flex rb:items-center rb:justify-center rb:font-regular rb:cursor-pointer rb:mb-5 rb:border rb:border-[#DFE4ED] rb:hover:border-[#155EEF] rb:hover:text-[#155EEF] rb:rounded-lg rb:py-2.5"
|
||||
onClick={() => handleChangeHistory(null)}
|
||||
>
|
||||
<div
|
||||
className="rb:w-[20px] rb:h-[20px] rb:cursor-pointer rb:mr-[8px] rb:bg-cover rb:bg-[url('@/assets/images/conversation/conversation.svg')] rb:group-hover:bg-[url('@/assets/images/conversation/conversation_hover.svg')]"
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:mr-2 rb:bg-cover rb:bg-[url('@/assets/images/conversation/conversation.svg')] rb:group-hover:bg-[url('@/assets/images/conversation/conversation_hover.svg')]"
|
||||
></div>
|
||||
{t('memoryConversation.startANewConversation')}
|
||||
</div>
|
||||
@@ -216,11 +232,11 @@ const Conversation: FC = () => {
|
||||
scrollableTarget="scrollableDiv"
|
||||
>
|
||||
{Object.entries(groupHistoryList).map(([date, items]) => (
|
||||
<div key={date} className="rb:mt-[24px] rb:first:mt-0">
|
||||
<div className="rb:leading-[20px] rb:text-[#5B6167] rb:mb-[8px] rb:pl-[4px] rb:font-regular">{date.replace(/\u200e|\u200f/g, '')}</div>
|
||||
<div key={date} className="rb:mt-6 rb:first:mt-0">
|
||||
<div className="rb:leading-5 rb:text-[#5B6167] rb:mb-2 rb:pl-1 rb:font-regular">{date.replace(/\u200e|\u200f/g, '')}</div>
|
||||
{items.map(item => (
|
||||
<div key={item.updated_at} className="rb:mb-[12px]">
|
||||
<div className={clsx("rb:p-[8px_13px] rb:rounded-[8px] rb:leading-[20px] rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
|
||||
<div key={item.updated_at} className="rb:mb-3">
|
||||
<div className={clsx("rb:p-[8px_13px] rb:rounded-lg rb:leading-5 rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
|
||||
'rb:bg-[#FFFFFF] rb:shadow-[0px_2px_4px_0px_rgba(0,0,0,0.15)] rb:font-medium rb:hover:bg-[#FFFFFF]!': item.id === conversation_id,
|
||||
})}
|
||||
onClick={() => handleChangeHistory(item.id)}
|
||||
@@ -237,18 +253,38 @@ const Conversation: FC = () => {
|
||||
<img src={BgImg} className="rb:absolute rb:bottom-0 rb:left-0 rb:w-[345px]" />
|
||||
</div>
|
||||
|
||||
<div className="rb:relative rb:h-[100vh] rb:px-[16px] rb:flex-[1_1_auto]">
|
||||
<div className="rb:relative rb:h-screen rb:px-4 rb:flex-[1_1_auto]">
|
||||
<Chat
|
||||
source="conversation"
|
||||
empty={
|
||||
<Empty url={AnalysisEmptyIcon} subTitle={t('memoryConversation.emptyDesc')} />
|
||||
}
|
||||
query={query}
|
||||
empty={<Empty url={AnalysisEmptyIcon} className="rb:h-full" subTitle={t('memoryConversation.emptyDesc')} />}
|
||||
contentClassName="rb:h-[calc(100%-152px)]"
|
||||
data={chatList}
|
||||
streamLoading={streamLoading}
|
||||
loading={loading}
|
||||
onChange={setQuery}
|
||||
onChange={setMessage}
|
||||
onSend={handleSend}
|
||||
/>
|
||||
labelFormat={(item) => dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}
|
||||
>
|
||||
<Form form={form}>
|
||||
<Flex gap={8}>
|
||||
<Form.Item name="web_search" valuePropName="checked" className="rb:mb-0!">
|
||||
<ButtonCheckbox
|
||||
icon={OnlineIcon}
|
||||
checkedIcon={OnlineCheckedIcon}
|
||||
>
|
||||
{t(`memoryConversation.web_search`)}
|
||||
</ButtonCheckbox>
|
||||
</Form.Item>
|
||||
<Form.Item name="memory" valuePropName="checked" className="rb:mb-0!">
|
||||
<ButtonCheckbox
|
||||
icon={MemoryFunctionIcon}
|
||||
checkedIcon={MemoryFunctionCheckedIcon}
|
||||
>
|
||||
{t(`memoryConversation.memory`)}
|
||||
</ButtonCheckbox>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form>
|
||||
</Chat>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
|
||||
@@ -10,4 +10,12 @@ export interface HistoryItem {
|
||||
is_active: boolean;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export interface QueryParams {
|
||||
message?: string;
|
||||
web_search?: boolean;
|
||||
memory?: boolean;
|
||||
stream: boolean;
|
||||
conversation_id?: string | null;
|
||||
}
|
||||
@@ -132,7 +132,7 @@ const MemberModal = forwardRef<MemberModalRef, MemberModalProps>(({
|
||||
label={t('member.email')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.inputPlaceholder', { title: t('member.email') })} disabled={!!editingUser} />
|
||||
<Input placeholder={t('common.enterPlaceholder', { title: t('member.email') })} disabled={!!editingUser} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { type FC, type ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import { Flex } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
import ChatInput from './ChatInput'
|
||||
import type { TestParams } from '../index'
|
||||
import dayjs from 'dayjs'
|
||||
import Markdown from '@/components/Markdown'
|
||||
|
||||
interface ChatProps {
|
||||
empty?: ReactNode;
|
||||
data: ChatItem[];
|
||||
query?: TestParams;
|
||||
onChange: (query: TestParams) => void;
|
||||
onSend: () => void;
|
||||
loading: boolean;
|
||||
source?: 'conversation' | 'memory';
|
||||
}
|
||||
export interface ChatItem {
|
||||
id?: string;
|
||||
conversation_id?: string | null;
|
||||
role?: 'user' | 'assistant';
|
||||
content?: string;
|
||||
message?: string;
|
||||
created_at?: number | string;
|
||||
meta_data?: Record<string, string | number>[];
|
||||
}
|
||||
|
||||
const Chat: FC<ChatProps> = ({ empty, data, query, onChange, onSend, loading, source = 'memory' }) => {
|
||||
const scrollContainerRefs = useRef<(HTMLDivElement | null)>(null)
|
||||
const [isMemory, setIsMemory] = useState<boolean>(source === 'memory')
|
||||
|
||||
useEffect(() => {
|
||||
setIsMemory(source === 'memory')
|
||||
}, [source])
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (scrollContainerRefs.current) {
|
||||
scrollContainerRefs.current.scrollTop = scrollContainerRefs.current.scrollHeight;
|
||||
}
|
||||
}, 0);
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:relative rb:pt-[8px]">
|
||||
{data.length === 0 ? (
|
||||
<Flex vertical justify="space-between" className="rb:h-full rb:w-full rb:relative">
|
||||
{/* Empty */}
|
||||
<div className="rb:h-[calc(100%-144px)] rb:overflow-y-auto rb:overflow-x-hidden rb:flex rb:items-center rb:justify-center">
|
||||
{empty}
|
||||
</div>
|
||||
|
||||
<ChatInput source={source} query={query} onChange={onChange} onSend={onSend} loading={loading} />
|
||||
</Flex>
|
||||
)
|
||||
: (
|
||||
<div ref={scrollContainerRefs} className={clsx("rb:relative rb:overflow-y-auto", {
|
||||
'rb:h-[calc(100%-152px)]': !isMemory,
|
||||
'rb:h-[calc(100vh-362px)]': isMemory
|
||||
})}>
|
||||
{data.map((item, index) => (
|
||||
<div key={index} className={clsx("rb:relative", {
|
||||
'rb:mt-[24px]': index !== 0,
|
||||
'rb:right-[0] rb:text-right': item.role === 'user',
|
||||
'rb:left-[0] rb:text-left': item.role === 'assistant',
|
||||
})}>
|
||||
<div className={clsx('rb:border rb:text-left rb:rounded-[8px] rb:mt-[6px] rb:leading-[18px] rb:p-[10px_12px_2px_12px] rb:inline-block rb:max-w-[400px]', {
|
||||
'rb:bg-[rgba(21,94,239,0.08)] rb:border-[rgba(21,94,239,0.30)]': item.role === 'user',
|
||||
'rb:bg-[#FFFFFF] rb:border-[#EBEBEB]': item.role === 'assistant',
|
||||
})}>
|
||||
<Markdown content={item.content || ''} />
|
||||
</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-[16px] rb:font-regular">{dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ChatInput source={source} query={query} onChange={onChange} onSend={onSend} loading={loading} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Chat
|
||||
@@ -1,143 +0,0 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Flex, Input, Form } from 'antd'
|
||||
import MemoryFunctionIcon from '@/assets/images/conversation/memoryFunction.svg'
|
||||
import OnlineIcon from '@/assets/images/conversation/online.svg'
|
||||
import DeepThinkingIcon from '@/assets/images/conversation/deepThinking.svg'
|
||||
import SendIcon from '@/assets/images/conversation/send.svg'
|
||||
import SendDisabledIcon from '@/assets/images/conversation/sendDisabled.svg'
|
||||
import ButtonCheckbox from '@/components/ButtonCheckbox'
|
||||
import DeepThinkingCheckedIcon from '@/assets/images/conversation/deepThinkingChecked.svg'
|
||||
import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg'
|
||||
import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg'
|
||||
import LoadingIcon from '@/assets/images/conversation/loading.svg'
|
||||
import type { TestParams } from '../index'
|
||||
|
||||
interface ChatInputProps {
|
||||
query?: TestParams;
|
||||
onChange: (query: TestParams) => void;
|
||||
onSend: () => void;
|
||||
loading: boolean;
|
||||
source: 'conversation' | 'memory';
|
||||
}
|
||||
const searchSwitchList = [
|
||||
{
|
||||
icon: DeepThinkingIcon,
|
||||
checkedIcon: DeepThinkingCheckedIcon,
|
||||
value: '0',
|
||||
label: 'deepThinking' // 深度思考
|
||||
},
|
||||
{
|
||||
icon: MemoryFunctionIcon,
|
||||
checkedIcon: MemoryFunctionCheckedIcon,
|
||||
value: '1',
|
||||
label: 'normalReply' // 普通回复
|
||||
},
|
||||
{
|
||||
icon: OnlineIcon,
|
||||
checkedIcon: OnlineCheckedIcon,
|
||||
value: '2',
|
||||
label: 'quickReply' // 快速回复
|
||||
},
|
||||
]
|
||||
|
||||
const ChatInput: FC<ChatInputProps> = ({ source,query, onChange, onSend, loading }) => {
|
||||
const [form] = Form.useForm()
|
||||
const { t } = useTranslation();
|
||||
const values = Form.useWatch([], form);
|
||||
const [search_switch, setSearchSwitch] = useState('0')
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
onChange({...values, search_switch})
|
||||
}
|
||||
}, [values, search_switch, onChange])
|
||||
useEffect(() => {
|
||||
if (!query?.message) {
|
||||
form.setFieldsValue({
|
||||
message: undefined,
|
||||
})
|
||||
}
|
||||
}, [form, query?.message])
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
form.setFieldsValue({
|
||||
message: undefined,
|
||||
})
|
||||
}
|
||||
}, [loading])
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
form.setFieldsValue({
|
||||
search_switch: value,
|
||||
})
|
||||
setSearchSwitch(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form form={form} layout="vertical" className="rb:absolute rb:bottom-[12px] rb:left-0 rb:right-0">
|
||||
<Flex vertical justify="space-between" className="rb:border rb:border-[#DFE4ED] rb:rounded-[12px] rb:min-h-[120px]">
|
||||
<Form.Item name="message" className="rb:mb-[0]!">
|
||||
<Input.TextArea
|
||||
className="rb:m-[10px_12px_10px_12px]! rb:p-[0]! rb:w-[calc(100%-24px)]! rb:flex-[1_1_auto]"
|
||||
// rows={4}
|
||||
variant="borderless"
|
||||
autoSize={{ minRows: 2, maxRows: 2 }}
|
||||
onChange={(e) => onChange({ ...query, message: e.target.value })}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && e.target.value?.trim() !== '' && !loading) {
|
||||
e.preventDefault();
|
||||
onSend();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Flex align="center" justify="space-between" className="rb:m-[0_10px_10px_10px]!">
|
||||
{source === 'memory' &&
|
||||
<Flex gap={8}>
|
||||
{searchSwitchList.map(item => (
|
||||
<ButtonCheckbox
|
||||
key={item.value}
|
||||
icon={item.icon}
|
||||
checkedIcon={item.checkedIcon}
|
||||
checked={search_switch === item.value}
|
||||
onChange={() => handleChange(item.value)}
|
||||
>
|
||||
{t(`memoryConversation.${item.label}`)}
|
||||
</ButtonCheckbox>
|
||||
))}
|
||||
</Flex>
|
||||
}
|
||||
{source === 'conversation' &&
|
||||
<Flex gap={8}>
|
||||
<Form.Item name="web_search" valuePropName="checked" className="rb:mb-[0]!">
|
||||
<ButtonCheckbox
|
||||
icon={OnlineIcon}
|
||||
checkedIcon={OnlineCheckedIcon}
|
||||
>
|
||||
{t(`memoryConversation.web_search`)}
|
||||
</ButtonCheckbox>
|
||||
</Form.Item>
|
||||
<Form.Item name="memory" valuePropName="checked" className="rb:mb-[0]!">
|
||||
<ButtonCheckbox
|
||||
icon={MemoryFunctionIcon}
|
||||
checkedIcon={MemoryFunctionCheckedIcon}
|
||||
>
|
||||
{t(`memoryConversation.memory`)}
|
||||
</ButtonCheckbox>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
}
|
||||
{loading ? <img src={LoadingIcon} className="rb:w-[22px] rb:h-[22px] rb:cursor-pointer" />:
|
||||
!values || !values?.message || values?.message?.trim() === '' ?
|
||||
<img src={SendDisabledIcon} className="rb:w-[22px] rb:h-[22px] rb:cursor-pointer" />
|
||||
: <img src={SendIcon} className="rb:w-[22px] rb:h-[22px] rb:cursor-pointer" onClick={onSend} />
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatInput
|
||||
@@ -1,16 +1,48 @@
|
||||
import { type FC, type ReactNode, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Col, Row, App, Skeleton, Space, Select } from 'antd'
|
||||
import { Col, Row, App, Skeleton, Space, Select, Flex } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg'
|
||||
import AnalysisEmptyIcon from '@/assets/images/conversation/analysisEmpty.png'
|
||||
import Card from './components/Card'
|
||||
import Chat from './components/Chat'
|
||||
import { readService, getUserMemoryList } from '@/api/memory'
|
||||
import Empty from '@/components/Empty'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import type { Data } from '@/views/UserMemory/types'
|
||||
import Chat from '@/components/Chat'
|
||||
import MemoryFunctionIcon from '@/assets/images/conversation/memoryFunction.svg'
|
||||
import OnlineIcon from '@/assets/images/conversation/online.svg'
|
||||
import DeepThinkingIcon from '@/assets/images/conversation/deepThinking.svg'
|
||||
import ButtonCheckbox from '@/components/ButtonCheckbox'
|
||||
import DeepThinkingCheckedIcon from '@/assets/images/conversation/deepThinkingChecked.svg'
|
||||
import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg'
|
||||
import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import dayjs from 'dayjs'
|
||||
import type { AnyObject } from 'antd/es/_util/type';
|
||||
|
||||
|
||||
const searchSwitchList = [
|
||||
{
|
||||
icon: DeepThinkingIcon,
|
||||
checkedIcon: DeepThinkingCheckedIcon,
|
||||
value: '0',
|
||||
label: 'deepThinking' // 深度思考
|
||||
},
|
||||
{
|
||||
icon: MemoryFunctionIcon,
|
||||
checkedIcon: MemoryFunctionCheckedIcon,
|
||||
value: '1',
|
||||
label: 'normalReply' // 普通回复
|
||||
},
|
||||
{
|
||||
icon: OnlineIcon,
|
||||
checkedIcon: OnlineCheckedIcon,
|
||||
value: '2',
|
||||
label: 'quickReply' // 快速回复
|
||||
},
|
||||
]
|
||||
|
||||
export interface TestParams {
|
||||
group_id: string;
|
||||
@@ -30,8 +62,8 @@ interface DataItem {
|
||||
export interface LogItem {
|
||||
type: string;
|
||||
title: string;
|
||||
data?: DataItem[] | Record<string, string>;
|
||||
raw_results?: string;
|
||||
data?: DataItem[] | AnyObject;
|
||||
raw_results?: string | AnyObject;
|
||||
summary?: string;
|
||||
query?: string;
|
||||
reason?: string;
|
||||
@@ -41,7 +73,7 @@ export interface LogItem {
|
||||
}
|
||||
|
||||
const ContentWrapper: FC<{ children: ReactNode }> = ({ children }) => (
|
||||
<div className="rb:p-[12px] rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-[8px]">
|
||||
<div className="rb:p-3 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
@@ -49,17 +81,13 @@ const ContentWrapper: FC<{ children: ReactNode }> = ({ children }) => (
|
||||
const MemoryConversation: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { message } = App.useApp();
|
||||
const [query, setQuery] = useState<TestParams>({
|
||||
group_id: '',
|
||||
message: '',
|
||||
search_switch: '0',
|
||||
history: [],
|
||||
})
|
||||
const [userId, setUserId] = useState<string>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [chatData, setChatData] = useState<{ content: string; created_at: string | number; role: string }[]>([])
|
||||
const [chatData, setChatData] = useState<ChatItem[]>([])
|
||||
const [logs, setLogs] = useState<LogItem[]>([])
|
||||
const [userList, setUserList] = useState<Data[]>([])
|
||||
const [search_switch, setSearchSwitch] = useState('0')
|
||||
const [msg, setMsg] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
getUserMemoryList().then(res => {
|
||||
@@ -75,11 +103,12 @@ const MemoryConversation: FC = () => {
|
||||
message.warning(t('common.inputPlaceholder', { title: t('memoryConversation.userID') }))
|
||||
return
|
||||
}
|
||||
setChatData(prev => [...prev, { content: query.message || '', created_at: new Date().getTime(), role: 'user' }])
|
||||
setChatData(prev => [...prev, { content: msg, created_at: new Date().getTime(), role: 'user' }])
|
||||
setLoading(true)
|
||||
readService({
|
||||
...query,
|
||||
message: msg,
|
||||
group_id: userId,
|
||||
search_switch: search_switch,
|
||||
history: [],
|
||||
})
|
||||
.then(res => {
|
||||
@@ -92,6 +121,10 @@ const MemoryConversation: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setSearchSwitch(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row gutter={16}>
|
||||
@@ -101,7 +134,7 @@ const MemoryConversation: FC = () => {
|
||||
value: item.end_user?.id,
|
||||
label: item?.name,
|
||||
}))}
|
||||
filterOption={(inputValue, option) => option.label?.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1}
|
||||
filterOption={(inputValue, option) => option?.label?.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1}
|
||||
showSearch={true}
|
||||
// filterOption={(inputValue, option) => option.label?.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1}
|
||||
placeholder={t('memoryConversation.searchPlaceholder')}
|
||||
@@ -118,14 +151,29 @@ const MemoryConversation: FC = () => {
|
||||
>
|
||||
<Chat
|
||||
empty={
|
||||
<Empty url={ConversationEmptyIcon} size={[140, 100]} title={t('memoryConversation.conversationContentEmpty')} />
|
||||
<Empty url={ConversationEmptyIcon} className="rb:h-full" size={[140, 100]} title={t('memoryConversation.conversationContentEmpty')} />
|
||||
}
|
||||
contentClassName='rb:h-[calc(100vh-362px)]'
|
||||
data={chatData}
|
||||
query={query}
|
||||
onChange={setQuery}
|
||||
onChange={setMsg}
|
||||
onSend={handleSend}
|
||||
loading={loading}
|
||||
/>
|
||||
labelFormat={(item) => dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}
|
||||
>
|
||||
<Flex gap={8}>
|
||||
{searchSwitchList.map(item => (
|
||||
<ButtonCheckbox
|
||||
key={item.value}
|
||||
icon={item.icon}
|
||||
checkedIcon={item.checkedIcon}
|
||||
checked={search_switch === item.value}
|
||||
onChange={() => handleChange(item.value)}
|
||||
>
|
||||
{t(`memoryConversation.${item.label}`)}
|
||||
</ButtonCheckbox>
|
||||
))}
|
||||
</Flex>
|
||||
</Chat>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
@@ -147,8 +195,8 @@ const MemoryConversation: FC = () => {
|
||||
{logs.map((log, logIndex) => (
|
||||
<div key={logIndex}
|
||||
className={clsx(
|
||||
`rb:p-[16px_24px] rb:rounded-[8px]`,
|
||||
'rb:border-[1px] rb:border-[#DFE4ED]',
|
||||
`rb:p-[16px_24px] rb:rounded-lg`,
|
||||
'rb:border rb:border-[#DFE4ED]',
|
||||
{
|
||||
'rb:shadow-[inset_4px_0px_0px_0px_#155EEF]': logIndex % 3 === 0,
|
||||
'rb:shadow-[inset_4px_0px_0px_0px_#369F21]': logIndex % 3 === 1,
|
||||
@@ -156,14 +204,14 @@ const MemoryConversation: FC = () => {
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-[22px] rb:mb-[24px]">{log.title}</div>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-[22px] rb:mb-6">{log.title}</div>
|
||||
{log.type === 'problem_split' && Array.isArray(log.data) && log.data.length > 0
|
||||
? <Space size={12} direction="vertical" style={{width: '100%'}}>
|
||||
{log.data.map(vo => (
|
||||
<ContentWrapper key={vo.id}>
|
||||
<>
|
||||
<div className="rb:font-medium rb:text-[#212332]">{vo.id}. {vo.question}</div>
|
||||
<div className="rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167]">{vo.reason}</div>
|
||||
<div className="rb:mt-2 rb:text-[12px] rb:text-[#5B6167]">{vo.reason}</div>
|
||||
</>
|
||||
</ContentWrapper>
|
||||
))}
|
||||
@@ -175,7 +223,7 @@ const MemoryConversation: FC = () => {
|
||||
<>
|
||||
<div className="rb:font-medium rb:text-[#212332]">{key}</div>
|
||||
{(log.data as Record<string, string[]>)[key].map((item, index) => (
|
||||
<div key={index} className="rb:mt-[8px] rb:text-[#5B6167] rb:text-[12px]">{item}</div>
|
||||
<div key={index} className="rb:mt-2 rb:text-[#5B6167] rb:text-[12px]">{item}</div>
|
||||
))}
|
||||
</>
|
||||
</ContentWrapper>
|
||||
@@ -183,15 +231,15 @@ const MemoryConversation: FC = () => {
|
||||
</Space>
|
||||
: log.type === 'search_result' && log.raw_results
|
||||
? <ContentWrapper>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-[8px]">{log.query}</div>
|
||||
<div className='rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167]'>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-2">{log.query}</div>
|
||||
<div className='rb:mt-2 rb:text-[12px] rb:text-[#5B6167]'>
|
||||
{typeof log.raw_results === 'string'
|
||||
? <Markdown content={log.raw_results} />
|
||||
: <>
|
||||
{log.raw_results.reranked_results?.statements.length > 0 && log.raw_results.reranked_results?.statements.map((item, index) => (
|
||||
{log.raw_results.reranked_results?.statements.length > 0 && log.raw_results.reranked_results?.statements.map((item: { statement: string }, index: number) => (
|
||||
<div key={index}>{item.statement}</div>
|
||||
))}
|
||||
{log.raw_results.reranked_results?.summaries.length > 0 && log.raw_results.reranked_results?.summaries.map((item, index) => (
|
||||
{log.raw_results.reranked_results?.summaries.length > 0 && log.raw_results.reranked_results?.summaries.map((item: { content: string }, index: number) => (
|
||||
<div key={index}>{item.content}</div>
|
||||
))}
|
||||
</>
|
||||
@@ -203,26 +251,26 @@ const MemoryConversation: FC = () => {
|
||||
: log.type === 'verification'
|
||||
? <ContentWrapper>
|
||||
<div className="rb:font-medium rb:text-[#212332]">{log.query}</div>
|
||||
<div className="rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167]">{log.reason}</div>
|
||||
<div className="rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167]">{log.result}</div>
|
||||
<div className="rb:mt-2 rb:text-[12px] rb:text-[#5B6167]">{log.reason}</div>
|
||||
<div className="rb:mt-2 rb:text-[12px] rb:text-[#5B6167]">{log.result}</div>
|
||||
</ContentWrapper>
|
||||
: log.type === 'output_type'
|
||||
? <ContentWrapper>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-[8px]">{log.query}</div>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-2">{log.query}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167]">{log.summary}</div>
|
||||
</ContentWrapper>
|
||||
: log.type === 'input_summary' && log.raw_results
|
||||
? <ContentWrapper>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-[8px]">{log.query}</div>
|
||||
<div className="rb:font-medium rb:text-[12px] rb:text-[#5B6167] rb:mb-[8px]">{log.summary}</div>
|
||||
<div className='rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167]'>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-2">{log.query}</div>
|
||||
<div className="rb:font-medium rb:text-[12px] rb:text-[#5B6167] rb:mb-2">{log.summary}</div>
|
||||
<div className='rb:mt-2 rb:text-[12px] rb:text-[#5B6167]'>
|
||||
{typeof log.raw_results === 'string'
|
||||
? <Markdown content={log.raw_results} />
|
||||
: <>
|
||||
{log.raw_results.reranked_results?.statements.length > 0 && log.raw_results.reranked_results?.statements.map((item, index) => (
|
||||
{log.raw_results.reranked_results?.statements.length > 0 && log.raw_results.reranked_results?.statements.map((item: { statement: string; } , index: number) => (
|
||||
<div key={index}>{item.statement}</div>
|
||||
))}
|
||||
{log.raw_results.reranked_results?.summaries.length > 0 && log.raw_results.reranked_results?.summaries.map((item, index) => (
|
||||
{log.raw_results.reranked_results?.summaries.length > 0 && log.raw_results.reranked_results?.summaries.map((item: { content: string; }, index: number) => (
|
||||
<div key={index}>{item.content}</div>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -12,6 +12,7 @@ interface CardProps {
|
||||
expanded?: boolean;
|
||||
handleExpand?: (type: string) => void;
|
||||
className?: string;
|
||||
headerClassName?: string;
|
||||
bodyClassName?: string;
|
||||
}
|
||||
|
||||
@@ -23,6 +24,7 @@ const Card: FC<CardProps> = ({
|
||||
expanded,
|
||||
handleExpand,
|
||||
className,
|
||||
headerClassName,
|
||||
bodyClassName,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -37,12 +39,13 @@ const Card: FC<CardProps> = ({
|
||||
onClick={() => handleExpand(type)}
|
||||
>
|
||||
{expanded ? t('common.foldUp') : t('common.expanded')}
|
||||
<img src={down} className={clsx("rb:w-[16px] rb:h-[16px] rb:ml-[4px]", {
|
||||
<img src={down} className={clsx("rb:w-4 rb:h-4 rb:ml-1", {
|
||||
'rb:rotate-180': !expanded,
|
||||
})} />
|
||||
</div>
|
||||
)}
|
||||
className={className}
|
||||
headerClassName={headerClassName}
|
||||
bodyClassName={bodyClassName}
|
||||
>
|
||||
{(expanded || !(type && handleExpand)) && children}
|
||||
|
||||
426
web/src/views/MemoryExtractionEngine/components/Result.tsx
Normal file
426
web/src/views/MemoryExtractionEngine/components/Result.tsx
Normal file
@@ -0,0 +1,426 @@
|
||||
import { type FC, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Space, Button, Progress } from 'antd'
|
||||
import { ExclamationCircleFilled, CheckCircleFilled, ClockCircleOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||
import clsx from 'clsx'
|
||||
import Card from './Card'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import RbAlert from '@/components/RbAlert'
|
||||
import type { TestResult } from '../types'
|
||||
import { pilotRunMemoryExtractionConfig } from '@/api/memory'
|
||||
import { type SSEMessage } from '@/utils/stream'
|
||||
import Tag, { type TagProps } from '@/components/Tag'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import { groupDataByType } from '../constant'
|
||||
import type { AnyObject } from 'antd/es/_util/type';
|
||||
|
||||
const resultObj = {
|
||||
extractTheNumberOfEntities: 'entities.extracted_count',
|
||||
numberOfEntityDisambiguation: 'disambiguation.block_count',
|
||||
memoryFragments: 'memory.chunks',
|
||||
numberOfRelationalTriples: 'triplets.count'
|
||||
}
|
||||
interface ResultProps {
|
||||
loading: boolean;
|
||||
handleSave: () => void;
|
||||
}
|
||||
interface ModuleItem {
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||
data: any[],
|
||||
result: any,
|
||||
start_at?: number;
|
||||
end_at?: number;
|
||||
}
|
||||
const tagColors: {
|
||||
[key: string]: TagProps['color']
|
||||
} = {
|
||||
pending: 'default',
|
||||
processing: 'processing',
|
||||
completed: 'success',
|
||||
failed: 'error'
|
||||
}
|
||||
const initObj = {
|
||||
data: [],
|
||||
status: 'pending',
|
||||
result: null
|
||||
}
|
||||
|
||||
const Result: FC<ResultProps> = ({ loading, handleSave }) => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams()
|
||||
const [runLoading, setRunLoading] = useState(false)
|
||||
const [testResult, setTestResult] = useState<TestResult>({} as TestResult)
|
||||
|
||||
const [textPreprocessing, setTextPreprocessing] = useState<ModuleItem>(initObj as ModuleItem)
|
||||
const [knowledgeExtraction, setKnowledgeExtraction] = useState<ModuleItem>(initObj as ModuleItem)
|
||||
const [creatingNodesEdges, setCreatingNodesEdges] = useState<ModuleItem>(initObj as ModuleItem)
|
||||
const [deduplication, setDeduplication] = useState<ModuleItem>(initObj as ModuleItem)
|
||||
|
||||
const handleRun = () => {
|
||||
if(!id) return
|
||||
setTextPreprocessing({...initObj} as ModuleItem)
|
||||
setKnowledgeExtraction({...initObj} as ModuleItem)
|
||||
setCreatingNodesEdges({...initObj} as ModuleItem)
|
||||
setDeduplication({...initObj} as ModuleItem)
|
||||
setTestResult({} as TestResult)
|
||||
const handleStreamMessage = (list: SSEMessage[]) => {
|
||||
|
||||
list.forEach((data: AnyObject) => {
|
||||
switch(data.event) {
|
||||
case 'text_preprocessing': // 开始预处理文本
|
||||
setTextPreprocessing(prev => ({
|
||||
...prev,
|
||||
status: 'processing',
|
||||
start_at: data.data.time
|
||||
}))
|
||||
break
|
||||
case 'text_preprocessing_result': // 预处理文本分块中
|
||||
setTextPreprocessing(prev => ({
|
||||
...prev,
|
||||
data: [...prev.data, data.data?.data]
|
||||
}))
|
||||
break
|
||||
case 'text_preprocessing_complete': // 预处理文本完成
|
||||
setTextPreprocessing(prev => ({
|
||||
...prev,
|
||||
result: data.data?.data,
|
||||
status: 'completed',
|
||||
end_at: data.data.time
|
||||
}))
|
||||
break
|
||||
case 'knowledge_extraction': // 开始知识抽取
|
||||
setKnowledgeExtraction(prev => ({
|
||||
...prev,
|
||||
status: 'processing',
|
||||
start_at: data.data.time
|
||||
}))
|
||||
break
|
||||
case 'knowledge_extraction_result': // 知识抽取中
|
||||
setKnowledgeExtraction(prev => ({
|
||||
...prev,
|
||||
data: [...prev.data, data.data?.data]
|
||||
}))
|
||||
break
|
||||
case 'knowledge_extraction_complete': // 知识抽取完成
|
||||
setKnowledgeExtraction(prev => ({
|
||||
...prev,
|
||||
result: data.data?.data,
|
||||
status: 'completed',
|
||||
end_at: data.data.time
|
||||
}))
|
||||
break
|
||||
case 'creating_nodes_edges': // 开始创建节点和边
|
||||
setCreatingNodesEdges(prev => ({
|
||||
...prev,
|
||||
status: 'processing',
|
||||
start_at: data.data.time
|
||||
}))
|
||||
break
|
||||
case 'creating_nodes_edges_result': // 创建节点和边中
|
||||
setCreatingNodesEdges(prev => ({
|
||||
...prev,
|
||||
data: [...prev.data, data.data?.data]
|
||||
}))
|
||||
break
|
||||
case 'creating_nodes_edges_complete': // 创建节点和边完成
|
||||
setCreatingNodesEdges(prev => ({
|
||||
...prev,
|
||||
result: data.data?.data,
|
||||
status: 'completed',
|
||||
end_at: data.data.time
|
||||
}))
|
||||
break
|
||||
case 'deduplication': // 开始去重消歧
|
||||
setDeduplication(prev => ({
|
||||
...prev,
|
||||
status: 'processing',
|
||||
start_at: data.data.time
|
||||
}))
|
||||
break
|
||||
case 'dedup_disambiguation_result': // 去重消歧中
|
||||
setDeduplication(prev => ({
|
||||
...prev,
|
||||
data: [...prev.data, data.data.data]
|
||||
}))
|
||||
break
|
||||
case 'dedup_disambiguation_complete': // 去重消歧完成
|
||||
setDeduplication(prev => ({
|
||||
...prev,
|
||||
result: data.data?.data,
|
||||
status: 'completed',
|
||||
end_at: data.data.time
|
||||
}))
|
||||
break
|
||||
case 'generating_results': // 开始生成结果
|
||||
break
|
||||
case 'result': // 结果
|
||||
setTestResult(data.data?.extracted_result)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
setRunLoading(true)
|
||||
pilotRunMemoryExtractionConfig({
|
||||
config_id: id,
|
||||
dialogue_text: t('memoryExtractionEngine.exampleText'),
|
||||
}, handleStreamMessage)
|
||||
.finally(() => {
|
||||
setRunLoading(false)
|
||||
})
|
||||
}
|
||||
const completedNum = [textPreprocessing, knowledgeExtraction, creatingNodesEdges, deduplication].filter(item => item.status === 'completed').length
|
||||
const deduplicationData = groupDataByType(deduplication.data, 'result_type')
|
||||
|
||||
const formatTag = (status: string) => {
|
||||
return (
|
||||
<Tag color={tagColors[status]}>
|
||||
{status === 'pending' && <ClockCircleOutlined className="rb:mr-1" />}
|
||||
{status === 'processing' && <LoadingOutlined spin className="rb:mr-1" />}
|
||||
{t(`memoryExtractionEngine.status.${status}`)}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
const formatTime = (data: ModuleItem, color?: string) => {
|
||||
if (typeof data.end_at === 'number' && typeof data.start_at === 'number') {
|
||||
return <div className={`rb:mt-3 rb:text-[${color ?? '#155EEF'}]`}>{t('memoryExtractionEngine.time')}{data.end_at - data.start_at}ms</div>
|
||||
}
|
||||
return null
|
||||
}
|
||||
const lowercaseFirst = (str: string) => str.charAt(0).toLowerCase() + str.slice(1)
|
||||
return (
|
||||
<Card
|
||||
title={t('memoryExtractionEngine.exampleMemoryExtractionResults')}
|
||||
subTitle={t('memoryExtractionEngine.exampleMemoryExtractionResultsSubTitle')}
|
||||
className="rb:min-h-[calc(100vh-330px)]!"
|
||||
headerClassName="rb:pb-0! rb:pt-4!"
|
||||
bodyClassName="rb:min-h-[calc(100vh-388px)] rb:p-[16px_20px]!"
|
||||
>
|
||||
<div className="rb:min-h-[calc(100vh-480px)] rb:overflow-y-auto">
|
||||
{runLoading
|
||||
? <>
|
||||
<RbAlert color="blue" icon={<ExclamationCircleFilled />} className="rb:mb-3.5">
|
||||
{t('memoryExtractionEngine.processing')}
|
||||
</RbAlert>
|
||||
{/* 整体进度 */}
|
||||
<div className="rb:mb-2">
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[12px] rb:leading-4 rb:font-regular">
|
||||
{t('memoryExtractionEngine.overallProgress')}
|
||||
<span className="rb:text-[#155eef]">{`${completedNum}/4`}</span>
|
||||
</div>
|
||||
<Progress percent={completedNum * 100/4} showInfo={false} />
|
||||
</div>
|
||||
</>
|
||||
: !testResult || Object.keys(testResult).length === 0
|
||||
? <RbAlert color="orange" icon={<ExclamationCircleFilled />} className="rb:mb-3.5">
|
||||
{t('memoryExtractionEngine.warning')}
|
||||
</RbAlert>
|
||||
: <RbAlert color="green" icon={<ExclamationCircleFilled />} className="rb:mb-3.5">
|
||||
{t('memoryExtractionEngine.success')}
|
||||
</RbAlert>
|
||||
}
|
||||
<Space size={16} direction="vertical" style={{ width: '100%' }}>
|
||||
{/* 文本预处理 */}
|
||||
<RbCard
|
||||
title={t(`memoryExtractionEngine.text_preprocessing`)}
|
||||
extra={formatTag(textPreprocessing.status)}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
>
|
||||
{textPreprocessing.data.map((vo, index) => (
|
||||
<div key={index} className="rb:mb-3 rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:font-regular">
|
||||
<Markdown content={'-' + t('memoryExtractionEngine.fragment') + vo.chunk_index + ': ' + (vo.content.startsWith('\n') ? vo.content : '\n' + vo.content)} />
|
||||
</div>
|
||||
))}
|
||||
{formatTime(textPreprocessing)}
|
||||
{textPreprocessing.result &&
|
||||
<RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-3">
|
||||
{t('memoryExtractionEngine.text_preprocessing_desc', { count: textPreprocessing.result.total_chunks })},
|
||||
{t('memoryExtractionEngine.chunkerStrategy')}: {t(`memoryExtractionEngine.${lowercaseFirst(textPreprocessing.result.chunker_strategy)}`)}
|
||||
</RbAlert>
|
||||
}
|
||||
</RbCard>
|
||||
{/* 知识抽取 */}
|
||||
<RbCard
|
||||
title={t(`memoryExtractionEngine.knowledge_extraction`)}
|
||||
extra={formatTag(knowledgeExtraction.status)}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
>
|
||||
{knowledgeExtraction.data.map(vo =>
|
||||
<div key={vo.statement_index} className="rb:mb-3 rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:font-regular">{vo.statement}</div>
|
||||
)}
|
||||
{formatTime(knowledgeExtraction)}
|
||||
{knowledgeExtraction.result && <RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-3">
|
||||
{t('memoryExtractionEngine.knowledge_extraction_desc', {
|
||||
entities: knowledgeExtraction.result.entities_count,
|
||||
statements: knowledgeExtraction.result.statements_count,
|
||||
temporal_ranges_count: knowledgeExtraction.result.temporal_ranges_count,
|
||||
triplets: knowledgeExtraction.result.triplets_count
|
||||
})}
|
||||
</RbAlert>}
|
||||
</RbCard>
|
||||
{/* 创建实体关系 */}
|
||||
<RbCard
|
||||
title={t(`memoryExtractionEngine.creating_nodes_edges`)}
|
||||
extra={formatTag(creatingNodesEdges.status)}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#9C6FFF]!"
|
||||
>
|
||||
{creatingNodesEdges.data?.map((vo, index) => (
|
||||
<div key={index} className="rb:mb-3 rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:font-regular">
|
||||
{vo?.result_type === 'entity_nodes_creation'
|
||||
? <>{vo.type_display_name}: {vo.entity_names.join(', ')}</>
|
||||
: <>{vo?.relationship_text}</>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
{formatTime(creatingNodesEdges, '#9C6FFF')}
|
||||
{creatingNodesEdges.result && <RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-3">
|
||||
{t('memoryExtractionEngine.creating_nodes_edges_desc', {num: creatingNodesEdges.result.entity_entity_edges_count})}
|
||||
</RbAlert>}
|
||||
</RbCard>
|
||||
{/* 去重消歧 */}
|
||||
<RbCard
|
||||
title={t(`memoryExtractionEngine.deduplication`)}
|
||||
extra={formatTag(deduplication.status)}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#9C6FFF]!"
|
||||
>
|
||||
{Object.keys(deduplicationData).length > 0 && Object.keys(deduplicationData).map(key => {
|
||||
return deduplicationData[key].map((vo, index) => (
|
||||
<div key={index} className="rb:mb-3 rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:font-regular">
|
||||
{vo.message}
|
||||
</div>
|
||||
))
|
||||
})}
|
||||
{formatTime(deduplication, '#9C6FFF')}
|
||||
{deduplication.result && <RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-3">
|
||||
{t('memoryExtractionEngine.deduplication_desc', { count: deduplication.result.summary.total_merges })}<br />
|
||||
</RbAlert>}
|
||||
</RbCard>
|
||||
|
||||
{testResult && Object.keys(testResult).length > 0 && resultObj && Object.keys(resultObj).length > 0 &&
|
||||
<RbCard>
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-[40px_57px]">
|
||||
{Object.keys(resultObj).map((key, index) => {
|
||||
const keys = (resultObj as Record<string, string>)[key].split('.')
|
||||
return (
|
||||
<div key={index}>
|
||||
<div className="rb:text-[24px] rb:leading-[30px] rb:font-extrabold">{(testResult?.[keys[0] as keyof TestResult] as any)?.[keys[1]]}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:font-regular">{t(`memoryExtractionEngine.${key}`)}</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#369F21] rb:leading-3.5 rb:font-regular">
|
||||
{}
|
||||
{key === 'extractTheNumberOfEntities' && testResult.dedup
|
||||
? t(`memoryExtractionEngine.${key}Desc`, {
|
||||
num: testResult.dedup.total_merged_count,
|
||||
exact: testResult.dedup.breakdown.exact,
|
||||
fuzzy: testResult.dedup.breakdown.fuzzy,
|
||||
llm: testResult.dedup.breakdown.llm,
|
||||
})
|
||||
: key === 'numberOfEntityDisambiguation' && testResult.disambiguation
|
||||
? t(`memoryExtractionEngine.${key}Desc`, { num: testResult.disambiguation.effects?.length, block_count: testResult.disambiguation.block_count })
|
||||
: key === 'numberOfRelationalTriples' && testResult.triplets
|
||||
? t(`memoryExtractionEngine.${key}Desc`, { num: testResult.triplets.count })
|
||||
:t(`memoryExtractionEngine.${key}Desc`)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)})}
|
||||
</div>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
{testResult?.dedup?.impact && testResult.dedup.impact?.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.entityDeduplicationImpact')}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-medium rb:leading-4">{t('memoryExtractionEngine.identifyDuplicates')}</div>
|
||||
{testResult.dedup.impact.map((item, index) => (
|
||||
<div key={index} className="rb:pl-2 rb:mt-2 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">
|
||||
-{t('memoryExtractionEngine.identifyDuplicatesDesc', { ...item })}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-3">
|
||||
{t('memoryExtractionEngine.entityDeduplicationImpactDesc', { count: testResult.dedup.impact.length })}
|
||||
</RbAlert>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
{testResult?.disambiguation && testResult.disambiguation?.effects?.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.theEffectOfEntityDisambiguationLLMDriven')}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
>
|
||||
{testResult.disambiguation.effects.map((item, index) => (
|
||||
<div key={index} className={clsx("rb:text-[12px] rb:text-[#5B6167] rb:leading-4", {
|
||||
'rb:mt-4': index > 0,
|
||||
})}>
|
||||
<div className="rb:font-medium rb:mb-2">Disagreement Case {index +1}:</div>
|
||||
-{item.left.name}({item.left.type}) vs {item.right.name}({item.right.type}) → <span className="rb:text-[#369F21]">{item.result}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-3">
|
||||
{t('memoryExtractionEngine.entityDeduplicationImpactDesc', { count: testResult.dedup.impact.length })}
|
||||
</RbAlert>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
{testResult?.core_entities && testResult?.core_entities.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.coreEntitiesAfterDedup')}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#369F21]!"
|
||||
>
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-6">
|
||||
{testResult.core_entities.map((item, idx) => (
|
||||
<div key={idx} className="rb:text-[12px]">
|
||||
<div className="rb:text-[#369F21] rb:font-medium">{item.type}({item.count})</div>
|
||||
|
||||
<div>
|
||||
{item.entities.map((entity, index) => (
|
||||
<div key={index} className="rb:text-[#5B6167] rb:font-regular rb:leading-4">
|
||||
-{entity}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
{testResult?.triplet_samples && testResult?.triplet_samples.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.extractRelationalTriples')}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#9C6FFF]!"
|
||||
>
|
||||
<Space size={8} direction="vertical" className="rb:w-full">
|
||||
{testResult.triplet_samples.map((item, index) => (
|
||||
<div key={index} className="rb:text-[12px]">
|
||||
-({item.subject}, <span className="rb:text-[#9C6FFF] rb:font-medium">{item.predicate}</span>, {item.object})
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
<RbAlert color="purple" icon={<CheckCircleFilled />} className="rb:mt-3">
|
||||
{t('memoryExtractionEngine.extractRelationalTriplesDesc', { count: testResult.triplet_samples.length })}
|
||||
</RbAlert>
|
||||
</RbCard>
|
||||
}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-5">
|
||||
<Button block loading={loading} onClick={handleSave}>{t('common.save')}</Button>
|
||||
<Button block type="primary" loading={runLoading} onClick={handleRun}>{t('memoryExtractionEngine.debug')}</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
export default Result
|
||||
1698
web/src/views/MemoryExtractionEngine/constant.ts
Normal file
1698
web/src/views/MemoryExtractionEngine/constant.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,16 @@
|
||||
import { type FC, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Space, Switch, Select, InputNumber, Slider, Button, App, Skeleton, Form } from 'antd'
|
||||
import { ExclamationCircleFilled, CheckCircleFilled } from '@ant-design/icons'
|
||||
import { Row, Col, Space, Switch, Select, InputNumber, Slider, App, Form } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
import Card from './components/Card'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import RbAlert from '@/components/RbAlert'
|
||||
import Empty from '@/components/Empty'
|
||||
import type { ConfigForm, ConfigVo, Variable, TestResult } from './types'
|
||||
import { getMemoryExtractionConfig, updateMemoryExtractionConfig, pilotRunMemoryExtractionConfig } from '@/api/memory'
|
||||
import type { ConfigForm, Variable } from './types'
|
||||
import { getMemoryExtractionConfig, updateMemoryExtractionConfig } from '@/api/memory'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import { getModelList } from '@/api/models';
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import { configList } from './constant'
|
||||
import Result from './components/Result'
|
||||
|
||||
const keys = [
|
||||
// 'example',
|
||||
@@ -20,229 +18,16 @@ const keys = [
|
||||
'arrangementLayerModule'
|
||||
]
|
||||
|
||||
const configList: ConfigVo[] = [
|
||||
{
|
||||
type: 'storageLayerModule',
|
||||
data: [
|
||||
{
|
||||
title: 'entityDeduplicationDisambiguation',
|
||||
list: [
|
||||
{
|
||||
label: 'enableLlmDedupBlockwise',
|
||||
variableName: 'enable_llm_dedup_blockwise',
|
||||
control: 'button', // switch
|
||||
type: 'tinyint',
|
||||
},
|
||||
{
|
||||
label: 'enableLlmDisambiguation',
|
||||
variableName: 'enable_llm_disambiguation',
|
||||
control: 'button',
|
||||
type: 'tinyint',
|
||||
},
|
||||
{
|
||||
label: 'tNameStrict',
|
||||
control: 'slider',
|
||||
variableName: 't_name_strict',
|
||||
type: 'decimal',
|
||||
},
|
||||
{
|
||||
label: 'tTypeStrict',
|
||||
control: 'slider',
|
||||
variableName: 't_type_strict',
|
||||
type: 'decimal',
|
||||
},
|
||||
{
|
||||
label: 'tOverall',
|
||||
control: 'slider',
|
||||
variableName: 't_overall',
|
||||
type: 'decimal',
|
||||
},
|
||||
]
|
||||
},
|
||||
// 语义锚点标注
|
||||
{
|
||||
title: 'semanticAnchorAnnotationModule',
|
||||
list: [
|
||||
// 句子提取颗粒度
|
||||
{
|
||||
label: 'statementGranularity',
|
||||
variableName: 'statement_granularity',
|
||||
control: 'slider',
|
||||
type: 'decimal',
|
||||
max: 3,
|
||||
min: 1,
|
||||
step: 1,
|
||||
meaning: 'statementGranularityDesc',
|
||||
},
|
||||
// 是否包含对话上下文
|
||||
{
|
||||
label: 'includeDialogueContext',
|
||||
variableName: 'include_dialogue_context',
|
||||
control: 'button', // switch
|
||||
type: 'tinyint',
|
||||
meaning: 'includeDialogueContextDesc'
|
||||
},
|
||||
// 上下文文字上限
|
||||
{
|
||||
label: 'maxDialogueContextChars',
|
||||
variableName: 'max_context',
|
||||
control: 'inputNumber',
|
||||
min: 100,
|
||||
type: 'decimal',
|
||||
meaning: 'maxDialogueContextCharsDesc',
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'arrangementLayerModule',
|
||||
data: [
|
||||
{
|
||||
title: 'queryMode',
|
||||
list: [
|
||||
{
|
||||
label: 'deepRetrieval',
|
||||
variableName: 'deep_retrieval',
|
||||
control: 'button',
|
||||
type: 'tinyint',
|
||||
meaning: 'deepRetrievalMeaning',
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'dataPreprocessing',
|
||||
list: [
|
||||
{
|
||||
label: 'chunkerStrategy',
|
||||
variableName: 'chunker_strategy',
|
||||
control: 'select',
|
||||
type: 'enum',
|
||||
options: [
|
||||
{ label: 'recursiveChunker', value: 'RecursiveChunker' }, // 递归分块
|
||||
{ label: 'tokenChunker', value: 'TokenChunker' }, // token 分块
|
||||
{ label: 'semanticChunker', value: 'SemanticChunker' }, // 语义分块
|
||||
{ label: 'neuralChunker', value: 'NeuralChunker' }, // 神经网络分块
|
||||
{ label: 'hybridChunker', value: 'HybridChunker' }, // 混合分块
|
||||
{ label: 'llmChunker', value: 'LLMChunker' }, // LLM 分块
|
||||
{ label: 'sentenceChunker', value: 'SentenceChunker' }, // 句子分块
|
||||
{ label: 'lateChunker', value: 'LateChunker' }, // 延迟分块
|
||||
],
|
||||
meaning: 'chunkerStrategyDesc',
|
||||
},
|
||||
]
|
||||
},
|
||||
// 智能语义剪枝
|
||||
{
|
||||
title: 'intelligentSemanticPruning',
|
||||
list: [
|
||||
// 智能语义剪枝功能
|
||||
{
|
||||
label: 'intelligentSemanticPruningFunction',
|
||||
variableName: 'pruning_enabled',
|
||||
control: 'button',
|
||||
type: 'tinyint',
|
||||
meaning: 'intelligentSemanticPruningFunctionDesc',
|
||||
},
|
||||
// 智能语义剪枝场景
|
||||
{
|
||||
label: 'intelligentSemanticPruningScene',
|
||||
variableName: 'pruning_scene',
|
||||
control: 'select',
|
||||
type: 'enum',
|
||||
options: [
|
||||
{ label: 'education', value: 'education' },
|
||||
{ label: 'online_service', value: 'online_service' },
|
||||
{ label: 'outbound', value: 'outbound' },
|
||||
],
|
||||
meaning: 'intelligentSemanticPruningSceneDesc',
|
||||
},
|
||||
// 智能语义剪枝阈值
|
||||
{
|
||||
label: 'intelligentSemanticPruningThreshold',
|
||||
control: 'slider',
|
||||
variableName: 'pruning_threshold',
|
||||
type: 'decimal',
|
||||
max: 0.9,
|
||||
min: 0,
|
||||
step: 0.1,
|
||||
meaning: 'intelligentSemanticPruningThresholdDesc',
|
||||
},
|
||||
]
|
||||
},
|
||||
// 自我反思引擎
|
||||
{
|
||||
title: 'selfReflexionEngine',
|
||||
list: [
|
||||
// 是否启用反思引擎
|
||||
{
|
||||
label: 'enableSelfReflexion',
|
||||
variableName: 'enable_self_reflexion',
|
||||
control: 'button',
|
||||
type: 'tinyint',
|
||||
},
|
||||
// 迭代周期
|
||||
{
|
||||
label: 'iterationPeriod',
|
||||
variableName: 'iteration_period',
|
||||
control: 'select',
|
||||
type: 'enum',
|
||||
options: [
|
||||
{ label: 'oneHour', value: '1' },
|
||||
{ label: 'threeHours', value: '3' },
|
||||
{ label: 'sixHours', value: '6' },
|
||||
{ label: 'twelveHours', value: '12' },
|
||||
{ label: 'daily', value: '24' },
|
||||
],
|
||||
meaning: 'iterationPeriodDesc',
|
||||
},
|
||||
// 反思范围
|
||||
{
|
||||
label: 'reflexionRange',
|
||||
variableName: 'reflexion_range',
|
||||
control: 'select',
|
||||
type: 'enum',
|
||||
options: [
|
||||
{ label: 'retrieval', value: 'retrieval' },
|
||||
{ label: 'database', value: 'database' },
|
||||
],
|
||||
meaning: 'reflexionRangeDesc',
|
||||
},
|
||||
// 反思基线
|
||||
{
|
||||
label: 'reflectOnTheBaseline',
|
||||
variableName: 'baseline',
|
||||
control: 'select',
|
||||
type: 'enum',
|
||||
options: [
|
||||
{ label: 'basedOnTime', value: 'TIME' },
|
||||
{ label: 'basedOnFacts', value: 'FACT' },
|
||||
{ label: 'basedOnFactsAndTime', value: 'TIME-FACT' },
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const resultObj = {
|
||||
extractTheNumberOfEntities: 'entities.extracted_count',
|
||||
numberOfEntityDisambiguation: 'disambiguation.block_count',
|
||||
memoryFragments: 'memory.chunks',
|
||||
numberOfRelationalTriples: 'triplets.count'
|
||||
}
|
||||
|
||||
const ConfigDesc: FC<{ config: Variable, className?: string }> = ({config, className}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className={className}>
|
||||
<Space size={8} className={clsx("rb:mt-[4px] rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px] ")}>
|
||||
<Space size={8} className={clsx("rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 ")}>
|
||||
{config.variableName && <span className="rb:font-regular">{t('memoryExtractionEngine.variableName')}: {config.variableName}</span>}
|
||||
{config.control && <span className="rb:font-regular">{t('memoryExtractionEngine.control')}: {t(`memoryExtractionEngine.${config.control}`)}</span>}
|
||||
{config.type && <span className="rb:font-regular">{t('memoryExtractionEngine.type')}: {config.type}</span>}
|
||||
</Space>
|
||||
{config.meaning && <div className={clsx("rb:mt-[4px] rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px] ")}>{t('memoryExtractionEngine.Meaning')}: {t(`memoryExtractionEngine.${config.meaning}`)}</div>}
|
||||
{config.meaning && <div className={clsx("rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 ")}>{t('memoryExtractionEngine.Meaning')}: {t(`memoryExtractionEngine.${config.meaning}`)}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -253,12 +38,9 @@ const MemoryExtractionEngine: FC = () => {
|
||||
const [expandedKeys, setExpandedKeys] = useState<string[]>(keys)
|
||||
const [form] = Form.useForm<ConfigForm>()
|
||||
const [modelForm] = Form.useForm()
|
||||
// const [data, setData] = useState<ConfigForm>()
|
||||
const modelValues = Form.useWatch([], modelForm)
|
||||
const values = Form.useWatch<ConfigForm>([], form)
|
||||
const [testResult, setTestResult] = useState<TestResult | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [runLoading, setRunLoading] = useState(false)
|
||||
const [iterationPeriodDisabled, setIterationPeriodDisabled] = useState(false)
|
||||
const [modelList, setModelList] = useState<Model[]>([])
|
||||
|
||||
@@ -305,8 +87,6 @@ const MemoryExtractionEngine: FC = () => {
|
||||
if (id) {
|
||||
getConfig()
|
||||
getModels()
|
||||
const lastResult = localStorage.getItem(`${id}_testResult`)
|
||||
setTestResult(lastResult ? JSON.parse(lastResult) : null)
|
||||
}
|
||||
}, [id])
|
||||
|
||||
@@ -332,35 +112,11 @@ const MemoryExtractionEngine: FC = () => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
const handleRun = () => {
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
setRunLoading(true)
|
||||
updateMemoryExtractionConfig({
|
||||
...values,
|
||||
...modelValues,
|
||||
config_id: id,
|
||||
}).then(() => {
|
||||
pilotRunMemoryExtractionConfig({
|
||||
config_id: id,
|
||||
dialogue_text: t('memoryExtractionEngine.exampleText'),
|
||||
}).then((res) => {
|
||||
message.success(t('common.testSuccess'))
|
||||
const response = res as { extracted_result: TestResult }
|
||||
setTestResult(response.extracted_result || {})
|
||||
localStorage.setItem(`${id}_testResult`, JSON.stringify(response.extracted_result || {}))
|
||||
})
|
||||
.finally(() => {
|
||||
setRunLoading(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rb:text-[24px] rb:font-semibold rb:leading-[32px] rb:mb-[8px]">{t('memoryExtractionEngine.title')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:leading-[20px] rb:mb-[24px]">{t('memoryExtractionEngine.subTitle')}</div>
|
||||
<div className="rb:text-[24px] rb:font-semibold rb:leading-8 rb:mb-2">{t('memoryExtractionEngine.title')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:mb-6">{t('memoryExtractionEngine.subTitle')}</div>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
@@ -388,12 +144,12 @@ const MemoryExtractionEngine: FC = () => {
|
||||
handleExpand={handleExpand}
|
||||
>
|
||||
{expandedKeys.includes('example') &&
|
||||
<div className="rb:text-[14px] rb:text-[#5B6167] rb:font-regular rb:leading-[20px]">
|
||||
<div className="rb:text-[14px] rb:text-[#5B6167] rb:font-regular rb:leading-5">
|
||||
<Markdown content={t('memoryExtractionEngine.exampleText')} />
|
||||
</div>
|
||||
}
|
||||
</Card>
|
||||
<Row gutter={[16, 16]} className="rb:mt-[16px]">
|
||||
<Row gutter={[16, 16]} className="rb:mt-4">
|
||||
<Col span={14}>
|
||||
<Form
|
||||
form={form}
|
||||
@@ -412,8 +168,8 @@ const MemoryExtractionEngine: FC = () => {
|
||||
<div
|
||||
key={vo.title}
|
||||
className={clsx(
|
||||
`rb:p-[16px_24px] rb:rounded-[8px]`,
|
||||
'rb:border-[1px] rb:border-[#DFE4ED]',
|
||||
`rb:p-[16px_24px] rb:rounded-lg`,
|
||||
'rb:border rb:border-[#DFE4ED]',
|
||||
{
|
||||
'rb:shadow-[inset_4px_0px_0px_0px_#155EEF]': index % 2 === 0,
|
||||
'rb:shadow-[inset_4px_0px_0px_0px_#369F21]': index % 2 !== 0,
|
||||
@@ -421,20 +177,20 @@ const MemoryExtractionEngine: FC = () => {
|
||||
)}
|
||||
>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-[22px]">{t(`memoryExtractionEngine.${vo.title}`)}</div>
|
||||
<div className="rb:mt-[4px] rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px]">{t(`memoryExtractionEngine.${vo.title}SubTitle`)}</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`memoryExtractionEngine.${vo.title}SubTitle`)}</div>
|
||||
|
||||
{vo.list.map(config => (
|
||||
<div key={config.label}>
|
||||
{config.control === 'button' &&
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mt-[24px]">
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mt-6">
|
||||
<div>
|
||||
<span className="rb:text-[14px] rb:font-medium rb:leading-[20px]">-{t(`memoryExtractionEngine.${config.label}`)}</span>
|
||||
<ConfigDesc config={config} className="rb:ml-[8px]" />
|
||||
<span className="rb:text-[14px] rb:font-medium rb:leading-5">-{t(`memoryExtractionEngine.${config.label}`)}</span>
|
||||
<ConfigDesc config={config} className="rb:ml-2" />
|
||||
</div>
|
||||
<Form.Item
|
||||
name={config.variableName}
|
||||
valuePropName="checked"
|
||||
className="rb:ml-[8px] rb:mb-[0px]!"
|
||||
className="rb:ml-2 rb:mb-0!"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
@@ -442,10 +198,10 @@ const MemoryExtractionEngine: FC = () => {
|
||||
}
|
||||
{config.control === 'select' &&
|
||||
<>
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-[20px] rb:mt-[24px] rb:mb-[8px]">
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mt-6 rb:mb-2">
|
||||
-{t(`memoryExtractionEngine.${config.label}`)}
|
||||
</div>
|
||||
<div className="rb:pl-[8px]">
|
||||
<div className="rb:pl-2">
|
||||
<Form.Item
|
||||
name={config.variableName}
|
||||
>
|
||||
@@ -454,17 +210,17 @@ const MemoryExtractionEngine: FC = () => {
|
||||
options={config.options ? config.options.map(item => ({ ...item, label: t(`memoryExtractionEngine.${item.label}`) })) : []}
|
||||
/>
|
||||
</Form.Item>
|
||||
<ConfigDesc config={config} className="rb:mt-[-16px]!" />
|
||||
<ConfigDesc config={config} className="rb:-mt-4!" />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{config.control === 'slider' &&
|
||||
<>
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-[20px] rb:mt-[24px] rb:mb-[8px]">
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mt-6 rb:mb-2">
|
||||
-{t(`memoryExtractionEngine.${config.label}`)}
|
||||
</div>
|
||||
<div className="rb:pl-[8px]">
|
||||
<ConfigDesc config={config} className="rb:mb-[10px]" />
|
||||
<div className="rb:pl-2">
|
||||
<ConfigDesc config={config} className="rb:mb-2.5" />
|
||||
<Form.Item
|
||||
name={config.variableName}
|
||||
>
|
||||
@@ -475,7 +231,7 @@ const MemoryExtractionEngine: FC = () => {
|
||||
step={config.step || 0.01}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-[20px] rb:mt-[-26px]">
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5 rb:mt-[-26px]">
|
||||
{config.min || 0}
|
||||
<span>{t('memoryExtractionEngine.CurrentValue')}: {values?.[config.variableName as keyof ConfigForm]}</span>
|
||||
</div>
|
||||
@@ -484,16 +240,16 @@ const MemoryExtractionEngine: FC = () => {
|
||||
}
|
||||
{config.control === 'inputNumber' &&
|
||||
<>
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-[20px] rb:mt-[24px] rb:mb-[8px]">
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mt-6 rb:mb-2">
|
||||
-{t(`memoryExtractionEngine.${config.label}`)}
|
||||
</div>
|
||||
<div className="rb:pl-[8px]">
|
||||
<div className="rb:pl-2">
|
||||
<Form.Item
|
||||
name={config.variableName}
|
||||
>
|
||||
<InputNumber min={config.min || 0} style={{ width: '100%' }} placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<ConfigDesc config={config} className="rb:mt-[-16px]!" />
|
||||
<ConfigDesc config={config} className="rb:-mt-4!" />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@@ -508,148 +264,10 @@ const MemoryExtractionEngine: FC = () => {
|
||||
</Form>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Card
|
||||
title={t('memoryExtractionEngine.exampleMemoryExtractionResults')}
|
||||
subTitle={t('memoryExtractionEngine.exampleMemoryExtractionResultsSubTitle')}
|
||||
className="rb:min-h-[calc(100vh-330px)]!"
|
||||
bodyClassName="rb:min-h-[calc(100vh-388px)]"
|
||||
>
|
||||
<div
|
||||
className="rb:min-h-[calc(100vh-480px)] rb:overflow-y-auto"
|
||||
>
|
||||
{testResult && Object.keys(testResult).length > 0
|
||||
? <>
|
||||
<RbAlert color="orange" icon={<ExclamationCircleFilled />} className="rb:mb-[14px]">
|
||||
{t('memoryExtractionEngine.warning')}
|
||||
</RbAlert>
|
||||
|
||||
<Space size={16} direction="vertical" style={{ width: '100%' }}>
|
||||
{resultObj && Object.keys(resultObj).length > 0 &&
|
||||
<RbCard>
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-[40px_57px]">
|
||||
{Object.keys(resultObj).map(key => {
|
||||
const keys = (resultObj as Record<string, string>)[key].split('.')
|
||||
return (
|
||||
<div key={key}>
|
||||
<div className="rb:text-[24px] rb:leading-[30px] rb:font-extrabold">{testResult?.[keys[0] as keyof TestResult]?.[keys[1]]}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px] rb:font-regular">{t(`memoryExtractionEngine.${key}`)}</div>
|
||||
<div className="rb:mt-[4px] rb:text-[12px] rb:text-[#369F21] rb:leading-[14px] rb:font-regular">
|
||||
{}
|
||||
{key === 'extractTheNumberOfEntities'
|
||||
? t(`memoryExtractionEngine.${key}Desc`, {
|
||||
num: testResult.dedup.total_merged_count,
|
||||
exact: testResult.dedup.breakdown.exact,
|
||||
fuzzy: testResult.dedup.breakdown.fuzzy,
|
||||
llm: testResult.dedup.breakdown.llm,
|
||||
})
|
||||
: key === 'numberOfEntityDisambiguation'
|
||||
? t(`memoryExtractionEngine.${key}Desc`, { num: testResult.disambiguation.effects?.length, block_count: testResult.disambiguation.block_count })
|
||||
: key === 'numberOfRelationalTriples'
|
||||
? t(`memoryExtractionEngine.${key}Desc`, { num: testResult.triplets.count })
|
||||
:t(`memoryExtractionEngine.${key}Desc`)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)})}
|
||||
</div>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
{testResult?.dedup?.impact && testResult.dedup.impact?.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.entityDeduplicationImpact')}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-medium rb:leading-[16px]">{t('memoryExtractionEngine.identifyDuplicates')}</div>
|
||||
{testResult.dedup.impact.map((item, index) => (
|
||||
<div key={index} className="rb:pl-[8px] rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px]">
|
||||
-{t('memoryExtractionEngine.identifyDuplicatesDesc', { ...item })}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-[12px]">
|
||||
{t('memoryExtractionEngine.entityDeduplicationImpactDesc', { count: testResult.dedup.impact.length })}
|
||||
</RbAlert>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
{testResult?.disambiguation && testResult.disambiguation?.effects?.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.theEffectOfEntityDisambiguationLLMDriven')}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
>
|
||||
{testResult.disambiguation.effects.map((item, index) => (
|
||||
<div key={index} className={clsx("rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px]", {
|
||||
'rb:mt-[16px]': index > 0,
|
||||
})}>
|
||||
<div className="rb:font-medium rb:mb-[8px]">Disagreement Case {index +1}:</div>
|
||||
-{item.left.name}({item.left.type}) vs {item.right.name}({item.right.type}) → <span className="rb:text-[#369F21]">{item.result}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<RbAlert color="blue" icon={<CheckCircleFilled />} className="rb:mt-[12px]">
|
||||
{t('memoryExtractionEngine.entityDeduplicationImpactDesc', { count: testResult.dedup.impact.length })}
|
||||
</RbAlert>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
{testResult?.core_entities && testResult?.core_entities.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.coreEntitiesAfterDedup')}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#369F21]!"
|
||||
>
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-[24px]">
|
||||
{testResult.core_entities.map(item => (
|
||||
<div key={item.type} className="rb:text-[12px]">
|
||||
<div className="rb:text-[#369F21] rb:font-medium">{item.type}({item.count})</div>
|
||||
|
||||
<div>
|
||||
{item.entities.map((entity, index) => (
|
||||
<div key={index} className="rb:text-[#5B6167] rb:font-regular rb:leading-[16px]">
|
||||
-{entity}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RbCard>
|
||||
}
|
||||
|
||||
{testResult?.triplet_samples && testResult?.triplet_samples.length > 0 &&
|
||||
<RbCard
|
||||
title={t('memoryExtractionEngine.extractRelationalTriples')}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#9C6FFF]!"
|
||||
>
|
||||
<Space size={8} direction="vertical" className="rb:w-full">
|
||||
{testResult.triplet_samples.map((item, index) => (
|
||||
<div key={index} className="rb:text-[12px]">
|
||||
-({item.subject}, <span className="rb:text-[#9C6FFF] rb:font-medium">{item.predicate}</span>, {item.object})
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
<RbAlert color="purple" icon={<CheckCircleFilled />} className="rb:mt-[12px]">
|
||||
{t('memoryExtractionEngine.extractRelationalTriplesDesc', { count: testResult.triplet_samples.length })}
|
||||
</RbAlert>
|
||||
</RbCard>
|
||||
}
|
||||
</Space>
|
||||
</>
|
||||
: loading
|
||||
? <Skeleton />
|
||||
: <Empty className="rb:h-full" />
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-2 rb:gap-[16px] rb:mt-[20px]">
|
||||
<Button block loading={loading} onClick={handleSave}>{t('common.save')}</Button>
|
||||
<Button block type="primary" loading={runLoading} onClick={handleRun}>{t('memoryExtractionEngine.debug')}</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<Result
|
||||
loading={loading}
|
||||
handleSave={handleSave}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
|
||||
@@ -123,7 +123,7 @@ const ConfigModal = forwardRef<ConfigModalRef, ConfigModalProps>(({
|
||||
<CustomSelect
|
||||
url={modelTypeUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: item, value: item }))}
|
||||
format={(items) => items.map((item) => ({ label: t(`model.${item}`), value: item }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
@@ -138,10 +138,9 @@ const ConfigModal = forwardRef<ConfigModalRef, ConfigModalProps>(({
|
||||
<CustomSelect
|
||||
url={modelProviderUrl}
|
||||
hasAll={false}
|
||||
format={(items) => items.map((item) => ({ label: item, value: item }))}
|
||||
format={(items) => items.map((item) => ({ label: t(`model.${item}`), value: item }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
{/* TODO:改成模型名称 */}
|
||||
<Form.Item
|
||||
name="model_name"
|
||||
label={t('model.modelName')}
|
||||
|
||||
@@ -8,6 +8,7 @@ const NoPermission = () => {
|
||||
return (
|
||||
<Empty
|
||||
url={noPermission}
|
||||
size={[240, 240]}
|
||||
title={t('empty.noPermission')}
|
||||
subTitle={t('empty.noPermissionDesc')}
|
||||
className="rb:h-[calc(100vh-84px)]"
|
||||
|
||||
@@ -8,6 +8,7 @@ const NotFound = () => {
|
||||
return (
|
||||
<Empty
|
||||
url={notFoundImg}
|
||||
size={[328, 146]}
|
||||
title={t('empty.notFound')}
|
||||
subTitle={t('empty.notFoundDesc')}
|
||||
className="rb:h-[calc(100vh-84px)]"
|
||||
|
||||
@@ -7,16 +7,17 @@ export interface Data {
|
||||
other_address: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
},
|
||||
memory_num: {
|
||||
total: number;
|
||||
counts: {
|
||||
dialogue: number;
|
||||
chunk: number;
|
||||
statement: number;
|
||||
entity: number;
|
||||
}
|
||||
},
|
||||
memory_num: {
|
||||
total: number;
|
||||
counts: {
|
||||
dialogue: number;
|
||||
chunk: number;
|
||||
statement: number;
|
||||
entity: number;
|
||||
}
|
||||
},
|
||||
name?: string;
|
||||
}
|
||||
export interface ConfigModalData {
|
||||
llm: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { type FC, useEffect, useState, useRef } from 'react'
|
||||
import React, { type FC, useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Col } from 'antd'
|
||||
@@ -29,13 +29,9 @@ const RelationshipNetwork:FC = () => {
|
||||
const [categories, setCategories] = useState<{ name: string }[]>([])
|
||||
const [selectedNode, setSelectedNode] = useState<Node | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getEdgeData()
|
||||
}, [id])
|
||||
|
||||
// 关系网络
|
||||
const getEdgeData = () => {
|
||||
const getEdgeData = useCallback(() => {
|
||||
if (!id) return
|
||||
setSelectedNode(null)
|
||||
getMemorySearchEdges(id).then((res) => {
|
||||
@@ -45,20 +41,20 @@ const RelationshipNetwork:FC = () => {
|
||||
const categories: { name: string }[] = []
|
||||
|
||||
list.forEach(item => {
|
||||
if (item.edge) {
|
||||
if (item.edge && item.edge.target_id && item.edge.source_id) {
|
||||
links.push({
|
||||
...item.edge,
|
||||
target: item.edge?.target_id,
|
||||
source: item.edge?.source_id,
|
||||
target: item.edge.target_id,
|
||||
source: item.edge.source_id,
|
||||
})
|
||||
}
|
||||
if (item.sourceNode) {
|
||||
nodes.push(item.sourceNode)
|
||||
categories.push({name: item.sourceNode.entity_type})
|
||||
categories.push({name: item.sourceNode.entity_type || 'Unknown'})
|
||||
}
|
||||
if (item.targetNode) {
|
||||
nodes.push(item.targetNode)
|
||||
categories.push({name: item.targetNode.entity_type})
|
||||
categories.push({name: item.targetNode.entity_type || 'Unknown'})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -76,14 +72,58 @@ const RelationshipNetwork:FC = () => {
|
||||
setLinks(uniqueLinks)
|
||||
setCategories(uniqueCategories)
|
||||
|
||||
// Calculate node frequency based on appearance in links
|
||||
const nodeFrequency = new Map<string, number>()
|
||||
|
||||
// Count each node's appearance in links (both as source and target)
|
||||
uniqueLinks.forEach(link => {
|
||||
// Increment source node frequency (only if source exists and is a string)
|
||||
if (typeof link.source === 'string') {
|
||||
nodeFrequency.set(link.source, (nodeFrequency.get(link.source) || 0) + 1)
|
||||
}
|
||||
// Increment target node frequency (only if target exists and is a string)
|
||||
if (typeof link.target === 'string') {
|
||||
nodeFrequency.set(link.target, (nodeFrequency.get(link.target) || 0) + 1)
|
||||
}
|
||||
})
|
||||
|
||||
// Set minimum frequency to 1 for nodes not in any links
|
||||
uniqueNodes.forEach(node => {
|
||||
if (node.id && typeof node.id === 'string') {
|
||||
if (!nodeFrequency.has(node.id)) {
|
||||
nodeFrequency.set(node.id, 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
uniqueNodes.map(item => {
|
||||
const index = uniqueCategories.findIndex((n) => n.name === item.entity_type)
|
||||
const index = uniqueCategories.findIndex((n) => n.name === (item.entity_type || 'Unknown'))
|
||||
item.category = index
|
||||
item.symbolSize = index < 10 ? 5 : index <100 ? 8 : 10
|
||||
|
||||
// Get frequency for the node, ensuring id is a string
|
||||
const frequency = (item.id && typeof item.id === 'string') ? (nodeFrequency.get(item.id) || 1) : 1
|
||||
|
||||
// Set symbolSize based on frequency
|
||||
// Adjust these thresholds based on expected frequency ranges
|
||||
if (frequency <= 1) {
|
||||
item.symbolSize = 5
|
||||
} else if (frequency <= 10) {
|
||||
item.symbolSize = 10
|
||||
} else if (frequency <= 15) {
|
||||
item.symbolSize = 15
|
||||
} else if (frequency <= 20) {
|
||||
item.symbolSize = 25
|
||||
} else {
|
||||
item.symbolSize = 35
|
||||
}
|
||||
})
|
||||
setNodes(uniqueNodes)
|
||||
})
|
||||
}
|
||||
}, [id])
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getEdgeData()
|
||||
}, [id])
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
@@ -95,7 +135,7 @@ const RelationshipNetwork:FC = () => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const resizeObserver = new ResizeObserver(handleResize)
|
||||
const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement
|
||||
if (chartElement) {
|
||||
@@ -106,6 +146,8 @@ const RelationshipNetwork:FC = () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [nodes])
|
||||
|
||||
console.log('nodes', nodes)
|
||||
return (
|
||||
<>
|
||||
{/* 关系网络 */}
|
||||
@@ -175,12 +217,10 @@ const RelationshipNetwork:FC = () => {
|
||||
if (params.dataType === 'node') {
|
||||
// 处理节点点击事件
|
||||
console.log('Node clicked:', params.data);
|
||||
setSelectedNode(params.data)
|
||||
if (selectedNode?.id === params.data.id) {
|
||||
setSelectedNode(null)
|
||||
} else {
|
||||
setSelectedNode(params.data)
|
||||
}
|
||||
// 使用函数式更新避免状态依赖问题
|
||||
setSelectedNode(prevSelected =>
|
||||
prevSelected?.id === params.data.id ? null : params.data
|
||||
)
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user