From 2b30a69b94b74ddb0a237348071eb09990a98310 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 25 Dec 2025 11:54:31 +0800 Subject: [PATCH] feat(web): 1. user memory; 2. update workspace's model config --- web/src/api/memory.ts | 5 +- web/src/api/workspaces.ts | 5 +- web/src/i18n/en.ts | 11 ++ web/src/i18n/zh.ts | 14 +- web/src/views/SelfReflectionEngine/index.tsx | 4 +- .../UserMemory/components/ConfigModal.tsx | 18 +- web/src/views/UserMemoryDetail/Neo4j.tsx | 155 ++++++++++-------- .../components/MemoryInsight.tsx | 11 +- .../components/NodeStatistics.tsx | 18 +- web/src/views/UserMemoryDetail/types.ts | 3 + 10 files changed, 155 insertions(+), 89 deletions(-) diff --git a/web/src/api/memory.ts b/web/src/api/memory.ts index 6d75aad3..1a496961 100644 --- a/web/src/api/memory.ts +++ b/web/src/api/memory.ts @@ -131,6 +131,9 @@ export const getEmotionHealth = (group_id: string) => { export const getEmotionSuggestions = (group_id: string) => { return request.post(`/memory/emotion/suggestions`, { group_id, limit: 20 }) } +export const analyticsRefresh = (end_user_id: string) => { + return request.post('/memory-storage/analytics/generate_cache', { end_user_id }) +} /*************** end 用户记忆 相关接口 ******************************/ @@ -189,7 +192,7 @@ export const updateMemoryReflectionConfig = (values: SelfReflectionEngineConfig) return request.post('/memory/reflection/save', values) } // 反思引擎-试运行 -export const pilotRunMemoryReflectionConfig = (values: { config_id: number | string; dialogue_text: string; }) => { +export const pilotRunMemoryReflectionConfig = (values: { config_id: number | string; language_type: string; }) => { return request.get('/memory/reflection/run', values) } diff --git a/web/src/api/workspaces.ts b/web/src/api/workspaces.ts index 428b1280..4e78194b 100644 --- a/web/src/api/workspaces.ts +++ b/web/src/api/workspaces.ts @@ -1,5 +1,6 @@ import { request } from '@/utils/request' import type { SpaceModalData } from '@/views/SpaceManagement/types' +import type { ConfigModalData } from '@/views/UserMemory/types' // 空间列表 export const getWorkspaces = () => { @@ -22,6 +23,6 @@ export const getWorkspaceModels = () => { return request.get(`/workspaces/workspace_models`) } // 更新空间模型配置 -export const updateWorkspaceModels = () => { - return request.post(`/workspaces/workspace_models`) +export const updateWorkspaceModels = (data: ConfigModalData) => { + return request.put(`/workspaces/workspace_models`, data) } diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index d16c9e67..f342d8fd 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -329,6 +329,7 @@ export const en = { publicApiCannotRefreshToken: 'Public API cannot refresh token', refreshTokenNotExist: 'Refresh token does not exist', reset: 'Reset', + refresh: 'Refresh' }, model: { searchPlaceholder: 'search model…', @@ -1039,6 +1040,16 @@ export const en = { MemorySummary: 'Episodic Memory', Statement: 'Emotional Memory', ExtractedEntity: 'Short-term Memory', + + PERCEPTUAL_MEMORY: 'Perceptual Memory', + WORKING_MEMORY: 'Working Memory', + SHORT_TERM_MEMORY: 'Shot Term Memory', + LONG_TERM_MEMORY: 'Long Term Memory', + EXPLICIT_MEMORY: 'Explicit Memory', + IMPLICIT_MEMORY: 'Implicit Memory', + EMOTIONAL_MEMORY: 'Emotional Memory', + EPISODIC_MEMORY: 'Episodic Memory', + endUserProfile: 'Core Profile', editEndUserProfile: 'Edit', name: 'Name', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index ef15d06b..0225eab1 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -795,7 +795,8 @@ export const zh = { logoutApiCannotRefreshToken: '退出登录接口不能刷新token', publicApiCannotRefreshToken: '公共接口不能刷新token', refreshTokenNotExist: '刷新token不存在', - reset: '重置' + reset: '重置', + refresh: '刷新' }, product: { applicationManagement: '应用管理', @@ -1111,10 +1112,21 @@ export const zh = { nodeStatistics: '记忆分类', total: '总计', + Chunk: '长期记忆', MemorySummary: '情景记忆', Statement: '情绪记忆', ExtractedEntity: '短期记忆', + + PERCEPTUAL_MEMORY: '感知记忆', + WORKING_MEMORY: '工作记忆', + SHORT_TERM_MEMORY: '短期记忆', + LONG_TERM_MEMORY: '长期记忆', + EXPLICIT_MEMORY: '显性记忆', + IMPLICIT_MEMORY: '隐性记忆', + EMOTIONAL_MEMORY: '情绪记忆', + EPISODIC_MEMORY: '情景记忆', + endUserProfile: '核心档案', editEndUserProfile: '编辑', name: '姓名', diff --git a/web/src/views/SelfReflectionEngine/index.tsx b/web/src/views/SelfReflectionEngine/index.tsx index 12196163..5af88b35 100644 --- a/web/src/views/SelfReflectionEngine/index.tsx +++ b/web/src/views/SelfReflectionEngine/index.tsx @@ -10,6 +10,7 @@ import type { ConfigForm, Result, ReflexionData, MemoryVerify, QualityAssessment import CustomSelect from '@/components/CustomSelect'; import { getModelListUrl } from '@/api/models' import Tag from '@/components/Tag' +import { useI18n } from '@/store/locale'; const configList = [ // 启用反思引擎 @@ -78,6 +79,7 @@ const SelfReflectionEngine: React.FC = () => { const [loading, setLoading] = useState(false) const [runLoading, setRunLoading] = useState(false) const [result, setResult] = useState(null) + const { language } = useI18n() const values = Form.useWatch([], form); @@ -135,7 +137,7 @@ const SelfReflectionEngine: React.FC = () => { .then(() => { pilotRunMemoryReflectionConfig({ config_id: id, - dialogue_text: t('reflectionEngine.exampleText') + language_type: language }) .then((res) => { setResult(res as Result) diff --git a/web/src/views/UserMemory/components/ConfigModal.tsx b/web/src/views/UserMemory/components/ConfigModal.tsx index d10b9530..86ea8f19 100644 --- a/web/src/views/UserMemory/components/ConfigModal.tsx +++ b/web/src/views/UserMemory/components/ConfigModal.tsx @@ -41,15 +41,15 @@ const ConfigModal = forwardRef((_props, ref) => { .validateFields() .then(() => { setLoading(true) - // updateWorkspaceModels(values as ConfigModalData) - // .then(() => { - // setLoading(false) - // handleClose() - // message.success(t('common.createSuccess')) - // }) - // .catch(() => { - // setLoading(false) - // }); + updateWorkspaceModels(values) + .then(() => { + setLoading(false) + handleClose() + message.success(t('common.updateSuccess')) + }) + .catch(() => { + setLoading(false) + }); handleClose() }) diff --git a/web/src/views/UserMemoryDetail/Neo4j.tsx b/web/src/views/UserMemoryDetail/Neo4j.tsx index b26a9069..784f962e 100644 --- a/web/src/views/UserMemoryDetail/Neo4j.tsx +++ b/web/src/views/UserMemoryDetail/Neo4j.tsx @@ -1,7 +1,7 @@ -import { type FC, useEffect, useState } from 'react' +import { type FC, useEffect, useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import clsx from 'clsx' -import { Row, Col, Skeleton } from 'antd' +import { Row, Col, Skeleton, Flex, Button } from 'antd' import { useParams } from 'react-router-dom' import aboutUs from '@/assets/images/userMemory/aboutUs.svg' import down from '@/assets/images/userMemory/down.svg' @@ -10,7 +10,9 @@ import PieCard from './components/PieCard' import RbCard from '@/components/RbCard/Card' import { getUserSummary, + analyticsRefresh } from '@/api/memory' +import type { MemoryInsightRef } from './types' import RelationshipNetwork from './components/RelationshipNetwork' import MemoryInsight from './components/MemoryInsight' import Empty from '@/components/Empty' @@ -45,10 +47,12 @@ const Title: FC = ({ type, title, icon, t, expanded, onClick }) => ( const Neo4j: FC = () => { const { t } = useTranslation() const { id } = useParams() + const memoryInsightRef = useRef(null) const [expanded, setExpanded] = useState(['aboutUs', 'interestDistribution', 'importantRelationships', 'importantMomentsInLife']) const [summary, setSummary] = useState(null) const [loading, setLoading] = useState>({ summary: false, + refresh: false }) useEffect(() => { @@ -70,73 +74,96 @@ const Neo4j: FC = () => { setLoading(prev => ({ ...prev, summary: false })) }) } + const handleRefresh = () => { + setLoading(prev => ({ ...prev, refresh: true })) + analyticsRefresh(id as string) + .then(res => { + const response = res as { insight_success: boolean; summary_success: boolean; } + if (response.insight_success) { + memoryInsightRef.current?.getInsightReport() + } + if (response.summary_success) { + getSummary() + } + }) + .finally(() => { + setLoading(prev => ({ ...prev, refresh: false })) + }) + } return ( - - - - - - - - - {/* 关于我 */} - <> - - {expanded.includes('aboutUs') && ( - <> - {loading.summary - ? <Skeleton className="rb:mt-4" /> - : summary - ? <div className="rb:font-regular rb:leading-5.5 rb:pt-4"> - {summary || '-'} - </div> - : <Empty size={88} className="rb:mt-12 rb:mb-20.25" /> - } - </> - )} - </> + <div> + <Flex justify="flex-end"> + <Button type="primary" loading={loading.refresh} className="rb:mb-3" onClick={handleRefresh}> + {t('common.refresh')} + </Button> + </Flex> + <Row gutter={[16, 16]} className="rb:pb-6"> + <Col span={8}> + <Row gutter={[16, 16]}> + <Col span={24}> + <EndUserProfile /> + </Col> + <Col span={24}> + <RbCard> + {/* 关于我 */} + <> + <Title + type="aboutUs" + title={t('userMemory.aboutMe')} + icon={aboutUs} + t={t} + expanded={expanded.includes('aboutUs')} + onClick={handleTitleClick} + /> + {expanded.includes('aboutUs') && ( + <> + {loading.summary + ? <Skeleton className="rb:mt-4" /> + : summary + ? <div className="rb:font-regular rb:leading-5.5 rb:pt-4"> + {summary || '-'} + </div> + : <Empty size={88} className="rb:mt-12 rb:mb-20.25" /> + } + </> + )} + </> - {/* 兴趣分布 */} - <> - <Title - type="interestDistribution" - title={t('userMemory.interestDistribution')} - icon={interestDistribution} - t={t} - expanded={expanded.includes('interestDistribution')} - onClick={handleTitleClick} - /> + {/* 兴趣分布 */} + <> + <Title + type="interestDistribution" + title={t('userMemory.interestDistribution')} + icon={interestDistribution} + t={t} + expanded={expanded.includes('interestDistribution')} + onClick={handleTitleClick} + /> - {expanded.includes('interestDistribution') && ( - <PieCard /> - )} - </> - </RbCard> - </Col> + {expanded.includes('interestDistribution') && ( + <PieCard /> + )} + </> + </RbCard> + </Col> + </Row> + </Col> + <Col span={16}> + <Row gutter={[16, 16]}> + <Col span={24}> + <NodeStatistics /> + </Col> + {/* 记忆洞察 */} + <Col span={24}> + <MemoryInsight ref={memoryInsightRef} /> + </Col> + {/* 关系网络 + 记忆详情 */} + <RelationshipNetwork /> + </Row> + </Col> </Row> - </Col> - <Col span={16}> - <Row gutter={[16, 16]}> - <Col span={24}> - <NodeStatistics /> - </Col> - {/* 记忆洞察 */} - <Col span={24}> - <MemoryInsight /> - </Col> - {/* 关系网络 + 记忆详情 */} - <RelationshipNetwork /> - </Row> - </Col> - </Row> + </div> ) } export default Neo4j \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/MemoryInsight.tsx b/web/src/views/UserMemoryDetail/components/MemoryInsight.tsx index 1471a2d3..983e8a41 100644 --- a/web/src/views/UserMemoryDetail/components/MemoryInsight.tsx +++ b/web/src/views/UserMemoryDetail/components/MemoryInsight.tsx @@ -1,4 +1,4 @@ -import { type FC, useEffect, useState } from 'react' +import { type FC, useEffect, useState, forwardRef, useImperativeHandle } from 'react' import { useTranslation } from 'react-i18next' import { useParams } from 'react-router-dom' import { Skeleton } from 'antd'; @@ -7,8 +7,9 @@ import Empty from '@/components/Empty'; import { getMemoryInsightReport, } from '@/api/memory' +import type { MemoryInsightRef } from '../types' -const MemoryInsight:FC = () => { +const MemoryInsight = forwardRef<MemoryInsightRef>((_props, ref) => { const { t } = useTranslation() const { id } = useParams() const [loading, setLoading] = useState<boolean>(false) @@ -31,6 +32,10 @@ const MemoryInsight:FC = () => { setLoading(false) }) } + // 暴露给父组件的方法 + useImperativeHandle(ref, () => ({ + getInsightReport, + })); return ( <RbCard title={t('userMemory.memoryInsight')} @@ -51,5 +56,5 @@ const MemoryInsight:FC = () => { } </RbCard> ) -} +}) export default MemoryInsight \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/NodeStatistics.tsx b/web/src/views/UserMemoryDetail/components/NodeStatistics.tsx index 82c633a0..7bfd19f3 100644 --- a/web/src/views/UserMemoryDetail/components/NodeStatistics.tsx +++ b/web/src/views/UserMemoryDetail/components/NodeStatistics.tsx @@ -29,9 +29,11 @@ const NodeStatistics: FC = () => { if (!id) return setLoading(true) getNodeStatistics(id).then((res) => { - const response = res as { nodes: NodeStatisticsItem[], total: number } - setData(response.nodes) - setTotal(response.total) + const response = res as NodeStatisticsItem[] + setData(response) + // 计算count总计 + const totalCount = response.reduce((sum, item) => sum + (item.count || 0), 0) + setTotal(totalCount) setLoading(false) }) .finally(() => { @@ -40,7 +42,7 @@ const NodeStatistics: FC = () => { } const handleViewDetail = (type: string) => { switch (type) { - case 'Statement': + case 'EMOTIONAL_MEMORY': navigate(`/statement/${id}`) break } @@ -56,19 +58,19 @@ const NodeStatistics: FC = () => { > {loading ? <Skeleton /> - : data.length > 0 - ? <div className={`rb:w-full rb:grid rb:grid-cols-${data.length} rb:gap-2`}> + : data && data.length > 0 + ? <div className={`rb:w-full rb:grid rb:grid-cols-3 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' + 'rb:cursor-pointer': vo.type === 'EMOTIONAL_MEMORY' })} 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 + {vo.type === 'EMOTIONAL_MEMORY' && <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> diff --git a/web/src/views/UserMemoryDetail/types.ts b/web/src/views/UserMemoryDetail/types.ts index 10b0c2fa..8bbf60b6 100644 --- a/web/src/views/UserMemoryDetail/types.ts +++ b/web/src/views/UserMemoryDetail/types.ts @@ -130,4 +130,7 @@ export interface EndUser { } export interface EndUserProfileModalRef { handleOpen: (vo: EndUser) => void; +} +export interface MemoryInsightRef { + getInsightReport: () => void } \ No newline at end of file