Files
boss/src/components/master-agent-prompt-memory-client.tsx

832 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import clsx from "clsx";
import type {
MasterAgentMemory,
MasterAgentPromptPolicy,
MasterMemoryScope,
MasterMemoryType,
ProjectAgentControls,
UserMasterPrompt,
} from "@/lib/boss-data";
import type { MasterAgentChatPageAnchors } from "@/lib/master-agent-chat-menu";
import { getMasterAgentModelOptions } from "@/lib/master-agent-model-options";
import { formatTimestampLabel } from "@/lib/boss-projections";
type MemoryDraft = {
scope: MasterMemoryScope;
projectId: string;
title: string;
content: string;
memoryType: MasterMemoryType;
tags: string;
sourceMessageId: string;
};
type ClawAvailability = {
status: "disabled" | "misconfigured" | "ready";
selectable: boolean;
reason: string;
reasonLabel: string;
};
const memoryScopeOptions: Array<{ value: MasterMemoryScope; label: string }> = [
{ value: "global", label: "通用记忆" },
{ value: "project", label: "项目记忆" },
];
const memoryTypeOptions: Array<{ value: MasterMemoryType; label: string }> = [
{ value: "user_preference", label: "用户偏好" },
{ value: "project_progress", label: "项目进度" },
{ value: "decision", label: "决策" },
{ value: "risk", label: "风险" },
{ value: "blocking_issue", label: "阻塞" },
{ value: "research_note", label: "调研" },
{ value: "workflow_rule", label: "工作规则" },
];
function memoryTypeLabel(value: MasterMemoryType) {
return memoryTypeOptions.find((item) => item.value === value)?.label ?? value;
}
function memoryScopeLabel(value: MasterMemoryScope) {
return memoryScopeOptions.find((item) => item.value === value)?.label ?? value;
}
function tagsToText(tags: string[]) {
return tags.join(", ");
}
function textToTags(value: string) {
return value
.split(/[,,、\n]/)
.map((item) => item.trim())
.filter(Boolean);
}
function draftFromMemory(memory: MasterAgentMemory): MemoryDraft {
return {
scope: memory.scope,
projectId: memory.projectId ?? "master-agent",
title: memory.title,
content: memory.content,
memoryType: memory.memoryType,
tags: tagsToText(memory.tags),
sourceMessageId: memory.sourceMessageId ?? "",
};
}
function makeNewMemoryDraft(): MemoryDraft {
return {
scope: "global",
projectId: "",
title: "",
content: "",
memoryType: "user_preference",
tags: "",
sourceMessageId: "",
};
}
function Field({
label,
value,
onChange,
placeholder,
type = "text",
}: {
label: string;
value: string;
onChange: (value: string) => void;
placeholder?: string;
type?: "text" | "password";
}) {
return (
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]">{label}</div>
<input
type={type}
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
/>
</label>
);
}
function TextArea({
label,
value,
onChange,
placeholder,
readOnly = false,
}: {
label: string;
value: string;
onChange: (value: string) => void;
placeholder?: string;
readOnly?: boolean;
}) {
return (
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]">{label}</div>
<textarea
value={value}
onChange={(event) => onChange(event.target.value)}
readOnly={readOnly}
placeholder={placeholder}
rows={6}
className={clsx(
"w-full rounded-xl border px-3 py-2 text-[13px] leading-6 text-[#111111] outline-none",
readOnly ? "border-[#E5E5EA] bg-[#F7F8FA] text-[#57606A]" : "border-[#E5E5EA] bg-white",
)}
/>
</label>
);
}
export function MasterAgentPromptMemoryClient({
isAdmin,
promptPolicy,
userPrompt,
projectControls,
clawAvailability,
globalMemories,
projectMemories,
anchors,
}: {
isAdmin: boolean;
promptPolicy: MasterAgentPromptPolicy | null;
userPrompt: UserMasterPrompt | null;
projectControls: ProjectAgentControls | null;
clawAvailability: ClawAvailability;
globalMemories: MasterAgentMemory[];
projectMemories: MasterAgentMemory[];
anchors: MasterAgentChatPageAnchors;
}) {
const router = useRouter();
const [busyKey, setBusyKey] = useState<string | null>(null);
const [message, setMessage] = useState("");
const [globalPrompt, setGlobalPrompt] = useState(promptPolicy?.globalPrompt ?? "");
const [userPromptContent, setUserPromptContent] = useState(userPrompt?.content ?? "");
const [modelOverride, setModelOverride] = useState(projectControls?.modelOverride ?? "");
const [reasoningEffortOverride, setReasoningEffortOverride] = useState(
projectControls?.reasoningEffortOverride ?? "",
);
const [promptOverride, setPromptOverride] = useState(projectControls?.promptOverride ?? "");
const storedClawOverrideUnavailable =
projectControls?.backendOverride === "claw-runtime" && !clawAvailability.selectable;
const [backendOverride, setBackendOverride] = useState(
projectControls?.backendOverride === "claw-runtime" && clawAvailability.selectable ? "claw-runtime" : "",
);
const [newMemory, setNewMemory] = useState<MemoryDraft>(makeNewMemoryDraft());
const [memoryDrafts, setMemoryDrafts] = useState<Record<string, MemoryDraft>>(() => {
const next: Record<string, MemoryDraft> = {};
[...globalMemories, ...projectMemories].forEach((memory) => {
next[memory.memoryId] = draftFromMemory(memory);
});
return next;
});
const allMemories = useMemo(() => [...projectMemories, ...globalMemories], [projectMemories, globalMemories]);
const modelOptions = useMemo(() => getMasterAgentModelOptions(modelOverride), [modelOverride]);
const promptPreview = useMemo(() => {
const sections = [
globalPrompt.trim() ? `【管理员全局主提示词】\n${globalPrompt.trim()}` : null,
userPromptContent.trim() ? `【用户私有主提示词】\n${userPromptContent.trim()}` : null,
promptOverride.trim() ? `【当前对话附加提示词】\n${promptOverride.trim()}` : null,
backendOverride.trim()
? `【执行后端】\n${backendOverride.trim()}`
: storedClawOverrideUnavailable
? "【执行后端】\n默认Claw Runtime 当前不可用,运行时会自动回退)"
: null,
].filter(Boolean);
return sections.length > 0 ? sections.join("\n\n") : "当前还没有组合后的提示词内容。";
}, [backendOverride, globalPrompt, promptOverride, storedClawOverrideUnavailable, userPromptContent]);
function updateMemoryDraft(memoryId: string, updater: (draft: MemoryDraft) => MemoryDraft) {
setMemoryDrafts((current) => ({
...current,
[memoryId]: updater(current[memoryId] ?? draftFromMemory(allMemories.find((item) => item.memoryId === memoryId)!)),
}));
}
async function saveGlobalPrompt() {
if (!isAdmin) {
setMessage("只有管理员可以编辑全局主提示词。");
return;
}
setBusyKey("global_prompt");
const response = await fetch("/api/v1/master-agent/prompt-policy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ globalPrompt }),
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusyKey(null);
setMessage(result.ok ? "管理员全局主提示词已保存。" : result.message ?? "保存失败。");
if (result.ok) router.refresh();
}
async function saveUserPrompt() {
setBusyKey("user_prompt");
const response = await fetch("/api/v1/master-agent/prompt", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: userPromptContent }),
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusyKey(null);
setMessage(result.ok ? "用户主提示词已保存。" : result.message ?? "保存失败。");
if (result.ok) router.refresh();
}
async function clearUserPrompt() {
setBusyKey("user_prompt_clear");
const response = await fetch("/api/v1/master-agent/prompt", {
method: "DELETE",
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusyKey(null);
setMessage(result.ok ? "用户主提示词已清空。" : result.message ?? "清空失败。");
if (result.ok) {
setUserPromptContent("");
router.refresh();
}
}
async function saveConversationPrompt() {
setBusyKey("conversation_prompt");
const response = await fetch("/api/v1/projects/master-agent/agent-controls", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
modelOverride: modelOverride.trim() || null,
reasoningEffortOverride: reasoningEffortOverride.trim() || null,
promptOverride: promptOverride.trim() || null,
backendOverride: backendOverride.trim() || null,
}),
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusyKey(null);
setMessage(result.ok ? "当前对话覆盖已保存。" : result.message ?? "保存失败。");
if (result.ok) router.refresh();
}
async function createMemory() {
if (!newMemory.title.trim() || !newMemory.content.trim()) {
setMessage("记忆标题和内容不能为空。");
return;
}
if (newMemory.scope === "project" && !newMemory.projectId.trim()) {
setMessage("项目记忆必须填写 projectId。");
return;
}
setBusyKey("memory_create");
const response = await fetch("/api/v1/master-agent/memories", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
scope: newMemory.scope,
projectId: newMemory.scope === "project" ? newMemory.projectId.trim() : undefined,
title: newMemory.title.trim(),
content: newMemory.content.trim(),
memoryType: newMemory.memoryType,
tags: textToTags(newMemory.tags),
sourceMessageId: newMemory.sourceMessageId.trim() || undefined,
}),
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusyKey(null);
setMessage(result.ok ? "记忆已新增。" : result.message ?? "新增失败。");
if (result.ok) {
setNewMemory(makeNewMemoryDraft());
router.refresh();
}
}
async function saveMemory(memoryId: string) {
const draft = memoryDrafts[memoryId];
if (!draft?.title.trim() || !draft.content.trim()) {
setMessage("记忆标题和内容不能为空。");
return;
}
if (draft.scope === "project" && !draft.projectId.trim()) {
setMessage("项目记忆必须填写 projectId。");
return;
}
setBusyKey(`memory_save:${memoryId}`);
const response = await fetch(`/api/v1/master-agent/memories/${memoryId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
scope: draft.scope,
projectId: draft.scope === "project" ? draft.projectId.trim() : null,
title: draft.title.trim(),
content: draft.content.trim(),
memoryType: draft.memoryType,
tags: textToTags(draft.tags),
sourceMessageId: draft.sourceMessageId.trim() || null,
}),
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusyKey(null);
setMessage(result.ok ? "记忆已保存。" : result.message ?? "保存失败。");
if (result.ok) router.refresh();
}
async function archiveMemory(memoryId: string) {
setBusyKey(`memory_delete:${memoryId}`);
const response = await fetch(`/api/v1/master-agent/memories/${memoryId}`, {
method: "DELETE",
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusyKey(null);
setMessage(result.ok ? "记忆已归档。" : result.message ?? "删除失败。");
if (result.ok) router.refresh();
}
return (
<div className="flex flex-col gap-4 px-[18px] pb-6">
<div id={anchors.prompt.split("#")[1]} className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 scroll-mt-4">
<div className="text-[16px] font-semibold text-[#111111]"> Agent </div>
<div className="mt-2 text-[12px] leading-6 text-[#8C8C8C]">
</div>
</div>
<div className="space-y-3 rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="mt-1 text-[12px] text-[#8C8C8C]"></div>
</div>
<span className="rounded-full bg-[#EEF5FF] px-3 py-1 text-[11px] font-semibold text-[#2457C5]">
</span>
</div>
<TextArea
label="全局主提示词"
value={globalPrompt}
onChange={setGlobalPrompt}
placeholder="请输入管理员全局主提示词"
readOnly={!isAdmin}
/>
<button
type="button"
onClick={() => void saveGlobalPrompt()}
disabled={!isAdmin || busyKey === "global_prompt"}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === "global_prompt" ? "保存中" : isAdmin ? "保存全局主提示词" : "仅管理员可修改"}
</button>
</div>
<div className="space-y-3 rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="text-[12px] leading-6 text-[#8C8C8C]"> Agent </div>
<TextArea
label="用户私有主提示词"
value={userPromptContent}
onChange={setUserPromptContent}
placeholder="例如:回复要简洁、直接、中文优先"
/>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => void saveUserPrompt()}
disabled={busyKey === "user_prompt"}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === "user_prompt" ? "保存中" : "保存用户提示词"}
</button>
<button
type="button"
onClick={() => void clearUserPrompt()}
disabled={busyKey === "user_prompt_clear"}
className="rounded-full border border-[#E5E5EA] bg-white px-4 py-2 text-[13px] font-semibold text-[#111111] disabled:opacity-60"
>
{busyKey === "user_prompt_clear" ? "清空中" : "清空"}
</button>
</div>
</div>
<div className="space-y-3 rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="mt-1 text-[12px] text-[#8C8C8C]"> master-agent </div>
</div>
<span className="rounded-full bg-[#FFF5E8] px-3 py-1 text-[11px] font-semibold text-[#B54708]">
</span>
</div>
<div className="grid gap-3 md:grid-cols-3">
<label id={anchors.model.split("#")[1]} className="space-y-1 scroll-mt-4">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={modelOverride}
onChange={(event) => setModelOverride(event.target.value)}
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
>
<option value=""></option>
{modelOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</label>
<label id={anchors.reasoningEffort.split("#")[1]} className="space-y-1 scroll-mt-4">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={reasoningEffortOverride}
onChange={(event) => setReasoningEffortOverride(event.target.value)}
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
>
<option value=""></option>
<option value="low">low</option>
<option value="medium">medium</option>
<option value="high">high</option>
</select>
</label>
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={backendOverride}
onChange={(event) => setBackendOverride(event.target.value)}
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
>
<option value=""></option>
{clawAvailability.selectable ? <option value="claw-runtime">Claw Runtime</option> : null}
</select>
</label>
</div>
{!clawAvailability.selectable ? (
<div className="rounded-2xl border border-[#F4C7C3] bg-[#FFF7F6] px-4 py-3 text-[12px] leading-6 text-[#B54708]">
<div className="font-semibold text-[#912018]">Claw Runtime </div>
<div>{clawAvailability.reasonLabel}</div>
{storedClawOverrideUnavailable ? (
<div className="mt-1 text-[#912018]">
Claw Runtime退
</div>
) : null}
</div>
) : null}
<TextArea
label="当前对话附加提示词"
value={promptOverride}
onChange={setPromptOverride}
placeholder="例如:这轮先输出结论,再输出执行计划"
/>
<button
type="button"
onClick={() => void saveConversationPrompt()}
disabled={busyKey === "conversation_prompt"}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === "conversation_prompt" ? "保存中" : "保存当前对话设置"}
</button>
</div>
<div className="space-y-3 rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="text-[12px] leading-6 text-[#8C8C8C]">
Agent
</div>
<div className="whitespace-pre-wrap rounded-2xl bg-[#F7F8FA] px-4 py-4 text-[13px] leading-6 text-[#57606A]">
{promptPreview}
</div>
</div>
<div id={anchors.memory.split("#")[1]} className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 scroll-mt-4">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="mt-2 text-[12px] leading-6 text-[#8C8C8C]">
master-agent
</div>
<div className="mt-4 space-y-3">
<div className="grid gap-3 md:grid-cols-2">
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={newMemory.memoryType}
onChange={(event) =>
setNewMemory((current) => ({
...current,
memoryType: event.target.value as MasterMemoryType,
}))
}
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
>
{memoryTypeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={newMemory.scope}
onChange={(event) =>
setNewMemory((current) => ({
...current,
scope: event.target.value as MasterMemoryScope,
}))
}
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
>
{memoryScopeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
</div>
{newMemory.scope === "project" ? (
<Field
label="projectId"
value={newMemory.projectId}
onChange={(value) => setNewMemory((current) => ({ ...current, projectId: value }))}
placeholder="例如 wenshenapp"
/>
) : null}
<Field
label="标题"
value={newMemory.title}
onChange={(value) => setNewMemory((current) => ({ ...current, title: value }))}
placeholder="例如:项目进度"
/>
<TextArea
label="内容"
value={newMemory.content}
onChange={(value) => setNewMemory((current) => ({ ...current, content: value }))}
placeholder="例如:主 Agent 提示词与记忆链路已经接通。"
/>
<Field
label="标签(逗号分隔)"
value={newMemory.tags}
onChange={(value) => setNewMemory((current) => ({ ...current, tags: value }))}
placeholder="例如主Agent, 记忆"
/>
<Field
label="sourceMessageId可选"
value={newMemory.sourceMessageId}
onChange={(value) => setNewMemory((current) => ({ ...current, sourceMessageId: value }))}
placeholder="可留空"
/>
<button
type="button"
onClick={() => void createMemory()}
disabled={busyKey === "memory_create"}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === "memory_create" ? "新增中" : "新增记忆"}
</button>
</div>
</div>
<div className="space-y-3 rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="text-[12px] text-[#8C8C8C]"></div>
{projectMemories.length === 0 ? (
<div className="rounded-2xl bg-[#F7F8FA] px-4 py-3 text-[12px] text-[#57606A]"></div>
) : null}
<div className="space-y-3">
{projectMemories.map((memory) => {
const draft = memoryDrafts[memory.memoryId] ?? draftFromMemory(memory);
return (
<div key={memory.memoryId} className="rounded-2xl border border-[#E5E5EA] bg-[#FCFCFD] px-4 py-4">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[13px] font-semibold text-[#111111]">{memory.title}</div>
<div className="mt-1 text-[11px] text-[#8C8C8C]">
{memoryScopeLabel(memory.scope)} · {memoryTypeLabel(memory.memoryType)}
{memory.projectId ? ` · ${memory.projectId}` : ""}
</div>
</div>
<div className="text-[11px] text-[#8C8C8C]">
{memory.archived ? "已归档" : formatTimestampLabel(memory.updatedAt)}
</div>
</div>
<div className="mt-3 grid gap-3">
<Field
label="标题"
value={draft.title}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, title: value }))
}
placeholder="记忆标题"
/>
<TextArea
label="内容"
value={draft.content}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, content: value }))
}
placeholder="记忆内容"
/>
<div className="grid gap-3 md:grid-cols-2">
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={draft.scope}
onChange={(event) =>
updateMemoryDraft(memory.memoryId, (current) => ({
...current,
scope: event.target.value as MasterMemoryScope,
}))
}
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
>
{memoryScopeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={draft.memoryType}
onChange={(event) =>
updateMemoryDraft(memory.memoryId, (current) => ({
...current,
memoryType: event.target.value as MasterMemoryType,
}))
}
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
>
{memoryTypeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
</div>
{draft.scope === "project" ? (
<Field
label="projectId"
value={draft.projectId}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, projectId: value }))
}
placeholder="例如 master-agent"
/>
) : null}
<Field
label="标签"
value={draft.tags}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, tags: value }))
}
placeholder="逗号分隔"
/>
<Field
label="sourceMessageId"
value={draft.sourceMessageId}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, sourceMessageId: value }))
}
placeholder="可选"
/>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => void saveMemory(memory.memoryId)}
disabled={busyKey === `memory_save:${memory.memoryId}`}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === `memory_save:${memory.memoryId}` ? "保存中" : "保存"}
</button>
<button
type="button"
onClick={() => void archiveMemory(memory.memoryId)}
disabled={busyKey === `memory_delete:${memory.memoryId}`}
className="rounded-full border border-[#E5E5EA] bg-white px-4 py-2 text-[13px] font-semibold text-[#111111] disabled:opacity-60"
>
{busyKey === `memory_delete:${memory.memoryId}` ? "归档中" : "删除"}
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
<div className="space-y-3 rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="text-[12px] text-[#8C8C8C]"></div>
{globalMemories.length === 0 ? (
<div className="rounded-2xl bg-[#F7F8FA] px-4 py-3 text-[12px] text-[#57606A]"></div>
) : null}
<div className="space-y-3">
{globalMemories.map((memory) => {
const draft = memoryDrafts[memory.memoryId] ?? draftFromMemory(memory);
return (
<div key={memory.memoryId} className="rounded-2xl border border-[#E5E5EA] bg-[#FCFCFD] px-4 py-4">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[13px] font-semibold text-[#111111]">{memory.title}</div>
<div className="mt-1 text-[11px] text-[#8C8C8C]">
{memoryScopeLabel(memory.scope)} · {memoryTypeLabel(memory.memoryType)}
</div>
</div>
<div className="text-[11px] text-[#8C8C8C]">
{memory.archived ? "已归档" : formatTimestampLabel(memory.updatedAt)}
</div>
</div>
<div className="mt-3 grid gap-3">
<Field
label="标题"
value={draft.title}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, title: value }))
}
placeholder="记忆标题"
/>
<TextArea
label="内容"
value={draft.content}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, content: value }))
}
placeholder="记忆内容"
/>
<div className="grid gap-3 md:grid-cols-2">
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={draft.memoryType}
onChange={(event) =>
updateMemoryDraft(memory.memoryId, (current) => ({
...current,
memoryType: event.target.value as MasterMemoryType,
}))
}
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
>
{memoryTypeOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<Field
label="标签"
value={draft.tags}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, tags: value }))
}
placeholder="逗号分隔"
/>
</div>
<Field
label="sourceMessageId"
value={draft.sourceMessageId}
onChange={(value) =>
updateMemoryDraft(memory.memoryId, (current) => ({ ...current, sourceMessageId: value }))
}
placeholder="可选"
/>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => void saveMemory(memory.memoryId)}
disabled={busyKey === `memory_save:${memory.memoryId}`}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === `memory_save:${memory.memoryId}` ? "保存中" : "保存"}
</button>
<button
type="button"
onClick={() => void archiveMemory(memory.memoryId)}
disabled={busyKey === `memory_delete:${memory.memoryId}`}
className="rounded-full border border-[#E5E5EA] bg-white px-4 py-2 text-[13px] font-semibold text-[#111111] disabled:opacity-60"
>
{busyKey === `memory_delete:${memory.memoryId}` ? "归档中" : "删除"}
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
{message ? (
<div className="rounded-2xl bg-[#F7F8FA] px-4 py-3 text-[12px] leading-6 text-[#57606A]">{message}</div>
) : null}
</div>
);
}