Files
OpenFin/frontend/src/pages/SuperContexte.tsx
OpenSquared d256b65d30 Initial commit — GeoOptions Intelligence Cockpit v2.0
Stack: FastAPI + React/TypeScript + SQLite + GPT-4o
Features: Radar géopolitique, Marchés, Régime Macro, Journal de Bord MTM,
Rapport IA, Super Contexte (base de raisonnement évolutive), Boucle feedback IA.
Deploy: Docker + docker-compose + nginx pour openfin.open-squared.tech

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:29:59 +02:00

452 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import {
Brain, RefreshCw, Clock, ChevronDown, ChevronUp,
AlertTriangle, Target, TrendingUp, TrendingDown,
Shield, Zap, BookOpen, CheckCircle, XCircle, HelpCircle,
BarChart2, Activity,
} from 'lucide-react'
import {
useKnowledgeState, useKnowledgeHistory, useKnowledgeStateVersion,
useKnowledgeEntries, useSynthesizeKnowledge, usePatchKbEntryStatus,
} from '../hooks/useApi'
// ─── Status badge ─────────────────────────────────────────────────────────────
function StatusBadge({ status }: { status: string }) {
const map: Record<string, { icon: React.ReactNode; color: string; label: string }> = {
active: { icon: <CheckCircle className="w-3 h-3" />, color: 'text-emerald-400 bg-emerald-400/10 border-emerald-400/20', label: 'Validé' },
tentative: { icon: <HelpCircle className="w-3 h-3" />, color: 'text-amber-400 bg-amber-400/10 border-amber-400/20', label: 'Tentative' },
invalidated: { icon: <XCircle className="w-3 h-3" />, color: 'text-rose-400 bg-rose-400/10 border-rose-400/20', label: 'Invalidé' },
}
const s = map[status] || map['tentative']
return (
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full border text-[10px] font-medium ${s.color}`}>
{s.icon}{s.label}
</span>
)
}
// ─── Confidence bar ───────────────────────────────────────────────────────────
function ConfidenceBar({ value }: { value: number }) {
const color = value >= 70 ? 'bg-emerald-500' : value >= 40 ? 'bg-amber-500' : 'bg-rose-500'
return (
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 rounded-full bg-slate-700">
<div className={`h-full rounded-full ${color}`} style={{ width: `${value}%` }} />
</div>
<span className="text-[10px] text-slate-400 w-7 text-right">{value}%</span>
</div>
)
}
// ─── KB Entry card ────────────────────────────────────────────────────────────
function KbEntry({ entry, onStatusChange }: { entry: any; onStatusChange: (id: number, s: string) => void }) {
const [open, setOpen] = useState(false)
return (
<div className="bg-slate-800/60 border border-slate-700/50 rounded-lg p-3 space-y-2">
<div className="flex items-start gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs font-medium text-slate-200">{entry.title}</span>
<StatusBadge status={entry.status} />
</div>
<ConfidenceBar value={entry.confidence} />
</div>
<button onClick={() => setOpen(o => !o)} className="text-slate-500 hover:text-slate-300 mt-0.5">
{open ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
</div>
{open && (
<div className="space-y-2 pt-1">
<p className="text-xs text-slate-300 leading-relaxed">{entry.content}</p>
{entry.tags && (
<div className="flex gap-1 flex-wrap">
{entry.tags.split(',').filter(Boolean).map((t: string) => (
<span key={t} className="px-1.5 py-0.5 rounded bg-slate-700 text-[10px] text-slate-400">{t.trim()}</span>
))}
</div>
)}
<div className="flex gap-2 pt-1">
{['active', 'tentative', 'invalidated'].map(s => (
<button
key={s}
onClick={() => onStatusChange(entry.id, s)}
className={`text-[10px] px-2 py-1 rounded border transition-colors ${
entry.status === s
? 'bg-slate-600 border-slate-500 text-slate-200'
: 'border-slate-700 text-slate-500 hover:border-slate-500 hover:text-slate-300'
}`}
>
{s === 'active' ? 'Valider' : s === 'tentative' ? 'Tentative' : 'Invalider'}
</button>
))}
</div>
<p className="text-[10px] text-slate-600">
Vu le {entry.first_seen_at?.slice(0, 10)} · Confirmé {entry.confirmation_count}×
</p>
</div>
)}
</div>
)
}
// ─── Category section ─────────────────────────────────────────────────────────
const CAT_META: Record<string, { icon: React.ReactNode; color: string; label: string }> = {
régimes: { icon: <Activity className="w-4 h-4" />, color: 'text-blue-400', label: 'Régimes macro' },
patterns: { icon: <BarChart2 className="w-4 h-4" />, color: 'text-purple-400', label: 'Patterns' },
erreurs: { icon: <AlertTriangle className="w-4 h-4" />, color: 'text-rose-400', label: 'Erreurs récurrentes' },
corrélations: { icon: <TrendingUp className="w-4 h-4" />, color: 'text-emerald-400', label: 'Corrélations' },
général: { icon: <BookOpen className="w-4 h-4" />, color: 'text-slate-400', label: 'Général' },
}
function CategorySection({
category, entries, onStatusChange,
}: { category: string; entries: any[]; onStatusChange: (id: number, s: string) => void }) {
const [open, setOpen] = useState(true)
const meta = CAT_META[category] || CAT_META['général']
const active = entries.filter(e => e.status !== 'invalidated').length
return (
<div className="border border-slate-700/50 rounded-xl overflow-hidden">
<button
onClick={() => setOpen(o => !o)}
className="w-full flex items-center justify-between px-4 py-3 bg-slate-800/80 hover:bg-slate-800 transition-colors"
>
<div className="flex items-center gap-2">
<span className={meta.color}>{meta.icon}</span>
<span className="text-sm font-medium text-slate-200">{meta.label}</span>
<span className="text-xs text-slate-500">{active}/{entries.length} actifs</span>
</div>
{open ? <ChevronUp className="w-4 h-4 text-slate-500" /> : <ChevronDown className="w-4 h-4 text-slate-500" />}
</button>
{open && (
<div className="p-3 grid gap-2 bg-slate-900/40">
{entries.map(e => (
<KbEntry key={e.id} entry={e} onStatusChange={onStatusChange} />
))}
</div>
)}
</div>
)
}
// ─── Synthesis insight list ───────────────────────────────────────────────────
function InsightList({ title, icon, items, color }: {
title: string; icon: React.ReactNode; items: string[]; color: string
}) {
if (!items?.length) return null
return (
<div className="space-y-2">
<div className={`flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide ${color}`}>
{icon}{title}
</div>
<ul className="space-y-1">
{items.map((item, i) => (
<li key={i} className="flex items-start gap-2 text-xs text-slate-300">
<span className={`mt-0.5 w-1.5 h-1.5 rounded-full flex-shrink-0 ${color.replace('text-', 'bg-')}`} />
{typeof item === 'string' ? item : (item as any).mistake || JSON.stringify(item)}
</li>
))}
</ul>
</div>
)
}
// ─── Macro regime card ────────────────────────────────────────────────────────
function RegimeCard({ r }: { r: any }) {
const conf = r.confidence ?? 50
const color = conf >= 70 ? 'border-emerald-500/30 bg-emerald-500/5' : conf >= 40 ? 'border-amber-500/30 bg-amber-500/5' : 'border-slate-700/50 bg-slate-800/30'
return (
<div className={`border rounded-lg p-3 space-y-1 ${color}`}>
<div className="flex items-center justify-between">
<span className="text-xs font-semibold text-slate-200">{r.regime}</span>
<span className="text-[10px] text-slate-400">{conf}% conf · {r.trade_count ?? '?'} trades</span>
</div>
<p className="text-xs text-slate-400">{r.observation}</p>
</div>
)
}
// ─── Main page ────────────────────────────────────────────────────────────────
export default function SuperContexte() {
const [selectedHistoryId, setSelectedHistoryId] = useState<number | null>(null)
const [showHistory, setShowHistory] = useState(false)
const { data: stateData, isLoading: stateLoading } = useKnowledgeState()
const { data: histData } = useKnowledgeHistory()
const { data: entriesData } = useKnowledgeEntries()
const { data: versionData } = useKnowledgeStateVersion(selectedHistoryId)
const synthesize = useSynthesizeKnowledge()
const patchStatus = usePatchKbEntryStatus()
const currentState = selectedHistoryId
? versionData?.state
: stateData?.state
const synthesis = currentState?.synthesis || {}
const narrative = currentState?.narrative || ''
const history = histData?.history || []
const byCategory = entriesData?.by_category || {}
const totalEntries = entriesData?.total || 0
const handleSynthesize = () => {
synthesize.mutate(undefined, {
onSuccess: () => setSelectedHistoryId(null),
})
}
const handleStatusChange = (id: number, status: string) => {
patchStatus.mutate({ id, status })
}
return (
<div className="min-h-screen bg-slate-950 text-white">
{/* Header */}
<div className="border-b border-slate-800 bg-slate-900/60 backdrop-blur px-6 py-4">
<div className="max-w-7xl mx-auto flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<Brain className="w-6 h-6 text-violet-400" />
<div>
<h1 className="text-xl font-bold text-slate-100">Super Contexte</h1>
<p className="text-xs text-slate-500">Base de raisonnement évolutive cerveau du système</p>
</div>
</div>
<div className="flex items-center gap-3">
{currentState && (
<div className="text-right text-xs text-slate-500">
<div>v{currentState.version} · {currentState.created_at?.slice(0, 16)}</div>
<div>{currentState.reports_used} rapports · {currentState.trades_analyzed} trades analysés</div>
</div>
)}
<button
onClick={() => setShowHistory(h => !h)}
className={`p-2 rounded-lg border transition-colors ${showHistory ? 'border-violet-500/50 text-violet-400 bg-violet-500/10' : 'border-slate-700 text-slate-400 hover:border-slate-500'}`}
>
<Clock className="w-4 h-4" />
</button>
<button
onClick={handleSynthesize}
disabled={synthesize.isPending}
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-violet-600 hover:bg-violet-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm font-medium"
>
{synthesize.isPending ? (
<RefreshCw className="w-4 h-4 animate-spin" />
) : (
<Brain className="w-4 h-4" />
)}
{synthesize.isPending ? 'Synthèse en cours…' : 'Lancer synthèse GPT-4o'}
</button>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-6 py-6 flex gap-6">
{/* History sidebar */}
{showHistory && (
<div className="w-56 flex-shrink-0 space-y-2">
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Versions</p>
<button
onClick={() => setSelectedHistoryId(null)}
className={`w-full text-left px-3 py-2 rounded-lg text-xs transition-colors ${!selectedHistoryId ? 'bg-violet-600/20 border border-violet-500/30 text-violet-300' : 'border border-slate-700/50 text-slate-400 hover:border-slate-600'}`}
>
Dernière version
</button>
{history.map((h: any) => (
<button
key={h.id}
onClick={() => setSelectedHistoryId(h.id)}
className={`w-full text-left px-3 py-2 rounded-lg text-xs transition-colors ${selectedHistoryId === h.id ? 'bg-violet-600/20 border border-violet-500/30 text-violet-300' : 'border border-slate-700/50 text-slate-400 hover:border-slate-600'}`}
>
<div className="font-medium">v{h.version}</div>
<div className="text-slate-500">{h.created_at?.slice(0, 16)}</div>
<div className="text-slate-600">{h.reports_used} rapports · {h.trades_analyzed} trades</div>
</button>
))}
</div>
)}
{/* Main content */}
<div className="flex-1 min-w-0 space-y-6">
{stateLoading && (
<div className="flex items-center justify-center h-40 text-slate-500">
<RefreshCw className="w-5 h-5 animate-spin mr-2" /> Chargement
</div>
)}
{!currentState && !stateLoading && (
<div className="border border-violet-500/20 rounded-xl p-8 text-center bg-violet-500/5">
<Brain className="w-12 h-12 text-violet-400/50 mx-auto mb-3" />
<p className="text-slate-300 font-medium mb-1">Aucune synthèse disponible</p>
<p className="text-slate-500 text-sm mb-4">
Lance la première synthèse GPT-4o pour initialiser la base de raisonnement.
Le système analysera tous les rapports et trades disponibles.
</p>
<button
onClick={handleSynthesize}
disabled={synthesize.isPending}
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-violet-600 hover:bg-violet-500 disabled:opacity-50 text-sm font-medium transition-colors"
>
{synthesize.isPending ? <RefreshCw className="w-4 h-4 animate-spin" /> : <Brain className="w-4 h-4" />}
{synthesize.isPending ? 'Synthèse en cours…' : 'Initialiser le Super Contexte'}
</button>
</div>
)}
{synthesize.isSuccess && (
<div className="border border-emerald-500/30 bg-emerald-500/5 rounded-xl p-4 flex items-center gap-3">
<CheckCircle className="w-5 h-5 text-emerald-400 flex-shrink-0" />
<div className="text-sm">
<span className="text-emerald-300 font-medium">Synthèse complète. </span>
<span className="text-slate-400">
{(synthesize.data as any)?.kb_entries_added ?? 0} entrées KB ajoutées ·{' '}
{(synthesize.data as any)?.sources?.reports ?? 0} rapports ·{' '}
{(synthesize.data as any)?.sources?.trades ?? 0} trades analysés.
</span>
</div>
</div>
)}
{currentState && (
<>
{/* Narrative */}
<div className="border border-violet-500/20 bg-violet-500/5 rounded-xl p-5 space-y-3">
<div className="flex items-center gap-2 text-violet-300">
<Brain className="w-5 h-5" />
<span className="text-sm font-semibold">Narrative de raisonnement</span>
{selectedHistoryId && (
<span className="text-xs text-slate-500 ml-auto">v{currentState.version} (archivé)</span>
)}
</div>
<p className="text-sm text-slate-300 leading-relaxed whitespace-pre-wrap">{narrative}</p>
</div>
{/* Synthesis grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Régimes */}
{synthesis.regime_insights?.length > 0 && (
<div className="border border-slate-700/50 rounded-xl p-4 space-y-3">
<div className="flex items-center gap-2 text-blue-400 text-xs font-semibold uppercase tracking-wide">
<Activity className="w-4 h-4" />Régimes macro identifiés
</div>
<div className="space-y-2">
{synthesis.regime_insights.map((r: any, i: number) => (
<RegimeCard key={i} r={r} />
))}
</div>
</div>
)}
{/* Patterns */}
{synthesis.pattern_insights?.length > 0 && (
<div className="border border-slate-700/50 rounded-xl p-4 space-y-3">
<div className="flex items-center gap-2 text-purple-400 text-xs font-semibold uppercase tracking-wide">
<BarChart2 className="w-4 h-4" />Patterns documentés
</div>
<div className="space-y-2">
{synthesis.pattern_insights.map((p: any, i: number) => (
<div key={i} className="border border-slate-700/40 rounded-lg p-2.5 space-y-1">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-slate-200">{p.pattern}</span>
<span className="text-[10px] text-slate-500">conf {p.confidence}% · win {p.win_rate_pct}%</span>
</div>
<p className="text-xs text-slate-400">{p.observation}</p>
</div>
))}
</div>
</div>
)}
{/* Macro correlations */}
{synthesis.macro_correlations?.length > 0 && (
<div className="border border-slate-700/50 rounded-xl p-4 space-y-3">
<div className="flex items-center gap-2 text-emerald-400 text-xs font-semibold uppercase tracking-wide">
<TrendingUp className="w-4 h-4" />Corrélations macro/géo
</div>
<div className="space-y-2">
{synthesis.macro_correlations.map((c: any, i: number) => (
<div key={i} className="border border-slate-700/40 rounded-lg p-2.5 space-y-1">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-slate-200">{c.trigger}</span>
<span className={`text-[10px] ${c.reliability === 'haute' ? 'text-emerald-400' : c.reliability === 'faible' ? 'text-rose-400' : 'text-amber-400'}`}>
{c.reliability}
</span>
</div>
<p className="text-xs text-slate-400">{c.market_reaction}</p>
</div>
))}
</div>
</div>
)}
{/* Risk params + insights */}
<div className="border border-slate-700/50 rounded-xl p-4 space-y-4">
<InsightList
title="Priorités stratégiques"
icon={<Target className="w-4 h-4" />}
items={synthesis.strategic_priorities || []}
color="text-violet-400"
/>
<InsightList
title="Forces identifiées"
icon={<Zap className="w-4 h-4" />}
items={synthesis.strengths || []}
color="text-emerald-400"
/>
<InsightList
title="Angles morts"
icon={<AlertTriangle className="w-4 h-4" />}
items={synthesis.blind_spots || []}
color="text-amber-400"
/>
<InsightList
title="Erreurs récurrentes"
icon={<TrendingDown className="w-4 h-4" />}
items={(synthesis.recurring_mistakes || []).map((m: any) =>
typeof m === 'string' ? m : `${m.mistake}${m.mitigation}`
)}
color="text-rose-400"
/>
{synthesis.risk_parameters && (
<>
<InsightList
title="Préférer quand"
icon={<Shield className="w-4 h-4" />}
items={synthesis.risk_parameters.prefer_when || []}
color="text-blue-400"
/>
<InsightList
title="Éviter quand"
icon={<XCircle className="w-4 h-4" />}
items={synthesis.risk_parameters.avoid_when || []}
color="text-rose-400"
/>
</>
)}
</div>
</div>
</>
)}
{/* Knowledge Base entries */}
{totalEntries > 0 && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<BookOpen className="w-4 h-4 text-slate-400" />
<h2 className="text-sm font-semibold text-slate-300">Base de connaissances ({totalEntries} entrées)</h2>
</div>
<div className="space-y-3">
{Object.entries(byCategory).map(([cat, entries]: [string, any]) => (
<CategorySection
key={cat}
category={cat}
entries={entries}
onStatusChange={handleStatusChange}
/>
))}
</div>
</div>
)}
</div>
</div>
</div>
)
}