fix(web): render evidence card before quality summary
This commit is contained in:
@@ -237,6 +237,7 @@
|
||||
"title": "AI Automation Evidence",
|
||||
"claimReady": "Loop claim ready",
|
||||
"claimBlocked": "Gaps remain",
|
||||
"claimChecking": "Quality pending",
|
||||
"loading": "Loading AI automation evidence...",
|
||||
"empty": "No AI automation evidence is available yet.",
|
||||
"missingApiBase": "NEXT_PUBLIC_API_URL is not set",
|
||||
@@ -250,8 +251,10 @@
|
||||
"mcpDetail": "{success} success / {failed} failed, latest {server}",
|
||||
"autoRepair": "Auto repair",
|
||||
"qualityDetail": "Average {score}, red {red}",
|
||||
"qualityPending": "Quality summary is still calculating; other evidence is already shown",
|
||||
"humanGap": "Human gap",
|
||||
"humanGapDetail": "{gate} missing {count}",
|
||||
"humanGapClear": "Quality summary has no top gap",
|
||||
"modelRoute": "Model route",
|
||||
"routeDetail": "{model}; current {selected}; {primary}={primaryStatus}; fallback {fallback}",
|
||||
"routeReasonSeparator": "; ",
|
||||
|
||||
@@ -238,6 +238,7 @@
|
||||
"title": "AI 自動化證據鏈",
|
||||
"claimReady": "可宣稱閉環",
|
||||
"claimBlocked": "仍有缺口",
|
||||
"claimChecking": "品質計算中",
|
||||
"loading": "讀取 AI 自動化證據中...",
|
||||
"empty": "尚無可呈現的 AI 自動化證據。",
|
||||
"missingApiBase": "NEXT_PUBLIC_API_URL 未設定",
|
||||
@@ -251,8 +252,10 @@
|
||||
"mcpDetail": "成功 {success} / 失敗 {failed},最新 {server}",
|
||||
"autoRepair": "自動修復",
|
||||
"qualityDetail": "平均 {score},紅燈 {red}",
|
||||
"qualityPending": "品質摘要計算中,其他證據已先顯示",
|
||||
"humanGap": "人工缺口",
|
||||
"humanGapDetail": "{gate} 缺 {count} 筆",
|
||||
"humanGapClear": "品質摘要未列出主要缺口",
|
||||
"modelRoute": "模型路由",
|
||||
"routeDetail": "{model};目前 {selected};{primary}={primaryStatus};備援 {fallback}",
|
||||
"routeReasonSeparator": ";",
|
||||
|
||||
@@ -248,30 +248,39 @@ export function AutomationEvidenceCard() {
|
||||
async function load() {
|
||||
try {
|
||||
setError(null)
|
||||
const [
|
||||
quality,
|
||||
coverage,
|
||||
recurrence,
|
||||
runs,
|
||||
route,
|
||||
] = await Promise.all([
|
||||
fetchJson<AutomationQualitySummary>('/api/v1/platform/truth-chain/quality/summary?project_id=awoooi&hours=24&limit=200', controller.signal),
|
||||
const [coverage, recurrence, runs, route] = await Promise.all([
|
||||
fetchJson<DossierCoverageResponse>('/api/v1/platform/events/dossier/coverage?project_id=awoooi&limit=100', controller.signal),
|
||||
fetchJson<EventRecurrenceResponse>('/api/v1/platform/events/dossier/recurrence?project_id=awoooi&limit=100', controller.signal),
|
||||
fetchJson<RunsResponse>('/api/v1/platform/runs/list?project_id=awoooi&per_page=25', controller.signal),
|
||||
fetchJson<AiRouteStatusResponse>('/api/v1/platform/ai-route-status?workload_type=deep_rca', controller.signal),
|
||||
])
|
||||
|
||||
setSnapshot({
|
||||
quality,
|
||||
setSnapshot((current) => ({
|
||||
quality: current?.quality ?? null,
|
||||
coverage,
|
||||
recurrence,
|
||||
runs: Array.isArray(runs?.runs) ? runs.runs : Array.isArray(runs?.items) ? runs.items : [],
|
||||
route,
|
||||
})
|
||||
}))
|
||||
setLoading(false)
|
||||
|
||||
const quality = await fetchJson<AutomationQualitySummary>(
|
||||
'/api/v1/platform/truth-chain/quality/summary?project_id=awoooi&hours=24&limit=200',
|
||||
controller.signal
|
||||
)
|
||||
if (!controller.signal.aborted) {
|
||||
setSnapshot((current) => ({
|
||||
quality: quality ?? current?.quality ?? null,
|
||||
coverage: current?.coverage ?? coverage,
|
||||
recurrence: current?.recurrence ?? recurrence,
|
||||
runs: current?.runs ?? (Array.isArray(runs?.runs) ? runs.runs : Array.isArray(runs?.items) ? runs.items : []),
|
||||
route: current?.route ?? route,
|
||||
}))
|
||||
}
|
||||
} catch (err) {
|
||||
if (!controller.signal.aborted) {
|
||||
setError(err instanceof Error ? err.message : t('loadFailed'))
|
||||
setLoading(false)
|
||||
}
|
||||
} finally {
|
||||
if (!controller.signal.aborted) setLoading(false)
|
||||
@@ -304,6 +313,7 @@ export function AutomationEvidenceCard() {
|
||||
)
|
||||
|
||||
const topGate = quality?.gate_failures?.[0]
|
||||
const qualityLoaded = Boolean(quality)
|
||||
const claimReady = Boolean(quality?.production_claim?.can_claim_full_auto_repair)
|
||||
const route = snapshot?.route ?? null
|
||||
const primaryProvider = route?.policy_order?.[0]?.provider_name ?? null
|
||||
@@ -332,6 +342,7 @@ export function AutomationEvidenceCard() {
|
||||
recurrence,
|
||||
runEvidence,
|
||||
topGate,
|
||||
qualityLoaded,
|
||||
claimReady,
|
||||
selectedProvider,
|
||||
fallback,
|
||||
@@ -340,7 +351,13 @@ export function AutomationEvidenceCard() {
|
||||
}
|
||||
}, [snapshot, t])
|
||||
|
||||
const hasData = Boolean(snapshot?.quality || snapshot?.coverage || snapshot?.recurrence)
|
||||
const hasData = Boolean(
|
||||
snapshot?.quality ||
|
||||
snapshot?.coverage ||
|
||||
snapshot?.recurrence ||
|
||||
snapshot?.route ||
|
||||
(snapshot?.runs?.length ?? 0) > 0
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
@@ -364,10 +381,21 @@ export function AutomationEvidenceCard() {
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: derived.claimReady ? '#22C55E' : '#F59E0B', flexShrink: 0 }} />
|
||||
<div style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: '50%',
|
||||
background: !derived.qualityLoaded ? '#9CA3AF' : derived.claimReady ? '#22C55E' : '#F59E0B',
|
||||
flexShrink: 0,
|
||||
}} />
|
||||
{t('title')}
|
||||
<span style={{ marginLeft: 'auto', fontSize: 10, color: derived.claimReady ? '#17602a' : '#8a5a08', fontWeight: 700 }}>
|
||||
{derived.claimReady ? t('claimReady') : t('claimBlocked')}
|
||||
<span style={{
|
||||
marginLeft: 'auto',
|
||||
fontSize: 10,
|
||||
color: !derived.qualityLoaded ? '#77736a' : derived.claimReady ? '#17602a' : '#8a5a08',
|
||||
fontWeight: 700,
|
||||
}}>
|
||||
{!derived.qualityLoaded ? t('claimChecking') : derived.claimReady ? t('claimReady') : t('claimBlocked')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -413,21 +441,25 @@ export function AutomationEvidenceCard() {
|
||||
/>
|
||||
<EvidenceMetric
|
||||
label={t('autoRepair')}
|
||||
value={`${derived.quality?.verified_auto_repair_total ?? 0}/${derived.quality?.evaluated_total ?? 0}`}
|
||||
detail={t('qualityDetail', {
|
||||
score: (derived.quality?.average_score ?? 0).toFixed(1),
|
||||
red: derived.quality?.score_buckets?.red ?? 0,
|
||||
})}
|
||||
tone={derived.claimReady ? 'good' : 'warn'}
|
||||
value={derived.quality ? `${derived.quality.verified_auto_repair_total}/${derived.quality.evaluated_total}` : '--'}
|
||||
detail={derived.quality
|
||||
? t('qualityDetail', {
|
||||
score: derived.quality.average_score.toFixed(1),
|
||||
red: derived.quality.score_buckets?.red ?? 0,
|
||||
})
|
||||
: t('qualityPending')}
|
||||
tone={!derived.qualityLoaded ? 'neutral' : derived.claimReady ? 'good' : 'warn'}
|
||||
icon={Activity}
|
||||
/>
|
||||
<EvidenceMetric
|
||||
label={t('humanGap')}
|
||||
value={derived.recurrence?.automation_gap_group_total ?? 0}
|
||||
detail={t('humanGapDetail', {
|
||||
gate: tQuality(gateLabelKey(derived.topGate?.gate) as never),
|
||||
count: derived.topGate?.total ?? 0,
|
||||
})}
|
||||
detail={derived.topGate
|
||||
? t('humanGapDetail', {
|
||||
gate: tQuality(gateLabelKey(derived.topGate.gate) as never),
|
||||
count: derived.topGate.total,
|
||||
})
|
||||
: derived.qualityLoaded ? t('humanGapClear') : t('qualityPending')}
|
||||
tone={(derived.recurrence?.automation_gap_group_total ?? 0) > 0 ? 'warn' : 'good'}
|
||||
icon={ShieldCheck}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user