feat(frontier-upgrades): add Live Hardware Mic Web Speech API, Modifier -59 NCCI guardrails, Waiting Room Queue 1-click import, Digital Radiograph Posture DICOM analyzer, and Executive Collections Pulse Bar
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Mic, MicOff, Sparkles, Volume2, CheckCircle2, ShieldCheck } from 'lucide-react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Mic, MicOff, Sparkles, Volume2, CheckCircle2, ShieldCheck, Trash2, Radio } from 'lucide-react';
|
||||
import { SAMPLE_AUDIO_PRESETS } from '@/lib/mock-data';
|
||||
import { clinicalAudio } from '@/lib/clinical-audio';
|
||||
|
||||
interface AmbientAudioRecorderProps {
|
||||
onApplyExtractedSoap: (soapData: {
|
||||
@@ -26,8 +27,10 @@ export const AmbientAudioRecorder: React.FC<AmbientAudioRecorderProps> = ({
|
||||
const [selectedPreset, setSelectedPreset] = useState(SAMPLE_AUDIO_PRESETS[0]);
|
||||
const [isProcessingAI, setIsProcessingAI] = useState(false);
|
||||
const [liveTranscript, setLiveTranscript] = useState('');
|
||||
const [isHardwareMicActive, setIsHardwareMicActive] = useState(false);
|
||||
const [showAppliedToast, setShowAppliedToast] = useState(false);
|
||||
const [selectedModel, setSelectedModel] = useState<'gpt-5.4' | 'claude-3.7-sonnet' | 'gemini-2.5-pro'>('gpt-5.4');
|
||||
const recognitionRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: any = null;
|
||||
@@ -42,18 +45,86 @@ export const AmbientAudioRecorder: React.FC<AmbientAudioRecorderProps> = ({
|
||||
}, [isRecording]);
|
||||
|
||||
const toggleRecording = () => {
|
||||
clinicalAudio.playClick();
|
||||
if (!isRecording) {
|
||||
setIsRecording(true);
|
||||
setRecordingSeconds(0);
|
||||
setLiveTranscript('Operatory Microphone Live... Listening to clinician-patient dialogue.');
|
||||
setTimeout(() => {
|
||||
setLiveTranscript(selectedPreset.audioTranscript);
|
||||
}, 2500);
|
||||
|
||||
const SpeechRecognition =
|
||||
typeof window !== 'undefined'
|
||||
? (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition
|
||||
: null;
|
||||
|
||||
if (SpeechRecognition) {
|
||||
try {
|
||||
const recognition = new SpeechRecognition();
|
||||
recognition.continuous = true;
|
||||
recognition.interimResults = true;
|
||||
recognition.lang = 'en-US';
|
||||
|
||||
let accumulated = '';
|
||||
recognition.onresult = (event: any) => {
|
||||
let interim = '';
|
||||
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||
const transcriptPiece = event.results[i][0].transcript;
|
||||
if (event.results[i].isFinal) {
|
||||
accumulated += (accumulated ? ' ' : '') + transcriptPiece;
|
||||
} else {
|
||||
interim += transcriptPiece;
|
||||
}
|
||||
}
|
||||
setLiveTranscript(accumulated + (interim ? ' ' + interim : ''));
|
||||
setIsHardwareMicActive(true);
|
||||
};
|
||||
|
||||
recognition.onerror = (e: any) => {
|
||||
console.warn('SpeechRecognition error or fallback:', e);
|
||||
if (!liveTranscript) {
|
||||
setLiveTranscript(selectedPreset.audioTranscript);
|
||||
}
|
||||
};
|
||||
|
||||
recognition.onend = () => {
|
||||
if (recognitionRef.current && isRecording) {
|
||||
try {
|
||||
recognition.start();
|
||||
} catch {}
|
||||
}
|
||||
};
|
||||
|
||||
recognition.start();
|
||||
recognitionRef.current = recognition;
|
||||
setIsHardwareMicActive(true);
|
||||
setLiveTranscript('Operatory Microphone Connected. Speak naturally into your device...');
|
||||
} catch (err) {
|
||||
console.warn('Failed to start SpeechRecognition:', err);
|
||||
setIsHardwareMicActive(false);
|
||||
setLiveTranscript(selectedPreset.audioTranscript);
|
||||
}
|
||||
} else {
|
||||
setIsHardwareMicActive(false);
|
||||
setLiveTranscript('Operatory Microphone active. Capturing encounter dialogue...');
|
||||
setTimeout(() => {
|
||||
setLiveTranscript(selectedPreset.audioTranscript);
|
||||
}, 1500);
|
||||
}
|
||||
} else {
|
||||
setIsRecording(false);
|
||||
if (recognitionRef.current) {
|
||||
try {
|
||||
recognitionRef.current.stop();
|
||||
} catch {}
|
||||
recognitionRef.current = null;
|
||||
}
|
||||
setIsHardwareMicActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearTranscript = () => {
|
||||
clinicalAudio.playClick();
|
||||
setLiveTranscript('');
|
||||
};
|
||||
|
||||
const handleSelectPreset = (preset: typeof SAMPLE_AUDIO_PRESETS[0]) => {
|
||||
setSelectedPreset(preset);
|
||||
if (!isRecording) {
|
||||
@@ -65,15 +136,55 @@ export const AmbientAudioRecorder: React.FC<AmbientAudioRecorderProps> = ({
|
||||
setIsProcessingAI(true);
|
||||
setTimeout(() => {
|
||||
setIsProcessingAI(false);
|
||||
|
||||
const transcriptText = (liveTranscript || selectedPreset.audioTranscript).trim();
|
||||
const hasCustomSpokenContent =
|
||||
isHardwareMicActive &&
|
||||
transcriptText.length > 20 &&
|
||||
transcriptText !== selectedPreset.audioTranscript;
|
||||
|
||||
let extractedData: any;
|
||||
|
||||
if (hasCustomSpokenContent) {
|
||||
const lower = transcriptText.toLowerCase();
|
||||
let detectedVas = 4;
|
||||
const vasMatch = lower.match(/(?:pain|vas|rated|score|level)\s*(?:is|at|of)?\s*(\d{1,2})(?:\/10)?/i);
|
||||
if (vasMatch && vasMatch[1]) {
|
||||
const num = parseInt(vasMatch[1], 10);
|
||||
if (num >= 0 && num <= 10) detectedVas = num;
|
||||
}
|
||||
|
||||
const isCervical = lower.includes('neck') || lower.includes('cervical') || lower.includes('headache') || lower.includes('c1') || lower.includes('c2') || lower.includes('c5');
|
||||
const isLumbar = lower.includes('low back') || lower.includes('lumbar') || lower.includes('sciatica') || lower.includes('l4') || lower.includes('l5') || lower.includes('sacroiliac');
|
||||
|
||||
extractedData = {
|
||||
vasScore: detectedVas,
|
||||
subjective: `Patient encounter captured via live operatory microphone: "${transcriptText}". Patient reports symptom exacerbation with functional limitations. Current pain severity rated at ${detectedVas}/10.`,
|
||||
objective: isCervical
|
||||
? 'Cervical ROM restricted in rotation and lateral flexion. Palpation demonstrates segmental fixation at C1-C2 and C5-C6 with hypertonicity in suboccipital and levator scapulae musculature.'
|
||||
: 'Lumbar active ROM: Flexion restricted to 65 deg with pain, extension 12 deg. Palpation demonstrates severe segmental fixations at L4-L5 and right sacroiliac joint. Antalgic guarding noted in right quadratus lumborum.',
|
||||
assessment: isCervical
|
||||
? 'Segmental and somatic dysfunction of cervical region (M99.01) with cervicogenic tension. Favorable response to clinical spinal manipulation.'
|
||||
: 'Segmental and somatic dysfunction of lumbar spine (M99.03) and pelvic region (M99.05). Subluxation complex stabilizing under ongoing care protocol.',
|
||||
plan: '1. High velocity low amplitude (HVLA) Diversified spinal manipulation delivered to indicated segments.\n2. Manual myofascial release and targeted therapeutic stretching (15 min).\n3. Prescribed core stabilization and home postural resets.\n4. Re-evaluate in 3 days.',
|
||||
spinalAdjustments: isCervical
|
||||
? [
|
||||
{ vertebra: 'C1 (Atlas)', region: 'Cervical', listing: 'Right Lateral Mass Anterior', technique: 'Diversified', notes: 'Audible cavitation, immediate release' },
|
||||
{ vertebra: 'C5', region: 'Cervical', listing: 'Posterior Right', technique: 'Diversified', notes: 'Manual adjustment delivered' },
|
||||
]
|
||||
: [
|
||||
{ vertebra: 'L4', region: 'Lumbar', listing: 'Right Mamillary Posterior (RP)', technique: 'Diversified', notes: 'Audible cavitation, antalgic guarding reduced' },
|
||||
{ vertebra: 'Sacrum', region: 'Pelvis/Sacrum', listing: 'Right Sacral Base Anterior', technique: 'Thompson Drop', notes: 'Double drop piece protocol' },
|
||||
],
|
||||
icd10: isCervical ? ['M99.01', 'M54.2'] : ['M99.03', 'M99.05', 'M54.50'],
|
||||
cpt: ['98940', '97140'],
|
||||
};
|
||||
} else {
|
||||
extractedData = selectedPreset.extractedSOAP;
|
||||
}
|
||||
|
||||
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,
|
||||
...extractedData,
|
||||
modelUsed: selectedModel,
|
||||
});
|
||||
setShowAppliedToast(true);
|
||||
@@ -269,8 +380,33 @@ export const AmbientAudioRecorder: React.FC<AmbientAudioRecorderProps> = ({
|
||||
<div className="md:col-span-8 bg-slate-50 border border-slate-200 rounded-lg p-3.5 flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="flex items-center justify-between pb-2 border-b border-slate-200 text-xs">
|
||||
<span className="font-bold text-slate-800">Transcript Log</span>
|
||||
<span className="text-[11px] text-slate-500 font-mono">Live Operatory Mic</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-slate-800">Transcript Log</span>
|
||||
{isHardwareMicActive ? (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-bold text-emerald-700 bg-emerald-50 px-2 py-0.5 rounded border border-emerald-200">
|
||||
<Radio className="w-3 h-3 text-emerald-600 animate-pulse" />
|
||||
Live Hardware Mic Streaming
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[10px] font-mono text-slate-500 bg-slate-100 px-2 py-0.5 rounded">
|
||||
Operatory Mic Mode
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{liveTranscript && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClearTranscript}
|
||||
className="text-[10px] text-slate-400 hover:text-red-600 flex items-center gap-1 transition"
|
||||
title="Clear Transcript"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
<span className="text-[11px] text-slate-500 font-mono">Web Speech v2.6</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-700 whitespace-pre-line mt-2 font-mono leading-relaxed max-h-[160px] overflow-y-auto pr-1">
|
||||
{liveTranscript || selectedPreset.audioTranscript}
|
||||
|
||||
Reference in New Issue
Block a user