feat: add patient education handouts
This commit is contained in:
parent
1cbe248450
commit
416fff624a
22 changed files with 307 additions and 39 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
106
public/js/app.js
106
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 = '<i class="fas fa-person-breastfeeding"></i> 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 =
|
||||
'<div class="card-header output-header">' +
|
||||
'<h3><i class="fas fa-person-breastfeeding" style="color:#0ea5e9;"></i> Patient Education Handout</h3>' +
|
||||
'<div class="output-actions">' +
|
||||
'<button class="btn-sm btn-primary" id="' + prefix + '-patient-ed-generate" type="button"><i class="fas fa-wand-magic-sparkles"></i> Generate</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div style="padding:10px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;align-items:end;">' +
|
||||
'<div class="demo-field"><label>Parent language</label><select id="' + prefix + '-patient-ed-language">' +
|
||||
'<option>English</option><option>Spanish</option><option>French</option><option>Arabic</option><option>Haitian Creole</option><option>Chinese</option><option>Russian</option><option>Portuguese</option>' +
|
||||
'</select></div>' +
|
||||
'<div class="demo-field"><label>Diagnosis/context</label><input type="text" id="' + prefix + '-patient-ed-diagnosis" placeholder="Optional: diagnosis to emphasize"></div>' +
|
||||
'<div class="demo-field"><label>Medications</label><input type="text" id="' + prefix + '-patient-ed-meds" placeholder="Optional: meds/doses from plan"></div>' +
|
||||
'</div>' +
|
||||
'<div id="' + prefix + '-patient-ed-text" class="output-text" contenteditable="true" style="margin:0 16px 12px;min-height:120px;" data-placeholder="Generated parent handout appears here..."></div>' +
|
||||
'<div style="padding:0 16px 12px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;">' +
|
||||
'<button class="btn-sm btn-primary" data-action="copy" data-target="' + prefix + '-patient-ed-text"><i class="fas fa-copy"></i> Copy</button>' +
|
||||
'<span style="font-size:11px;color:var(--g500);">Parent-facing draft. Verify before sharing.</span>' +
|
||||
'</div>';
|
||||
|
||||
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 = '<i class="fas fa-spinner fa-spin"></i> 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);
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -205,6 +205,7 @@
|
|||
'<div id="ed-final-note-text" class="output-text"></div>';
|
||||
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' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 || '{}');
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
||||
|
|
|
|||
66
src/routes/patientEducation.js
Normal file
66
src/routes/patientEducation.js
Normal file
|
|
@ -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;
|
||||
|
|
@ -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 ────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Reference in a new issue