Merge pull request #62 from SuanmoSuanyangTechnology/feature/workflow_zy
Feature/workflow zy
This commit is contained in:
@@ -138,7 +138,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
|
||||
</div>
|
||||
<Form form={form} layout="vertical">
|
||||
<Space size={20} direction="vertical" style={{width: '100%'}}>
|
||||
<Card title={t('application.handoffs')}>
|
||||
<Card title={t('application.collaboration')}>
|
||||
<Form.Item
|
||||
name="orchestration_mode"
|
||||
noStyle
|
||||
|
||||
@@ -88,11 +88,11 @@ export interface MultiAgentConfig {
|
||||
app_id: string;
|
||||
default_model_config_id?: string;
|
||||
model_parameters: ModelConfig;
|
||||
orchestration_mode: 'conditional' | 'sequential' | 'parallel';
|
||||
sub_agents?: SubAgentItem[];
|
||||
routing_rules: null;
|
||||
orchestration_mode: 'supervisor' | 'collaboration';
|
||||
execution_config: {
|
||||
routing_mode: 'master' | 'handoffs'
|
||||
sub_agent_execution_mode: 'sequential' | 'parallel';
|
||||
};
|
||||
aggregation_strategy: 'merge' | 'vote' | 'priority'
|
||||
}
|
||||
|
||||
@@ -104,15 +104,15 @@ export default function UserMemory() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row gutter={16} className="rb:mb-[16px]">
|
||||
<Row gutter={16} className="rb:mb-4">
|
||||
{countList.map(key => (
|
||||
<Col key={key} span={6}>
|
||||
<div className="rb:bg-[#FBFDFF] rb:border-[1px] rb:border-[#DFE4ED] rb:rounded-[12px] rb:p-[18px_20px_20px_20px]">
|
||||
<div className="rb:text-[28px] rb:font-extrabold rb:leading-[35px] rb:flex rb:items-center rb:justify-between rb:mb-[12px]">
|
||||
<div className="rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-xl rb:p-[18px_20px_20px_20px]">
|
||||
<div className="rb:text-[28px] rb:font-extrabold rb:leading-8.75 rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||
{countData[key] || 0}{key === 'avgInteractionTime' ? 's' : ''}
|
||||
<img className="rb:w-[24px] rb:h-[24px]" src={IconList[key]} />
|
||||
<img className="rb:w-6 rb:h-6" src={IconList[key]} />
|
||||
</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-[16px]">{t(`userMemory.${key}`)}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`userMemory.${key}`)}</div>
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
@@ -140,22 +140,22 @@ export default function UserMemory() {
|
||||
return (
|
||||
<List.Item key={index}>
|
||||
<div
|
||||
className="rb:p-[20px] rb:rounded-[12px] rb:border-[1px] rb:border-[#DFE4ED] rb:cursor-pointer"
|
||||
className="rb:p-5 rb:rounded-xl rb:border rb:border-[#DFE4ED] rb:cursor-pointer"
|
||||
style={{
|
||||
background: bgList[index % bgList.length],
|
||||
}}
|
||||
onClick={() => handleViewDetail(end_user.id)}
|
||||
>
|
||||
<div className="rb:flex rb:items-center">
|
||||
<div className="rb:w-[48px] rb:h-[48px] rb:text-center rb:font-semibold rb:text-[28px] rb:leading-[48px] rb:rounded-[8px] rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name[0]}</div>
|
||||
<div className="rb:max-w-[calc(100%-60px)] rb:text-base rb:font-medium rb:leading-[24px] rb:ml-[12px] rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">
|
||||
<div className="rb:w-12 rb:h-12 rb:text-center rb:font-semibold rb:text-[28px] rb:leading-12 rb:rounded-lg rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name[0]}</div>
|
||||
<div className="rb:max-w-[calc(100%-60px)] rb:text-base rb:font-medium rb:leading-6 rb:ml-3 rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">
|
||||
{name || '-'}<br/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rb:grid rb:grid-cols-1 rb:gap-[12px] rb:mt-[28px] rb:mb-[28px]">
|
||||
<div className="rb:grid rb:grid-cols-1 rb:gap-3 rb:mt-7 rb:mb-7">
|
||||
<div className="rb:text-center">
|
||||
<div className="rb:text-[24px] rb:leading-[30px] rb:font-extrabold">{memory_num.total || 0}</div>
|
||||
<div className="rb:break-words">{t(`userMemory.knowledgeEntryCount`)}</div>
|
||||
<div className="rb:text-[24px] rb:leading-7.5 rb:font-extrabold">{memory_num.total || 0}</div>
|
||||
<div className="rb:wrap-break-word">{t(`userMemory.knowledgeEntryCount`)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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';
|
||||
@@ -25,7 +25,7 @@ const Neo4j: FC = () => {
|
||||
const aboutMeRef = useRef<AboutMeRef>(null)
|
||||
|
||||
const handleNameUpdate = (data: { other_name?: string; id: string }) => {
|
||||
setName(data.other_name ?? data.id)
|
||||
setName(data.other_name && data.other_name !== '' ? data.other_name : data.id)
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { type FC, useRef, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactEcharts from 'echarts-for-react';
|
||||
import Loading from '@/components/Empty/Loading'
|
||||
import Empty from '@/components/Empty'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
|
||||
interface ActivationMetricsPieCardProps {
|
||||
chartData: Array<Record<string, string | number>>;
|
||||
loading: boolean;
|
||||
}
|
||||
const Colors = ['#155EEF', '#FFB048', '#FF5D34']
|
||||
|
||||
const ActivationMetricsPieCard: FC<ActivationMetricsPieCardProps> = ({ chartData, loading }) => {
|
||||
const { t } = useTranslation()
|
||||
const chartRef = useRef<ReactEcharts>(null);
|
||||
const resizeScheduledRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (chartRef.current && !resizeScheduledRef.current) {
|
||||
resizeScheduledRef.current = true
|
||||
requestAnimationFrame(() => {
|
||||
chartRef.current?.getEchartsInstance().resize();
|
||||
resizeScheduledRef.current = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(handleResize)
|
||||
const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement
|
||||
if (chartElement) {
|
||||
resizeObserver.observe(chartElement)
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [chartData])
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('forgetDetail.activationValueDistribution')}
|
||||
headerType="borderless"
|
||||
>
|
||||
{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: 'item',
|
||||
textStyle: {
|
||||
color: '#5B6167',
|
||||
fontSize: 12,
|
||||
width: 27,
|
||||
height: 16,
|
||||
},
|
||||
formatter: '{d}%',
|
||||
padding: [8, 5],
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderColor: '#DFE4ED',
|
||||
extraCssText: 'width: 36px; height: 36px; box-shadow: 0px 2px 4px 0px rgba(33,35,50,0.12);border-radius: 36px;'
|
||||
},
|
||||
legend: {
|
||||
bottom: 14 ,
|
||||
padding: 0,
|
||||
itemGap: 24,
|
||||
itemWidth: 40,
|
||||
itemHeight: 12,
|
||||
borderRadius: 2,
|
||||
orient: 'horizontal',
|
||||
textStyle: {
|
||||
color: '#5B6167',
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
lineHeight: 16,
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'Access From',
|
||||
type: 'pie',
|
||||
radius: ['50%', '90%'],
|
||||
avoidLabelOverlap: false,
|
||||
percentPrecision: 0,
|
||||
padAngle: 4,
|
||||
width: 200,
|
||||
height: 200,
|
||||
left: 143,
|
||||
itemStyle: {
|
||||
borderRadius: 0
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#212332',
|
||||
formatter: '{d}%\n{b}',
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: chartData
|
||||
}
|
||||
]
|
||||
}}
|
||||
style={{ height: '265px', width: '100%', minWidth: '400px' }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActivationMetricsPieCard
|
||||
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
|
||||
@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Skeleton } from 'antd';
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Empty from '@/components/Empty';
|
||||
import {
|
||||
getNodeStatistics,
|
||||
} from '@/api/memory'
|
||||
@@ -15,11 +14,25 @@ const BG_LIST = [
|
||||
'rb:bg-[linear-gradient(316deg,rgba(21,94,239,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(316deg,rgba(54,159,33,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(314deg,rgba(156,111,255,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(314deg,rgba(255,93,52,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(180deg,rgba(156,111,255,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(180deg,rgba(21,94,239,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(180deg,rgba(54,159,33,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[]',
|
||||
'rb:bg-[linear-gradient(332deg,rgba(255,93,52,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(313deg,rgba(156,111,255,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
'rb:bg-[linear-gradient(332deg,rgba(54,159,33,0.06)_0%,rgba(251,253,255,0)_100%)]',
|
||||
]
|
||||
const typeList = [
|
||||
{ key: 'PERCEPTUAL_MEMORY', bg: 0 },
|
||||
{ key: 'WORKING_MEMORY', bg: 1 },
|
||||
{ key: 'EMOTIONAL_MEMORY', bg: 2 },
|
||||
{ key: 'SHORT_TERM_MEMORY', bg: 3 },
|
||||
{
|
||||
key: 'LONG_TERM_MEMORY',
|
||||
bg: 4,
|
||||
children: [
|
||||
{ key: 'IMPLICIT_MEMORY' },
|
||||
{ key: 'EPISODIC_MEMORY' },
|
||||
{ key: 'EXPLICIT_MEMORY' }
|
||||
]
|
||||
},
|
||||
{ key: 'FORGETTING_MANAGEMENT', bg: 5 },
|
||||
]
|
||||
|
||||
const NodeStatistics: FC = () => {
|
||||
@@ -52,43 +65,59 @@ const NodeStatistics: FC = () => {
|
||||
})
|
||||
}
|
||||
const handleViewDetail = (type: string) => {
|
||||
switch (type) {
|
||||
case 'EMOTIONAL_MEMORY':
|
||||
navigate(`/statement/${id}`)
|
||||
break
|
||||
}
|
||||
navigate(`/user-memory/detail/${id}/${type}`)
|
||||
}
|
||||
const renderCard = (key: string, bgIndex: number | null, isChild: boolean = false) => {
|
||||
const item = data.find((item) => item.type === key)
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"rb:flex rb:flex-col rb:justify-between rb:group rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:pt-3 rb:px-4 rb:pb-5 rb:cursor-pointer",
|
||||
{
|
||||
'rb:h-45': !isChild,
|
||||
'rb:h-31': isChild
|
||||
},
|
||||
typeof bgIndex === 'number' ? BG_LIST[bgIndex] : 'rb:bg-[#FBFDFF]'
|
||||
)}
|
||||
onClick={() => handleViewDetail(key)}
|
||||
>
|
||||
<div>
|
||||
<div className={clsx("rb:text-[#5B6167] rb:leading-5 rb:font-regular", {
|
||||
'rb:mb-2': !isChild,
|
||||
'rb:mb-1': isChild
|
||||
})}>
|
||||
{t(`userMemory.${key}`)}
|
||||
</div>
|
||||
<div className="rb:w-3 rb:h-3 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/arrow_right.svg')] rb:group-hover:bg-[url('@/assets/images/userMemory/arrow_right_hover.svg')]"></div>
|
||||
</div>
|
||||
<div className="rb:text-[28px] rb:leading-8.75 rb:font-extrabold">{item?.count ?? 0}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={<>{t('userMemory.nodeStatistics')} <span className="rb:text-[#5B6167] rb:font-normal!">({t('userMemory.total')}: {total})</span></>}
|
||||
headerType="borderless"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton />
|
||||
: data && data.length > 0
|
||||
? <div className={`rb:w-full rb:grid rb:grid-cols-8 rb:gap-3`}>
|
||||
{data.map((vo, index) => (
|
||||
<div
|
||||
key={vo.type}
|
||||
className={clsx("rb:flex rb:flex-col rb:justify-between rb:group rb:border rb:border-[#DFE4ED] rb:h-45 rb:rounded-lg rb:pt-3 rb:px-4 rb:pb-5", {
|
||||
'rb:cursor-pointer': vo.type === 'EMOTIONAL_MEMORY'
|
||||
}, BG_LIST[index])}
|
||||
onClick={() => handleViewDetail(vo.type)}
|
||||
>
|
||||
<div>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular">
|
||||
{t(`userMemory.${vo.type}`)}
|
||||
</div>
|
||||
{vo.type === 'EMOTIONAL_MEMORY' && <div
|
||||
className="rb:w-3 rb:h-3 rb:-ml-0.75 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/home/arrow_top_right.svg')] rb:group-hover:bg-[url('@/assets/images/home/arrow_top_right_hover.svg')]"
|
||||
></div>}
|
||||
? <Skeleton active />
|
||||
: <div className="rb:w-full rb:grid rb:grid-cols-8 rb:gap-3">
|
||||
{typeList.map((vo) => {
|
||||
if (!vo.children) {
|
||||
return <div key={vo.key}>{renderCard(vo.key, vo.bg)}</div>
|
||||
}
|
||||
return (
|
||||
<div key={vo.key} className={clsx("rb:col-span-3 rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:p-3", BG_LIST[vo.bg])}>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-3">{t(`userMemory.${vo.key}`)}</div>
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-3">
|
||||
{vo.children.map((child) => <div key={child.key}>{renderCard(child.key, null, true)}</div>)}
|
||||
</div>
|
||||
<div className="rb:text-[28px] rb:leading-8.75 rb:font-extrabold">{vo.count ?? 0}</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
: <Empty size={80} />
|
||||
}
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ const { Header } = Layout;
|
||||
interface ConfigHeaderProps {
|
||||
name?: string;
|
||||
operation?: ReactNode;
|
||||
source?: 'detail' | 'statement'
|
||||
source?: 'detail' | 'node'
|
||||
}
|
||||
const PageHeader: FC<ConfigHeaderProps> = ({
|
||||
name,
|
||||
|
||||
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
|
||||
@@ -0,0 +1,191 @@
|
||||
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 RbCard from '@/components/RbCard/Card'
|
||||
|
||||
interface RecentTrendsLineCardProps {
|
||||
chartData: Array<Record<string, string | number>>;
|
||||
seriesList: string[];
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const Colors = ['#155EEF', '#FF5D34']
|
||||
|
||||
const RecentTrendsLineCard: FC<RecentTrendsLineCardProps> = ({ chartData, seriesList, loading }) => {
|
||||
const { t } = useTranslation()
|
||||
const chartRef = useRef<ReactEcharts>(null);
|
||||
|
||||
const getSeries = () => {
|
||||
return seriesList.map((key, index) => ({
|
||||
name: key === 'merged_count' ? t('forgetDetail.merged_count') : t('forgetDetail.average_activation'),
|
||||
type: 'line',
|
||||
yAxisIndex: key === 'merged_count' ? 0 : 1,
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: Colors[index]
|
||||
},
|
||||
itemStyle: {
|
||||
color: Colors[index]
|
||||
},
|
||||
areaStyle: {
|
||||
color: Colors[index],
|
||||
opacity: 0.08
|
||||
},
|
||||
data: chartData.map(item => item[key])
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<RbCard
|
||||
title={t('forgetDetail.forgettingTrend')}
|
||||
headerType="borderless"
|
||||
>
|
||||
{loading
|
||||
? <Loading size={249} />
|
||||
: !chartData || chartData.length === 0
|
||||
? <Empty size={120} className="rb:mt-12 rb:mb-20.25" />
|
||||
: <ReactEcharts
|
||||
ref={chartRef}
|
||||
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',
|
||||
}
|
||||
},
|
||||
formatter: function(params: any) {
|
||||
let result = `${params[0].axisValue}<br/>`
|
||||
params.forEach((param: any) => {
|
||||
result += `${param.marker}${param.seriesName}: ${param.value}<br/>`
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
bottom: 2,
|
||||
padding: 0,
|
||||
itemGap: 24,
|
||||
itemWidth: 40,
|
||||
itemHeight: 12,
|
||||
borderRadius: 2,
|
||||
orient: 'horizontal',
|
||||
textStyle: {
|
||||
color: '#5B6167',
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
lineHeight: 16,
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 16,
|
||||
left: 30,
|
||||
right: 36,
|
||||
bottom: 48,
|
||||
// containLabel: false
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: chartData.map(item => item.date),
|
||||
boundaryGap: false,
|
||||
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',
|
||||
position: 'left',
|
||||
axisLabel: {
|
||||
color: Colors[0],
|
||||
fontFamily: 'PingFangSC, PingFang SC'
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: Colors[0]
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
position: 'right',
|
||||
axisLabel: {
|
||||
color: Colors[1],
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
formatter: '{value}'
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: Colors[1]
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#EBEBEB',
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
max: 1,
|
||||
min: 0
|
||||
}
|
||||
],
|
||||
series: getSeries()
|
||||
}}
|
||||
style={{ height: '265px', width: '100%', minWidth: '100%' }}
|
||||
notMerge={true}
|
||||
lazyUpdate={true}
|
||||
/>
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecentTrendsLineCard
|
||||
@@ -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>
|
||||
|
||||
250
web/src/views/UserMemoryDetail/pages/EpisodicDetail.tsx
Normal file
250
web/src/views/UserMemoryDetail/pages/EpisodicDetail.tsx
Normal file
@@ -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<string, "processing" | "success" | "warning" | "error" | "default"> = {
|
||||
conversation: "processing",
|
||||
project_work: "success",
|
||||
learning: "warning",
|
||||
decision: "warning",
|
||||
important_event: "error",
|
||||
}
|
||||
const BG_COLORS: Record<string, string> = {
|
||||
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<string, string> = {
|
||||
'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<boolean>(false)
|
||||
const [data, setData] = useState<EpisodicOverviewData>({} as EpisodicOverviewData)
|
||||
const values = Form.useWatch([], form)
|
||||
const [detailLoading, setDetailLoading] = useState<boolean>(false)
|
||||
const [detail, setDetail] = useState<EpisodicMemoryDetail | null>(null)
|
||||
const [selected, setSelected] = useState<EpisodicMemory | null>(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 (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:flex rb:justify-between rb:items-center rb:text-[#FFFFFF] rb:leading-5 rb:h-30 rb:p-5 rb:bg-[url('@/assets/images/userMemory/shortTerm.png')] rb:bg-cover rb:mb-6">
|
||||
<div className="rb:max-w-135">{t('episodicDetail.title')}</div>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-1 rb:gap-4">
|
||||
<div className="rb:bg-[rgba(255,255,255,0.2)] rb:rounded-lg rb:p-3.5 rb:text-[12px] rb:text-center">
|
||||
<div className="rb:text-[24px] rb:leading-8 rb:mb-1">{data.total_all ?? 0}</div>
|
||||
{t(`episodicDetail.total_all`)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form form={form} initialValues={{ time_range: 'all', episodic_type: 'all' }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Form.Item name="time_range">
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[
|
||||
{ value: 'all', label: t('episodicDetail.all') },
|
||||
{ value: 'today', label: t('episodicDetail.today') },
|
||||
{ value: 'this_week', label: t('episodicDetail.this_week') },
|
||||
{ value: 'this_month', label: t('episodicDetail.this_month') },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name="episodic_type">
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[
|
||||
{ value: 'all', label: t('episodicDetail.all') },
|
||||
{ value: 'conversation', label: t('episodicDetail.conversation') },
|
||||
{ value: 'project_work', label: t('episodicDetail.project_work') },
|
||||
{ value: 'learning', label: t('episodicDetail.learning') },
|
||||
{ value: 'decision', label: t('episodicDetail.decision') },
|
||||
{ value: 'important_event', label: t('episodicDetail.important_event') },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name="title_keyword">
|
||||
<Input placeholder={t('episodicDetail.titleKeywordPlaceholder')} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<RbCard
|
||||
title={<>{t('episodicDetail.curResult')}<span className="rb:text-[#5B6167] rb:font-regular!"> ({data.total || 0}{t('episodicDetail.unix')})</span></>}
|
||||
headerType="borderless"
|
||||
>
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: !data.episodic_memories || data.episodic_memories.length === 0
|
||||
? <Empty />
|
||||
: (
|
||||
<Space size={8} direction="vertical" className="rb:w-full">
|
||||
{data.episodic_memories.map((vo, index) => (
|
||||
<div
|
||||
key={vo.id}
|
||||
className={clsx("rb:cursor-pointer rb:flex rb:items-center rb:bg-[#FFFFFF] rb:border rb:rounded-lg rb:px-3 rb:py-2 rb:leading-5", {
|
||||
'rb:border-[#DFE4ED] rb:shadow-[0px_2px_4px_0px_rgba(33,35,50,0.16)]': selected?.id !== vo.id,
|
||||
'rb:border-[#155EEF]': selected?.id === vo.id,
|
||||
})}
|
||||
onClick={() => setSelected(vo)}
|
||||
>
|
||||
<div className={clsx("rb:bg-[#369F21] rb:rounded-lg rb:text-[#FFFFFF] rb:size-6 rb:text-[12px] rb:leading-6 rb:text-center rb:mr-3", BG_COLORS[getTypeKey(vo.type)])}>{index + 1}</div>
|
||||
<div className="rb:flex-1">
|
||||
<div className="rb:flex rb:items-center rb:justify-between">{vo.title} <Tag color={TAG_COLORS[getTypeKey(vo.type)]}>{t(`episodicDetail.${getTypeKey(vo.type)}`)}</Tag></div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px]">{formatDateTime(vo.created_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<RbCard
|
||||
title={selected?.title}
|
||||
headerType="borderless"
|
||||
>
|
||||
{detailLoading
|
||||
? <Skeleton active />
|
||||
: !selected || !detail
|
||||
? <Empty className="rb:mt-14" />
|
||||
: (
|
||||
<Space size={12} direction="vertical" className="rb:w-full">
|
||||
<div className="rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-3 rb:py-2 rb:leading-5">
|
||||
<Row gutter={[12, 16]}>
|
||||
<Col span={12}>
|
||||
<div className="rb:text-[#5B6167]">{t('episodicDetail.created')}<br />{formatDateTime(detail.created_at)}</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="rb:text-[#5B6167]">{t('episodicDetail.episodic_type')}<br />{detail.episodic_type}</div>
|
||||
</Col>
|
||||
{detail.involved_objects.length > 0 && <Col span={24}>
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-1">{t('episodicDetail.involved_objects')}</div>
|
||||
<Space size={8}>{detail.involved_objects.map((vo, index) => <Tag key={index}>{vo}</Tag>)}</Space>
|
||||
</Col>}
|
||||
</Row>
|
||||
</div>
|
||||
<div className="rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-3 rb:py-2 rb:leading-5">
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-1">{t('episodicDetail.content_records')}</div>
|
||||
{detail.content_records.map((vo, index) => <div key={index} className="rb:text-[#5B6167] rb:leading-5">- {vo}</div>)}
|
||||
</div>
|
||||
<RbAlert>
|
||||
{t('episodicDetail.emotion')}: {t(`statementDetail.${detail.emotion}`)}
|
||||
</RbAlert>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
</RbCard>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default EpisodicDetail
|
||||
159
web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx
Normal file
159
web/src/views/UserMemoryDetail/pages/ForgetDetail.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { type FC, useEffect, useState, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Progress } from 'antd'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getForgetStats,
|
||||
} from '@/api/memory'
|
||||
import type { ForgetData } from '../types'
|
||||
import ActivationMetricsPieCard from '../components/ActivationMetricsPieCard'
|
||||
import RecentTrendsLineCard from '../components/RecentTrendsLineCard'
|
||||
import Table from '@/components/Table'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import StatusTag from '@/components/StatusTag'
|
||||
|
||||
const statusTagColors: Record<string, 'success' | 'purple' | 'default' | 'warning' | 'error' | 'lightBlue'> = {
|
||||
statement: 'success',
|
||||
entity: 'purple',
|
||||
summary: 'default',
|
||||
chunk: 'warning',
|
||||
}
|
||||
|
||||
const ForgetDetail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<ForgetData>({} as ForgetData)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
// 记忆洞察
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getForgetStats(id).then((res) => {
|
||||
const response = res as ForgetData
|
||||
setData(response)
|
||||
setLoading(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
const chartData = useMemo(() => {
|
||||
const { activation_metrics } = data
|
||||
if (!activation_metrics) return []
|
||||
|
||||
let health_nodes = (activation_metrics.total_nodes || 0) - (activation_metrics.low_activation_nodes || 0) - (activation_metrics.nodes_without_activation || 0)
|
||||
|
||||
return [
|
||||
{ name: t('forgetDetail.health_nodes'), value: health_nodes },
|
||||
{ name: t('forgetDetail.nodes_without_activation'), value: activation_metrics.nodes_without_activation || 0 },
|
||||
{ name: t('forgetDetail.low_activation_nodes'), value: activation_metrics.low_activation_nodes || 0 },
|
||||
]
|
||||
|
||||
}, [data.activation_metrics, t])
|
||||
|
||||
const seriesList = useMemo(() => {
|
||||
const { recent_trends = [] } = data
|
||||
if (!recent_trends || recent_trends.length === 0) return { chartData: [], seriesList: [] }
|
||||
|
||||
return {
|
||||
chartData: recent_trends,
|
||||
seriesList: ['merged_count', 'average_activation']
|
||||
}
|
||||
}, [data.recent_trends])
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:mt-3">{t('forgetDetail.title')}</div>
|
||||
<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={8}>
|
||||
<RbCard>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-2">{t('forgetDetail.totalMemory')}</div>
|
||||
<div className="rb:text-[26px] rb:font-bold rb:leading-8.5">{data?.activation_metrics?.total_nodes ?? 0}</div>
|
||||
<div className="rb:mt-4 rb:grid rb:grid-cols-2 rb:gap-x-2 rb:gap-y-5 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-3 rb:py-2">
|
||||
{['statement_count', 'entity_count', 'summary_count', 'chunk_count'].map((key, index) => (
|
||||
<div key={index}>
|
||||
<div className="rb:text-[16px] rb:font-bold rb:leading-5.5">{data?.node_distribution?.[key as keyof typeof data.node_distribution] ?? 0}</div>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mt-1">{t(`forgetDetail.${key}`)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<RbCard>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-2">{t('forgetDetail.MemoryHealth')}</div>
|
||||
<div className="rb:text-[26px] rb:font-bold rb:leading-8.5">{data?.activation_metrics?.average_activation_value ?? 0}</div>
|
||||
<Progress className="rb:mt-px" showInfo={false} percent={data?.activation_metrics?.average_activation_value ?? 0} />
|
||||
<div className="rb:mt-4 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-3 rb:py-2">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular">{t('forgetDetail.healthStatus')}</div>
|
||||
<div className="rb:text-[20px] rb:font-semibold rb:leading-7">{data?.activation_metrics?.average_activation_value > data.activation_metrics?.forgetting_threshold ? t('forgetDetail.healthy') : t('forgetDetail.unhealthy')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:mt-2">
|
||||
{t('forgetDetail.average')}<br />
|
||||
{t('forgetDetail.threshold')}{data.activation_metrics?.forgetting_threshold ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<RbCard>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:font-regular rb:mb-2">{t('forgetDetail.riskOfForgetting')}</div>
|
||||
<div className="rb:text-[26px] rb:font-bold rb:leading-8.5">{data.activation_metrics?.low_activation_nodes ?? 0}</div>
|
||||
<div className="rb:mb-31.5 rb:text-[#A8A9AA] rb:text-[12px] rb:leading-4 rb:font-regular rb:mt-1">{t('forgetDetail.low_nodes')}</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<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.memoryHealthVisualization')}</div>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<ActivationMetricsPieCard chartData={chartData} loading={loading} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<RecentTrendsLineCard chartData={seriesList.chartData} seriesList={seriesList.seriesList} loading={loading} />
|
||||
</Col>
|
||||
</Row>
|
||||
<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.pending_nodes')}</div>
|
||||
<Table
|
||||
rowKey='node_id'
|
||||
initialData={data.pending_nodes ?? []}
|
||||
columns={[
|
||||
{
|
||||
title: t('forgetDetail.content_summary'),
|
||||
dataIndex: 'content_summary',
|
||||
key: 'content_summary',
|
||||
width: 340,
|
||||
render: (content_summary) => <div className="rb:wrap-break-word rb:line-clamp-2">{content_summary}</div>
|
||||
},
|
||||
{
|
||||
title: t('forgetDetail.node_type'),
|
||||
dataIndex: 'node_type',
|
||||
key: 'node_type',
|
||||
render: (node_type: string) => {
|
||||
return <StatusTag status={statusTagColors[node_type] || 'default'} text={node_type} />}
|
||||
},
|
||||
{
|
||||
title: t('forgetDetail.last_access_time'),
|
||||
dataIndex: 'last_access_time',
|
||||
key: 'last_access_time',
|
||||
render: (last_access_time) => formatDateTime(last_access_time, 'YYYY-MM-DD HH:mm')
|
||||
},
|
||||
{
|
||||
title: t('forgetDetail.activation_value'),
|
||||
dataIndex: 'activation_value',
|
||||
key: 'activation_value',
|
||||
},
|
||||
]}
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ForgetDetail
|
||||
14
web/src/views/UserMemoryDetail/pages/GraphDetail.tsx
Normal file
14
web/src/views/UserMemoryDetail/pages/GraphDetail.tsx
Normal file
@@ -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 (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
GraphDetail
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default GraphDetail
|
||||
34
web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx
Normal file
34
web/src/views/UserMemoryDetail/pages/ImplicitDetail.tsx
Normal file
@@ -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 (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:mt-3">{t('implicitDetail.title')}</div>
|
||||
|
||||
<Preferences />
|
||||
|
||||
<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.portraitTitle')}</div>
|
||||
<div className="rb:my-3 rb:text-[#5B6167] rb:leading-5">{t('implicitDetail.portraitSubTitle')}</div>
|
||||
<Row gutter={[16, 16]} className="rb:mt-4">
|
||||
<Col span={12}>
|
||||
<Portrait />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<InterestAreas />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Habits />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ImplicitDetail
|
||||
32
web/src/views/UserMemoryDetail/pages/PerceptualDetail.tsx
Normal file
32
web/src/views/UserMemoryDetail/pages/PerceptualDetail.tsx
Normal file
@@ -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 (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mt-6 rb:rounded-md rb:mb-4">{t('perceptualDetail.lastInfo')}</div>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<PerceptualLastInfo type="last_visual" />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<PerceptualLastInfo type="last_listen" />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<PerceptualLastInfo type="last_text" />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mt-6 rb:rounded-md rb:mb-4">{t('perceptualDetail.timeLine')}</div>
|
||||
<Timeline />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default PerceptualDetail
|
||||
114
web/src/views/UserMemoryDetail/pages/ShortTermDetail.tsx
Normal file
114
web/src/views/UserMemoryDetail/pages/ShortTermDetail.tsx
Normal file
@@ -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<boolean>(false)
|
||||
const [data, setData] = useState<ShortData>({} 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 (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:flex rb:justify-between rb:items-center rb:text-[#FFFFFF] rb:leading-5 rb:h-30 rb:p-5 rb:bg-[url('@/assets/images/userMemory/shortTerm.png')] rb:bg-cover">
|
||||
<div className="rb:max-w-135">{t('shortTermDetail.title')}</div>
|
||||
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-4">
|
||||
{(['retrieval_number', 'entity', 'long_term_number'] as const).map(key => (
|
||||
<div key={key} className="rb:bg-[rgba(255,255,255,0.2)] rb:rounded-lg rb:p-3.5 rb:text-[12px] rb:text-center">
|
||||
<div className="rb:text-[24px] rb:leading-8 rb:mb-1">{(data as any)[key] ?? 0}</div>
|
||||
{t(`shortTermDetail.${key}`)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<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('shortTermDetail.shortTermTitle')}</div>
|
||||
<div className="rb:my-3 rb:text-[#5B6167] rb:leading-5">{t('shortTermDetail.shortTermSubTitle')}</div>
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: !data.short_term || data.short_term.length === 0
|
||||
? <Empty />
|
||||
:data.short_term?.map((vo, voIdx) => (
|
||||
<div key={voIdx} className="rb:leading-5 rb:shadow-[inset_3px_0px_0px_0px_#155EEF] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-6 rb:py-3">
|
||||
<div className="rb:font-medium rb:text-[16px] rb:leading-5.5 rb:mb-3">{vo.message}</div>
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
{vo.retrieval.map((item, index) => (
|
||||
<div key={index} className="rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-3 rb:py-2.5 rb:leading-5">
|
||||
<div className="rb:font-medium rb:mb-3">{t('shortTermDetail.query')}: {item.query}</div>
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-1">{t('shortTermDetail.answer')}:</div>
|
||||
{item.retrieval.length > 0 ? item.retrieval.map((retrieval, retrievalIdx) => (
|
||||
<div key={retrievalIdx} className="rb:text-[#5B6167] rb:text-[12px]">- {retrieval}</div>
|
||||
)) : <div className="rb:text-[#5B6167] rb:text-[12px]">{t('shortTermDetail.noAnswer')}</div>}
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-1">{t('shortTermDetail.answer')}</div>
|
||||
<div className="rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-3 rb:py-2.5 rb:leading-5">{vo.answer}</div>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</Space>
|
||||
|
||||
<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('shortTermDetail.longTermTitle')}</div>
|
||||
<div className="rb:my-3 rb:text-[#5B6167] rb:leading-5">{t('shortTermDetail.shortTermSubTitle')}</div>
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
{loading
|
||||
? <Skeleton active />
|
||||
: !data.long_term || data.long_term.length === 0
|
||||
? <Empty />
|
||||
: data.long_term?.map((vo, voIdx) => (
|
||||
<div key={voIdx} className="rb:leading-5 rb:shadow-[inset_3px_0px_0px_0px_#155EEF] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:px-6 rb:py-3">
|
||||
<div className="rb:mb-1 rb:font-medium rb:leading-5.5">{vo.query}</div>
|
||||
<div className="rb:mt-1 rb:leading-5 rb:text-[#5B6167] rb:text-[12px]">{vo.retrieval}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ShortTermDetail
|
||||
@@ -1,53 +1,26 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { type FC } from 'react'
|
||||
import { Row, Col, Space } from 'antd';
|
||||
|
||||
import WordCloud from '../components/WordCloud'
|
||||
import EmotionTags from '../components/EmotionTags'
|
||||
import Health from '../components/Health'
|
||||
import Suggestions from '../components/Suggestions'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import {
|
||||
getEndUserProfile,
|
||||
} from '@/api/memory'
|
||||
|
||||
|
||||
const StatementDetail: FC = () => {
|
||||
const { id } = useParams()
|
||||
const [name, setName] = useState<string>('')
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
getEndUserProfile(id).then((res) => {
|
||||
const response = res as { other_name: string; id: string; }
|
||||
setName(response.other_name ?? response.id)
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className="rb:h-full rb:w-full">
|
||||
<PageHeader
|
||||
name={name}
|
||||
source="statement"
|
||||
/>
|
||||
<div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
<WordCloud />
|
||||
<EmotionTags />
|
||||
<Health />
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Suggestions />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<Space size={16} direction="vertical" className="rb:w-full">
|
||||
<WordCloud />
|
||||
<EmotionTags />
|
||||
<Health />
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Suggestions />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
70
web/src/views/UserMemoryDetail/pages/index.tsx
Normal file
70
web/src/views/UserMemoryDetail/pages/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
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<string>('')
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
const getData = () => {
|
||||
if (!id) return
|
||||
getEndUserProfile(id).then((res) => {
|
||||
const response = res as { other_name: string; id: string; }
|
||||
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 })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:w-full">
|
||||
<PageHeader
|
||||
name={name}
|
||||
source="node"
|
||||
operation={
|
||||
<Dropdown menu={{ items, onClick, selectedKeys: type ? [type] : [] }}>
|
||||
<div className="rb:cursor-pointer rb:group rb:flex rb:items-center rb:gap-1">
|
||||
- {type ? t(`userMemory.${type}`) : ''}
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/up_border.svg')] rb:transform-[rotate(180deg)] rb:group-hover:transform-[rotate(0deg)]"
|
||||
></div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
}
|
||||
/>
|
||||
<div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4">
|
||||
{type === 'EMOTIONAL_MEMORY' && <StatementDetail />}
|
||||
{type === 'FORGETTING_MANAGEMENT' && <ForgetDetail />}
|
||||
{type === 'IMPLICIT_MEMORY' && <ImplicitDetail />}
|
||||
{type === 'SHORT_TERM_MEMORY' && <ShortTermDetail />}
|
||||
{type === 'PERCEPTUAL_MEMORY' && <PerceptualDetail />} {/** TODO */}
|
||||
{type === 'EPISODIC_MEMORY' && <EpisodicDetail />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Detail
|
||||
@@ -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 {
|
||||
@@ -140,4 +150,38 @@ export interface AboutMeRef {
|
||||
}
|
||||
export interface EndUserProfileRef {
|
||||
data: EndUser | null
|
||||
}
|
||||
|
||||
|
||||
export interface ForgetData {
|
||||
activation_metrics: {
|
||||
total_nodes: number;
|
||||
nodes_with_activation: number;
|
||||
nodes_without_activation: number;
|
||||
average_activation_value: number;
|
||||
low_activation_nodes: number;
|
||||
timestamp: number;
|
||||
forgetting_threshold: number;
|
||||
},
|
||||
node_distribution: {
|
||||
statement_count: number;
|
||||
entity_count: number;
|
||||
summary_count: number;
|
||||
chunk_count: number;
|
||||
},
|
||||
recent_trends: {
|
||||
date: string;
|
||||
merged_count: number;
|
||||
average_activation: number;
|
||||
total_nodes: number;
|
||||
execution_time: number;
|
||||
}[],
|
||||
pending_nodes: {
|
||||
node_id: string;
|
||||
node_type: string;
|
||||
content_summary: string;
|
||||
activation_value: number;
|
||||
last_access_time: number;
|
||||
}[],
|
||||
timestamp: number;
|
||||
}
|
||||
Reference in New Issue
Block a user