feat(treatment): add voice detail entry #65
@@ -145,9 +145,25 @@ path**. `onAddDetail` is not called and not changed.
|
||||
`setEntryStep('treatment')` matters: `showChrome` is always on, so the control is visible
|
||||
during the **Lab** wizard step too. Confirming there returns to the treatment step.
|
||||
|
||||
**Confirm also saves.** The new detail is persisted immediately (`persistDraft({force:true})`),
|
||||
and when the result carries a lab, a due date or a prosthesis map the lab case is saved with it
|
||||
(`persistLabCases`). Not politeness — the autosave effect watches `details` only, so a lab draft
|
||||
left in component state alone loses the destination lab, the due date and the whole prosthesis
|
||||
map on the next reload. The detail survives, which is what makes that loss look like a
|
||||
successful save.
|
||||
|
||||
One guard on it: `persistDraft` returns a **preview** treatment instead of saving when any
|
||||
detail still lacks a treatment type — the blank chip the workspace opens with is enough — and a
|
||||
preview's detail id falls back to the client id. Confirm therefore checks *what came back*, not
|
||||
the precondition, and skips the lab-case save when it did not get a real id; posting a lab case
|
||||
against an id the server has never seen fails the whole save. Checking the result rather than
|
||||
the condition keeps this true for every early return `persistDraft` has.
|
||||
|
||||
> Accepted consequences:
|
||||
> - Tapping Add and then 🎤 leaves behind the blank chip that Add created. It carries the
|
||||
> usual trash affordance.
|
||||
> - That same blank chip blocks confirm's immediate lab-case save until it is given a type
|
||||
> or removed; the lab rows stay in local state until the ordinary Lab-step save.
|
||||
> - Dictating into an existing detail is not supported in v1 — voice always makes a new
|
||||
> one.
|
||||
|
||||
@@ -522,8 +538,8 @@ who can edit treatments, in every configured locale. `Plan.features.voiceTreatme
|
||||
and the availability API stay documented here as the intended gate, deferred rather than
|
||||
dropped, so turning them on later is additive.
|
||||
|
||||
Consequence to accept deliberately: with no plan gate and no duration cap (§2), the
|
||||
per-user throttle is the **only** control on metered vendor spend. See open item 14.
|
||||
Consequence to accept deliberately: with no plan gate, the per-user throttle and the 2-minute
|
||||
recording cap are the **only** controls on metered vendor spend. See open item 14.
|
||||
|
||||
---
|
||||
|
||||
@@ -535,15 +551,31 @@ English Nest exception for a user-facing failure.
|
||||
|
||||
| Code | When |
|
||||
|---|---|
|
||||
| `VOICE_MIC_DENIED` | browser permission refused — **client-side only**: needs the `errors.X` key in all three message files, but no `ErrorCode` entry and no throw site |
|
||||
| `VOICE_CLIP_TOO_LONG` | over `maxMs` (server-side re-check), or over vendor limits |
|
||||
| `VOICE_UNSUPPORTED_FORMAT` | recorder produced a container the profile rejects |
|
||||
| `VOICE_MIC_DENIED` | microphone permission actually refused, or no input device — **client-side only**: needs the `errors.X` key in all three message files, but no `ErrorCode` entry and no throw site. Reserved for a real permission failure: see the note below |
|
||||
| `VOICE_CLIP_TOO_LONG` | over `maxMs` (server-side re-check), over vendor limits, or a request body past the DTO's size cap |
|
||||
| `VOICE_UNSUPPORTED_FORMAT` | **the browser cannot record at all** — no `MediaRecorder`, no container both it and the API accept, or a recorder that throws after permission was granted; and server-side, a `format` outside `VOICE_AUDIO_FORMATS` |
|
||||
| `VOICE_ASR_FAILED` | transcription stage failed |
|
||||
| `VOICE_EXTRACT_FAILED` | transcript obtained, structuring failed |
|
||||
| `VOICE_NOTHING_RECOGNIZED` | empty or unusable transcript |
|
||||
| `VOICE_NOT_AVAILABLE` | no profile for locale (v1); plan flag off, once enforced |
|
||||
| `VOICE_RATE_LIMITED` | throttle |
|
||||
|
||||
**Two of these are raised by DTO validation, not by a throw site.**
|
||||
`validationExceptionFactory` returns a constraint's `message` verbatim when the message is
|
||||
itself a known `ErrorCode`, so the voice DTO names its own failures:
|
||||
`@MaxLength(…, { message: ErrorCode.VOICE_CLIP_TOO_LONG })` and
|
||||
`@IsIn(…, { message: ErrorCode.VOICE_UNSUPPORTED_FORMAT })`. Left to the shared constraint map
|
||||
they fall through to `VALIDATION_FIELD_REQUIRED` and `VALIDATION_LANGUAGE_INVALID` — an
|
||||
oversized recording telling the clinician a field is missing, and an unsupported container
|
||||
telling them their language is invalid. Any new voice constraint should name its code the same
|
||||
way.
|
||||
|
||||
**`VOICE_MIC_DENIED` is only for a real permission failure.** Three client paths used to report
|
||||
it for something else entirely — no `MediaRecorder`, no acceptable container, and a recorder
|
||||
that throws after permission was already granted. All three are "this browser cannot record"
|
||||
and now report `VOICE_UNSUPPORTED_FORMAT`; blaming the microphone sends the clinician hunting
|
||||
in site settings for a permission nothing ever asked for.
|
||||
|
||||
**Transcript salvage — specified, NOT built.** The backend half exists: `VOICE_EXTRACT_FAILED`
|
||||
carries `details.transcript` and `HttpExceptionFilter` forwards it. The client half was
|
||||
never written — `onError` only resolves a message through `getUserFacingError`, which never
|
||||
@@ -701,7 +733,18 @@ enabling this for real clinics.
|
||||
- no layout shift in the header row on record start, stop, or the 2:00 auto-stop;
|
||||
- **hold past 2:00** → auto-stops and proceeds to processing, not an error;
|
||||
- **cancel during processing** → the vendor request is actually aborted;
|
||||
- **review sheet on mobile** → full-screen overlay; closing it leaves the draft intact.
|
||||
- **review sheet on mobile** → full-screen overlay; closing it leaves the draft intact;
|
||||
- **confirm with a lab, a due date or a prosthesis map, then reload** → all three are still
|
||||
there. They live on the lab case, which the autosave effect does not watch, so this is
|
||||
the check that catches a lab draft left unsaved in component state;
|
||||
- **record straight after opening a visit**, while the blank chip is still untyped, and
|
||||
confirm with a lab ticked → no error toast: confirm detects the preview treatment and
|
||||
skips the lab-case save rather than posting an id the server has never seen;
|
||||
- **dictate two different prosthesis types** ("۱۲ روکش PFM، ۱۳ روکش PFZ") → the form shows
|
||||
both, and the dispatch panel's remembered "last type for this lab" does **not** overwrite
|
||||
either. Also check the bulk «اعمال برای همه دندانها» select stays on its placeholder;
|
||||
- **a hand-made prosthesis detail with an empty map** → still gets the remembered default
|
||||
pre-filled. The exemption is for previewed cases only, not a removal of the convenience.
|
||||
|
||||
---
|
||||
|
||||
@@ -733,6 +776,14 @@ Settled in a grilling session on 2026-08-20.
|
||||
| 28 | Review sheet | Modal on desktop, full-screen overlay (not a route) on mobile; candidate chips are its only interactive part |
|
||||
| 29 | Cancel | Aborts the in-flight vendor call |
|
||||
| 30 | v1 gating | Open to everyone; `Plan.features` gate deferred, not dropped |
|
||||
|
||||
Added while getting the first live recordings working (2026-08-21):
|
||||
|
||||
| # | Question | Decision |
|
||||
|---|---|---|
|
||||
| 31 | Tooth numbering | A spoken number **is** its FDI code, in all three locales. A lone digit stays unresolved and offers its candidate teeth as chips (§6, §7) |
|
||||
| 32 | What confirm writes | Confirm persists the detail *and* its lab case, because autosave watches `details` only — but skips the lab-case save when it got a preview treatment back (§2) |
|
||||
| 33 | Preview as contract | Applying a voice result fills exactly what the sheet showed. Per-detail conveniences that would add more are suppressed for that case (§7) |
|
||||
| 15 | Gating | `Plan.features` flag — its first consumer |
|
||||
|
||||
UI placement settled in a second grilling session on 2026-08-20.
|
||||
|
||||
Reference in New Issue
Block a user