import { useState, useEffect, useCallback, useRef } from 'react' import { Search, RefreshCw, Plus, Trash2, Edit3, Sparkles, ExternalLink, ChevronDown, ChevronUp, Loader2, CheckCircle, AlertCircle, X, SlidersHorizontal, BarChart2, Star, Users } from 'lucide-react' import clsx from 'clsx' // ── Types ───────────────────────────────────────────────────────────────────── interface SourceRef { title: string source: string url: string date: string original_score: number } interface InstrumentImpact { id: number instrument_id: string impact_score: number direction: 'bullish' | 'bearish' | 'neutral' | 'depends_on_outcome' rationale: string confidence: number ai_generated: number manually_adjusted: number adjusted_score: number | null adjusted_direction: string | null override_rationale: string } interface MarketEvent { id: number name: string start_date: string end_date: string | null level: 'long' | 'medium' | 'short' category: string sub_type: string description: string market_impact: string affected_assets: string[] impact_score: number absorption_pct: number | null source_refs: SourceRef[] origin?: string impacts?: InstrumentImpact[] evaluated?: boolean } // ── Constants ───────────────────────────────────────────────────────────────── const CAT_COLORS: Record = { event_calendar: 'bg-amber-500/20 text-amber-300 border-amber-500/30', geopolitical: 'bg-red-500/20 text-red-300 border-red-500/30', fundamental: 'bg-emerald-500/20 text-emerald-300 border-emerald-500/30', report: 'bg-blue-500/20 text-blue-300 border-blue-500/30', sentiment: 'bg-purple-500/20 text-purple-300 border-purple-500/30', technical: 'bg-cyan-500/20 text-cyan-300 border-cyan-500/30', } const CAT_DOT: Record = { event_calendar: 'bg-amber-400', geopolitical: 'bg-red-400', fundamental: 'bg-emerald-400', report: 'bg-blue-400', sentiment: 'bg-purple-400', technical: 'bg-cyan-400', } const LEVEL_COLORS: Record = { long: 'text-purple-400', medium: 'text-amber-400', short: 'text-emerald-400', } const ORIGIN_META: Record = { bootstrap_macro: { label: 'Bootstrap macro', icon: '📚', color: 'text-slate-400', description: 'Données historiques macro/géopolitiques chargées au démarrage' }, bootstrap_eco: { label: 'Bootstrap éco', icon: '📅', color: 'text-amber-400', description: 'Calendrier économique FRED historique (FOMC, CPI, NFP…)' }, bootstrap_ma: { label: 'Bootstrap MA', icon: '📈', color: 'text-cyan-400', description: 'Signal technique MA détecté sur données historiques (yfinance)' }, bootstrap_legacy: { label: 'Bootstrap legacy', icon: '🗄️', color: 'text-slate-500', description: 'Données initiales (bootstrap, origine exacte inconnue)' }, detector_news: { label: 'News RSS', icon: '📰', color: 'text-red-400', description: 'Généré depuis un flux RSS — validé par IA (GPT-4o-mini)' }, detector_eco: { label: 'Surprise FRED', icon: '📊', color: 'text-amber-400', description: 'Surprise économique FRED détectée par le cycle (z-score > seuil)' }, detector_technical: { label: 'Signal technique', icon: '📐', color: 'text-cyan-400', description: 'Croisement MA50/MA100/MA200 détecté automatiquement (yfinance)' }, detector_report: { label: 'Rapport instit.', icon: '📋', color: 'text-blue-400', description: 'Rapport institutionnel haute importance (COT, EIA, Fed…)' }, manual: { label: 'Manuel', icon: '✏️', color: 'text-emerald-400', description: 'Créé manuellement dans l\'interface' }, unknown: { label: 'Inconnu', icon: '❓', color: 'text-slate-600', description: 'Origine non tracée (données antérieures au versioning)' }, } const DIR_COLORS: Record = { bullish: 'text-emerald-400', bearish: 'text-red-400', neutral: 'text-slate-400', depends_on_outcome: 'text-amber-400', } const DIR_ICONS: Record = { bullish: '▲', bearish: '▼', neutral: '●', depends_on_outcome: '◆', } const CATEGORIES = ['event_calendar', 'geopolitical', 'fundamental', 'report', 'sentiment', 'technical'] const LEVELS = ['long', 'medium', 'short'] const ALL_INSTRUMENTS = [ 'SPY','QQQ','IWM','EEM','EFA','GLD','SLV','USO','UNG','TLT', 'HYG','EURUSD=X','USDJPY=X','GBPUSD=X','VXX','AAPL','NVDA','GS','XOM','BTC-USD', ] // ── Helpers ─────────────────────────────────────────────────────────────────── function ScoreBar({ score, max = 1 }: { score: number; max?: number }) { const pct = Math.round((score / max) * 100) const color = score >= 0.7 ? 'bg-red-500' : score >= 0.5 ? 'bg-amber-500' : 'bg-emerald-500' return (
{score.toFixed(2)}
) } function CatBadge({ cat }: { cat: string }) { return ( {cat.replace('_', ' ')} ) } // ── Instrument impact row (inline editable) ─────────────────────────────────── function ImpactRow({ imp, eventId, onUpdated, onDeleted, }: { imp: InstrumentImpact eventId: number onUpdated: () => void onDeleted: () => void }) { const [editing, setEditing] = useState(false) const [score, setScore] = useState(String(imp.adjusted_score ?? imp.impact_score)) const [dir, setDir] = useState(imp.adjusted_direction || imp.direction) const [note, setNote] = useState(imp.override_rationale || '') const [saving, setSaving] = useState(false) const effectiveScore = imp.adjusted_score ?? imp.impact_score const effectiveDir = imp.adjusted_direction || imp.direction const save = async () => { setSaving(true) await fetch(`/api/market-events/${eventId}/impacts/${imp.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ adjusted_score: +score, adjusted_direction: dir, override_rationale: note }), }) setSaving(false) setEditing(false) onUpdated() } const del = async () => { if (!confirm(`Supprimer l'impact sur ${imp.instrument_id} ?`)) return await fetch(`/api/market-events/${eventId}/impacts/${imp.id}`, { method: 'DELETE' }) onDeleted() } return (
{/* Ticker */} {imp.instrument_id} {/* Direction */} {DIR_ICONS[effectiveDir] ?? '●'} {/* Score bar */}
{/* Confidence */} {Math.round(imp.confidence * 100)}% {/* Rationale */} {imp.rationale} {/* AI badge */} {imp.ai_generated ? ( AI ) : ( manual )} {imp.manually_adjusted ? adj. : null} {/* Actions */}
{editing && (
)}
) } // ── Add instrument panel ────────────────────────────────────────────────────── function AddInstrumentPanel({ eventId, onAdded }: { eventId: number; onAdded: () => void }) { const [open, setOpen] = useState(false) const [ticker, setTicker] = useState('') const [score, setScore] = useState(0.5) const [dir, setDir] = useState('neutral') const [rationale, setRat] = useState('') const [saving, setSaving] = useState(false) const submit = async () => { if (!ticker) return setSaving(true) await fetch(`/api/market-events/${eventId}/impacts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ instrument_id: ticker, impact_score: score, direction: dir, rationale, confidence: 0.8 }), }) setSaving(false) setOpen(false) setTicker('') setRat('') onAdded() } if (!open) { return ( ) } return (
setScore(+e.target.value)} className="accent-emerald-400" /> {score.toFixed(2)}
setRat(e.target.value)} placeholder="Rationale..." className="bg-dark-900 border border-slate-700 rounded px-2 py-1 text-slate-200 text-sm" />
) } // ── Event detail panel (right side) ────────────────────────────────────────── function EventDetail({ event, onClose, onUpdated, onDeleted, }: { event: MarketEvent onClose: () => void onUpdated: (ev: MarketEvent) => void onDeleted: (id: number) => void }) { const [detail, setDetail] = useState(event) const [evaluating, setEval] = useState(false) const [evalMsg, setEvalMsg] = useState(null) const [editing, setEditing] = useState(false) const [editData, setEditData] = useState>({}) const [saving, setSaving] = useState(false) const [allCats, setAllCats] = useState([]) const [catSaving, setCatSaving] = useState(false) const loadDetail = useCallback(async () => { const r = await fetch(`/api/market-events/${event.id}`) if (r.ok) { const d = await r.json() setDetail(d) } }, [event.id]) useEffect(() => { loadDetail() }, [loadDetail]) useEffect(() => { fetch('/api/impact/categories') .then(r => r.json()) .then(d => setAllCats(Array.isArray(d) ? d : [])) .catch(() => {}) }, []) // Quick-save sub_type without entering full edit mode const assignCategory = async (catName: string) => { const cat = allCats.find(c => c.name === catName) setCatSaving(true) const payload = { ...detail, sub_type: catName, category: cat?.type ?? detail.category, source_refs: detail.source_refs ?? [], } const r = await fetch(`/api/market-events/${event.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) setCatSaving(false) if (r.ok) { setDetail(d => ({ ...d, sub_type: catName, category: cat?.type ?? d.category })) onUpdated({ ...detail, sub_type: catName, category: cat?.type ?? detail.category }) } } const evaluate = async (force = false) => { setEval(true) setEvalMsg(null) try { const r = await fetch(`/api/market-events/${event.id}/evaluate?force=${force}`, { method: 'POST' }) const d = await r.json() if (!r.ok) throw new Error(d.detail || r.statusText) const catInfo = d.matched_category ? ` · catégorie: ${d.matched_category}` : '' setEvalMsg(`✓ ${d.n_instruments} instruments évalués${catInfo}`) await loadDetail() } catch (e: any) { setEvalMsg(`✗ ${e.message}`) } finally { setEval(false) } } const saveEdit = async () => { setSaving(true) const payload = { ...detail, ...editData, source_refs: detail.source_refs ?? [] } const r = await fetch(`/api/market-events/${event.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) setSaving(false) if (r.ok) { setEditing(false) await loadDetail() onUpdated({ ...detail, ...editData } as MarketEvent) } } const del = async () => { if (!confirm(`Supprimer "${detail.name}" ?`)) return await fetch(`/api/market-events/${event.id}`, { method: 'DELETE' }) onDeleted(event.id) onClose() } const impacts = detail.impacts || [] const evalScore = detail.impact_score return (
{/* Header */}
{editing ? ( setEditData(d => ({ ...d, name: e.target.value }))} className="bg-dark-900 border border-amber-500/50 rounded px-3 py-1.5 text-slate-100 font-bold text-sm w-full" /> ) : (

{detail.name}

)}
{detail.level} {detail.start_date} {detail.end_date && → {detail.end_date}}
{/* Scrollable body */}
{/* Impact score + absorption */}
Score impact
{detail.absorption_pct != null && (
Absorption marché
{detail.absorption_pct}%
)}
{/* ── Catégorie IA ─────────────────────────────────────────────── */} {(() => { const currentSub = editing ? (editData.sub_type ?? detail.sub_type ?? '') : (detail.sub_type ?? '') const matchedCat = allCats.find(c => c.name === currentSub) const nDefaults = matchedCat?.default_impacts?.length ?? 0 return (
Catégorie IA {matchedCat && ( {nDefaults} impact{nDefaults !== 1 ? 's' : ''} par défaut )}
{editing ? ( ) : (
{catSaving && }
)} {matchedCat?.description && (
{matchedCat.description}
)} {matchedCat && nDefaults > 0 && (
{matchedCat.default_impacts.slice(0, 8).map((di: DefaultImpact) => ( {di.typical_direction === 'bullish' ? '↑' : di.typical_direction === 'bearish' ? '↓' : '–'} {di.instrument_id} {(di.sensitivity * 100).toFixed(0)}% ))}
)}
) })()} {/* Description */} {editing ? (