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() conn = get_conn()
rows = conn.execute("""SELECT id, preset_id, theme, analysis_date, horizon_days, rows = conn.execute("""SELECT id, preset_id, theme, analysis_date, horizon_days,
assets, status, created_at, evaluated_at, 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() FROM backtest_lab_runs ORDER BY created_at DESC LIMIT 100""").fetchall()
conn.close() conn.close()
return [_row_to_dict(r) for r in rows] return [_row_to_dict(r) for r in rows]

View File

@@ -296,7 +296,7 @@ export default function PatternLab() {
// ── Current run state (events mode) // ── Current run state (events mode)
const [activeRun, setActiveRun] = useState<any | null>(null) const [activeRun, setActiveRun] = useState<any | null>(null)
const [savedIdx, setSavedIdx] = useState<Set<number>>(new Set()) 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 [matchResults, setMatchResults] = useState<Record<string, MatchResult>>({})
const [matchLoading, setMatchLoading] = useState<Record<string, boolean>>({}) const [matchLoading, setMatchLoading] = useState<Record<string, boolean>>({})
@@ -307,9 +307,9 @@ export default function PatternLab() {
return true return true
}), [search, yearFilter, catFilter]) }), [search, yearFilter, catFilter])
const showToast = (msg: string) => { const showToast = (msg: string, type: 'ok' | 'err' = 'ok') => {
setToast(msg) setToast({ msg, type })
setTimeout(() => setToast(null), 3000) setTimeout(() => setToast(null), 4000)
} }
const handleSelectPreset = (p: Preset) => { const handleSelectPreset = (p: Preset) => {
@@ -344,7 +344,7 @@ export default function PatternLab() {
const res = await discoverEvents({ query: discoverQuery.trim(), n: 6 }) const res = await discoverEvents({ query: discoverQuery.trim(), n: 6 })
setDiscoverResults(res.events) setDiscoverResults(res.events)
} catch (e: any) { } 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) setActiveRun(result)
setSavedIdx(new Set()) setSavedIdx(new Set())
} catch (e: any) { } 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) const result = await evaluateRun(activeRun.run_id)
setActiveRun((prev: any) => ({ ...prev, outcome: result.outcomes })) setActiveRun((prev: any) => ({ ...prev, outcome: result.outcomes }))
} catch (e: any) { } 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 }) const result = await findMatching({ run_id: activeRun.run_id, pattern_index: idx })
setMatchResults(prev => ({ ...prev, [key]: result })) setMatchResults(prev => ({ ...prev, [key]: result }))
} catch (e: any) { } 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 { } finally {
setMatchLoading(prev => ({ ...prev, [key]: false })) setMatchLoading(prev => ({ ...prev, [key]: false }))
} }
@@ -399,7 +399,10 @@ export default function PatternLab() {
target_id?: string, target_id?: string,
regime_tag?: 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] const pat = activeRun.ai_result?.patterns?.[idx]
if (!pat) return if (!pat) return
try { try {
@@ -415,9 +418,9 @@ export default function PatternLab() {
}) })
setSavedIdx(prev => new Set([...prev, idx])) setSavedIdx(prev => new Set([...prev, idx]))
const actionLabel = action === 'instance' ? 'merged as instance' : action === 'counter' ? 'saved as counter-scenario' : 'saved' 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) { } 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) const result = await evaluateInst(instRun.run_id)
setInstRun((prev: any) => ({ ...prev, outcome: result.outcomes })) setInstRun((prev: any) => ({ ...prev, outcome: result.outcomes }))
} catch (e: any) { } 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 { try {
await savePattern({ run_id: instRun.run_id, pattern_index: idx, name: pat.name, category: pat.category, signal_direction: pat.signal_direction }) 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])) setInstSavedIdx(prev => new Set([...prev, idx]))
showToast(`"${pat.name}" saved to Pattern Library`) showToast(`"${pat.name}" saved to Pattern Library`, 'ok')
} catch (e: any) { } 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(', ')) setEditAssets((Array.isArray(run.assets) ? run.assets : []).join(', '))
setEditHint(run.theme_hint ?? '') setEditHint(run.theme_hint ?? '')
setActiveRun({ setActiveRun({
run_id: run.id, run_id: run.id,
context: run.context_snapshot, context: run.context_snapshot,
ai_result: run.ai_result, ai_result: run.ai_result,
outcome: run.outcome, outcome: run.outcome,
}) })
setSavedIdx(new Set()) setSavedIdx(new Set())
setMatchResults({}) setMatchResults({})
@@ -1135,8 +1138,13 @@ export default function PatternLab() {
{/* ── Toast ── */} {/* ── Toast ── */}
{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"> <div className={clsx(
{toast} '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>
)} )}
</div> </div>