- 配置 Playwright:baseURL 改为 localhost:2026,视频仅在 CI 保留 - 更新 .gitignore 排除 Playwright 报告/缓存 - 新增线程记忆 E2E 测试:验证发送消息后可加载 summary 且无日志报错 - thread-memory-panel 添加 data-testid 属性便于定位
144 lines
4.8 KiB
TypeScript
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>
|
|
);
|
|
}
|