feat(web): user memory detail
This commit is contained in:
85
web/src/views/UserMemoryDetail/components/Habits.tsx
Normal file
85
web/src/views/UserMemoryDetail/components/Habits.tsx
Normal file
@@ -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<boolean>(false)
|
||||
const [data, setData] = useState<HabitsItem[]>([])
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('implicitDetail.habits')}</div>
|
||||
<div className="rb:my-3 rb:text-[#5B6167] rb:leading-5">{t('implicitDetail.habitsSubTitle')}</div>
|
||||
<RbCard>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: data.length === 0
|
||||
? <Empty size={88} />
|
||||
: <Space size={12} direction="vertical" className="rb:w-full!">
|
||||
{data.map((vo, voIdx) => (
|
||||
<div key={voIdx} className="rb:leading-5 rb:shadow-[inset_3px_0px_0px_0px_#155EEF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:p-3">
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div>
|
||||
<div className="rb:mb-1">{vo.habit_description}</div>
|
||||
<div className="rb:mb-1 rb:text-[#5B6167]">{vo.time_context}</div>
|
||||
</div>
|
||||
<div className="rb:text-[24px] rb:font-medium">{vo.confidence_level}%</div>
|
||||
</div>
|
||||
|
||||
{vo.specific_examples.length > 0 && <>
|
||||
<div className="rb:mt-3 rb:mb-2">{t('implicitDetail.specific_examples')}</div>
|
||||
<div className="rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:p-3">
|
||||
{vo.specific_examples.map((item, index) => (
|
||||
<div key={index} className="rb:text-[#5B6167] rb:text-[12px] rb:mt-1">- {item}</div>
|
||||
))}
|
||||
</div>
|
||||
</>}
|
||||
<Progress percent={vo.confidence_level} showInfo={false} className="rb:mt-3" />
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
}
|
||||
</RbCard>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Habits
|
||||
75
web/src/views/UserMemoryDetail/components/InterestAreas.tsx
Normal file
75
web/src/views/UserMemoryDetail/components/InterestAreas.tsx
Normal file
@@ -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<boolean>(false)
|
||||
const [data, setData] = useState<InterestAreasItem>({} 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 (
|
||||
<RbCard
|
||||
title={t('implicitDetail.interestAreas')}
|
||||
headerType="borderless"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: <div>
|
||||
{(['art', 'music', 'tech', 'lifestyle'] as const).map((key) => {
|
||||
return (
|
||||
<div key={key} >
|
||||
<div className="rb:flex rb:justify-between rb:items-center">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-1">{t(`implicitDetail.${key}`)}</div>
|
||||
{data[key]?.percentage ?? 0}%
|
||||
</div>
|
||||
<Progress percent={data[key]?.percentage || 0} showInfo={false} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
export default InterestAreas
|
||||
120
web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx
Normal file
120
web/src/views/UserMemoryDetail/components/PerceptualLastInfo.tsx
Normal file
@@ -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<boolean>(false)
|
||||
const [data, setData] = useState<PerceptualLastInfoItem>({} 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 (
|
||||
<RbCard
|
||||
title={t(`perceptualDetail.${type}`)}
|
||||
headerType="borderless"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: <div>
|
||||
<div className="rb:bg-[#F0F3F8] rb:h-36 rb:rounded-sm rb:flex rb:items-center rb:justify-center rb:overflow-hidden">
|
||||
{data.file_path ? (
|
||||
type === 'last_visual' ? (
|
||||
/\.(mp4|webm|ogg|mov)$/i.test(data.file_name) ? (
|
||||
<video controls className="rb:max-w-full rb:max-h-full">
|
||||
<source src={data.file_path} />
|
||||
</video>
|
||||
) : /\.(jpg|jpeg|png|gif|webp|svg)$/i.test(data.file_name) ? (
|
||||
<Image src={data.file_path} alt={data.file_name} />
|
||||
// <img src={data.file_path} alt={data.file_name} className="rb:max-w-full rb:max-h-full rb:object-contain" />
|
||||
) : (
|
||||
<div className="rb:text-gray-500">{data.file_name}</div>
|
||||
)
|
||||
) : type === 'last_listen' && /\.(mp3|wav|ogg|m4a|aac)$/i.test(data.file_name) ? (
|
||||
<audio controls className="rb:w-full">
|
||||
<source src={data.file_path} />
|
||||
</audio>
|
||||
) : (
|
||||
<div className="rb:text-gray-500">{data.file_name}</div>
|
||||
)
|
||||
) : (
|
||||
<div className="rb:text-gray-400">No file</div>
|
||||
)}
|
||||
</div>
|
||||
<Space size={4} direction="vertical" className="rb:w-full rb:mt-3">
|
||||
{KEYS[type].map(key => {
|
||||
const value = (data as any)[key]
|
||||
return (
|
||||
<div key={key} className="rb:flex rb:justify-between rb:items-center rb:gap-3">
|
||||
<div className="rb:text-[#5B6167]">{t(`perceptualDetail.${key}`)}</div>
|
||||
{key === 'summary' ? (
|
||||
<Tooltip title={value}>
|
||||
<div className="rb:flex-1 rb:text-right rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">
|
||||
{typeof value === 'string' ? value : Array.isArray(value) ? value.join('、') : '-'}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
: <div className="rb:flex-1 rb:text-right">
|
||||
{typeof value === 'string' ? value : Array.isArray(value) ? value.join('、') : '-'}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
export default PerceptualLastInfo
|
||||
77
web/src/views/UserMemoryDetail/components/Portrait.tsx
Normal file
77
web/src/views/UserMemoryDetail/components/Portrait.tsx
Normal file
@@ -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<boolean>(false)
|
||||
const [data, setData] = useState<PortraitItem>({} 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 (
|
||||
<RbCard
|
||||
title={t('implicitDetail.portrait')}
|
||||
headerType="borderless"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: <div className="rb:mt-1">
|
||||
{(['aesthetic', 'creativity', 'literature', 'technology'] as const).map((key) => {
|
||||
const item = data[key] as Item
|
||||
return (
|
||||
<div key={key}>
|
||||
<div className="rb:flex rb:justify-between rb:items-center">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-1">{t(`implicitDetail.${key}`)}</div>
|
||||
{item?.percentage ?? 0}%
|
||||
</div>
|
||||
<Progress percent={item?.percentage || 0} showInfo={false} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
export default Portrait
|
||||
183
web/src/views/UserMemoryDetail/components/Preferences.tsx
Normal file
183
web/src/views/UserMemoryDetail/components/Preferences.tsx
Normal file
@@ -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<string, string> = {}
|
||||
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<HTMLDivElement>(null)
|
||||
const chartInstance = useRef<echarts.ECharts | null>(null)
|
||||
const [selectedWord, setSelectedWord] = useState<number | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState<PreferenceItem[]>([])
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-4 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('forgetDetail.overviewTitle')}</div>
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
<RbCard
|
||||
title={t('implicitDetail.preferences')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
bodyClassName='rb:p-0! rb:pb-3! rb:relative rb:h-[350px]'
|
||||
>
|
||||
{loading
|
||||
? <Skeleton active className="rb:px-4" />
|
||||
: data && data.length > 0
|
||||
? <div ref={chartRef} className="rb:mt-6 rb:px-6" style={{ height: '350px' }} />
|
||||
: <Empty size={88} className="rb:h-full" />
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<RbCard
|
||||
title={detailTitle}
|
||||
headerType="borderless"
|
||||
height="100%"
|
||||
bodyClassName='rb:p-3! rb:h-[326px]'
|
||||
>
|
||||
{selectedWord === null
|
||||
? <Empty size={88} className="rb:h-full!" />
|
||||
: <>
|
||||
<div className="rb:leading-5 rb:mb-1 rb:font-medium">{t('implicitDetail.context_details')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular">{data[selectedWord].context_details}</div>
|
||||
|
||||
<div className="rb:leading-5 rb:mt-3 rb:font-medium">{t('implicitDetail.supporting_evidence')}</div>
|
||||
{data[selectedWord].supporting_evidence.map((vo, index) => <div key={index} className="rb:text-[#5B6167] rb:leading-5 rb:font-regular">-{vo}</div>)}
|
||||
</>
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Preferences
|
||||
@@ -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 (
|
||||
<Row gutter={16}>
|
||||
{/* 关系网络 */}
|
||||
@@ -240,8 +246,14 @@ const RelationshipNetwork:FC = () => {
|
||||
title={t('userMemory.memoryDetails')}
|
||||
headerType="borderless"
|
||||
bodyClassName='rb:p-0!'
|
||||
extra={selectedNode && <Button type="text" onClick={handleViewAll}>
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/view.svg')] rb:hover:bg-[url('@/assets/images/userMemory/view_hover.svg')]"
|
||||
></div>
|
||||
{t('userMemory.completeMemory')}
|
||||
</Button>}
|
||||
>
|
||||
<div className="rb:h-133.5">
|
||||
<div className="rb:h-133.5 rb:overflow-y-auto">
|
||||
{!selectedNode
|
||||
? <Empty
|
||||
url={detailEmpty}
|
||||
@@ -267,9 +279,52 @@ const RelationshipNetwork:FC = () => {
|
||||
</>
|
||||
<div className="rb:font-medium rb:mb-2 rb:mt-4">
|
||||
<div className="rb:font-medium rb:leading-5">{t('userMemory.created_at')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4">
|
||||
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
|
||||
{dayjs(selectedNode?.properties.created_at).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</div>
|
||||
|
||||
{selectedNode?.properties.associative_memory > 0 && <div className="rb:mt-4">
|
||||
<div className="rb:font-medium rb:leading-5">{t('userMemory.associative_memory')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
|
||||
<span className="rb:text-[#155EEF] rb:font-medium">{selectedNode?.properties.associative_memory}</span> {t('userMemory.unix')}{t('userMemory.associative_memory')}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{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 (
|
||||
<div className="rb:mt-4" key={key}>
|
||||
{t(`userMemory.Statement_${key}`)}
|
||||
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
|
||||
{key === 'emotion_keywords'
|
||||
? <Space>{statementProps.emotion_keywords.map((vo, index) => <Tag key={index}>{vo}</Tag>)}</Space>
|
||||
: statementProps[key]
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
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 (
|
||||
<div className="rb:mt-4" key={key}>
|
||||
{t(`userMemory.ExtractedEntity_${key}`)}
|
||||
<div className="rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:mt-1 rb:pb-4 rb:border-b rb:border-[#DFE4ED]">
|
||||
{entityProps[key]}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
82
web/src/views/UserMemoryDetail/components/Timeline.tsx
Normal file
82
web/src/views/UserMemoryDetail/components/Timeline.tsx
Normal file
@@ -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<number, string> = {
|
||||
1: 'last_visual',
|
||||
2: 'last_listen',
|
||||
3: 'last_text',
|
||||
}
|
||||
|
||||
const Timeline: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<TimelineItem[]>([])
|
||||
|
||||
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 (
|
||||
<RbCard>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: data.length === 0
|
||||
? <Empty />
|
||||
: <Space size={8} direction="vertical" className="rb:w-full">
|
||||
{data.map((vo, index) => (
|
||||
<div key={vo.id} className="rb:flex rb:gap-6 rb:min-h-16">
|
||||
<div className="rb:text-[#155EEF] rb:leading-5 rb:font-medium rb:flex rb:flex-col rb:gap-2 rb:items-center">
|
||||
{formatDateTime(vo.created_time)}
|
||||
{index !== data.length - 1 && <Divider type="vertical" className="rb:flex-1 rb:w-px rb:border-[#155EEF]!" />}
|
||||
</div>
|
||||
<div className="rb:flex rb:justify-between rb:flex-1 rb:mb-4">
|
||||
<div className="rb:w-150 rb:leading-5">{vo.summary}</div>
|
||||
<div className="rb:text-[#5B6167] rb:font-medium">{t(`perceptualDetail.${perceptual_type[vo.perceptual_type]}`)}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
export default Timeline
|
||||
@@ -114,7 +114,7 @@ const WordCloud: FC = () => {
|
||||
<div key={item.emotion_type}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:font-medium">
|
||||
{t(`statementDetail.${item.emotion_type}`)}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular">{item.count}{t('statementDetail.pieces')}</div>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user