feat: Initial Mediusa Clinic OS practice management platform ('Jane App But Better')

This commit is contained in:
2026-09-05 14:12:19 -07:00
parent 441e5eb57d
commit deb5197e53
17 changed files with 4675 additions and 71 deletions
+256
View File
@@ -0,0 +1,256 @@
'use client';
import React, { useState, useEffect } from 'react';
import { Mic, MicOff, Sparkles, Volume2, Play, Pause, RotateCcw, CheckCircle2, AlertCircle } from 'lucide-react';
import { SAMPLE_AUDIO_PRESETS } from '@/lib/mock-data';
interface AmbientAudioRecorderProps {
onApplyExtractedSoap: (soapData: {
vasScore: number;
subjective: string;
objective: string;
assessment: string;
plan: string;
spinalAdjustments: Array<{ vertebra: string; region: any; listing: string; technique: any; notes?: string }>;
icd10: string[];
cpt: string[];
}) => void;
}
export const AmbientAudioRecorder: React.FC<AmbientAudioRecorderProps> = ({
onApplyExtractedSoap,
}) => {
const [isRecording, setIsRecording] = useState(false);
const [recordingSeconds, setRecordingSeconds] = useState(0);
const [selectedPreset, setSelectedPreset] = useState(SAMPLE_AUDIO_PRESETS[0]);
const [isProcessingAI, setIsProcessingAI] = useState(false);
const [liveTranscript, setLiveTranscript] = useState('');
const [showAppliedToast, setShowAppliedToast] = useState(false);
// Timer simulation
useEffect(() => {
let interval: any = null;
if (isRecording) {
interval = setInterval(() => {
setRecordingSeconds((prev) => prev + 1);
}, 1000);
} else {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [isRecording]);
const toggleRecording = () => {
if (!isRecording) {
setIsRecording(true);
setRecordingSeconds(0);
setLiveTranscript('Listening to ambient doctor-patient dialogue in operatory...');
// Simulate live ambient audio transcription
setTimeout(() => {
setLiveTranscript(selectedPreset.audioTranscript);
}, 2500);
} else {
setIsRecording(false);
}
};
const handleSelectPreset = (preset: typeof SAMPLE_AUDIO_PRESETS[0]) => {
setSelectedPreset(preset);
if (!isRecording) {
setLiveTranscript(preset.audioTranscript);
}
};
const handleSynthesizeSoap = () => {
setIsProcessingAI(true);
setTimeout(() => {
setIsProcessingAI(false);
onApplyExtractedSoap({
vasScore: selectedPreset.extractedSOAP.vasScore,
subjective: selectedPreset.extractedSOAP.subjective,
objective: selectedPreset.extractedSOAP.objective,
assessment: selectedPreset.extractedSOAP.assessment,
plan: selectedPreset.extractedSOAP.plan,
spinalAdjustments: selectedPreset.extractedSOAP.spinalAdjustments as any,
icd10: selectedPreset.extractedSOAP.icd10,
cpt: selectedPreset.extractedSOAP.cpt,
});
setShowAppliedToast(true);
setTimeout(() => setShowAppliedToast(false), 4000);
}, 1200);
};
const formatTime = (secs: number) => {
const mins = Math.floor(secs / 60);
const remaining = secs % 60;
return `${mins.toString().padStart(2, '0')}:${remaining.toString().padStart(2, '0')}`;
};
return (
<div className="bg-gradient-to-br from-slate-900 via-slate-900 to-teal-950/40 border border-teal-500/30 rounded-2xl p-5 shadow-2xl text-slate-100">
{/* Top Banner */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-4 border-b border-slate-800">
<div>
<div className="flex items-center gap-2">
<span className="p-1.5 rounded-lg bg-teal-500/20 text-teal-400 border border-teal-500/30">
<Sparkles className="w-4 h-4" />
</span>
<h3 className="font-semibold text-white text-base tracking-tight">
Mediusa Ambient Clinical AI Scribe
</h3>
<span className="text-[11px] font-semibold px-2 py-0.5 rounded-full bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">
Powered by gpt-5.4-mini Clinical Engine
</span>
</div>
<p className="text-xs text-slate-400 mt-1">
Zero typing required. Scribe listens to ambient doctor-patient encounter and converts conversational dialogue into compliant SOAP + CPT/ICD-10 codes.
</p>
</div>
{/* Recording Status / Mic Button */}
<div className="flex items-center gap-3">
{isRecording && (
<div className="flex items-center gap-2 text-xs font-mono font-semibold text-red-400 animate-pulse">
<span className="w-2.5 h-2.5 rounded-full bg-red-500" />
LIVE {formatTime(recordingSeconds)}
</div>
)}
<button
type="button"
onClick={toggleRecording}
className={`px-4 py-2 rounded-xl text-xs font-bold flex items-center gap-2 transition-all shadow-lg ${
isRecording
? 'bg-red-500/20 text-red-400 border border-red-500/50 hover:bg-red-500/30'
: 'bg-teal-500 hover:bg-teal-400 text-slate-950 shadow-teal-500/20'
}`}
>
{isRecording ? (
<>
<MicOff className="w-4 h-4" />
Stop Listening
</>
) : (
<>
<Mic className="w-4 h-4" />
Start Ambient Scribe
</>
)}
</button>
</div>
</div>
{/* Waveform Graphic Animation */}
<div className="mt-4 p-4 bg-slate-950/60 rounded-xl border border-slate-800/80">
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider flex items-center gap-1.5">
<Volume2 className="w-3.5 h-3.5 text-teal-400" />
Operatory Ambient Stream
</span>
<span className="text-[10px] text-slate-500 font-mono">
44.1kHz 24-bit HIPAA Speech Encryption
</span>
</div>
{/* Dynamic Animated Bars */}
<div className="h-10 flex items-center justify-center gap-1">
{[
12, 28, 45, 18, 62, 85, 40, 92, 70, 30, 80, 65, 45, 90, 35, 75, 55, 20, 68, 88,
42, 78, 52, 22, 60, 95, 38, 72, 58, 25, 48, 82, 32, 64, 44, 15,
].map((height, i) => (
<div
key={i}
className={`w-1 rounded-full transition-all duration-150 ${
isRecording
? 'bg-gradient-to-t from-teal-500 to-emerald-300 animate-pulse'
: 'bg-slate-800'
}`}
style={{
height: isRecording ? `${Math.max(10, (height * (1 + (i % 3) * 0.2)) % 40)}px` : '6px',
animationDelay: `${(i * 40) % 600}ms`,
}}
/>
))}
</div>
</div>
{/* Preset Encounter Selector & Live Transcript View */}
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 mt-4">
{/* Left 4 Cols: Presets */}
<div className="md:col-span-4 flex flex-col gap-2">
<label className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
Clinical Encounter Presets (1-Click Demo)
</label>
<div className="space-y-2">
{SAMPLE_AUDIO_PRESETS.map((preset) => (
<button
key={preset.id}
type="button"
onClick={() => handleSelectPreset(preset)}
className={`w-full text-left p-2.5 rounded-xl border transition text-xs ${
selectedPreset.id === preset.id
? 'bg-slate-800 border-teal-500/60 text-white shadow-sm'
: 'bg-slate-900/60 border-slate-800 text-slate-400 hover:bg-slate-800/40 hover:text-slate-200'
}`}
>
<div className="font-semibold text-slate-200 truncate">{preset.title}</div>
<div className="text-[10px] text-teal-400 mt-0.5">Encounter Duration: {preset.duration}</div>
</button>
))}
</div>
<button
type="button"
onClick={handleSynthesizeSoap}
disabled={isProcessingAI}
className="mt-3 w-full py-2.5 px-4 rounded-xl text-xs font-bold bg-gradient-to-r from-teal-500 to-emerald-500 hover:from-teal-400 hover:to-emerald-400 text-slate-950 shadow-lg shadow-teal-500/20 transition flex items-center justify-center gap-2 disabled:opacity-50"
>
{isProcessingAI ? (
<>
<span className="w-3.5 h-3.5 border-2 border-slate-950 border-t-transparent rounded-full animate-spin" />
AI Extracting SOAP...
</>
) : (
<>
<Sparkles className="w-4 h-4 text-slate-950" />
Extract SOAP &amp; Auto-Codes
</>
)}
</button>
</div>
{/* Right 8 Cols: Dialogue Transcript */}
<div className="md:col-span-8 bg-slate-950/70 border border-slate-800 rounded-xl p-3.5 flex flex-col justify-between">
<div>
<div className="flex items-center justify-between pb-2 border-b border-slate-800 text-xs">
<span className="font-semibold text-slate-300">Encounter Dialogue Feed</span>
<span className="text-[10px] text-teal-400/90 font-mono">Live Whisper Stream</span>
</div>
<p className="text-xs text-slate-300 whitespace-pre-line mt-2 font-mono leading-relaxed max-h-[160px] overflow-y-auto pr-1">
{liveTranscript || selectedPreset.audioTranscript}
</p>
</div>
<div className="mt-3 pt-2 border-t border-slate-800/80 flex items-center justify-between text-[11px] text-slate-400">
<span className="flex items-center gap-1 text-teal-400">
<CheckCircle2 className="w-3.5 h-3.5" />
Auto-detects Subluxations, VAS score, CPT 98940, ICD-10 M99.0x
</span>
<span className="text-slate-500">HIPAA BAA Compliant</span>
</div>
</div>
</div>
{showAppliedToast && (
<div className="mt-3 p-3 bg-emerald-500/20 border border-emerald-500/40 rounded-xl text-xs text-emerald-300 flex items-center justify-between animate-in fade-in slide-in-from-top-2 duration-200">
<div className="flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-emerald-400" />
<span>
<strong>Success!</strong> Encounter conversation extracted into Subjective, Objective, Assessment, Plan, Spine segments, and Billing codes.
</span>
</div>
</div>
)}
</div>
);
};