The spec still described the flow as designed, not as it works after the
first live recordings.
§2 gains what confirm actually writes: the detail and its lab case are
persisted on confirm, because the autosave effect watches `details` only
and a lab draft left in component state loses the lab, the due date and the
prosthesis map on reload — while the detail survives, which is what makes
that loss look like a save. Plus the guard: a preview treatment comes back
instead when any detail is still untyped, and confirm skips the lab-case
save rather than posting an id the server has never seen.
§9 corrects three codes: VOICE_MIC_DENIED is now only a real permission
failure, VOICE_UNSUPPORTED_FORMAT covers every "this browser cannot record"
path, and both it and VOICE_CLIP_TOO_LONG are named on their DTO
constraints rather than falling through the shared map to an unrelated
message. §8 no longer claims there is no duration cap — there is, 2
minutes, decided before implementation.
§12 gains the checks these changes need, including the reload that catches
an unsaved lab case, and decisions 31-33 record the three rules the live
testing settled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dictating "12 روکش PFM, 13 روکش PFZ" previewed correctly and then landed in
the form as PFM on both teeth.
The stored data was never wrong — the dev database holds 12 → pfm_crown and
13 → pfz_crown with selectionGroupIds matching the detail's groups exactly,
and a page reload renders it correctly. The damage was live client state:
the dispatch panel's "last type used for this lab" default rebuilt the
*entire* map from one code, so a single row reading as unfilled destroyed
every type already set.
Two changes:
- It fills blanks now, and leaves every entry that already carries a type
alone. The bulk "apply to all" select only pre-sets itself when the fill
really did cover every tooth, instead of claiming one type while the rows
below disagree.
- A lab case created by confirming a voice result is exempt from the
default entirely. The review sheet is a contract: topping the case up
with a type for a tooth the preview never showed makes the confirmation
step a lie about what it was going to fill.
The exemption is tracked in workspace state rather than on LabCaseDraft
because a draft field is dropped by mapLabCaseDraftFromApi on the first
server round-trip — exactly the window this failure lives in.
isProsthesisMapComplete is deliberately untouched: its strict
selectionGroupId match succeeds on the real data, so loosening it would
have been a blind change to a working path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The backend returns the transcript on VOICE_EXTRACT_FAILED and the client
never reads it, so dictation the clinic paid for is shipped in an error
body and dropped. The spec claimed the whole feature was implemented; it
now names the gap and the two ways out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The spec's tooth section described the design that the first live test
disproved — descriptive phrasing primary, bare numerals refused in en.
Rewrites §6 around the rule the product actually wants, records the chip
affordance in §7, and closes open item §11.5: a two-digit number is FDI in
all three locales, with the Universal-numbering trade-off written down
rather than left implied.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implemented across 12 commits on feat/voice-treatment-entry. Still blocked on
the Persian ASR spike before it is trustworthy in front of patients: nothing in
the implementation compensates for a bad transcript.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The microphone becomes the second segment of the Add detail button, built like
the detail chip's trash affordance in the same file — an overflow-hidden rounded
wrapper holding two raw <button>s divided by border-s — rather than two shared
Buttons, which each hardcode their own rounding and would fight a segmented
control. border-s puts the mic at the logical end: visually right in en/nl,
visually left in fa, on the same side as the chip's trash in both directions.
The two halves share a wrapper and nothing else. Add keeps its exact behaviour.
The control never changes size while recording; the timer and level meter live
in a bar between the header row and the chip strip, because the header is
sm:justify-between and growing the button would shove the row on every start and
stop. The meter exists to prove the microphone is actually hearing something —
silence and a dead mic look identical otherwise.
Voice reaches the editor as one optional `voice` prop, so its absence *is* the
unavailable state and the two cannot disagree.
Fixes from review of this commit:
- mountedRef was set false on unmount and never re-armed, so under StrictMode
the hook was permanently "unmounted" in dev and recording silently never
started.
- onStart guarded only on `phase`, which does not change until getUserMedia
resolves; a second click during the permission prompt orphaned the first
MediaStream, leaving the mic indicator lit.
- Week start is now per locale. "Next Thursday" is week-relative, and hardcoding
Saturday put an en/nl clinician's deadline a week out.
- A missing `which` on a weekday intent is read as "this" rather than failing —
a bare weekday carries no qualifier, and rejecting it discarded a real
deadline.
- durationMs is client-reported and so is a claim, not enforcement; the cap is
now also checked against the vendor's own usage.seconds.
- Blob type falls back to the recorder's actual mimeType before webm, so old
Safari's mp4/aac clips are not mislabelled.
Two review findings were rejected as incorrect, both re-verified against live
sources: google/gemini-3.7-flash does exist on OpenRouter (1M context,
$0.375/$1.875 per M), and base64 JSON input_audio is the documented primary
path for /audio/transcriptions, with multipart as the OpenAI-compatible
alternative. The spec's stale "unverified" note is corrected, and the provider
now has unit tests covering the request shape, usage parsing, and that a vendor
error body never reaches the thrown message.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Design spec for filling a TreatmentDetail by voice, settled across three
grilling sessions (30 decisions, logged in the spec).
Key shape:
- two-stage pipeline: OpenRouter whisper-1 -> gemini-3.7-flash
- the LLM emits *intents*, never FDI codes or ISO dates; pure Jest-tested
backend resolvers own quadrant mapping and Jalali conversion
- provider registry keyed by locale so fa can diverge from en/nl
- review sheet confirms before anything touches the form
- audio and transcripts are never persisted
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>