JIS-ASR / dashboard.html
ramalMr's picture
Update dashboard.html
8e2c6c9 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASR Audio Intelligence Platform</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root { --asr-primary: #0f172a; --asr-accent: #3b82f6; --asr-success: #10b981; }
body { font-family: 'Inter', system-ui, sans-serif; }
.asr-gradient { background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%); }
.asr-accent-gradient { background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); }
.glass-card { background: rgba(255,255,255,0.95); backdrop-filter: blur(10px); }
.metric-card:hover { transform: translateY(-2px); box-shadow: 0 20px 40px rgba(0,0,0,0.1); }
.upload-zone { border: 2px dashed #cbd5e1; transition: all 0.3s; }
.upload-zone:hover, .upload-zone.dragover { border-color: #3b82f6; background: #f0f9ff; }
.progress-ring { transform: rotate(-90deg); }
.segment-row:hover { background: #f8fafc; }
@keyframes pulse-ring { 0% { transform: scale(0.8); opacity: 1; } 100% { transform: scale(1.4); opacity: 0; } }
.live-indicator::before { content: ''; position: absolute; width: 100%; height: 100%; background: #10b981; border-radius: 50%; animation: pulse-ring 1.5s infinite; }
</style>
</head>
<body class="bg-slate-50 min-h-screen">
<!-- Header -->
<header class="asr-gradient text-white sticky top-0 z-40 shadow-xl">
<div class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<div class="w-12 h-12 bg-white rounded-xl flex items-center justify-center">
<span class="text-slate-900 font-black text-xl">ASR</span>
</div>
<div>
<h1 class="text-2xl font-bold tracking-tight">Audio Intelligence Platform</h1>
<p class="text-slate-400 text-sm">Enterprise Speech Analytics & Transcription</p>
</div>
</div>
<div class="flex items-center space-x-6">
<div class="text-right">
<div class="text-xs text-slate-400 uppercase tracking-wider">System Status</div>
<div class="flex items-center mt-1">
<span class="relative flex h-3 w-3 mr-2">
<span class="live-indicator absolute inline-flex h-full w-full rounded-full bg-emerald-400"></span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
</span>
<span class="font-medium" id="serverStatus">Operational</span>
</div>
</div>
</div>
</div>
</div>
</header>
<main class="container mx-auto px-6 py-8">
<!-- Upload Section -->
<section class="glass-card rounded-2xl shadow-lg p-8 mb-8 border border-slate-200">
<div class="flex items-center justify-between mb-6">
<div>
<h2 class="text-xl font-bold text-slate-800">Audio Upload</h2>
<p class="text-slate-500 text-sm mt-1">Upload audio files for analysis. Stereo files will be automatically separated by channel.</p>
</div>
<div class="flex items-center space-x-2 text-sm">
<span class="px-3 py-1 bg-blue-100 text-blue-700 rounded-full font-medium">Stereo: Split Channels</span>
<span class="px-3 py-1 bg-slate-100 text-slate-700 rounded-full font-medium">Mono: Single Speaker</span>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Upload Zone -->
<div class="upload-zone rounded-xl p-8 text-center cursor-pointer" id="uploadZone">
<input type="file" id="fileInput" class="hidden" accept=".wav,.mp3,.m4a,.flac,.ogg,.opus">
<div class="w-16 h-16 mx-auto mb-4 bg-slate-100 rounded-full flex items-center justify-center">
<i class="fas fa-cloud-arrow-up text-2xl text-slate-400"></i>
</div>
<p class="text-lg text-slate-700 font-medium">Drop audio file or <span class="text-blue-600 hover:underline">browse</span></p>
<p class="text-sm text-slate-400 mt-2">WAV, MP3, M4A, FLAC, OGG, OPUS</p>
</div>
<!-- Record Zone -->
<div class="rounded-xl p-8 text-center border-2 border-dashed border-slate-200 hover:border-rose-400 transition-all" id="recordZone">
<div class="w-16 h-16 mx-auto mb-4 rounded-full flex items-center justify-center transition-all duration-300" id="micButton">
<button onclick="toggleRecording()" class="w-16 h-16 bg-slate-100 hover:bg-rose-100 rounded-full flex items-center justify-center transition-all duration-300" id="recordBtn">
<i class="fas fa-microphone text-2xl text-slate-400" id="micIcon"></i>
</button>
</div>
<p class="text-lg text-slate-700 font-medium" id="recordText">Click to record</p>
<p class="text-sm text-slate-400 mt-2" id="recordTimer">Max 5 minutes</p>
</div>
</div>
<div id="uploadProgress" class="hidden mt-6">
<div class="bg-slate-50 rounded-xl p-6 border border-slate-200">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center">
<div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center mr-4">
<i class="fas fa-spinner fa-spin text-blue-600"></i>
</div>
<div>
<p class="font-semibold text-slate-800" id="uploadStatus">Processing...</p>
<p class="text-sm text-slate-500" id="stageText">Initializing...</p>
</div>
</div>
<span id="progressPercent" class="text-2xl font-bold text-blue-600">0%</span>
</div>
<div class="w-full bg-slate-200 rounded-full h-2">
<div id="progressBar" class="asr-accent-gradient h-2 rounded-full transition-all duration-500" style="width: 0%"></div>
</div>
</div>
</div>
</section>
<!-- Statistics Dashboard -->
<section class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 mb-8">
<div class="glass-card rounded-xl p-5 border border-slate-200 metric-card transition-all duration-300">
<div class="flex items-center justify-between">
<div>
<p class="text-xs text-slate-500 uppercase tracking-wider font-medium">Total Calls</p>
<p class="text-3xl font-bold text-slate-800 mt-1" id="totalCalls">0</p>
</div>
<div class="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center">
<i class="fas fa-phone-volume text-blue-600 text-lg"></i>
</div>
</div>
</div>
<div class="glass-card rounded-xl p-5 border border-slate-200 metric-card transition-all duration-300">
<div class="flex items-center justify-between">
<div>
<p class="text-xs text-slate-500 uppercase tracking-wider font-medium">Stereo</p>
<p class="text-3xl font-bold text-purple-600 mt-1" id="stereoCalls">0</p>
</div>
<div class="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center">
<i class="fas fa-code-branch text-purple-600 text-lg"></i>
</div>
</div>
</div>
<div class="glass-card rounded-xl p-5 border border-slate-200 metric-card transition-all duration-300">
<div class="flex items-center justify-between">
<div>
<p class="text-xs text-slate-500 uppercase tracking-wider font-medium">Quality Score</p>
<p class="text-3xl font-bold text-emerald-600 mt-1" id="avgScore">0</p>
</div>
<div class="w-12 h-12 bg-emerald-50 rounded-xl flex items-center justify-center">
<i class="fas fa-chart-line text-emerald-600 text-lg"></i>
</div>
</div>
</div>
<div class="glass-card rounded-xl p-5 border border-slate-200 metric-card transition-all duration-300">
<div class="flex items-center justify-between">
<div>
<p class="text-xs text-slate-500 uppercase tracking-wider font-medium">Clarity</p>
<p class="text-3xl font-bold text-cyan-600 mt-1" id="avgClarity">0</p>
</div>
<div class="w-12 h-12 bg-cyan-50 rounded-xl flex items-center justify-center">
<i class="fas fa-microphone text-cyan-600 text-lg"></i>
</div>
</div>
</div>
<div class="glass-card rounded-xl p-5 border border-slate-200 metric-card transition-all duration-300">
<div class="flex items-center justify-between">
<div>
<p class="text-xs text-slate-500 uppercase tracking-wider font-medium">Confidence</p>
<p class="text-3xl font-bold text-amber-600 mt-1" id="avgConfidence">0</p>
</div>
<div class="w-12 h-12 bg-amber-50 rounded-xl flex items-center justify-center">
<i class="fas fa-shield-check text-amber-600 text-lg"></i>
</div>
</div>
</div>
<div class="glass-card rounded-xl p-5 border border-slate-200 metric-card transition-all duration-300">
<div class="flex items-center justify-between">
<div>
<p class="text-xs text-slate-500 uppercase tracking-wider font-medium">Segments</p>
<p class="text-3xl font-bold text-rose-600 mt-1" id="totalSegments">0</p>
</div>
<div class="w-12 h-12 bg-rose-50 rounded-xl flex items-center justify-center">
<i class="fas fa-wave-square text-rose-600 text-lg"></i>
</div>
</div>
</div>
</section>
<!-- Calls List -->
<section class="glass-card rounded-2xl shadow-lg border border-slate-200 overflow-hidden">
<div class="p-6 border-b border-slate-200 flex items-center justify-between">
<div>
<h2 class="text-xl font-bold text-slate-800">Analyzed Recordings</h2>
<p class="text-sm text-slate-500 mt-1">Click on any recording to view detailed analysis</p>
</div>
<button onclick="loadCalls()" class="flex items-center px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg transition-colors font-medium">
<i class="fas fa-arrows-rotate mr-2"></i>Refresh
</button>
</div>
<div id="callsList" class="divide-y divide-slate-100"></div>
</section>
</main>
<!-- Analysis Modal -->
<div id="analysisModal" class="hidden fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl max-w-7xl w-full max-h-[95vh] overflow-hidden flex flex-col">
<div class="asr-gradient text-white p-6 flex justify-between items-center shrink-0">
<div class="flex items-center space-x-4">
<div class="w-10 h-10 bg-white/20 rounded-lg flex items-center justify-center">
<i class="fas fa-chart-bar"></i>
</div>
<div>
<h3 class="text-xl font-bold" id="modalTitle">Analysis Report</h3>
<p class="text-slate-300 text-sm">Comprehensive audio analysis</p>
</div>
</div>
<button onclick="closeModal()" class="w-10 h-10 bg-white/10 hover:bg-white/20 rounded-lg flex items-center justify-center transition-colors">
<i class="fas fa-xmark text-xl"></i>
</button>
</div>
<div class="p-6 overflow-y-auto flex-1" id="modalContent"></div>
</div>
</div>
<script>
const API_BASE = window.location.origin;
let currentJobId = null;
let pollInterval = null;
// Recording variables
let mediaRecorder = null;
let audioChunks = [];
let isRecording = false;
let recordingStartTime = null;
let timerInterval = null;
document.addEventListener('DOMContentLoaded', () => {
checkServerHealth();
loadStatistics();
loadCalls();
const uploadZone = document.getElementById('uploadZone');
const fileInput = document.getElementById('fileInput');
uploadZone.addEventListener('click', () => fileInput.click());
uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('dragover'); });
uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover'));
uploadZone.addEventListener('drop', e => {
e.preventDefault();
uploadZone.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) handleFileUpload(e.dataTransfer.files[0]);
});
fileInput.addEventListener('change', e => {
if (e.target.files.length > 0) handleFileUpload(e.target.files[0]);
});
});
// Recording functions
async function toggleRecording() {
if (isRecording) {
stopRecording();
} else {
await startRecording();
}
}
async function startRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: 1,
echoCancellation: true,
noiseSuppression: true
}
});
mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
audioChunks = [];
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) audioChunks.push(e.data);
};
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
const file = new File([audioBlob], `recording_${Date.now()}.webm`, { type: 'audio/webm' });
handleFileUpload(file);
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start(1000);
isRecording = true;
recordingStartTime = Date.now();
// Update UI
document.getElementById('recordBtn').classList.remove('bg-slate-100', 'hover:bg-rose-100');
document.getElementById('recordBtn').classList.add('bg-rose-500', 'animate-pulse');
document.getElementById('micIcon').classList.remove('text-slate-400');
document.getElementById('micIcon').classList.add('text-white');
document.getElementById('micIcon').classList.replace('fa-microphone', 'fa-stop');
document.getElementById('recordText').textContent = 'Recording... Click to stop';
document.getElementById('recordZone').classList.add('border-rose-400', 'bg-rose-50');
// Start timer
timerInterval = setInterval(updateTimer, 1000);
} catch (err) {
console.error('Microphone access denied:', err);
alert('Mikrofona icazə verilmədi. Brauzerin icazələrini yoxlayın.');
}
}
function stopRecording() {
if (mediaRecorder && isRecording) {
mediaRecorder.stop();
isRecording = false;
clearInterval(timerInterval);
// Reset UI
document.getElementById('recordBtn').classList.add('bg-slate-100', 'hover:bg-rose-100');
document.getElementById('recordBtn').classList.remove('bg-rose-500', 'animate-pulse');
document.getElementById('micIcon').classList.add('text-slate-400');
document.getElementById('micIcon').classList.remove('text-white');
document.getElementById('micIcon').classList.replace('fa-stop', 'fa-microphone');
document.getElementById('recordText').textContent = 'Click to record';
document.getElementById('recordTimer').textContent = 'Max 5 minutes';
document.getElementById('recordZone').classList.remove('border-rose-400', 'bg-rose-50');
}
}
function updateTimer() {
const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
document.getElementById('recordTimer').textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
// Auto-stop after 5 minutes
if (elapsed >= 300) {
stopRecording();
}
}
async function checkServerHealth() {
try {
const res = await fetch(`${API_BASE}/health`);
const data = await res.json();
document.getElementById('serverStatus').textContent = data.status === 'healthy' ? 'Operational' : 'Offline';
} catch { document.getElementById('serverStatus').textContent = 'Offline'; }
}
async function loadStatistics() {
try {
const res = await fetch(`${API_BASE}/api/statistics`);
const s = await res.json();
document.getElementById('totalCalls').textContent = s.total_calls || 0;
document.getElementById('stereoCalls').textContent = s.stereo_calls || 0;
document.getElementById('avgScore').textContent = (s.avg_quality_score || 0).toFixed(1);
document.getElementById('avgClarity').textContent = (s.avg_clarity || 0).toFixed(1);
document.getElementById('avgConfidence').textContent = (s.avg_confidence || 0).toFixed(1);
document.getElementById('totalSegments').textContent = s.total_segments || 0;
} catch (e) { console.error('Stats error:', e); }
}
async function loadCalls() {
try {
const res = await fetch(`${API_BASE}/api/calls`);
const calls = await res.json();
const list = document.getElementById('callsList');
if (calls.length === 0) {
list.innerHTML = `<div class="p-12 text-center"><div class="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-4"><i class="fas fa-folder-open text-2xl text-slate-400"></i></div><p class="text-slate-600 font-medium">No recordings yet</p><p class="text-slate-400 text-sm mt-1">Upload an audio file to get started</p></div>`;
return;
}
list.innerHTML = calls.map(call => `
<div onclick="viewAnalysis('${call}')" class="p-5 flex items-center justify-between hover:bg-slate-50 cursor-pointer transition-colors">
<div class="flex items-center space-x-4">
<div class="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center text-white"><i class="fas fa-waveform-lines"></i></div>
<div>
<p class="font-semibold text-slate-800">${call}</p>
<p class="text-sm text-slate-500">Click to view analysis</p>
</div>
</div>
<div class="flex items-center space-x-3">
<span class="px-3 py-1 bg-emerald-100 text-emerald-700 rounded-full text-sm font-medium">Analyzed</span>
<i class="fas fa-chevron-right text-slate-400"></i>
</div>
</div>
`).join('');
} catch (e) { console.error('Calls error:', e); }
}
async function handleFileUpload(file) {
const formData = new FormData();
formData.append('file', file);
document.getElementById('uploadProgress').classList.remove('hidden');
updateProgress('Uploading...', 'Transferring file to server', 10);
try {
const res = await fetch(`${API_BASE}/api/upload`, { method: 'POST', body: formData });
const result = await res.json();
if (!res.ok) throw new Error(result.error);
currentJobId = result.job_id;
updateProgress('Processing...', 'Analysis started', 25);
pollJobStatus();
} catch (e) {
updateProgress('Error', e.message, 0);
setTimeout(() => document.getElementById('uploadProgress').classList.add('hidden'), 3000);
}
}
function updateProgress(status, stage, percent) {
document.getElementById('uploadStatus').textContent = status;
document.getElementById('stageText').textContent = stage;
document.getElementById('progressBar').style.width = percent + '%';
document.getElementById('progressPercent').textContent = percent + '%';
}
function pollJobStatus() {
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(async () => {
try {
const res = await fetch(`${API_BASE}/api/jobs/${currentJobId}`);
const job = await res.json();
const stages = {
'pending': { text: 'Queued...', progress: 20 },
'initializing': { text: 'Loading models...', progress: 30 },
'diarization': { text: 'Separating speakers...', progress: 45 },
'transcription': { text: 'Transcribing speech...', progress: 65 },
'audio_analysis': { text: 'Analyzing audio features...', progress: 85 },
'done': { text: 'Complete!', progress: 100 }
};
if (job.stage && stages[job.stage]) {
let stageText = stages[job.stage].text;
if (job.is_stereo !== null) stageText += job.is_stereo ? ' (Stereo)' : ' (Mono)';
updateProgress('Processing...', stageText, stages[job.stage].progress);
}
if (job.status === 'completed') {
clearInterval(pollInterval);
updateProgress('Success!', 'Analysis complete', 100);
setTimeout(() => {
document.getElementById('uploadProgress').classList.add('hidden');
loadStatistics();
loadCalls();
}, 1500);
} else if (job.status === 'failed') {
clearInterval(pollInterval);
updateProgress('Failed', job.error || 'Unknown error', 0);
setTimeout(() => document.getElementById('uploadProgress').classList.add('hidden'), 3000);
}
} catch (e) { clearInterval(pollInterval); }
}, 1500);
}
async function viewAnalysis(callName) {
try {
const res = await fetch(`${API_BASE}/api/analysis/${callName}`);
const data = await res.json();
const a = data.analysis;
const t = data.transcription;
document.getElementById('modalTitle').textContent = callName;
const isStereo = a.audio_type === 'stereo';
const profiles = a.speaker_profiles || {};
const dynamics = a.dynamics || {};
let profilesHTML = '';
for (const [spk, p] of Object.entries(profiles)) {
profilesHTML += `
<div class="bg-slate-50 rounded-xl p-5 border border-slate-200">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 ${spk === 'CUSTOMER' ? 'bg-blue-100' : spk === 'AGENT' ? 'bg-emerald-100' : 'bg-purple-100'} rounded-full flex items-center justify-center">
<i class="fas fa-user ${spk === 'CUSTOMER' ? 'text-blue-600' : spk === 'AGENT' ? 'text-emerald-600' : 'text-purple-600'}"></i>
</div>
<div>
<p class="font-bold text-slate-800">${spk}</p>
<p class="text-sm text-slate-500">${p.communication_style} style</p>
</div>
</div>
<div class="text-right">
<p class="text-2xl font-bold text-slate-800">${p.overall_score}</p>
<p class="text-xs text-slate-500">Quality Score</p>
</div>
</div>
<div class="grid grid-cols-4 gap-3 mb-4">
<div class="text-center p-2 bg-white rounded-lg"><p class="text-lg font-bold text-slate-800">${p.avg_pitch.toFixed(0)}</p><p class="text-xs text-slate-500">Pitch (Hz)</p></div>
<div class="text-center p-2 bg-white rounded-lg"><p class="text-lg font-bold text-slate-800">${p.avg_energy.toFixed(1)}</p><p class="text-xs text-slate-500">Energy (dB)</p></div>
<div class="text-center p-2 bg-white rounded-lg"><p class="text-lg font-bold text-slate-800">${p.avg_speaking_rate.toFixed(1)}</p><p class="text-xs text-slate-500">Rate (/s)</p></div>
<div class="text-center p-2 bg-white rounded-lg"><p class="text-lg font-bold text-slate-800">${p.avg_clarity.toFixed(0)}</p><p class="text-xs text-slate-500">Clarity</p></div>
</div>
<div class="flex flex-wrap gap-2">
${p.strengths.map(s => `<span class="px-2 py-1 bg-emerald-100 text-emerald-700 text-xs rounded-full">${s}</span>`).join('')}
${p.improvements.map(i => `<span class="px-2 py-1 bg-amber-100 text-amber-700 text-xs rounded-full">${i}</span>`).join('')}
</div>
</div>
`;
}
let transcriptHTML = '';
let totalInferenceTime = 0;
if (t && t.transcriptions) {
t.transcriptions.forEach(seg => totalInferenceTime += (seg.inference_time || 0));
transcriptHTML = t.transcriptions.map(seg => `
<div class="flex ${seg.speaker === 'CUSTOMER' ? 'justify-start' : seg.speaker === 'AGENT' ? 'justify-end' : 'justify-center'}">
<div class="max-w-[70%] ${seg.speaker === 'CUSTOMER' ? 'bg-blue-50 border-blue-200' : seg.speaker === 'AGENT' ? 'bg-emerald-50 border-emerald-200' : 'bg-slate-50 border-slate-200'} border rounded-xl p-3">
<div class="flex items-center justify-between mb-1">
<div class="flex items-center space-x-2">
<span class="font-semibold text-sm ${seg.speaker === 'CUSTOMER' ? 'text-blue-700' : seg.speaker === 'AGENT' ? 'text-emerald-700' : 'text-slate-700'}">${seg.speaker}</span>
<span class="text-xs text-slate-400">${seg.start_time}</span>
</div>
<span class="text-xs text-orange-500 font-medium"><i class="fas fa-clock mr-1"></i>${seg.inference_time}s</span>
</div>
<p class="text-slate-800">${seg.text}</p>
</div>
</div>
`).join('');
}
document.getElementById('modalContent').innerHTML = `
<div class="space-y-6">
<!-- Overview -->
<div class="grid grid-cols-5 gap-4">
<div class="bg-gradient-to-br from-slate-800 to-slate-900 text-white rounded-xl p-5 text-center">
<p class="text-3xl font-bold">${a.overall_quality_score}</p>
<p class="text-slate-300 text-sm mt-1">Quality Score</p>
</div>
<div class="bg-slate-50 rounded-xl p-5 text-center border border-slate-200">
<p class="text-3xl font-bold text-slate-800">${a.audio_duration.toFixed(1)}s</p>
<p class="text-slate-500 text-sm mt-1">Duration</p>
</div>
<div class="bg-slate-50 rounded-xl p-5 text-center border border-slate-200">
<p class="text-3xl font-bold ${isStereo ? 'text-purple-600' : 'text-blue-600'}">${isStereo ? 'STEREO' : 'MONO'}</p>
<p class="text-slate-500 text-sm mt-1">Audio Type</p>
</div>
<div class="bg-slate-50 rounded-xl p-5 text-center border border-slate-200">
<p class="text-3xl font-bold text-slate-800">${a.segments.length}</p>
<p class="text-slate-500 text-sm mt-1">Segments</p>
</div>
<div class="bg-slate-50 rounded-xl p-5 text-center border border-slate-200">
<p class="text-3xl font-bold text-emerald-600">${dynamics.engagement_score?.toFixed(0) || 0}</p>
<p class="text-slate-500 text-sm mt-1">Engagement</p>
</div>
</div>
<!-- Speaker Profiles -->
<div>
<h4 class="font-bold text-slate-800 mb-4 flex items-center"><i class="fas fa-users text-blue-600 mr-2"></i>Speaker Profiles</h4>
<div class="grid ${isStereo ? 'grid-cols-2' : 'grid-cols-1 max-w-xl'} gap-4">${profilesHTML}</div>
</div>
<!-- Transcription -->
${t ? `
<div>
<div class="flex items-center justify-between mb-4">
<h4 class="font-bold text-slate-800 flex items-center"><i class="fas fa-closed-captioning text-blue-600 mr-2"></i>Transcription</h4>
<span class="px-3 py-1 bg-orange-100 text-orange-700 rounded-full text-sm font-medium"><i class="fas fa-bolt mr-1"></i>Total: ${totalInferenceTime.toFixed(2)}s</span>
</div>
<div class="bg-slate-50 rounded-xl p-4 border border-slate-200 max-h-80 overflow-y-auto space-y-3">${transcriptHTML}</div>
</div>
` : ''}
<!-- Segment Analysis -->
<div>
<h4 class="font-bold text-slate-800 mb-4 flex items-center"><i class="fas fa-wave-square text-blue-600 mr-2"></i>Segment Analysis</h4>
<div class="bg-slate-50 rounded-xl border border-slate-200 overflow-hidden max-h-96 overflow-y-auto">
<table class="w-full text-sm">
<thead class="bg-slate-100 sticky top-0">
<tr>
<th class="px-4 py-3 text-left font-semibold text-slate-600">#</th>
<th class="px-4 py-3 text-left font-semibold text-slate-600">Speaker</th>
<th class="px-4 py-3 text-left font-semibold text-slate-600">Time</th>
<th class="px-4 py-3 text-center font-semibold text-slate-600">Pitch</th>
<th class="px-4 py-3 text-center font-semibold text-slate-600">Energy</th>
<th class="px-4 py-3 text-center font-semibold text-slate-600">Rate</th>
<th class="px-4 py-3 text-center font-semibold text-slate-600">Clarity</th>
<th class="px-4 py-3 text-center font-semibold text-slate-600">Emotion</th>
<th class="px-4 py-3 text-center font-semibold text-slate-600">Score</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
${a.segments.map((s, i) => `
<tr class="segment-row">
<td class="px-4 py-3 font-medium text-slate-800">${i+1}</td>
<td class="px-4 py-3"><span class="px-2 py-1 ${s.speaker === 'CUSTOMER' ? 'bg-blue-100 text-blue-700' : s.speaker === 'AGENT' ? 'bg-emerald-100 text-emerald-700' : 'bg-purple-100 text-purple-700'} rounded-full text-xs font-medium">${s.speaker}</span></td>
<td class="px-4 py-3 text-slate-600">${s.start_time}</td>
<td class="px-4 py-3 text-center font-medium">${s.pitch.mean_f0.toFixed(0)} Hz</td>
<td class="px-4 py-3 text-center font-medium">${s.energy.mean_rms.toFixed(1)} dB</td>
<td class="px-4 py-3 text-center font-medium">${s.rhythm.speaking_rate.toFixed(1)}</td>
<td class="px-4 py-3 text-center"><span class="px-2 py-1 ${s.voice_quality.clarity_score > 70 ? 'bg-emerald-100 text-emerald-700' : s.voice_quality.clarity_score > 50 ? 'bg-amber-100 text-amber-700' : 'bg-red-100 text-red-700'} rounded text-xs font-medium">${s.voice_quality.clarity_score.toFixed(0)}</span></td>
<td class="px-4 py-3 text-center"><span class="px-2 py-1 bg-slate-100 text-slate-700 rounded text-xs">${s.emotion.primary_emotion}</span></td>
<td class="px-4 py-3 text-center font-bold text-slate-800">${s.overall_quality_score.toFixed(0)}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</div>
`;
document.getElementById('analysisModal').classList.remove('hidden');
} catch (e) { console.error('Analysis error:', e); alert('Failed to load analysis'); }
}
function closeModal() { document.getElementById('analysisModal').classList.add('hidden'); }
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
</script>
</body>
</html>