From d9fd4fcd6939955bb76eb64eebc056b4c263e0d9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 23 Apr 2026 22:19:06 +0200 Subject: [PATCH] feat(client): port Encounter HPI, SOAP, Sick Visit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three more tabs behind /app/*: /encounter → Encounter.tsx — POST /api/generate-hpi-encounter /soap → Soap.tsx — POST /api/generate-soap (full or subjective-only output type) /sickvisit → SickVisit.tsx — POST /api/sick-visit/note (chief complaint is the only required field; transcript optional) All three share the same skeleton: demographic triad + textarea + Zod-validated submit + copy-to-clipboard output pane. Follow-up work per tab (save/load, refine, audio capture) lands in subsequent commits — this first pass proves the AI generate path for each. Layout sidebar updated: Encounter HPI / SOAP Note / Sick Visit now marked available. Pending stubs remain for Hospital Course, Chart Review, Well Visit, and all Clinical Tools + Settings. Build: 361 kB / 109 kB gzipped (+11 kB over previous, as expected for three small pages using the existing lib/api + shared schemas). Typecheck + vite build both green on the client side. --- client/src/App.tsx | 13 +- client/src/components/Layout.tsx | 6 +- client/src/pages/Encounter.tsx | 112 +++++++++++++++++ client/src/pages/SickVisit.tsx | 101 +++++++++++++++ client/src/pages/Soap.tsx | 119 ++++++++++++++++++ public/app/assets/index-B2vc_21R.css | 2 - .../{index-BmpHzFRb.js => index-CJTSwzwK.js} | 12 +- public/app/assets/index-Cge-0uqv.css | 2 + public/app/index.html | 4 +- 9 files changed, 356 insertions(+), 15 deletions(-) create mode 100644 client/src/pages/Encounter.tsx create mode 100644 client/src/pages/SickVisit.tsx create mode 100644 client/src/pages/Soap.tsx delete mode 100644 public/app/assets/index-B2vc_21R.css rename public/app/assets/{index-BmpHzFRb.js => index-CJTSwzwK.js} (63%) create mode 100644 public/app/assets/index-Cge-0uqv.css diff --git a/client/src/App.tsx b/client/src/App.tsx index 4502774..6e43c49 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -4,6 +4,9 @@ import Layout from '@/components/Layout'; import Extensions from '@/pages/Extensions'; import Faq from '@/pages/Faq'; import Dictation from '@/pages/Dictation'; +import Encounter from '@/pages/Encounter'; +import Soap from '@/pages/Soap'; +import SickVisit from '@/pages/SickVisit'; const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 30_000, retry: 1 } }, @@ -19,8 +22,11 @@ function Home() {

Ported tabs so far:

@@ -34,8 +40,11 @@ export default function App() { }> } /> - } /> + } /> } /> + } /> + } /> + } /> } /> {/* catch-all falls back to home while more tabs port over */} } /> diff --git a/client/src/components/Layout.tsx b/client/src/components/Layout.tsx index dfb4a3d..57f53c5 100644 --- a/client/src/components/Layout.tsx +++ b/client/src/components/Layout.tsx @@ -22,7 +22,7 @@ const NAV: NavGroup[] = [ { label: 'Encounters', items: [ - { to: '/encounter', label: 'Encounter HPI', available: false }, + { to: '/encounter', label: 'Encounter HPI', available: true }, { to: '/dictation', label: 'Dictation HPI', available: true }, ], }, @@ -31,9 +31,9 @@ const NAV: NavGroup[] = [ items: [ { to: '/hospital', label: 'Hospital Course', available: false }, { to: '/chart', label: 'Chart Review', available: false }, - { to: '/soap', label: 'SOAP Note', available: false }, + { to: '/soap', label: 'SOAP Note', available: true }, { to: '/wellvisit', label: 'Well Visit', available: false }, - { to: '/sickvisit', label: 'Sick Visit', available: false }, + { to: '/sickvisit', label: 'Sick Visit', available: true }, ], }, { diff --git a/client/src/pages/Encounter.tsx b/client/src/pages/Encounter.tsx new file mode 100644 index 0000000..c6710e8 --- /dev/null +++ b/client/src/pages/Encounter.tsx @@ -0,0 +1,112 @@ +// ============================================================ +// ENCOUNTER — live encounter → HPI via /api/generate-hpi-encounter +// Minimum-viable port: same shape as Dictation (same endpoint family). +// Audio capture + save/load + refine deferred to follow-up commits. +// ============================================================ + +import { useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { api, ApiError } from '@/lib/api'; +import type { HpiOk } from '@/shared/types'; +import { HpiEncounterRequestSchema, type HpiEncounterRequest } from '@/shared/schemas'; + +type Setting = 'outpatient' | 'inpatient'; + +export default function Encounter() { + const [patientAge, setPatientAge] = useState(''); + const [patientGender, setPatientGender] = useState(''); + const [setting, setSetting] = useState('outpatient'); + const [transcript, setTranscript] = useState(''); + const [result, setResult] = useState(null); + const [validationError, setValidationError] = useState(null); + + const generate = useMutation({ + mutationFn: (body) => api.post('/api/generate-hpi-encounter', body), + onSuccess: (data) => setResult(data.hpi), + }); + + function submit(e: React.FormEvent) { + e.preventDefault(); + setValidationError(null); + const body: HpiEncounterRequest = { transcript, patientAge, patientGender, setting }; + const parsed = HpiEncounterRequestSchema.safeParse(body); + if (!parsed.success) { + setValidationError(parsed.error.issues.map((i: { message: string }) => i.message).join(', ')); + return; + } + setResult(null); + generate.mutate(parsed.data); + } + + const input = 'w-full rounded-md border border-input bg-background px-3 py-2 text-sm'; + + return ( +
+
+

Live Encounter → HPI

+

+ Record or paste an encounter transcript; generate a structured HPI. +

+
+ +
+
+ + + +
+ +