feat(web): add space config page; user memory page update

This commit is contained in:
zhaoying
2026-01-13 16:16:46 +08:00
parent 1ebab759b1
commit e187c01dc9
17 changed files with 321 additions and 316 deletions

View File

@@ -176,10 +176,10 @@ export const getPerceptualTimeline = (end_user: string) => {
}
// 情景记忆-总览
export const getEpisodicOverview = (data: { end_user_id: string; time_range: string; episodic_type: string; } ) => {
return request.post(`/memory-storage/classifications/episodic-memory`, data)
return request.post(`/memory/episodic-memory/overview`, data)
}
export const getEpisodicDetail = (data: { end_user_id: string; summary_id: string; } ) => {
return request.post(`/memory-storage/classifications/episodic-memory-details`, data)
return request.post(`/memory/episodic-memory/details`, data)
}
// 关系演化
export const getRelationshipEvolution = (data: { id: string; label: string; } ) => {
@@ -190,10 +190,10 @@ export const getTimelineMemories = (data: { id: string; label: string; }) => {
return request.get(`/memory-storage/memory_space/timeline_memories`, data)
}
export const getExplicitMemory = (end_user_id: string) => {
return request.post(`/memory-storage/classifications/explicit-memory`, { end_user_id })
return request.post(`/memory/explicit-memory/overview`, { end_user_id })
}
export const getExplicitMemoryDetails = (data: { end_user_id: string, memory_id: string; }) => {
return request.post(`/memory-storage/classifications/explicit-memory-details`, data)
return request.post(`/memory/explicit-memory/details`, data)
}
export const getConversations = (end_user: string) => {
return request.get(`/memory/work/${end_user}/conversations`)

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>模型 (1)</title>
<g id="v0.2.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="红熊空间-记忆管理" transform="translate(-24, -409)" stroke="#5B6167">
<g id="记忆对话备份-2" transform="translate(12, 401)">
<g id="模型-(1)" transform="translate(12, 8)">
<g id="编组-21" transform="translate(1.5, 1.5)">
<path d="M7,0.288675135 L11.6291651,2.96132487 C11.9385662,3.13995766 12.1291651,3.47008468 12.1291651,3.82735027 L12.1291651,9.17264973 C12.1291651,9.52991532 11.9385662,9.86004234 11.6291651,10.0386751 L7,12.7113249 C6.69059892,12.8899577 6.30940108,12.8899577 6,12.7113249 L1.37083488,10.0386751 C1.0614338,9.86004234 0.870834875,9.52991532 0.870834875,9.17264973 L0.870834875,3.82735027 C0.870834875,3.47008468 1.0614338,3.13995766 1.37083488,2.96132487 L6,0.288675135 C6.30940108,0.11004234 6.69059892,0.11004234 7,0.288675135 Z" id="多边形"></path>
<polyline id="路径-15" points="0.931223827 3.37218958 6.5 6.5 6.5 12.8581283"></polyline>
<line x1="6.5" y1="6.49748419" x2="12.0714286" y2="3.37218958" id="路径-16"></line>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>模型 (1)</title>
<g id="v0.2.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="红熊空间-记忆管理" transform="translate(-24, -409)" stroke="#212332">
<g id="记忆对话备份-2" transform="translate(12, 401)">
<g id="模型-(1)" transform="translate(12, 8)">
<g id="编组-21" transform="translate(1.5, 1.5)">
<path d="M7,0.288675135 L11.6291651,2.96132487 C11.9385662,3.13995766 12.1291651,3.47008468 12.1291651,3.82735027 L12.1291651,9.17264973 C12.1291651,9.52991532 11.9385662,9.86004234 11.6291651,10.0386751 L7,12.7113249 C6.69059892,12.8899577 6.30940108,12.8899577 6,12.7113249 L1.37083488,10.0386751 C1.0614338,9.86004234 0.870834875,9.52991532 0.870834875,9.17264973 L0.870834875,3.82735027 C0.870834875,3.47008468 1.0614338,3.13995766 1.37083488,2.96132487 L6,0.288675135 C6.30940108,0.11004234 6.69059892,0.11004234 7,0.288675135 Z" id="多边形"></path>
<polyline id="路径-15" points="0.931223827 3.37218958 6.5 6.5 6.5 12.8581283"></polyline>
<line x1="6.5" y1="6.49748419" x2="12.0714286" y2="3.37218958" id="路径-16"></line>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 13备份</title>
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="红熊空间-记忆管理" transform="translate(-947, -144)">
<g id="1备份-2" transform="translate(651, 128)">
<g id="编组-13备份" transform="translate(296, 16)">
<rect id="矩形" stroke="#DFE4ED" x="0.5" y="0.5" width="27" height="27" rx="6"></rect>
<g id="进入@2x" transform="translate(5.8333, 5.8333)">
<g id="编组-11" transform="translate(2.0417, 2.5521)">
<path d="M5.42385066,3.34516089 L8.15899029,5.47250014 C8.23746067,5.5335329 8.25159666,5.64662254 8.1905639,5.72509292 C8.1813906,5.73688711 8.17078448,5.74749323 8.15899029,5.75666652 L5.42385066,7.88400578 C5.34538028,7.94503854 5.23229064,7.93090256 5.17125788,7.85243218 C5.14668314,7.82083621 5.13334107,7.78195037 5.13334107,7.74192259 L5.13334107,6.2384308 L5.13334107,6.2384308 L0,6.2384308 L0,4.99073587 L5.13334107,4.99073587 L5.13334107,3.48724407 C5.13334107,3.38783282 5.21392981,3.30724407 5.31334107,3.30724407 C5.35336884,3.30724407 5.39225469,3.32058615 5.42385066,3.34516089 Z" id="路径" fill="#5B6167" fill-rule="nonzero"></path>
<path d="M1.60417096,2.83745334 L1.60417096,0.9 C1.60417096,0.402943725 2.00711469,0 2.50417096,-1.11022302e-16 L10.3291667,-1.11022302e-16 C10.8262229,-2.22044605e-16 11.2291667,0.402943725 11.2291667,0.9 L11.2291667,10.3291667 C11.2291667,10.8262229 10.8262229,11.2291667 10.3291667,11.2291667 L2.50417096,11.2291667 C2.00711469,11.2291667 1.60417096,10.8262229 1.60417096,10.3291667 L1.60417096,8.46506778 L1.60417096,8.46506778" id="路径" stroke="#5B6167" stroke-width="1.1" stroke-linejoin="round"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,4 +1,4 @@
import { useEffect, useState, useCallback, useRef, type FC, type Key } from 'react';
import { useEffect, useState, type FC, type Key } from 'react';
import { Select } from 'antd'
import type { SelectProps, DefaultOptionType } from 'antd/es/select'
import { useTranslation } from 'react-i18next';
@@ -26,7 +26,7 @@ interface CustomSelectProps extends Omit<SelectProps, 'filterOption'> {
disabled?: boolean;
style?: React.CSSProperties;
className?: string;
filterOption?: (inputValue: string, option: DefaultOptionType) => boolean;
filterOption?: (inputValue: string, option?: DefaultOptionType) => boolean;
}
interface OptionType {
[key: string]: Key | string | number;
@@ -48,44 +48,27 @@ const CustomSelect: FC<CustomSelectProps> = ({
}) => {
const { t } = useTranslation();
const [options, setOptions] = useState<OptionType[]>([]);
// 创建防抖定时器引用
const debounceRef = useRef<number>();
// 防抖搜索函数
const handleSearch = useCallback((value?: string) => {
// 清除之前的定时器
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
// 设置新的定时器
debounceRef.current = window.setTimeout(() => {
request.get<ApiResponse<OptionType>>(url, {...params, [optionFilterProp]: value}).then((res) => {
const data = res;
setOptions(Array.isArray(data) ? data || [] : Array.isArray(data?.items) ? data.items || [] : []);
});
}, 300); // 300毫秒防抖延迟
}, [url, params, optionFilterProp]);
// 默认模糊搜索函数
const defaultFilterOption = (inputValue: string, option?: DefaultOptionType) => {
if (!option || !inputValue) return true;
const label = String(option.children || option.label || '');
return label.toLowerCase().includes(inputValue.toLowerCase());
};
// 组件挂载时获取初始数据
useEffect(() => {
handleSearch();
// 组件卸载时清除定时器
return () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
};
}, [url, handleSearch]);
request.get<ApiResponse<OptionType>>(url, params).then((res) => {
const data = res;
setOptions(Array.isArray(data) ? data || [] : Array.isArray(data?.items) ? data.items || [] : []);
});
}, []);
return (
<Select
placeholder={placeholder ? placeholder : t('common.select')}
onChange={onChange}
defaultValue={hasAll ? null : undefined}
showSearch={showSearch}
onSearch={handleSearch}
filterOption={filterOption || false} // 禁用本地过滤,使用服务器端过滤
filterOption={filterOption || defaultFilterOption}
{...props}
>
{hasAll && (<Select.Option>{allTitle || t('common.all')}</Select.Option>)}

View File

@@ -40,6 +40,8 @@ import apiKeyIcon from '@/assets/images/menu/apiKey.png';
import apiKeyActiveIcon from '@/assets/images/menu/apiKey_active.png';
import pricingIcon from '@/assets/images/menu/pricing.svg'
import pricingActiveIcon from '@/assets/images/menu/pricing_active.svg'
import spaceConfigIcon from '@/assets/images/menu/spaceConfig.svg'
import spaceConfigActiveIcon from '@/assets/images/menu/spaceConfig_active.svg'
// 图标路径映射表
const iconPathMap: Record<string, string> = {
@@ -68,7 +70,9 @@ const iconPathMap: Record<string, string> = {
'apiKey': apiKeyIcon,
'apiKeyActive': apiKeyActiveIcon,
'pricing': pricingIcon,
'pricingActive': pricingActiveIcon
'pricingActive': pricingActiveIcon,
'spaceConfig': spaceConfigIcon,
'spaceConfigActive': spaceConfigActiveIcon,
};
const { Sider } = Layout;

View File

@@ -95,6 +95,7 @@ export const en = {
pricing: 'Pricing Management',
orderPayment: 'Order Payment',
orderHistory: 'Order History',
spaceConfig: 'Space Configuration'
},
dashboard: {
total_models: 'Total number of available models',
@@ -1250,6 +1251,12 @@ export const en = {
negative: 'Negative Emotion',
neutral: 'Neutral Emotion',
interactionCountData: 'Interaction Count',
capacity: 'Capacity',
type: 'Type',
person: 'Personal',
memoryNum: 'memories',
memory_config_name: 'Memory Engine',
searchPlaceholder: 'Search memory store name',
},
space: {
createSpace: 'Create Space',
@@ -1265,7 +1272,8 @@ export const en = {
neo4jDesc: 'Based on knowledge graph, suitable for relational reasoning and path query',
llmModel: 'LLM Model',
embeddingModel: 'Embedding Model',
rerankModel: 'Rerank Model'
rerankModel: 'Rerank Model',
configAlert: 'Space model configuration ensures that the space can correctly call the corresponding models to process business data during runtime.',
},
memoryExtractionEngine: {
title: 'Memory Engine Module Configuration Center',
@@ -1594,10 +1602,10 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
JsonTool_desc: 'Data Format Conversion',
JsonTool_features: 'JSON formatting, compression, validation and conversion functions',
jsonFormat: 'JSON Formatting',
jsonGzip: 'JSON Compression',
jsonCheck: 'JSON Validation',
jsonConversion: 'Format Conversion',
jsonParse: 'JSON Parse',
jsonInsert: 'JSON Insert',
jsonReplace: 'JSON Validation',
jsonDelete: 'JSON Delete',
jsonEg: 'Example JSON',
enterJson: 'Enter JSON',
jsonPlaceholder: 'Enter JSON data, e.g.: {"name": "test", "value": 123}',

View File

@@ -95,6 +95,7 @@ export const zh = {
pricing: '收费管理',
orderPayment: '订单支付',
orderHistory: '订单记录',
spaceConfig: '空间配置'
},
knowledgeBase: {
home: '首页',
@@ -1328,6 +1329,12 @@ export const zh = {
negative: '负向情绪',
neutral: '中性情绪',
interactionCountData: '互动次数',
capacity: '容量',
type: '类型',
person: '个人',
memoryNum: '条记忆',
memory_config_name: '记忆引擎',
searchPlaceholder: '搜索记忆库名称',
},
space: {
createSpace: '创建空间',
@@ -1343,7 +1350,8 @@ export const zh = {
neo4jDesc: '基于知识图谱,适合关系推理和路径查询',
llmModel: 'LLM 模型',
embeddingModel: 'Embedding 模型',
rerankModel: 'Rerank 模型'
rerankModel: 'Rerank 模型',
configAlert: '空间模型配置为空间的模型模型,保障空间运行时能正确的调用到相应的模型来处理业务数据。',
},
memoryExtractionEngine: {
title: '记忆引擎模块配置中心',
@@ -1690,10 +1698,10 @@ export const zh = {
JsonTool_desc: '数据格式转换',
JsonTool_features: 'JSON格式化、压缩、验证和转换功能',
jsonFormat: 'JSON格式化',
jsonGzip: 'JSON压缩',
jsonCheck: 'JSON验证',
jsonConversion: '格式转换',
jsonParse: 'JSON解析',
jsonInsert: 'JSON插入',
jsonReplace: 'JSON验证',
jsonDelete: 'JSON删除',
jsonEg: '示例JSON',
enterJson: '输入JSON',
jsonPlaceholder: '输入JSON数据例如{"name": "测试", "value": 123}',

View File

@@ -66,6 +66,7 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
OrderHistory: lazy(() => import('@/views/OrderHistory')),
Pricing: lazy(() => import('@/views/Pricing')),
ToolManagement: lazy(() => import('@/views/ToolManagement')),
SpaceConfig: lazy(() => import('@/views/SpaceConfig')),
Login: lazy(() => import('@/views/Login')),
InviteRegister: lazy(() => import('@/views/InviteRegister')),
NoPermission: lazy(() => import('@/views/NoPermission')),

View File

@@ -33,6 +33,7 @@
{ "path": "/api-key", "element": "ApiKeyManagement" },
{ "path": "/emotion-engine/:id", "element": "EmotionEngine" },
{ "path": "/reflection-engine/:id", "element": "SelfReflectionEngine" },
{ "path": "/space-config", "element": "SpaceConfig" },
{ "path": "/no-permission", "element": "NoPermission" },
{ "path": "/*", "element": "NotFound" }
]

View File

@@ -376,6 +376,21 @@
"icon": null,
"iconActive": null,
"subs": null
},
{
"id": 12,
"parent": 0,
"code": "spaceConfig",
"label": "空间配置",
"i18nKey": "menu.spaceConfig",
"path": "/space-config",
"enable": true,
"display": true,
"level": 1,
"sort": 0,
"icon": null,
"iconActive": null,
"subs": null
}
]
}

View File

@@ -0,0 +1,118 @@
import { type FC, useEffect, useState } from 'react';
import { Form, App, Button, Skeleton } from 'antd';
import { useTranslation } from 'react-i18next';
import type { SpaceConfigData } from './types'
import { getWorkspaceModels, updateWorkspaceModels } from '@/api/workspaces'
import { getModelListUrl } from '@/api/models'
import CustomSelect from '@/components/CustomSelect'
import RbAlert from '@/components/RbAlert';
const SpaceConfig: FC = () => {
const { t } = useTranslation();
const { message } = App.useApp();
const [pageLoading, setPageLoding] = useState(false)
const [form] = Form.useForm<SpaceConfigData>();
const [loading, setLoading] = useState(false)
const values = Form.useWatch([], form);
useEffect(() => {
setPageLoding(true)
getWorkspaceModels().then((res) => {
const { llm, embedding, rerank } = res as SpaceConfigData
form.setFieldsValue({
llm,
embedding,
rerank
})
})
.finally(() => {
setPageLoding(false)
})
}, [])
// 封装保存方法,添加提交逻辑
const handleSave = () => {
form
.validateFields()
.then(() => {
setLoading(true)
updateWorkspaceModels(values)
.then(() => {
setLoading(false)
message.success(t('common.updateSuccess'))
})
.catch(() => {
setLoading(false)
});
})
.catch((err) => {
console.log('err', err)
});
}
return (
<div className="rb:h-full rb:max-w-140 rb:mx-auto">
{pageLoading
? <Skeleton active />
: <Form
form={form}
layout="vertical"
>
<Form.Item
label={t('space.llmModel')}
name="llm"
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'llm', pagesize: 100 }}
valueKey="id"
labelKey="name"
hasAll={false}
style={{width: '100%'}}
/>
</Form.Item>
<Form.Item
label={t('space.embeddingModel')}
name="embedding"
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'embedding', pagesize: 100 }}
valueKey="id"
labelKey="name"
hasAll={false}
style={{width: '100%'}}
/>
</Form.Item>
<Form.Item
label={t('space.rerankModel')}
name="rerank"
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'rerank', pagesize: 100 }}
valueKey="id"
labelKey="name"
hasAll={false}
style={{width: '100%'}}
/>
</Form.Item>
<RbAlert>{t('space.configAlert')}</RbAlert>
<Form.Item className="rb:text-right">
<Button type="primary" className="rb:mt-6" onClick={handleSave} loading={loading}>
{t('common.save')}
</Button>
</Form.Item>
</Form>
}
</div>
);
};
export default SpaceConfig;

