feat: expose master agent evolution dashboard
This commit is contained in:
237
src/components/master-agent-evolution-client.tsx
Normal file
237
src/components/master-agent-evolution-client.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user