fix: PatternLab save UX — color-coded toasts + meaningful error feedback

- handleSave now shows a red toast "Lancez d'abord un backtest" instead of
  silently returning when activeRun is null (was completely invisible to user)
- All toasts now color-coded: green (emerald) for success, red for errors
- list_runs now includes context_snapshot so market data table shows when
  loading a historical run from the right panel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-22 22:30:47 +02:00
parent 33097e1812
commit f2dd859ba0
2 changed files with 28 additions and 20 deletions

View File

@@ -168,7 +168,7 @@ def list_runs():
conn = get_conn()
rows = conn.execute("""SELECT id, preset_id, theme, analysis_date, horizon_days,
assets, status, created_at, evaluated_at,
outcome, ai_result
outcome, ai_result, context_snapshot
FROM backtest_lab_runs ORDER BY created_at DESC LIMIT 100""").fetchall()
conn.close()
return [_row_to_dict(r) for r in rows]

View File

@@ -296,7 +296,7 @@ export default function PatternLab() {
// ── Current run state (events mode)
const [activeRun, setActiveRun] = useState<any | null>(null)
const [savedIdx, setSavedIdx] = useState<Set<number>>(new Set())
const [toast, setToast] = useState<string | null>(null)
const [toast, setToast] = useState<{ msg: string; type: 'ok' | 'err' } | null>(null)
const [matchResults, setMatchResults] = useState<Record<string, MatchResult>>({})
const [matchLoading, setMatchLoading] = useState<Record<string, boolean>>({})
@@ -307,9 +307,9 @@ export default function PatternLab() {
return true
}), [search, yearFilter, catFilter])
const showToast = (msg: string) => {
setToast(msg)
setTimeout(() => setToast(null), 3000)
const showToast = (msg: string, type: 'ok' | 'err' = 'ok') => {
setToast({ msg, type })
setTimeout(() => setToast(null), 4000)
}
const handleSelectPreset = (p: Preset) => {
@@ -344,7 +344,7 @@ export default function PatternLab() {
const res = await discoverEvents({ query: discoverQuery.trim(), n: 6 })
setDiscoverResults(res.events)
} catch (e: any) {
showToast(`Discover failed: ${e?.response?.data?.detail ?? e?.message}`)
showToast(`Discover failed: ${e?.response?.data?.detail ?? e?.message}`, 'err')
}
}
@@ -363,7 +363,7 @@ export default function PatternLab() {
setActiveRun(result)
setSavedIdx(new Set())
} catch (e: any) {
showToast(`Error: ${e?.response?.data?.detail ?? e?.message ?? 'unknown'}`)
showToast(`Error: ${e?.response?.data?.detail ?? e?.message ?? 'unknown'}`, 'err')
}
}
@@ -373,7 +373,7 @@ export default function PatternLab() {
const result = await evaluateRun(activeRun.run_id)
setActiveRun((prev: any) => ({ ...prev, outcome: result.outcomes }))
} catch (e: any) {
showToast(`Error: ${e?.response?.data?.detail ?? e?.message ?? 'unknown'}`)
showToast(`Error: ${e?.response?.data?.detail ?? e?.message ?? 'unknown'}`, 'err')
}
}
@@ -387,7 +387,7 @@ export default function PatternLab() {
const result = await findMatching({ run_id: activeRun.run_id, pattern_index: idx })
setMatchResults(prev => ({ ...prev, [key]: result }))
} catch (e: any) {
showToast(`Find matching failed: ${e?.response?.data?.detail ?? e?.message}`)
showToast(`Find matching failed: ${e?.response?.data?.detail ?? e?.message}`, 'err')
} finally {
setMatchLoading(prev => ({ ...prev, [key]: false }))
}
@@ -399,7 +399,10 @@ export default function PatternLab() {
target_id?: string,
regime_tag?: string,
) => {
if (!activeRun?.run_id) return
if (!activeRun?.run_id) {
showToast('Lancez d\'abord un backtest (bouton Run)', 'err')
return
}
const pat = activeRun.ai_result?.patterns?.[idx]
if (!pat) return
try {
@@ -415,9 +418,9 @@ export default function PatternLab() {
})
setSavedIdx(prev => new Set([...prev, idx]))
const actionLabel = action === 'instance' ? 'merged as instance' : action === 'counter' ? 'saved as counter-scenario' : 'saved'
showToast(`"${pat.name}" ${actionLabel}`)
showToast(`"${pat.name}" ${actionLabel}`, 'ok')
} catch (e: any) {
showToast(`Save failed: ${e?.response?.data?.detail ?? e?.message}`)
showToast(`Save failed: ${e?.response?.data?.detail ?? e?.message}`, 'err')
}
}
@@ -442,7 +445,7 @@ export default function PatternLab() {
const result = await evaluateInst(instRun.run_id)
setInstRun((prev: any) => ({ ...prev, outcome: result.outcomes }))
} catch (e: any) {
showToast(`Error: ${e?.response?.data?.detail ?? e?.message ?? 'unknown'}`)
showToast(`Error: ${e?.response?.data?.detail ?? e?.message ?? 'unknown'}`, 'err')
}
}
@@ -453,9 +456,9 @@ export default function PatternLab() {
try {
await savePattern({ run_id: instRun.run_id, pattern_index: idx, name: pat.name, category: pat.category, signal_direction: pat.signal_direction })
setInstSavedIdx(prev => new Set([...prev, idx]))
showToast(`"${pat.name}" saved to Pattern Library`)
showToast(`"${pat.name}" saved to Pattern Library`, 'ok')
} catch (e: any) {
showToast(`Save failed: ${e?.response?.data?.detail ?? e?.message}`)
showToast(`Save failed: ${e?.response?.data?.detail ?? e?.message}`, 'err')
}
}
@@ -1089,10 +1092,10 @@ export default function PatternLab() {
setEditAssets((Array.isArray(run.assets) ? run.assets : []).join(', '))
setEditHint(run.theme_hint ?? '')
setActiveRun({
run_id: run.id,
context: run.context_snapshot,
run_id: run.id,
context: run.context_snapshot,
ai_result: run.ai_result,
outcome: run.outcome,
outcome: run.outcome,
})
setSavedIdx(new Set())
setMatchResults({})
@@ -1135,8 +1138,13 @@ export default function PatternLab() {
{/* ── Toast ── */}
{toast && (
<div className="fixed bottom-5 left-1/2 -translate-x-1/2 bg-slate-800 border border-slate-600 rounded-lg px-4 py-2 text-sm text-slate-200 shadow-xl z-50">
{toast}
<div className={clsx(
'fixed bottom-5 left-1/2 -translate-x-1/2 rounded-lg px-4 py-2 text-sm shadow-xl z-50 border',
toast.type === 'ok'
? 'bg-emerald-900/90 border-emerald-600 text-emerald-100'
: 'bg-red-900/90 border-red-600 text-red-100'
)}>
{toast.msg}
</div>
)}
</div>