View File

@@ -0,0 +1,8 @@
export interface SpaceConfigData {
llm: string;
embedding: string;
rerank: string;
}
export interface SpaceConfigRef {
handleOpen: () => void;
}

View File

@@ -10,10 +10,10 @@ export const InnerConfigData: Record<string, InnerConfigItem> = {
},
JsonTool: {
features: [
'jsonFormat',
'jsonGzip',
'jsonCheck',
'jsonConversion'
'jsonParse',
'jsonInsert',
'jsonReplace',
'jsonDelete'
],
eg: '{"name":"工具","tool_class":"内置"}'
},

View File

@@ -1,127 +0,0 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, App } from 'antd';
import { useTranslation } from 'react-i18next';
import type { ConfigModalData, ConfigModalRef } from '../types'
import { getWorkspaceModels, updateWorkspaceModels } from '@/api/workspaces'
import { getModelListUrl } from '@/api/models'
import CustomSelect from '@/components/CustomSelect'
import RbModal from '@/components/RbModal'
const ConfigModal = forwardRef<ConfigModalRef>((_props, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<ConfigModalData>();
const [loading, setLoading] = useState(false)
const values = Form.useWatch([], form);
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
setVisible(false);
form.resetFields();
setLoading(false)
};
const handleOpen = () => {
getWorkspaceModels().then((res) => {
const { llm, embedding, rerank } = res as ConfigModalData
form.setFieldsValue({
llm,
embedding,
rerank
})
})
setVisible(true);
};
// 封装保存方法,添加提交逻辑
const handleSave = () => {
form
.validateFields()
.then(() => {
setLoading(true)
updateWorkspaceModels(values)
.then(() => {
setLoading(false)
handleClose()
message.success(t('common.updateSuccess'))
})
.catch(() => {
setLoading(false)
});
handleClose()
})
.catch((err) => {
console.log('err', err)
});
}
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={t(`userMemory.editConfig`)}
open={visible}
onCancel={handleClose}
okText={t('common.save')}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
label={t('space.llmModel')}
name="llm"
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'llm', pagesize: 100 }}
valueKey="id"
labelKey="name"
hasAll={false}
style={{width: '100%'}}
/>
</Form.Item>
<Form.Item
label={t('space.embeddingModel')}
name="embedding"
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'embedding', pagesize: 100 }}
valueKey="id"
labelKey="name"
hasAll={false}
style={{width: '100%'}}
/>
</Form.Item>
<Form.Item
label={t('space.rerankModel')}
name="rerank"
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'rerank', pagesize: 100 }}
valueKey="id"
labelKey="name"
hasAll={false}
style={{width: '100%'}}
/>
</Form.Item>
</Form>
</RbModal>
);
});
export default ConfigModal;

