Merge branch 'feature/20251219_zy' into develop_web
This commit is contained in:
@@ -15,6 +15,7 @@ import type {
|
||||
ConfigForm as SelfReflectionEngineConfig
|
||||
} from '@/views/SelfReflectionEngine/types'
|
||||
import type { TestParams } from '@/views/MemoryConversation'
|
||||
import type { EndUser } from '@/views/UserMemoryDetail/types'
|
||||
import { handleSSE, type SSEMessage } from '@/utils/stream'
|
||||
|
||||
// 记忆对话
|
||||
@@ -66,6 +67,7 @@ export const getTotalEndUsers = () => {
|
||||
export const getUserProfile = (end_user_id: string) => {
|
||||
return request.get(`/memory/analytics/user_profile`, { end_user_id })
|
||||
}
|
||||
|
||||
// 用户记忆-记忆洞察
|
||||
export const getMemoryInsightReport = (end_user_id: string) => {
|
||||
return request.get(`/memory-storage/analytics/memory_insight/report`, { end_user_id })
|
||||
@@ -74,9 +76,20 @@ export const getMemoryInsightReport = (end_user_id: string) => {
|
||||
export const getUserSummary = (end_user_id: string) => {
|
||||
return request.get(`/memory-storage/analytics/user_summary`, { end_user_id })
|
||||
}
|
||||
// 记忆分类
|
||||
export const getNodeStatistics = (end_user_id: string) => {
|
||||
return request.get(`/memory-storage/analytics/node_statistics`, { end_user_id })
|
||||
}
|
||||
// 基本信息
|
||||
export const getEndUserProfile = (end_user_id: string) => {
|
||||
return request.get(`/memory-storage/read_end_user/profile`, { end_user_id })
|
||||
}
|
||||
export const updatedEndUserProfile = (values: EndUser) => {
|
||||
return request.post(`/memory-storage/updated_end_user/profile`, values)
|
||||
}
|
||||
// 用户记忆-关系网络
|
||||
export const getMemorySearchEdges = (end_user_id: string) => {
|
||||
return request.get(`/memory-storage/search/entity_graph`, { end_user_id })
|
||||
return request.get(`/memory-storage/analytics/graph_data`, { end_user_id })
|
||||
}
|
||||
// 用户记忆-用户兴趣分布
|
||||
export const getHotMemoryTagsByUser = (end_user_id: string) => {
|
||||
|
||||
16
web/src/assets/images/home/arrow_top_right_hover.svg
Normal file
16
web/src/assets/images/home/arrow_top_right_hover.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?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>编组 16</title>
|
||||
<g id="V1.0版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="红熊空间-记忆看板" transform="translate(-1372, -1136)" stroke="#212332">
|
||||
<g id="快速操作" transform="translate(848, 1048)">
|
||||
<g id="1" transform="translate(296, 68)">
|
||||
<g id="编组-16" transform="translate(228, 20)">
|
||||
<line x1="13.7142857" y1="2.28571429" x2="2.28571429" y2="13.7142857" id="路径-15"></line>
|
||||
<polyline id="路径" points="5.55102041 2.28571429 13.7142857 2.28571429 13.7142857 10.4489796"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 929 B |
@@ -27,7 +27,7 @@ const Empty: FC<EmptyProps> = ({
|
||||
<div className={`rb:flex rb:items-center rb:justify-center rb:flex-col ${className}`}>
|
||||
<img src={url || emptyIcon} alt="404" style={{ width: `${width}px`, height: `${height}px` }} />
|
||||
{title && <div className="rb:mt-2 rb:leading-5">{title}</div>}
|
||||
{curSubTitle && <div className={`rb:mt-[${url ? 8 : 5}px] rb:leading-4 rb:text-[12px] rb:text-[#A8A9AA]`}>{subTitle}</div>}
|
||||
{curSubTitle && <div className={`rb:mt-[${url ? 8 : 5}px] rb:leading-4 rb:text-[12px] rb:text-[#A8A9AA]`}>{curSubTitle}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type FC, useRef } from 'react';
|
||||
import { type FC, useCallback, useRef } from 'react';
|
||||
import { Layout, Dropdown, Space, Breadcrumb } from 'antd';
|
||||
import type { MenuProps, BreadcrumbProps } from 'antd';
|
||||
import { UserOutlined, LogoutOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useUser } from '@/store/user';
|
||||
import { useMenu } from '@/store/menu';
|
||||
import styles from './index.module.css'
|
||||
@@ -13,7 +13,8 @@ const { Header } = Layout;
|
||||
|
||||
const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const settingModalRef = useRef<SettingModalRef>(null)
|
||||
const userInfoModalRef = useRef<UserInfoModalRef>(null)
|
||||
|
||||
@@ -54,7 +55,7 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
key: '1',
|
||||
label: (<>
|
||||
<div>{user.username}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-[8px]">{user.email}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2">{user.email}</div>
|
||||
</>),
|
||||
},
|
||||
{
|
||||
@@ -89,7 +90,7 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
onClick: handleLogout,
|
||||
},
|
||||
];
|
||||
const formatBreadcrumbNames = () => {
|
||||
const formatBreadcrumbNames = useCallback(() => {
|
||||
return breadcrumbs.map((menu, index) => {
|
||||
const item: any = {
|
||||
title: menu.i18nKey ? t(menu.i18nKey) : menu.label,
|
||||
@@ -108,13 +109,23 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
};
|
||||
item.href = '#';
|
||||
} else if (menu.path && menu.path !== '#') {
|
||||
// 只有当 path 不是 '#' 时才设置 path
|
||||
item.path = menu.path;
|
||||
// 对于三级面包屑的二级菜单,如果路径包含动态参数,替换为当前参数值
|
||||
if (breadcrumbs.length === 3 && index === 1 && menu.path.includes(':id') && params.id) {
|
||||
const dynamicPath = menu.path.replace(':id', params.id);
|
||||
item.onClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
navigate(dynamicPath);
|
||||
};
|
||||
item.href = '#';
|
||||
} else {
|
||||
// 只有当 path 不是 '#' 时才设置 path
|
||||
item.path = menu.path;
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
}, [breadcrumbs, params.id, t, navigate])
|
||||
return (
|
||||
<Header className={styles.header}>
|
||||
<Breadcrumb separator=">" items={formatBreadcrumbNames() as BreadcrumbProps['items']} />
|
||||
|
||||
@@ -11,85 +11,47 @@ export const useNavigationBreadcrumbs = (source: 'space' | 'manage' = 'manage')
|
||||
const menus = allMenus[source] || [];
|
||||
|
||||
// 查找匹配的菜单项并构建keyPath
|
||||
const findMenuKeyPath = (menuList: any[], parentKeys: string[] = []): string[] | null => {
|
||||
let bestMatch: { path: string; parentId?: string; score: number } | null = null;
|
||||
const findMenuKeyPath = (menuList: any[]): string[] | null => {
|
||||
const checkDynamicMatch = (pattern: string, path: string) => {
|
||||
const pathPattern = pattern.replace(/:[\w-]+/g, '[^/]+');
|
||||
const regex = new RegExp(`^${pathPattern}$`);
|
||||
return regex.test(path);
|
||||
};
|
||||
|
||||
for (const menu of menuList) {
|
||||
// 检查子菜单
|
||||
if (menu.subs && menu.subs.length > 0) {
|
||||
const menuPath = menu.path ? (menu.path[0] !== '/' ? '/' + menu.path : menu.path) : '';
|
||||
for (const sub of menu.subs) {
|
||||
// 检查三级菜单
|
||||
if (sub.subs && sub.subs.length > 0) {
|
||||
for (const subSub of sub.subs) {
|
||||
if (subSub.path) {
|
||||
const subSubPath = subSub.path[0] !== '/' ? '/' + subSub.path : subSub.path;
|
||||
if (subSubPath === currentPath || (subSubPath.includes(':') && checkDynamicMatch(subSubPath, currentPath))) {
|
||||
return [subSub.path, `${sub.id}`, `${menu.id}`];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查二级菜单
|
||||
if (sub.path) {
|
||||
const subPath = sub.path[0] !== '/' ? '/' + sub.path : sub.path;
|
||||
|
||||
// 精确匹配优先
|
||||
if (subPath === currentPath) {
|
||||
if (subPath === currentPath || (subPath.includes(':') && checkDynamicMatch(subPath, currentPath))) {
|
||||
return [sub.path, `${menu.id}`];
|
||||
}
|
||||
console.log('menuPath', menuPath)
|
||||
// 动态路由匹配
|
||||
if (subPath.includes(':')) {
|
||||
// 检查是否在父菜单下
|
||||
if (menuPath && currentPath.startsWith(menuPath + '/')) {
|
||||
const relativePath = currentPath.replace(menuPath, '');
|
||||
const pathSegments = subPath.split('/');
|
||||
const relativeSegments = relativePath.split('/');
|
||||
if (pathSegments.length === relativeSegments.length) {
|
||||
const pathPattern = subPath.replace(/:[\w-]+/g, '[^/]+').replace(/\[[\w-]+\]/g, '[^/]+');
|
||||
const regex = new RegExp(`^${pathPattern}$`);
|
||||
if (regex.test(relativePath)) {
|
||||
return [sub.path, `${menu.id}`];
|
||||
}
|
||||
}
|
||||
}
|
||||
// 直接匹配子菜单路径
|
||||
const pathSegments = subPath.split('/');
|
||||
const currentSegments = currentPath.split('/');
|
||||
if (pathSegments.length === currentSegments.length) {
|
||||
const pathPattern = subPath.replace(/:[\w-]+/g, '[^/]+').replace(/\[[\w-]+\]/g, '[^/]+');
|
||||
const regex = new RegExp(`^${pathPattern}$`);
|
||||
if (regex.test(currentPath)) {
|
||||
return [sub.path, `${menu.id}`];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查主菜单
|
||||
// 检查一级菜单
|
||||
if (menu.path) {
|
||||
const menuPath = menu.path[0] !== '/' ? '/' + menu.path : menu.path;
|
||||
// 精确匹配优先
|
||||
if (menuPath === currentPath) {
|
||||
return [menu.path, ...parentKeys].reverse();
|
||||
}
|
||||
// 动态路由匹配
|
||||
if (menuPath.includes(':')) {
|
||||
const pathSegments = menuPath.split('/');
|
||||
const currentSegments = currentPath.split('/');
|
||||
if (pathSegments.length === currentSegments.length) {
|
||||
const pathPattern = menuPath.replace(/:[\w-]+/g, '[^/]+').replace(/\[[\w-]+\]/g, '[^/]+');
|
||||
const regex = new RegExp(`^${pathPattern}$`);
|
||||
if (regex.test(currentPath)) {
|
||||
const score = menuPath.split('/').length;
|
||||
if (!bestMatch || score > bestMatch.score) {
|
||||
bestMatch = { path: menu.path, score };
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (currentPath.startsWith(menuPath + '/')) {
|
||||
const score = menuPath.split('/').length;
|
||||
if (!bestMatch || score > bestMatch.score) {
|
||||
bestMatch = { path: menu.path, score };
|
||||
}
|
||||
if (menuPath === currentPath || (menuPath.includes(':') && checkDynamicMatch(menuPath, currentPath))) {
|
||||
return [menu.path];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMatch) {
|
||||
return bestMatch.parentId ? [bestMatch.path, bestMatch.parentId] : [bestMatch.path];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export const en = {
|
||||
apiKeyManagement: 'API KEY Management',
|
||||
toolManagement: 'Tool Management',
|
||||
emotionEngine: 'Emotion Engine',
|
||||
emotionDetail: 'Emotion Memory',
|
||||
statementDetail: 'Emotion Memory',
|
||||
selfReflectionEngine: 'Self Reflection Engine',
|
||||
},
|
||||
dashboard: {
|
||||
@@ -1037,7 +1037,24 @@ export const en = {
|
||||
conversationMemory: 'Conversation Storage Content',
|
||||
sortByTimeDesc: 'Sort by time in descending order',
|
||||
editConfig: 'Edit Config',
|
||||
chooseModel: 'Choose Model'
|
||||
chooseModel: 'Choose Model',
|
||||
|
||||
nodeStatistics: 'Memory Classification',
|
||||
total: 'Total',
|
||||
Chunk: 'Long-term Memory',
|
||||
MemorySummary: 'Episodic Memory',
|
||||
Statement: 'Emotional Memory',
|
||||
ExtractedEntity: 'Short-term Memory',
|
||||
endUserProfile: 'Core Profile',
|
||||
editEndUserProfile: 'Edit',
|
||||
name: 'Name',
|
||||
position: 'Position',
|
||||
department: 'Department',
|
||||
contact: 'Contact',
|
||||
phone: 'Phone',
|
||||
hire_date: 'Hire Date',
|
||||
memoryContent: 'Memory Content',
|
||||
created_at: 'Created At',
|
||||
},
|
||||
space: {
|
||||
createSpace: 'Create Space',
|
||||
@@ -1626,7 +1643,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
expect_improvement_tag: 'Only low threshold will record',
|
||||
confidence: 'Confidence'
|
||||
},
|
||||
emotionDetail: {
|
||||
statementDetail: {
|
||||
wordCloud: 'Emotion Distribution Analysis',
|
||||
pieces: 'items',
|
||||
emotionTags: 'High-Frequency Emotion Keywords',
|
||||
|
||||
@@ -36,7 +36,7 @@ export const zh = {
|
||||
userMemoryDetail: '用户记忆详情',
|
||||
toolManagement: '工具管理',
|
||||
emotionEngine: '情感引擎',
|
||||
emotionDetail: '情绪记忆',
|
||||
statementDetail: '情绪记忆',
|
||||
selfReflectionEngine: '反思引擎',
|
||||
},
|
||||
knowledgeBase: {
|
||||
@@ -1114,6 +1114,23 @@ export const zh = {
|
||||
occupation: '职业',
|
||||
memories: '记忆',
|
||||
expanded: '展开',
|
||||
|
||||
nodeStatistics: '记忆分类',
|
||||
total: '总计',
|
||||
Chunk: '长期记忆',
|
||||
MemorySummary: '情景记忆',
|
||||
Statement: '情绪记忆',
|
||||
ExtractedEntity: '短期记忆',
|
||||
endUserProfile: '核心档案',
|
||||
editEndUserProfile: '编辑',
|
||||
name: '姓名',
|
||||
position: '职位',
|
||||
department: '部门',
|
||||
contact: '联系方式',
|
||||
phone: '电话',
|
||||
hire_date: '入职时间',
|
||||
memoryContent: '记忆内容',
|
||||
created_at: '创建时间',
|
||||
},
|
||||
space: {
|
||||
createSpace: '创建空间',
|
||||
@@ -1714,7 +1731,7 @@ export const zh = {
|
||||
expect_improvement_tag: '仅低阈值会记录',
|
||||
confidence: '置信度'
|
||||
},
|
||||
emotionDetail: {
|
||||
statementDetail: {
|
||||
wordCloud: '情感分布分析',
|
||||
pieces: '条',
|
||||
emotionTags: '高频情绪关键词',
|
||||
|
||||
@@ -56,7 +56,7 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
|
||||
SpaceManagement: lazy(() => import('@/views/SpaceManagement')),
|
||||
ApiKeyManagement: lazy(() => import('@/views/ApiKeyManagement')),
|
||||
EmotionEngine: lazy(() => import('@/views/EmotionEngine')),
|
||||
EmotionDetail: lazy(() => import('@/views/UserMemoryDetail/pages/EmotionDetail')),
|
||||
StatementDetail: lazy(() => import('@/views/UserMemoryDetail/pages/StatementDetail')),
|
||||
SelfReflectionEngine: lazy(() => import('@/views/SelfReflectionEngine')),
|
||||
Login: lazy(() => import('@/views/Login')),
|
||||
InviteRegister: lazy(() => import('@/views/InviteRegister')),
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{ "path": "/api-key", "element": "ApiKeyManagement" },
|
||||
{ "path": "/emotion-engine/:id", "element": "EmotionEngine" },
|
||||
{ "path": "/reflection-engine/:id", "element": "SelfReflectionEngine" },
|
||||
{ "path": "/user-memory/emotion/:id", "element": "EmotionDetail" },
|
||||
{ "path": "/statement/:id", "element": "StatementDetail" },
|
||||
{ "path": "/no-permission", "element": "NoPermission" },
|
||||
{ "path": "/*", "element": "NotFound" }
|
||||
]
|
||||
|
||||
@@ -26,19 +26,6 @@
|
||||
"sort": 0,
|
||||
"subs": []
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"parent": 0,
|
||||
"code": "tool",
|
||||
"label": "工具管理",
|
||||
"i18nKey": "menu.toolManagement",
|
||||
"path": "/tool",
|
||||
"enable": true,
|
||||
"display": true,
|
||||
"level": 1,
|
||||
"sort": 1,
|
||||
"subs": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"parent": 0,
|
||||
@@ -251,15 +238,15 @@
|
||||
"sort": 0,
|
||||
"subs": [
|
||||
{
|
||||
"id": 81,
|
||||
"parent": 8,
|
||||
"code": "emotionDetail",
|
||||
"id": 811,
|
||||
"parent": 81,
|
||||
"code": "statementDetail",
|
||||
"label": "记忆详情",
|
||||
"i18nKey": "menu.emotionDetail",
|
||||
"path": "/user-memory/emotion/:id",
|
||||
"i18nKey": "menu.statementDetail",
|
||||
"path": "/statement/:id",
|
||||
"enable": true,
|
||||
"display": false,
|
||||
"level": 2,
|
||||
"level": 3,
|
||||
"sort": 0,
|
||||
"subs": null
|
||||
}
|
||||
|
||||
@@ -57,20 +57,47 @@ export const useMenu = create<MenuState>((set, get) => ({
|
||||
const { allMenus } = get()
|
||||
const menus = allMenus[source] || []
|
||||
let result: MenuItem[] = []
|
||||
const matchedMenu: MenuItem | undefined = menus.find(menu => menu.path === paths[paths.length - 1] || `${menu.id}` === paths[1]);
|
||||
|
||||
if (matchedMenu) {
|
||||
let matchedSubMenu: MenuItem | undefined = undefined;
|
||||
if (paths.length > 1 && matchedMenu?.subs?.length) {
|
||||
matchedSubMenu = matchedMenu.subs.find(menu => menu.path === paths[0]);
|
||||
|
||||
console.log('updateBreadcrumbs paths:', paths);
|
||||
|
||||
if (paths.length === 3) {
|
||||
// 三级菜单:[subSubPath, subId, menuId]
|
||||
const menuId = paths[2];
|
||||
const subId = paths[1];
|
||||
const subSubPath = paths[0];
|
||||
|
||||
const matchedMenu = menus.find(menu => `${menu.id}` === menuId);
|
||||
if (matchedMenu && matchedMenu.subs) {
|
||||
const matchedSub = matchedMenu.subs.find(sub => `${sub.id}` === subId);
|
||||
if (matchedSub && matchedSub.subs) {
|
||||
const matchedSubSub = matchedSub.subs.find(subSub => subSub.path === subSubPath);
|
||||
if (matchedSubSub) {
|
||||
result = [
|
||||
{ ...matchedMenu, subs: null },
|
||||
{ ...matchedSub, subs: null },
|
||||
{ ...matchedSubSub, subs: null }
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
result = [
|
||||
{ ...matchedMenu, subs: null },
|
||||
matchedSubMenu
|
||||
].filter(item => item !== undefined) as MenuItem[]
|
||||
} else {
|
||||
result = [] as MenuItem[]
|
||||
// 原有逻辑处理一级和二级菜单
|
||||
const matchedMenu: MenuItem | undefined = menus.find(menu => menu.path === paths[paths.length - 1] || `${menu.id}` === paths[1]);
|
||||
|
||||
if (matchedMenu) {
|
||||
let matchedSubMenu: MenuItem | undefined = undefined;
|
||||
if (paths.length > 1 && matchedMenu?.subs?.length) {
|
||||
matchedSubMenu = matchedMenu.subs.find(menu => menu.path === paths[0]);
|
||||
}
|
||||
result = [
|
||||
{ ...matchedMenu, subs: null },
|
||||
matchedSubMenu
|
||||
].filter(item => item !== undefined) as MenuItem[]
|
||||
} else {
|
||||
result = [] as MenuItem[]
|
||||
}
|
||||
}
|
||||
|
||||
const allBreadcrumbs = { ...get().allBreadcrumbs, [source]: result }
|
||||
set({ allBreadcrumbs })
|
||||
localStorage.setItem('breadcrumbs', JSON.stringify(allBreadcrumbs))
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Row, Col, Form, App, Button, Switch, Space, Select } from 'antd';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import RbCard from '@/components/RbCard/Card';
|
||||
import strategyImpactSimulator from '@/assets/images/memory/strategyImpactSimulator.svg'
|
||||
import { getMemoryReflectionConfig, updateMemoryReflectionConfig, pilotRunMemoryReflectionConfig } from '@/api/memory'
|
||||
@@ -139,6 +140,9 @@ const SelfReflectionEngine: React.FC = () => {
|
||||
.then((res) => {
|
||||
setResult(res as Result)
|
||||
})
|
||||
.catch(() => {
|
||||
setRunLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setRunLoading(false)
|
||||
})
|
||||
|
||||
@@ -8,22 +8,15 @@ import down from '@/assets/images/userMemory/down.svg'
|
||||
import interestDistribution from '@/assets/images/userMemory/interestDistribution.svg'
|
||||
import PieCard from './components/PieCard'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import type { Data } from './types'
|
||||
import {
|
||||
getUserSummary,
|
||||
getUserProfile,
|
||||
getTotalMemoryCountByUser,
|
||||
} from '@/api/memory'
|
||||
import RelationshipNetwork from './components/RelationshipNetwork'
|
||||
import MemoryInsight from './components/MemoryInsight'
|
||||
import Empty from '@/components/Empty'
|
||||
|
||||
import WordCloud from './components/WordCloud'
|
||||
import EmotionTags from './components/EmotionTags'
|
||||
import Health from './components/Health'
|
||||
import Suggestions from './components/Suggestions'
|
||||
|
||||
const tagColors = ['21, 94, 239', '156, 111, 255', '255, 93, 52', '54, 159, 33']
|
||||
import NodeStatistics from './components/NodeStatistics'
|
||||
import EndUserProfile from './components/EndUserProfile'
|
||||
|
||||
interface TitleProps {
|
||||
type: string;
|
||||
@@ -34,15 +27,15 @@ interface TitleProps {
|
||||
onClick: (type: string) => void;
|
||||
}
|
||||
const Title: FC<TitleProps> = ({ type, title, icon, t, expanded, onClick }) => (
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:py-[17px] rb:border-b-[1px] rb:border-[#DFE4ED] rb:text-[16px] rb:font-semibold rb:leading-[22px]">
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:py-4.25 rb:border-b rb:border-[#DFE4ED] rb:text-[16px] rb:font-semibold rb:leading-5.5">
|
||||
<span className="rb:flex rb:items-center">
|
||||
<img src={icon} className="rb:w-[20px] rb:h-[20px] rb:mr-[8px]" />
|
||||
<img src={icon} className="rb:w-5 rb:h-5 rb:mr-2" />
|
||||
{title}
|
||||
</span>
|
||||
|
||||
<span className="rb:flex rb:items-center rb:cursor-pointer rb:text-[#5B6167] rb:text-[14px] rb:font-regular rb:leading-[20px]" onClick={() => onClick(type)}>
|
||||
<span className="rb:flex rb:items-center rb:cursor-pointer rb:text-[#5B6167] rb:text-[14px] rb:font-regular rb:leading-5" onClick={() => onClick(type)}>
|
||||
{t(`userMemory.${expanded ? 'foldUp' : '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,
|
||||
})} />
|
||||
</span>
|
||||
@@ -52,47 +45,20 @@ const Title: FC<TitleProps> = ({ type, title, icon, t, expanded, onClick }) => (
|
||||
const Neo4j: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [data, setData] = useState<Data | null>(null)
|
||||
const [expanded, setExpanded] = useState<string[]>(['aboutUs', 'interestDistribution', 'importantRelationships', 'importantMomentsInLife'])
|
||||
const [summary, setSummary] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState<Record<string, boolean>>({
|
||||
detail: false,
|
||||
summary: false,
|
||||
})
|
||||
const [memory, setMemory] = useState<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getMemory()
|
||||
getSummary()
|
||||
getDetail()
|
||||
}, [id])
|
||||
|
||||
const handleTitleClick = (key: string) => {
|
||||
setExpanded(expanded.includes(key) ? expanded.filter((item) => item !== key) : [...expanded, key])
|
||||
}
|
||||
// 用户记忆详情
|
||||
const getDetail = () => {
|
||||
if (!id) return
|
||||
setLoading(prev => ({ ...prev, detail: true }))
|
||||
getUserProfile(id).then((res) => {
|
||||
setData((res as Data))
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(prev => ({ ...prev, detail: false }))
|
||||
})
|
||||
}
|
||||
// 记忆总览
|
||||
const getMemory = () => {
|
||||
if (!id) return
|
||||
setLoading(prev => ({ ...prev, memory: true }))
|
||||
getTotalMemoryCountByUser(id).then((res) => {
|
||||
setMemory(res.total)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(prev => ({ ...prev, memory: false }))
|
||||
})
|
||||
}
|
||||
// 用户摘要
|
||||
const getSummary = () => {
|
||||
if (!id) return
|
||||
@@ -105,42 +71,15 @@ const Neo4j: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const name = loading.detail ? '' : data?.name && data?.name !== '' ? data.name : id
|
||||
return (
|
||||
<Row gutter={[16, 16]} className="rb:pb-6">
|
||||
<Col span={8}>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<EndUserProfile />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<RbCard>
|
||||
<div className="rb:flex rb:items-center">
|
||||
<div className="rb:flex-[0_0_auto] rb:w-[80px] rb:h-[80px] rb:text-center rb:font-semibold rb:text-[28px] rb:leading-[80px] rb:rounded-[8px] rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name?.[0]}</div>
|
||||
<div className="rb:text-[24px] rb:font-semibold rb:leading-[32px] rb:ml-[16px]">
|
||||
{name}<br/>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px] rb:mt-[8px]">{data?.tags?.join(' | ')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rb:flex rb:gap-[8px] rb:mb-[8px] rb:flex-wrap rb:mt-[25px]">
|
||||
{data?.hot_tags?.map((tag, tagIndex) => (
|
||||
<span key={tag} className="rb:rounded-[11px] rb:p-[0_8px] rb:leading-[22px] rb:border"
|
||||
style={{
|
||||
backgroundColor: `rgba(${tagColors[tagIndex % tagColors.length]}, 0.08)`,
|
||||
borderColor: `rgba(${tagColors[tagIndex % tagColors.length]}, 0.3)`,
|
||||
color: `rgba(${tagColors[tagIndex % tagColors.length]}, 1)`,
|
||||
}}
|
||||
>
|
||||
{tag.name}({tag.frequency})
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 记忆总量 */}
|
||||
<div className="rb:font-regular rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px] rb:mb-[25px]">
|
||||
{t('userMemory.totalNumOfMemories')}
|
||||
<div className="rb:font-extrabold rb:text-[24px] rb:text-[#212332] rb:leading-[30px] rb:mt-[8px]">{memory || 0}</div>
|
||||
</div>
|
||||
|
||||
{/* 关于我 */}
|
||||
<>
|
||||
<Title
|
||||
@@ -154,12 +93,12 @@ const Neo4j: FC = () => {
|
||||
{expanded.includes('aboutUs') && (
|
||||
<>
|
||||
{loading.summary
|
||||
? <Skeleton className="rb:mt-[16px]" />
|
||||
? <Skeleton className="rb:mt-4" />
|
||||
: summary
|
||||
? <div className="rb:font-regular rb:leading-[22px] rb:pt-[16px]">
|
||||
? <div className="rb:font-regular rb:leading-5.5 rb:pt-4">
|
||||
{summary || '-'}
|
||||
</div>
|
||||
: <Empty size={88} className="rb:mt-[48px] rb:mb-[81px]" />
|
||||
: <Empty size={88} className="rb:mt-12 rb:mb-20.25" />
|
||||
}
|
||||
</>
|
||||
)}
|
||||
@@ -182,28 +121,19 @@ const Neo4j: FC = () => {
|
||||
</>
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<EmotionTags />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<NodeStatistics />
|
||||
</Col>
|
||||
{/* 记忆洞察 */}
|
||||
<Col span={24}>
|
||||
<MemoryInsight />
|
||||
</Col>
|
||||
{/* 关系网络 + 记忆详情 */}
|
||||
<RelationshipNetwork />
|
||||
<Col span={12}>
|
||||
<WordCloud />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Health />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Suggestions />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -28,15 +28,15 @@ interface TitleProps {
|
||||
onClick: (type: string) => void;
|
||||
}
|
||||
const Title: FC<TitleProps> = ({ type, title, icon, t, expanded, onClick }) => (
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:py-[17px] rb:border-b-[1px] rb:border-[#DFE4ED] rb:text-[16px] rb:font-semibold rb:leading-[22px]">
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:py-4.25 rb:border-b rb:border-[#DFE4ED] rb:text-[16px] rb:font-semibold rb:leading-5.5">
|
||||
<span className="rb:flex rb:items-center">
|
||||
<img src={icon} className="rb:w-[20px] rb:h-[20px] rb:mr-[8px]" />
|
||||
<img src={icon} className="rb:w-5 rb:h-5 rb:mr-2" />
|
||||
{title}
|
||||
</span>
|
||||
|
||||
<span className="rb:flex rb:items-center rb:cursor-pointer rb:text-[#5B6167] rb:text-[14px] rb:font-regular rb:leading-[20px]" onClick={() => onClick(type)}>
|
||||
<span className="rb:flex rb:items-center rb:cursor-pointer rb:text-[#5B6167] rb:text-[14px] rb:font-regular rb:leading-5" onClick={() => onClick(type)}>
|
||||
{t(`userMemory.${expanded ? 'foldUp' : '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,
|
||||
})} />
|
||||
</span>
|
||||
@@ -115,20 +115,20 @@ const Rag: FC = () => {
|
||||
}
|
||||
const name = loading.detail ? '' : data?.name && data?.name !== '' ? data.name : id
|
||||
return (
|
||||
<Row gutter={[16, 16]} className="rb:pb-[24px]">
|
||||
<Row gutter={[16, 16]} className="rb:pb-6">
|
||||
<Col span={8}>
|
||||
<RbCard>
|
||||
<div className="rb:flex rb:items-center">
|
||||
<div className="rb:flex-[0_0_auto] rb:w-[80px] rb:h-[80px] rb:text-center rb:font-semibold rb:text-[28px] rb:leading-[80px] rb:rounded-[8px] rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name?.[0]}</div>
|
||||
<div className="rb:text-[24px] rb:font-semibold rb:leading-[32px] rb:ml-[16px]">
|
||||
<div className="rb:flex-[0_0_auto] rb:w-20 rb:h-20 rb:text-center rb:font-semibold rb:text-[28px] rb:leading-20 rb:rounded-lg rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name?.[0]}</div>
|
||||
<div className="rb:text-[24px] rb:font-semibold rb:leading-8 rb:ml-4">
|
||||
{name}<br/>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px] rb:mt-[8px]">{personas?.join(' | ')}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 rb:mt-2">{personas?.join(' | ')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rb:flex rb:gap-[8px] rb:mb-[8px] rb:flex-wrap rb:mt-[25px]">
|
||||
<div className="rb:flex rb:gap-2 rb:mb-2 rb:flex-wrap rb:mt-6.25">
|
||||
{tags?.map((tag, tagIndex) => (
|
||||
<span key={tag.tag} className="rb:rounded-[11px] rb:p-[0_8px] rb:leading-[22px] rb:border"
|
||||
<span key={tag.tag} className="rb:rounded-[11px] rb:p-[0_8px] rb:leading-5.5 rb:border"
|
||||
style={{
|
||||
backgroundColor: `rgba(${tagColors[tagIndex % tagColors.length]}, 0.08)`,
|
||||
borderColor: `rgba(${tagColors[tagIndex % tagColors.length]}, 0.3)`,
|
||||
@@ -141,9 +141,9 @@ const Rag: FC = () => {
|
||||
</div>
|
||||
|
||||
{/* 记忆总量 */}
|
||||
<div className="rb:font-regular rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px] rb:mb-[25px]">
|
||||
<div className="rb:font-regular rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:mb-6.25">
|
||||
{t('userMemory.totalNumOfMemories')}
|
||||
<div className="rb:font-extrabold rb:text-[24px] rb:text-[#212332] rb:leading-[30px] rb:mt-[8px]">{memory || 0}</div>
|
||||
<div className="rb:font-extrabold rb:text-[24px] rb:text-[#212332] rb:leading-7.5 rb:mt-2">{memory || 0}</div>
|
||||
</div>
|
||||
|
||||
{/* 关于我 */}
|
||||
@@ -159,12 +159,12 @@ const Rag: FC = () => {
|
||||
{expanded.includes('aboutUs') && (
|
||||
<>
|
||||
{loading.summary
|
||||
? <Skeleton className="rb:mt-[16px]" />
|
||||
? <Skeleton className="rb:mt-4" />
|
||||
: summary
|
||||
? <div className="rb:font-regular rb:leading-[22px] rb:pt-[16px]">
|
||||
? <div className="rb:font-regular rb:leading-5.5 rb:pt-4">
|
||||
{summary || '-'}
|
||||
</div>
|
||||
: <Empty size={88} className="rb:mt-[48px] rb:mb-[81px]" />
|
||||
: <Empty size={88} className="rb:mt-12 rb:mb-20.25" />
|
||||
}
|
||||
</>
|
||||
)}
|
||||
@@ -182,12 +182,12 @@ const Rag: FC = () => {
|
||||
{expanded.includes('memoryInsight') && (
|
||||
<>
|
||||
{loading.insight
|
||||
? <Skeleton className="rb:mt-[16px]" />
|
||||
? <Skeleton className="rb:mt-4" />
|
||||
: insight
|
||||
? <div className="rb:font-regular rb:leading-[22px] rb:pt-[16px]">
|
||||
? <div className="rb:font-regular rb:leading-5.5 rb:pt-4">
|
||||
{insight || '-'}
|
||||
</div>
|
||||
: <Empty size={88} className="rb:mt-[48px] rb:mb-[81px]" />
|
||||
: <Empty size={88} className="rb:mt-12 rb:mb-20.25" />
|
||||
}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -10,11 +10,11 @@ interface CardProps {
|
||||
|
||||
const Card: FC<CardProps> = ({ title, children, theme = 'default', className }) => {
|
||||
return (
|
||||
<div className={clsx('rb:h-full rb:border rb:rounded-[12px] rb:p-[16px] rb:border-[#DFE4ED]', {
|
||||
<div className={clsx('rb:h-full rb:border rb:rounded-xl rb:p-4 rb:border-[#DFE4ED]', {
|
||||
'rb:bg-[#FBFDFF]': theme === 'default',
|
||||
'rb:bg-[linear-gradient(180deg,_#F1F9FE_0%,_#FBFCFF_100%)]': theme === 'custom',
|
||||
'rb:bg-[linear-gradient(180deg,#F1F9FE_0%,#FBFCFF_100%)]': theme === 'custom',
|
||||
}, className)}>
|
||||
{title && <div className="rb:text-[18px] rb:font-semibold rb:leading-[25px] rb:pb-[16px]">{title}</div>}
|
||||
{title && <div className="rb:text-[18px] rb:font-semibold rb:leading-6.25 rb:pb-4">{title}</div>}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -47,7 +47,7 @@ const ConversationMemory:FC = () => {
|
||||
<List.Item>
|
||||
<div
|
||||
key={index}
|
||||
className="rb:rounded-lg rb:border rb:border-[#DFE4ED] rb:px-4 rb:py-3 rb:bg-[#F0F3F8] rb:rounded-lg rb:mt-2 rb:text-gray-800 rb:text-sm"
|
||||
className="rb:rounded-lg rb:border rb:border-[#DFE4ED] rb:px-4 rb:py-3 rb:bg-[#F0F3F8] rb:mt-2 rb:text-gray-800 rb:text-sm"
|
||||
>
|
||||
<Markdown content={item} />
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ interface TagList {
|
||||
const EmotionTags: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [tagList, setTagList] = useState<TagList | null>(null)
|
||||
const [data, setData] = useState<TagList | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
getEmotionTagData()
|
||||
@@ -25,18 +25,18 @@ const EmotionTags: FC = () => {
|
||||
}
|
||||
getWordCloud(id)
|
||||
.then((res) => {
|
||||
setTagList(res as TagList)
|
||||
setData(res as TagList)
|
||||
})
|
||||
}
|
||||
|
||||
const [visibleCount, setVisibleCount] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!tagList || tagList?.keywords.length === 0) return
|
||||
if (!data || data?.keywords.length === 0) return
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setVisibleCount(prev => {
|
||||
if (prev >= tagList?.keywords.length) {
|
||||
if (prev >= data?.keywords.length) {
|
||||
clearInterval(timer)
|
||||
return prev
|
||||
}
|
||||
@@ -45,7 +45,7 @@ const EmotionTags: FC = () => {
|
||||
}, 200)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [tagList?.keywords.length])
|
||||
}, [data?.keywords.length])
|
||||
|
||||
const getEmotionColor = (emotionType: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
@@ -59,19 +59,19 @@ const EmotionTags: FC = () => {
|
||||
return colors[emotionType] || '#8c8c8c'
|
||||
}
|
||||
|
||||
const emotionStats = tagList?.keywords.reduce((acc, item) => {
|
||||
const emotionStats = data?.keywords.reduce((acc, item) => {
|
||||
acc[item.emotion_type] = (acc[item.emotion_type] || 0) + item.frequency
|
||||
return acc
|
||||
}, {} as Record<string, number>) ?? {}
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('emotionDetail.emotionTags')}
|
||||
title={t('statementDetail.emotionTags')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
bodyClassName='rb:p-0! rb:relative'
|
||||
bodyClassName='rb:p-0! rb:pb-3! rb:relative'
|
||||
>
|
||||
{tagList
|
||||
{data?.keywords && data?.keywords.length > 0
|
||||
? <>
|
||||
<div className="rb:flex rb:flex-wrap rb:items-center rb:gap-6 rb:text-sm rb:mt-3 rb:p-3 rb:bg-[#F0F3F8]">
|
||||
{Object.entries(emotionStats).map(([type, count]) => {
|
||||
@@ -79,13 +79,13 @@ const EmotionTags: FC = () => {
|
||||
return (
|
||||
<div key={type} className="rb:flex rb:items-center rb:gap-2">
|
||||
<div className="rb:w-3 rb:h-3 rb:rounded-full" style={{ backgroundColor: getEmotionColor(type) }}></div>
|
||||
<span className="rb:text-gray-600">{t(`emotionDetail.${type || 'neutral'}`)} ({count}个)</span>
|
||||
<span className="rb:text-gray-600">{t(`statementDetail.${type || 'neutral'}`)} ({count}个)</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="rb:mt-6 rb:flex rb:items-center rb:flex-wrap rb:gap-3 rb:mb-3 rb:px-6">
|
||||
{tagList.keywords.slice(0, visibleCount).map((item, index) => (
|
||||
{data.keywords.slice(0, visibleCount).map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rb:flex rb:items-center rb:justify-center rb:animate-fadeIn rb:px-4 rb:py-2 rb:rounded-full rb:text-white rb:font-medium"
|
||||
@@ -102,7 +102,7 @@ const EmotionTags: FC = () => {
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
: <Empty />
|
||||
: <Empty size={88} />
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
|
||||
72
web/src/views/UserMemoryDetail/components/EndUserProfile.tsx
Normal file
72
web/src/views/UserMemoryDetail/components/EndUserProfile.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { type FC, useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Descriptions, Button } from 'antd';
|
||||
import dayjs from 'dayjs'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Empty from '@/components/Empty';
|
||||
import {
|
||||
getEndUserProfile,
|
||||
} from '@/api/memory'
|
||||
import EndUserProfileModal from './EndUserProfileModal'
|
||||
import type { EndUser, EndUserProfileModalRef } from '../types'
|
||||
|
||||
const EndUserProfile:FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const endUserProfileModalRef = useRef<EndUserProfileModalRef>(null)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<EndUser | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
// 记忆洞察
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getEndUserProfile(id).then((res) => {
|
||||
setData(res as EndUser)
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
const formatItems = useCallback(() => {
|
||||
if (!data) return []
|
||||
return ['name', 'position', 'department', 'contact', 'phone', 'hire_date'].map(key => ({
|
||||
key,
|
||||
label: t(`userMemory.${key}`),
|
||||
children: key === 'hire_date' ? dayjs(data[key as keyof EndUser]).format('YYYY-MM-DD') : String(data[key as keyof EndUser] || ''),
|
||||
}))
|
||||
}, [data])
|
||||
const handleEdit = () => {
|
||||
if (!data) return
|
||||
endUserProfileModalRef.current?.handleOpen(data)
|
||||
}
|
||||
return (
|
||||
<RbCard
|
||||
title={t('userMemory.endUserProfile')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton />
|
||||
: data
|
||||
? <div className="rb:flex rb:flex-wrap rb:justify-between rb:h-full">
|
||||
<Descriptions column={1} items={formatItems()} classNames={{ label: 'rb:w-24' }} />
|
||||
<Button className="rb:mt-3" block onClick={handleEdit}>{t('common.edit')}</Button>
|
||||
</div>
|
||||
: <Empty size={80} />
|
||||
}
|
||||
<EndUserProfileModal
|
||||
ref={endUserProfileModalRef}
|
||||
refresh={getData}
|
||||
/>
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
export default EndUserProfile
|
||||
@@ -0,0 +1,128 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, DatePicker } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { EndUser, EndUserProfileModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { updatedEndUserProfile, } from '@/api/memory'
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface EndUserProfileModalProps {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const EndUserProfileModal = forwardRef<EndUserProfileModalRef, EndUserProfileModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<EndUser>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = (user: EndUser) => {
|
||||
form.setFieldsValue({
|
||||
...user,
|
||||
end_user_id: user.id,
|
||||
hire_date: dayjs(user.hire_date)
|
||||
});
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
setLoading(true)
|
||||
updatedEndUserProfile({
|
||||
...values,
|
||||
hire_date: values.hire_date.valueOf()
|
||||
})
|
||||
.then(() => {
|
||||
setLoading(false)
|
||||
refresh()
|
||||
handleClose()
|
||||
message.success(t('common.saveSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('common.edit')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<FormItem name="end_user_id" hidden></FormItem>
|
||||
<FormItem
|
||||
name="name"
|
||||
label={t('userMemory.name')}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="position"
|
||||
label={t('userMemory.position')}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="department"
|
||||
label={t('userMemory.department')}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="contact"
|
||||
label={t('userMemory.contact')}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="phone"
|
||||
label={t('userMemory.phone')}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="hire_date"
|
||||
label={t('userMemory.hire_date')}
|
||||
>
|
||||
<DatePicker className="rb:w-full" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default EndUserProfileModal;
|
||||
@@ -56,12 +56,11 @@ const Health: FC = () => {
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('emotionDetail.health')}
|
||||
title={t('statementDetail.health')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
height="100%"
|
||||
>
|
||||
{health
|
||||
{health?.health_score && health?.health_score > 0
|
||||
? <>
|
||||
<div className="rb:flex rb:justify-center rb:items-center">
|
||||
<Progress
|
||||
@@ -78,20 +77,20 @@ const Health: FC = () => {
|
||||
|
||||
{health.dimensions && <>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mt-6">
|
||||
<div className="rb:w-40 rb:mr-3">{t('emotionDetail.positivity_rate')}</div>
|
||||
<div className="rb:w-40 rb:mr-3">{t('statementDetail.positivity_rate')}</div>
|
||||
<Progress className="rb:w-[calc(100%-180px)]" percent={health.dimensions.positivity_rate.score} />
|
||||
</div>
|
||||
<div className="rb:flex rb:items-center rb:gap-3 rb:mt-3">
|
||||
<div className="rb:w-40 rb:mr-3">{t('emotionDetail.stability')}</div>
|
||||
<div className="rb:w-40 rb:mr-3">{t('statementDetail.stability')}</div>
|
||||
<Progress className="rb:w-[calc(100%-180px)]" percent={health.dimensions.stability.score} />
|
||||
</div>
|
||||
<div className="rb:flex rb:items-center rb:gap-3 rb:mt-3">
|
||||
<div className="rb:w-40 rb:mr-3">{t('emotionDetail.resilience')}</div>
|
||||
<div className="rb:w-40 rb:mr-3">{t('statementDetail.resilience')}</div>
|
||||
<Progress className="rb:w-[calc(100%-180px)]" percent={health.dimensions.resilience.score} />
|
||||
</div>
|
||||
</>}
|
||||
</>
|
||||
: <Empty />
|
||||
: <Empty size={88} className="rb:h-full" />
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ const MemoryInsight:FC = () => {
|
||||
? <Skeleton />
|
||||
: report
|
||||
? <div className="rb:flex rb:flex-wrap rb:justify-between rb:h-full">
|
||||
<div className="rb:leading-[22px]">
|
||||
<div className="rb:leading-5.5">
|
||||
{report|| '-'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
90
web/src/views/UserMemoryDetail/components/NodeStatistics.tsx
Normal file
90
web/src/views/UserMemoryDetail/components/NodeStatistics.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Skeleton } from 'antd';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Empty from '@/components/Empty';
|
||||
import {
|
||||
getNodeStatistics,
|
||||
} from '@/api/memory'
|
||||
import type { NodeStatisticsItem } from '../types'
|
||||
|
||||
|
||||
const NodeStatistics: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [total, setTotal] = useState<number>(0)
|
||||
const [data, setData] = useState<NodeStatisticsItem[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
// 记忆洞察
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getNodeStatistics(id).then((res) => {
|
||||
const response = res as { nodes: NodeStatisticsItem[], total: number }
|
||||
setData(response.nodes)
|
||||
setTotal(response.total)
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
const handleViewDetail = (type: string) => {
|
||||
switch (type) {
|
||||
case 'Statement':
|
||||
navigate(`/statement/${id}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
return (
|
||||
<RbCard
|
||||
title={t('userMemory.nodeStatistics')}
|
||||
extra={<div>{t('userMemory.total')}: {total}</div>}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
bgColor="linear-gradient(180deg,#F1F9FE 0%, #FBFCFF 100%)"
|
||||
height="100%"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton />
|
||||
: data.length > 0
|
||||
? <div className={`rb:w-full rb:grid rb:grid-cols-${data.length} rb:gap-2`}>
|
||||
{data.map(vo => (
|
||||
<div
|
||||
key={vo.type}
|
||||
className={clsx("rb:group rb:border rb:border-[#DFE4ED] rb:p-0 rb:rounded-xl rb:hover:shadow-[0px_2px_4px_0px_rgba(0,0,0,0.15)]", {
|
||||
'rb:cursor-pointer': vo.type === 'Statement'
|
||||
})}
|
||||
onClick={() => handleViewDetail(vo.type)}
|
||||
>
|
||||
<div className="rb:gap-0.5 rb:p-3 rb:leading-4 rb:text-[#5B6167] rb:flex rb:items-center rb:justify-between rb:border-b rb:border-[#DFE4ED]">
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1">{t(`userMemory.${vo.type}`)}</div>
|
||||
{vo.type === 'Statement' && <div
|
||||
className="rb:w-3 rb:h-3 rb:-ml-0.75 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/home/arrow_top_right.svg')] rb:group-hover:bg-[url('@/assets/images/home/arrow_top_right_hover.svg')]"
|
||||
></div>}
|
||||
</div>
|
||||
|
||||
<div className="rb:p-3 rb:flex rb:justify-between rb:items-center rb:font-bold rb:text-[20px] rb:text-[#212332] rb:text-left">
|
||||
{vo.count ?? 0}
|
||||
<div className="rb:text-right rb:font-normal rb:text-[14px] rb:text-[#5F6266] rb:leading-4 rb:gap-1">
|
||||
{vo.percentage ?? 0}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
: <Empty size={80} />
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
export default NodeStatistics
|
||||
@@ -59,7 +59,7 @@ const PieCard: FC = () => {
|
||||
{loading
|
||||
? <Loading size={249} />
|
||||
: !data || data.length === 0
|
||||
? <Empty size={88} className="rb:mt-[48px] rb:mb-[81px]" />
|
||||
? <Empty size={88} className="rb:mt-12 rb:mb-20.25" />
|
||||
: data && data.length > 0 &&
|
||||
<ReactEcharts
|
||||
option={{
|
||||
|
||||
@@ -8,11 +8,12 @@ import zoom from '@/assets/images/userMemory/zoom.svg'
|
||||
import drag from '@/assets/images/userMemory/drag.svg'
|
||||
import pointer from '@/assets/images/userMemory/pointer.svg'
|
||||
import empty from '@/assets/images/userMemory/empty.svg'
|
||||
import type { EdgeData, Node, Edge } from '../types'
|
||||
import type { Node, Edge, GraphData } from '../types'
|
||||
import {
|
||||
getMemorySearchEdges,
|
||||
} from '@/api/memory'
|
||||
import Empty from '@/components/Empty'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const operations = [
|
||||
{ name: 'click', icon: pointer },
|
||||
@@ -35,89 +36,76 @@ const RelationshipNetwork:FC = () => {
|
||||
if (!id) return
|
||||
setSelectedNode(null)
|
||||
getMemorySearchEdges(id).then((res) => {
|
||||
const list = (res as { detials?: EdgeData[] }).detials || []
|
||||
const nodes: Node[] = [];
|
||||
const links: Edge[] = [];
|
||||
const categories: { name: string }[] = []
|
||||
const { nodes, edges, statistics } = res as GraphData
|
||||
const curNodes: Node[] = []
|
||||
const curEdges: Edge[] = []
|
||||
const curNodeTypes = Object.keys(statistics.node_types)
|
||||
|
||||
list.forEach(item => {
|
||||
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,
|
||||
})
|
||||
}
|
||||
if (item.sourceNode) {
|
||||
nodes.push(item.sourceNode)
|
||||
categories.push({name: item.sourceNode.entity_type || 'Unknown'})
|
||||
}
|
||||
if (item.targetNode) {
|
||||
nodes.push(item.targetNode)
|
||||
categories.push({name: item.targetNode.entity_type || 'Unknown'})
|
||||
}
|
||||
// 计算每个节点的连接数
|
||||
const connectionCount: Record<string, number> = {}
|
||||
edges.forEach(edge => {
|
||||
connectionCount[edge.source] = (connectionCount[edge.source] || 0) + 1
|
||||
connectionCount[edge.target] = (connectionCount[edge.target] || 0) + 1
|
||||
})
|
||||
|
||||
// 根据ID字段去重节点
|
||||
const uniqueNodes = nodes.filter((node, index, self) =>
|
||||
index === self.findIndex((n) => n.id === node.id && n.name === node.name)
|
||||
)
|
||||
const uniqueLinks = links.filter((node, index, self) =>
|
||||
index === self.findIndex((n) => n.target === node.target && n.source === node.source)
|
||||
)
|
||||
const uniqueCategories = categories.filter((node, index, self) =>
|
||||
index === self.findIndex((n) => n.name === node.name)
|
||||
)
|
||||
|
||||
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 || 'Unknown'))
|
||||
item.category = index
|
||||
// 处理节点数据
|
||||
nodes.forEach(node => {
|
||||
const connections = connectionCount[node.id] || 0
|
||||
const categoryIndex = curNodeTypes.indexOf(node.label)
|
||||
|
||||
// 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
|
||||
// 根据节点类型获取显示名称
|
||||
let displayName = ''
|
||||
switch (node.label) {
|
||||
case 'Statement':
|
||||
displayName = 'statement' in node.properties ? node.properties.statement?.slice(0, 5) || '' : ''
|
||||
break
|
||||
case 'ExtractedEntity':
|
||||
displayName = 'name' in node.properties ? node.properties.name || '' : ''
|
||||
break
|
||||
default:
|
||||
displayName = 'content' in node.properties ? node.properties.content?.slice(0, 5) || '' : ''
|
||||
break
|
||||
}
|
||||
let symbolSize = 0
|
||||
if (connections <= 1) {
|
||||
symbolSize = 5
|
||||
} else if (connections <= 10) {
|
||||
symbolSize = 10
|
||||
} else if (connections <= 15) {
|
||||
symbolSize = 15
|
||||
} else if (connections <= 20) {
|
||||
symbolSize = 25
|
||||
} else {
|
||||
item.symbolSize = 35
|
||||
symbolSize = 35
|
||||
}
|
||||
|
||||
curNodes.push({
|
||||
...node,
|
||||
name: displayName,
|
||||
category: categoryIndex >= 0 ? categoryIndex : 0,
|
||||
symbolSize: symbolSize, // 根据连接数调整节点大小
|
||||
itemStyle: {
|
||||
color: ['#155EEF', '#4DA8FF', '#9C6FFF', '#8BAEF7', '#369F21', '#FF5D34', '#FF8A4C', '#FFB048'][categoryIndex % 8]
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(uniqueNodes)
|
||||
|
||||
// 处理边数据
|
||||
edges.forEach(edge => {
|
||||
curEdges.push({
|
||||
...edge,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
value: edge.weight || 1
|
||||
})
|
||||
})
|
||||
|
||||
// 设置分类
|
||||
const curCategories = curNodeTypes.map(type => ({ name: type }))
|
||||
|
||||
setNodes(curNodes)
|
||||
setLinks(curEdges)
|
||||
setCategories(curCategories)
|
||||
})
|
||||
}, [id])
|
||||
useEffect(() => {
|
||||
@@ -147,7 +135,6 @@ const RelationshipNetwork:FC = () => {
|
||||
}
|
||||
}, [nodes])
|
||||
|
||||
console.log('nodes', nodes)
|
||||
return (
|
||||
<>
|
||||
{/* 关系网络 */}
|
||||
@@ -157,7 +144,7 @@ const RelationshipNetwork:FC = () => {
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
>
|
||||
<div className="rb:h-[496px]">
|
||||
<div className="rb:h-124">
|
||||
{nodes.length === 0 ? (
|
||||
<Empty className="rb:h-full" />
|
||||
) : (
|
||||
@@ -175,8 +162,13 @@ const RelationshipNetwork:FC = () => {
|
||||
links: links || [],
|
||||
categories: categories || [],
|
||||
roam: true,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
formatter: '{b}',
|
||||
},
|
||||
lineStyle: {
|
||||
color: 'source',
|
||||
color: '#5B6167',
|
||||
curveness: 0.3
|
||||
},
|
||||
force: {
|
||||
@@ -218,19 +210,17 @@ const RelationshipNetwork:FC = () => {
|
||||
// 处理节点点击事件
|
||||
console.log('Node clicked:', params.data);
|
||||
// 使用函数式更新避免状态依赖问题
|
||||
setSelectedNode(prevSelected =>
|
||||
prevSelected?.id === params.data.id ? null : params.data
|
||||
)
|
||||
setSelectedNode(params.data)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="rb:bg-[#F0F3F8] rb:flex rb:items-center rb:gap-[24px] rb:rounded-[0px_0px_12px_12px] rb:p-[14px_40px] rb:m-[0_-20px_-16px_-16px]">
|
||||
<div className="rb:bg-[#F0F3F8] rb:flex rb:items-center rb:gap-6 rb:rounded-[0px_0px_12px_12px] rb:p-[14px_40px] rb:m-[0_-20px_-16px_-16px]">
|
||||
{operations.map((item) => (
|
||||
<div key={item.name} className="rb:flex rb:items-center rb:text-[#5B6167] rb:leading-[20px]">
|
||||
<img src={item.icon} className="rb:w-[20px] rb:h-[20px] rb:mr-[4px]" />
|
||||
<div key={item.name} className="rb:flex rb:items-center rb:text-[#5B6167] rb:leading-5">
|
||||
<img src={item.icon} className="rb:w-5 rb:h-5 rb:mr-1" />
|
||||
{t(`userMemory.${item.name}`)}
|
||||
</div>
|
||||
))}
|
||||
@@ -244,27 +234,35 @@ const RelationshipNetwork:FC = () => {
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
>
|
||||
{(!selectedNode || (!selectedNode?.description && !selectedNode?.entity_type))
|
||||
{!selectedNode
|
||||
? <Empty
|
||||
url={empty}
|
||||
title={t('userMemory.memoryDetailEmpty')}
|
||||
subTitle={t('userMemory.memoryDetailEmptyDesc')}
|
||||
className="rb:mb-[12px]"
|
||||
className="rb:mb-3"
|
||||
size={88}
|
||||
/>
|
||||
: <>
|
||||
{selectedNode?.description &&
|
||||
<div className="rb:font-medium rb:mb-[8px]">
|
||||
{t('userMemory.description')}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-[8px]"> {selectedNode.description}</div>
|
||||
|
||||
<div className="rb:font-medium rb:mb-2">
|
||||
{t('userMemory.memoryContent')}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2">
|
||||
{['Chunk', 'Dialogue', 'MemorySummary'].includes(selectedNode.label) && 'content' in selectedNode.properties
|
||||
? selectedNode.properties.content
|
||||
: selectedNode.label === 'ExtractedEntity' && 'description' in selectedNode.properties
|
||||
? selectedNode.properties.description
|
||||
: selectedNode.label === 'Statement' && 'statement' in selectedNode.properties
|
||||
? selectedNode.properties.statement
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{selectedNode?.entity_type &&
|
||||
<div className="rb:font-medium rb:mb-[8px]">
|
||||
{t('userMemory.entityType')}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-[8px]"> {selectedNode.entity_type}</div>
|
||||
</div>
|
||||
<div className="rb:font-medium rb:mb-2">
|
||||
{t('userMemory.created_at')}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2">
|
||||
{dayjs(selectedNode?.properties.created_at).format('YYYY/MM/DD HH:mm:ss')}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</RbCard>
|
||||
|
||||
@@ -39,11 +39,11 @@ const Suggestions: FC = () => {
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('emotionDetail.suggestions')}
|
||||
title={t('statementDetail.suggestions')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
>
|
||||
{suggestions
|
||||
{suggestions?.suggestions && suggestions?.suggestions.length > 0
|
||||
? <>
|
||||
<RbAlert className="rb:mb-3">{suggestions.health_summary}</RbAlert>
|
||||
{suggestions.suggestions.map((item, index) => (
|
||||
@@ -54,7 +54,7 @@ const Suggestions: FC = () => {
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
: <Empty />
|
||||
: <Empty size={88} className="rb:h-full" />
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
|
||||
@@ -81,7 +81,7 @@ const WordCloud: FC = () => {
|
||||
},
|
||||
radar: {
|
||||
indicator: radarData.map(item => ({
|
||||
name: t(`emotionDetail.${item.name}`),
|
||||
name: t(`statementDetail.${item.name}`),
|
||||
max: 100,
|
||||
min: 1
|
||||
}))
|
||||
@@ -99,12 +99,12 @@ const WordCloud: FC = () => {
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('emotionDetail.wordCloud')}
|
||||
title={t('statementDetail.wordCloud')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
height="100%"
|
||||
>
|
||||
{wordCloud
|
||||
{wordCloud?.total_count && wordCloud?.total_count > 0
|
||||
? <div className="rb:flex rb:h-100">
|
||||
<ReactEcharts ref={chartRef} option={radarOption} style={{ width: '50%', height: '100%' }} />
|
||||
<div className="rb:w-[50%] rb:pl-4 rb:flex rb:flex-col rb:justify-center">
|
||||
@@ -113,8 +113,8 @@ const WordCloud: FC = () => {
|
||||
{wordCloud.tags.map(item => (
|
||||
<div key={item.emotion_type}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:font-medium">
|
||||
{t(`emotionDetail.${item.emotion_type}`)}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular">{item.count}{t('emotionDetail.pieces')}</div>
|
||||
{t(`statementDetail.${item.emotion_type}`)}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular">{item.count}{t('statementDetail.pieces')}</div>
|
||||
</div>
|
||||
<Progress size="small" percent={item.percentage} />
|
||||
</div>
|
||||
@@ -122,7 +122,7 @@ const WordCloud: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
: <Empty />
|
||||
: <Empty size={88} />
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type FC } from 'react'
|
||||
import { Row, Col } from 'antd';
|
||||
import { Row, Col, Space } from 'antd';
|
||||
|
||||
import WordCloud from '../components/WordCloud'
|
||||
import EmotionTags from '../components/EmotionTags'
|
||||
@@ -7,17 +7,15 @@ import Health from '../components/Health'
|
||||
import Suggestions from '../components/Suggestions'
|
||||
|
||||
|
||||
const EmotionDetail: FC = () => {
|
||||
const StatementDetail: FC = () => {
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<WordCloud />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<EmotionTags />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Health />
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
<WordCloud />
|
||||
<EmotionTags />
|
||||
<Health />
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Suggestions />
|
||||
@@ -26,4 +24,4 @@ const EmotionDetail: FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default EmotionDetail
|
||||
export default StatementDetail
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Dayjs } from "dayjs";
|
||||
|
||||
export interface Data {
|
||||
id: string | number
|
||||
name: string;
|
||||
@@ -39,30 +41,93 @@ export interface Data {
|
||||
}[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
export interface BaseProperties {
|
||||
content: string;
|
||||
created_at: number;
|
||||
}
|
||||
export interface StatementNodeProperties {
|
||||
temporal_info: string;
|
||||
stmt_type: string;
|
||||
statement: string;
|
||||
valid_at: string;
|
||||
created_at: number;
|
||||
}
|
||||
export interface ExtractedEntityNodeProperties {
|
||||
description: string;
|
||||
name: string;
|
||||
entity_type: string;
|
||||
created_at: number;
|
||||
}
|
||||
export interface MemorySummaryNode {
|
||||
id: string;
|
||||
label: 'MemorySummary';
|
||||
category: number;
|
||||
symbolSize: number;
|
||||
itemStyle: {
|
||||
color: string;
|
||||
}
|
||||
name: string;
|
||||
properties: {
|
||||
content: string;
|
||||
created_at: number;
|
||||
}
|
||||
caption: string;
|
||||
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
id: string;
|
||||
description?: string;
|
||||
label: 'Dialogue' | 'ExtractedEntity' | 'Chunk' | 'MemorySummary' | 'Statement';
|
||||
category: number;
|
||||
symbolSize: number;
|
||||
name: string;
|
||||
connect_strength?: string;
|
||||
entity_idx: number;
|
||||
entity_type?: string;
|
||||
fact_summary?: string[];
|
||||
category?: number;
|
||||
symbolSize?: number;
|
||||
itemStyle: {
|
||||
color: string;
|
||||
}
|
||||
properties: BaseProperties | StatementNodeProperties | ExtractedEntityNodeProperties
|
||||
caption: string;
|
||||
}
|
||||
export interface Edge {
|
||||
statement: string;
|
||||
rel_id: string;
|
||||
source_id: string;
|
||||
predicate: string;
|
||||
target_id: string;
|
||||
statement_id: string;
|
||||
target?: string;
|
||||
source?: string;
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
type: string;
|
||||
properties: {
|
||||
run_id: string;
|
||||
group_id: string;
|
||||
created_at: string;
|
||||
expired_at: string;
|
||||
}
|
||||
caption: string;
|
||||
value: number;
|
||||
weight: number;
|
||||
}
|
||||
export interface EdgeData {
|
||||
sourceNode: Node;
|
||||
edge: Edge;
|
||||
targetNode: Node;
|
||||
export interface GraphData {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
statistics: {
|
||||
total_nodes: number;
|
||||
total_edges: number;
|
||||
node_types: Record<string, number>;
|
||||
edge_types: Record<string, number>;
|
||||
}
|
||||
}
|
||||
|
||||
export interface NodeStatisticsItem {
|
||||
type: string;
|
||||
count: number;
|
||||
percentage: number;
|
||||
}
|
||||
export interface EndUser {
|
||||
end_user_id: string;
|
||||
id: string;
|
||||
name: string;
|
||||
position: string;
|
||||
department: string;
|
||||
contact: string;
|
||||
phone: string;
|
||||
hire_date: string | number | Dayjs
|
||||
}
|
||||
export interface EndUserProfileModalRef {
|
||||
handleOpen: (vo: EndUser) => void;
|
||||
}
|
||||
@@ -54,7 +54,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
setChatList([])
|
||||
}
|
||||
const handleEditVariables = () => {
|
||||
variableConfigModalRef.current?.handleOpen()
|
||||
variableConfigModalRef.current?.handleOpen(variables)
|
||||
}
|
||||
const handleSave = (values: StartVariableItem[]) => {
|
||||
setVariables([...values])
|
||||
@@ -91,7 +91,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
}])
|
||||
setChatList(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: message,
|
||||
content: '',
|
||||
created_at: Date.now(),
|
||||
}])
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@ interface VariableEditModalProps {
|
||||
|
||||
const VariableConfigModal = forwardRef<VariableEditModalRef, VariableEditModalProps>(({
|
||||
refresh,
|
||||
variables
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<{variables: StartVariableItem[]}>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [initialValues, setInitialValues] = useState<StartVariableItem[]>([])
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
@@ -26,9 +26,10 @@ const VariableConfigModal = forwardRef<VariableEditModalRef, VariableEditModalPr
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
|
||||
const handleOpen = (values: StartVariableItem[]) => {
|
||||
setVisible(true);
|
||||
form.setFieldsValue({variables: values})
|
||||
setInitialValues([...values])
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
@@ -59,18 +60,18 @@ const VariableConfigModal = forwardRef<VariableEditModalRef, VariableEditModalPr
|
||||
form={form}
|
||||
layout="horizontal"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
initialValues={{ variables: variables }}
|
||||
>
|
||||
<Form.List name="variables">
|
||||
{(fields) => (
|
||||
<>
|
||||
{fields.map(({ name }, index) => {
|
||||
const field = variables[index]
|
||||
const field = initialValues[index]
|
||||
return (
|
||||
<Form.Item
|
||||
key={name}
|
||||
name={[name, 'value']}
|
||||
label={field.type === 'boolean' ? undefined : `${field.name}·${field.description}`}
|
||||
valuePropName={field.type === 'boolean' ? 'checked' : 'value'}
|
||||
rules={[
|
||||
{ required: field.required, message: field.type === 'boolean' ? t('common.pleaseSelect') : t('common.pleaseEnter') },
|
||||
]}
|
||||
|
||||
@@ -138,6 +138,15 @@ export const useWorkflowGraph = ({
|
||||
})
|
||||
graphRef.current.addEdges(edgeList.filter(vo => vo !== null))
|
||||
}
|
||||
|
||||
// 初始化完成后,将节点展示在可视区域内
|
||||
if (nodes.length > 0 || edges.length > 0) {
|
||||
setTimeout(() => {
|
||||
if (graphRef.current) {
|
||||
graphRef.current.centerContent()
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
|
||||
const saveState = () => {
|
||||
|
||||
@@ -75,7 +75,7 @@ export interface WorkflowConfig {
|
||||
}
|
||||
|
||||
export interface VariableEditModalRef {
|
||||
handleOpen: (values?: StartVariableItem) => void;
|
||||
handleOpen: (values: StartVariableItem[]) => void;
|
||||
}
|
||||
export interface StartVariableItem {
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user