From f2dd859ba059cb936818cc2631ffdb4e7e45a91c Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Mon, 22 Jun 2026 22:30:47 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20PatternLab=20save=20UX=20=E2=80=94=20col?= =?UTF-8?q?or-coded=20toasts=20+=20meaningful=20error=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backend/routers/pattern_lab.py | 2 +- frontend/src/pages/PatternLab.tsx | 46 ++++++++++++++++++------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/backend/routers/pattern_lab.py b/backend/routers/pattern_lab.py index b4519e8..78a45d9 100644 --- a/backend/routers/pattern_lab.py +++ b/backend/routers/pattern_lab.py @@ -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] diff --git a/frontend/src/pages/PatternLab.tsx b/frontend/src/pages/PatternLab.tsx index 48ca3a7..537e22e 100644 --- a/frontend/src/pages/PatternLab.tsx +++ b/frontend/src/pages/PatternLab.tsx @@ -296,7 +296,7 @@ export default function PatternLab() { // ── Current run state (events mode) const [activeRun, setActiveRun] = useState(null) const [savedIdx, setSavedIdx] = useState>(new Set()) - const [toast, setToast] = useState(null) + const [toast, setToast] = useState<{ msg: string; type: 'ok' | 'err' } | null>(null) const [matchResults, setMatchResults] = useState>({}) const [matchLoading, setMatchLoading] = useState>({}) @@ -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 && ( -
- {toast} +
+ {toast.msg}
)}