feat: expose master agent evolution dashboard

This commit is contained in:
kris
2026-04-16 05:46:45 +08:00
parent 504d112218
commit f0490de180
12 changed files with 445 additions and 23 deletions

View File

@@ -0,0 +1,237 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import clsx from "clsx";
type EvolutionConfig = {
mode: "controlled" | "autonomous";
autoApplyLowRiskRules: boolean;
};
type EvolutionSignal = {
signalId: string;
kind: string;
requestText: string;
createdAt: string;
};
type EvolutionProposal = {
proposalId: string;
proposalType: string;
title: string;
summary: string;
status: string;
confidence: number;
riskLevel: string;
createdAt: string;
};
type EvolutionRule = {
ruleId: string;
ruleType: string;
createdAt: string;
sourceProposalId?: string;
};
function formatTime(value?: string) {
if (!value) {
return "-";
}
return new Date(value).toLocaleString("zh-CN", { hour12: false });
}
async function readJson<T>(response: Response): Promise<T> {
return (await response.json()) as T;
}
export function MasterAgentEvolutionClient({
isAdmin,
config,
signals,
proposals,
rules,
}: {
isAdmin: boolean;
config: EvolutionConfig;
signals: EvolutionSignal[];
proposals: EvolutionProposal[];
rules: EvolutionRule[];
}) {
const router = useRouter();
const [busyKey, setBusyKey] = useState<string | null>(null);
const [message, setMessage] = useState("");
async function switchMode(mode: "controlled" | "autonomous") {
setBusyKey(`mode:${mode}`);
const response = await fetch("/api/v1/master-agent/evolution/config", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mode }),
});
const result = await readJson<{ ok: boolean; message?: string }>(response);
setBusyKey(null);
setMessage(result.ok ? `已切到 ${mode === "autonomous" ? "完全自我进化" : "受控自动进化"}` : result.message ?? "切换失败。");
if (result.ok) {
router.refresh();
}
}
async function reviewProposal(proposalId: string, action: "approve" | "reject") {
setBusyKey(`${action}:${proposalId}`);
const response = await fetch(`/api/v1/master-agent/evolution/proposals/${proposalId}/${action}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const result = await readJson<{ ok: boolean; message?: string }>(response);
setBusyKey(null);
setMessage(result.ok ? (action === "approve" ? "提案已批准。" : "提案已拒绝。") : result.message ?? "提交失败。");
if (result.ok) {
router.refresh();
}
}
const pendingProposals = proposals.filter((item) => item.status === "pending_review");
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="flex items-center justify-between gap-3">
<div>
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="mt-1 text-[12px] leading-6 text-[#8C8C8C]">
{config.mode === "autonomous" ? "完全自我进化" : "受控自动进化"}
{config.autoApplyLowRiskRules ? "低风险提案会自动采纳。" : "所有提案都需要人工确认。"}
</div>
</div>
<span
className={clsx(
"rounded-full px-3 py-1 text-[11px] font-semibold",
config.mode === "autonomous" ? "bg-[#EEF8F1] text-[#117A37]" : "bg-[#FFF5E8] text-[#B54708]",
)}
>
{config.mode === "autonomous" ? "全自动" : "受控"}
</span>
</div>
{isAdmin ? (
<div className="mt-4 grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => void switchMode("controlled")}
disabled={busyKey === "mode:controlled" || config.mode === "controlled"}
className="rounded-full border border-[#E5E5EA] bg-white px-4 py-2 text-[13px] font-semibold text-[#111111] disabled:opacity-60"
>
{busyKey === "mode:controlled" ? "切换中" : "切到受控模式"}
</button>
<button
type="button"
onClick={() => void switchMode("autonomous")}
disabled={busyKey === "mode:autonomous" || config.mode === "autonomous"}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === "mode:autonomous" ? "切换中" : "切到全自动模式"}
</button>
</div>
) : null}
</div>
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<span className="rounded-full bg-[#F2F3F5] px-3 py-1 text-[11px] font-semibold text-[#57606A]">
{pendingProposals.length}
</span>
</div>
<div className="mt-3 space-y-3">
{pendingProposals.length === 0 ? (
<div className="rounded-xl bg-[#F7F8FA] px-3 py-3 text-[13px] text-[#8C8C8C]"></div>
) : (
pendingProposals.map((proposal) => (
<div key={proposal.proposalId} className="rounded-xl border border-[#EAECEF] px-3 py-3">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[14px] font-semibold text-[#111111]">{proposal.title}</div>
<div className="mt-1 text-[12px] leading-6 text-[#57606A]">{proposal.summary}</div>
</div>
<span className="rounded-full bg-[#FFF5E8] px-2 py-1 text-[11px] font-semibold text-[#B54708]">
{proposal.riskLevel} / {Math.round(proposal.confidence * 100)}%
</span>
</div>
<div className="mt-2 text-[12px] text-[#8C8C8C]">
{proposal.proposalType} · {formatTime(proposal.createdAt)}
</div>
{isAdmin ? (
<div className="mt-3 grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => void reviewProposal(proposal.proposalId, "reject")}
disabled={busyKey === `reject:${proposal.proposalId}`}
className="rounded-full border border-[#E5E5EA] bg-white px-4 py-2 text-[13px] font-semibold text-[#111111] disabled:opacity-60"
>
{busyKey === `reject:${proposal.proposalId}` ? "处理中" : "拒绝"}
</button>
<button
type="button"
onClick={() => void reviewProposal(proposal.proposalId, "approve")}
disabled={busyKey === `approve:${proposal.proposalId}`}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === `approve:${proposal.proposalId}` ? "处理中" : "批准"}
</button>
</div>
) : null}
</div>
))
)}
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<span className="rounded-full bg-[#F2F3F5] px-3 py-1 text-[11px] font-semibold text-[#57606A]">
{signals.length}
</span>
</div>
<div className="mt-3 space-y-3">
{signals.slice(0, 8).map((signal) => (
<div key={signal.signalId} className="rounded-xl bg-[#F7F8FA] px-3 py-3">
<div className="text-[13px] font-semibold text-[#111111]">{signal.kind}</div>
<div className="mt-1 text-[12px] leading-6 text-[#57606A]">{signal.requestText}</div>
<div className="mt-1 text-[12px] text-[#8C8C8C]">{formatTime(signal.createdAt)}</div>
</div>
))}
</div>
</div>
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<span className="rounded-full bg-[#F2F3F5] px-3 py-1 text-[11px] font-semibold text-[#57606A]">
{rules.length}
</span>
</div>
<div className="mt-3 space-y-3">
{rules.slice(0, 8).map((rule) => (
<div key={rule.ruleId} className="rounded-xl bg-[#F7F8FA] px-3 py-3">
<div className="text-[13px] font-semibold text-[#111111]">{rule.ruleType}</div>
<div className="mt-1 text-[12px] text-[#8C8C8C]">
{formatTime(rule.createdAt)}
{rule.sourceProposalId ? ` · ${rule.sourceProposalId}` : ""}
</div>
</div>
))}
</div>
</div>
</div>
{message ? (
<div className="rounded-2xl border border-[#D8E8FF] bg-[#F4F8FF] px-4 py-3 text-[13px] text-[#2457C5]">
{message}
</div>
) : null}
</div>
);
}