feat: Initial Mediusa Clinic OS practice management platform ('Jane App But Better')
This commit is contained in:
@@ -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 & 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user