Three concurrent themes from this session:
═══════════════════════════════════════════════════════════════════
ED ENCOUNTERS — per-stage cards + consolidate→MDM finalize
═══════════════════════════════════════════════════════════════════
UX redesign per Daniel's feedback ("every stage note should be shown,
if AI is told to modify that particular note then the modified version
is used in final mdm"):
- Each generated stage stays on screen as its own editable card with
its own embedded "Don't Miss" panel. No more single rolling note
element that gets replaced on each generation.
- gatherCurrentNotes() reads contenteditable text from each stage card
before any operation (advance, finalize, persist) so inline edits
flow into the next AI call and the final consolidate.
- Stage badge is now state-accurate. "Stage N (recording)" with yellow
background after Add-more before generation; "Stage N" with gray
after generation. Fixes the bug where the badge flipped to Stage 2
the moment Add-more was clicked.
- Save & Done now runs TWO server-side AI calls in /finalize:
1. edConsolidate (new prompt) → polished single final note that
integrates every stage chronologically (HPI / ROS / PE / ED Course /
A&P with disposition).
2. edFinalize (rewritten with full inline 2023 AMA E/M element
rubric — problems / data / risk definitions, level mapping with
concrete examples) → MDM JSON.
- Two new cards render after finalize: blue-bordered Final Consolidated
Note + green-bordered MDM. Stage cards become read-only.
- partial_data on the saved row now stores {stages, finalNote, mdm,
finalized} so resume re-renders the full state.
Why two-call finalize: a single combined prompt makes the model cut
corners on one task. Two focused calls cost ~2× latency at the very end
of an encounter — acceptable since finalize is a one-time terminal
action, not a per-stage hot path.
Files: public/components/ed-encounter.html, public/js/ed-encounters.js,
src/routes/edEncounters.js, src/utils/prompts.js (edConsolidate added,
edFinalize rewritten).
═══════════════════════════════════════════════════════════════════
EXTENSIONS / PAGERS — visual polish
═══════════════════════════════════════════════════════════════════
Multiple iterations based on Daniel's feedback:
- Layout: align-items:flex-start so action buttons stay pinned top-right
when long numbers wrap (was align-items:center → buttons drifted into
the text area, causing visible overlap).
- Number: word-break:break-all + min-width:0 + font-feature-settings:tnum
so long numbers wrap within their column instead of pushing under the
buttons. Click-to-copy with a 0.55s green flash + ✓ copied badge.
- Phone/pager Font Awesome icon next to the number in the type color —
at-a-glance type signal (replacing an earlier 3px left stripe that
Daniel found visually bulky).
- Name: font-weight 700, font-size 14.5px, color g900, letter-spacing
-0.012em — scan-target headline typography for long lists.
- Alternating subtle backgrounds by index (white vs #fafbfc) so a long
list reads as distinct rows.
- Hover: card lifts 1px with a soft shadow; action buttons fade from
55% to 100% opacity. Cubic-bezier transition on transform.
- Entrance: staggered fade-up animation per card (35ms × index, capped
at 12). prefers-reduced-motion media query disables motion.
- Empty state: 48px FA icon + heading instead of plain gray text.
Files: public/js/extensions.js, public/css/styles.css.
═══════════════════════════════════════════════════════════════════
DOCS REORGANIZATION + APPLICATION-LOGIC DOCS + ADMIN VIEWER
═══════════════════════════════════════════════════════════════════
Document moves (preserving git history via git mv):
BROWSER_WHISPER_SETUP.md → docs/browser-whisper-setup.md
BROWSER_WHISPER_TROUBLESHOOTING.md → docs/browser-whisper-troubleshooting.md
DEVELOPER_GUIDE.md → docs/developer-guide-extended.md
EMBEDDINGS_SETUP.md → docs/embeddings-setup.md
FEATURES_EXPLAINED.md → docs/features-explained.md
IMPROVEMENTS.md → docs/improvements.md
OPENID_SETUP.md → docs/openid-setup.md
TRANSCRIPTION_OPTIONS.md → docs/transcription-options.md
README.md updated with the new paths + a Documentation section that
links to docs/logic/ at the top.
New application-logic doc series (~8,300 lines total) at docs/logic/.
Built with 5 parallel doc-writing agents per Daniel's "use multiple
agents" directive. Each doc explains how a part of the app actually
works — application logic, data flow, design decisions, sacred zones,
how-to-extend recipes — at a depth that lets a new dev (or an AI
assistant) modify the code confidently.
docs/logic/README.md — index + recommended reading order
docs/logic/architecture.md (2166 L) — frontend IIFE pattern, lazy tab
load, backend route convention,
schema, encryption, deployment
docs/logic/clinical-notes.md (1546L) — every note tab + helper trio
docs/logic/bedside-and-calculators.md (1373L) — bedside ES module
pocket + calculators + PE Guide
+ suture selector
docs/logic/auth-admin-learning.md (1281L) — auth (local+OIDC+2FA) +
admin panel + Learning Hub
(Quiz engine logic at sub-detail
only — TODO follow-up)
docs/logic/ai-and-voice.md (1128 L) — callAI 5-provider routing,
prompts, voice/STT, helper trio
docs/logic/ed-encounters.md (821 L) — multi-stage ED + MDM (this
session's worked example)
Admin-only docs viewer:
- New route /api/admin/docs/{tree,file}: recursively walks docs/, returns
the tree as JSON; /file?path=X validates path stays inside docs/ and
renders markdown via marked. Both gated by req.user.role==='admin'.
- New tab "Docs" (book icon) in the sidebar, hidden by default and
revealed in auth.js when user.role==='admin' (same pattern as the
existing Admin and CMS tabs).
- New component public/components/admin-docs.html: split-pane layout
with a tree sidebar + filter input + a markdown reader pane.
- New module public/js/admin-docs.js: lazy-loads the tree on first tab
activation, renders collapsible folders, persists expanded state and
last-opened path via UIState. Server-rendered HTML so no client
markdown parser needed.
- CSS for the viewer (responsive split-pane, code-block styling, table
scrolling, etc.).
- Mounted at /api/admin/docs (NOT /api) — important: mounting a router
with router.use(authMiddleware) at /api accidentally 401s every other
/api/* path (caught and fixed during testing — /api/health was 401'ing).
Files: docs/* (moved + new), README.md, public/components/admin-docs.html
(new), public/js/admin-docs.js (new), src/routes/adminDocs.js (new),
public/index.html (tab + section + script), public/js/auth.js (admin
gate + logout cleanup), public/css/styles.css (viewer styles), server.js
(mount).
═══════════════════════════════════════════════════════════════════
KNOWN GAPS (TODO follow-ups)
═══════════════════════════════════════════════════════════════════
- Learning Hub quiz engine (MCQ / multi-select / T-F scoring + attempt
tracking + progress dashboard) is covered at the architectural level
in docs/logic/auth-admin-learning.md but not drilled into the quiz
data model and scoring flow. Worth a focused follow-up doc.
- ED finalize: if MDM step JSON parse fails, server returns 502 with
the consolidated finalNote in the error payload, but client doesn't
surface the partial result. Add a "MDM failed, retry" affordance.
- No e2e Playwright coverage for ED encounters or the new docs viewer.
821 lines
41 KiB
Markdown
821 lines
41 KiB
Markdown
# ED Encounters — Application Logic
|
|
|
|
> Multi-stage emergency department documentation with per-stage AI generation,
|
|
> "don't miss" tooltips per stage, and a final consolidate→MDM pipeline at
|
|
> Save & Done. Lives in its own tab between **Dictation HPI** and the
|
|
> **Notes** sidebar group.
|
|
|
|
This is the deepest, freshest doc in the `logic/` series — the feature was
|
|
built and revised across one focused session in late April 2026 and most of
|
|
the architectural decisions are explicitly motivated below. Read this first if
|
|
you want to understand how a clinical workflow gets composed in this codebase.
|
|
|
|
---
|
|
|
|
## 1. What this is
|
|
|
|
An ED encounter is structurally different from every other clinical note in
|
|
the app:
|
|
|
|
- A sick visit, well visit, SOAP note, or HPI is **one transcript → one
|
|
generated note**. The physician records, clicks Generate, edits, saves.
|
|
- An ED encounter is **multiple successive recordings → multiple successive
|
|
notes → one final consolidated note + MDM**. The physician dictates the
|
|
initial assessment, generates a note. Labs come back, they record more,
|
|
generate again. After consult, more dictation, generate again. When done
|
|
(could be after 1, 2, 3, or N stages), they click Save & Done and the
|
|
server consolidates everything into one polished note plus a 2023 AMA E/M
|
|
Medical Decision-Making block for billing.
|
|
|
|
The user-visible model: each generated stage stays on screen as its own
|
|
editable card with its own "Don't Miss" panel. The physician can edit any
|
|
stage at any time. Whatever's on screen at finalize time is what gets sent
|
|
to the consolidate step.
|
|
|
|
Physicians can also include direct asides in their dictation
|
|
("include normal cardiac exam", "assessment is viral URI") and the AI is
|
|
explicitly instructed to route those to the right note section instead of
|
|
quoting them.
|
|
|
|
---
|
|
|
|
## 2. The user flow, end to end
|
|
|
|
1. **Open the tab.** Sidebar → **ED Encounter**. Tab is lazy-loaded (the
|
|
`<section id="ed-tab" data-component="ed-encounter">` placeholder in
|
|
`public/index.html` triggers a fetch of `public/components/ed-encounter.html`
|
|
on first activation).
|
|
2. **Enter patient info.** Label (required for save), age, gender, chief
|
|
complaint (required for generation), and a model dropdown (the same
|
|
`class="tab-model-select"` pattern every clinical tab uses; auto-populated
|
|
by `app.js` against the admin's allowed-models list).
|
|
3. **Record or type Stage 1 dictation.** Standard recorder (`AudioRecorder` from
|
|
`public/js/audioBackup.js`) + browser SpeechRecognition for live transcript
|
|
preview + final server STT pass on stop. Same plumbing as every other
|
|
clinical tab.
|
|
4. **Click "Generate Stage 1 Note".** Frontend POSTs to
|
|
`/api/ed-encounters/generate` with `{stage: 1, transcript, chiefComplaint,
|
|
patientAge, patientGender, physicianMemories, model}`. Server returns
|
|
`{success, note, dontMiss[], model}`. Note appears as **Stage 1** card.
|
|
Don't-miss items appear as a yellow/orange section embedded in that same
|
|
card.
|
|
5. **Edit if needed.** Each stage's note element is `contenteditable`. Type
|
|
freely; edits persist to localStorage on each input event.
|
|
6. **Refine the latest stage** (optional). The "Refine latest" textarea +
|
|
button at the bottom of the stage list calls `/api/refine` with the
|
|
latest stage's text + your instruction. The latest stage's text is
|
|
replaced inline with the refined version.
|
|
7. **Add another stage** (optional). Click "Add more (next stage)". Stage 1
|
|
card stays visible. Transcript box clears. Badge changes to "Stage 2
|
|
(recording)" — yellow background — meaning we've advanced but no Stage 2
|
|
note has been generated yet.
|
|
8. **Repeat** for as many stages as you need.
|
|
9. **Click "Save & Done (with MDM)"** at any point.
|
|
- Server runs `edConsolidate` → produces one polished final note that
|
|
integrates every stage chronologically.
|
|
- Server then runs `edFinalize` → produces a 2023 E/M MDM block as JSON.
|
|
- Both come back in one HTTP response.
|
|
- Frontend renders a **"Final Consolidated Note"** card (blue left border)
|
|
and a **"Medical Decision Making (2023 E/M)"** card (green left border)
|
|
below the stage cards.
|
|
- Stage cards become read-only.
|
|
- The whole thing persists to `saved_encounters` with `status='final'`.
|
|
- Local draft cleared.
|
|
|
|
---
|
|
|
|
## 3. State model
|
|
|
|
Lives in a closure variable in `public/js/ed-encounters.js`:
|
|
|
|
```js
|
|
_state = {
|
|
stage: 1, // current stage number (what the recorder is for)
|
|
stages: [ // per-stage history — one entry per generated stage
|
|
{ transcript, note, dontMiss[], model, generatedAt }
|
|
],
|
|
finalized: false,
|
|
finalNote: null, // consolidated final note from /finalize
|
|
mdm: null // 2023 E/M MDM block from /finalize
|
|
}
|
|
```
|
|
|
|
### Invariants
|
|
|
|
- `_state.stage` increments monotonically. It only goes up via the user
|
|
clicking "Add more". It is the **target** stage of the recorder/generate
|
|
button.
|
|
- `_state.stages.length` is the number of stages **already generated**. The
|
|
array is indexed 0..N-1; stage 1 is at index 0, stage 2 at index 1, etc.
|
|
- The relationship `_state.stages.length === _state.stage` means "the
|
|
current stage has been generated, ready to finalize or advance."
|
|
- The relationship `_state.stages.length === _state.stage - 1` means
|
|
"we're recording into a new stage that hasn't been generated yet" (the
|
|
yellow `Stage N (recording)` badge state).
|
|
- `_state.finalized` flips to `true` only when finalize succeeds. Once true,
|
|
Add more / Generate / Refine all reject with a toast.
|
|
|
|
### Why this shape
|
|
|
|
Earlier iterations stored a single `currentNote` string and rotated stages
|
|
out of view on each generation. Daniel's clarification was explicit: every
|
|
stage's note must remain visible and editable; the final MDM should reflect
|
|
whatever's on screen, including any inline physician edits to earlier stages.
|
|
The `stages[]` array is the source of truth and `gatherCurrentNotes()`
|
|
re-syncs it from the DOM before any operation that needs current text.
|
|
|
|
---
|
|
|
|
## 4. The badge — accurate state communication
|
|
|
|
Top-right of the save bar. Exactly four states:
|
|
|
|
| Condition | Text | Background |
|
|
|---|---|---|
|
|
| `_state.stages.length >= _state.stage` (current stage has been generated) | `Stage N` | Gray |
|
|
| `_state.stages.length < _state.stage` (advanced past last generation; no note for stage N yet) | `Stage N (recording)` | Yellow |
|
|
| `_state.finalized && !_state.mdm` (transient) | (not reached — finalize is atomic) | — |
|
|
| `_state.finalized` | `Finalized` | Green |
|
|
|
|
This badge was the source of the most confusing UX bug in v1: clicking
|
|
"Add more" used to immediately flip the badge to "Stage 2" even though
|
|
no Stage 2 note existed yet. The fix isn't subtle — `updateBadge()` derives
|
|
the label from the relationship between `_state.stages.length` and
|
|
`_state.stage`, with explicit color coding so the difference is
|
|
unmistakable.
|
|
|
|
---
|
|
|
|
## 5. Frontend file map
|
|
|
|
### `public/components/ed-encounter.html`
|
|
|
|
The static markup. Roughly:
|
|
|
|
- **Save bar** (`#ed-save-bar`) — label input, badge (`#ed-stage-badge`),
|
|
Save draft / Load / New buttons, plus a Load popover for saved drafts.
|
|
- **Patient Info card** — age (`#ed-age`), gender (`#ed-gender`), chief
|
|
complaint (`#ed-cc`), and the model select (`#ed-model-select` with the
|
|
`tab-model-select` class).
|
|
- **Recording card** — header showing `Stage <span id="ed-rec-stage-num">N</span>
|
|
Recording / Dictation`, the Listen In / Pause buttons, the recording
|
|
indicator with timer, and a contenteditable transcript box (`#ed-transcript`).
|
|
- **Generate button** (`#btn-ed-generate`) — `Generate Stage <span id="ed-gen-stage-num">N</span> Note`.
|
|
- **`#ed-stages-container`** — empty div. JS appends one card per stage here.
|
|
- **`#ed-tail-controls`** — refine bar (textarea + Refine latest + Shorter)
|
|
+ stage-control row (Add more, Save & Done). Hidden until at least one
|
|
stage exists. Hidden again after finalize (encounter is locked).
|
|
- **`#ed-mdm-card`** — the green-bordered MDM card. Hidden until finalize.
|
|
The blue-bordered "Final Consolidated Note" card is **created
|
|
dynamically** by `renderFinalNote()` and inserted before the MDM card.
|
|
|
|
### `public/js/ed-encounters.js`
|
|
|
|
Single IIFE module. Key functions:
|
|
|
|
| Function | What it does |
|
|
|---|---|
|
|
| `freshState()` | Returns a clean `_state` object — used at module load and `resetEncounter()` |
|
|
| `gatherCurrentNotes()` | Walks the DOM stage cards (`#ed-stage-text-N` elements) and writes their current text back into `_state.stages[N].note`. Called before persist, advance, finalize, generate (the last because the AI prompt for stage N+1 needs the latest text of stage N as `previousNote`). |
|
|
| `persistLocal()` | Debounced 300ms localStorage save under key `ped_ed_draft_v1`. Snapshot includes `_state` plus the current label/age/gender/CC/transcript box content. |
|
|
| `loadLocal()` | Reverse of `persistLocal()` — restores state on tab open if a draft exists. |
|
|
| `renderStages()` | Rebuilds `#ed-stages-container` from `_state.stages[]`. Each card gets a unique id `ed-stage-text-N`. Cards become `contenteditable=false` after finalize. |
|
|
| `buildStageCard(idx, stage)` | Constructs one card's DOM. Includes the editable note + an embedded yellow "Don't Miss — Stage N" section if `stage.dontMiss` is non-empty. |
|
|
| `renderFinalNote(note)` | Lazily creates `#ed-final-note-card` (blue border) and inserts it before the MDM card. |
|
|
| `renderMdm(mdm)` | Fills `#ed-mdm-card` with structured MDM HTML (problems / data / risk paragraphs + suggested level + rationale + disclaimer). |
|
|
| `updateBadge()` | Sets `#ed-stage-badge` text + background color based on state. |
|
|
| `initRecording()` | Wires the record / pause buttons, browser SpeechRecognition, AudioRecorder, transcribe-on-stop. Identical pattern to `sickVisit.js` — copy-pasted because the recording paths are sacred and shouldn't be factored into a shared helper. |
|
|
| `generateStage()` | Validates inputs, calls `gatherCurrentNotes()`, fetches user templates via `getUserMemoryContext()`, POSTs `/api/ed-encounters/generate`, pushes the result into `_state.stages[stage-1]`, re-renders, autoSaves a draft to the DB. |
|
|
| `advanceStage()` | Validates current stage exists, gathers edits, increments `_state.stage`, clears the transcript box, updates the badge to "(recording)", scrolls to the recorder. **Does NOT touch any displayed cards.** |
|
|
| `finalize()` | Validates label + at least one stage, gathers edits, POSTs `/api/ed-encounters/finalize` with the full stages array, on success renders Final Note + MDM cards, marks finalized, calls `saveEncounter` with `status='final'`, clears localStorage. |
|
|
| `composeFinalNoteForSave(note, mdm)` | Concatenates the final note + MDM block into a single text blob written to `saved_encounters.generated_note` (so the saved encounter has a single coherent stored note for any downstream consumer). |
|
|
| `autoSaveDraft()` | Best-effort — saves a `status='draft'` row to `saved_encounters` if a label is set. Called after each stage generation. Silent if no label. |
|
|
| `resetEncounter()` | "New" button — wipes state, removes stage cards, hides Final Note + MDM, clears localStorage, drops the saved-encounter id. |
|
|
| `refineLatestStage()` / `shortenLatestStage()` | Resolve the latest stage's text element id (`stageTextElId(stages.length - 1)`) and call the global `refineDocument` / `shortenDocument` helpers from `app.js`. |
|
|
|
|
### How the file integrates with `encounters.js`
|
|
|
|
`public/js/encounters.js` is **sacred** (per the project memory file —
|
|
don't refactor without per-change approval, especially the save/idempotency
|
|
logic). ED encounters needed exactly two minimal touches to it:
|
|
|
|
1. Line 98: `'ed'` added to the sessionStorage restore array so the
|
|
`_savedEncId_ed` value survives page refresh.
|
|
2. Lines 305-310: `ed: 'ed'` added to the `tabMap` in `resumeEncounter` so
|
|
the saved-encounters list can navigate to the ED tab when a user
|
|
clicks a saved ED row.
|
|
|
|
That's it. Save logic, idempotency, optimistic versioning — all reused
|
|
unchanged via `window.saveEncounter()`. ED rows store with `enc_type='ed'`
|
|
and `partial_data` containing the full `{stages, finalNote, mdm, finalized}`
|
|
JSON for resume.
|
|
|
|
A `registerEncounterLoadHandler('ed', fn)` call near the bottom of
|
|
`ed-encounters.js` registers the resume handler with `encounters.js`. When
|
|
a user clicks an ED row in the Load popover, `encounters.js` invokes that
|
|
handler with the decrypted row, and the handler restores `_state` and
|
|
re-renders.
|
|
|
|
---
|
|
|
|
## 6. Backend file map
|
|
|
|
### `src/routes/edEncounters.js`
|
|
|
|
Two endpoints, both auth-gated.
|
|
|
|
#### `POST /api/ed-encounters/generate`
|
|
|
|
Per-stage note generation. Body:
|
|
|
|
| Field | Type | Notes |
|
|
|---|---|---|
|
|
| `stage` | number | Informational; the prompt is told this is "Stage N" |
|
|
| `transcript` | string | **Required.** This stage's raw dictation. |
|
|
| `chiefComplaint` | string | **Required.** Same as in other tabs. |
|
|
| `patientAge` | string | Optional but strongly preferred — drives "don't miss" tailoring |
|
|
| `patientGender` | string | Optional |
|
|
| `previousNote` | string | Stage 2+ only. The previous stage's current text (after edits). |
|
|
| `physicianMemories` | string | Concatenated user templates from `/api/memories/context` |
|
|
| `model` | string | Optional override. Validated by callAI's allowlist. |
|
|
|
|
Returns:
|
|
|
|
```json
|
|
{ "success": true, "note": "<plain-text note>", "dontMiss": [{"point","why"}], "model": "<id>" }
|
|
```
|
|
|
|
The route assembles a structured user message:
|
|
|
|
```
|
|
ED ENCOUNTER — STAGE N
|
|
Patient: <age>, <gender>
|
|
Chief Complaint: <wrapped>
|
|
|
|
CURRENT STAGE TRANSCRIPT (may include direct physician asides — preserve and route them per the prompt rules):
|
|
<wrapped transcript>
|
|
|
|
PREVIOUS-STAGE NOTE (baseline to integrate on top of — do not start fresh):
|
|
<wrapped previous note> [only stage 2+]
|
|
|
|
PHYSICIAN TEMPLATES AND PREFERENCES: [if any]
|
|
<wrapped templates>
|
|
```
|
|
|
|
Calls `callAI` with `PROMPTS.edEncounterStaged + INJECTION_GUARD` as system
|
|
and the assembled user message. Parses the response with `extractJson`.
|
|
|
|
**Recovery logic:** if the model returns plain prose instead of JSON
|
|
(model occasionally shortcuts), the route treats the entire response as
|
|
the note and returns an empty don't-miss list rather than 500-ing. The
|
|
physician still gets a usable note.
|
|
|
|
`dontMiss` is filtered to entries with non-empty `point` and trimmed.
|
|
|
|
Audit + apiCall logs are written.
|
|
|
|
#### `POST /api/ed-encounters/finalize`
|
|
|
|
Two-call server-side pipeline. Body:
|
|
|
|
| Field | Type | Notes |
|
|
|---|---|---|
|
|
| `stages` | `[{transcript, note}]` | **Required.** Array in chronological order. Empty stages are filtered. |
|
|
| `chiefComplaint` | string | Optional but strongly preferred |
|
|
| `patientAge` | string | Optional |
|
|
| `patientGender` | string | Optional |
|
|
| `model` | string | Optional override |
|
|
|
|
The route:
|
|
|
|
1. **Step 1 — consolidate.** Builds a context with chief complaint, demographics,
|
|
and a labeled `=== STAGE N ===` block for each stage (transcript +
|
|
working note). System prompt is `PROMPTS.edConsolidate`. Returns the
|
|
model's plain-text response as `finalNote`.
|
|
|
|
2. **Step 2 — MDM.** Builds a context with the consolidated `finalNote` plus
|
|
the full transcript across all stages. System prompt is
|
|
`PROMPTS.edFinalize`. Parses JSON for `{mdm: {...}}`.
|
|
|
|
Returns:
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"finalNote": "<plain-text consolidated note>",
|
|
"mdm": {
|
|
"problemsAddressed": "minimal|low|moderate|high",
|
|
"problemsNarrative": "...",
|
|
"dataReviewed": "minimal|limited|moderate|extensive",
|
|
"dataNarrative": "...",
|
|
"risk": "minimal|low|moderate|high",
|
|
"riskNarrative": "...",
|
|
"suggestedLevel": "99281|99282|99283|99284|99285",
|
|
"levelRationale": "..."
|
|
},
|
|
"model": "<id>"
|
|
}
|
|
```
|
|
|
|
**Why two calls instead of one combined prompt:** each task has a
|
|
focused rubric (the consolidate prompt enforces section structure and
|
|
chronological integration; the MDM prompt enforces the 2023 AMA element
|
|
definitions). Asking for both in one JSON tends to make the model
|
|
shortcut one or the other. Two calls cost ~2x latency at the very end of
|
|
the encounter — acceptable since finalize is a one-time terminal action.
|
|
|
|
If the MDM step's JSON parse fails, the route returns 502 but **still
|
|
includes the finalNote** in the error payload so the client doesn't lose
|
|
work. (The client doesn't currently surface this case to the user — TODO
|
|
to render the partial result with a "MDM failed, retry" affordance.)
|
|
|
|
Token usage from both calls is summed for the apiCall log.
|
|
|
|
### `src/utils/prompts.js` — the three ED prompts
|
|
|
|
#### `edEncounterStaged`
|
|
|
|
System prompt for per-stage generation. Returns strict JSON `{note, dontMiss[]}`.
|
|
|
|
Key instructions:
|
|
|
|
- Note structure is **fixed**: Chief Complaint, HPI (OLDCARTS, historian
|
|
noted), ROS (per ROS_PE_RULES), PE (per ROS_PE_RULES), ED Course (only
|
|
when present), Assessment and Plan.
|
|
- Don't-miss list is **uncapped** for ED (unlike the global `dontMissTooltip`
|
|
prompt which hard-caps at 5 for sick visit / encounter HPI). Quality
|
|
over quantity. Tailored strictly to age + chief complaint.
|
|
- **PRESERVE INSTRUCTIONS WITHIN DICTATION** — explicit instruction that
|
|
physician asides like "include normal cardiac exam" or "assessment is
|
|
viral URI" are first-class clinical input. Route exam findings to PE,
|
|
assessment statements to A&P, plan statements to A&P. Never echo as
|
|
quoted speech.
|
|
- **Templates** — the user's templates (especially `template_ed`, but also
|
|
matching `template_hpi`/`template_soap`/`template_sickvisit`) are
|
|
delivered in the user message as PHYSICIAN TEMPLATES AND PREFERENCES.
|
|
Apply matching template sections; never copy clinical content from a
|
|
template — only formatting/structure.
|
|
- **Previous-stage note** — explicit instruction: integrate the new
|
|
transcript on top of the previous note, do not start fresh. Drop
|
|
don't-miss items that have been addressed.
|
|
|
|
The prompt is appended with `INJECTION_GUARD` from `promptSafe.js` to
|
|
defend against prompt-injection attempts inside the dictation.
|
|
|
|
#### `edConsolidate`
|
|
|
|
System prompt for the consolidate step at finalize. **Plain text output**
|
|
(no JSON wrapper).
|
|
|
|
Key instructions:
|
|
|
|
- Same fixed note structure as the staged prompt.
|
|
- **Integration rules**: use the latest stage as the structural baseline
|
|
(it already integrates earlier stages); use earlier stages and
|
|
transcripts to fill gaps. ED Course should reflect chronological
|
|
progression. Resolve contradictions by using the later value AND
|
|
noting the change in ED Course (e.g., "now afebrile after antipyretic").
|
|
- Preserve every clinical fact; never drop information; never invent.
|
|
|
|
#### `edFinalize`
|
|
|
|
System prompt for the MDM step. Returns strict JSON `{mdm: {...}}`.
|
|
|
|
Includes a **full inline rubric** of the 2023 AMA E/M MDM table so the
|
|
model has clear definitions to score against:
|
|
|
|
- **Element 1 — Problems addressed**: minimal / low / moderate / high
|
|
with explicit definitions (e.g., "high = chronic illness with severe
|
|
exacerbation, OR acute illness/injury that poses threat to life or
|
|
bodily function").
|
|
- **Element 2 — Data reviewed**: categories (tests reviewed, tests
|
|
ordered, independent interpretation, discussion with another physician,
|
|
external records, independent historian) and counting rules for
|
|
minimal / limited / moderate / extensive.
|
|
- **Element 3 — Risk**: minimal / low / moderate / high with concrete
|
|
examples per level (drug therapy requiring intensive monitoring,
|
|
decision regarding hospitalization, etc.).
|
|
- **Level mapping** (2 of 3 elements must meet the level): 99281 through
|
|
99285 with descriptions of the typical encounter at each level
|
|
(99284 = "MODERATE complexity MDM, most common ED visit with workup,
|
|
labs, or imaging and prescription decisions"; 99285 = "HIGH complexity
|
|
MDM, admission for high-acuity care").
|
|
|
|
Critical rules at the bottom:
|
|
- Use only information present in the note (and transcript if provided).
|
|
- Never invent.
|
|
- Conservative when ambiguous — pick the lower level.
|
|
- `levelRationale` must reference specific elements actually documented.
|
|
|
|
This prompt is the most important to get right — it's what determines the
|
|
suggested billing level. Daniel called this out explicitly: "make sure mdm
|
|
is configured well." The full element rubric is inline so the model isn't
|
|
relying on its training to remember the 2023 guidelines correctly.
|
|
|
|
---
|
|
|
|
## 7. Persistence — three layers
|
|
|
|
### Layer 1 — localStorage (`ped_ed_draft_v1`)
|
|
|
|
Debounced 300ms after every input event. Snapshot includes the entire
|
|
`_state` plus the current label / age / gender / CC / transcript box
|
|
content. Survives page refresh, browser restart, signing out + back in.
|
|
|
|
Cleared on `resetEncounter()` and on successful finalize.
|
|
|
|
This is the **fast** layer — captures every keystroke without round-trips.
|
|
|
|
### Layer 2 — saved_encounters DB row, status='draft'
|
|
|
|
Best-effort auto-save after each stage generation. Requires a label to be
|
|
set; silent no-op otherwise. Uses `window.saveEncounter` from
|
|
`encounters.js` with:
|
|
|
|
- `enc_type: 'ed'`
|
|
- `status: 'draft'`
|
|
- `generated_note`: the latest stage's note (so the saved-encounters list
|
|
has something to preview)
|
|
- `partial_data`: encrypted JSON containing `{stages, finalized: false}`
|
|
- `idempotency_key: 'ed-draft-' + savedId`
|
|
|
|
The same row gets updated on each subsequent generation (by passing
|
|
the saved id back to `saveEncounter`).
|
|
|
|
This is the **durable** layer — survives device loss because it's on the
|
|
server, encrypted at rest with the app key.
|
|
|
|
### Layer 3 — saved_encounters DB row, status='final'
|
|
|
|
Written exactly once on successful finalize. Includes:
|
|
|
|
- `generated_note`: the **final consolidated note + MDM block** combined
|
|
via `composeFinalNoteForSave()`, so any downstream consumer (export,
|
|
copy, print) gets a single coherent text.
|
|
- `partial_data`: `{stages, finalNote, mdm, finalized: true}` — full
|
|
fidelity for resume / audit / future re-render.
|
|
- `idempotency_key: 'ed-final-' + Date.now()` — unique per finalize
|
|
attempt so a network retry doesn't create a duplicate row.
|
|
|
|
After finalize, localStorage is cleared. The encounter is locked from
|
|
further edits in-app (stage cards become `contenteditable=false`, tail
|
|
controls hidden). The user can still load the row later for review;
|
|
editing requires loading then unlocking by some manual workflow that
|
|
doesn't yet exist (TODO if requested).
|
|
|
|
---
|
|
|
|
## 8. The "Don't Miss" tooltip — per-stage
|
|
|
|
Each stage's response from `/api/ed-encounters/generate` includes a
|
|
`dontMiss[]` array of `{point, why}` objects. Same JSON call as the
|
|
note — no second AI request. The stage card embeds these as a
|
|
yellow/orange section beneath the note text:
|
|
|
|
```
|
|
┌─────────────────────────────────────────┐
|
|
│ Stage 2 Note [model] [Copy] │
|
|
├─────────────────────────────────────────┤
|
|
│ Chief Complaint: ... │
|
|
│ HPI: ... │
|
|
│ ... │
|
|
├─────────────────────────────────────────┤ ← yellow background
|
|
│ ⚠ Don't Miss — Stage 2 │
|
|
│ • Document hydration status │
|
|
│ (tachycardia + 4-day vomiting) │
|
|
│ • Consider DKA workup │
|
|
│ (polyuria + weight loss in HPI) │
|
|
│ • ... │
|
|
└─────────────────────────────────────────┘
|
|
```
|
|
|
|
ED don't-miss is **uncapped** by design — Daniel wanted no limit ("just like
|
|
remember to ask this, do this, keep this in mind etc."). Compare with
|
|
`/api/dont-miss` (used by sick visit + encounter HPI) which caps at 5
|
|
both in the prompt and via server-side `.slice(0, 5)`.
|
|
|
|
The same-call design (note + don't-miss in one JSON) was a deliberate
|
|
choice to keep per-stage latency down. The risk (model occasionally
|
|
shortcuts the don't-miss list) is acceptable per Daniel since don't-miss
|
|
is informational, not load-bearing.
|
|
|
|
---
|
|
|
|
## 9. Templates — `template_ed`
|
|
|
|
When a physician saves a template under category `template_ed`
|
|
(Settings → Templates), it gets included in the `physicianMemories` string
|
|
that the frontend fetches via `getUserMemoryContext()` and passes to
|
|
`/api/ed-encounters/generate`.
|
|
|
|
The flow:
|
|
|
|
1. User saves a template named e.g. "ED Pearls" with category `template_ed`.
|
|
2. Server stores it in `user_memories` (encrypted name + content).
|
|
3. On next ED note generation, `ed-encounters.js` calls
|
|
`getUserMemoryContext()` (defined in `public/js/memories.js`).
|
|
4. That function fetches `/api/memories/context` which returns a single
|
|
string containing every active template (correction_* rows are
|
|
filtered out at the SQL level — the AI corrections feature was
|
|
removed in late April 2026).
|
|
5. The string is included in the user message under the
|
|
"PHYSICIAN TEMPLATES AND PREFERENCES" header.
|
|
6. The system prompt's "PHYSICIAN TEMPLATES" section instructs the model
|
|
to apply matching sections from any template (especially `template_ed`,
|
|
but also matching HPI/SOAP/sickvisit templates).
|
|
|
|
`template_ed` was added to `VALID_CATEGORIES` in `src/routes/memories.js`
|
|
and to the dropdown in `public/components/settings.html`.
|
|
|
|
---
|
|
|
|
## 10. Why the design looks like this
|
|
|
|
Each major decision, with the constraint that motivated it.
|
|
|
|
### Why per-stage cards (vs. one rolling note element)
|
|
|
|
**Daniel's clarification.** The first build used one `#ed-note-text`
|
|
element that got replaced on each generation. After demo: "every stage
|
|
note should be shown, if AI is told to modify that particular note then
|
|
the modified version is used in final mdm." The cards model is the
|
|
direct response — every stage stays visible, every stage is editable,
|
|
edits flow into finalize.
|
|
|
|
### Why the badge has an explicit "(recording)" state
|
|
|
|
**Bug from first user test.** Clicking "Add more" used to flip the badge
|
|
to "Stage 2" immediately, before any Stage 2 note existed. Daniel: "the
|
|
title changes to stage 2 even without a new recording and generate being
|
|
hit." The fix isn't subtle — `updateBadge()` derives state from the
|
|
relationship between `stages.length` and `_state.stage`, with explicit
|
|
color coding so the difference is unmistakable.
|
|
|
|
### Why finalize is two server-side AI calls instead of one
|
|
|
|
**Quality concern.** A single combined "produce finalNote AND mdm in one
|
|
JSON" prompt makes the model cut corners on one of the two tasks
|
|
(usually the MDM rubric gets compressed). Two focused calls each get
|
|
their own dedicated system prompt with no competing pressure. Cost: ~2x
|
|
latency at finalize. Justification: finalize is a one-time terminal
|
|
action, not a per-stage hot path.
|
|
|
|
### Why edConsolidate returns plain text instead of JSON
|
|
|
|
**Reliability + simplicity.** The consolidate step produces ONE thing —
|
|
a clinical note. JSON wrapping adds parse-failure surface area for zero
|
|
benefit. The text is rendered directly into a `contenteditable` element.
|
|
The MDM step does need JSON because it has structured fields the UI
|
|
displays in a table layout.
|
|
|
|
### Why the MDM prompt has the full 2023 AMA rubric inline
|
|
|
|
**Daniel's directive: "make sure mdm is configured well."** Models'
|
|
training data includes pre-2023 guidelines mixed with 2023; relying on
|
|
"you know the AMA E/M MDM table" produces drift. The prompt now
|
|
includes element-by-element definitions (problems / data / risk),
|
|
counting rules for data, and concrete examples per level. The level
|
|
rationale must reference specific findings.
|
|
|
|
### Why the recorder code is copy-pasted from sickVisit.js
|
|
|
|
**Project memory: "Voice/STT is sacred — Don't refactor recorder/transcribe
|
|
plumbing; fix only named bugs in smallest diff."** The recording paths
|
|
in every clinical tab look almost identical; refactoring to a shared
|
|
helper is a textbook clean-code move that has burned this project before
|
|
(silent breakage of recording when the abstraction ate an edge case).
|
|
The deliberate non-DRY duplication is the safer choice.
|
|
|
|
### Why finalize sends the whole stages array instead of just stage texts
|
|
|
|
The consolidate prompt benefits from seeing each stage's transcript
|
|
(physician's actual dictation) AND each stage's note (which may include
|
|
physician edits). The transcripts let the AI catch facts that didn't
|
|
make it into the working notes; the notes show physician interpretation.
|
|
Both together produce a more faithful consolidation.
|
|
|
|
### Why `previousNote` is sent during stage 2+ generation, not just the transcript
|
|
|
|
The per-stage AI is told to "integrate the new transcript on top of the
|
|
previous note as baseline, do not start fresh." Without the previous
|
|
note as input, stage 2 would have to regenerate everything from
|
|
transcripts alone (slower, less faithful to physician edits made between
|
|
stages).
|
|
|
|
---
|
|
|
|
## 11. Sacred / fragile zones
|
|
|
|
These are not refactor-without-permission lines.
|
|
|
|
### `public/js/encounters.js`
|
|
|
|
Per project memory: don't touch without per-change approval; even
|
|
pre-approved changes get rejected if they refactor save/idempotency. The
|
|
ED feature touched it in exactly two places (sessionStorage array and
|
|
tabMap) and that's it. **All ED save/load goes through `window.saveEncounter`
|
|
and `registerEncounterLoadHandler` — established interfaces. Do not
|
|
add new methods to encounters.js or modify the save body shape.**
|
|
|
|
### Recorder + transcribe paths
|
|
|
|
`public/js/audioBackup.js` (the AudioRecorder class), the
|
|
`transcribeAudio` global function, the SpeechRecognition wrapper from
|
|
`public/js/speechRecognition.js`. The ED tab's recording logic in
|
|
`initRecording()` was copied from `sickVisit.js` deliberately. Don't
|
|
factor it out into a shared `record-and-transcribe-helper.js`.
|
|
|
|
### The MDM prompt rubric
|
|
|
|
The 2023 AMA E/M element definitions and level mapping in
|
|
`PROMPTS.edFinalize` are load-bearing for billing accuracy. Don't trim
|
|
them to "save tokens" — the cost of a miscoded encounter to a real
|
|
practice is much higher than the prompt overhead. Update only with
|
|
explicit billing/coding source citation.
|
|
|
|
---
|
|
|
|
## 12. How to extend — concrete recipes
|
|
|
|
### Add a new prompt key
|
|
|
|
1. Add the entry to `PROMPTS` in `src/utils/prompts.js`. Use the same
|
|
`${CORE_RULES}` and (if relevant) `${ROS_PE_RULES}` preambles other
|
|
prompts use.
|
|
2. The DB-override system (`loadFromDb` in the same file) auto-picks up
|
|
the new key on next startup, so admins can override it from the
|
|
Admin → Prompts UI without code changes.
|
|
|
|
### Tweak the MDM rubric
|
|
|
|
Edit `PROMPTS.edFinalize` in `src/utils/prompts.js`. Cite the source
|
|
(2023 AMA E/M Office or Other Outpatient guideline updates, or AMA
|
|
errata) in the commit message. The rubric structure is stable —
|
|
changes are usually wording refinements, not category rewrites.
|
|
|
|
### Add a new field to the stage card (e.g., timestamp)
|
|
|
|
1. The data is already in `_state.stages[i].generatedAt`.
|
|
2. In `buildStageCard(idx, stage)` in `public/js/ed-encounters.js`,
|
|
add a small `<div>` next to the model tag in the card header.
|
|
3. No backend change needed; `generatedAt` is already saved in
|
|
`partial_data`.
|
|
|
|
### Add per-stage refine (instead of "refine latest")
|
|
|
|
1. In `buildStageCard()`, render a refine input + button for every
|
|
stage card.
|
|
2. Update the refine button click handler to read `data-stage-idx` from
|
|
the clicked button and pass `stageTextElId(idx)` to `refineDocument`.
|
|
3. Be aware: physicians editing earlier stages then refining them then
|
|
regenerating later stages creates a complex causality chain. Daniel's
|
|
current call is "refine targets the latest stage" to avoid this.
|
|
|
|
### Add a new ED-specific output (e.g., a discharge instructions card)
|
|
|
|
1. Decide if it's per-stage or once-per-encounter. Per-encounter is
|
|
simpler — generate it on finalize.
|
|
2. Add a third server-side AI call in `/api/ed-encounters/finalize`
|
|
between consolidate and MDM. Add the result to the response payload.
|
|
3. Add a new card to `ed-encounter.html` (or create dynamically like
|
|
`renderFinalNote`).
|
|
4. Render it in the finalize success handler.
|
|
|
|
### Unlock a finalized encounter for editing (TODO — not implemented)
|
|
|
|
Currently no UI for this. Would require:
|
|
|
|
1. A new endpoint `POST /api/ed-encounters/:id/unlock` that flips
|
|
`status` from `'final'` back to `'draft'` and clears `partial_data.finalized`.
|
|
2. An "Unlock for editing" button on the load popover for finalized
|
|
rows.
|
|
3. UI logic in `ed-encounters.js` to handle the unlocked state
|
|
(re-enable editing on stage cards, re-show tail controls).
|
|
|
|
### Add a fourth stage type (currently the prompt is generic)
|
|
|
|
The current design treats all stages identically — same prompt, same
|
|
structure. If there's a value in distinguishing "initial assessment"
|
|
vs "post-workup" vs "post-consult" stages with different prompts, that's
|
|
a meaningful shift. Probable plan:
|
|
|
|
1. Add a `stageType` field to each stage entry.
|
|
2. Branch on `stageType` in the route to pick a prompt variant.
|
|
3. UI: dropdown next to the recorder that defaults to "Initial /
|
|
Workup / Consult / Disposition" based on stage number.
|
|
|
|
This isn't currently planned — the generic stage works because the
|
|
physician's dictation is what differentiates stages, not a metadata tag.
|
|
|
|
---
|
|
|
|
## 13. Testing pointers
|
|
|
|
There are currently **no Playwright e2e tests** for ED encounters — flagged
|
|
as TODO in the session that built the feature. A reasonable first batch:
|
|
|
|
1. **Stage 1 happy path.** Open tab, fill label/age/gender/CC, type
|
|
transcript, click Generate, assert Stage 1 card appears with note
|
|
text and don't-miss section.
|
|
2. **Multi-stage flow.** Stage 1 → Add more → badge says "Stage 2
|
|
(recording)" with yellow background → type Stage 2 transcript →
|
|
Generate → both Stage 1 and Stage 2 cards visible.
|
|
3. **Edit-then-finalize.** Generate Stage 1 → edit the note text inline
|
|
→ Save & Done → assert the consolidate step received the edited text
|
|
(mock `/api/ed-encounters/finalize`, inspect the request body).
|
|
4. **Finalize renders both cards.** Mock `/finalize` to return
|
|
`{finalNote, mdm}` → assert blue Final Note card AND green MDM card
|
|
appear → assert stage cards become read-only.
|
|
5. **Resume from saved.** Save a draft, reload, click Load, pick the
|
|
ED row → all stages reappear with their don't-miss sections.
|
|
|
|
The mocking pattern is in `e2e/fixtures.js` — `mockAI(page, overrides)`.
|
|
Add `'**/api/ed-encounters/generate'` and `'**/api/ed-encounters/finalize'`
|
|
to the routes table with canned responses.
|
|
|
|
---
|
|
|
|
## 14. Known issues / TODOs
|
|
|
|
- No e2e coverage (above).
|
|
- No "unlock" UI for finalized encounters.
|
|
- The MDM-step partial-success case (consolidate succeeded, MDM failed)
|
|
returns 502 with `finalNote` in the error payload, but the client
|
|
doesn't render the partial result. Currently the user sees a generic
|
|
error toast and loses the consolidate work.
|
|
- Per-stage refine isn't supported — only "refine latest." If the user
|
|
wants to refine an earlier stage, they edit it inline (works) but
|
|
don't get an AI-assisted refine for that specific stage.
|
|
- The "(recording)" badge color (yellow) might be confusing in the
|
|
dark theme if one is added — currently the app is light-only.
|
|
- The consolidate step uses the configured default model unless the
|
|
user picks a specific one in the dropdown. There's no way to use one
|
|
model for per-stage generation and a different model for finalize.
|
|
Probably fine; flag if a user wants this.
|
|
- `extractJson` is defined locally in `src/routes/edEncounters.js` and
|
|
duplicated from `notes.js`. Candidate for `src/utils/jsonRecover.js`
|
|
if a third route ever needs it.
|
|
|
|
---
|
|
|
|
## 15. Quick reference — the ED encounter at a glance
|
|
|
|
```
|
|
┌───────────────────────────────────────────────────────────────────┐
|
|
│ ED ENCOUNTER TAB │
|
|
│ [Patient label] [Stage N badge] │
|
|
│ Age | Gender | Chief Complaint | Model dropdown │
|
|
├───────────────────────────────────────────────────────────────────┤
|
|
│ Stage N Recording — [Listen In] [Pause] [00:23 indicator] │
|
|
│ [contenteditable transcript box] │
|
|
├───────────────────────────────────────────────────────────────────┤
|
|
│ [✨ Generate Stage N Note] │
|
|
├───────────────────────────────────────────────────────────────────┤
|
|
│ ┌─────────────────────────────────────────────────┐ │
|
|
│ │ Stage 1 Note [model] [Copy] │ │
|
|
│ │ [editable note text] │ │
|
|
│ │ ──────────────────────────────────────────────── │ │
|
|
│ │ ⚠ Don't Miss — Stage 1 │ │
|
|
│ │ • point 1 │ │
|
|
│ │ • point 2 │ │
|
|
│ └─────────────────────────────────────────────────┘ │
|
|
│ ┌─────────────────────────────────────────────────┐ │
|
|
│ │ Stage 2 Note [model] [Copy] │ │
|
|
│ │ ... │ │
|
|
│ └─────────────────────────────────────────────────┘ │
|
|
├───────────────────────────────────────────────────────────────────┤
|
|
│ [Refine input] [Refine latest] [Shorter] │
|
|
│ [+ Add more (next stage)] [✓ Save & Done (with MDM)] │
|
|
├───────────────────────────────────────────────────────────────────┤
|
|
│ ┌─────────────────────────────────────────────────┐ (after │
|
|
│ │ 📋 Final Consolidated Note [Copy] │ finalize) │
|
|
│ │ [polished single note from edConsolidate] │ │
|
|
│ └─────────────────────────────────────────────────┘ │
|
|
│ ┌─────────────────────────────────────────────────┐ │
|
|
│ │ 💵 Medical Decision Making (2023 E/M) [99284] │ │
|
|
│ │ Problems Addressed (moderate): ... │ │
|
|
│ │ Data Reviewed (moderate): ... │ │
|
|
│ │ Risk (moderate): ... │ │
|
|
│ │ Suggested Level: 99284 — rationale... │ │
|
|
│ └─────────────────────────────────────────────────┘ │
|
|
└───────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
**Files involved** (so a reader can map this to the codebase):
|
|
|
|
| Path | Role |
|
|
|---|---|
|
|
| `public/components/ed-encounter.html` | Tab markup |
|
|
| `public/js/ed-encounters.js` | All client logic (~500 lines) |
|
|
| `public/js/encounters.js` | Sacred — saveEncounter, sessionStorage, tabMap (2-string touch only) |
|
|
| `src/routes/edEncounters.js` | `/generate` + `/finalize` endpoints |
|
|
| `src/utils/prompts.js` | `edEncounterStaged`, `edConsolidate`, `edFinalize` keys |
|
|
| `src/utils/promptSafe.js` | `wrapUserText` + `INJECTION_GUARD` |
|
|
| `src/routes/encounters.js` | Generic save infrastructure (saved_encounters table) |
|
|
| `src/routes/memories.js` | `template_ed` category in `VALID_CATEGORIES` |
|
|
| `public/js/memories.js` | `template_ed: 'ED Template'` label + `getUserMemoryContext` |
|
|
| `public/components/settings.html` | `<option value="template_ed">` in the category dropdown |
|
|
| `public/index.html` | Tab button, lazy-load section, script tag |
|
|
| `server.js` | Mounts `edEncounters` route on `/api` |
|