# Clinical notes — application logic This doc covers every "note-producing" tab in the ped-ai app: how each one collects input, what AI prompt it uses, which endpoint it hits, what DOM elements it touches, and how the shared save/resume infrastructure ties them together. It is written for a developer who has never opened the codebase. File paths are absolute. Where a fact depends on a specific line, the line number is named so you can jump straight to it. The covered tabs are: - **Encounter HPI** — live mic recording → HPI - **Dictation HPI** — physician dictation → HPI - **Sick Visit** — chief complaint + auto-inferred ROS/PE + dictation → sick visit note - **Well Visit** — multi-pane Bright Futures workflow (by-visit, milestones, SSHADESS, visit note) - **SOAP** — transcript or dictation → SOAP note (full or subjective only) - **Hospital Course** — pasted progress notes / H&P / ED note → admission summary - **Chart Review** — pasted outpatient / subspecialty / ED notes → AI summary - **Personal Notes** — free-form rich-text editor with optional voice → AI cleanup ED Encounter is referenced in passing because it shares the recorder and the `enc_type='ed'` save path, but it has its own separate logic doc and is not the focus here. --- ## 1. Overview Each clinical-notes tab is independent: its own DOM IDs, its own JS module (an IIFE), its own `/api/...` endpoint, and its own AI prompt. But every tab sits on top of the same scaffolding: 1. A small header bar with patient demographics (age, gender, sometimes setting / chief complaint / PMH). 2. A **save bar** at the top with `Save / Load / New` buttons and a patient-label input. 3. A **recorder** (most tabs) that writes into a `contenteditable` transcript box. 4. A **Generate** button that POSTs the assembled context to a route in `src/routes/`. 5. An **output card** that hides until generation succeeds, then reveals the AI's note plus a `Refine` and `Shorten` bar. 6. After generation, a few of the tabs auto-call **billing-codes** and **don't-miss** helpers that mount additional cards beneath the note. Why a tab per note type? Each note type has different inputs (e.g. Sick Visit needs CC + ROS + PE, Hospital Course needs many dated notes + H&P + labs, Personal Notes needs neither demographics nor a transcript), and a different AI system prompt. A "one-size note generator" would have an unwieldy form and a prompt full of conditionals. Splitting them gives each tab a focused UI and a clean prompt. The shared lifecycle — recorder → transcribe → generate → save — is the same skeleton in every recording tab; only the per-tab payload, prompt, and post-generation UI differ. --- ## 2. The shared lifecycle ### 2.1 Lazy tab loading `public/js/app.js` (lines 26–104) implements the SPA tab system. The sidebar buttons are ` ``` in the sidebar (around line 197) and: ```
``` in the tab-content area (around line 301). The `data-component` value is the filename (without `.html`) under `public/components/`. 3. **JS module** — create `public/js/.js`. Use this skeleton: ``` (function() { var _inited = false; document.addEventListener('tabChanged', function(e) { if (e.detail.tab !== '' || _inited) return; _inited = true; init(); }); function init() { // Wire buttons. Read inputs. Call generate on click. } function generate() { var memoriesPromise = (typeof getUserMemoryContext === 'function') ? getUserMemoryContext() : Promise.resolve(''); memoriesPromise.then(function(memCtx) { return fetch('/api/', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ ..., physicianMemories: memCtx || null, model: getSelectedModel() }) }); }).then(...).then(function(data) { setOutputText(noteEl, data.note); storeSourceContext('-text', sourceMaterial); outputCard.classList.remove('hidden'); // Optionally: if (typeof suggestBillingCodes === 'function') suggestBillingCodes(...); if (typeof suggestDontMiss === 'function') suggestDontMiss(...); }); } // Register a load handler so resume works if (typeof registerEncounterLoadHandler === 'function') { registerEncounterLoadHandler('', function(enc) { // Restore transcript + generated_note + any per-tab state }); } })(); ``` Add a `` to `public/index.html` (the script load order is fixed; check the existing block). 4. **Server route + prompt** — create `src/routes/.js`: ``` var express = require('express'); var router = express.Router(); var { callAI } = require('../utils/ai'); var PROMPTS = require('../utils/prompts'); var { authMiddleware } = require('../middleware/auth'); var logger = require('../utils/logger'); var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe'); router.post('/', authMiddleware, async function(req, res) { try { var { ..., physicianMemories, model } = req.body; // Validate inputs, build context with wrapUserText() per field. // Append style hints if physicianMemories. var result = await callAI([ { role: 'system', content: PROMPTS. + INJECTION_GUARD }, { role: 'user', content: context } ], { model }); res.json({ success: true, note: result.content, model: result.model }); logger.audit(req.user.id, 'generate_', '...', req, { category: 'clinical' }); } catch (e) { logger.error('[] failed', e.message); res.status(500).json({ error: 'Request failed' }); } }); module.exports = router; ``` Add a new prompt key in `src/utils/prompts.js` near related ones, following the style — start with `${CORE_RULES}` and (if relevant) `${ROS_PE_RULES}`, list required sections, end with constraints. 5. **Mount in `server.js`** — add a line in the existing block (around line 280): ``` app.use('/api', require('./src/routes/')); ``` 6. **Register in `encounters.js` for save/resume** (NB: this is the minimum sacred-file change; treat as "additive, narrow"): - Add the new `enc_type` to the sessionStorage restore list (`encounters.js:98`): ``` ['encounter','dictation','ed','hospital','chart','wellvisit', 'sickvisit','soap',''].forEach(...); ``` - Add to `tabMap`, `domPfxMap`, `noteIdMap` in `resumeEncounter()` (lines 305, 317, 322). - Add a click case in the document-level click handler (around line 398) for `#btn--save`, `#btn--load`, `#btn--new`. - Add to `pfxMap`, `noteElMap`, `outputElMap` in `clearTab()` (lines 460–473) so the New Patient button clears your tab. - If the type appears in load popovers, add the prefix to the popover-list refresher arrays at lines 166 and 256 and 299 and 424. 7. **Test the round-trip**: tab loads, recording works (if used), generate hits the route, save persists, page-refresh + Save doesn't duplicate, Load → Resume restores the state, New clears everything, idempotency_key resets on New. --- ## Appendix — file map (quick reference) Frontend: | Tab | JS module | HTML component | Save type | |---|---|---|---| | Encounter HPI | `public/js/liveEncounter.js` | `public/components/encounter.html` | `encounter` | | Dictation HPI | `public/js/voiceDictation.js` | `public/components/dictation.html` | `dictation` | | Sick Visit | `public/js/sickVisit.js` (+ `shadess.js` for shared ROS/PE/Dx) | `public/components/sickvisit.html` | `sickvisit` | | Well Visit | `public/js/wellVisit.js` + `public/js/shadess.js` | `public/components/wellvisit.html` | `wellvisit` | | SOAP | `public/js/soap.js` | `public/components/soap.html` | `soap` | | Hospital Course | `public/js/hospitalCourse.js` | `public/components/hospital.html` | `hospital` | | Chart Review | `public/js/chartReview.js` | `public/components/chart.html` | `chart` | | Personal Notes | `public/js/notes.js` | `public/components/notes.html` | n/a (own table) | Shared frontend: - `public/js/app.js` — tab loader, `AudioRecorder`, `transcribeAudio`, `setOutputText`, `storeSourceContext`, `suggestBillingCodes`, `suggestDontMiss`, `refineDocument`, `shortenDocument`, `createSpeechRecognition`, `deduplicateFinal`. - `public/js/encounters.js` — save/load/resume infrastructure (sacred). - `public/js/audioBackup.js` — IndexedDB + server audio backup (sacred-adjacent — touch only narrowly). - `public/js/memories.js` — `getUserMemoryContext()`. Backend: | Endpoint | File | |---|---| | `POST /api/generate-hpi-encounter` | `src/routes/hpi.js:10` | | `POST /api/generate-hpi-dictation` | `src/routes/hpi.js:35` | | `POST /api/sick-visit/note` | `src/routes/sickVisit.js:14` | | `POST /api/well-visit/shadess` | `src/routes/wellVisit.js:63` | | `POST /api/well-visit/note` | `src/routes/wellVisit.js:135` | | `POST /api/generate-soap` | `src/routes/soap.js:9` | | `POST /api/generate-hospital-course` | `src/routes/hospitalCourse.js:10` | | `POST /api/hospital-course-clarify` | `src/routes/hospitalCourse.js:100` | | `POST /api/hospital-course-update` | `src/routes/hospitalCourse.js:116` | | `POST /api/generate-chart-review` | `src/routes/chartReview.js:10` | | `GET/POST/PUT/DELETE /api/notes` (+ `/trash`, `/restore`, `/empty`) | `src/routes/notes.js` | | `POST /api/notes/from-voice` | `src/routes/notes.js:224` | | `GET /api/encounters/saved` | `src/routes/encounters.js:23` | | `GET /api/encounters/saved/:id` | `src/routes/encounters.js:54` | | `POST /api/encounters/saved` | `src/routes/encounters.js:70` | | `DELETE /api/encounters/saved/:id` | `src/routes/encounters.js:163` | | `POST /api/refine` | `src/routes/refine.js:14` | | `POST /api/shorten` | `src/routes/refine.js:47` | | `POST /api/clarify` | `src/routes/refine.js:67` | | `POST /api/suggest-codes` | `src/routes/billing.js:291` | | `POST /api/dont-miss` | `src/routes/dontMiss.js:36` | Prompts (all in `src/utils/prompts.js`): | Use | Key | Line | |---|---|---| | Encounter HPI (outpatient) | `hpiEncounter` | 53 | | Encounter / Dictation HPI (inpatient) | `hpiInpatient` | 70 | | Dictation HPI | `hpiDictation` | 62 | | Hospital course (short / prose) | `hospitalCourseShort` | 82 | | Hospital course (day-by-day) | `hospitalCourseLong` | 94 | | Hospital course (ICU) | `hospitalCourseICU` | 105 | | Hospital course (psych) | `hospitalCoursePsych` | 124 | | Chart review (outpatient) | `chartReviewOutpatient` | 136 | | Chart review (subspecialty) | `chartReviewSubspecialty` | 148 | | Chart review (ED) | `chartReviewED` | 158 | | SOAP (full) | `soapFull` | 168 | | SOAP (subjective only) | `soapSubjective` | 176 | | Refine | `refine` | 287 | | Shorten | `shortenDocument` | 295 | | Clarify (gap-finder) | `askClarification` | 304 | | SSHADESS assessment | `shadessAssessment` | 311 | | Well visit (full) | `wellVisitNote` | 321 | | Well visit (short SOAP) | `wellVisitShort` | 354 | | Sick visit | `sickVisitNote` | 364 | | Don't-miss tooltip | `dontMissTooltip` | 505 | Cross-cutting: - `src/utils/promptSafe.js` — `wrapUserText()` and `INJECTION_GUARD` used by every clinical-notes route. - `src/utils/ai.js` — `callAI()` multi-provider router (OpenRouter, Bedrock, Azure, Vertex, LiteLLM). - `src/utils/crypto.js` — AES-256-GCM helpers used to encrypt every encounter / note column at rest.