feat: causal lab

This commit is contained in:
OpenSquared
2026-06-28 18:08:09 +02:00
parent c286c7c000
commit 6ebbf4326e
5 changed files with 481 additions and 133 deletions

View File

@@ -380,6 +380,8 @@ function TabLibrary({ initialTemplateId }: { initialTemplateId?: number | null }
const [saving, setSaving] = useState(false)
const [deleting, setDeleting] = useState(false)
const [savingLag, setSavingLag] = useState(false)
const [savingTheory, setSavingTheory] = useState(false)
const [genTheory, setGenTheory] = useState(false)
const [editCoefs, setEditCoefs] = useState<Record<string, number>>({})
const [editEdgesLag, setEdgesLag] = useState<CausalEdge[]>([])
const [loading, setLoading] = useState(true)
@@ -440,6 +442,30 @@ function TabLibrary({ initialTemplateId }: { initialTemplateId?: number | null }
} finally { setSavingLag(false) }
}
async function generateTheory() {
if (!selected) return
setGenTheory(true)
try {
await api(`/api/causal-lab/template/${selected.id}/generate-theory`, { method: 'POST' })
const fresh = await api(`/api/causal-lab/template/${selected.id}`)
setSelected(fresh)
} finally { setGenTheory(false) }
}
async function saveTheoryParams(updates: Record<string, unknown>) {
if (!selected) return
setSavingTheory(true)
try {
const newCalib = { ...(selected.calibration_json || {}), ...updates }
await api(`/api/causal-lab/template/${selected.id}`, {
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ calibration_json: newCalib }),
})
const fresh = await api(`/api/causal-lab/template/${selected.id}`)
setSelected(fresh)
} finally { setSavingTheory(false) }
}
function updateEdgeLag(i: number, field: 'lag_min' | 'diffusion_min' | 'decay_days', val: string) {
setEdgesLag(prev => prev.map((e, idx) => idx !== i ? e : {
...e,
@@ -609,6 +635,82 @@ function TabLibrary({ initialTemplateId }: { initialTemplateId?: number | null }
</button>
</div>
)}
{/* Paramètres théoriques */}
<div className="bg-dark-700 rounded-lg p-4 border border-violet-800/30">
<div className="flex items-center gap-2 mb-3">
<h4 className="text-slate-300 text-xs font-semibold uppercase tracking-wider flex items-center gap-2 flex-1">
<span className="text-violet-400"></span> Paramètres théoriques
</h4>
<button
onClick={generateTheory}
disabled={genTheory}
className="px-3 py-1 bg-violet-700 hover:bg-violet-600 disabled:opacity-50 rounded text-xs font-medium text-white flex items-center gap-1.5"
>
{genTheory ? (
<><span className="animate-spin inline-block"></span> Génération</>
) : (
<><span></span> Générer IA</>
)}
</button>
</div>
{(() => {
const calib = selected.calibration_json || {}
const absorption = calib.absorption_days as number | undefined
const decay = calib.decay_type as string | undefined
const conf = calib.confidence as number | undefined
const rationale = calib.theory_rationale as string | undefined
const genAt = calib.theory_generated_at as string | undefined
return (
<div className="space-y-3">
{rationale && (
<p className="text-xs text-violet-300 italic border-l-2 border-violet-700/60 pl-2">{rationale}</p>
)}
<div className="grid grid-cols-2 gap-3">
<div>
<div className="text-xs text-slate-500 mb-1">Durée absorption (j)</div>
<input
type="number" min={1} max={60} step={1}
value={absorption ?? ''}
placeholder="—"
onChange={e => saveTheoryParams({ absorption_days: parseInt(e.target.value) || 7 })}
className="w-full bg-dark-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 text-center"
/>
</div>
<div>
<div className="text-xs text-slate-500 mb-1">Type de décroissance</div>
<select
value={decay ?? 'exp'}
onChange={e => saveTheoryParams({ decay_type: e.target.value })}
className="w-full bg-dark-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200"
>
<option value="step">step tout ou rien</option>
<option value="linear">linear déclin linéaire</option>
<option value="exp">exp déclin exponentiel</option>
</select>
</div>
</div>
<div className="flex items-center gap-3 text-xs text-slate-500">
{conf !== undefined && (
<span className="flex items-center gap-1">
Confiance IA :
<span className={`font-mono font-semibold ${conf >= 0.7 ? 'text-emerald-400' : conf >= 0.4 ? 'text-yellow-400' : 'text-red-400'}`}>
{Math.round(conf * 100)}%
</span>
</span>
)}
{genAt && (
<span className="ml-auto opacity-60">
{new Date(genAt).toLocaleDateString('fr-FR')}
</span>
)}
{savingTheory && <span className="text-violet-400 animate-pulse">Sauvegarde</span>}
</div>
</div>
)
})()}
</div>
</div>
) : (
<div className="flex-1 flex items-center justify-center text-slate-600 text-sm">