feat: add master-agent prompts and memory management

This commit is contained in:
kris
2026-04-01 04:10:11 +08:00
parent 9000a9f185
commit d316f0490e
31 changed files with 4398 additions and 32 deletions

View File

@@ -0,0 +1,771 @@
"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 { formatTimestampLabel } from "@/lib/boss-projections";
type MemoryDraft = {
scope: MasterMemoryScope;
projectId: string;
title: string;
content: string;
memoryType: MasterMemoryType;
tags: string;
sourceMessageId: 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: "master-agent",
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,
globalMemories,
projectMemories,
}: {
isAdmin: boolean;
promptPolicy: MasterAgentPromptPolicy | null;
userPrompt: UserMasterPrompt | null;
projectControls: ProjectAgentControls | null;
globalMemories: MasterAgentMemory[];
projectMemories: MasterAgentMemory[];
}) {
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 [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]);
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() {
if (!isAdmin) {
setMessage("只有管理员可以修改当前对话附加提示词。");
return;
}
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,
}),
});
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 className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-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-2">
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={modelOverride}
onChange={(event) => setModelOverride(event.target.value)}
disabled={!isAdmin}
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="gpt-5.4">gpt-5.4</option>
<option value="gpt-4.1">gpt-4.1</option>
<option value="gpt-4.1-mini">gpt-4.1-mini</option>
</select>
</label>
<label className="space-y-1">
<div className="text-[12px] text-[#8C8C8C]"></div>
<select
value={reasoningEffortOverride}
onChange={(event) => setReasoningEffortOverride(event.target.value)}
disabled={!isAdmin}
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>
</div>
<TextArea
label="当前对话附加提示词"
value={promptOverride}
onChange={setPromptOverride}
placeholder="例如:这轮先输出结论,再输出执行计划"
readOnly={!isAdmin}
/>
<button
type="button"
onClick={() => void saveConversationPrompt()}
disabled={!isAdmin || busyKey === "conversation_prompt"}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === "conversation_prompt" ? "保存中" : isAdmin ? "保存当前对话设置" : "仅管理员可修改"}
</button>
</div>
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="mt-2 text-[12px] leading-6 text-[#8C8C8C]">
</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="例如 master-agent"
/>
) : 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]"> master-agent </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>
);
}