Code-review fixes for collocation coach + vocab garden
Correctness: - useCheckpoint: clear the busy flag unconditionally so overlapping explicit passes don't strand each other's spinner; explicit actions now also supersede a queued auto-check and clear the stranded "checking" dot. Deduped runVoice/runCollocation into runExplicitPass. - EditorCore: token-guard the auto-capture so a late capture can't resurrect a removed word; move toggleSaveWord side effects out of the setWordInfo updater (StrictMode double-fire); fix sentenceAround offset desync via shared exampleAt (textBetween + parentOffset, single resolve); optimistic saved state so the heart doesn't flash unsaved. - vocab capture: normalize word to lower+trim (matches lexicon) so "Apple"/"apple" don't make duplicate cards; check rows.Err() in queryList. - GardenPanel: Promise.allSettled so a /due failure doesn't blank the whole garden; scrim click during review ends the review (mirrors Esc); gate footer on !error; O(1) due lookup via a Set. Features requested in review: - Definition-only review card: add vocab_words.definition (migration 0007) as an English fallback meaning, threaded through capture and used by review/garden when there's no Chinese gloss. - Scheduler caps: maxEase 3.0 + maxInterval 365d so "easy" growth can't push a word out of rotation for years. Tests: TestCaptureCaseInsensitive, TestCaptureStoresDefinitionFallback, TestCapsBoundGrowth. go build/vet/test, tsc, vitest 51/51, vite build clean. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
@@ -68,51 +68,53 @@ export function useCheckpoint(docId: string | null) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Run the voice-consistency pass now (explicit "Check my voice" action). Like
|
||||
// runCheck it returns the unified pending set, so grammar highlights survive.
|
||||
// Shares the run token so navigating away discards a late voice response.
|
||||
const runVoice = useCallback(async () => {
|
||||
const id = docIdRef.current
|
||||
if (!id) return
|
||||
clearTimeout(retryRef.current) // a voice pass supersedes a queued grammar retry
|
||||
const run = ++runRef.current
|
||||
setVoicing(true)
|
||||
try {
|
||||
const full = await api.voiceDoc(id)
|
||||
if (run === runRef.current && id === docIdRef.current) {
|
||||
setSuggestions(full)
|
||||
setLlmDown(false)
|
||||
// runExplicitPass drives a whole-document, explicit-action pass (voice,
|
||||
// collocation): it returns the unified pending set, so the other families'
|
||||
// highlights survive, and shares the run token so navigating away discards a
|
||||
// late response. It supersedes any queued grammar retry AND clears the
|
||||
// checkpoint's "checking" state, so the breathing rose dot can't linger on
|
||||
// after the retry timer it would have cleared is cancelled here.
|
||||
const runExplicitPass = useCallback(
|
||||
async (call: (id: string) => Promise<Suggestion[]>, setBusy: (b: boolean) => void, label: string) => {
|
||||
const id = docIdRef.current
|
||||
if (!id) return
|
||||
// An explicit action fully supersedes a queued auto-check and grammar
|
||||
// retry, and takes over the indicator — clear the stranded "checking" dot.
|
||||
clearTimeout(debounceRef.current)
|
||||
clearTimeout(retryRef.current)
|
||||
setChecking(false)
|
||||
const run = ++runRef.current
|
||||
setBusy(true)
|
||||
try {
|
||||
const full = await call(id)
|
||||
if (run === runRef.current && id === docIdRef.current) {
|
||||
setSuggestions(full)
|
||||
setLlmDown(false)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`${label} failed`, err)
|
||||
if (run === runRef.current) setLlmDown(true)
|
||||
} finally {
|
||||
// setBusy is THIS invocation's own flag (a newer run sets its own), so
|
||||
// clear it unconditionally — otherwise an overlapping pass that bumped
|
||||
// the run token would strand this spinner on "Reading…" forever.
|
||||
setBusy(false)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('voice pass failed', err)
|
||||
if (run === runRef.current) setLlmDown(true)
|
||||
} finally {
|
||||
if (run === runRef.current) setVoicing(false)
|
||||
}
|
||||
}, [])
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
// Run the voice-consistency pass now (explicit "Check my voice" action).
|
||||
const runVoice = useCallback(
|
||||
() => runExplicitPass(api.voiceDoc, setVoicing, 'voice pass'),
|
||||
[runExplicitPass],
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
}, [])
|
||||
const runCollocation = useCallback(
|
||||
() => runExplicitPass(api.collocationDoc, setCollocating, 'collocation pass'),
|
||||
[runExplicitPass],
|
||||
)
|
||||
|
||||
// Call on every edit; schedules a check 4s after typing settles.
|
||||
const schedule = useCallback(() => {
|
||||
|
||||
Reference in New Issue
Block a user