The pass announces the verdict it just decided
Storing doc_lang on the document row is not enough on its own. The editor sees that row when the document is opened or saved, and the pass that decides the verdict runs after a save — so the client was always one save behind, and read-aloud is reached for precisely when she has stopped typing and no further save is coming. Heard in a browser: a Portuguese paragraph read in an American voice, twice, until another keystroke went in. /check, /voice and /collocation now answer with X-Petal-Doc-Lang. A header rather than a wider body: all three answer with a bare array of the unified pending set and every caller reads it as one, and a verdict is metadata about the pass rather than another suggestion. It reaches the app through the same handler shape onUnauthorized already uses. Claude-Session: https://claude.ai/code/session_01GJHNvirh7Hzhc9RL3HAvz7
This commit is contained in:
+27
-4
@@ -276,11 +276,34 @@ function signedOut(): UnauthorizedError {
|
||||
return new UnauthorizedError()
|
||||
}
|
||||
|
||||
async function req<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
// The document-language verdict is decided by the checkpoint pass, so the pass's
|
||||
// own response is the first moment the client can know it. It rides on a header
|
||||
// (the pass answers with a bare array of suggestions, and every caller reads it
|
||||
// as one), and reaches the app through a handler registered here — the same
|
||||
// shape onUnauthorized already uses, for the same reason: it is one fact from
|
||||
// deep inside a request that a component several layers up needs.
|
||||
//
|
||||
// Without it the editor learns the verdict only from a document save, which is
|
||||
// always one save behind the pass — and read-aloud is reached for precisely when
|
||||
// she has stopped typing and no further save is coming.
|
||||
let docLangHandler: ((docId: string, lang: DocLang) => void) | null = null
|
||||
|
||||
export function onDocLang(handler: (docId: string, lang: DocLang) => void) {
|
||||
docLangHandler = handler
|
||||
}
|
||||
|
||||
// `verdictFor` names the document whose language this response may announce.
|
||||
// Only the three pass endpoints pass it; everything else has no verdict to carry
|
||||
// and never touches the handler.
|
||||
async function req<T>(path: string, init?: RequestInit, verdictFor?: string): Promise<T> {
|
||||
const res = await fetch(`/api${path}`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...init,
|
||||
})
|
||||
if (verdictFor && res.ok) {
|
||||
const lang = res.headers.get('X-Petal-Doc-Lang')
|
||||
if (lang === '' || lang === 'en' || lang === 'pair') docLangHandler?.(verdictFor, lang)
|
||||
}
|
||||
if (res.status === 401) throw signedOut()
|
||||
if (!res.ok) {
|
||||
const detail = await res.text().catch(() => '')
|
||||
@@ -322,14 +345,14 @@ export const api = {
|
||||
// Rate-limited per document server-side (returns the existing set if too soon).
|
||||
// Both passes return the UNIFIED pending set (grammar + voice), so the client
|
||||
// never drops one family's highlights when the other refreshes.
|
||||
checkDoc: (id: string) => req<Suggestion[]>(`/docs/${id}/check`, { method: 'POST' }),
|
||||
checkDoc: (id: string) => req<Suggestion[]>(`/docs/${id}/check`, { method: 'POST' }, id),
|
||||
// Voice-consistency pass: whole-document, explicit-action, slower. Returns the
|
||||
// unified pending set too. Rate-limited per document server-side.
|
||||
voiceDoc: (id: string) => req<Suggestion[]>(`/docs/${id}/voice`, { method: 'POST' }),
|
||||
voiceDoc: (id: string) => req<Suggestion[]>(`/docs/${id}/voice`, { method: 'POST' }, id),
|
||||
// Collocation coach: whole-document, explicit-action pass flagging non-native
|
||||
// word pairings ("do a decision" → "make a decision"). Returns the unified
|
||||
// pending set too. Rate-limited per document server-side.
|
||||
collocationDoc: (id: string) => req<Suggestion[]>(`/docs/${id}/collocation`, { method: 'POST' }),
|
||||
collocationDoc: (id: string) => req<Suggestion[]>(`/docs/${id}/collocation`, { method: 'POST' }, id),
|
||||
// Mechanics pass: persist the client-detected deterministic fixes as the
|
||||
// 'mechanics' family and return the unified pending set. Not rate-limited (it's
|
||||
// free, local detection); runs alongside the grammar checkpoint.
|
||||
|
||||
Reference in New Issue
Block a user