diff --git a/web/package.json b/web/package.json index 9d157982..e28e8b56 100644 --- a/web/package.json +++ b/web/package.json @@ -30,6 +30,7 @@ "dayjs": "^1.11.18", "echarts": "^5.6.0", "echarts-for-react": "^3.0.2", + "echarts-wordcloud": "^2.1.0", "i18next": "^25.6.0", "js-yaml": "^4.1.1", "lexical": "^0.39.0", diff --git a/web/src/api/memory.ts b/web/src/api/memory.ts index 1ead7548..39136da3 100644 --- a/web/src/api/memory.ts +++ b/web/src/api/memory.ts @@ -134,9 +134,53 @@ export const getEmotionSuggestions = (group_id: string) => { export const analyticsRefresh = (end_user_id: string) => { return request.post('/memory-storage/analytics/generate_cache', { end_user_id }) } +// 遗忘 export const getForgetStats = (group_id: string) => { return request.get(`/memory/forget/stats`, { group_id }) } +// 隐性记忆-偏好 +export const getImplicitPreferences = (end_user_id: string) => { + return request.get(`/memory/implicit-memory/preferences/${end_user_id}`) +} +// 隐性记忆-核心特质 +export const getImplicitPortrait = (end_user_id: string) => { + return request.get(`/memory/implicit-memory/portrait/${end_user_id}`) +} +// 隐性记忆-兴趣领域分布 +export const getImplicitInterestAreas = (end_user_id: string) => { + return request.get(`/memory/implicit-memory/interest-areas/${end_user_id}`) +} +// 隐性记忆-用户习惯分析 +export const getImplicitHabits = (end_user_id: string) => { + return request.get(`/memory/implicit-memory/habits/${end_user_id}`) +} +// 短期记忆 +export const getShortTerm = (end_user_id: string) => { + return request.get(`/memory/short/short_term`, { end_user_id }) +} +// 感知记忆-视觉记忆 +export const getPerceptualLastVisual = (end_user: string) => { + return request.get(`/memory/perceptual/${end_user}/last_visual`) +} +// 感知记忆-音频记忆 +export const getPerceptualLastListen = (end_user: string) => { + return request.get(`/memory/perceptual/${end_user}/last_listen`) +} +// 感知记忆-文本记忆 +export const getPerceptualLastText = (end_user: string) => { + return request.get(`/memory/perceptual/${end_user}/last_text`) +} +// 感知记忆-感知记忆时间线 +export const getPerceptualTimeline = (end_user: string) => { + return request.get(`/memory/perceptual/${end_user}/timeline`) +} +// 情景记忆-总览 +export const getEpisodicOverview = (data: { end_user_id: string; time_range: string; episodic_type: string; } ) => { + return request.post(`/memory-storage/classifications/episodic-memory`, data) +} +export const getEpisodicDetail = (data: { end_user_id: string; summary_id: string; } ) => { + return request.post(`/memory-storage/classifications/episodic-memory-details`, data) +} /*************** end 用户记忆 相关接口 ******************************/ diff --git a/web/src/assets/images/userMemory/shortTerm.png b/web/src/assets/images/userMemory/shortTerm.png new file mode 100644 index 00000000..37a880ec Binary files /dev/null and b/web/src/assets/images/userMemory/shortTerm.png differ diff --git a/web/src/assets/images/userMemory/up_border.svg b/web/src/assets/images/userMemory/up_border.svg new file mode 100644 index 00000000..a7fe9978 --- /dev/null +++ b/web/src/assets/images/userMemory/up_border.svg @@ -0,0 +1,14 @@ + + + 下拉备份 + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/userMemory/view.svg b/web/src/assets/images/userMemory/view.svg new file mode 100644 index 00000000..642841ae --- /dev/null +++ b/web/src/assets/images/userMemory/view.svg @@ -0,0 +1,19 @@ + + + 查看 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/userMemory/view_hover.svg b/web/src/assets/images/userMemory/view_hover.svg new file mode 100644 index 00000000..642841ae --- /dev/null +++ b/web/src/assets/images/userMemory/view_hover.svg @@ -0,0 +1,19 @@ + + + 查看 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 75afc6fc..c7c9a937 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1223,6 +1223,23 @@ export const en = { growth_trajectory: 'Growth Trajectory', personality: 'Personality Traits', core_values: 'Core Values', + + Statement_emotion_keywords: 'Emotion Keywords', + Statement_emotion_type: 'Emotion Type', + Statement_emotion_subject: 'Emotion Subject', + Statement_importance_score: 'Importance Score', + + ExtractedEntity_description: 'Description', + ExtractedEntity_name: 'Content', + ExtractedEntity_entity_type: 'Type', + ExtractedEntity_created_at: 'Created At', + ExtractedEntity_aliases: 'Aliases', + ExtractedEntity_connect_strngth: 'Connection Strength', + ExtractedEntity_importance_score: 'Importance Score', + + associative_memory: 'Associative Memory', + unix: 'items', + completeMemory: 'Complete Memory', }, space: { createSpace: 'Create Space', @@ -2151,15 +2168,15 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re create_time: 'Creation Time', }, forgetDetail: { - title: 'The forgetting management system helps AI intelligently manage memory lifecycle by automatically identifying low-value memories, setting forgetting strategies, and performing regular cleanup to optimize memory storage space and improve retrieval efficiency.', + title: 'The forgetting management system helps AI intelligently manage memory lifecycle by automatically identifying low-value memories, setting forgetting strategies, and executing regular cleanup to optimize memory storage space and improve retrieval efficiency.', overviewTitle: 'Core Metrics Overview', totalMemory: 'Total Memory', MemoryHealth: 'Memory Health', - riskOfForgetting: 'Risk of Forgetting', - statement_count: 'Statement', - entity_count: 'Entity', - summary_count: 'Summary', - chunk_count: 'Chunk', + riskOfForgetting: 'Forgetting Risk', + statement_count: 'Statements', + entity_count: 'Entities', + summary_count: 'Summaries', + chunk_count: 'Chunks', healthStatus: 'Health Status', average: 'Average Activation Value', threshold: 'Threshold Reference:', @@ -2174,7 +2191,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re low_activation_nodes: 'Forgetting Zone', health_nodes: 'Healthy Zone', average_activation: 'Average Activation Value', - merged_count: 'Daily Merged Nodes Count', + merged_count: 'Daily Merged Node Count', pending_nodes: 'Risk Node Forgetting Pool', content_summary: 'Content Summary', @@ -2182,5 +2199,75 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re last_access_time: 'Last Activation Time', activation_value: 'Current Activation Value', }, + episodicDetail: { + title: 'Record every important scene you have truly experienced', + total_all: 'Total Episodic Memories', + all: "All", + today: 'Today', + this_week: 'This Week', + this_month: 'This Month', + conversation: "Conversation", + project_work: "Project/Work", + learning: "Learning", + decision: "Decision", + important_event: "Important Event", + titleKeywordPlaceholder: 'Search episode title or content', + curResult: 'Current Filter Results', + unix: 'items', + created: 'Occurrence Time', + episodic_type: 'Episode Type', + involved_objects: 'Involved Objects', + content_records: 'Episode Content Records', + emotion: 'Emotion and State Records', + }, + implicitDetail: { + title: 'The invisible forces that shaped me', + preferences: 'My Subconscious Preferences', + preferencesDetail: 'Association Network', + portraitTitle: 'My Subconscious Portrait', + portraitSubTitle: 'Personalized insights generated by AI based on your preference tags', + portrait: 'Core Traits', + aesthetic: 'Aesthetic Driven', + creativity: 'Creative Thinking', + literature: 'Cultural Sensitivity', + technology: 'Technology Affinity', + interestAreas: 'Interest Area Distribution', + art: 'Art & Design', + music: 'Music & Culture', + tech: 'Technology & Future', + lifestyle: 'Lifestyle', + habits: 'User Habit Analysis', + habitsSubTitle: 'Habit characteristics identified based on your behavior patterns', + context_details: 'Preference Details', + supporting_evidence: 'Preference Source', + specific_examples: 'Source', + }, + shortTermDetail: { + title: 'Short-term memory is the "workbench" of the AI system, connecting instant conversations with long-term knowledge bases. Through real-time capture, deep retrieval, intelligent extraction and filtering transformation, temporary unstructured information is converted into valuable long-term knowledge.', + retrieval_number: 'Retrieval Count', + entity: 'Extracted Entities', + long_term_number: 'Long-term Candidates', + shortTermTitle: 'Deep Retrieval & Extended Answer Area', + shortTermSubTitle: 'Stores deep information retrieval performed to answer questions and the extended answers generated from it, including original questions, retrieved information, and generated answers.', + longTermTitle: 'Long-term Memory Candidate Pool', + longTermSubTitle: 'Aggregates short-term memory, filters and prepares content for storage in long-term memory. This is the "transfer station" and "filter" from short-term to long-term memory.', + answer: 'Answer', + query: 'Question', + noAnswer: 'No reply yet', + }, + perceptualDetail: { + last_visual: 'Visual Perception Stream', + last_listen: 'Auditory Perception Stream', + last_text: 'Text Perception', + summary: 'Summary', + keywords: 'Keywords', + topic: 'Topic', + domain: 'Domain', + scene: 'Scene', + speaker_count: 'Number of Speakers', + section_count: 'Number of Sections', + timeLine: 'Perception Timeline', + lastInfo: 'Real-time Perception Dashboard', + } }, }; diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index b05b6224..d858b643 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1304,6 +1304,23 @@ export const zh = { growth_trajectory: '成长轨迹', personality: '性格特点', core_values: '核心价值观', + + Statement_emotion_keywords: '情感关键词', + Statement_emotion_type: '情感类型', + Statement_emotion_subject: '情感主体', + Statement_importance_score: '重要性评分', + + ExtractedEntity_description: '描述', + ExtractedEntity_name: '内容', + ExtractedEntity_entity_type: '类型', + ExtractedEntity_created_at: '创建时间', + ExtractedEntity_aliases: '别名', + ExtractedEntity_connect_strngth: '连接强度', + ExtractedEntity_importance_score: '重要性评分', + + associative_memory: '关联记忆', + unix: '个', + completeMemory: '完整记忆', }, space: { createSpace: '创建空间', @@ -2282,5 +2299,75 @@ export const zh = { last_access_time: '最后激活时间', activation_value: '当前激活值', }, + episodicDetail: { + title: '记录你真实经历过的每一个重要场景', + total_all: '情景记忆总数', + all: "全部", + today: '今天', + this_week: '本周', + this_month: '本月', + conversation: "对话", + project_work: "项目/工作", + learning: "学习", + decision: "决策", + important_event: "重要事件", + titleKeywordPlaceholder: '搜索情景标题或内容', + curResult: '当前筛选结果', + unix: '条', + created: '发生时间', + episodic_type: '情景类型', + involved_objects: '涉及对象', + content_records: '情景内容记录', + emotion: '情绪与状态记录', + }, + implicitDetail: { + title: '那些塑造了我的无形力量', + preferences: '我的潜意识偏好', + preferencesDetail: '的联想网络', + portraitTitle: '我的潜意识画像', + portraitSubTitle: '基于您的偏好标签,AI为您生成的个性化洞察', + portrait: '核心特质', + aesthetic: '审美驱动', + creativity: '创造性思维', + literature: '文化敏感度', + technology: '技术亲和力', + interestAreas: '兴趣领域分布', + art: '艺术与设计', + music: '音乐与文化', + tech: '科技与未来', + lifestyle: '生活方式', + habits: '用户习惯分析', + habitsSubTitle: '基于您的行为模式识别的习惯特征', + context_details: '偏好详情', + supporting_evidence: '偏好来源', + specific_examples: '来源', + }, + shortTermDetail: { + title: '短期记忆是AI系统的"工作台",连接即时对话与长期知识库。通过实时捕获、深度检索、智能提取和筛选转化,将临时的非结构化信息转化为有价值的长期知识。', + retrieval_number: '检索次数', + entity: '提取实体', + long_term_number: '长期候选', + shortTermTitle: '深度检索与扩展答案区', + shortTermSubTitle: '存放为回答问题而进行的深度信息检索和由此生成的扩展答案,包含原始问题、检索信息和生成答案。', + longTermTitle: '长期记忆候选池', + longTermSubTitle: '聚合短期记忆,筛选并准备存入长期记忆的内容。这是从短时记忆到长时记忆的"中转站"和"过滤器"。', + answer: '回答', + query: '问题', + noAnswer: '暂无回复', + }, + perceptualDetail: { + last_visual: '视觉感知流', + last_listen: '听觉感知流', + last_text: '文本感知', + summary: '摘要', + keywords: '关键词', + topic: '主题', + domain: '领域', + scene: '场景', + speaker_count: '对话人数', + section_count: '段落数', + timeLine: '感知时间线', + lastInfo: '实时感知仪表盘', + } }, } \ No newline at end of file diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx index f3bd9c2d..bc2f61e6 100644 --- a/web/src/routes/index.tsx +++ b/web/src/routes/index.tsx @@ -61,6 +61,7 @@ const componentMap: Record>> = StatementDetail: lazy(() => import('@/views/UserMemoryDetail/pages/StatementDetail')), ForgetDetail: lazy(() => import('@/views/UserMemoryDetail/pages/ForgetDetail')), MemoryNodeDetail: lazy(() => import('@/views/UserMemoryDetail/pages/index')), + GraphDetail: lazy(() => import('@/views/UserMemoryDetail/pages/GraphDetail')), SelfReflectionEngine: lazy(() => import('@/views/SelfReflectionEngine')), OrderPayment: lazy(() => import('@/views/OrderPayment')), OrderHistory: lazy(() => import('@/views/OrderHistory')), diff --git a/web/src/routes/routes.json b/web/src/routes/routes.json index ca6a3271..2f332b72 100644 --- a/web/src/routes/routes.json +++ b/web/src/routes/routes.json @@ -44,7 +44,8 @@ { "path": "/conversation/:token", "element": "Conversation" }, { "path": "/user-memory/neo4j/:id", "element": "Neo4jUserMemoryDetail" }, { "path": "/statement/:id", "element": "StatementDetail" }, - { "path": "/user-memory/detail/:id/:type", "element": "MemoryNodeDetail" } + { "path": "/user-memory/detail/:id/:type", "element": "MemoryNodeDetail" }, + { "path": "/graph/:id", "element": "GraphDetail" } ] }, { diff --git a/web/src/views/UserMemory/index.tsx b/web/src/views/UserMemory/index.tsx index af7db5e1..7065f036 100644 --- a/web/src/views/UserMemory/index.tsx +++ b/web/src/views/UserMemory/index.tsx @@ -104,15 +104,15 @@ export default function UserMemory() { return (
- + {countList.map(key => ( -
-
+
+
{countData[key] || 0}{key === 'avgInteractionTime' ? 's' : ''} - +
-
{t(`userMemory.${key}`)}
+
{t(`userMemory.${key}`)}
))} @@ -140,22 +140,22 @@ export default function UserMemory() { return (
handleViewDetail(end_user.id)} >
-
{name[0]}
-
+
{name[0]}
+
{name || '-'}
-
+
-
{memory_num.total || 0}
-
{t(`userMemory.knowledgeEntryCount`)}
+
{memory_num.total || 0}
+
{t(`userMemory.knowledgeEntryCount`)}
diff --git a/web/src/views/UserMemoryDetail/Neo4j.tsx b/web/src/views/UserMemoryDetail/Neo4j.tsx index 2425b503..a6dedd8b 100644 --- a/web/src/views/UserMemoryDetail/Neo4j.tsx +++ b/web/src/views/UserMemoryDetail/Neo4j.tsx @@ -1,4 +1,4 @@ -import { type FC, useEffect, useRef, useState } from 'react' +import { type FC, useRef, useState } from 'react' import { useParams } from 'react-router-dom' import { Row, Col, Space, Button } from 'antd' import { useTranslation } from 'react-i18next'; diff --git a/web/src/views/UserMemoryDetail/components/Habits.tsx b/web/src/views/UserMemoryDetail/components/Habits.tsx new file mode 100644 index 00000000..746d7164 --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/Habits.tsx @@ -0,0 +1,85 @@ +import { type FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Skeleton, Space, Progress } from 'antd'; +import RbCard from '@/components/RbCard/Card' +import Empty from '@/components/Empty' +import { + getImplicitHabits, +} from '@/api/memory' + +interface HabitsItem { + habit_description: string; + frequency_pattern: string; + time_context: string; + confidence_level: string; + supporting_summaries: string[]; + first_observed: string; + last_observed: string; + is_current: boolean; + specific_examples: string[]; +} + +const Habits: FC = () => { + const { t } = useTranslation() + const { id } = useParams() + const [loading, setLoading] = useState(false) + const [data, setData] = useState([]) + + useEffect(() => { + if (!id) return + getData() + }, [id]) + + // 记忆洞察 + const getData = () => { + if (!id) return + setLoading(true) + getImplicitHabits(id).then((res) => { + const response = res as HabitsItem[] + setData(response) + setLoading(false) + }) + .finally(() => { + setLoading(false) + }) + } + + return ( + <> +
{t('implicitDetail.habits')}
+
{t('implicitDetail.habitsSubTitle')}
+ + {loading + ? + : data.length === 0 + ? + : + {data.map((vo, voIdx) => ( +
+
+
+
{vo.habit_description}
+
{vo.time_context}
+
+
{vo.confidence_level}%
+
+ + {vo.specific_examples.length > 0 && <> +
{t('implicitDetail.specific_examples')}
+
+ {vo.specific_examples.map((item, index) => ( +
- {item}
+ ))} +
+ } + +
+ ))} +
+ } +
+ + ) +} +export default Habits \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/InterestAreas.tsx b/web/src/views/UserMemoryDetail/components/InterestAreas.tsx new file mode 100644 index 00000000..357336f4 --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/InterestAreas.tsx @@ -0,0 +1,75 @@ +import { type FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Skeleton, Progress } from 'antd'; +import RbCard from '@/components/RbCard/Card' +import { + getImplicitInterestAreas, +} from '@/api/memory' + +interface Item { + category_name: string; + percentage: number; + evidence: string[]; + trending_direction: string | null; +} +interface InterestAreasItem { + user_id: string; + analysis_timestamp: number | string; + total_summaries_analyzed: number; + tech: Item; + lifestyle: Item; + music: Item; + art: Item; +} + +const InterestAreas: FC = () => { + const { t } = useTranslation() + const { id } = useParams() + const [loading, setLoading] = useState(false) + const [data, setData] = useState({} as InterestAreasItem) + + useEffect(() => { + if (!id) return + getData() + }, [id]) + + // 记忆洞察 + const getData = () => { + if (!id) return + setLoading(true) + getImplicitInterestAreas(id).then((res) => { + const response = res as InterestAreasItem + setData(response) + setLoading(false) + }) + .finally(() => { + setLoading(false) + }) + } + + return ( + + {loading + ? + :
+ {(['art', 'music', 'tech', 'lifestyle'] as const).map((key) => { + return ( +
+
+
{t(`implicitDetail.${key}`)}
+ {data[key]?.percentage ?? 0}% +
+ +
+ ) + })} +
+ } +
+ ) +} +export default InterestAreas \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx b/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx new file mode 100644 index 00000000..d3788a74 --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx @@ -0,0 +1,120 @@ +import { type FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Skeleton, Space, Tooltip, Image } from 'antd'; +import RbCard from '@/components/RbCard/Card' +import { + getPerceptualLastVisual, + getPerceptualLastListen, + getPerceptualLastText, +} from '@/api/memory' + +interface PerceptualLastInfoItem { + id: string; + file_name: string; + file_ext: string; + file_path: string; + storage_type: number; + summary: string; + keywords: string[]; + topic: string; + domain: string; + created_time: number | string; + scene: string[] + speaker_count: number; + section_count: number; +} + +const KEYS = { + last_visual: ['summary', 'keywords', 'topic', 'domain', 'scene'], + last_listen: ['summary', 'keywords', 'topic', 'domain', 'speaker_count'], + last_text: ['summary', 'keywords', 'topic', 'domain', 'section_count'], +} + +const PerceptualLastInfo: FC<{ type: 'last_visual' | 'last_listen' | 'last_text' }> = ({ type }) => { + const { t } = useTranslation() + const { id } = useParams() + const [loading, setLoading] = useState(false) + const [data, setData] = useState({} as PerceptualLastInfoItem) + + useEffect(() => { + if (!id) return + getData() + }, [id, type]) + const getData = () => { + if (!id || !type) return + setLoading(true) + const request = type === 'last_visual' + ? getPerceptualLastVisual(id) + : type === 'last_listen' + ? getPerceptualLastListen(id) + : getPerceptualLastText(id) + request.then((res) => { + const response = res as PerceptualLastInfoItem + setData(response) + setLoading(false) + }) + .finally(() => { + setLoading(false) + }) + } + + return ( + + {loading + ? + :
+
+ {data.file_path ? ( + type === 'last_visual' ? ( + /\.(mp4|webm|ogg|mov)$/i.test(data.file_name) ? ( + + ) : /\.(jpg|jpeg|png|gif|webp|svg)$/i.test(data.file_name) ? ( + {data.file_name} + // {data.file_name} + ) : ( +
{data.file_name}
+ ) + ) : type === 'last_listen' && /\.(mp3|wav|ogg|m4a|aac)$/i.test(data.file_name) ? ( + + ) : ( +
{data.file_name}
+ ) + ) : ( +
No file
+ )} +
+ + {KEYS[type].map(key => { + const value = (data as any)[key] + return ( +
+
{t(`perceptualDetail.${key}`)}
+ {key === 'summary' ? ( + +
+ {typeof value === 'string' ? value : Array.isArray(value) ? value.join('、') : '-'} +
+
+ ) + :
+ {typeof value === 'string' ? value : Array.isArray(value) ? value.join('、') : '-'} +
+ } +
+ ) + })} +
+
+ } +
+ ) +} +export default PerceptualLastInfo \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/Portrait.tsx b/web/src/views/UserMemoryDetail/components/Portrait.tsx new file mode 100644 index 00000000..3164ae06 --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/Portrait.tsx @@ -0,0 +1,77 @@ +import { type FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Skeleton, Progress } from 'antd'; +import RbCard from '@/components/RbCard/Card' +import { + getImplicitPortrait, +} from '@/api/memory' + +interface Item { + dimension_name: string; + percentage: number; + evidence: string[]; + reasoning: string; + confidence_level: string; +} +interface PortraitItem { + user_id: string; + analysis_timestamp: number | string; + total_summaries_analyzed: number; + historical_trends: null; + creativity: Item; + aesthetic: Item; + technology: Item; + literature: Item; +} + +const Portrait: FC = () => { + const { t } = useTranslation() + const { id } = useParams() + const [loading, setLoading] = useState(false) + const [data, setData] = useState({} as PortraitItem) + + useEffect(() => { + if (!id) return + getData() + }, [id]) + + const getData = () => { + if (!id) return + setLoading(true) + getImplicitPortrait(id).then((res) => { + const response = res as PortraitItem + setData(response) + setLoading(false) + }) + .finally(() => { + setLoading(false) + }) + } + + return ( + + {loading + ? + :
+ {(['aesthetic', 'creativity', 'literature', 'technology'] as const).map((key) => { + const item = data[key] as Item + return ( +
+
+
{t(`implicitDetail.${key}`)}
+ {item?.percentage ?? 0}% +
+ +
+ ) + })} +
+ } +
+ ) +} +export default Portrait \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/Preferences.tsx b/web/src/views/UserMemoryDetail/components/Preferences.tsx new file mode 100644 index 00000000..3d1372de --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/Preferences.tsx @@ -0,0 +1,183 @@ +import { type FC, useEffect, useState, useRef, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Row, Col, Skeleton } from 'antd' +import * as echarts from 'echarts' +import 'echarts-wordcloud' + +import Empty from '@/components/Empty' +import RbCard from '@/components/RbCard/Card' +import { getImplicitPreferences } from '@/api/memory' + +interface PreferenceItem { + tag_name: string; + confidence_score: number; + supporting_evidence: string[]; + context_details: string; + created_at: number | string; // TODO + updated_at: number | string; // TODO + conversation_references: string[]; + category: string; +} + +const DEFAULT_COLORS = ['#FF5D34', '#155EEF', '#9C6FFF', '#369F21', '#4DA8FF', '#FF8C00', '#32CD32', '#FF69B4', '#20B2AA', '#DDA0DD'] + +const generateCategoryColors = (categories: string[]) => { + const colors: Record = {} + categories.forEach((category, index) => { + colors[category] = DEFAULT_COLORS[index % DEFAULT_COLORS.length] + }) + return colors +} + +const Preferences: FC = () => { + const { t } = useTranslation() + const { id } = useParams() + const chartRef = useRef(null) + const chartInstance = useRef(null) + const [selectedWord, setSelectedWord] = useState(null) + const [loading, setLoading] = useState(false) + const [data, setData] = useState([]) + + useEffect(() => { + if (!id) return + getData() + }, [id]) + + const getData = () => { + if (!id) { + return + } + setLoading(true) + setSelectedWord(null) + getImplicitPreferences(id) + .then((res) => { + setData(res as PreferenceItem[]) + }) + .finally(() => { + setLoading(false) + }) + } + + const uniqueCategories = [...new Set(data.map(item => item.category).filter(Boolean))] + const categoryColors = generateCategoryColors(uniqueCategories) + + const getCategoryColor = (category: string) => { + return categoryColors[category] || '#4DA8FF' + } + + useEffect(() => { + if (!chartRef.current || !data.length) return + + if (chartInstance.current) { + chartInstance.current.dispose() + } + + chartInstance.current = echarts.init(chartRef.current) + + const wordCloudData = data.map((item, index) => ({ + name: item.tag_name, + value: Math.round(item.confidence_score * 100), + itemIndex: index, + textStyle: { + color: getCategoryColor(item.category) + } + })) + + const option = { + series: [{ + type: 'wordCloud', + gridSize: 8, + sizeRange: [14, 60], + rotationRange: [-45, 45], + shape: 'pentagon', + width: '100%', + height: '100%', + textStyle: { + fontFamily: 'sans-serif', + fontWeight: 'bold' + }, + emphasis: { + textStyle: { + shadowBlur: 10, + shadowColor: '#333' + } + }, + data: wordCloudData + }] + } + + chartInstance.current.setOption(option) + + chartInstance.current.on('click', (params) => { + const clickedIndex = (params.data as any).itemIndex + if (selectedWord !== clickedIndex) { + setSelectedWord(clickedIndex) + } + + // Highlight selected word without redrawing + chartInstance.current?.dispatchAction({ + type: 'highlight', + dataIndex: clickedIndex + }) + }) + + return () => { + if (chartInstance.current) { + chartInstance.current.dispose() + chartInstance.current = null + } + } + }, [data]) + + + console.log(selectedWord, data) + + const detailTitle = useMemo(() => { + return selectedWord !== null && data[selectedWord].tag_name ? <>{data[selectedWord].tag_name}{t('implicitDetail.preferencesDetail')} : '' + }, [selectedWord, data, t]) + + return ( + <> +
{t('forgetDetail.overviewTitle')}
+ + + + {loading + ? + : data && data.length > 0 + ?
+ : + } + + + + + {selectedWord === null + ? + : <> +
{t('implicitDetail.context_details')}
+
{data[selectedWord].context_details}
+ +
{t('implicitDetail.supporting_evidence')}
+ {data[selectedWord].supporting_evidence.map((vo, index) =>
-{vo}
)} + + } +
+ + + + ) +} + +export default Preferences \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx b/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx index db7c9e57..4cce1100 100644 --- a/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx +++ b/web/src/views/UserMemoryDetail/components/RelationshipNetwork.tsx @@ -1,17 +1,18 @@ import React, { type FC, useEffect, useState, useRef, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useParams } from 'react-router-dom' -import { Col, Row } from 'antd' +import { Col, Row, Space, Button } from 'antd' import dayjs from 'dayjs' import RbCard from '@/components/RbCard/Card' import ReactEcharts from 'echarts-for-react' import detailEmpty from '@/assets/images/userMemory/detail_empty.png' -import type { Node, Edge, GraphData } from '../types' +import type { Node, Edge, GraphData, StatementNodeProperties, ExtractedEntityNodeProperties } from '../types' import { getMemorySearchEdges, } from '@/api/memory' import Empty from '@/components/Empty' +import Tag from '@/components/Tag' const colors = ['#155EEF', '#369F21', '#4DA8FF', '#FF5D34', '#9C6FFF', '#FF8A4C', '#8BAEF7', '#FFB048'] const RelationshipNetwork:FC = () => { @@ -136,6 +137,11 @@ const RelationshipNetwork:FC = () => { console.log('selectedNode', selectedNode) + const handleViewAll = () => { + if (!selectedNode) return + window.open(`/#/graph/${selectedNode.id}`); + } + return ( {/* 关系网络 */} @@ -240,8 +246,14 @@ const RelationshipNetwork:FC = () => { title={t('userMemory.memoryDetails')} headerType="borderless" bodyClassName='rb:p-0!' + extra={selectedNode && } > -
+
{!selectedNode ? {
{t('userMemory.created_at')}
-
+
{dayjs(selectedNode?.properties.created_at).format('YYYY-MM-DD HH:mm:ss')}
+ + {selectedNode?.properties.associative_memory > 0 &&
+
{t('userMemory.associative_memory')}
+
+ {selectedNode?.properties.associative_memory} {t('userMemory.unix')}{t('userMemory.associative_memory')} +
+
} + + {selectedNode.label === 'Statement' && <> + {(['emotion_keywords', 'emotion_type', 'emotion_subject', 'importance_score'] as const).map(key => { + const statementProps = selectedNode.properties as StatementNodeProperties; + if ((key === 'emotion_keywords' && statementProps[key]?.length > 0) || statementProps[key]) { + return ( +
+ {t(`userMemory.Statement_${key}`)} +
+ {key === 'emotion_keywords' + ? {statementProps.emotion_keywords.map((vo, index) => {vo})} + : statementProps[key] + } +
+
+ ) + } + return null + })} + } + {selectedNode.label === 'ExtractedEntity' && <> + {(['name', 'entity_type', 'aliases', 'connect_strngth', 'importance_score'] as const).map(key => { + const entityProps = selectedNode.properties as ExtractedEntityNodeProperties; + if (entityProps[key]) { + return ( +
+ {t(`userMemory.ExtractedEntity_${key}`)} +
+ {entityProps[key]} +
+
+ ) + } + return null + })} + }
diff --git a/web/src/views/UserMemoryDetail/components/Timeline.tsx b/web/src/views/UserMemoryDetail/components/Timeline.tsx new file mode 100644 index 00000000..d7b9b273 --- /dev/null +++ b/web/src/views/UserMemoryDetail/components/Timeline.tsx @@ -0,0 +1,82 @@ +import { type FC, useEffect, useState } from 'react' +import clsx from 'clsx' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Skeleton, Progress, Space, Tooltip, Divider } from 'antd'; +import RbCard from '@/components/RbCard/Card' +import { + getPerceptualTimeline +} from '@/api/memory' +import { formatDateTime } from '@/utils/format'; +import Empty from '@/components/Empty' + +interface TimelineItem { + id: string; + perceptual_type: number; + file_path: string; + file_name: string; + summary: string; + storage_type: number; + created_time: string | number; +} + +const KEYS = { + last_visual: ['summary', 'keywords', 'topic', 'domain', 'scene'], + last_listen: ['summary', 'keywords', 'topic', 'domain', 'speaker_count'], + last_text: ['summary', 'keywords', 'topic', 'domain', 'section_count'], +} + +const perceptual_type: Record = { + 1: 'last_visual', + 2: 'last_listen', + 3: 'last_text', +} + +const Timeline: FC = () => { + const { t } = useTranslation() + const { id } = useParams() + const [loading, setLoading] = useState(false) + const [data, setData] = useState([]) + + useEffect(() => { + if (!id) return + getData() + }, [id]) + const getData = () => { + if (!id) return + setLoading(true) + getPerceptualTimeline(id).then((res) => { + const response = res as { memories: TimelineItem[] } + setData(response.memories || []) + setLoading(false) + }) + .finally(() => { + setLoading(false) + }) + } + + return ( + + {loading + ? + : data.length === 0 + ? + : + {data.map((vo, index) => ( +
+
+ {formatDateTime(vo.created_time)} + {index !== data.length - 1 && } +
+
+
{vo.summary}
+
{t(`perceptualDetail.${perceptual_type[vo.perceptual_type]}`)}
+
+
+ ))} +
+ } +
+ ) +} +export default Timeline \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/components/WordCloud.tsx b/web/src/views/UserMemoryDetail/components/WordCloud.tsx index 64f2bfaa..b249a03a 100644 --- a/web/src/views/UserMemoryDetail/components/WordCloud.tsx +++ b/web/src/views/UserMemoryDetail/components/WordCloud.tsx @@ -114,7 +114,7 @@ const WordCloud: FC = () => {
{t(`statementDetail.${item.emotion_type}`)} -
{item.count}{t('statementDetail.pieces')}
+
{item.count} {t('statementDetail.pieces')}
diff --git a/web/src/views/UserMemoryDetail/pages/EpisodicDetail.tsx b/web/src/views/UserMemoryDetail/pages/EpisodicDetail.tsx new file mode 100644 index 00000000..4a7e4b1f --- /dev/null +++ b/web/src/views/UserMemoryDetail/pages/EpisodicDetail.tsx @@ -0,0 +1,250 @@ +import { type FC, useEffect, useState } from 'react' +import clsx from 'clsx' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Row, Col, Select, Form, Space, Skeleton, Input } from 'antd' +import RbCard from '@/components/RbCard/Card' +import { + getEpisodicOverview, + getEpisodicDetail, +} from '@/api/memory' +import { formatDateTime } from '@/utils/format' +import Tag from '@/components/Tag' +import RbAlert from '@/components/RbAlert' +import Empty from '@/components/Empty' + +interface EpisodicMemory { + id: string; + title: string; + type: string; + created_at: number; +} +interface EpisodicOverviewData { + total: number; + total_all: number; + episodic_memories: EpisodicMemory[] +} +interface EpisodicMemoryDetail { + id: string; + created_at: number; + involved_objects: string[]; + episodic_type: string; + content_records: string[]; + emotion: string; +} + +const TAG_COLORS: Record = { + conversation: "processing", + project_work: "success", + learning: "warning", + decision: "warning", + important_event: "error", +} +const BG_COLORS: Record = { + conversation: "rb:bg-[#155EEF]", + project_work: "rb:bg-[#369F21]", + learning: "rb:bg-[#FF5D34]", + decision: "rb:bg-[#FF5D34]", + important_event: "rb:bg-[#5B6167]", +} + +// Map display types to internal keys +const getTypeKey = (type: string): string => { + const typeMap: Record = { + 'Learning': 'learning', + 'Project/Work': 'project_work', + 'Conversation': 'conversation', + 'Decision': 'decision', + 'Important Event': 'important_event', + } + return typeMap[type] || type.toLowerCase().replace(/[^a-z0-9]/g, '_') +} +const EpisodicDetail: FC = () => { + const { t } = useTranslation() + const { id } = useParams() + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + const [data, setData] = useState({} as EpisodicOverviewData) + const values = Form.useWatch([], form) + const [detailLoading, setDetailLoading] = useState(false) + const [detail, setDetail] = useState(null) + const [selected, setSelected] = useState(null) + + useEffect(() => { + if (!id) return + // getData() + }, [id]) + + // 记忆洞察 + const getData = () => { + if (!id) return + setLoading(true) + setSelected(null) + setDetail(null) + getEpisodicOverview({ + end_user_id: id, + ...values + }).then((res) => { + const response = res as EpisodicOverviewData + setData(response) + if (response.episodic_memories.length > 0) { + setSelected(response.episodic_memories[0]) + } + setLoading(false) + }) + .finally(() => { + setLoading(false) + }) + } + + useEffect(() => { + getData() + }, [values]) + + useEffect(() => { + getDetail() + }, [selected]) + + const getDetail = () => { + if (!selected || !selected.id) return + + setDetailLoading(true) + getEpisodicDetail({ + end_user_id: id as string, + summary_id: selected.id + }) + .then(res => { + setDetail(res as EpisodicMemoryDetail) + }) + .finally(() => { + setDetailLoading(false) + }) + } + + return ( +
+
+
{t('episodicDetail.title')}
+ +
+
+
{data.total_all ?? 0}
+ {t(`episodicDetail.total_all`)} +
+
+
+ +
+ + + + + + + + + + + + +
+ + + + {t('episodicDetail.curResult')} ({data.total || 0}{t('episodicDetail.unix')})} + headerType="borderless" + > + {loading + ? + : !data.episodic_memories || data.episodic_memories.length === 0 + ? + : ( + + {data.episodic_memories.map((vo, index) => ( +
setSelected(vo)} + > +
{index + 1}
+
+
{vo.title} {t(`episodicDetail.${getTypeKey(vo.type)}`)}
+
{formatDateTime(vo.created_at)}
+
+
+ ))} +
+ ) + } + +
+ + + + {detailLoading + ? + : !selected || !detail + ? + : ( + +
+ + +
{t('episodicDetail.created')}
{formatDateTime(detail.created_at)}
+ + +
{t('episodicDetail.episodic_type')}
{detail.episodic_type}
+ + {detail.involved_objects.length > 0 && +
{t('episodicDetail.involved_objects')}
+ {detail.involved_objects.map((vo, index) => {vo})} + } +
+
+
+
{t('episodicDetail.content_records')}
+ {detail.content_records.map((vo, index) =>
- {vo}
)} +
+ + {t('episodicDetail.emotion')}: {t(`statementDetail.${detail.emotion}`)} + +
+ ) + } +
+ +
+
+ ) +} +export default EpisodicDetail \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx b/web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx index 6a59d41a..f0ba04ff 100644 --- a/web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx +++ b/web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx @@ -20,7 +20,7 @@ const statusTagColors: Record { +const ForgetDetail: FC = () => { const { t } = useTranslation() const { id } = useParams() const [loading, setLoading] = useState(false) @@ -156,4 +156,4 @@ const ForgetOverview: FC = () => {
) } -export default ForgetOverview \ No newline at end of file +export default ForgetDetail \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/pages/GraphDetail.tsx b/web/src/views/UserMemoryDetail/pages/GraphDetail.tsx new file mode 100644 index 00000000..f3cf716c --- /dev/null +++ b/web/src/views/UserMemoryDetail/pages/GraphDetail.tsx @@ -0,0 +1,14 @@ +import { type FC } from 'react' +import { useTranslation } from 'react-i18next' +import { Row, Col } from 'antd' + +const GraphDetail: FC = () => { + const { t } = useTranslation() + + return ( +
+ GraphDetail +
+ ) +} +export default GraphDetail \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx b/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx new file mode 100644 index 00000000..ef23463a --- /dev/null +++ b/web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx @@ -0,0 +1,34 @@ +import { type FC } from 'react' +import { useTranslation } from 'react-i18next' +import { Row, Col } from 'antd' + +import Preferences from '../components/Preferences' +import Portrait from '../components/Portrait' +import InterestAreas from '../components/InterestAreas' +import Habits from '../components/Habits' + +const ImplicitDetail: FC = () => { + const { t } = useTranslation() + + return ( +
+
{t('implicitDetail.title')}
+ + + +
{t('implicitDetail.portraitTitle')}
+
{t('implicitDetail.portraitSubTitle')}
+ + + + + + + + + + +
+ ) +} +export default ImplicitDetail \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/pages/PerceptualDetail.tsx b/web/src/views/UserMemoryDetail/pages/PerceptualDetail.tsx new file mode 100644 index 00000000..7e2d5353 --- /dev/null +++ b/web/src/views/UserMemoryDetail/pages/PerceptualDetail.tsx @@ -0,0 +1,32 @@ +import { type FC } from 'react' +import { useTranslation } from 'react-i18next' +import { Row, Col } from 'antd' + +import PerceptualLastInfo from '../components/PerceptualLastInfo' +import Timeline from '../components/Timeline' + +const PerceptualDetail: FC = () => { + const { t } = useTranslation() + + return ( +
+
{t('perceptualDetail.lastInfo')}
+ + + + + + + + + + + + + +
{t('perceptualDetail.timeLine')}
+ +
+ ) +} +export default PerceptualDetail \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/pages/ShortTermDetail.tsx b/web/src/views/UserMemoryDetail/pages/ShortTermDetail.tsx new file mode 100644 index 00000000..6cc8eafc --- /dev/null +++ b/web/src/views/UserMemoryDetail/pages/ShortTermDetail.tsx @@ -0,0 +1,114 @@ +import { type FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router-dom' +import { Space, Skeleton } from 'antd' +import { + getShortTerm, +} from '@/api/memory' +import Empty from '@/components/Empty' + +interface ShortTermItem { + retrieval: Array<{ query: string; retrieval: string[]; }>; + message: string; + answer: string; +} +interface LongTermItem { + query: string; + retrieval: string; +} +interface ShortData { + short_term: ShortTermItem[]; + long_term: LongTermItem[]; + entity: number; + retrieval_number: number; + long_term_number: number; +} +const ShortTermDetail: FC = () => { + const { t } = useTranslation() + const { id } = useParams() + const [loading, setLoading] = useState(false) + const [data, setData] = useState({} as ShortData) + + useEffect(() => { + if (!id) return + getData() + }, [id]) + + const getData = () => { + if (!id) return + setLoading(true) + getShortTerm(id).then((res) => { + const response = res as ShortData + setData(response) + setLoading(false) + }) + .finally(() => { + setLoading(false) + }) + } + + return ( +
+
+
{t('shortTermDetail.title')}
+ +
+ {(['retrieval_number', 'entity', 'long_term_number'] as const).map(key => ( +
+
{(data as any)[key] ?? 0}
+ {t(`shortTermDetail.${key}`)} +
+ ))} +
+
+ + +
{t('shortTermDetail.shortTermTitle')}
+
{t('shortTermDetail.shortTermSubTitle')}
+ + {loading + ? + : !data.short_term || data.short_term.length === 0 + ? + :data.short_term?.map((vo, voIdx) => ( +
+
{vo.message}
+ + {vo.retrieval.map((item, index) => ( +
+
{t('shortTermDetail.query')}: {item.query}
+
{t('shortTermDetail.answer')}:
+ {item.retrieval.length > 0 ? item.retrieval.map((retrieval, retrievalIdx) => ( +
- {retrieval}
+ )) :
{t('shortTermDetail.noAnswer')}
} +
+ ))} +
+
{t('shortTermDetail.answer')}
+
{vo.answer}
+
+
+
+ )) + } +
+ +
{t('shortTermDetail.longTermTitle')}
+
{t('shortTermDetail.shortTermSubTitle')}
+ + {loading + ? + : !data.long_term || data.long_term.length === 0 + ? + : data.long_term?.map((vo, voIdx) => ( +
+
{vo.query}
+
{vo.retrieval}
+
+ )) + } +
+
+ ) +} +export default ShortTermDetail \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/pages/index.tsx b/web/src/views/UserMemoryDetail/pages/index.tsx index 6b78a210..da62c14e 100644 --- a/web/src/views/UserMemoryDetail/pages/index.tsx +++ b/web/src/views/UserMemoryDetail/pages/index.tsx @@ -1,15 +1,23 @@ -import { type FC, useEffect, useState } from 'react' -import { useParams } from 'react-router-dom' +import { type FC, useEffect, useState, useMemo } from 'react' +import { useParams, useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { Dropdown } from 'antd' import PageHeader from '../components/PageHeader' import StatementDetail from './StatementDetail' import ForgetDetail from './ForgetDetail' +import ImplicitDetail from './ImplicitDetail' +import ShortTermDetail from './ShortTermDetail' +import PerceptualDetail from './PerceptualDetail' +import EpisodicDetail from './EpisodicDetail' import { getEndUserProfile, } from '@/api/memory' const Detail: FC = () => { + const { t } = useTranslation() const { id, type } = useParams() + const navigate = useNavigate() const [name, setName] = useState('') useEffect(() => { if (!id) return @@ -23,17 +31,37 @@ const Detail: FC = () => { setName(response.other_name || response.id) }) } + const items = useMemo(() => { + return ['PERCEPTUAL_MEMORY', 'WORKING_MEMORY', 'EMOTIONAL_MEMORY', 'SHORT_TERM_MEMORY', 'IMPLICIT_MEMORY', 'EPISODIC_MEMORY', 'EXPLICIT_MEMORY', 'FORGETTING_MANAGEMENT'] + .map(key => ({ key, label: t(`userMemory.${key}`) })) + }, [t]) + const onClick = ({ key }: { key: string }) => { + navigate(`/user-memory/detail/${id}/${key}`, { replace: true }) + } - console.log('Detail', name) return (
+
+ - {type ? t(`userMemory.${type}`) : ''} +
+
+ + } />
{type === 'EMOTIONAL_MEMORY' && } {type === 'FORGETTING_MANAGEMENT' && } + {type === 'IMPLICIT_MEMORY' && } + {type === 'SHORT_TERM_MEMORY' && } + {type === 'PERCEPTUAL_MEMORY' && } {/** TODO */} + {type === 'EPISODIC_MEMORY' && }
) diff --git a/web/src/views/UserMemoryDetail/types.ts b/web/src/views/UserMemoryDetail/types.ts index 77dd653e..263494d0 100644 --- a/web/src/views/UserMemoryDetail/types.ts +++ b/web/src/views/UserMemoryDetail/types.ts @@ -44,6 +44,7 @@ export interface Data { export interface BaseProperties { content: string; created_at: number; + associative_memory: number; } export interface StatementNodeProperties { temporal_info: string; @@ -51,12 +52,21 @@ export interface StatementNodeProperties { statement: string; valid_at: string; created_at: number; + emotion_keywords: string[]; + emotion_type: string; + emotion_subject: string; + importance_score: number; + associative_memory: number; } export interface ExtractedEntityNodeProperties { description: string; name: string; entity_type: string; created_at: number; + aliases: string; + connect_strngth: string; + importance_score: number; + associative_memory: number; } export interface MemorySummaryNode { id: string; @@ -72,7 +82,7 @@ export interface MemorySummaryNode { created_at: number; } caption: string; - + associative_memory: number; } export interface Node {