feat: add patient education handouts
This commit is contained in:
parent
626171b039
commit
8b23ff995c
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.
|
- Live encounter capture with structured pediatric HPI generation.
|
||||||
- Dictation cleanup for narrative notes.
|
- Dictation cleanup for narrative notes.
|
||||||
- SOAP, sick visit, well visit, hospital course, chart review, precharting, and ED encounter workflows.
|
- 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.
|
- Pediatric developmental milestone tooling.
|
||||||
- Templates, physician memory, and per-tab model overrides.
|
- Templates, physician memory, and per-tab model overrides.
|
||||||
- Server-side speech-to-text routing through configured providers.
|
- 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
|
- **Auth required:** Yes
|
||||||
- **Request body:**
|
- **Request body:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"transcript": "string",
|
"chiefComplaint": "string",
|
||||||
|
"transcript": "string (optional)",
|
||||||
|
"dictation": "string (optional)",
|
||||||
"patientAge": "string",
|
"patientAge": "string",
|
||||||
"patientGender": "string",
|
"patientGender": "string",
|
||||||
"model": "string",
|
"model": "string",
|
||||||
"diagnoses": "string | array",
|
"ros": "string (optional)",
|
||||||
|
"physicalExam": "string (optional)",
|
||||||
|
"diagnoses": "string (optional)",
|
||||||
"physicianMemories": "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
|
### POST /api/generate-milestone-narrative
|
||||||
|
|
||||||
Generate a developmental milestone narrative from milestone data.
|
Generate a developmental milestone narrative from milestone data.
|
||||||
|
|
@ -498,8 +530,9 @@ Refine existing clinical text according to provided instructions.
|
||||||
- **Request body:**
|
- **Request body:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"text": "string",
|
"currentDocument": "string",
|
||||||
"instructions": "string",
|
"instructions": "string",
|
||||||
|
"sourceContext": "string (optional)",
|
||||||
"model": "string"
|
"model": "string"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -515,7 +548,7 @@ Shorten a block of clinical text while preserving key information.
|
||||||
- **Request body:**
|
- **Request body:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"text": "string",
|
"document": "string",
|
||||||
"model": "string"
|
"model": "string"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -531,7 +564,8 @@ Improve clarity and readability of clinical text.
|
||||||
- **Request body:**
|
- **Request body:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"text": "string",
|
"document": "string",
|
||||||
|
"context": "string (optional)",
|
||||||
"model": "string"
|
"model": "string"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -550,7 +584,7 @@ Check whether speech-to-text transcription is available and which provider is co
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"available": true,
|
"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
|
models.js, prompts.js, ai.js # AI provider + model + prompt management
|
||||||
embeddings.js # Vertex / LiteLLM / OpenAI embeddings
|
embeddings.js # Vertex / LiteLLM / OpenAI embeddings
|
||||||
transcribe*.js, tts*.js # STT / TTS provider clients
|
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
|
public/ # SPA
|
||||||
index.html # shell, loads components on demand
|
index.html # shell, loads components on demand
|
||||||
|
|
@ -82,7 +82,7 @@ request
|
||||||
→ express.json (10 MB cap)
|
→ express.json (10 MB cap)
|
||||||
→ rate limiters (general 200 req/min, per-endpoint tighter on auth)
|
→ 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)
|
→ 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)
|
→ authMiddleware (on protected routes: JWT, DB session check, 24h idle, last_activity update)
|
||||||
→ handler
|
→ handler
|
||||||
→ response
|
→ 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
|
modules talk via `window` globals and `CustomEvent` on `document` — no
|
||||||
bundler, no framework. Loader order is fixed in `index.html`.
|
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
|
`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
|
authenticated request as a signal to clear local session state and redirect to
|
||||||
login. A `BroadcastChannel('pedscribe-auth')` pushes that signal to sibling
|
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)
|
│ │ ├── wellVisit.js # Well-visit note generation (ROS/PE/ICD-10)
|
||||||
│ │ ├── sickVisit.js # Sick visit note generation
|
│ │ ├── sickVisit.js # Sick visit note generation
|
||||||
│ │ ├── refine.js # Refine/shorten any generated document
|
│ │ ├── 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)
|
│ │ ├── tts.js # Text-to-speech (if configured)
|
||||||
│ │ ├── nextcloud.js # Nextcloud WebDAV connect/export/disconnect
|
│ │ ├── nextcloud.js # Nextcloud WebDAV connect/export/disconnect
|
||||||
│ │ ├── learningHub.js # User-facing: feed, content, quiz submission
|
│ │ ├── 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
|
│ │ │ # resend verification link handler, 2FA code input
|
||||||
│ │ ├── admin.js # Admin panel: user management, site settings, SMTP config,
|
│ │ ├── admin.js # Admin panel: user management, site settings, SMTP config,
|
||||||
│ │ │ # model enable/disable, prompt editor, announcement banner
|
│ │ │ # 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
|
│ │ │ # 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
|
│ │ ├── hospitalCourse.js # Paste/dictate hospital course → AI summary
|
||||||
│ │ ├── chartReview.js # Paste/dictate chart data → AI outpatient review
|
│ │ ├── chartReview.js # Paste/dictate chart data → AI outpatient review
|
||||||
│ │ ├── soap.js # Paste/dictate → AI SOAP note
|
│ │ ├── soap.js # Paste/dictate → AI SOAP note
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ src/
|
||||||
embeddings.js Vertex / LiteLLM / OpenAI embeddings
|
embeddings.js Vertex / LiteLLM / OpenAI embeddings
|
||||||
notify.js ntfy push
|
notify.js ntfy push
|
||||||
transcribe*.js, tts*.js STT / TTS provider clients
|
transcribe*.js, tts*.js STT / TTS provider clients
|
||||||
routes/ 27 routers
|
routes/ Express routers for auth, AI workflows, education, logs, and user data
|
||||||
|
|
||||||
public/
|
public/
|
||||||
index.html SPA shell, version-stamped asset refs
|
index.html SPA shell, version-stamped asset refs
|
||||||
|
|
@ -49,7 +49,7 @@ public/
|
||||||
js/ 24 vanilla JS modules (no bundler)
|
js/ 24 vanilla JS modules (no bundler)
|
||||||
components/ per-tab HTML fragments loaded on demand
|
components/ per-tab HTML fragments loaded on demand
|
||||||
css/styles.css
|
css/styles.css
|
||||||
models/ bundled Whisper WASM
|
template-guide.md downloadable user template guide
|
||||||
|
|
||||||
mobile/ Capacitor 6 wrapper (Android + iOS)
|
mobile/ Capacitor 6 wrapper (Android + iOS)
|
||||||
.github/workflows/ CI (auto-version, APK, docker)
|
.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.
|
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
|
## Speech
|
||||||
|
|
||||||
Final transcription is server-side. Configure Google/Gemini, AWS Transcribe, LiteLLM, or OpenAI Whisper according to your deployment requirements.
|
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. |
|
| Browser Whisper | Removed | No public worker or model download path. |
|
||||||
| Learning Hub CMS | Active | Articles, pearls, quizzes, presentations. |
|
| Learning Hub CMS | Active | Articles, pearls, quizzes, presentations. |
|
||||||
| Nextcloud WebDAV | Active | Used for file browsing/content import. |
|
| 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. |
|
| Audio backups | Active | Failure recovery only. |
|
||||||
| TTS preview | Active | Depends on configured provider. |
|
| 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
|
### 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
|
### 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.
|
`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:
|
5. **`server.js:21–54`** — Helmet with a hand-crafted CSP. CSP allows:
|
||||||
- `'self'` everywhere by default.
|
- `'self'` everywhere by default.
|
||||||
- `'wasm-unsafe-eval'` and `'unsafe-eval'` because in-browser
|
- CDN origins used by the current frontend vendor assets.
|
||||||
Whisper (`@xenova/transformers`) needs both.
|
|
||||||
- `cdn.jsdelivr.net` for the transformers worker.
|
|
||||||
- `huggingface.co` + the cdn-lfs subdomains for Whisper model
|
|
||||||
downloads.
|
|
||||||
- `https://challenges.cloudflare.com` for Turnstile.
|
- `https://challenges.cloudflare.com` for Turnstile.
|
||||||
- `connectSrc` extended to `clinicaltables.nlm.nih.gov` for ICD
|
- `connectSrc` extended to `clinicaltables.nlm.nih.gov` for ICD
|
||||||
autocomplete.
|
autocomplete.
|
||||||
- `upgradeInsecureRequests: null` so the e2e container can be
|
- `upgradeInsecureRequests: null` so the e2e container can be
|
||||||
hit over plain HTTP. Production has Caddy in front, mixed
|
hit over plain HTTP. Production has Caddy in front, mixed
|
||||||
content is a non-issue.
|
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
|
6. **`server.js:59–80`** — CORS, scoped to `/api/*` only. Static
|
||||||
assets must not be CORS-checked because ES-module scripts always
|
assets must not be CORS-checked because ES-module scripts always
|
||||||
send an Origin header. In production, fail-closed: if neither
|
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/chartReview.js` | `/api` | Chart review summarization (multi-note input) |
|
||||||
| `routes/milestones.js` | `/api` | Read milestones, run developmental check |
|
| `routes/milestones.js` | `/api` | Read milestones, run developmental check |
|
||||||
| `routes/peGuide.js` | `/api` | PE guide AI prompts (audio findings → narrative) |
|
| `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/soap.js` | `/api` | SOAP note generation |
|
||||||
| `routes/tts.js` | `/api` | Text-to-speech proxy (Google / ElevenLabs / LiteLLM) |
|
| `routes/tts.js` | `/api` | Text-to-speech proxy (Google / ElevenLabs / LiteLLM) |
|
||||||
| `routes/nextcloud.js` | `/api` | WebDAV upload of finished notes |
|
| `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/sickVisit.js` | `/api` | Sick-visit AI note pipeline |
|
||||||
| `routes/edEncounters.js` | `/api` | ED encounter pipeline |
|
| `routes/edEncounters.js` | `/api` | ED encounter pipeline |
|
||||||
| `routes/dontMiss.js` | `/api` | "Don't miss" differential / red-flag prompt |
|
| `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/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) |
|
| `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 . .
|
COPY . .
|
||||||
RUN chmod +x /app/docker-entrypoint.sh
|
RUN chmod +x /app/docker-entrypoint.sh
|
||||||
RUN mkdir -p /app/data/logs
|
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
|
EXPOSE 3000
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \
|
||||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
|
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).
|
Final image stays slim (~220 MB).
|
||||||
- **`npm install --omit=dev`** — devDependencies (jsdom, dompurify
|
- **`npm install --omit=dev`** — devDependencies (jsdom, dompurify
|
||||||
for tests) are not in the runtime image.
|
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
|
- **Healthcheck.** Polls `/api/health` every 30 s. Container marked
|
||||||
unhealthy after 5 failures.
|
unhealthy after 5 failures.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,9 @@ But every tab sits on top of the same scaffolding:
|
||||||
`src/routes/`.
|
`src/routes/`.
|
||||||
5. An **output card** that hides until generation succeeds, then
|
5. An **output card** that hides until generation succeeds, then
|
||||||
reveals the AI's note plus a `Refine` and `Shorten` bar.
|
reveals the AI's note plus a `Refine` and `Shorten` bar.
|
||||||
6. After generation, a few of the tabs auto-call **billing-codes** and
|
6. After generation, shared helpers can mount additional cards beneath
|
||||||
**don't-miss** helpers that mount additional cards beneath the note.
|
the note, including billing suggestions, don't-miss review, and
|
||||||
|
parent-facing patient education handouts.
|
||||||
|
|
||||||
Why a tab per note type?
|
Why a tab per note type?
|
||||||
Each note type has different inputs (e.g. Sick Visit needs CC + ROS +
|
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
|
`transcribeAudio(blob)` in `public/js/app.js:739–795` is the single
|
||||||
entry point for STT:
|
entry point for STT:
|
||||||
|
|
||||||
- If browser-Whisper is enabled in user settings (and supported), it
|
- It POSTs the Blob to `/api/transcribe` (handled by
|
||||||
runs locally — no network call, no PHI leaves the device.
|
|
||||||
- Otherwise it POSTs the Blob to `/api/transcribe` (handled by
|
|
||||||
`src/routes/transcribe.js` — out of scope here). The server picks
|
`src/routes/transcribe.js` — out of scope here). The server picks
|
||||||
whichever provider the admin configured: Gemini, AWS Transcribe
|
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
|
- On any failure with a non-empty Blob, the audio is auto-saved via
|
||||||
`saveAudioBackup(blob, 'failed-transcription')` (`audioBackup.js`)
|
`saveAudioBackup(blob, 'failed-transcription')` (`audioBackup.js`)
|
||||||
so the user can retry from the Settings page.
|
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
|
A startup probe (`checkTranscribeStatus`, `app.js:722`) queries
|
||||||
`/api/transcribe/status` and stores the result in
|
`/api/transcribe/status` and stores the result in
|
||||||
`window._transcribeAvailable`. If the server reports `false`, every tab
|
`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)`
|
- `suggestDontMiss(outputId, noteText, noteType, age, cc)`
|
||||||
(`app.js:894`) — POSTs to `/api/dont-miss` and mounts an
|
(`app.js:894`) — POSTs to `/api/dont-miss` and mounts an
|
||||||
orange-bordered card with up to 5 high-yield "don't miss" items.
|
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,
|
Refine and Shorten use the same pattern: `refineDocument(outputId,
|
||||||
inputId)` (`app.js:946`) and `shortenDocument(outputId)` (`app.js:971`)
|
inputId)` (`app.js:946`) and `shortenDocument(outputId)` (`app.js:971`)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# PedScribe Mobile App
|
# 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
|
## 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)
|
- Configurable server URL (supports self-hosted instances)
|
||||||
- Haptic feedback on recording start/stop
|
- Haptic feedback on recording start/stop
|
||||||
- Keep screen awake during recording
|
- 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.
|
stored in iOS Keychain / Android Keystore, gated by OS biometric.
|
||||||
Enrolled on first password sign-in (opt-in prompt). 2FA still applies
|
Enrolled on first password sign-in (opt-in prompt). 2FA still applies
|
||||||
on top — biometric replaces the password step only.
|
on top — biometric replaces the password step only.
|
||||||
- App Store and Play Store ready
|
- Android and iOS project scaffolds for store builds
|
||||||
|
|
||||||
## Prerequisites
|
## 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) {
|
function refineDocument(outputElementId, inputElementId) {
|
||||||
var doc = document.getElementById(outputElementId);
|
var doc = document.getElementById(outputElementId);
|
||||||
var input = document.getElementById(inputElementId);
|
var input = document.getElementById(inputElementId);
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,7 @@
|
||||||
outputCard.classList.remove('hidden');
|
outputCard.classList.remove('hidden');
|
||||||
outputCard.scrollIntoView({ behavior: 'smooth' });
|
outputCard.scrollIntoView({ behavior: 'smooth' });
|
||||||
showToast('Chart review generated!', 'success');
|
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);
|
if (typeof suggestBillingCodes === 'function') suggestBillingCodes('cr-review-text', data.review, 'chart', document.getElementById('cr-age').value);
|
||||||
} else showToast(data.error || 'Failed', 'error');
|
} else showToast(data.error || 'Failed', 'error');
|
||||||
})
|
})
|
||||||
|
|
@ -190,6 +191,7 @@
|
||||||
var outputCard = document.getElementById('cr-output');
|
var outputCard = document.getElementById('cr-output');
|
||||||
if (reviewText) reviewText.textContent = enc.generated_note;
|
if (reviewText) reviewText.textContent = enc.generated_note;
|
||||||
if (outputCard) outputCard.classList.remove('hidden');
|
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>';
|
'<div id="ed-final-note-text" class="output-text"></div>';
|
||||||
var t = container.querySelector('#ed-final-note-text');
|
var t = container.querySelector('#ed-final-note-text');
|
||||||
if (t) t.textContent = note;
|
if (t) t.textContent = note;
|
||||||
|
if (typeof attachPatientEducation === 'function') attachPatientEducation('ed-final-note-text', { patientAge: getVal('ed-age') });
|
||||||
container.classList.remove('hidden');
|
container.classList.remove('hidden');
|
||||||
container.scrollIntoView({ behavior: 'smooth' });
|
container.scrollIntoView({ behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,7 @@
|
||||||
clarifyOutput.classList.add('hidden');
|
clarifyOutput.classList.add('hidden');
|
||||||
outputCard.scrollIntoView({ behavior: 'smooth' });
|
outputCard.scrollIntoView({ behavior: 'smooth' });
|
||||||
showToast('Hospital course generated!', 'success');
|
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);
|
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');
|
} else showToast(data.error || 'Failed', 'error');
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,7 @@
|
||||||
outputCard.classList.remove('hidden');
|
outputCard.classList.remove('hidden');
|
||||||
outputCard.scrollIntoView({ behavior: 'smooth' });
|
outputCard.scrollIntoView({ behavior: 'smooth' });
|
||||||
showToast('HPI generated!', 'success');
|
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 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);
|
if (typeof suggestDontMiss === 'function') suggestDontMiss('enc-hpi-text', data.hpi, 'hpi', document.getElementById('enc-age').value);
|
||||||
} else showToast(data.error || 'Failed', 'error');
|
} else showToast(data.error || 'Failed', 'error');
|
||||||
|
|
|
||||||
|
|
@ -921,6 +921,7 @@
|
||||||
if (tag) tag.textContent = (data.model || '').split('/').pop();
|
if (tag) tag.textContent = (data.model || '').split('/').pop();
|
||||||
if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); }
|
if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); }
|
||||||
showToast('Well visit note generated!', 'success');
|
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);
|
if (typeof suggestBillingCodes === 'function') suggestBillingCodes('wv-note-text', data.note, 'wellvisit', document.getElementById('wv-note-age').value);
|
||||||
} else {
|
} else {
|
||||||
showToast(data.error || 'Generation failed', 'error');
|
showToast(data.error || 'Generation failed', 'error');
|
||||||
|
|
|
||||||
|
|
@ -335,6 +335,7 @@
|
||||||
if (tag) tag.textContent = (data.model || '').split('/').pop();
|
if (tag) tag.textContent = (data.model || '').split('/').pop();
|
||||||
if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); }
|
if (outCard) { outCard.classList.remove('hidden'); outCard.scrollIntoView({ behavior: 'smooth' }); }
|
||||||
showToast('Sick visit note generated!', 'success');
|
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 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);
|
if (typeof suggestDontMiss === 'function') suggestDontMiss('sick-note-text', data.note, 'sickvisit', document.getElementById('sick-age').value, document.getElementById('sick-cc').value);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,7 @@
|
||||||
outputCard.classList.remove('hidden');
|
outputCard.classList.remove('hidden');
|
||||||
outputCard.scrollIntoView({ behavior: 'smooth' });
|
outputCard.scrollIntoView({ behavior: 'smooth' });
|
||||||
showToast('SOAP note generated!', 'success');
|
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);
|
if (typeof suggestBillingCodes === 'function') suggestBillingCodes('soap-text', data.soap, 'soap', document.getElementById('soap-age').value);
|
||||||
} else showToast(data.error || 'Failed', 'error');
|
} else showToast(data.error || 'Failed', 'error');
|
||||||
})
|
})
|
||||||
|
|
@ -209,6 +210,7 @@
|
||||||
if (enc.generated_note) {
|
if (enc.generated_note) {
|
||||||
setOutputText(soapText, enc.generated_note);
|
setOutputText(soapText, enc.generated_note);
|
||||||
outputCard.classList.remove('hidden');
|
outputCard.classList.remove('hidden');
|
||||||
|
if (typeof attachPatientEducation === 'function') attachPatientEducation('soap-text', { patientAge: document.getElementById('soap-age').value });
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
var pd = JSON.parse(enc.partial_data || '{}');
|
var pd = JSON.parse(enc.partial_data || '{}');
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,7 @@
|
||||||
outputCard.classList.remove('hidden');
|
outputCard.classList.remove('hidden');
|
||||||
outputCard.scrollIntoView({ behavior: 'smooth' });
|
outputCard.scrollIntoView({ behavior: 'smooth' });
|
||||||
showToast('Generated!', 'success');
|
showToast('Generated!', 'success');
|
||||||
|
if (typeof attachPatientEducation === 'function') attachPatientEducation('dict-hpi-text', { patientAge: document.getElementById('dict-age').value });
|
||||||
} else showToast(data.error || 'Failed', 'error');
|
} else showToast(data.error || 'Failed', 'error');
|
||||||
})
|
})
|
||||||
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });
|
.catch(function(err) { hideBusy(); showToast(err.message, 'error'); });
|
||||||
|
|
@ -202,6 +203,7 @@
|
||||||
if (enc.generated_note) {
|
if (enc.generated_note) {
|
||||||
hpiText.textContent = enc.generated_note;
|
hpiText.textContent = enc.generated_note;
|
||||||
outputCard.classList.remove('hidden');
|
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/sickVisit'));
|
||||||
app.use('/api', require('./src/routes/edEncounters'));
|
app.use('/api', require('./src/routes/edEncounters'));
|
||||||
app.use('/api', require('./src/routes/dontMiss'));
|
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/user', require('./src/routes/userPreferences'));
|
||||||
app.use('/api/admin/learning', require('./src/routes/learningAI'));
|
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
|
- Items already clearly documented in the note
|
||||||
- Generic boilerplate ("consider differential diagnosis")
|
- Generic boilerplate ("consider differential diagnosis")
|
||||||
- Treatment recommendations (this is a documentation/clinical-reasoning prompt, not a treatment plan)
|
- 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 ────────────────────────────────────────────────────
|
// ── DB override support ────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue