import React, { useEffect, useRef, useState, useCallback } from 'react'; // Corrected import: AnalysisResult is defined in types.ts, not LiveInsightPanel.tsx import { Scenario, AnalysisResult } from '../types'; import { GoogleGenAI, LiveServerMessage, Modality, Chat } from "@google/genai"; import { Mic, MicOff, X, Radio, AlertCircle, Headphones, BrainCircuit, LayoutPanelLeft } from 'lucide-react'; import { arrayBufferToBase64, base64ToUint8Array, decodeAudioData, floatTo16BitPCM } from '../services/audioUtils'; import { CBT_ANALYSIS_SCHEMA, SYSTEM_SUMMARY_ANALYSIS_PROMPT, SYSTEM_ANALYSIS_PROMPT } from '../constants'; // Sub-components import AudioVisualizer from './AudioVisualizer'; // Corrected import: LiveInsightPanel is a default export, AnalysisResult is already imported from types import LiveInsightPanel from './LiveInsightPanel'; import SessionReport from './SessionReport'; interface VoiceSessionProps { scenario: Scenario; onExit: () => void; } const VoiceSession: React.FC = ({ scenario, onExit }) => { const [status, setStatus] = useState<'disconnected' | 'connecting' | 'connected' | 'error' | 'analyzing' | 'finished'>('disconnected'); const [errorMessage, setErrorMessage] = useState(null); const [finalAnalysis, setFinalAnalysis] = useState(null); // Live Analysis State const [liveAnalysis, setLiveAnalysis] = useState(null); const [isAnalyzingTurn, setIsAnalyzingTurn] = useState(false); const [showLiveInsights, setShowLiveInsights] = useState(false); // Audio Refs const inputContextRef = useRef(null); const outputContextRef = useRef(null); const nextStartTimeRef = useRef(0); const sourcesRef = useRef>(new Set()); // Transcript accumulation const transcriptRef = useRef(""); const currentTurnUserText = useRef(""); const currentTurnPartnerText = useRef(""); // AI Refs const sessionRef = useRef(null); const analysisChatRef = useRef(null); // Visualization const [volumeLevel, setVolumeLevel] = useState(0); useEffect(() => { return () => { sourcesRef.current.forEach(source => { try { source.stop(); } catch(e) {} }); }; }, []); const disconnectSession = useCallback(async () => { sourcesRef.current.forEach(source => { try { source.stop(); } catch(e) {} }); sourcesRef.current.clear(); try { if (inputContextRef.current && inputContextRef.current.state !== 'closed') { await inputContextRef.current.close(); } if (outputContextRef.current && outputContextRef.current.state !== 'closed') { await outputContextRef.current.close(); } } catch (e) { console.error("Error closing contexts", e); } inputContextRef.current = null; outputContextRef.current = null; if (sessionRef.current) { try { const session = await sessionRef.current; if (session && typeof session.close === 'function') { session.close(); } } catch (e) { console.error("Error closing session:", e); } sessionRef.current = null; } }, []); const handleTerminateAndAnalyze = async () => { await disconnectSession(); if (transcriptRef.current.length < 10) { setStatus('disconnected'); onExit(); return; } setStatus('analyzing'); try { const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); const systemPrompt = `${SYSTEM_SUMMARY_ANALYSIS_PROMPT} SCENARIO CONTEXT: Title: ${scenario.title} Description: ${scenario.description} CBT Focus: ${scenario.cbtFocus.join(', ')}`; const result = await ai.models.generateContent({ model: 'gemini-3-flash-preview', config: { systemInstruction: systemPrompt, responseMimeType: 'application/json', responseSchema: CBT_ANALYSIS_SCHEMA, }, contents: [{ role: 'user', parts: [{ text: `SESSION TRANSCRIPT:\n${transcriptRef.current}` }] }] }); if (result.text) { setFinalAnalysis(JSON.parse(result.text)); setStatus('finished'); } else { throw new Error("Analysis synthesis failed."); } } catch (err) { console.error("Post-session analysis error", err); setStatus('disconnected'); onExit(); } }; const startSession = async () => { try { setStatus('connecting'); setErrorMessage(null); transcriptRef.current = ""; currentTurnUserText.current = ""; currentTurnPartnerText.current = ""; setLiveAnalysis(null); const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext; outputContextRef.current = new AudioContextClass({ sampleRate: 24000 }); inputContextRef.current = new AudioContextClass({ sampleRate: 16000 }); if (outputContextRef.current.state === 'suspended') await outputContextRef.current.resume(); if (inputContextRef.current.state === 'suspended') await inputContextRef.current.resume(); const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); analysisChatRef.current = ai.chats.create({ model: 'gemini-3-flash-preview', config: { systemInstruction: `${SYSTEM_ANALYSIS_PROMPT}\n\nSCENARIO CONTEXT:\nTitle: ${scenario.title}\nDescription: ${scenario.description}\nCBT Focus: ${scenario.cbtFocus.join(', ')}`, responseMimeType: 'application/json', responseSchema: CBT_ANALYSIS_SCHEMA, } }); const effectiveSystemInstruction = ` ${scenario.systemInstruction} IMPORTANT: This is a voice conversation. Keep responses concise (1-2 sentences). You are strictly in character. START THE CONVERSATION IMMEDIATELY by saying: "${scenario.initialMessage}" `; const sessionPromise = ai.live.connect({ model: 'gemini-2.5-flash-native-audio-preview-09-2025', config: { responseModalities: [Modality.AUDIO], systemInstruction: effectiveSystemInstruction, outputAudioTranscription: {}, inputAudioTranscription: {}, speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: 'Puck' } }, }, }, callbacks: { onopen: () => { setStatus('connected'); if (!inputContextRef.current) return; const source = inputContextRef.current.createMediaStreamSource(stream); const processor = inputContextRef.current.createScriptProcessor(4096, 1, 1); const gainNode = inputContextRef.current.createGain(); gainNode.gain.value = 0; processor.onaudioprocess = (e) => { const inputData = e.inputBuffer.getChannelData(0); let sum = 0; for(let i=0; i { session.sendRealtimeInput({ media: { mimeType: 'audio/pcm;rate=16000', data: b64Data } }); }).catch(err => console.error("Error sending audio:", err)); }; source.connect(processor); processor.connect(gainNode); gainNode.connect(inputContextRef.current.destination); }, onmessage: async (msg: LiveServerMessage) => { if (msg.serverContent?.inputTranscription) { const text = msg.serverContent.inputTranscription.text; currentTurnUserText.current += text; transcriptRef.current += `[USER]: ${text}\n`; } if (msg.serverContent?.outputTranscription) { const text = msg.serverContent.outputTranscription.text; currentTurnPartnerText.current += text; transcriptRef.current += `[PARTNER]: ${text}\n`; } const audioData = msg.serverContent?.modelTurn?.parts?.[0]?.inlineData?.data; if (audioData && outputContextRef.current) { const ctx = outputContextRef.current; const bytes = base64ToUint8Array(audioData); try { const audioBuffer = await decodeAudioData(bytes, ctx, 24000, 1); const source = ctx.createBufferSource(); source.buffer = audioBuffer; source.connect(ctx.destination); const now = ctx.currentTime; if (nextStartTimeRef.current < now) nextStartTimeRef.current = now; source.start(nextStartTimeRef.current); nextStartTimeRef.current += audioBuffer.duration; sourcesRef.current.add(source); source.addEventListener('ended', () => sourcesRef.current.delete(source)); } catch (decodingErr) { console.error("Audio decoding error", decodingErr); } } if (msg.serverContent?.interrupted) { sourcesRef.current.forEach(src => { try { src.stop(); } catch(e){} }); sourcesRef.current.clear(); if (outputContextRef.current) nextStartTimeRef.current = outputContextRef.current.currentTime; } if (msg.serverContent?.turnComplete) { if (analysisChatRef.current && (currentTurnUserText.current || currentTurnPartnerText.current)) { setIsAnalyzingTurn(true); const turnContext = `User said: "${currentTurnUserText.current}". Roleplay partner said: "${currentTurnPartnerText.current}".`; analysisChatRef.current.sendMessage({ message: turnContext }).then(res => { try { if (res.text) { setLiveAnalysis(JSON.parse(res.text)); if (!showLiveInsights) setShowLiveInsights(true); } } catch (e) { console.error("Failed to parse live analysis", e); } finally { setIsAnalyzingTurn(false); currentTurnUserText.current = ""; currentTurnPartnerText.current = ""; } }); } } }, onclose: () => { if (status !== 'error' && status !== 'analyzing' && status !== 'finished') setStatus('disconnected'); }, onerror: (err) => { console.error("Session error", err); setErrorMessage("Clinical bridge interrupted. Resetting connection."); setStatus('error'); disconnectSession(); } } }); sessionRef.current = sessionPromise; } catch (err: any) { console.error("Failed to start session", err); setErrorMessage(err.message || "Microphone access denied or link failure."); setStatus('error'); } }; if (status === 'analyzing') { return (

Synthesizing Clinical Insights

Please wait while the engine analyzes your vocal patterns and cognitive restructuring efficiency.

); } if (status === 'finished' && finalAnalysis) { return ; } return (
{/* Main Voice Hub */}
{status === 'connected' && (
)} {/* Header */}

{scenario.title}

{status}
{status === 'connected' && ( )}
{status === 'disconnected' &&

System ready for audio link.
Please proceed when in a quiet environment.

} {status === 'connecting' &&

Establishing Bi-Directional Bridge...

} {status === 'connected' &&

Active Session. Listening...

} {status === 'error' && (

{errorMessage}

)}
{status !== 'connected' ? ( ) : ( )}
{showLiveInsights && }
); }; export default VoiceSession;