From 09e334359f22a5e0271caa4d145875ca7fb0e89e Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 8 Apr 2026 18:49:38 -0700 Subject: [PATCH] fix: pessimistic submit/undo, config null-safe, load config on mount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sft.py GET /config: use `or {}` guard so `sft: ~` (null YAML) doesn't return None instead of the default empty config - CorrectionsView: convert handleCorrect/Discard/Flag and handleUndo from optimistic to pessimistic — queue mutation only happens after server confirms; failures leave item in queue so user can retry cleanly - SettingsView: call loadSftConfig() on mount so saved bench_results_dir is populated instead of always starting empty --- app/sft.py | 3 +- web/src/views/CorrectionsView.vue | 105 ++++++++++++++++++------------ web/src/views/SettingsView.vue | 17 ++++- 3 files changed, 82 insertions(+), 43 deletions(-) diff --git a/app/sft.py b/app/sft.py index 97ae573..929b98a 100644 --- a/app/sft.py +++ b/app/sft.py @@ -285,7 +285,8 @@ def get_sft_config() -> dict: raw = yaml.safe_load(f.read_text(encoding="utf-8")) or {} except yaml.YAMLError: return {"bench_results_dir": ""} - return raw.get("sft", {"bench_results_dir": ""}) + sft_section = raw.get("sft") or {} + return {"bench_results_dir": sft_section.get("bench_results_dir", "")} class SftConfigPayload(BaseModel): diff --git a/web/src/views/CorrectionsView.vue b/web/src/views/CorrectionsView.vue index f4f194f..3245a86 100644 --- a/web/src/views/CorrectionsView.vue +++ b/web/src/views/CorrectionsView.vue @@ -116,61 +116,84 @@ function startCorrection() { async function handleCorrect(text: string) { if (!store.current) return const item = store.current - store.setLastAction('correct', item) - store.removeCurrentFromQueue() - correcting.value = false - store.totalRemaining = Math.max(0, store.totalRemaining - 1) - await fetch('/api/sft/submit', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id: item.id, action: 'correct', corrected_response: text }), - }) - fetchStats() - if (store.queue.length < 5) fetchBatch() + correcting.value = false + try { + const res = await fetch('/api/sft/submit', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: item.id, action: 'correct', corrected_response: text }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + store.removeCurrentFromQueue() + store.setLastAction('correct', item) + store.totalRemaining = Math.max(0, store.totalRemaining - 1) + fetchStats() + if (store.queue.length < 5) fetchBatch() + } catch (err) { + console.error('handleCorrect failed:', err) + } } async function handleDiscard() { if (!store.current) return const item = store.current - store.setLastAction('discard', item) - store.removeCurrentFromQueue() - store.totalRemaining = Math.max(0, store.totalRemaining - 1) - await fetch('/api/sft/submit', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id: item.id, action: 'discard' }), - }) - fetchStats() - if (store.queue.length < 5) fetchBatch() + try { + const res = await fetch('/api/sft/submit', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: item.id, action: 'discard' }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + store.removeCurrentFromQueue() + store.setLastAction('discard', item) + store.totalRemaining = Math.max(0, store.totalRemaining - 1) + fetchStats() + if (store.queue.length < 5) fetchBatch() + } catch (err) { + console.error('handleDiscard failed:', err) + } } async function handleFlag() { if (!store.current) return const item = store.current - store.setLastAction('flag', item) - store.removeCurrentFromQueue() - store.totalRemaining = Math.max(0, store.totalRemaining - 1) - await fetch('/api/sft/submit', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id: item.id, action: 'flag' }), - }) - fetchStats() - if (store.queue.length < 5) fetchBatch() + try { + const res = await fetch('/api/sft/submit', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: item.id, action: 'flag' }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + store.removeCurrentFromQueue() + store.setLastAction('flag', item) + store.totalRemaining = Math.max(0, store.totalRemaining - 1) + fetchStats() + if (store.queue.length < 5) fetchBatch() + } catch (err) { + console.error('handleFlag failed:', err) + } } async function handleUndo() { if (!store.lastAction) return - const { item } = store.lastAction - store.restoreItem(item) - store.totalRemaining++ - store.clearLastAction() - await fetch('/api/sft/undo', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id: item.id }), - }) - fetchStats() + const action = store.lastAction + const { item } = action + try { + const res = await fetch('/api/sft/undo', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: item.id }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + store.restoreItem(item) + store.totalRemaining++ + store.clearLastAction() + fetchStats() + } catch (err) { + // Backend did not restore — clear the undo UI without restoring queue state + console.error('handleUndo failed:', err) + store.clearLastAction() + } } onMounted(() => { diff --git a/web/src/views/SettingsView.vue b/web/src/views/SettingsView.vue index bcfe7ac..032ddc1 100644 --- a/web/src/views/SettingsView.vue +++ b/web/src/views/SettingsView.vue @@ -206,6 +206,18 @@ const importingRunId = ref(null) const importResult = ref<{ imported: number; skipped: number } | null>(null) const saveStatus = ref('') +async function loadSftConfig() { + try { + const res = await fetch('/api/sft/config') + if (res.ok) { + const data = await res.json() + benchResultsDir.value = data.bench_results_dir ?? '' + } + } catch { + // non-fatal — leave field empty + } +} + async function saveSftConfig() { saveStatus.value = 'Saving…' try { @@ -322,7 +334,10 @@ function onKeyHintsChange() { document.documentElement.classList.toggle('hide-key-hints', !keyHints.value) } -onMounted(reload) +onMounted(() => { + reload() + loadSftConfig() +})