832 lines
34 KiB
TypeScript
832 lines
34 KiB
TypeScript
"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>
|
||
);
|
||
}
|