fix: pessimistic submit/undo, config null-safe, load config on mount

- 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
This commit is contained in:
pyr0ball 2026-04-08 18:49:38 -07:00
parent 353d0a47a0
commit 09e334359f
3 changed files with 82 additions and 43 deletions

View file

@ -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):

View file

@ -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', {
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', {
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', {
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', {
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(() => {

View file

@ -206,6 +206,18 @@ const importingRunId = ref<string | null>(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()
})
</script>
<style scoped>