deerflow2/frontend/src/components/workspace/thread-memory-panel.tsx
MT-Mint 29203a14b8 test(e2e): 添加 E2E 测试基础设施和线程记忆测试
- 配置 Playwright:baseURL 改为 localhost:2026,视频仅在 CI 保留
- 更新 .gitignore 排除 Playwright 报告/缓存
- 新增线程记忆 E2E 测试:验证发送消息后可加载 summary 且无日志报错
- thread-memory-panel 添加 data-testid 属性便于定位
2026-06-11 17:47:25 +08:00

144 lines
4.8 KiB
TypeScript

"use client";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { getBackendBaseURL } from "@/core/config";
import { useI18n } from "@/core/i18n/hooks";
type ThreadMemoryPanelProps = {
threadId?: string;
};
export function ThreadMemoryPanel({ threadId }: ThreadMemoryPanelProps) {
const [memorySummary, setMemorySummary] = useState("");
const [memoryVersion, setMemoryVersion] = useState<number | null>(null);
const [loadingSummary, setLoadingSummary] = useState(false);
const [savingSummary, setSavingSummary] = useState(false);
const [deletingMemory, setDeletingMemory] = useState(false);
const { t } = useI18n();
if (!threadId || threadId === "new") return null;
const handleLoadMemorySummary = async () => {
setLoadingSummary(true);
try {
const res = await fetch(
`${getBackendBaseURL()}/api/threads/${encodeURIComponent(threadId)}/memory-summary`,
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = (await res.json()) as { summary: string; memoryVersion: number };
setMemorySummary(data.summary ?? "");
setMemoryVersion(data.memoryVersion ?? 0);
toast.success(t.threadMemoryPanel.toastLoadSuccess);
} catch {
toast.error(t.threadMemoryPanel.toastLoadFailed);
} finally {
setLoadingSummary(false);
}
};
const handleSaveMemorySummary = async () => {
if (memoryVersion == null) return;
setSavingSummary(true);
try {
const res = await fetch(
`${getBackendBaseURL()}/api/threads/${encodeURIComponent(threadId)}/memory-summary`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ summary: memorySummary, memoryVersion }),
},
);
if (res.status === 409) {
toast.error(t.threadMemoryPanel.toastConflict);
return;
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = (await res.json()) as { memoryVersion?: number };
if (typeof data.memoryVersion === "number") setMemoryVersion(data.memoryVersion);
toast.success(t.threadMemoryPanel.toastSaveSuccess);
} catch {
toast.error(t.threadMemoryPanel.toastSaveFailed);
} finally {
setSavingSummary(false);
}
};
const handleDeleteMemory = async () => {
setDeletingMemory(true);
try {
const res = await fetch(
`${getBackendBaseURL()}/api/threads/${encodeURIComponent(threadId)}/memory`,
{ method: "DELETE" },
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setMemorySummary("");
setMemoryVersion(0);
toast.success(t.threadMemoryPanel.toastDeleteSuccess);
} catch {
toast.error(t.threadMemoryPanel.toastDeleteFailed);
} finally {
setDeletingMemory(false);
}
};
return (
<div
className="w-[380px] space-y-2 rounded-lg border border-ws-divider bg-ws-surface-elevated p-3 shadow-lg"
data-testid="thread-memory-panel"
>
<div className="text-sm font-semibold">
<span className="hidden sm:inline">{t.threadMemoryPanel.title}</span>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
onClick={() => {
void handleLoadMemorySummary();
}}
disabled={loadingSummary}
data-testid="thread-memory-load"
>
{loadingSummary ? t.threadMemoryPanel.loading : t.threadMemoryPanel.load}
</Button>
<Button
size="sm"
onClick={() => {
void handleSaveMemorySummary();
}}
disabled={savingSummary || memoryVersion == null}
>
{savingSummary ? t.threadMemoryPanel.saving : t.threadMemoryPanel.save}
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => {
void handleDeleteMemory();
}}
disabled={deletingMemory}
>
{deletingMemory ? t.threadMemoryPanel.removing : t.threadMemoryPanel.remove}
</Button>
</div>
<div className="text-xs text-ws-text-subtle-strong">
{t.threadMemoryPanel.threadId}: {threadId.slice(0, 8)}... |{" "}
{t.threadMemoryPanel.version}:{" "}
{memoryVersion == null ? t.threadMemoryPanel.unavailableVersion : memoryVersion}
</div>
<Textarea
value={memorySummary}
onChange={(e) => setMemorySummary(e.target.value)}
placeholder={t.threadMemoryPanel.summaryPlaceholder}
className="min-h-32 bg-white/80"
data-testid="thread-memory-summary"
/>
</div>
</div>
);
}