import { useState, useEffect } from 'react' import { FlaskConical, Library, Zap, BarChart3, RefreshCw, ChevronRight, Sliders, Brain, Search, Filter, CheckCircle, XCircle, Minus, AlertCircle, } from 'lucide-react' import clsx from 'clsx' // ── Types ───────────────────────────────────────────────────────────────────── interface CausalNode { id: string; label: string; type: 'input' | 'intermediate' | 'output' x: number; y: number; formula?: string; unit?: string; instrument?: string } interface CausalEdge { from: string; to: string; style: 'solid' | 'dashed'; type?: string; label?: string } interface Coefficient { value: number; calibrated: number | null; description: string } interface GraphJson { nodes: CausalNode[]; edges: CausalEdge[] coefficients: Record instruments: string[] input_mapping: Record } interface Template { id: number; name: string; category: string; sub_type: string instruments: string[]; description: string; graph_json: GraphJson ai_rationale: string; calibration_json: Record heuristic_ver: number } interface MarketEvent { id: number; name: string; category: string; sub_type: string start_date: string; level: string; impact_score: number market_impact: string; affected_assets: string actual_value: number | null; expected_value: number | null; surprise_pct: number | null analysis_id: number | null; template_id: number | null; activation_score: number | null } interface NodeResult { pred: number | null; act: number | null; status: string; label: string } interface AnalysisResult { event: MarketEvent; template_id: number; template_name: string; instrument: string inputs: Record; node_values: Record actual_moves: Record; yields: Record activation: { score: number | null; nodes: Record; correct: number; total: number } drift: { pre_pips: number | null; post_pips: number | null; drift_ratio: number | null; leak: string } prices_mode: string; analyzed_at: string } interface Recommendation { template_id: number | null; template_name: string; confidence: number rationale: string; coefficient_suggestions: Record instrument_focus: string[]; alternative_template_id: number | null; notes: string error?: string } interface CalibRow { id: number; name: string; category: string; sub_type: string n_analyses: number; avg_activation: number | null calibration: Record } // ── API ─────────────────────────────────────────────────────────────────────── const api = async (path: string, opts?: RequestInit) => { const r = await fetch(`http://localhost:8000${path}`, opts) if (!r.ok) { const t = await r.text(); throw new Error(t) } return r.json() } // ── Couleurs / labels ───────────────────────────────────────────────────────── const NODE_COLORS: Record = { input: '#3b82f6', intermediate: '#8b5cf6', output: '#10b981', } const STATUS_COLOR: Record = { correct: '#10b981', wrong: '#ef4444', neutral: '#64748b', unknown: '#64748b', } const CAT_LABELS: Record = { macro_us: 'Macro US', macro_eu: 'Macro EU', geopolitical: 'Géopolitique', commodity: 'Matières premières', report: 'Report', sentiment: 'Sentiment', } const fmtPips = (v: number | null | undefined) => v == null ? '—' : `${v > 0 ? '+' : ''}${v.toFixed(0)} pip` // ── SVG Graph renderer ──────────────────────────────────────────────────────── function GraphSVG({ graph, nodeValues, activationNodes, compact = false, }: { graph: GraphJson nodeValues?: Record activationNodes?: Record compact?: boolean }) { const nodes = graph.nodes || [] const edges = graph.edges || [] const xs = nodes.map(n => n.x) const ys = nodes.map(n => n.y) const minX = Math.min(...xs, 0) - 40 const minY = Math.min(...ys, 0) - 30 const maxX = Math.max(...xs, 400) + 120 const maxY = Math.max(...ys, 400) + 60 const W = maxX - minX; const H = maxY - minY const nodeMap = Object.fromEntries(nodes.map(n => [n.id, n])) const NW = compact ? 90 : 115; const NH = compact ? 28 : 38 function nodeColor(n: CausalNode) { if (activationNodes?.[n.id]) return STATUS_COLOR[activationNodes[n.id].status] ?? NODE_COLORS[n.type] return NODE_COLORS[n.type] ?? '#64748b' } function edgePath(e: CausalEdge) { const s = nodeMap[e.from]; const t = nodeMap[e.to] if (!s || !t) return '' const x1 = s.x; const y1 = s.y + NH / 2 const x2 = t.x; const y2 = t.y - NH / 2 const mx = (x1 + x2) / 2 return `M${x1},${y1} C${x1},${mx} ${x2},${mx} ${x2},${y2}` } return ( {edges.map((e, i) => { const s = nodeMap[e.from]; const t = nodeMap[e.to] if (!s || !t) return null return ( ) })} {nodes.map(n => { const col = nodeColor(n) const val = nodeValues?.[n.id] const unit = n.unit || '' return ( {n.label.length > 18 ? n.label.slice(0, 17) + '…' : n.label} {val != null && ( {val > 0 ? '+' : ''}{val.toFixed(1)}{unit ? ` ${unit}` : ''} )} ) })} ) } // ── Tab Bibliothèque ────────────────────────────────────────────────────────── function TabLibrary() { const [templates, setTemplates] = useState([]) const [selected, setSelected] = useState