fix(web): surface ai route action summaries
This commit is contained in:
@@ -3437,6 +3437,29 @@ function aiRouteOperatorActionLabelKey(action?: string | null) {
|
||||
return "operatorActions.unknown";
|
||||
}
|
||||
|
||||
function aiRouteSummaryTone(mode?: string | null) {
|
||||
if (mode === "primary") {
|
||||
return "border-[#b8d8bd] bg-[#f0faf2] text-[#17602a]";
|
||||
}
|
||||
if (mode === "cloud_fallback" || mode === "unavailable") {
|
||||
return "border-[#e1aaa2] bg-[#fff3f1] text-[#9f2f25]";
|
||||
}
|
||||
return "border-[#d9b36f] bg-[#fff7e8] text-[#6d4707]";
|
||||
}
|
||||
|
||||
function aiRouteStandbySummary(status: AiRouteStatusResponse) {
|
||||
const standby = status.policy_order
|
||||
.filter((item) => item.provider_name !== status.selected_provider)
|
||||
.map((item) => item.provider_name)
|
||||
.slice(0, 3)
|
||||
.join(" -> ");
|
||||
const skipped = (status.skipped_lanes ?? [])
|
||||
.map((lane) => lane.provider_name)
|
||||
.filter(Boolean)
|
||||
.join(" -> ");
|
||||
return { standby: standby || "--", skipped: skipped || "--" };
|
||||
}
|
||||
|
||||
const AI_ROUTE_REPAIR_BLOCKER_KEYS = new Set([
|
||||
"gcloud_compute_instances_get_missing",
|
||||
"gcloud_compute_instances_list_missing",
|
||||
@@ -3499,6 +3522,7 @@ function AiRouteStatusPanel({
|
||||
const repairBlockers = repairEvidence?.access_blockers?.filter(Boolean).slice(0, 4) ?? [];
|
||||
const repairProbes = Object.entries(repairEvidence?.live_probe ?? {}).slice(0, 6);
|
||||
const skippedLanes = status?.skipped_lanes ?? [];
|
||||
const routeSummary = status ? aiRouteStandbySummary(status) : { standby: "--", skipped: "--" };
|
||||
const skippedProviderSet = new Set(
|
||||
skippedLanes
|
||||
.map((lane) => lane.provider_name)
|
||||
@@ -3551,6 +3575,27 @@ function AiRouteStatusPanel({
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={cn(
|
||||
"border-b px-4 py-3 text-sm",
|
||||
aiRouteSummaryTone(laneMode)
|
||||
)}>
|
||||
<p className="font-semibold text-[#141413]">
|
||||
{laneMode === "primary"
|
||||
? t("summary.primaryTitle", { provider: selectedProvider ?? "--" })
|
||||
: t("summary.fallbackTitle", { provider: selectedProvider ?? "--" })}
|
||||
</p>
|
||||
<p className="mt-1 leading-5">
|
||||
{laneMode === "primary"
|
||||
? t("summary.primaryDetail", {
|
||||
standby: routeSummary.standby,
|
||||
})
|
||||
: t("summary.fallbackDetail", {
|
||||
skipped: routeSummary.skipped,
|
||||
action: t(operatorActionKey as never),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{laneMode && laneMode !== "primary" && (
|
||||
<div className="flex items-start gap-3 border-b border-[#e0ddd4] bg-[#fff7e8] px-4 py-3 text-sm text-[#6d4707]">
|
||||
<TriangleAlert className="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
@@ -3852,6 +3897,27 @@ export default function RunsPage() {
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const fetchAiRouteStatus = useCallback(async () => {
|
||||
try {
|
||||
const routeStatusRes = await fetch(
|
||||
`${API_BASE}/api/v1/platform/ai-route-status?workload_type=deep_rca`
|
||||
);
|
||||
if (routeStatusRes.ok) {
|
||||
const routeStatusData: AiRouteStatusResponse = await routeStatusRes.json();
|
||||
setAiRouteStatus(routeStatusData);
|
||||
setAiRouteStatusError(null);
|
||||
} else {
|
||||
setAiRouteStatus(null);
|
||||
setAiRouteStatusError(`HTTP ${routeStatusRes.status}`);
|
||||
}
|
||||
} catch (routeStatusError) {
|
||||
setAiRouteStatus(null);
|
||||
setAiRouteStatusError(
|
||||
routeStatusError instanceof Error ? routeStatusError.message : "route status failed"
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchRuns = useCallback(async (options?: { refresh?: boolean }) => {
|
||||
try {
|
||||
setError(null);
|
||||
@@ -3989,25 +4055,6 @@ export default function RunsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const routeStatusRes = await fetch(
|
||||
`${API_BASE}/api/v1/platform/ai-route-status?workload_type=deep_rca`
|
||||
);
|
||||
if (routeStatusRes.ok) {
|
||||
const routeStatusData: AiRouteStatusResponse = await routeStatusRes.json();
|
||||
setAiRouteStatus(routeStatusData);
|
||||
setAiRouteStatusError(null);
|
||||
} else {
|
||||
setAiRouteStatus(null);
|
||||
setAiRouteStatusError(`HTTP ${routeStatusRes.status}`);
|
||||
}
|
||||
} catch (routeStatusError) {
|
||||
setAiRouteStatus(null);
|
||||
setAiRouteStatusError(
|
||||
routeStatusError instanceof Error ? routeStatusError.message : "route status failed"
|
||||
);
|
||||
}
|
||||
|
||||
setLastRefresh(new Date());
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "載入失敗");
|
||||
@@ -4020,18 +4067,20 @@ export default function RunsPage() {
|
||||
// 初次載入
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetchAiRouteStatus();
|
||||
fetchRuns();
|
||||
}, [fetchRuns]);
|
||||
}, [fetchAiRouteStatus, fetchRuns]);
|
||||
|
||||
// 30 秒自動刷新
|
||||
useEffect(() => {
|
||||
intervalRef.current = setInterval(() => {
|
||||
fetchAiRouteStatus();
|
||||
fetchRuns();
|
||||
}, AUTO_REFRESH_INTERVAL);
|
||||
return () => {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||
};
|
||||
}, [fetchRuns]);
|
||||
}, [fetchAiRouteStatus, fetchRuns]);
|
||||
|
||||
const totalPages = Math.ceil(total / PER_PAGE);
|
||||
const laneSummary = useMemo(() => {
|
||||
@@ -4106,7 +4155,11 @@ export default function RunsPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">每 30 秒自動刷新</span>
|
||||
<button
|
||||
onClick={() => { setLoading(true); fetchRuns({ refresh: true }); }}
|
||||
onClick={() => {
|
||||
setLoading(true);
|
||||
fetchAiRouteStatus();
|
||||
fetchRuns({ refresh: true });
|
||||
}}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg transition disabled:opacity-50"
|
||||
aria-label="立即重新整理"
|
||||
|
||||
@@ -2060,6 +2060,21 @@ function callbackTraceRecoveryAction(
|
||||
return { actionKey: "observeRecovery", humanRequired: false };
|
||||
}
|
||||
|
||||
function aiRouteOperatorActionText(
|
||||
action: string | null | undefined,
|
||||
t: ReturnType<typeof useTranslations>
|
||||
) {
|
||||
if (
|
||||
action === "monitor" ||
|
||||
action === "repair_skipped_primary_lane" ||
|
||||
action === "restore_ollama_lanes" ||
|
||||
action === "inspect_ai_router"
|
||||
) {
|
||||
return t(`evidence.aiRouteActions.${action}` as never);
|
||||
}
|
||||
return t("evidence.aiRouteActions.unknown");
|
||||
}
|
||||
|
||||
function buildWorkItems(
|
||||
telemetry: Telemetry,
|
||||
t: ReturnType<typeof useTranslations>
|
||||
@@ -2105,6 +2120,14 @@ function buildWorkItems(
|
||||
?.map((lane) => lane.provider_name)
|
||||
.filter(Boolean)
|
||||
.join(" -> ");
|
||||
const aiRouteActionText = aiRouteOperatorActionText(
|
||||
aiRoute?.operator_action?.action,
|
||||
t
|
||||
);
|
||||
const aiRouteHumanRequired = aiRoute?.operator_action?.human_required === true;
|
||||
const aiRouteHumanRequiredText = aiRouteHumanRequired
|
||||
? t("evidence.humanRequired.yes")
|
||||
: t("evidence.humanRequired.no");
|
||||
const remediationQueue = telemetry.slo?.adr100?.verification_coverage?.remediation_queue;
|
||||
const remediationTotal = remediationQueue?.total ?? 0;
|
||||
const remediationReadyForAi = remediationQueue?.ready_for_ai ?? 0;
|
||||
@@ -2233,6 +2256,11 @@ function buildWorkItems(
|
||||
}),
|
||||
evidenceDetails: aiRouteRepairEvidence
|
||||
? [
|
||||
t("evidence.aiRouteRepairSummary", {
|
||||
selected: aiRoute?.selected_provider ?? "--",
|
||||
action: aiRouteActionText,
|
||||
human: aiRouteHumanRequiredText,
|
||||
}),
|
||||
t("evidence.aiRouteRepairWorkItemId", {
|
||||
id: aiRouteWorkItem?.work_item_id ?? "--",
|
||||
}),
|
||||
@@ -2251,7 +2279,14 @@ function buildWorkItems(
|
||||
safe: String(aiRouteOwnerAction?.safe_to_auto_repair ?? false),
|
||||
}),
|
||||
]
|
||||
: [t("evidence.aiRouteRepairUnavailable")],
|
||||
: [
|
||||
t("evidence.aiRouteRepairSummary", {
|
||||
selected: aiRoute?.selected_provider ?? "--",
|
||||
action: aiRouteActionText,
|
||||
human: aiRouteHumanRequiredText,
|
||||
}),
|
||||
t("evidence.aiRouteRepairUnavailable"),
|
||||
],
|
||||
href: aiRouteWorkItem?.target_href ?? "/awooop/runs",
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user