feat: instrument analysis
This commit is contained in:
@@ -1023,10 +1023,12 @@ function ExplanationScore({
|
||||
}) {
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [refreshDone, setRefreshDone] = useState(false)
|
||||
const [debugInfo, setDebugInfo] = useState<any>(null)
|
||||
|
||||
// For each linked event, compute the comprehension score and collect scored ones
|
||||
const scored: number[] = []
|
||||
const totalLinked: number[] = []
|
||||
const debugRows: {id?: number; name: string; pred: string; actual: string; score: number | null; reason: string}[] = []
|
||||
|
||||
for (const ev of events) {
|
||||
if (!ev.template_id) continue
|
||||
@@ -1035,7 +1037,35 @@ function ExplanationScore({
|
||||
const inst = causalInsts.find(ci => tmpl.instruments.includes(ci))
|
||||
if (!inst) continue
|
||||
totalLinked.push(ev.id ?? 0)
|
||||
|
||||
// Detailed debug per event
|
||||
const hasPred = !!(ev.prediction_json && ev.prediction_json !== '{}')
|
||||
const hasActual = !!(ev.actual_json && ev.actual_json !== '{}')
|
||||
let reason = ''
|
||||
if (!hasPred) reason = 'prediction_json vide'
|
||||
else if (!hasActual) reason = 'actual_json vide'
|
||||
else {
|
||||
try {
|
||||
const preds = JSON.parse(ev.prediction_json!)
|
||||
const actuals = JSON.parse(ev.actual_json!)
|
||||
const outputNode = tmpl.graph_json.nodes.find(n =>
|
||||
(n.type === 'market_asset' || n.type === 'output') && n.instrument?.toUpperCase() === inst.toUpperCase()
|
||||
)
|
||||
if (!outputNode) reason = `nœud output "${inst}" introuvable (types: ${[...new Set(tmpl.graph_json.nodes.map(n => n.type))].join(',')})`
|
||||
else if (preds[outputNode.id] == null) reason = `preds["${outputNode.id}"] absent`
|
||||
else if (actuals[inst] == null && actuals[inst.toUpperCase()] == null) reason = `actuals["${inst}"] absent — clés: ${Object.keys(actuals).join(',')}`
|
||||
else reason = 'ok'
|
||||
} catch (e) { reason = `parse error: ${e}` }
|
||||
}
|
||||
debugRows.push({
|
||||
id: ev.id, name: ev.title?.slice(0, 30) ?? '?',
|
||||
pred: hasPred ? '✓' : '✗',
|
||||
actual: hasActual ? '✓' : '✗',
|
||||
score: null, reason,
|
||||
})
|
||||
|
||||
const s = comprehensionScore(ev, tmpl, inst)
|
||||
if (debugRows.length > 0) debugRows[debugRows.length - 1].score = s
|
||||
if (s != null) scored.push(s)
|
||||
}
|
||||
|
||||
@@ -1056,12 +1086,17 @@ function ExplanationScore({
|
||||
: globalScore >= 70 ? 'bg-emerald-500' : globalScore >= 40 ? 'bg-amber-500' : 'bg-red-500'
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setDebugInfo(null)
|
||||
setRefreshing(true)
|
||||
try {
|
||||
await api.post('/causal-lab/auto-analyze/refresh')
|
||||
const res = await api.post('/causal-lab/auto-analyze/refresh')
|
||||
const data = res.data
|
||||
setDebugInfo(data)
|
||||
setRefreshDone(true)
|
||||
onRefreshDone?.()
|
||||
setTimeout(() => setRefreshDone(false), 3000)
|
||||
setTimeout(() => setRefreshDone(false), 8000)
|
||||
} catch (err: any) {
|
||||
setDebugInfo({ error: String(err) })
|
||||
} finally {
|
||||
setRefreshing(false)
|
||||
}
|
||||
@@ -1070,28 +1105,57 @@ function ExplanationScore({
|
||||
const needsRefresh = totalLinked.length > 0 && scored.length === 0 && !refreshDone
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-4 py-2.5 rounded-xl border border-slate-700/30 bg-dark-800/50">
|
||||
<span className="text-xs text-slate-500 whitespace-nowrap">Note globale</span>
|
||||
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div className={clsx('h-full rounded-full transition-all', barCls)} style={{ width: `${globalScore ?? 0}%` }} />
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3 px-4 py-2.5 rounded-xl border border-slate-700/30 bg-dark-800/50">
|
||||
<span className="text-xs text-slate-500 whitespace-nowrap">Note globale</span>
|
||||
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div className={clsx('h-full rounded-full transition-all', barCls)} style={{ width: `${globalScore ?? 0}%` }} />
|
||||
</div>
|
||||
<span className={clsx('text-xs font-semibold px-2 py-1 rounded-lg border whitespace-nowrap', badgeCls)}>
|
||||
{globalScore != null ? `${globalScore}% — ` : ''}{verdict}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-600 whitespace-nowrap">
|
||||
{scored.length}/{totalLinked.length} graphes
|
||||
</span>
|
||||
{(needsRefresh || scored.length === 0) && !refreshDone && totalLinked.length > 0 && (
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
className="text-[10px] text-violet-400 hover:text-violet-200 bg-violet-900/20 hover:bg-violet-900/40 border border-violet-800/40 rounded px-2 py-1 whitespace-nowrap disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{refreshing ? '…' : 'Recalculer'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<span className={clsx('text-xs font-semibold px-2 py-1 rounded-lg border whitespace-nowrap', badgeCls)}>
|
||||
{globalScore != null ? `${globalScore}% — ` : ''}{verdict}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-600 whitespace-nowrap">
|
||||
{scored.length}/{totalLinked.length} graphes
|
||||
</span>
|
||||
{needsRefresh && (
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
className="text-[10px] text-violet-400 hover:text-violet-200 bg-violet-900/20 hover:bg-violet-900/40 border border-violet-800/40 rounded px-2 py-1 whitespace-nowrap disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{refreshing ? '…' : 'Recalculer'}
|
||||
</button>
|
||||
)}
|
||||
{refreshDone && (
|
||||
<span className="text-[10px] text-emerald-500 whitespace-nowrap">✓ Rechargez la page</span>
|
||||
|
||||
{/* Debug panel — score diagnostics per event */}
|
||||
{debugRows.length > 0 && scored.length === 0 && (
|
||||
<div className="rounded-lg border border-slate-700/30 bg-dark-900/60 p-3 text-[10px] font-mono">
|
||||
<div className="text-slate-500 mb-1.5 uppercase tracking-wide">Diagnostic scores ({debugRows.length} graphes)</div>
|
||||
{debugRows.map((r, i) => (
|
||||
<div key={i} className="flex gap-2 py-0.5 border-b border-slate-800/40 last:border-0">
|
||||
<span className={clsx('shrink-0 w-4', r.pred === '✓' ? 'text-emerald-600' : 'text-red-600')}>{r.pred}</span>
|
||||
<span className={clsx('shrink-0 w-4', r.actual === '✓' ? 'text-emerald-600' : 'text-red-600')}>{r.actual}</span>
|
||||
<span className="text-slate-400 w-36 shrink-0 truncate" title={r.name}>{r.name}</span>
|
||||
<span className={clsx('flex-1', r.reason === 'ok' ? 'text-emerald-600' : 'text-amber-600')}>{r.reason}</span>
|
||||
{r.score != null && <span className="text-violet-400">{r.score}%</span>}
|
||||
</div>
|
||||
))}
|
||||
{debugInfo && (
|
||||
<div className="mt-2 pt-2 border-t border-slate-800/40 text-slate-600">
|
||||
{debugInfo.error
|
||||
? <span className="text-red-500">Erreur: {debugInfo.error}</span>
|
||||
: <span>Refresh: {debugInfo.refreshed} ok / {debugInfo.failed} err / {debugInfo.total} total</span>
|
||||
}
|
||||
{debugInfo.details?.map((d: any, i: number) => (
|
||||
<div key={i} className="mt-0.5">
|
||||
#{d.event_id} {(d.event_name ?? '').slice(0, 25)} → <span className={d.result === 'ok' ? 'text-emerald-600' : 'text-red-500'}>{d.result}</span>
|
||||
{d.surprise_pct != null && <span className="text-slate-500 ml-1">surp:{d.surprise_pct}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user