Phase 12 + 13: collocation coach + vocabulary garden

Phase 12 — collocation coach: a third suggestion family for gentle
"natives usually say…" hints on non-native word pairings, reusing the
existing runPass/pendingScope/rail machinery.
- llm/collocation.go (RunCollocation, 25s floor, reuses ParseCheckpoint)
  + collocationSystemPrompt/CollocationMessages (warm, Mandarin gloss,
  defers grammar/spelling to the grammar family)
- migration 0005 rebuilds the suggestions table to extend the type CHECK
  (SQLite can't ALTER a CHECK)
- collocationScope + CollocationLimit + POST /{id}/collocation
- fix: grammarScope was `type != 'voice'` and would wipe the new
  collocation flags; now `type NOT IN ('voice','collocation')`
- frontend: --color-blossom, "Make it sound natural 🌸" pill,
  collocating/runCollocation in useCheckpoint, StatusBar dot

Phase 13 — vocabulary garden: capture looked-up words and surface them
for gentle spaced repetition.
- new internal/vocab package: migration 0006 (vocab_words, SM-2-lite
  columns, doc_id ON DELETE SET NULL, UNIQUE(user_id,word)),
  scheduler.go (Leitner ladder 1/3/7/16/35 then geometric; gentle
  "again", no streak-shaming), handlers (capture-upsert/list/due/
  review/delete, owner-scoped, SQLite-side datetime math)
- auto-capture on word lookup (dictionary-known words only, captures
  the surrounding sentence + doc_id) + 🤍/💚 toggle on WordCard
- GardenPanel: blossom grid (bloom by reps), flashcard review (sentence
  blanked, flip, again/good/easy, recognition↔production), sleepy-kitten
  footer; opened from a global 🌷 header button

Tests: TestCollocationPassCoexists, vocab scheduler + handlers, db CHECK
extended. go build/vet/test + tsc + vite + vitest (51/51) clean;
migration verified against a copy of the live DB; live backend smoke
walked the full vocab lifecycle + the warm-502 collocation path.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
prosolis
2026-06-26 15:50:25 -07:00
parent 20442d1356
commit 8aa437ec82
23 changed files with 1614 additions and 58 deletions

View File

@@ -13,6 +13,8 @@ export function useCheckpoint(docId: string | null) {
const [checking, setChecking] = useState(false)
// True while a whole-document voice pass is in flight (explicit user action).
const [voicing, setVoicing] = useState(false)
// True while a whole-document collocation pass is in flight (explicit action).
const [collocating, setCollocating] = useState(false)
// True when the last LLM pass couldn't reach the model (server 502 or network
// error). Drives a gentle, reassuring "helper is resting" note — the writing
// itself still saves fine, so this is awareness, not an error. Cleared on the
@@ -89,6 +91,29 @@ export function useCheckpoint(docId: string | null) {
}
}, [])
// Run the collocation coach now (explicit "Make it sound natural" action).
// Like runVoice it returns the unified pending set, so grammar + voice
// highlights survive. Shares the run token so a late response is discarded.
const runCollocation = useCallback(async () => {
const id = docIdRef.current
if (!id) return
clearTimeout(retryRef.current) // a collocation pass supersedes a queued grammar retry
const run = ++runRef.current
setCollocating(true)
try {
const full = await api.collocationDoc(id)
if (run === runRef.current && id === docIdRef.current) {
setSuggestions(full)
setLlmDown(false)
}
} catch (err) {
console.error('collocation pass failed', err)
if (run === runRef.current) setLlmDown(true)
} finally {
if (run === runRef.current) setCollocating(false)
}
}, [])
// Call on every edit; schedules a check 4s after typing settles.
const schedule = useCallback(() => {
clearTimeout(debounceRef.current)
@@ -105,6 +130,7 @@ export function useCheckpoint(docId: string | null) {
setSuggestions([])
setChecking(false)
setVoicing(false)
setCollocating(false)
setLlmDown(false)
if (!docId) return
let cancelled = false
@@ -134,5 +160,5 @@ export function useCheckpoint(docId: string | null) {
setSuggestions((prev) => prev.filter((s) => s.id !== id))
}, [])
return { suggestions, checking, voicing, llmDown, schedule, runVoice, removeSuggestion }
return { suggestions, checking, voicing, collocating, llmDown, schedule, runVoice, runCollocation, removeSuggestion }
}