From bfe527fb3357d80d3a54e1f19097e7d606678248 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sat, 5 Sep 2026 15:44:58 -0700 Subject: [PATCH] feat: polish UX +5% with Cmd+K Command Palette, Web Audio tactile feedback, Patient Clinical Vitals HUD, Fast Macros & Toasts --- src/app/page.tsx | 160 ++++++++- src/components/charting/SoapChartEditor.tsx | 28 +- src/components/ui/ClinicalToast.tsx | 59 +++ src/components/ui/CommandPalette.tsx | 375 ++++++++++++++++++++ src/components/ui/PatientClinicalHud.tsx | 344 ++++++++++++++++++ src/components/ui/SpineVisualizer.tsx | 68 +++- src/lib/clinical-audio.ts | 126 +++++++ src/lib/mock-data.ts | 40 +++ src/types/clinical.ts | 12 + 9 files changed, 1191 insertions(+), 21 deletions(-) create mode 100644 src/components/ui/ClinicalToast.tsx create mode 100644 src/components/ui/CommandPalette.tsx create mode 100644 src/components/ui/PatientClinicalHud.tsx create mode 100644 src/lib/clinical-audio.ts diff --git a/src/app/page.tsx b/src/app/page.tsx index f796d84..68f3e66 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -35,6 +35,9 @@ import { RetailInventoryView } from '@/components/pos/RetailInventoryView'; import { MembershipsView } from '@/components/memberships/MembershipsView'; import { TelehealthRoom } from '@/components/telehealth/TelehealthRoom'; import { WaitlistModal } from '@/components/waitlist/WaitlistModal'; +import { CommandPalette } from '@/components/ui/CommandPalette'; +import { ClinicalToastContainer, ToastMessage } from '@/components/ui/ClinicalToast'; +import { clinicalAudio } from '@/lib/clinical-audio'; import { Calendar, FileText, @@ -51,6 +54,10 @@ import { Shield, ShieldAlert, UserCheck, + Search, + Volume2, + VolumeX, + Sparkles, } from 'lucide-react'; export default function Home() { @@ -78,6 +85,11 @@ export default function Home() { const [waitlist, setWaitlist] = useState(INITIAL_WAITLIST); const [isWaitlistOpen, setIsWaitlistOpen] = useState(false); + // Polish state: Command Palette, Toasts & Audio + const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); + const [toasts, setToasts] = useState([]); + const [audioEnabled, setAudioEnabled] = useState(true); + const [activePatient, setActivePatient] = useState(INITIAL_PATIENTS[0]); const [activeSoapNote, setActiveSoapNote] = useState(INITIAL_SOAP_NOTES[0]); const [activeSuperbill, setActiveSuperbill] = useState(INITIAL_SUPERBILLS[0]); @@ -86,7 +98,52 @@ export default function Home() { const activeProviders = INITIAL_PROVIDERS.filter((p) => p.tenantId === activeTenant.id); const activeProvider = activeProviders[0] || INITIAL_PROVIDERS[0]; + // Toast notification helper + const addToast = (type: 'success' | 'alert' | 'info', title: string, description?: string) => { + const id = `toast-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`; + const newToast: ToastMessage = { + id, + type, + title, + description, + timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }), + }; + setToasts((prev) => [...prev, newToast]); + setTimeout(() => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, 4500); + }; + + const handleDismissToast = (id: string) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }; + + // Keyboard shortcut listener for Cmd+K / Ctrl+K + React.useEffect(() => { + const handleGlobalKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault(); + setIsCommandPaletteOpen((prev) => !prev); + } + }; + window.addEventListener('keydown', handleGlobalKeyDown); + return () => window.removeEventListener('keydown', handleGlobalKeyDown); + }, []); + + const handleToggleAudio = () => { + const next = !audioEnabled; + setAudioEnabled(next); + clinicalAudio.setEnabled(next); + if (next) { + clinicalAudio.playSuccess(); + addToast('info', 'Tactile Audio Active', 'Synthesized Web Audio clicks & chimes enabled'); + } else { + addToast('info', 'Tactile Audio Muted', 'Sound effects silenced'); + } + }; + const handleSelectAppointment = (apt: Appointment) => { + clinicalAudio.playClick(); const patientMatch = patients.find((p) => p.id === apt.patientId) || patients[0]; setActivePatient(patientMatch); @@ -101,12 +158,15 @@ export default function Home() { }; const handleUpdateAppointmentStatus = (aptId: string, status: Appointment['status']) => { + clinicalAudio.playClick(); setAppointments((prev) => prev.map((a) => (a.id === aptId ? { ...a, status } : a)) ); + addToast('info', 'Encounter Status Updated', `Appointment transitioned to ${status}`); }; const handleSaveSoapNote = (note: SoapNote) => { + clinicalAudio.playSuccess(); setSoapNotes((prev) => { const idx = prev.findIndex((s) => s.id === note.id); if (idx >= 0) { @@ -117,9 +177,11 @@ export default function Home() { return [...prev, note]; }); setActiveSoapNote(note); + addToast('success', 'Chart Synced to HIPAA Vault', `${note.patientName} • ${note.status === 'signed' ? 'Signed & Locked' : 'Draft Saved'}`); }; const handleGenerateSuperbillFromNote = (note: SoapNote) => { + clinicalAudio.playSuccess(); const newSb: Superbill = { id: `sb-${Date.now()}`, tenantId: activeTenant.id, @@ -153,9 +215,11 @@ export default function Home() { setSuperbills((prev) => [newSb, ...prev]); setActiveSuperbill(newSb); setClinicTab('billing'); + addToast('success', 'Superbill Claim Created', `${newSb.invoiceNumber} • $${newSb.totalAmount.toFixed(2)} Fee Schedule`); }; const handleMarkPaid = (superbillId: string, method: Superbill['paymentMethod']) => { + clinicalAudio.playSuccess(); setSuperbills((prev) => prev.map((sb) => sb.id === superbillId @@ -171,9 +235,11 @@ export default function Home() { paymentMethod: method, })); } + addToast('success', 'Stripe Payment Captured', `Receipt processed via ${method}`); }; const handlePatientBookingComplete = (newAptData: any) => { + clinicalAudio.playSuccess(); const newApt: Appointment = { id: `apt-${Date.now()}`, tenantId: activeTenant.id, @@ -181,34 +247,44 @@ export default function Home() { ...newAptData, }; setAppointments((prev) => [newApt, ...prev]); + addToast('success', 'Intake & Booking Confirmed', `${newApt.patientName} scheduled for ${newApt.time}`); }; const handleAddTenant = (newTenant: ClinicTenant) => { setTenants((prev) => [...prev, newTenant]); + addToast('success', 'New Clinic Deployed', `${newTenant.name} onboarded to Mediusa OS`); }; const handleSwitchTenant = (tenantId: string) => { + clinicalAudio.playClick(); setActiveTenantId(tenantId); setPortalMode('clinic'); setClinicTab('calendar'); }; const handleUpdateProductStock = (productId: string, newStock: number) => { + clinicalAudio.playClick(); setProducts((prev) => prev.map((p) => (p.id === productId ? { ...p, stockQty: newStock } : p)) ); + addToast('info', 'POS Inventory Updated', `Stock quantity adjusted to ${newStock}`); }; const handleEnrollPatientInMembership = (patientId: string, planName: string) => { + clinicalAudio.playSuccess(); setPatients((prev) => prev.map((p) => (p.id === patientId ? { ...p, activeMembership: planName } : p)) ); + addToast('success', 'Membership Enrolled', `Patient subscribed to ${planName}`); }; const handleAutoFillWaitlistPatient = (entryId: string) => { + clinicalAudio.playPing(); + const entry = waitlist.find((w) => w.id === entryId); setWaitlist((prev) => prev.map((w) => (w.id === entryId ? { ...w, status: 'notified' } : w)) ); + addToast('alert', 'Waitlist SMS Dispatched', `Slot opening sent to ${entry?.patientName || 'patient'}`); }; return ( @@ -226,7 +302,7 @@ export default function Home() {
MEDIUSA CLINIC OS - CLINICAL v2.4 + CLINICAL v2.5
@@ -236,6 +312,36 @@ export default function Home() {
+ {/* Quick Command Palette Trigger (Cmd+K) */} + + + {/* Audio Toggle Button */} + + {/* Clinic Dropdown */}
{ + setQuery(e.target.value); + setSelectedIndex(0); + }} + placeholder="Type a patient name, clinical command, or view... (Cmd+K)" + className="w-full text-sm text-slate-800 placeholder-slate-400 bg-transparent border-none focus:outline-none focus:ring-0" + /> + + ESC + +
+ + {/* Results List */} +
+ {filtered.length === 0 ? ( +
+ No matching patients or clinical commands found for “{query}” +
+ ) : ( + filtered.map((item, idx) => { + const isSelected = idx === selectedIndex; + return ( + + ); + }) + )} +
+ + {/* Footer shortcuts helper */} +
+
+ + + + Navigate + + + + Select + +
+
+ Mediusa Quick Command Hub +
+
+ + + ); +}; diff --git a/src/components/ui/PatientClinicalHud.tsx b/src/components/ui/PatientClinicalHud.tsx new file mode 100644 index 0000000..170315e --- /dev/null +++ b/src/components/ui/PatientClinicalHud.tsx @@ -0,0 +1,344 @@ +'use client'; + +import React, { useState } from 'react'; +import { Patient, PatientVitals } from '@/types/clinical'; +import { + Activity, + Heart, + Wind, + Flame, + AlertTriangle, + ShieldCheck, + CreditCard, + Edit2, + Check, + X, + ChevronUp, + ChevronDown, +} from 'lucide-react'; +import { clinicalAudio } from '@/lib/clinical-audio'; + +interface PatientClinicalHudProps { + patient: Patient; + onUpdateVitals?: (patientId: string, newVitals: PatientVitals) => void; +} + +export const PatientClinicalHud: React.FC = ({ + patient, + onUpdateVitals, +}) => { + const [isExpanded, setIsExpanded] = useState(true); + const [isEditingVitals, setIsEditingVitals] = useState(false); + + const vitals = patient.vitals || { + bloodPressure: '120/80', + heartRate: 72, + temperature: '98.6 °F', + oxygenSat: 99, + painLevel: 4, + bmi: '23.5', + allergies: [], + contraindications: [], + }; + + const [editBp, setEditBp] = useState(vitals.bloodPressure); + const [editHr, setEditHr] = useState(vitals.heartRate); + const [editO2, setEditO2] = useState(vitals.oxygenSat); + const [editPain, setEditPain] = useState(vitals.painLevel); + + const handleSaveVitals = () => { + clinicalAudio.playSuccess(); + if (onUpdateVitals) { + onUpdateVitals(patient.id, { + ...vitals, + bloodPressure: editBp, + heartRate: Number(editHr), + oxygenSat: Number(editO2), + painLevel: Number(editPain), + }); + } + setIsEditingVitals(false); + }; + + const carePlanProgress = Math.round( + (patient.carePlan.completedVisits / patient.carePlan.totalVisits) * 100 + ); + + return ( +
+ {/* Top Banner Row */} +
+
+
+ {patient.firstName[0]} + {patient.lastName[0]} +
+ +
+
+

+ {patient.firstName} {patient.lastName} +

+ + MRN-{patient.id.toUpperCase()} + + + DOB: {patient.dob} ({patient.gender}) + +
+

+ Chief Complaint:{' '} + {patient.chiefComplaint} +

+
+
+ +
+ {/* Active Membership Badge */} + {patient.activeMembership && ( +
+ + {patient.activeMembership} + {patient.packageCreditsRemaining !== undefined && ( + + {patient.packageCreditsRemaining} left + + )} +
+ )} + + {/* Insurance Pill */} +
+ + {patient.insuranceName} +
+ + +
+
+ + {/* Expanded Clinical Data Row */} + {isExpanded && ( +
+ {/* Left: Vitals Gauges (Col 5) */} +
+ {/* BP */} +
+ +
+ + BP + + + {vitals.bloodPressure}{' '} + mmHg + +
+
+ + {/* Heart Rate */} +
+ +
+ + HR + + + {vitals.heartRate}{' '} + bpm + +
+
+ + {/* SpO2 */} +
+ +
+ + SpO₂ + + + {vitals.oxygenSat}% + +
+
+ + {/* Pain Scale */} +
+ +
+ + Pain VAS + + + {vitals.painLevel}/10 + +
+
+ + +
+ + {/* Center: Care Plan Progress (Col 3) */} +
+
+ Care Plan Cadence + + Visit {patient.carePlan.completedVisits} of {patient.carePlan.totalVisits} + +
+
+
+
+

+ {patient.carePlan.frequency} • {patient.carePlan.targetCondition} +

+
+ + {/* Right: Clinical Red Flags & Contraindications (Col 4) */} +
+
+ + Clinical Precautions & Allergies +
+
+ {vitals.contraindications.map((c, i) => ( + + {c} + + ))} + {vitals.allergies.length > 0 ? ( + vitals.allergies.map((a, i) => ( + + Allergy: {a} + + )) + ) : ( + No drug allergies listed + )} +
+
+
+ )} + + {/* Quick Edit Vitals Modal */} + {isEditingVitals && ( +
+
+
+
+ +

Update Clinical Vitals

+
+ +
+ +
+
+ + setEditBp(e.target.value)} + placeholder="120/80" + className="w-full px-3 py-2 border border-slate-300 rounded-lg text-slate-800 focus:outline-none focus:ring-2 focus:ring-sky-500" + /> +
+ +
+
+ + setEditHr(Number(e.target.value))} + className="w-full px-3 py-2 border border-slate-300 rounded-lg text-slate-800 focus:outline-none focus:ring-2 focus:ring-sky-500" + /> +
+
+ + setEditO2(Number(e.target.value))} + className="w-full px-3 py-2 border border-slate-300 rounded-lg text-slate-800 focus:outline-none focus:ring-2 focus:ring-sky-500" + /> +
+
+ +
+ +
+ setEditPain(Number(e.target.value))} + className="w-full accent-amber-600" + /> + {editPain}/10 +
+
+
+ +
+ + +
+
+
+ )} +
+ ); +}; diff --git a/src/components/ui/SpineVisualizer.tsx b/src/components/ui/SpineVisualizer.tsx index 32ec3f9..e54003e 100644 --- a/src/components/ui/SpineVisualizer.tsx +++ b/src/components/ui/SpineVisualizer.tsx @@ -3,6 +3,7 @@ import React, { useState } from 'react'; import { SpinalAdjustmentEntry } from '@/types/clinical'; import { Check, X, Zap, Activity } from 'lucide-react'; +import { clinicalAudio } from '@/lib/clinical-audio'; interface SpineVisualizerProps { adjustments: SpinalAdjustmentEntry[]; @@ -93,6 +94,7 @@ export const SpineVisualizer: React.FC = ({ const handleOpenModal = (v: VertebraDef) => { if (readOnly) return; + clinicalAudio.playClick(); const existing = getAdjustment(v.id); if (existing) { setActiveListing(existing.listing); @@ -106,6 +108,7 @@ export const SpineVisualizer: React.FC = ({ const handleSaveListing = () => { if (!selectedVertebra) return; + clinicalAudio.playSuccess(); onToggleAdjustment({ vertebra: selectedVertebra.id, region: selectedVertebra.region, @@ -117,6 +120,7 @@ export const SpineVisualizer: React.FC = ({ }; const handleRemoveListing = (vertebraId: string) => { + clinicalAudio.playClick(); const existing = getAdjustment(vertebraId); if (existing) { onToggleAdjustment(existing); @@ -288,13 +292,28 @@ export const SpineVisualizer: React.FC = ({ {/* Quick Subluxation Preset Macros */} {!readOnly && (
-
- Clinical Fast Macros +
+ + Clinical Fast Macros + + {adjustments.length > 0 && ( + + )}
+ +
diff --git a/src/lib/clinical-audio.ts b/src/lib/clinical-audio.ts new file mode 100644 index 0000000..3674afc --- /dev/null +++ b/src/lib/clinical-audio.ts @@ -0,0 +1,126 @@ +// Web Audio API Synthesizer for Clinical Tactile Audio Feedback +// Provides zero-latency, zero-asset medical UI sound effects + +class ClinicalAudioManager { + private audioCtx: AudioContext | null = null; + private enabled: boolean = true; + + constructor() { + // Lazy init audio context on first user interaction + if (typeof window !== 'undefined') { + const stored = localStorage.getItem('mediusa_sound_enabled'); + if (stored !== null) { + this.enabled = stored === 'true'; + } + } + } + + public isEnabled(): boolean { + return this.enabled; + } + + public setEnabled(val: boolean) { + this.enabled = val; + if (typeof window !== 'undefined') { + localStorage.setItem('mediusa_sound_enabled', String(val)); + } + } + + private getContext(): AudioContext | null { + if (!this.enabled) return null; + if (typeof window === 'undefined') return null; + + if (!this.audioCtx) { + const AudioContextClass = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext; + if (AudioContextClass) { + this.audioCtx = new AudioContextClass(); + } + } + + if (this.audioCtx && this.audioCtx.state === 'suspended') { + this.audioCtx.resume().catch(() => {}); + } + + return this.audioCtx; + } + + // Soft tactile click for spine segment selection, tabs, buttons + public playClick() { + const ctx = this.getContext(); + if (!ctx) return; + + try { + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + + osc.type = 'sine'; + osc.frequency.setValueAtTime(800, ctx.currentTime); + osc.frequency.exponentialRampToValueAtTime(300, ctx.currentTime + 0.04); + + gain.gain.setValueAtTime(0.08, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.04); + + osc.connect(gain); + gain.connect(ctx.destination); + + osc.start(); + osc.stop(ctx.currentTime + 0.04); + } catch { + // Audio context might be restricted before interaction + } + } + + // Two-tone harmonic chime for chart saving, Stripe payment, or sign-off + public playSuccess() { + const ctx = this.getContext(); + if (!ctx) return; + + try { + const now = ctx.currentTime; + const notes = [523.25, 659.25]; // C5 to E5 + + notes.forEach((freq, idx) => { + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, now + idx * 0.08); + + gain.gain.setValueAtTime(0.09, now + idx * 0.08); + gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.08 + 0.25); + + osc.connect(gain); + gain.connect(ctx.destination); + + osc.start(now + idx * 0.08); + osc.stop(now + idx * 0.08 + 0.26); + }); + } catch {} + } + + // Gentle medical ping for waitlist SMS dispatch, notification + public playPing() { + const ctx = this.getContext(); + if (!ctx) return; + + try { + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + + osc.type = 'triangle'; + osc.frequency.setValueAtTime(440, ctx.currentTime); + osc.frequency.exponentialRampToValueAtTime(880, ctx.currentTime + 0.12); + + gain.gain.setValueAtTime(0.08, ctx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.18); + + osc.connect(gain); + gain.connect(ctx.destination); + + osc.start(); + osc.stop(ctx.currentTime + 0.18); + } catch {} + } +} + +export const clinicalAudio = new ClinicalAudioManager(); diff --git a/src/lib/mock-data.ts b/src/lib/mock-data.ts index 6d33d02..ccf68a6 100644 --- a/src/lib/mock-data.ts +++ b/src/lib/mock-data.ts @@ -147,6 +147,16 @@ export const INITIAL_PATIENTS: Patient[] = [ status: 'active', activeMembership: 'Chiropractic Wellness Club', packageCreditsRemaining: 4, + vitals: { + bloodPressure: '118/76', + heartRate: 68, + temperature: '98.4 °F', + oxygenSat: 99, + painLevel: 5, + bmi: '23.8', + allergies: ['Penicillin', 'Latex'], + contraindications: ['Prior L4-L5 Microdiscectomy (2021)', 'Avoid aggressive lumbo-pelvic torsion'], + }, carePlan: { title: 'Lumbar Disc Decompression & Stabilization Protocol', totalVisits: 12, @@ -175,6 +185,16 @@ export const INITIAL_PATIENTS: Patient[] = [ daysSinceLastVisit: 18, status: 'dropout_risk', packageCreditsRemaining: 0, + vitals: { + bloodPressure: '124/82', + heartRate: 74, + temperature: '98.6 °F', + oxygenSat: 98, + painLevel: 7, + bmi: '22.1', + allergies: ['Sulfa drugs'], + contraindications: ['Hypermobile C4-C5 segment', 'Prefers gentle Activator on cervical spine'], + }, carePlan: { title: 'Cervical Postural Realignment & Headache Relief', totalVisits: 8, @@ -203,6 +223,16 @@ export const INITIAL_PATIENTS: Patient[] = [ daysSinceLastVisit: 1, status: 'active', activeMembership: 'Athletic Recovery & Decompression Pass', + vitals: { + bloodPressure: '120/78', + heartRate: 64, + temperature: '98.2 °F', + oxygenSat: 99, + painLevel: 3, + bmi: '25.4', + allergies: [], + contraindications: ['Mild right rotator cuff impingement'], + }, carePlan: { title: 'Thoracic Mobility & Biomechanical Alignment', totalVisits: 6, @@ -230,6 +260,16 @@ export const INITIAL_PATIENTS: Patient[] = [ nextAppointmentDate: '2026-09-08', daysSinceLastVisit: 4, status: 'active', + vitals: { + bloodPressure: '112/70', + heartRate: 78, + temperature: '98.7 °F', + oxygenSat: 100, + painLevel: 4, + bmi: '27.2', + allergies: ['Amoxicillin'], + contraindications: ['Pregnancy Week 32 - Webster technique prone pregnancy pillow required'], + }, carePlan: { title: 'Webster Technique Pelvic Balance & Comfort', totalVisits: 10, diff --git a/src/types/clinical.ts b/src/types/clinical.ts index 8ed7d9c..ce34b24 100644 --- a/src/types/clinical.ts +++ b/src/types/clinical.ts @@ -46,6 +46,17 @@ export interface CarePlan { status: 'on_track' | 'lagging' | 'at_risk' | 'completed'; } +export interface PatientVitals { + bloodPressure: string; + heartRate: number; + temperature: string; + oxygenSat: number; + painLevel: number; + bmi: string; + allergies: string[]; + contraindications: string[]; +} + export interface Patient { id: string; tenantId: string; @@ -66,6 +77,7 @@ export interface Patient { daysSinceLastVisit: number; activeMembership?: string; packageCreditsRemaining?: number; + vitals?: PatientVitals; } export type AppointmentStatus = 'booked' | 'confirmed' | 'arrived' | 'in_room' | 'completed' | 'no_show';