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>
452 lines
22 KiB
TypeScript
452 lines
22 KiB
TypeScript
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>
|
||
)
|
||
}
|