feat(web): user memory

This commit is contained in:
zhaoying
2026-01-12 14:42:02 +08:00
parent 24ace52e27
commit d56cbed0bf
14 changed files with 648 additions and 223 deletions

View File

@@ -195,6 +195,15 @@ export const getExplicitMemory = (end_user_id: string) => {
export const getExplicitMemoryDetails = (data: { end_user_id: string, memory_id: string; }) => {
return request.post(`/memory-storage/classifications/explicit-memory-details`, data)
}
export const getConversations = (end_user: string) => {
return request.get(`/memory/work/${end_user}/conversations`)
}
export const getConversationMessages = (end_user: string, conversation_id: string) => {
return request.get(`/memory/work/${end_user}/messages`, { conversation_id })
}
export const getConversationDetail = (end_user: string, conversation_id: string) => {
return request.get(`/memory/work/${end_user}/detail`, { conversation_id })
}
/*************** end 用户记忆 相关接口 ******************************/

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

View File

@@ -0,0 +1,16 @@
import { useTranslation } from 'react-i18next'
import LoadingIcon from '@/assets/images/empty/pageLoading.png'
import Empty from './index'
const PageLoading = ({ size = [240, 210] }: { size?: number | number[] }) => {
const { t } = useTranslation()
return (
<Empty
url={LoadingIcon}
title={t('empty.loadingEmpty')}
subTitle={t('empty.loadingEmptyDesc')}
size={size}
className="rb:h-full"
/>
)
}
export default PageLoading;

View File

@@ -1190,10 +1190,6 @@ export const en = {
nodeStatistics: 'Memory Classification',
total: 'Total',
Chunk: 'Long-term Memory',
MemorySummary: 'Episodic Memory',
Statement: 'Emotional Memory',
ExtractedEntity: 'Short-term Memory',
PERCEPTUAL_MEMORY: 'Perceptual Memory',
WORKING_MEMORY: 'Working Memory',
@@ -1244,6 +1240,10 @@ export const en = {
timelineMemories: 'Shared Memory Timeline',
emotionLine: 'Emotion Changes Over Time',
interaction: 'Interaction Frequency & Relationship Stages',
timelines_memory: 'All',
MemorySummary: 'Long-term Accumulation',
Statement: 'Emotional Memory',
ExtractedEntity: 'Episodic Memory',
},
space: {
createSpace: 'Create Space',
@@ -1987,6 +1987,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
},
statementDetail: {
wordCloud: 'Emotion Distribution Analysis',
totalCount: 'Sample Count',
pieces: 'items',
emotionTags: 'High-Frequency Emotion Keywords',
joy: 'Joy',
@@ -2281,6 +2282,14 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
emotion: 'Emotion',
core_definition: 'Core Definition',
detailed_notes: 'Detailed Notes',
},
workingDetail: {
conversationStream: 'Real-time Conversation Stream',
refresh: 'Refresh',
successfulTitle: 'Successful Experience',
question: 'Lessons Learned',
summary: 'Core Insights',
none: 'None'
}
},
};

View File

