238 lines
9.4 KiB
TypeScript
238 lines
9.4 KiB
TypeScript
"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>
|
||
);
|
||
}
|