{
console.log("Clearing auth data and redirecting to login");
- sessionStorage.clear();
- localStorage.clear()
+ // sessionStorage.clear();
+ // localStorage.clear()
cookieUtils.clear();
}
\ No newline at end of file
diff --git a/web/src/utils/stream.ts b/web/src/utils/stream.ts
index abaaca2d..7688cdd5 100644
--- a/web/src/utils/stream.ts
+++ b/web/src/utils/stream.ts
@@ -12,43 +12,57 @@ export function parseSSEToJSON(sseString: string) {
const lines = sseString.trim().split('\n')
let currentEvent: SSEMessage = {}
+ let dataContent = ''
+ for (const line of lines) {
+ if (line.startsWith('event:')) {
+ if (currentEvent.event && dataContent) {
+ currentEvent.data = parseDataContent(dataContent)
+ events.push(currentEvent)
+ }
+ currentEvent = { event: line.substring(6).trim() }
+ dataContent = ''
+ } else if (line.startsWith('data:')) {
+ if (dataContent) dataContent += '\n'
+ dataContent += line.substring(5).trim()
+ }
+ }
+
+
+ if (currentEvent.event && dataContent) {
+ currentEvent.data = parseDataContent(dataContent)
+ console.log('currentEvent', currentEvent)
+ events.push(currentEvent)
+ }
+
+ return events
+}
+
+function parseDataContent(dataContent: string): string | object {
try {
- for (const line of lines) {
- if (line.startsWith('event:')) {
- if (Object.keys(currentEvent).length > 0) {
- events.push(currentEvent)
- currentEvent = {}
- }
- currentEvent.event = line.substring(6).trim()
- } else if (line.startsWith('data:')) {
- const dataStr = line.substring(5).trim()
- if (dataStr) {
- try {
- // 尝试解析为 JSON
- currentEvent.data = JSON.parse(dataStr)
- } catch {
- // JSON 解析失败时,检查是否是被转义的 JSON 字符串
- try {
- const unescaped = dataStr.replace(/"/g, '"').replace(/&/g, '&')
- currentEvent.data = JSON.parse(unescaped)
- } catch {
- // 如果仍然失败,保存为原始字符串
- currentEvent.data = dataStr
- }
- }
- }
+ // 第一层解码:HTML实体
+ let unescaped = dataContent
+ .replace(/"/g, '"')
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/'/g, "'")
+
+ // 解析第一层JSON
+ const firstParse = JSON.parse(unescaped)
+
+ // 如果data字段是字符串且包含JSON,解析data层但保持chunk为字符串
+ if (firstParse.data && typeof firstParse.data === 'string' && firstParse.data.includes("{")) {
+ try {
+ firstParse.data = JSON.parse(firstParse.data)
+ } catch {
+ // 保持原字符串
}
}
- if (Object.keys(currentEvent).length > 0) {
- events.push(currentEvent)
- }
-
- return events
- } catch (error) {
- console.error('Parse stream error:', error)
- return []
+ return firstParse
+ } catch {
+ return dataContent
}
}
@@ -80,16 +94,30 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
const reader = response.body.getReader();
const decoder = new TextDecoder();
+ let buffer = ''; // 添加缓冲区来处理不完整的消息
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
- if (onMessage) {
- onMessage(parseSSEToJSON(chunk) ?? {});
+ buffer += chunk;
+
+ // 处理完整的事件
+ const events = buffer.split('\n\n');
+ buffer = events.pop() || ''; // 保留最后一个可能不完整的事件
+
+ for (const event of events) {
+ if (event.trim() && onMessage) {
+ onMessage(parseSSEToJSON(event) ?? {});
+ }
}
}
+
+ // 处理剩余的缓冲区内容
+ if (buffer.trim() && onMessage) {
+ onMessage(parseSSEToJSON(buffer) ?? {});
+ }
break;
}
} catch (error) {
diff --git a/web/src/views/UserMemoryDetail/components/EmotionLine.tsx b/web/src/views/UserMemoryDetail/components/EmotionLine.tsx
index 3652e7c5..c62fbfb9 100644
--- a/web/src/views/UserMemoryDetail/components/EmotionLine.tsx
+++ b/web/src/views/UserMemoryDetail/components/EmotionLine.tsx
@@ -4,6 +4,7 @@ import ReactEcharts from 'echarts-for-react';
import Empty from '@/components/Empty'
import Loading from '@/components/Empty/Loading'
import type { Emotion } from './GraphDetail'
+import { format } from 'echarts';
interface EmotionLineProps {
chartData: Emotion[];
@@ -26,7 +27,7 @@ const EmotionLine: FC
= ({ chartData, loading }) => {
const seriesData = timePoints.map(time => dataMap.get(time) || 0)
return {
- name: emotionType,
+ name: t(`userMemory.${emotionType}`),
type: 'line',
smooth: true,
lineStyle: {
@@ -71,7 +72,7 @@ const EmotionLine: FC = ({ chartData, loading }) => {
formatter: function(params: any) {
let result = `${params[0].axisValue}
`
params.forEach((param: any) => {
- result += `${param.marker}${param.seriesName}: ${param.value}
`
+ result += `${param.marker}${param.seriesName}: ${param.value}%
`
})
return result
}
@@ -92,7 +93,7 @@ const EmotionLine: FC = ({ chartData, loading }) => {
},
grid: {
top: 16,
- left: 30,
+ left: 40,
right: 36,
bottom: 48,
// containLabel: false
@@ -103,7 +104,7 @@ const EmotionLine: FC = ({ chartData, loading }) => {
boundaryGap: false,
axisLabel: {
color: '#A8A9AA',
- fontFamily: 'PingFangSC, PingFang SC'
+ fontFamily: 'PingFangSC, PingFang SC',
},
axisLine: {
show: true,
@@ -130,7 +131,8 @@ const EmotionLine: FC = ({ chartData, loading }) => {
type: 'value',
axisLabel: {
color: '#A8A9AA',
- fontFamily: 'PingFangSC, PingFang SC'
+ fontFamily: 'PingFangSC, PingFang SC',
+ formatter: '{value}%'
},
axisLine: {
show: true,
@@ -152,7 +154,7 @@ const EmotionLine: FC = ({ chartData, loading }) => {
type: 'solid'
}
},
- max: 1,
+ max: 100,
min: 0
},
series: getSeries()
diff --git a/web/src/views/UserMemoryDetail/components/ForgetRefreshModal.tsx b/web/src/views/UserMemoryDetail/components/ForgetRefreshModal.tsx
new file mode 100644
index 00000000..1d0974e3
--- /dev/null
+++ b/web/src/views/UserMemoryDetail/components/ForgetRefreshModal.tsx
@@ -0,0 +1,113 @@
+import { forwardRef, useImperativeHandle, useState } from 'react';
+import { useParams } from 'react-router-dom'
+import { Form, Slider } from 'antd';
+import { useTranslation } from 'react-i18next';
+
+import RbModal from '@/components/RbModal'
+import { forgetTrigger } from '@/api/memory'
+import type { ForgetRefreshModalRef } from '../pages/ForgetDetail'
+
+interface ForgetRefreshModalProps {
+ refresh: (flag: boolean) => void;
+}
+
+const ForgetRefreshModal = forwardRef(({
+ refresh
+}, ref) => {
+ const { t } = useTranslation();
+ const { id } = useParams()
+ const [visible, setVisible] = useState(false);
+ const [form] = Form.useForm<{ max_merge_batch_size: number; min_days_since_access: number; }>();
+ const [loading, setLoading] = useState(false)
+ const values = Form.useWatch([], form);
+
+ // 封装取消方法,添加关闭弹窗逻辑
+ const handleClose = () => {
+ setVisible(false);
+ form.resetFields();
+ setLoading(false)
+ };
+
+ const handleOpen = () => {
+ form.resetFields();
+ setVisible(true);
+ };
+ // 封装保存方法,添加提交逻辑
+ const handleSave = () => {
+ if(!id) return
+ form
+ .validateFields()
+ .then((values) => {
+ setLoading(true)
+ forgetTrigger({
+ ...values,
+ end_user_id: id
+ })
+ .then(() => {
+ refresh(true)
+ handleClose()
+ })
+ .finally(() => {
+ setLoading(false)
+ })
+ })
+ .catch((err) => {
+ console.log('err', err)
+ });
+ }
+
+ // 暴露给父组件的方法
+ useImperativeHandle(ref, () => ({
+ handleOpen,
+ handleClose
+ }));
+
+ return (
+
+
+
+ );
+});
+
+export default ForgetRefreshModal;
\ No newline at end of file
diff --git a/web/src/views/UserMemoryDetail/components/InteractionBar.tsx b/web/src/views/UserMemoryDetail/components/InteractionBar.tsx
index 51224676..0db33b6f 100644
--- a/web/src/views/UserMemoryDetail/components/InteractionBar.tsx
+++ b/web/src/views/UserMemoryDetail/components/InteractionBar.tsx
@@ -1,4 +1,4 @@
-import { type FC } from 'react'
+import { type FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import ReactEcharts from 'echarts-for-react'
import Empty from '@/components/Empty'
@@ -14,11 +14,13 @@ const Colors = ['#155EEF', '#369F21', '#FF5D34']
const InteractionBar: FC = ({ chartData, loading }) => {
const { t } = useTranslation()
- const series = [{
- name: 'Interaction Count',
- type: 'bar',
- data: chartData.map(item => item.count)
- }]
+ const series = useMemo(() => {
+ return [{
+ name: t('userMemory.interactionCountData'),
+ type: 'bar',
+ data: chartData.map(item => item.count)
+ }]
+ }, [chartData, t])
return (
<>
@@ -80,6 +82,7 @@ const InteractionBar: FC = ({ chartData, loading }) => {
},
yAxis: {
type: 'value',
+ minInterval: 1,
axisLabel: {
color: '#A8A9AA',
fontFamily: 'PingFangSC, PingFang SC'
@@ -104,8 +107,6 @@ const InteractionBar: FC = ({ chartData, loading }) => {
type: 'solid'
}
},
- max: 1,
- min: 0
},
series
}}
diff --git a/web/src/views/UserMemoryDetail/components/PageHeader.tsx b/web/src/views/UserMemoryDetail/components/PageHeader.tsx
index 56da70e0..68cdada8 100644
--- a/web/src/views/UserMemoryDetail/components/PageHeader.tsx
+++ b/web/src/views/UserMemoryDetail/components/PageHeader.tsx
@@ -1,20 +1,22 @@
import { type FC, type ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
-import { Layout } from 'antd';
+import { Layout, Space, Button } from 'antd';
import { useTranslation } from 'react-i18next';
-import logoutIcon from '@/assets/images/logout.svg'
+import logoutIcon from '@/assets/images/logout_hover.svg'
const { Header } = Layout;
interface ConfigHeaderProps {
name?: string;
operation?: ReactNode;
- source?: 'detail' | 'node'
+ source?: 'detail' | 'node';
+ extra?: ReactNode;
}
const PageHeader: FC = ({
name,
operation,
- source = 'detail'
+ source = 'detail',
+ extra
}) => {
const { t } = useTranslation();
const navigate = useNavigate();
@@ -33,10 +35,13 @@ const PageHeader: FC = ({
{operation}
-