@@ -1269,11 +1269,6 @@ export const zh = {
nodeStatistics: '记忆分类',
total: '总计',
Chunk: '长期记忆',
MemorySummary: '情景记忆',
Statement: '情绪记忆',
ExtractedEntity: '短期记忆',
PERCEPTUAL_MEMORY: '感知记忆',
WORKING_MEMORY: '工作记忆',
SHORT_TERM_MEMORY: '短期记忆',
@@ -1325,6 +1320,10 @@ export const zh = {
timelineMemories: '共同记忆时间线',
emotionLine: '情绪随时间变化',
interaction: '互动频率 & 关系阶段',
timelines_memory: '全部',
MemorySummary: '长期沉淀',
Statement: '情绪记忆',
ExtractedEntity: '情景记忆',
},
space: {
createSpace: '创建空间',
@@ -2087,6 +2086,7 @@ export const zh = {
},
statementDetail: {
wordCloud: '情感分布分析',
totalCount: '样本数',
pieces: '条',
emotionTags: '高频情绪关键词',
joy: '喜悦',
@@ -2381,6 +2381,14 @@ export const zh = {
emotion: '情绪',
core_definition: '核心定义',
detailed_notes: '详细笔记',
},
workingDetail: {
conversationStream: '实时对话流',
refresh: '刷新',
successfulTitle: '成功经验',
question: '踩过的坑',
summary: '核心洞察',
none: '无'
}
},
}

View File

@@ -1,7 +1,6 @@
import { type FC, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import ReactEcharts from 'echarts-for-react';
import * as echarts from 'echarts';
import Empty from '@/components/Empty'
import Loading from '@/components/Empty/Loading'
import type { Emotion } from './GraphDetail'
@@ -100,10 +99,7 @@ const EmotionLine: FC<EmotionLineProps> = ({ chartData, loading }) => {
},
xAxis: {
type: 'category',
data: [...new Set(chartData.map(item => item.created_at))].sort().map(time => {
const date = new Date(time)
return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}`
}),
data: [...new Set(chartData.map(item => item.created_at))].sort(),
boundaryGap: false,
axisLabel: {
color: '#A8A9AA',

View File

@@ -1,6 +1,8 @@
import { type FC, useEffect, useState } from 'react'
import { type FC, useEffect, useState, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import * as echarts from 'echarts'
import 'echarts-wordcloud'
import Empty from '@/components/Empty'
import RbCard from '@/components/RbCard/Card'
@@ -13,40 +15,23 @@ interface TagList {
const EmotionTags: FC = () => {
const { t } = useTranslation()
const { id } = useParams()
const chartRef = useRef<HTMLDivElement>(null)
const chartInstance = useRef<echarts.ECharts | null>(null)
const [data, setData] = useState<TagList | null>(null)
useEffect(() => {
if (!id) return
getEmotionTagData()
}, [id])
const getEmotionTagData = () => {
if (!id) {
return
}
if (!id) return
getWordCloud(id)
.then((res) => {
setData(res as TagList)
})
}
const [visibleCount, setVisibleCount] = useState(0)
useEffect(() => {
if (!data || data?.keywords.length === 0) return
const timer = setInterval(() => {
setVisibleCount(prev => {
if (prev >= data?.keywords.length) {
clearInterval(timer)
return prev
}
return prev + 1
})
}, 200)
return () => clearInterval(timer)
}, [data?.keywords.length])
const getEmotionColor = (emotionType: string) => {
const colors: Record<string, string> = {
joy: '#52c41a',
@@ -59,6 +44,56 @@ const EmotionTags: FC = () => {
return colors[emotionType] || '#8c8c8c'
}
useEffect(() => {
if (!chartRef.current || !data?.keywords.length) return
if (chartInstance.current) {
chartInstance.current.dispose()
}
chartInstance.current = echarts.init(chartRef.current)
const wordCloudData = data.keywords.map((item) => ({
name: item.keyword,
value: item.frequency,
textStyle: {
color: getEmotionColor(item.emotion_type)
}
}))
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)
return () => {
if (chartInstance.current) {
chartInstance.current.dispose()
chartInstance.current = null
}
}
}, [data])
const emotionStats = data?.keywords.reduce((acc, item) => {
acc[item.emotion_type] = (acc[item.emotion_type] || 0) + item.frequency
return acc
@@ -68,41 +103,25 @@ const EmotionTags: FC = () => {
<RbCard
title={t('statementDetail.emotionTags')}
headerType="borderless"
headerClassName="rb:text-[18px]! rb:leading-[24px]"
bodyClassName='rb:p-0! rb:pb-3! rb:relative'
headerClassName="rb:leading-[24px] rb:bg-[#F6F8FC]! rb:min-h-[46px]! rb:border-b! rb:border-b-[#DFE4ED]!"
bodyClassName="rb:p-0!"
>
{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]">
? <div>
<div ref={chartRef} className="rb:mt-6 rb:px-6" style={{ height: '320px', width: '100%' }} />
<div className="rb:flex rb:flex-wrap rb:items-center rb:justify-center rb:gap-10 rb:text-sm rb:mt-3 rb:p-3 rb:bg-[#F0F3F8] rb:rounded-[0_0_8px_8px]">
{Object.entries(emotionStats).map(([type, count]) => {
console.log(type)
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(`statementDetail.${type || 'neutral'}`)} ({count})</span>
<span className="rb:leading-5">{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">
{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"
style={{
backgroundColor: getEmotionColor(item.emotion_type),
fontSize: `${12 + item.avg_intensity * 8}px`,
animationDelay: `${index * 200}ms`,
height: `${20 + item.avg_intensity * 20}px`,
transition: 'all 0.3s ease-in-out'
}}
>
{item.keyword}
</div>
))}
</div>
</>
: <Empty size={88} />
</div>
: <Empty size={88} className="rb:h-full" />
}
</RbCard>
)

View File

@@ -1,13 +1,16 @@
import { type FC, useState, forwardRef, useImperativeHandle } from 'react'
import { useState, forwardRef, useImperativeHandle, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import { Row, Col, Tabs } from 'antd'
import { Row, Col, Tabs, Space, Skeleton } from 'antd'
import { getRelationshipEvolution, getTimelineMemories } from '@/api/memory'
import type { Node, GraphDetailRef } from '../types'
import RbDrawer from '@/components/RbDrawer'
import RbCard from '@/components/RbCard/Card'
import EmotionLine from './EmotionLine'
import { formatDateTime } from '@/utils/format'
import Tag from '@/components/Tag'
import InteractionBar from './InteractionBar'
import Empty from '@/components/Empty'
export interface Emotion {
emotion_intensity: number;
@@ -15,141 +18,72 @@ export interface Emotion {
created_at: string | number;
}
export interface Interaction {
name: string;
importance_score: number;
interaction_count: number;
created_at: string | number;
count: number;
}
interface TimelineMemory {
text: string;
type: string;
created_at: number | string;
}
interface Timeline {
MemorySummary: TimelineMemory[];
Statement: TimelineMemory[];
ExtractedEntity: TimelineMemory[];
timelines_memory: TimelineMemory[];
}
const GraphDetail = forwardRef<GraphDetailRef>((_props, ref) => {
const { t } = useTranslation()
const { id } = useParams()
const [open, setOpen] = useState(false);
const [vo, setVo] = useState<Node | null>(null)
const [emotionData, setEmotionData] = useState<Emotion[]>([
{
"emotion_intensity": 0.1,
"emotion_type": "neutral",
"created_at": "2026-01-07 19:14:34"
},
{
"emotion_intensity": 0.2,
"emotion_type": "neutral",
"created_at": "2026-02-08 19:14:34"
},
{
"emotion_intensity": 0.1,
"emotion_type": "neutral",
"created_at": "2026-03-09 19:14:34"
},
{
"emotion_intensity": 0.1,
"emotion_type": "neutral",
"created_at": "2026-04-10 19:14:34"
},
{
"emotion_intensity": 0.1,
"emotion_type": "sadness",
"created_at": "2026-01-07 19:14:34"
},
{
"emotion_intensity": 0.2,
"emotion_type": "sadness",
"created_at": "2026-02-08 19:14:34"
},
{
"emotion_intensity": 0.1,
"emotion_type": "sadness",
"created_at": "2026-03-09 19:14:34"
},
{
"emotion_intensity": 0.1,
"emotion_type": "sadness",
"created_at": "2026-04-10 19:14:34"
},
])
const [interactionData, setInteractionData] = useState<Interaction[]>([
{
"name": "小蓝",
"importance_score": 0.5,
"interaction_count": 1
}
])
const [timelineMemories, setTimelineMemories] = useState({
"code": 0,
"msg": "共同记忆时间线",
"data": {
"success": true,
"data": {
"MemorySummary": [
"小蓝今天原计划与小明野餐、与小绿看电影,但最终选择与姐姐小红一起看戏。",
"用户小明喜欢喝咖啡,每天都要喝拿铁。"
],
"Statement": [
"小蓝对是否去野餐或看电影感到犹豫。",
"小蓝和她姐姐小红出去看戏。",
"小明喜欢喝咖啡。",
"小明每天都要喝拿铁。",
"小明今天约小蓝出去野餐。"
],
"ExtractedEntity": [
"小明",
"咖啡",
"拿铁",
"小蓝",
"野餐"
],
"timelines_memory": [
"小蓝今天原计划与小明野餐、与小绿看电影,但最终选择与姐姐小红一起看戏。",
"用户小明喜欢喝咖啡,每天都要喝拿铁。",
"小蓝对是否去野餐或看电影感到犹豫。",
"小蓝和她姐姐小红出去看戏。",
"小明喜欢喝咖啡。",
"小明每天都要喝拿铁。",
"小明今天约小蓝出去野餐。",
"小明",
"咖啡",
"拿铁",
"小蓝",
"野餐"
]
}
},
"error": "",
"time": 1767852781464
})
const [loading, setLoading] = useState(false)
const [emotionData, setEmotionData] = useState<Emotion[]>([])
const [interactionData, setInteractionData] = useState<Interaction[]>([])
const [activeTab, setActiveTab] = useState('timelines_memory')
const [timelineLoading, setTimelineLoading] = useState(false)
const [timelineMemories, setTimelineMemories] = useState<Timeline>({ timelines_memory: [], MemorySummary: [], Statement: [], ExtractedEntity: []})
const handleCancel = () => {
setVo(null)
setOpen(false)
}
const handleOpen = (vo: Node) => {
setActiveTab('timelines_memory')
setOpen(true)
setVo(vo)
getRelationshipEvolutionData(vo)
getTimelineMemoriesData(vo)
}
const getRelationshipEvolutionData = (vo: Node) => {
if (!id || !vo.label) return
getRelationshipEvolution({ id: id as string, label: vo.label })
if (!vo.id || !vo.label) return
setLoading(true)
getRelationshipEvolution({ id: vo.id as string, label: vo.label })
.then(res => {
const { emotion, interaction } = res as { emotion: { data: Emotion[]}; interaction: {data: Interaction[]} } || {}
setEmotionData(emotion?.data)
setInteractionData(interaction?.data)
const { emotion, interaction } = res as { emotion: Emotion[]; interaction: Interaction[] } || {}
setEmotionData(emotion)
setInteractionData(interaction)
})
.finally(() => setLoading(false))
}
const getTimelineMemoriesData = (vo: Node) => {
if (!id || !vo.label) return
getTimelineMemories({ id: id as string, label: vo.label })
if (!vo.id || !vo.label) return
setTimelineLoading(true)
getTimelineMemories({ id: vo.id as string, label: vo.label })
.then(res => {
setTimelineMemories(res as Timeline)
})
.finally(() => setTimelineLoading(false))
}
useImperativeHandle(ref, () => ({
handleOpen,
}));
const activeContent = useMemo(() => {
return timelineMemories[activeTab as keyof Timeline] || []
}, [activeTab, timelineMemories])
return (
<RbDrawer
title={vo?.name}
@@ -157,17 +91,49 @@ const GraphDetail = forwardRef<GraphDetailRef>((_props, ref) => {
onClose={handleCancel}
width={1000}
>
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-3">{t('useMemory.relationshipEvolution')}</div>
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-3">{t('userMemory.relationshipEvolution')}</div>
<RbCard>
<Row gutter={16}>
<Col span={12}>
<EmotionLine chartData={emotionData} />
<EmotionLine chartData={emotionData} loading={loading} />
</Col>
<Col span={12}>
<div>{t('userMemory.interaction')}</div>
<InteractionBar chartData={interactionData} loading={loading} />
</Col>
</Row>
</RbCard>
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-3 rb:mt-6">{t('userMemory.timelineMemories')}</div>
<RbCard>
<Tabs
activeKey={activeTab}
items={['timelines_memory', 'ExtractedEntity', 'Statement', 'MemorySummary'].map(key => ({
label: t(`userMemory.${key}`),
key
}))}
onChange={(key: string) => setActiveTab(key)}
/>
{timelineLoading
? <Skeleton active />
: !activeContent || activeContent.length === 0
? <Empty size={120} className="rb:mt-12 rb:mb-20.25" />
: <Space size={16} direction="vertical" className="rb:w-full">
{activeContent.map((vo, index) => (
<RbCard
key={index}
headerType="borderL"
headerClassName="rb:before:bg-[#155EEF]!"
title={vo.text}
>
<div className="rb:text-[#A8A9AA] rb:text-[12px] rb:leading-4">{formatDateTime(vo.created_at)}</div>
<Tag className="rb:mt-2">{vo.type}</Tag>
</RbCard>
))}
</Space>
}
</RbCard>
</RbDrawer>
)
})

View File

@@ -1,7 +1,8 @@
import { type FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import { Progress } from 'antd'
import { Row, Col, Progress } from 'antd'
import ReactEcharts from 'echarts-for-react'
import Empty from '@/components/Empty'
import RbCard from '@/components/RbCard/Card'
@@ -58,37 +59,95 @@ const Health: FC = () => {
<RbCard
title={t('statementDetail.health')}
headerType="borderless"
headerClassName="rb:text-[18px]! rb:leading-[24px]"
headerClassName="rb:leading-[24px] rb:bg-[#F6F8FC]! rb:min-h-[46px]! rb:border-b! rb:border-b-[#DFE4ED]!"
bodyClassName="rb:px-[28px]! rb:py-[16px]!"
>
{health?.health_score && health?.health_score > 0
? <>
<div className="rb:flex rb:justify-center rb:items-center">
<Progress
size={250}
type="circle"
strokeColor={{
'0%': '#108ee9',
'100%': '#87d068',
}}
percent={health.health_score}
format={(percent) => `${percent}(${health.level})`}
/>
</div>
<Row gutter={59}>
<Col span={12}>
<div className="rb:flex rb:justify-center rb:items-center">
<ReactEcharts
option={{
series: [{
type: 'pie',
radius: ['65%', '80%'],
center: ['50%', '50%'],
startAngle: 90,
data: [
{
value: health.health_score,
name: health.level,
itemStyle: {
color: '#155EEF',
borderRadius: [10, 10, 10, 10]
}
},
{
value: 100 - health.health_score,
name: '',
itemStyle: {
color: '#DFE4ED',
borderRadius: [10, 10, 10, 10]
}
}
],
label: {
show: true,
position: 'center',
formatter: '{score|' + health.health_score + '}\n{level|' + health.level + '}',
rich: {
score: {
fontSize: 36,
fontWeight: 'bold',
color: '#212332',
lineHeight: 36
},
level: {
fontSize: 14,
color: '#5B6167',
lineHeight: 20
}
}
},
labelLine: { show: false },
emphasis: { disabled: true },
itemStyle: {
borderRadius: 10
}
}]
}}
style={{ height: '200px', width: '200px' }}
/>
</div>
</Col>
<Col span={12}>
{health.dimensions && <div className="rb:space-y-7">
<div>
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167]">
{t('statementDetail.positivity_rate')}
<div className="rb:text-[12px] rb:text-[#155EEF] rb:font-medium">{health.dimensions.positivity_rate.score}%</div>
</div>
<Progress strokeColor="#155EEF" percent={health.dimensions.positivity_rate.score} showInfo={false} />
</div>
<div>
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167]">
{t('statementDetail.stability')}
<div className="rb:text-[12px] rb:text-[#155EEF] rb:font-medium">{health.dimensions.stability.score}%</div>
</div>
<Progress strokeColor="#155EEF" percent={health.dimensions.stability.score} showInfo={false} />
</div>
<div>
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167]">
{t('statementDetail.resilience')}
<div className="rb:text-[12px] rb:text-[#155EEF] rb:font-medium">{health.dimensions.resilience.score}%</div>
</div>
<Progress strokeColor="#155EEF" percent={health.dimensions.resilience.score} showInfo={false} />
</div>
</div>}
</Col>
</Row>
{health.dimensions && <>
<div className="rb:flex rb:items-center rb:justify-between rb:mt-6">
<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('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('statementDetail.resilience')}</div>
<Progress className="rb:w-[calc(100%-180px)]" percent={health.dimensions.resilience.score} />
</div>
</>}
</>
: <Empty size={88} className="rb:h-full" />
}

View File

@@ -0,0 +1,119 @@
import { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import ReactEcharts from 'echarts-for-react'
import Empty from '@/components/Empty'
import Loading from '@/components/Empty/Loading'
import type { Interaction } from './GraphDetail'
interface InteractionBarProps {
chartData: Interaction[];
loading?: boolean;
}
const Colors = ['#155EEF', '#369F21', '#FF5D34']
const InteractionBar: FC<InteractionBarProps> = ({ chartData, loading }) => {
const { t } = useTranslation()
const series = [{
name: 'Interaction Count',
type: 'bar',
data: chartData.map(item => item.count)
}]
return (
<>
<div>{t('userMemory.interaction')}</div>
{loading
? <Loading size={249} />
: !chartData || chartData.length === 0
? <Empty size={120} className="rb:mt-12 rb:mb-20.25" />
: <ReactEcharts
option={{
color: Colors,
tooltip: {
trigger: 'axis',
extraCssText: 'box-shadow: 0px 2px 6px 0px rgba(33,35,50,0.16); border-radius: 8px;',
axisPointer: {
type: 'line',
crossStyle: {
color: '#5F6266',
},
lineStyle: {
color: '#5F6266',
}
},
},
grid: {
top: 16,
left: 30,
right: 36,
bottom: 48,
// containLabel: false
},
xAxis: {
type: 'category',
data: chartData.map(item => item.created_at),
axisLabel: {
color: '#A8A9AA',
fontFamily: 'PingFangSC, PingFang SC'
},
axisLine: {
show: true,
lineStyle: {
color: '#EBEBEB'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#EBEBEB',
type: 'solid'
}
},
axisTick: {
show: true,
lineStyle: {
color: '#EBEBEB',
type: 'solid'
}
}
},
yAxis: {
type: 'value',
axisLabel: {
color: '#A8A9AA',
fontFamily: 'PingFangSC, PingFang SC'
},
axisLine: {
show: true,
lineStyle: {
color: '#EBEBEB'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#EBEBEB',
type: 'solid'
}
},
axisTick: {
show: true,
lineStyle: {
color: '#EBEBEB',
type: 'solid'
}
},
max: 1,
min: 0
},
series
}}
style={{ height: '265px', width: '100%' }}
/>
}
</>
)
}
export default InteractionBar

View File

@@ -41,18 +41,24 @@ const Suggestions: FC = () => {
<RbCard
title={t('statementDetail.suggestions')}
headerType="borderless"
headerClassName="rb:text-[18px]! rb:leading-[24px]"
headerClassName="rb:leading-[24px] rb:bg-[#F6F8FC]! rb:min-h-[46px]! rb:border-b! rb:border-b-[#DFE4ED]!"
bodyClassName="rb:px-[16px]! rb:pt-[20px]! rb:pb-[24px]!"
>
{suggestions?.suggestions && suggestions?.suggestions.length > 0
? <>
<RbAlert className="rb:mb-3">{suggestions.health_summary}</RbAlert>
{suggestions.suggestions.map((item, index) => (
<div key={index} className="rb:mb-3">
<div className="rb:font-medium">{index + 1}. {item.title}</div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-1 rb:mb-2">{item.content}</div>
{item.actionable_steps.map((vo, idx) => <div key={idx} className="rb:ml-6 rb:text-[12px] rb:text-[#5B6167] rb:mt-1">- {vo}</div>)}
</div>
))}
<div className="rb:space-y-8">
{suggestions.suggestions.map((item, index) => (
<div key={index}>
<div className="rb:font-medium">{index + 1}. {item.title}</div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2 rb:mb-2 rb:leading-5">{item.content}</div>
<ul className="rb:list-disc rb:ml-4 rb:text-[12px] rb:text-[#5B6167] rb:leading-5">
{item.actionable_steps.map((vo, idx) => <li key={idx}>{vo}</li>)}
</ul>
</div>
))}
</div>
</>
: <Empty size={88} className="rb:h-full" />
}

View File

@@ -2,7 +2,7 @@ import { type FC, useEffect, useState, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import ReactEcharts from 'echarts-for-react'
import { Progress } from 'antd'
import { Progress, Row, Col } from 'antd'
import Empty from '@/components/Empty'
import RbCard from '@/components/RbCard/Card'
@@ -101,27 +101,35 @@ const WordCloud: FC = () => {
<RbCard
title={t('statementDetail.wordCloud')}
headerType="borderless"
headerClassName="rb:text-[18px]! rb:leading-[24px]"
height="100%"
headerClassName="rb:leading-[24px] rb:bg-[#F6F8FC]! rb:min-h-[46px]! rb:border-b! rb:border-b-[#DFE4ED]!"
bodyClassName="rb:px-[28px]! rb:py-[16px]!"
>
{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">
<div className="rb:text-[18px] rb:font-medium rb:mb-4">{wordCloud.total_count}</div>
<div className="rb:space-y-3">
? <Row gutter={50}>
<Col span={12}>
<ReactEcharts ref={chartRef} option={radarOption} style={{ width: '100%', height: 'calc(100% - 100px)' }} />
<div className="rb:mb-4 rb:text-center rb:bg-[#F5F7FC] rb:rounded-lg rb:p-2.5 rb:mt-4">
<span className="rb:text-[#155EEF] rb:text-[28px] rb:font-bold rb:leading-8">{wordCloud.total_count}</span><br />
<span className="rb:text-[#5B6167] rb:leading-5">{t('statementDetail.totalCount')}</span>
</div>
</Col>
<Col span={12}>
<div className="rb:space-y-5">
{wordCloud.tags.map(item => (
<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:flex rb:items-center rb:justify-between">
<div>
<span className="rb:font-medium">{t(`statementDetail.${item.emotion_type}`)}</span>
<span className="rb:font-regular rb:text-[#5B6167]"> ( {item.count} {t('statementDetail.pieces')} )</span>
</div>
<div className="rb:text-[12px] rb:text-[#155EEF] rb:font-medium">{item.percentage.toFixed(1)}%</div>
</div>
<Progress size="small" percent={item.percentage} />
<Progress strokeColor="#155EEF" percent={item.percentage} showInfo={false} />
</div>
))}
</div>
</div>
</div>
</Col>
</Row>
: <Empty size={88} />
}
</RbCard>

View File

@@ -0,0 +1,209 @@
import { type FC, useEffect, useState, useMemo } 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, Button, Divider } from 'antd'
import RbCard from '@/components/RbCard/Card'
import {
getConversations,
getConversationMessages,
getConversationDetail,
} from '@/api/memory'
import { formatDateTime } from '@/utils/format'
import Tag from '@/components/Tag'
import RbAlert from '@/components/RbAlert'
import Empty from '@/components/Empty'
import ChatContent from '@/components/Chat/ChatContent'
import type { ChatItem } from '@/components/Chat/types'
import PageLoading from '@/components/Empty/PageLoading'
interface Conversation {
title: string;
id: string;
}
interface Detail {
theme: string;
theme_intro: string;
summary: string;
question: string[];
takeaways: string[];
info_score: number;
}
const WorkingDetail: FC = () => {
const { t } = useTranslation()
const { id } = useParams()
const [form] = Form.useForm()
const [loading, setLoading] = useState<boolean>(false)
const [data, setData] = useState<Conversation[]>([])
const [messagesLoading, setMessagesLoading] = useState<boolean>(false)
const [messages, setMessages] = useState<ChatItem[]>([])
const [detailLoading, setDetailLoading] = useState<boolean>(false)
const [detail, setDetail] = useState<Detail | null>(null)
const [selected, setSelected] = useState<Conversation | null>(null)
useEffect(() => {
if (!id) return
getData()
}, [id])
const getData = () => {
if (!id) return
setLoading(true)
setSelected(null)
setDetail(null)
setData([])
getConversations(id).then((res) => {
const response = res as Conversation[]
setData(response)
setSelected(response[0] || null)
})
.finally(() => {
setLoading(false)
})
}
useEffect(() => {
if (!id || !selected || !selected.id) return
getDetail(selected.id)
}, [id, selected])
const getDetail = (conversationId: string) => {
if (!id || !conversationId) return
setDetail(null)
setMessages([])
setDetailLoading(true)
setMessagesLoading(true)
getConversationMessages(id, conversationId)
.then(res => {
setMessages(res as ChatItem[])
})
.finally(() => {
setMessagesLoading(false)
})
getConversationDetail(id, conversationId)
.then(res => {
setDetail(res as Detail)
})
.finally(() => {
setDetailLoading(false)
})
}
const timeRange = useMemo(() => {
const times = messages.filter(m => m.created_at).map(m => Number(m.created_at))
if (times.length === 0) return ''
const minTime = Math.min(...times)
const maxTime = Math.max(...times)
return `${formatDateTime(minTime, 'YYYY.MM')} - ${formatDateTime(maxTime, 'YYYY.MM')}`
}, [messages])
return (
<div className="rb:h-[calc(100vh-64px)]! rb:w-full rb:-mx-4 rb:-my-3">
{loading
? <PageLoading />
: data.length === 0
? <Empty />
:(
<Row gutter={16} className="rb:h-full">
<Col span={5}>
<div className="rb:h-full! rb:border-r rb:border-[#EAECEE] rb:py-3 rb:px-4">
{data.map(item => (
<div key={item.id} className="rb:mb-3">
<div className={clsx("rb:p-[8px_13px] rb:rounded-lg rb:leading-5 rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
'rb:bg-[#FFFFFF] rb:shadow-[0px_2px_4px_0px_rgba(0,0,0,0.15)] rb:font-medium rb:hover:bg-[#FFFFFF]!': item.id === selected?.id,
})}
onClick={() => getDetail(item.id)}
>
{item.title}
</div>
</div>
))}
</div>
</Col>
{selected && <>
<Col span={19}>
<div className="rb:text-[18px] rb:font-medium rb:leading-6 rb:mt-4">{selected.title}</div>
<div className="rb:mt-1 rb:text-[#5B6167] rb:leading-5">{timeRange}</div>
<Row gutter={16}>
<Col span={16}>
<RbCard
title={t('workingDetail.conversationStream')}
extra={<Button className="rb:h-6!" onClick={() => getDetail(selected.id)}>{t('workingDetail.refresh')}</Button>}
className="rb:mt-4!"
headerClassName='rb:bg-[#F6F8FC]! rb:border-b! rb:border-b-[#DFE4ED]! rb:min-h-11!'
headerType="borderless"
bodyClassName="rb:h-[calc(100vh-210px)]"
>
{messagesLoading
? <Skeleton active />
: messages.length === 0
? <Empty />
: (
<ChatContent
classNames="rb:h-[calc(100vh-244px)]"
data={messages}
streamLoading={false}
labelFormat={(item) => formatDateTime(item.created_at)}
/>
)
}
</RbCard>
</Col>
<Col span={8}>
<RbCard className="rb:mt-4!" bodyClassName="rb:h-[calc(100vh-166px)] rb:overflow-y-auto">
{detailLoading
? <Skeleton active />
: detail
? <>
<>
<div className="rb:text-[#369F21] rb:font-medium rb:text-[18px] rb:leading-4 rb:mb-3">{t('workingDetail.successfulTitle')}</div>
{detail.takeaways.length > 0
? (
<ul className="rb:text-[#5B6167] rb:leading-5.5 rb:list-disc rb:ml-4">
{detail.takeaways.map(vo => <li>{vo}</li>)}
</ul>
)
: <Empty size={88} />
}
</>
<>
<Divider />
<div className="rb:text-[#FF5D34] rb:font-medium rb:text-[18px] rb:leading-4 rb:mb-3">{t('workingDetail.question')}</div>
{detail.question.length > 0
? (
<ul className="rb:text-[#5B6167] rb:leading-5.5 rb:list-disc rb:ml-4">
{detail.question.map(vo => <li>{vo}</li>)}
</ul>
)
: <Empty size={88} />
}
</>
<>
<Divider />
<div className="rb:text-[#369F21] rb:font-medium rb:text-[18px] rb:leading-4 rb:mb-3">{t('workingDetail.summary')}</div>
{detail.summary
? <RbAlert className="rb:text-[#212332]! rb:text-[14px]! rb:leading-5.5! rb:p-3!">{detail.summary}</RbAlert>
: <Empty size={88} />
}
</>
</>
: <Empty />
}
</RbCard>
</Col>
</Row>
</Col>
</>}
</Row>
)
}
</div>
)
}
export default WorkingDetail

View File

@@ -11,6 +11,7 @@ import ShortTermDetail from './ShortTermDetail'
import PerceptualDetail from './PerceptualDetail'
import EpisodicDetail from './EpisodicDetail'
import ExplicitDetail from './ExplicitDetail'
import WorkingDetail from './WorkingDetail'
import {
getEndUserProfile,
} from '@/api/memory'
@@ -63,7 +64,7 @@ const Detail: FC = () => {
{type === 'SHORT_TERM_MEMORY' && <ShortTermDetail />}
{type === 'PERCEPTUAL_MEMORY' && <PerceptualDetail />} {/** TODO */}
{type === 'EPISODIC_MEMORY' && <EpisodicDetail />}
{/* {type === 'WORKING_MEMORY' && <WorkingDetail />} */} {/** TODO */}
{type === 'WORKING_MEMORY' && <WorkingDetail />} {/** TODO */}
{type === 'EXPLICIT_MEMORY' && <ExplicitDetail />} {/** TODO */}
</div>
</div>