feat(web): 視覺化 AwoooP 操作決策圖譜
This commit is contained in:
@@ -614,6 +614,209 @@ function ProgressRow({
|
||||
);
|
||||
}
|
||||
|
||||
function clampPercent(value: number): number {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
return Math.max(0, Math.min(100, value));
|
||||
}
|
||||
|
||||
function VisualFlowNode({
|
||||
label,
|
||||
value,
|
||||
detail,
|
||||
icon: Icon,
|
||||
tone,
|
||||
}: {
|
||||
label: string;
|
||||
value: string | number;
|
||||
detail: string;
|
||||
icon: typeof Activity;
|
||||
tone: "good" | "warn" | "neutral";
|
||||
}) {
|
||||
return (
|
||||
<div className="relative min-h-[132px] min-w-0 border border-[#e0ddd4] bg-white px-4 py-3">
|
||||
<div className="flex min-w-0 items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{label}</p>
|
||||
<p className="mt-2 break-words font-mono text-2xl font-semibold leading-none text-[#141413]">
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-8 w-8 shrink-0 items-center justify-center border",
|
||||
tone === "good" && "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]",
|
||||
tone === "warn" && "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
|
||||
tone === "neutral" && "border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-3 text-xs leading-5 text-[#5f5b52]">{detail}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VisualGateCell({
|
||||
label,
|
||||
value,
|
||||
detail,
|
||||
percent,
|
||||
tone,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
detail: string;
|
||||
percent: number;
|
||||
tone: "good" | "warn" | "neutral";
|
||||
}) {
|
||||
return (
|
||||
<div className="min-w-0 bg-white px-4 py-3">
|
||||
<div className="flex min-w-0 flex-wrap items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{label}</p>
|
||||
<p className="mt-1 text-xs leading-5 text-[#5f5b52]">{detail}</p>
|
||||
</div>
|
||||
<span className="shrink-0 font-mono text-sm font-semibold text-[#141413]">{value}</span>
|
||||
</div>
|
||||
<div className="mt-3 h-2 overflow-hidden border border-[#d8d3c7] bg-[#faf9f3]">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full",
|
||||
tone === "good" && "bg-[#6aa879]",
|
||||
tone === "warn" && "bg-[#d9b36f]",
|
||||
tone === "neutral" && "bg-[#9bb6d9]"
|
||||
)}
|
||||
style={{ width: `${clampPercent(percent)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OperationsDecisionMap({
|
||||
snapshot,
|
||||
sourceFlow,
|
||||
sourceError,
|
||||
quality,
|
||||
qualityError,
|
||||
}: {
|
||||
snapshot: Snapshot;
|
||||
sourceFlow: SourceFlowSummary | null;
|
||||
sourceError: boolean;
|
||||
quality: AutomationQualitySummary | null;
|
||||
qualityError: boolean;
|
||||
}) {
|
||||
const t = useTranslations("awooop.home.visualOps");
|
||||
const sourceTotal = sourceFlow?.source_event_total ?? 0;
|
||||
const linkedRuns = sourceFlow?.linked_run_total ?? 0;
|
||||
const openWork = sourceFlow?.open_work_item_group_total ?? 0;
|
||||
const manualGates = sourceFlow?.manual_gate_group_total ?? 0;
|
||||
const failedRepairs = sourceFlow?.failed_repair_group_total ?? 0;
|
||||
const evaluated = quality?.evaluated_total ?? 0;
|
||||
const verified = quality?.verified_auto_repair_total ?? 0;
|
||||
const red = quality?.score_buckets.red ?? 0;
|
||||
const yellow = quality?.score_buckets.yellow ?? 0;
|
||||
const green = quality?.score_buckets.green ?? 0;
|
||||
const linkedPercent = percentValue(linkedRuns, sourceTotal);
|
||||
const workClearPercent = percentValue(Math.max(0, (sourceFlow?.recurrence_group_total ?? 0) - openWork), sourceFlow?.recurrence_group_total ?? 0);
|
||||
const verifiedPercent = percentValue(verified, evaluated);
|
||||
const qualityHealthyPercent = percentValue(green, green + yellow + red);
|
||||
const isDegraded = sourceError || qualityError;
|
||||
|
||||
return (
|
||||
<section className="border border-[#e0ddd4] bg-[#e0ddd4]">
|
||||
<div className="flex min-w-0 flex-wrap items-start justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
|
||||
<div className="flex min-w-0 items-start gap-2">
|
||||
<Waypoints className="mt-0.5 h-4 w-4 shrink-0 text-[#d97757]" aria-hidden="true" />
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-semibold text-[#141413]">{t("title")}</h3>
|
||||
<p className="mt-1 text-xs leading-5 text-[#77736a]">{t("subtitle")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex shrink-0 border px-2 py-0.5 text-xs font-semibold",
|
||||
isDegraded && "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]",
|
||||
!isDegraded && "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]"
|
||||
)}
|
||||
>
|
||||
{isDegraded ? t("status.degraded") : t("status.ready")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-2 xl:grid-cols-5">
|
||||
<VisualFlowNode
|
||||
label={t("flow.ingest.label")}
|
||||
value={sourceError ? "--" : sourceTotal}
|
||||
detail={t("flow.ingest.detail", { linked: linkedRuns })}
|
||||
icon={GitBranch}
|
||||
tone={sourceError ? "warn" : sourceTotal > 0 ? "good" : "neutral"}
|
||||
/>
|
||||
<VisualFlowNode
|
||||
label={t("flow.evidence.label")}
|
||||
value={`${linkedPercent.toFixed(0)}%`}
|
||||
detail={t("flow.evidence.detail")}
|
||||
icon={SearchCheck}
|
||||
tone={linkedPercent >= 80 ? "good" : linkedPercent > 0 ? "warn" : "neutral"}
|
||||
/>
|
||||
<VisualFlowNode
|
||||
label={t("flow.gate.label")}
|
||||
value={manualGates + numberValue(snapshot.approvals)}
|
||||
detail={t("flow.gate.detail", { work: openWork })}
|
||||
icon={ShieldCheck}
|
||||
tone={manualGates + numberValue(snapshot.approvals) > 0 ? "warn" : "good"}
|
||||
/>
|
||||
<VisualFlowNode
|
||||
label={t("flow.verify.label")}
|
||||
value={`${verified}/${evaluated}`}
|
||||
detail={t("flow.verify.detail", { failed: failedRepairs })}
|
||||
icon={CheckCircle2}
|
||||
tone={verifiedPercent >= 80 ? "good" : evaluated > 0 ? "warn" : "neutral"}
|
||||
/>
|
||||
<VisualFlowNode
|
||||
label={t("flow.learn.label")}
|
||||
value={`${green}/${green + yellow + red}`}
|
||||
detail={t("flow.learn.detail")}
|
||||
icon={BrainCircuit}
|
||||
tone={qualityHealthyPercent >= 80 ? "good" : green + yellow + red > 0 ? "warn" : "neutral"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-2 xl:grid-cols-4">
|
||||
<VisualGateCell
|
||||
label={t("matrix.source.label")}
|
||||
value={`${linkedPercent.toFixed(0)}%`}
|
||||
detail={t("matrix.source.detail")}
|
||||
percent={linkedPercent}
|
||||
tone={linkedPercent >= 80 ? "good" : "warn"}
|
||||
/>
|
||||
<VisualGateCell
|
||||
label={t("matrix.work.label")}
|
||||
value={`${workClearPercent.toFixed(0)}%`}
|
||||
detail={t("matrix.work.detail", { count: openWork })}
|
||||
percent={workClearPercent}
|
||||
tone={openWork === 0 ? "good" : "warn"}
|
||||
/>
|
||||
<VisualGateCell
|
||||
label={t("matrix.repair.label")}
|
||||
value={`${verifiedPercent.toFixed(0)}%`}
|
||||
detail={t("matrix.repair.detail")}
|
||||
percent={verifiedPercent}
|
||||
tone={verifiedPercent >= 80 ? "good" : "warn"}
|
||||
/>
|
||||
<VisualGateCell
|
||||
label={t("matrix.quality.label")}
|
||||
value={`${qualityHealthyPercent.toFixed(0)}%`}
|
||||
detail={t("matrix.quality.detail", { red, yellow })}
|
||||
percent={qualityHealthyPercent}
|
||||
tone={red === 0 ? "good" : "warn"}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function SourceFlowActionLink({
|
||||
href,
|
||||
label,
|
||||
@@ -1539,6 +1742,14 @@ export default function AwoooPPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<OperationsDecisionMap
|
||||
snapshot={snapshot}
|
||||
sourceFlow={sourceFlowSummary}
|
||||
sourceError={sourceFlowError}
|
||||
quality={qualitySummary}
|
||||
qualityError={qualityError}
|
||||
/>
|
||||
|
||||
<section className="border border-[#e0ddd4] bg-white">
|
||||
<div className="grid gap-px bg-[#e0ddd4] lg:grid-cols-[1.4fr_0.9fr]">
|
||||
<div className="bg-white p-5">
|
||||
|
||||
Reference in New Issue
Block a user