The rule pack in prose.ts already found "a apple" — articles, pluralAfterNumber, subjectVerbAgreement, uncountables are all there, and they already surface as real mechanics cards. But mechanicsFindings only ran inside runCheck, behind the same 4s checkpoint debounce as the model, and only reached the screen via the server's reply. A free, instant, offline-capable detection was being delivered on an LLM-shaped delay. The rule pack now runs on its own 250ms fuse and renders its findings with no network at all, as provisional cards. The mechanics submit follows; its reply is authoritative and clears them. If the reply never comes — offline, server down — the cards simply stay, which is the whole point of having rules that need no model. Provisional cards are keyed by wording rather than position, so one can't flicker into a duplicate of its own persisted twin while she types around it. resolveServerId maps a card to the row the API can act on, awaiting the in-flight submit, so accepting inside that window still records the keep and plants its word in the garden instead of being quietly dropped; null means there is no row and the edit has landed regardless. Findings she actions while provisional are remembered client-side, because the detector has no memory between runs. runCheck no longer re-submits what the fast pass already filed — it's the catch-up path for when that submit failed. The arrival chime keys rule-pack cards by wording too, so a finding doesn't chime once as provisional and again as persisted. Not done, deliberately: no distinct style for unconfirmed local hits. The rail renders both engines identically on purpose, and a provisional card now lives for one LAN round-trip. Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
328 lines
14 KiB
TypeScript
328 lines
14 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
import { api, type MechanicsFinding, type Suggestion } from '../api/client'
|
|
import { mechanicsFindings } from '../components/Companion/prose'
|
|
|
|
const DEBOUNCE_MS = 4000
|
|
|
|
// The deterministic rule pack costs nothing to run, so it gets its own short
|
|
// debounce: "a apple" should underline while she's still on the sentence, not
|
|
// four seconds after she stops typing and then only once the server answers.
|
|
const FAST_MS = 250
|
|
|
|
// A local finding's identity, used to match a provisional card against its
|
|
// persisted twin and to remember the ones she's already dealt with. The span
|
|
// start is deliberately left out: the same fix at a shifted offset is the same
|
|
// fix, and offsets move under her cursor constantly.
|
|
export const findingKey = (f: { original: string; replacement: string }) =>
|
|
`${f.original} |