diff --git a/README.md b/README.md index 0e4359c..4d7d5c4 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ The app runs as an authenticated Express/Postgres service with a browser fronten - Live encounter capture with structured pediatric HPI generation. - Dictation cleanup for narrative notes. - SOAP, sick visit, well visit, hospital course, chart review, precharting, and ED encounter workflows. +- Parent-facing education handouts generated from clinician notes, with diagnosis, medication, emergency-care guidance, and preferred-language support. - Pediatric developmental milestone tooling. - Templates, physician memory, and per-tab model overrides. - Server-side speech-to-text routing through configured providers. diff --git a/docs/api-reference.md b/docs/api-reference.md index 3d16a4d..81d8a0c 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -436,19 +436,23 @@ Generate a SHADESS (Strengths, Home, Activities, Drugs, Emotions, Sexuality, Saf --- -### POST /api/generate-sick-visit +### POST /api/sick-visit/note -Generate a sick visit note from a transcript, incorporating diagnosis information. +Generate a sick visit note from chief complaint, transcript/dictation, ROS/PE, and diagnosis context. - **Auth required:** Yes - **Request body:** ```json { - "transcript": "string", + "chiefComplaint": "string", + "transcript": "string (optional)", + "dictation": "string (optional)", "patientAge": "string", "patientGender": "string", "model": "string", - "diagnoses": "string | array", + "ros": "string (optional)", + "physicalExam": "string (optional)", + "diagnoses": "string (optional)", "physicianMemories": "string (optional)" } ``` @@ -456,6 +460,34 @@ Generate a sick visit note from a transcript, incorporating diagnosis informatio --- +### POST /api/patient-education + +Generate a parent-facing education handout from an existing clinician note. The frontend exposes this as the Handout action beside generated notes. + +- **Auth required:** Yes +- **Request body:** + ```json + { + "noteText": "string", + "diagnosis": "string (optional)", + "medications": "string (optional)", + "patientAge": "string (optional)", + "language": "string (optional, defaults to English)", + "readingLevel": "string (optional)", + "model": "string (optional)" + } + ``` +- **Response:** + ```json + { + "success": true, + "handout": "plain-text parent handout", + "model": "string" + } + ``` + +--- + ### POST /api/generate-milestone-narrative Generate a developmental milestone narrative from milestone data. @@ -498,8 +530,9 @@ Refine existing clinical text according to provided instructions. - **Request body:** ```json { - "text": "string", + "currentDocument": "string", "instructions": "string", + "sourceContext": "string (optional)", "model": "string" } ``` @@ -515,7 +548,7 @@ Shorten a block of clinical text while preserving key information. - **Request body:** ```json { - "text": "string", + "document": "string", "model": "string" } ``` @@ -531,7 +564,8 @@ Improve clarity and readability of clinical text. - **Request body:** ```json { - "text": "string", + "document": "string", + "context": "string (optional)", "model": "string" } ``` @@ -550,7 +584,7 @@ Check whether speech-to-text transcription is available and which provider is co ```json { "available": true, - "provider": "whisper | deepgram | browser" + "provider": "google | aws | litellm | openai | local | none" } ``` diff --git a/docs/architecture.md b/docs/architecture.md index a9ca6ea..e4d609f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -50,7 +50,7 @@ src/ models.js, prompts.js, ai.js # AI provider + model + prompt management embeddings.js # Vertex / LiteLLM / OpenAI embeddings transcribe*.js, tts*.js # STT / TTS provider clients - routes/ # 27 Express routers (auth, hpi, soap, …) + routes/ # Express routers (auth, hpi, soap, patient education, …) public/ # SPA index.html # shell, loads components on demand @@ -82,7 +82,7 @@ request → express.json (10 MB cap) → rate limiters (general 200 req/min, per-endpoint tighter on auth) → static (public/ with no-cache on HTML, 1h on JS/CSS; ?v=BUILD_ID busts cache per deploy) - → route (27 routers under /api/*) + → route (feature routers under /api/*) → authMiddleware (on protected routes: JWT, DB session check, 24h idle, last_activity update) → handler → response @@ -121,6 +121,12 @@ per-feature HTML fragments under `public/components/` fetched on demand. JS modules talk via `window` globals and `CustomEvent` on `document` — no bundler, no framework. Loader order is fixed in `index.html`. +Post-note helpers such as billing suggestions, don't-miss review, and patient +education handouts are reusable browser-side actions backed by authenticated +JSON APIs. The patient education helper generates a parent-facing plain-text +draft from the edited note and keeps the clinician in the review loop before +copying or sharing. + `authFetch.js` installs a global `fetch` interceptor that treats any 401 on an authenticated request as a signal to clear local session state and redirect to login. A `BroadcastChannel('pedscribe-auth')` pushes that signal to sibling diff --git a/docs/developer-guide-extended.md b/docs/developer-guide-extended.md index 94694b3..12cf9cd 100644 --- a/docs/developer-guide-extended.md +++ b/docs/developer-guide-extended.md @@ -134,7 +134,7 @@ The `database.js` file exports a helper object (`db`) with convenience methods: │ │ ├── wellVisit.js # Well-visit note generation (ROS/PE/ICD-10) │ │ ├── sickVisit.js # Sick visit note generation │ │ ├── refine.js # Refine/shorten any generated document -│ │ ├── transcribe.js # Whisper audio transcription +│ │ ├── transcribe.js # Server-side audio transcription provider router │ │ ├── tts.js # Text-to-speech (if configured) │ │ ├── nextcloud.js # Nextcloud WebDAV connect/export/disconnect │ │ ├── learningHub.js # User-facing: feed, content, quiz submission @@ -170,9 +170,9 @@ The `database.js` file exports a helper object (`db`) with convenience methods: │ │ │ # resend verification link handler, 2FA code input │ │ ├── admin.js # Admin panel: user management, site settings, SMTP config, │ │ │ # model enable/disable, prompt editor, announcement banner -│ │ ├── liveEncounter.js # MediaRecorder → Whisper transcription → AI HPI generation. +│ │ ├── liveEncounter.js # MediaRecorder → server STT → AI HPI generation. │ │ │ # Handles start/stop recording, timer, save/load encounters -│ │ ├── voiceDictation.js # Web Speech API (real-time) or Whisper (recorded) dictation +│ │ ├── voiceDictation.js # Web Speech preview plus server-STT-backed dictation │ │ ├── hospitalCourse.js # Paste/dictate hospital course → AI summary │ │ ├── chartReview.js # Paste/dictate chart data → AI outpatient review │ │ ├── soap.js # Paste/dictate → AI SOAP note diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 5fc7efe..0971246 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -40,7 +40,7 @@ src/ embeddings.js Vertex / LiteLLM / OpenAI embeddings notify.js ntfy push transcribe*.js, tts*.js STT / TTS provider clients - routes/ 27 routers + routes/ Express routers for auth, AI workflows, education, logs, and user data public/ index.html SPA shell, version-stamped asset refs @@ -49,7 +49,7 @@ public/ js/ 24 vanilla JS modules (no bundler) components/ per-tab HTML fragments loaded on demand css/styles.css - models/ bundled Whisper WASM + template-guide.md downloadable user template guide mobile/ Capacitor 6 wrapper (Android + iOS) .github/workflows/ CI (auto-version, APK, docker) diff --git a/docs/features-explained.md b/docs/features-explained.md index 5bb30eb..2f84ede 100644 --- a/docs/features-explained.md +++ b/docs/features-explained.md @@ -8,6 +8,12 @@ Ped-AI generates pediatric clinical notes from typed input, dictation, or record Model selection is available per task where the UI exposes a tab-level selector. Admin defaults provide the baseline model and user/task choices can override that baseline. +Generated notes can expose post-note helper panels. Billing suggestions and don't-miss review are clinician-facing. Patient education handouts are parent-facing drafts generated from the edited note, with optional diagnosis, medication, and preferred-language context. The clinician must verify the handout before sharing it. + +## Phone Extensions And Pagers + +The bedside tools include a per-user phone extension and pager directory. Entries support active/trash views, search, soft delete/restore, permanent purge, ZIP export, and JSON/ZIP import. Import preview flags exact active duplicates, exact trashed matches that can be restored, and possible duplicates before committing changes. + ## Speech Final transcription is server-side. Configure Google/Gemini, AWS Transcribe, LiteLLM, or OpenAI Whisper according to your deployment requirements. @@ -61,6 +67,8 @@ Admins can manage users, roles, registration, security settings, model defaults, | Browser Whisper | Removed | No public worker or model download path. | | Learning Hub CMS | Active | Articles, pearls, quizzes, presentations. | | Nextcloud WebDAV | Active | Used for file browsing/content import. | +| Patient handouts | Active | Parent-facing, note-derived, preferred-language draft. | +| Extension transfer | Active | ZIP export plus JSON/ZIP import preview. | | Audio backups | Active | Failure recovery only. | | TTS preview | Active | Depends on configured provider. | diff --git a/docs/improvements.md b/docs/improvements.md index 78630d9..ffcac75 100644 --- a/docs/improvements.md +++ b/docs/improvements.md @@ -93,9 +93,9 @@ This would help administrators optimize model selection and identify training op ### 9. Patient Education Materials -**Current state:** The Learning Hub serves educational content to physicians. +**Current state:** Patient education handouts are active as post-note helpers. Generated notes can open a Handout panel that creates a parent-facing plain-text draft from the clinician note, with optional diagnosis, medication, patient age, and preferred language context. The Learning Hub remains the physician-facing education/CMS area. -**Improvement:** Add a patient-facing education module that generates age-appropriate handouts based on the diagnosis. For example, after generating a note for a child with asthma, the app could produce a parent-friendly handout explaining the diagnosis, medications, and when to seek emergency care — in the parent's preferred language. +**Further improvement:** Add handout templates, saved handout history, institution-approved language libraries, and printable/PDF export. ### 10. Multi-Language Support diff --git a/docs/logic/architecture.md b/docs/logic/architecture.md index 1fd4a97..4f72a44 100644 --- a/docs/logic/architecture.md +++ b/docs/logic/architecture.md @@ -560,17 +560,16 @@ it top-to-bottom and you have the whole boot sequence. `req.ip` is the real client IP for rate-limit + audit log. 5. **`server.js:21–54`** — Helmet with a hand-crafted CSP. CSP allows: - `'self'` everywhere by default. - - `'wasm-unsafe-eval'` and `'unsafe-eval'` because in-browser - Whisper (`@xenova/transformers`) needs both. - - `cdn.jsdelivr.net` for the transformers worker. - - `huggingface.co` + the cdn-lfs subdomains for Whisper model - downloads. + - CDN origins used by the current frontend vendor assets. - `https://challenges.cloudflare.com` for Turnstile. - `connectSrc` extended to `clinicaltables.nlm.nih.gov` for ICD autocomplete. - `upgradeInsecureRequests: null` so the e2e container can be hit over plain HTTP. Production has Caddy in front, mixed content is a non-issue. + +Browser Whisper, Transformers.js browser STT, public Whisper workers, and +browser-local model downloads are intentionally not part of the runtime or CSP. 6. **`server.js:59–80`** — CORS, scoped to `/api/*` only. Static assets must not be CORS-checked because ES-module scripts always send an Origin header. In production, fail-closed: if neither @@ -738,7 +737,7 @@ From `server.js:229–303`: | `routes/chartReview.js` | `/api` | Chart review summarization (multi-note input) | | `routes/milestones.js` | `/api` | Read milestones, run developmental check | | `routes/peGuide.js` | `/api` | PE guide AI prompts (audio findings → narrative) | -| `routes/extensions.js` | `/api` | CRUD on `user_phone_extensions` | +| `routes/extensions.js` | `/api` | CRUD, ZIP export, JSON/ZIP import, duplicate preview for `user_phone_extensions` | | `routes/soap.js` | `/api` | SOAP note generation | | `routes/tts.js` | `/api` | Text-to-speech proxy (Google / ElevenLabs / LiteLLM) | | `routes/nextcloud.js` | `/api` | WebDAV upload of finished notes | @@ -755,6 +754,7 @@ From `server.js:229–303`: | `routes/sickVisit.js` | `/api` | Sick-visit AI note pipeline | | `routes/edEncounters.js` | `/api` | ED encounter pipeline | | `routes/dontMiss.js` | `/api` | "Don't miss" differential / red-flag prompt | +| `routes/patientEducation.js` | `/api` | Parent-facing education handouts generated from clinician notes | | `routes/userPreferences.js` | `/api/user` | User-level preferences (STT model, TTS voice, etc.) | | `routes/learningAI.js` | `/api/admin/learning` | AI-assisted Learning Hub authoring (generate quiz from content, embed) | @@ -1519,12 +1519,6 @@ RUN apk add --no-cache --virtual .build-deps python3 make g++ \ COPY . . RUN chmod +x /app/docker-entrypoint.sh RUN mkdir -p /app/data/logs -RUN mkdir -p /app/public/models/Xenova/whisper-tiny.en/onnx && \ - cd /app/public/models && \ - curl -sL -o transformers.min.js https://cdn.jsdelivr.net/npm/@xenova/transformers@2.0.0/dist/transformers.min.js && \ - cd Xenova/whisper-tiny.en && \ - curl -sL -o config.json https://huggingface.co/Xenova/whisper-tiny.en/resolve/main/config.json && \ - ... EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1 @@ -1543,6 +1537,8 @@ Notes: Final image stays slim (~220 MB). - **`npm install --omit=dev`** — devDependencies (jsdom, dompurify for tests) are not in the runtime image. +- **No browser Whisper download step.** Browser-local Whisper assets and + Transformers.js browser STT are intentionally absent from the runtime image. - **Healthcheck.** Polls `/api/health` every 30 s. Container marked unhealthy after 5 failures. diff --git a/docs/logic/clinical-notes.md b/docs/logic/clinical-notes.md index 34132aa..122eed0 100644 --- a/docs/logic/clinical-notes.md +++ b/docs/logic/clinical-notes.md @@ -42,8 +42,9 @@ But every tab sits on top of the same scaffolding: `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. +6. After generation, shared helpers can mount additional cards beneath + the note, including billing suggestions, don't-miss review, and + parent-facing patient education handouts. Why a tab per note type? Each note type has different inputs (e.g. Sick Visit needs CC + ROS + @@ -133,16 +134,18 @@ react. `transcribeAudio(blob)` in `public/js/app.js:739–795` is the single entry point for STT: -- If browser-Whisper is enabled in user settings (and supported), it - runs locally — no network call, no PHI leaves the device. -- Otherwise it POSTs the Blob to `/api/transcribe` (handled by +- It POSTs the Blob to `/api/transcribe` (handled by `src/routes/transcribe.js` — out of scope here). The server picks whichever provider the admin configured: Gemini, AWS Transcribe - Medical, OpenAI Whisper, Local Whisper, or LiteLLM. + Medical, OpenAI Whisper, local server Whisper, or LiteLLM. - On any failure with a non-empty Blob, the audio is auto-saved via `saveAudioBackup(blob, 'failed-transcription')` (`audioBackup.js`) so the user can retry from the Settings page. +Browser Whisper and browser-local model downloads are removed from the +runtime. Browser-native Web Speech remains an explicit opt-in live preview +when supported by the browser, not the final transcription path. + A startup probe (`checkTranscribeStatus`, `app.js:722`) queries `/api/transcribe/status` and stores the result in `window._transcribeAvailable`. If the server reports `false`, every tab @@ -188,6 +191,10 @@ After the note is rendered, several tabs trigger optional follow-ups: - `suggestDontMiss(outputId, noteText, noteType, age, cc)` (`app.js:894`) — POSTs to `/api/dont-miss` and mounts an orange-bordered card with up to 5 high-yield "don't miss" items. +- `attachPatientEducation(outputId, { patientAge })` — adds a Handout + button that opens a parent-facing education panel. Generate POSTs the + edited note to `/api/patient-education` with optional diagnosis, + medication, and parent-language fields. Refine and Shorten use the same pattern: `refineDocument(outputId, inputId)` (`app.js:946`) and `shortenDocument(outputId)` (`app.js:971`) diff --git a/mobile/README.md b/mobile/README.md index 9b1462c..5fee377 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -1,10 +1,10 @@ # PedScribe Mobile App -Native mobile wrapper for Pediatric AI Scribe using Capacitor. Provides background audio recording, push notifications, haptic feedback, deep linking, and share intent support on both iOS and Android. +Capacitor mobile wrapper for the hosted Ped-AI web app. The app defaults to `https://app.pedshub.com`, lets users choose a self-hosted server URL, and keeps clinical workflows API-backed through the same Express service as the browser app. ## Features -- Background recording that survives screen lock (foreground service on Android, background audio on iOS) +- Hosted web workflow inside a native WebView; server updates reach mobile clients without app-store releases - Configurable server URL (supports self-hosted instances) - Haptic feedback on recording start/stop - Keep screen awake during recording @@ -15,7 +15,7 @@ Native mobile wrapper for Pediatric AI Scribe using Capacitor. Provides backgrou stored in iOS Keychain / Android Keystore, gated by OS biometric. Enrolled on first password sign-in (opt-in prompt). 2FA still applies on top — biometric replaces the password step only. -- App Store and Play Store ready +- Android and iOS project scaffolds for store builds ## Prerequisites diff --git a/public/js/app.js b/public/js/app.js index fdf3ede..0731d83 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -817,6 +817,112 @@ function suggestDontMiss(outputElementId, noteText, noteType, patientAge, chiefC }); } +// ── Patient education handout helper ──────────────────────── +// Adds a reusable "Handout" action beside generated clinical notes. The actual +// handout is generated only when the physician clicks Generate in the panel. +function attachPatientEducation(outputElementId, opts) { + var outputEl = document.getElementById(outputElementId); + if (!outputEl) return; + opts = opts || {}; + var card = outputEl.closest('.card, .output-card'); + if (!card) return; + var actions = card.querySelector('.output-actions'); + var prefix = outputElementId.replace(/[^a-zA-Z0-9_-]/g, ''); + + if (actions && !document.getElementById(prefix + '-patient-ed-btn')) { + var btn = document.createElement('button'); + btn.id = prefix + '-patient-ed-btn'; + btn.className = 'btn-sm btn-ghost'; + btn.type = 'button'; + btn.innerHTML = ' Handout'; + btn.addEventListener('click', function() { + var panel = ensurePatientEducationPanel(outputElementId, opts); + panel.classList.remove('hidden'); + panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + }); + actions.appendChild(btn); + } +} + +function ensurePatientEducationPanel(outputElementId, opts) { + var outputEl = document.getElementById(outputElementId); + var prefix = outputElementId.replace(/[^a-zA-Z0-9_-]/g, ''); + var panelId = prefix + '-patient-ed'; + var panel = document.getElementById(panelId); + if (panel) return panel; + + panel = document.createElement('div'); + panel.id = panelId; + panel.className = 'card patient-ed-card hidden'; + panel.style.cssText = 'margin-top:10px;border-left:3px solid #0ea5e9;'; + panel.innerHTML = + '
' + + '

Patient Education Handout

' + + '
' + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '' + + 'Parent-facing draft. Verify before sharing.' + + '
'; + + outputEl.parentNode.insertBefore(panel, outputEl.nextSibling); + var gen = panel.querySelector('#' + prefix + '-patient-ed-generate'); + if (gen) gen.addEventListener('click', function() { generatePatientEducation(outputElementId, opts); }); + return panel; +} + +function generatePatientEducation(outputElementId, opts) { + var outputEl = document.getElementById(outputElementId); + if (!outputEl) return; + opts = opts || {}; + var prefix = outputElementId.replace(/[^a-zA-Z0-9_-]/g, ''); + var handoutEl = document.getElementById(prefix + '-patient-ed-text'); + var langEl = document.getElementById(prefix + '-patient-ed-language'); + var dxEl = document.getElementById(prefix + '-patient-ed-diagnosis'); + var medsEl = document.getElementById(prefix + '-patient-ed-meds'); + var noteText = (outputEl.innerText || outputEl.textContent || '').trim(); + if (!noteText) { showToast('No note for handout', 'error'); return; } + + if (handoutEl) handoutEl.innerHTML = ' Generating parent handout...'; + fetch('/api/patient-education', { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ + noteText: noteText, + diagnosis: dxEl ? dxEl.value : '', + medications: medsEl ? medsEl.value : '', + patientAge: opts.patientAge || '', + language: langEl ? langEl.value : 'English', + readingLevel: '6th grade plain language', + model: getSelectedModel() + }) + }) + .then(function(r) { return r.json(); }) + .then(function(data) { + if (!data.success) { + if (handoutEl) handoutEl.textContent = ''; + showToast(data.error || 'Handout generation failed', 'error'); + return; + } + setOutputText(handoutEl, data.handout || ''); + showToast('Patient handout generated', 'success'); + }) + .catch(function(err) { + if (handoutEl) handoutEl.textContent = ''; + showToast(err.message || 'Handout generation failed', 'error'); + }); +} + function refineDocument(outputElementId, inputElementId) { var doc = document.getElementById(outputElementId); var input = document.getElementById(inputElementId); diff --git a/public/js/chartReview.js b/public/js/chartReview.js index 07cfb4e..6822f00 100644 --- a/public/js/chartReview.js +++ b/public/js/chartReview.js @@ -174,6 +174,7 @@ outputCard.classList.remove('hidden'); outputCard.scrollIntoView({ behavior: 'smooth' }); showToast('Chart review generated!', 'success'); + if (typeof attachPatientEducation === 'function') attachPatientEducation('cr-review-text', { patientAge: document.getElementById('cr-age').value }); if (typeof suggestBillingCodes === 'function') suggestBillingCodes('cr-review-text', data.review, 'chart', document.getElementById('cr-age').value); } else showToast(data.error || 'Failed', 'error'); }) @@ -190,6 +191,7 @@ var outputCard = document.getElementById('cr-output'); if (reviewText) reviewText.textContent = enc.generated_note; if (outputCard) outputCard.classList.remove('hidden'); + if (typeof attachPatientEducation === 'function') attachPatientEducation('cr-review-text', { patientAge: document.getElementById('cr-age').value }); } }); diff --git a/public/js/ed-encounters.js b/public/js/ed-encounters.js index 15261b3..d411035 100644 --- a/public/js/ed-encounters.js +++ b/public/js/ed-encounters.js @@ -205,6 +205,7 @@ '
'; var t = container.querySelector('#ed-final-note-text'); if (t) t.textContent = note; + if (typeof attachPatientEducation === 'function') attachPatientEducation('ed-final-note-text', { patientAge: getVal('ed-age') }); container.classList.remove('hidden'); container.scrollIntoView({ behavior: 'smooth' }); } diff --git a/public/js/hospitalCourse.js b/public/js/hospitalCourse.js index f3d7346..dd92d46 100644 --- a/public/js/hospitalCourse.js +++ b/public/js/hospitalCourse.js @@ -175,6 +175,7 @@ clarifyOutput.classList.add('hidden'); outputCard.scrollIntoView({ behavior: 'smooth' }); showToast('Hospital course generated!', 'success'); + if (typeof attachPatientEducation === 'function') attachPatientEducation('hc-course-text', { patientAge: document.getElementById('hc-age').value }); if (typeof suggestBillingCodes === 'function') suggestBillingCodes('hc-course-text', data.hospitalCourse, 'hospital', document.getElementById('hc-age').value, document.getElementById('hc-setting').value); } else showToast(data.error || 'Failed', 'error'); }) diff --git a/public/js/liveEncounter.js b/public/js/liveEncounter.js index a528e8d..972141f 100644 --- a/public/js/liveEncounter.js +++ b/public/js/liveEncounter.js @@ -213,6 +213,7 @@ outputCard.classList.remove('hidden'); outputCard.scrollIntoView({ behavior: 'smooth' }); showToast('HPI generated!', 'success'); + if (typeof attachPatientEducation === 'function') attachPatientEducation('enc-hpi-text', { patientAge: document.getElementById('enc-age').value }); if (typeof suggestBillingCodes === 'function') suggestBillingCodes('enc-hpi-text', data.hpi, 'hpi', document.getElementById('enc-age').value, document.getElementById('enc-setting').value); if (typeof suggestDontMiss === 'function') suggestDontMiss('enc-hpi-text', data.hpi, 'hpi', document.getElementById('enc-age').value); } else showToast(data.error || 'Failed', 'error'); diff --git a/public/js/shadess.js b/public/js/shadess.js index c094674..7799f3d 100644 --- a/public/js/shadess.js +++ b/public/js/shadess.js @@ -921,6 +921,7 @@ if (tag) tag.textContent = (data.model || '').split('/').pop(); if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); } showToast('Well visit note generated!', 'success'); + if (typeof attachPatientEducation === 'function') attachPatientEducation('wv-note-text', { patientAge: document.getElementById('wv-note-age').value }); if (typeof suggestBillingCodes === 'function') suggestBillingCodes('wv-note-text', data.note, 'wellvisit', document.getElementById('wv-note-age').value); } else { showToast(data.error || 'Generation failed', 'error'); diff --git a/public/js/sickVisit.js b/public/js/sickVisit.js index 5c7d577..495ba63 100644 --- a/public/js/sickVisit.js +++ b/public/js/sickVisit.js @@ -335,6 +335,7 @@ if (tag) tag.textContent = (data.model || '').split('/').pop(); if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); } showToast('Sick visit note generated!', 'success'); + if (typeof attachPatientEducation === 'function') attachPatientEducation('sick-note-text', { patientAge: document.getElementById('sick-age').value }); if (typeof suggestBillingCodes === 'function') suggestBillingCodes('sick-note-text', data.note, 'sickvisit', document.getElementById('sick-age').value); if (typeof suggestDontMiss === 'function') suggestDontMiss('sick-note-text', data.note, 'sickvisit', document.getElementById('sick-age').value, document.getElementById('sick-cc').value); } else { diff --git a/public/js/soap.js b/public/js/soap.js index 4753577..2156acc 100644 --- a/public/js/soap.js +++ b/public/js/soap.js @@ -193,6 +193,7 @@ outputCard.classList.remove('hidden'); outputCard.scrollIntoView({ behavior: 'smooth' }); showToast('SOAP note generated!', 'success'); + if (typeof attachPatientEducation === 'function') attachPatientEducation('soap-text', { patientAge: document.getElementById('soap-age').value }); if (typeof suggestBillingCodes === 'function') suggestBillingCodes('soap-text', data.soap, 'soap', document.getElementById('soap-age').value); } else showToast(data.error || 'Failed', 'error'); }) @@ -209,6 +210,7 @@ if (enc.generated_note) { setOutputText(soapText, enc.generated_note); outputCard.classList.remove('hidden'); + if (typeof attachPatientEducation === 'function') attachPatientEducation('soap-text', { patientAge: document.getElementById('soap-age').value }); } try { var pd = JSON.parse(enc.partial_data || '{}'); diff --git a/public/js/voiceDictation.js b/public/js/voiceDictation.js index 980c76c..63286eb 100644 --- a/public/js/voiceDictation.js +++ b/public/js/voiceDictation.js @@ -187,6 +187,7 @@ outputCard.classList.remove('hidden'); outputCard.scrollIntoView({ behavior: 'smooth' }); showToast('Generated!', 'success'); + if (typeof attachPatientEducation === 'function') attachPatientEducation('dict-hpi-text', { patientAge: document.getElementById('dict-age').value }); } else showToast(data.error || 'Failed', 'error'); }) .catch(function(err) { hideBusy(); showToast(err.message, 'error'); }); @@ -202,6 +203,7 @@ if (enc.generated_note) { hpiText.textContent = enc.generated_note; outputCard.classList.remove('hidden'); + if (typeof attachPatientEducation === 'function') attachPatientEducation('dict-hpi-text', { patientAge: document.getElementById('dict-age').value }); } }); } diff --git a/server.js b/server.js index 1f23902..41247cb 100644 --- a/server.js +++ b/server.js @@ -320,6 +320,7 @@ app.use('/api', require('./src/routes/wellVisit')); app.use('/api', require('./src/routes/sickVisit')); app.use('/api', require('./src/routes/edEncounters')); app.use('/api', require('./src/routes/dontMiss')); +app.use('/api', require('./src/routes/patientEducation')); app.use('/api/user', require('./src/routes/userPreferences')); app.use('/api/admin/learning', require('./src/routes/learningAI')); diff --git a/src/routes/patientEducation.js b/src/routes/patientEducation.js new file mode 100644 index 0000000..1968d3e --- /dev/null +++ b/src/routes/patientEducation.js @@ -0,0 +1,66 @@ +// ============================================================ +// PATIENT EDUCATION HANDOUTS — parent-facing education generated +// from an existing clinician note. +// ============================================================ + +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.use(authMiddleware); + +// ── POST /patient-education ───────────────────────────────── +// Body: +// noteText — generated clinician note (required) +// diagnosis — optional explicit diagnosis override/context +// medications — optional medication list or free text +// patientAge — optional, used for age-appropriate language +// language — parent preferred language; defaults to English +// readingLevel — optional plain-language target +// model — optional override +// Returns: { success, handout, model } +router.post('/patient-education', async function(req, res) { + var start = Date.now(); + try { + var body = req.body || {}; + var noteText = String(body.noteText || '').trim(); + if (!noteText) return res.status(400).json({ error: 'noteText is required' }); + + var language = String(body.language || 'English').trim() || 'English'; + var context = 'PARENT PREFERRED LANGUAGE: ' + language + '\n'; + context += 'PATIENT AGE: ' + (body.patientAge || 'unknown') + '\n'; + context += 'READING LEVEL: ' + (body.readingLevel || 'plain language') + '\n'; + if (body.diagnosis && String(body.diagnosis).trim()) { + context += 'DIAGNOSIS CONTEXT: ' + wrapUserText('diagnosis', body.diagnosis) + '\n'; + } + if (body.medications && String(body.medications).trim()) { + context += 'MEDICATION CONTEXT: ' + wrapUserText('medications', body.medications) + '\n'; + } + context += '\nCLINICIAN NOTE:\n' + wrapUserText('note', noteText); + + var result = await callAI([ + { role: 'system', content: PROMPTS.patientEducation + INJECTION_GUARD }, + { role: 'user', content: context } + ], { model: body.model, maxTokens: 1800 }); + + var duration = Date.now() - start; + logger.apiCall(req.user.id, '/api/patient-education', { + model: result.model, + tokensInput: result.usage && result.usage.input_tokens, + tokensOutput: result.usage && result.usage.output_tokens, + duration: duration, + statusCode: 200 + }); + + res.json({ success: true, handout: String(result.content || '').trim(), model: result.model }); + } catch (e) { + logger.error('[patientEducation] failed', e.message); + res.status(500).json({ error: 'Request failed' }); + } +}); + +module.exports = router; diff --git a/src/utils/prompts.js b/src/utils/prompts.js index aac91fe..c5fbddf 100644 --- a/src/utils/prompts.js +++ b/src/utils/prompts.js @@ -547,7 +547,39 @@ WHAT TO AVOID: - Items already clearly documented in the note - Generic boilerplate ("consider differential diagnosis") - Treatment recommendations (this is a documentation/clinical-reasoning prompt, not a treatment plan) -- Padding to reach 5 — fewer is fine` +- Padding to reach 5 — fewer is fine`, + + // ======================== PATIENT EDUCATION HANDOUT ======================== + patientEducation: `You are a pediatric clinician creating a parent-facing discharge/education handout from a clinician's note. + +CRITICAL SAFETY RULES: +- Use only diagnoses, medications, instructions, and clinical facts explicitly provided in the input +- Do not invent test results, medication doses, follow-up intervals, restrictions, or return precautions not supported by the input +- If a requested detail is missing, write a safe general sentence such as "Your care team did not specify this in the note" rather than guessing +- This is patient education, not a new medical plan. Do not add new treatment recommendations +- Use plain, reassuring, parent-friendly language at about a 6th grade reading level unless asked otherwise +- Translate the entire handout into the requested parent language. Keep medication names and emergency symptoms clinically clear +- PLAIN TEXT ONLY: do NOT use markdown formatting — no asterisks (*), no pound signs (#), no underscores (_), no backticks, no bold/italic markers + +Output exactly these sections, with section labels on their own lines: + +Diagnosis: +Explain the diagnosis or likely diagnosis in simple language. If no diagnosis is documented, say that the note does not list a final diagnosis. + +What this means: +Give a brief age-appropriate explanation for parents/caregivers. + +Medicines and home care: +List only medicines and care instructions documented in the input. Include doses only if provided. If none are documented, say none were specified in the note. + +When to seek emergency care: +List urgent warning signs that are either documented in the note or are broadly appropriate for the documented condition and pediatric age. Keep this section safety-focused and concise. + +Follow-up: +Include follow-up timing or clinician instructions only if documented. If not documented, say to follow the care team's instructions. + +Questions for your care team: +List 2-4 practical questions parents can ask if anything is unclear.` }; // ── DB override support ────────────────────────────────────────────────────