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:
@@ -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]
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user