'use client'; import React, { useState, useRef } from 'react'; import { getApiBaseUrl } from '@/lib/api'; import { motion, AnimatePresence } from 'framer-motion'; import { Mic, Square, UploadCloud, AlertTriangle, ShieldCheck, Activity, Play, Pause, X, BarChart2 } from 'lucide-react'; interface VADSpace { valence: number; arousal: number; dominance: number; } interface AnalysisResult { dissonance_score: number; conflict_detected: boolean; likely_sarcasm: boolean; audio_dominant: string; text_dominant: string; contributions: { audio_text_div: number; prosody_boost: number; text_confidence_penalty: number; }; audio_vad: number[]; text_vad: number[]; prosody: { pitch_mean: number; rms_energy: number; speech_rate: number; }; error?: string; } export default function DissonanceVisualizer() { const [transcript, setTranscript] = useState(''); const [audioFile, setAudioFile] = useState(null); const [audioUrl, setAudioUrl] = useState(null); const [isRecording, setIsRecording] = useState(false); const [recordingTime, setRecordingTime] = useState(0); const [isAnalyzing, setIsAnalyzing] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(null); const mediaRecorderRef = useRef(null); const audioChunksRef = useRef([]); const timerRef = useRef(null); const fileInputRef = useRef(null); const startRecording = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mediaRecorder = new MediaRecorder(stream); mediaRecorderRef.current = mediaRecorder; audioChunksRef.current = []; mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) { audioChunksRef.current.push(e.data); } }; mediaRecorder.onstop = () => { const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' }); setAudioFile(audioBlob); setAudioUrl(URL.createObjectURL(audioBlob)); stream.getTracks().forEach((track) => track.stop()); }; mediaRecorder.start(); setIsRecording(true); setRecordingTime(0); timerRef.current = setInterval(() => { setRecordingTime((prev) => prev + 1); }, 1000); } catch (err) { console.error('Error accessing microphone:', err); setError('Could not access microphone. Please check permissions.'); } }; const stopRecording = () => { if (mediaRecorderRef.current && isRecording) { mediaRecorderRef.current.stop(); setIsRecording(false); if (timerRef.current) clearInterval(timerRef.current); } }; const handleFileUpload = (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { const file = e.target.files[0]; setAudioFile(file); setAudioUrl(URL.createObjectURL(file)); setError(null); } }; const resetState = () => { setAudioFile(null); setAudioUrl(null); setResult(null); setError(null); setTranscript(''); setRecordingTime(0); }; const analyzeDissonance = async () => { if (!audioFile) { setError('Please provide an audio recording.'); return; } if (!transcript.trim()) { setError('Please provide a transcript.'); return; } setIsAnalyzing(true); setError(null); const formData = new FormData(); // Use .webm extension for blob, or keep original file name formData.append('audio', audioFile, audioFile instanceof File ? audioFile.name : 'recording.webm'); formData.append('transcript', transcript); try { const res = await fetch(`${getApiBaseUrl()}/finance/analyze/emotion-conflict`, { method: 'POST', body: formData, }); if (!res.ok) { const errData = await res.json(); throw new Error(errData.detail || 'Analysis failed'); } const data: AnalysisResult = await res.json(); setResult(data); } catch (err: any) { setError(err.message || 'An error occurred during analysis.'); } finally { setIsAnalyzing(false); } }; const formatTime = (seconds: number) => { const m = Math.floor(seconds / 60).toString().padStart(2, '0'); const s = (seconds % 60).toString().padStart(2, '0'); return `${m}:${s}`; }; return (
{/* Header */}

MMSA Deception Radar

Cross-Modal Emotion Conflict Engine

{/* Input Section */}

Audio Intake

{!audioFile ? (
{isRecording ? (
{formatTime(recordingTime)}
) : ( )}
{!isRecording && (

OR

)}
) : (

Audio Captured

{audioUrl &&
)}

Transcript