diff --git a/scripts/seed-forecasts.mjs b/scripts/seed-forecasts.mjs index 355ee65404..9f15d3a292 100644 --- a/scripts/seed-forecasts.mjs +++ b/scripts/seed-forecasts.mjs @@ -15990,11 +15990,11 @@ Return ONLY a JSON object with no markdown fences: { "round": 1, "summary": "<≤160 char>" }, { "round": 2, "summary": "<≤160 char>" } ], - "confidence": 0.0, + "confidence": 0.35, "timingMarkers": [{ "event": "<≤80 char>", "timing": "T+Nh" }] }, - { "pathId": "containment", "label": "...", "summary": "...", "keyActors": [], "roundByRoundEvolution": [], "confidence": 0.0, "timingMarkers": [] }, - { "pathId": "market_cascade", "label": "...", "summary": "...", "keyActors": [], "roundByRoundEvolution": [], "confidence": 0.0, "timingMarkers": [] } + { "pathId": "containment", "label": "...", "summary": "...", "keyActors": [], "roundByRoundEvolution": [], "confidence": 0.50, "timingMarkers": [] }, + { "pathId": "market_cascade", "label": "...", "summary": "...", "keyActors": [], "roundByRoundEvolution": [], "confidence": 0.15, "timingMarkers": [] } ], "stabilizers": ["<≤100 char>"], "invalidators": ["<≤100 char>"], @@ -16116,12 +16116,16 @@ async function writeSimulationOutcome(pkg, outcome, { storageConfig } = {}) { theaterId: tr.theaterId, theaterLabel: tr.theaterLabel || tr.theaterId, stateKind: tr.stateKind || '', - topPaths: (tr.topPaths || []).slice(0, 3).map((p) => ({ - label: p.label, - summary: p.summary, - confidence: p.confidence, - keyActors: (p.keyActors || []).slice(0, 4), - })), + topPaths: (tr.topPaths || []) + .sort((a, b) => (b.confidence || 0) - (a.confidence || 0)) + .slice(0, 3) + .map((p) => ({ + pathId: p.pathId || '', + label: p.label, + summary: p.summary, + confidence: p.confidence, + keyActors: (p.keyActors || []).slice(0, 4), + })), dominantReactions: (tr.dominantReactions || []).slice(0, 3), stabilizers: (tr.stabilizers || []).slice(0, 3), invalidators: (tr.invalidators || []).slice(0, 2), diff --git a/src/components/ForecastPanel.ts b/src/components/ForecastPanel.ts index 01abde0d68..bbfaea6815 100644 --- a/src/components/ForecastPanel.ts +++ b/src/components/ForecastPanel.ts @@ -38,7 +38,14 @@ const STATE_KIND_DOMAIN: Record = { }; // --- Types for simulation theater data ------------------------------------- +const PATH_ID_LABELS: Record = { + escalation: 'Escalation', + containment: 'Containment', + market_cascade: 'Market Cascade', +}; + interface SimulationPath { + pathId: string; label: string; summary: string; confidence: number; @@ -110,7 +117,11 @@ function injectStyles(): void { .fc-gauge-bg { fill: none; stroke: var(--border-color, #30363d); stroke-width: 4; } .fc-gauge-fill { fill: none; stroke-width: 4; stroke-linecap: round; } .fc-gauge-label { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); font-size: 9px; font-weight: 700; } - .fc-theater-path { font-size: 9px; color: var(--text-secondary, #7d8590); line-height: 1.4; margin-top: 4px; } + .fc-theater-path { font-size: 9px; color: var(--text-secondary, #7d8590); line-height: 1.4; margin-top: 4px; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; } + .fc-path-type { font-size: 8px; padding: 1px 4px; border-radius: 2px; font-weight: 600; letter-spacing: 0.03em; opacity: 0.75; white-space: nowrap; } + .fc-path-type-escalation { background: rgba(224,82,82,0.2); color: #e05252; border: 1px solid rgba(224,82,82,0.3); } + .fc-path-type-containment { background: rgba(63,185,80,0.15); color: #3fb950; border: 1px solid rgba(63,185,80,0.25); } + .fc-path-type-market_cascade { background: rgba(210,153,34,0.15); color: #d29922; border: 1px solid rgba(210,153,34,0.25); } .fc-cat-tag { font-size: 9px; padding: 1px 5px; border-radius: 3px; white-space: nowrap; flex-shrink: 0; font-weight: 500; display: inline-block; @@ -330,11 +341,11 @@ export class ForecastPanel extends Panel { stroke-dasharray="${circ.toFixed(1)}" stroke-dashoffset="${offset.toFixed(1)}"/> - ${confPct}% + ${conf > 0 ? confPct + '%' : '—'} ${escapeHtml(catLabel)} - ${dominantPath ? `
${escapeHtml(dominantPath.label)}
` : ''} + ${dominantPath ? `
${dominantPath.pathId ? `${escapeHtml(PATH_ID_LABELS[dominantPath.pathId] ?? dominantPath.pathId)}` : ''}${escapeHtml(dominantPath.label)}
` : ''} `; } @@ -348,10 +359,12 @@ export class ForecastPanel extends Panel { const pathsHtml = t.topPaths.map(p => { const pctColor = p.confidence >= 0.65 ? '#3fb950' : p.confidence >= 0.45 ? '#d29922' : '#e05252'; const actors = p.keyActors.map(a => `${escapeHtml(a)}`).join(''); + const typeTag = p.pathId ? `${escapeHtml(PATH_ID_LABELS[p.pathId] ?? p.pathId)}` : ''; + const confText = p.confidence > 0 ? `${Math.round(p.confidence * 100)}% probability` : '—'; return `
-
${escapeHtml(p.label)}
-
${Math.round(p.confidence * 100)}% probability
+
${typeTag}${escapeHtml(p.label)}
+
${confText}
${escapeHtml(p.summary)}
${actors ? `
${actors}
` : ''}