View File

@@ -1,56 +1,28 @@
import { useEffect, useState, useRef } from 'react';
import { useEffect, useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'
import { Row, Col, Radio, Button, List, Skeleton, Space } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import type { RadioChangeEvent } from 'antd';
import { AppstoreOutlined, MenuOutlined } from '@ant-design/icons';
import { Row, Col, List, Skeleton } from 'antd';
import Empty from '@/components/Empty'
import type { Data, ConfigModalRef } from './types'
import totalNum from '@/assets/images/memory/totalNum.svg'
import onlineNum from '@/assets/images/memory/onlineNum.svg'
import Table from '@/components/Table'
import { getTotalEndUsers, userMemoryListUrl, getUserMemoryList } from '@/api/memory';
import ConfigModal from './components/ConfigModal';
import type { Data } from './types'
import { getUserMemoryList } from '@/api/memory';
import { useUser } from '@/store/user'
import RbCard from '@/components/RbCard/Card'
import SearchInput from '@/components/SearchInput';
const bgList = [
'linear-gradient( 180deg, #F1F6FE 0%, #FBFDFF 100%)',
'linear-gradient( 180deg, #F1F9FE 0%, #FBFDFF 100%)',
'linear-gradient( 180deg, #FEFBF7 0%, #FBFDFF 100%)',
'linear-gradient( 180deg, #F1F9FE 0%, #FBFDFF 100%)',
]
const countList = [
'total_num', 'online_num',
]
const IconList: Record<string, string> = {
total_num: totalNum,
online_num: onlineNum,
}
export default function UserMemory() {
const { t } = useTranslation();
const navigate = useNavigate()
const { storageType } = useUser()
const configModalRef = useRef<ConfigModalRef>(null)
const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<Data[]>([]);
const [countData, setCountData] = useState<Record<string, number>>({});
const [layout, setLayout] = useState<'card' | 'list'>('card');
const [search, setSearch] = useState<string | undefined>(undefined);
// 获取数据
useEffect(() => {
getCountData()
getData()
}, []);
// 用户记忆统计
const getCountData = () => {
getTotalEndUsers().then((res) => {
setCountData(res as Record<string, number> || {})
})
}
const getData = () => {
setLoading(true)
getUserMemoryList().then((res) => {
@@ -60,7 +32,6 @@ export default function UserMemory() {
setLoading(false)
})
}
console.log('storageType', storageType)
const handleViewDetail = (id: string | number) => {
switch (storageType) {
case 'neo4j':
@@ -70,112 +41,77 @@ export default function UserMemory() {
navigate(`/user-memory/${id}`)
}
}
const handleChangeLayout = (e: RadioChangeEvent) => {
const type = e.target.value
setLayout(type)
const handleViewMemoryConfig = () => {
navigate(`/memory`)
}
// 表格列配置
const columns: ColumnsType = [
{
title: t('userMemory.user'),
dataIndex: 'end_user',
key: 'end_user',
render: (value) => value?.other_name && value?.other_name !== '' ? value?.other_name : value?.id || '-'
},
{
title: t('userMemory.knowledgeEntryCount'),
dataIndex: 'memory_num',
key: 'memory_num',
render: (value) => value?.total || 0
},
{
title: t('common.operation'),
key: 'action',
render: (_, record) => (
<Button
type="link"
onClick={() => handleViewDetail(record.end_user?.id)}
>
{t('common.viewDetail')}
</Button>
),
},
];
const filterData = useMemo(() => {
if (search && search.trim() !== '') {
return data.filter((item) => {
const { end_user } = item as Data;
const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id
return name?.includes(search)
})
}
return data
}, [search, data])
return (
<div>
<Row gutter={16} className="rb:mb-4">
{countList.map(key => (
<Col key={key} span={6}>
<div className="rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-xl rb:p-[18px_20px_20px_20px]">
<div className="rb:text-[28px] rb:font-extrabold rb:leading-8.75 rb:flex rb:items-center rb:justify-between rb:mb-3">
{countData[key] || 0}{key === 'avgInteractionTime' ? 's' : ''}
<img className="rb:w-6 rb:h-6" src={IconList[key]} />
</div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`userMemory.${key}`)}</div>
</div>
</Col>
))}
<Col span={12} className="rb:text-right">
<Space>
<Button type="primary" onClick={() => configModalRef?.current?.handleOpen()}>{t('userMemory.chooseModel')}</Button>
<Radio.Group value={layout} onChange={handleChangeLayout}>
<Radio.Button value="card" disabled={layout === 'card'}><AppstoreOutlined /></Radio.Button>
<Radio.Button value="list" disabled={layout === 'list'}><MenuOutlined /></Radio.Button>
</Radio.Group>
</Space>
<Col span={8}>
<SearchInput
placeholder={t('userMemory.searchPlaceholder')}
onSearch={(value) => setSearch(value)}
style={{ width: '100%' }}
/>
</Col>
</Row>
{layout === 'card' &&
<>
{loading ?
<Skeleton active />
: data.length > 0 ? (
<List
grid={{ gutter: 16, column: 4 }}
dataSource={data}
renderItem={(item, index) => {
const { end_user, memory_num } = item as Data;
const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id
return (
<List.Item key={index}>
<div
className="rb:p-5 rb:rounded-xl rb:border rb:border-[#DFE4ED] rb:cursor-pointer"
style={{
background: bgList[index % bgList.length],
}}
{loading ?
<Skeleton active />
: filterData.length > 0 ? (
<List
grid={{ gutter: 16, column: 3 }}
dataSource={filterData}
renderItem={(item, index) => {
const { end_user, memory_num, memory_config } = item as Data;
const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id
return (
<List.Item key={index}>
<RbCard
avatar={<div className="rb:w-12 rb:h-12 rb:text-center rb:font-semibold rb:text-[28px] rb:leading-12 rb:rounded-lg rb:text-[#FBFDFF] rb:bg-[#155EEF] rb:mr-2">{name[0]}</div>}
title={name || '-'}
extra={<div
className="rb:w-7 rb:h-7 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/goto.svg')]"
onClick={() => handleViewDetail(end_user.id)}
>
<div className="rb:flex rb:items-center">
<div className="rb:w-12 rb:h-12 rb:text-center rb:font-semibold rb:text-[28px] rb:leading-12 rb:rounded-lg rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name[0]}</div>
<div className="rb:max-w-[calc(100%-60px)] rb:text-base rb:font-medium rb:leading-6 rb:ml-3 rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">
{name || '-'}<br/>
</div>
</div>
<div className="rb:grid rb:grid-cols-1 rb:gap-3 rb:mt-7 rb:mb-7">
<div className="rb:text-center">
<div className="rb:text-[24px] rb:leading-7.5 rb:font-extrabold">{memory_num.total || 0}</div>
<div className="rb:wrap-break-word">{t(`userMemory.knowledgeEntryCount`)}</div>
</div>
</div>
></div>}
>
<div className="rb:flex rb:justify-between rb:items-center">
<div>{t('userMemory.capacity')}</div>
<div>{memory_num?.total || 0} {t('userMemory.memoryNum')}</div>
</div>
<div className="rb:flex rb:justify-between rb:items-center rb:mt-2.5">
<div>{t('userMemory.type')}</div>
<div>{t(`userMemory.${item.type || 'person'}`)}</div>
</div>
</List.Item>
)
}}
/>
) : <Empty />}
</>
}
{layout === 'list' &&
<Table
apiUrl={userMemoryListUrl}
columns={columns}
rowKey="end_user.id"
pagination={false}
/>
<div className="rb:mt-3 rb:bg-[#F6F8FC] rb:rounded-lg rb:border rb:border-[#DFE4ED] rb:py-2 rb:px-3" onClick={handleViewMemoryConfig}>
<div className="rb:text-[#5B6167] rb:leading-5 rb:flex rb:justify-between rb:items-center">
{t('userMemory.memory_config_name')}
<div
className="rb:w-7 rb:h-7 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/arrow_right.svg')]"
></div>
</div>
<div className="rb:font-medium rb:leading-5 rb:mt-1">{memory_config?.memory_config_name || '-'}</div>
</div>
</RbCard>
</List.Item>
)
}}
/>
) : <Empty />
}
<ConfigModal ref={configModalRef} />
</div>
);
}

View File

@@ -17,13 +17,10 @@ export interface Data {
entity: number;
}
},
memory_config: {
memory_config_id: string;
memory_config_name: string;
},
type: string;
name?: string;
}
export interface ConfigModalData {
llm: string;
embedding: string;
rerank: string;
}
export interface ConfigModalRef {
handleOpen: () => void;
}