| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Speech Intelligence and Intent Detection | Standalone Test Client</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script> |
| tailwind.config = { |
| theme: { |
| extend: { |
| colors: { |
| ai: { |
| blue: '#0A84FF', |
| cyan: '#32D74B', |
| purple: '#BF5AF2', |
| dark: '#030712' |
| } |
| } |
| } |
| } |
| } |
| </script> |
| <style> |
| body { |
| background-color: #030712; |
| background-image: radial-gradient(circle at 50% 0%, rgba(10, 132, 255, 0.1) 0%, transparent 50%), |
| radial-gradient(circle at 100% 100%, rgba(50, 215, 75, 0.05) 0%, transparent 50%); |
| background-attachment: fixed; |
| } |
| .glass { |
| background: rgba(255, 255, 255, 0.03); |
| backdrop-filter: blur(16px); |
| border: 1px solid rgba(255, 255, 255, 0.05); |
| } |
| .glass-input { |
| background: rgba(0, 0, 0, 0.2); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| } |
| .glass-input:focus { |
| border-color: #0A84FF; |
| outline: none; |
| box-shadow: 0 0 10px rgba(10, 132, 255, 0.2); |
| } |
| </style> |
| </head> |
| <body class="text-gray-200 min-h-screen font-sans"> |
| <div class="max-w-6xl mx-auto px-4 py-8"> |
| |
| |
| <header class="flex items-center justify-between mb-12 py-4 border-b border-white/5"> |
| <div class="flex items-center gap-3"> |
| <div class="w-10 h-10 rounded-xl bg-gradient-to-br from-ai-blue to-ai-cyan flex items-center justify-center shadow-lg shadow-ai-blue/20"> |
| <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path></svg> |
| </div> |
| <div> |
| <h1 class="text-2xl font-bold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-400">Speech Intelligence and Intent Detection</h1> |
| <p class="text-xs text-gray-500">Standalone Diagnostics & Integration Client</p> |
| </div> |
| </div> |
| <div class="flex items-center gap-3"> |
| <span class="text-xs text-gray-400">API Status:</span> |
| <span id="api-status-badge" class="px-2.5 py-1 rounded-full text-xs font-semibold bg-yellow-500/10 text-yellow-500 flex items-center gap-1.5 border border-yellow-500/20"> |
| <span class="w-1.5 h-1.5 rounded-full bg-yellow-500 animate-pulse"></span> Checking Connection |
| </span> |
| </div> |
| </header> |
|
|
| |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> |
| |
| |
| <div class="space-y-8"> |
| |
| |
| <div class="glass rounded-2xl p-6 shadow-xl space-y-4"> |
| <h2 class="text-lg font-semibold text-white flex items-center gap-2"> |
| <svg class="w-5 h-5 text-ai-blue" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path></svg> |
| Text Extraction & Lead Scoring |
| </h2> |
| <p class="text-xs text-gray-400">Type or paste a sales conversation transcript to extract buyer intent, budget, brands, objections, and score conversion probability.</p> |
| <textarea id="text-input" class="w-full h-36 rounded-xl glass-input px-4 py-3 text-sm text-gray-300 placeholder-gray-600 focus:ring-1 focus:ring-ai-blue transition" placeholder="John: Hello, my name is John. I am looking for a laptop. My budget is around 60,000 INR. I need it this week for my college..."></textarea> |
| <button id="btn-analyze-text" class="w-full py-3 rounded-xl bg-ai-blue text-white font-medium hover:bg-ai-blue/90 shadow-lg shadow-ai-blue/20 transition-all"> |
| Analyze Conversation Text |
| </button> |
| </div> |
|
|
| |
| <div class="glass rounded-2xl p-6 shadow-xl space-y-4"> |
| <h2 class="text-lg font-semibold text-white flex items-center gap-2"> |
| <svg class="w-5 h-5 text-ai-cyan" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path></svg> |
| Audio Upload & Speech Pipeline |
| </h2> |
| <p class="text-xs text-gray-400">Upload a WAV, MP3, or M4A audio file. It will be transcribed via Whisper, diarized by Pyannote, and analyzed for structured signals.</p> |
| |
| <div class="border border-dashed border-white/10 hover:border-ai-cyan/50 rounded-xl p-8 flex flex-col items-center justify-center cursor-pointer transition relative" onclick="document.getElementById('file-input').click()"> |
| <input type="file" id="file-input" class="hidden" accept="audio/*"> |
| <svg class="w-10 h-10 text-gray-600 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"></path></svg> |
| <span id="file-label" class="text-sm text-gray-400">Click to select audio file</span> |
| <span class="text-xs text-gray-600 mt-1">Supports wav, mp3, m4a, etc.</span> |
| </div> |
|
|
| <button id="btn-upload-audio" class="w-full py-3 rounded-xl bg-ai-cyan text-black font-semibold hover:bg-ai-cyan/90 shadow-lg shadow-ai-cyan/10 transition-all"> |
| Upload & Analyze Audio |
| </button> |
| |
| |
| <div id="job-status-container" class="hidden p-4 rounded-xl border border-white/5 bg-black/40 flex items-center justify-between"> |
| <div class="flex items-center gap-3"> |
| <div class="w-2.5 h-2.5 rounded-full bg-ai-cyan animate-pulse"></div> |
| <div> |
| <p class="text-xs text-gray-400" id="job-id-label">Job ID: ...</p> |
| <p class="text-sm font-semibold text-white" id="job-status-label">Status: pending</p> |
| </div> |
| </div> |
| <span class="text-xs text-gray-500" id="job-timer">0s elapsed</span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="space-y-8"> |
| |
| |
| <div class="grid grid-cols-2 gap-4"> |
| <div class="glass rounded-xl p-4 flex flex-col justify-between"> |
| <span class="text-xs text-gray-400 uppercase tracking-wider font-semibold">Conversion Prob</span> |
| <span id="metric-conversion-prob" class="text-3xl font-extrabold text-ai-blue mt-2">--%</span> |
| </div> |
| <div class="glass rounded-xl p-4 flex flex-col justify-between"> |
| <span class="text-xs text-gray-400 uppercase tracking-wider font-semibold">Dominant Sentiment</span> |
| <span id="metric-sentiment" class="text-3xl font-extrabold text-ai-purple mt-2">--</span> |
| </div> |
| </div> |
|
|
| |
| <div class="glass rounded-2xl p-6 shadow-xl flex-1 flex flex-col min-h-[450px]"> |
| <div class="flex items-center justify-between border-b border-white/5 pb-4 mb-4"> |
| <h2 class="text-lg font-semibold text-white">Analysis Results</h2> |
| <div class="flex gap-2"> |
| <button onclick="switchTab('json')" id="tab-btn-json" class="px-3 py-1.5 text-xs rounded-lg bg-white/10 text-white font-medium">Raw JSON</button> |
| <button onclick="switchTab('transcript')" id="tab-btn-transcript" class="px-3 py-1.5 text-xs rounded-lg text-gray-400 hover:text-white font-medium">Transcript</button> |
| <button onclick="switchTab('alerts')" id="tab-btn-alerts" class="px-3 py-1.5 text-xs rounded-lg text-gray-400 hover:text-white font-medium">Alerts</button> |
| </div> |
| </div> |
|
|
| |
| <div id="tab-json" class="flex-1 overflow-auto max-h-[360px] text-xs font-mono bg-black/50 p-4 rounded-xl border border-white/5"> |
| <pre class="text-gray-400" id="json-output">// Run an analysis to see raw JSON output</pre> |
| </div> |
|
|
| |
| <div id="tab-transcript" class="hidden flex-1 overflow-auto max-h-[360px] space-y-3 text-sm"> |
| <div class="text-center text-gray-500 text-xs py-8" id="transcript-placeholder"> |
| No transcript available yet. |
| </div> |
| <div id="transcript-turns" class="space-y-4"></div> |
| </div> |
|
|
| |
| <div id="tab-alerts" class="hidden flex-1 overflow-auto max-h-[360px] space-y-3"> |
| <div class="text-center text-gray-500 text-xs py-8" id="alerts-placeholder"> |
| No follow-up alerts detected. |
| </div> |
| <div id="alerts-list" class="space-y-3"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| const BACKEND_BASE = 'http://localhost:8000'; |
| let activeTab = 'json'; |
| let fileToUpload = null; |
| |
| |
| async function checkHealth() { |
| const badge = document.getElementById('api-status-badge'); |
| try { |
| const response = await fetch(`${BACKEND_BASE}/api/health`); |
| if (response.ok) { |
| badge.className = 'px-2.5 py-1 rounded-full text-xs font-semibold bg-green-500/10 text-green-500 flex items-center gap-1.5 border border-green-500/20'; |
| badge.innerHTML = '<span class="w-1.5 h-1.5 rounded-full bg-green-500"></span> Operational'; |
| } else { |
| throw new Error('Not ok'); |
| } |
| } catch (err) { |
| badge.className = 'px-2.5 py-1 rounded-full text-xs font-semibold bg-red-500/10 text-red-500 flex items-center gap-1.5 border border-red-500/20'; |
| badge.innerHTML = '<span class="w-1.5 h-1.5 rounded-full bg-red-500"></span> Connection Offline'; |
| } |
| } |
| |
| |
| function switchTab(tab) { |
| activeTab = tab; |
| const jsonTab = document.getElementById('tab-json'); |
| const transTab = document.getElementById('tab-transcript'); |
| const alertTab = document.getElementById('tab-alerts'); |
| |
| const btnJson = document.getElementById('tab-btn-json'); |
| const btnTrans = document.getElementById('tab-btn-transcript'); |
| const btnAlert = document.getElementById('tab-btn-alerts'); |
| |
| jsonTab.classList.add('hidden'); |
| transTab.classList.add('hidden'); |
| alertTab.classList.add('hidden'); |
| |
| btnJson.className = 'px-3 py-1.5 text-xs rounded-lg text-gray-400 hover:text-white font-medium'; |
| btnTrans.className = 'px-3 py-1.5 text-xs rounded-lg text-gray-400 hover:text-white font-medium'; |
| btnAlert.className = 'px-3 py-1.5 text-xs rounded-lg text-gray-400 hover:text-white font-medium'; |
| |
| if (tab === 'json') { |
| jsonTab.classList.remove('hidden'); |
| btnJson.className = 'px-3 py-1.5 text-xs rounded-lg bg-white/10 text-white font-medium'; |
| } else if (tab === 'transcript') { |
| transTab.classList.remove('hidden'); |
| btnTrans.className = 'px-3 py-1.5 text-xs rounded-lg bg-white/10 text-white font-medium'; |
| } else if (tab === 'alerts') { |
| alertTab.classList.remove('hidden'); |
| btnAlert.className = 'px-3 py-1.5 text-xs rounded-lg bg-white/10 text-white font-medium'; |
| } |
| } |
| |
| |
| function updateResults(data) { |
| |
| document.getElementById('json-output').innerText = JSON.stringify(data, null, 2); |
| |
| |
| const conversionScore = data.conversionScore || data.prediction || {}; |
| const prob = conversionScore.probability; |
| if (prob !== undefined) { |
| document.getElementById('metric-conversion-prob').innerText = `${Math.round(prob * 100)}%`; |
| } else { |
| document.getElementById('metric-conversion-prob').innerText = '--%'; |
| } |
| |
| const dominant = (data.summary && data.summary.dominant) || 'Neutral'; |
| document.getElementById('metric-sentiment').innerText = dominant; |
| |
| |
| const turns = data.diarizedTranscript || []; |
| const transContainer = document.getElementById('transcript-turns'); |
| const transPlaceholder = document.getElementById('transcript-placeholder'); |
| |
| transContainer.innerHTML = ''; |
| if (turns.length > 0) { |
| transPlaceholder.classList.add('hidden'); |
| turns.forEach(turn => { |
| const div = document.createElement('div'); |
| const isAgent = turn.speaker === 'Agent'; |
| div.className = `flex flex-col gap-1 max-w-[85%] ${isAgent ? 'ml-auto items-end' : 'mr-auto items-start'}`; |
| div.innerHTML = ` |
| <span class="text-xs text-gray-500 font-semibold">${turn.speaker}</span> |
| <div class="px-4 py-2.5 rounded-2xl text-sm ${isAgent ? 'bg-ai-blue text-white rounded-tr-none' : 'bg-white/5 text-gray-200 rounded-tl-none border border-white/5'}"> |
| ${turn.text} |
| </div> |
| `; |
| transContainer.appendChild(div); |
| }); |
| } else { |
| transPlaceholder.classList.remove('hidden'); |
| } |
| |
| |
| const alerts = data.followUpAlerts || []; |
| const alertsContainer = document.getElementById('alerts-list'); |
| const alertsPlaceholder = document.getElementById('alerts-placeholder'); |
| |
| alertsContainer.innerHTML = ''; |
| if (alerts.length > 0) { |
| alertsPlaceholder.classList.add('hidden'); |
| alerts.forEach(alert => { |
| const div = document.createElement('div'); |
| const prioColor = alert.priority === 'High' ? 'text-red-500 bg-red-500/10 border-red-500/20' : alert.priority === 'Medium' ? 'text-yellow-500 bg-yellow-500/10 border-yellow-500/20' : 'text-green-500 bg-green-500/10 border-green-500/20'; |
| div.className = 'p-4 rounded-xl border border-white/5 bg-white/5 space-y-2'; |
| div.innerHTML = ` |
| <div class="flex items-center justify-between"> |
| <span class="text-sm font-semibold text-white">${alert.customer_name || 'Customer'}</span> |
| <span class="px-2 py-0.5 rounded text-[10px] font-bold uppercase border ${prioColor}">${alert.priority} Priority</span> |
| </div> |
| <p class="text-xs text-gray-400"><strong class="text-gray-300">Action:</strong> ${alert.action_needed}</p> |
| <p class="text-xs text-gray-500"><strong class="text-gray-400">Reason:</strong> ${alert.reason}</p> |
| `; |
| alertsContainer.appendChild(div); |
| }); |
| } else { |
| alertsPlaceholder.classList.remove('hidden'); |
| } |
| } |
| |
| |
| document.getElementById('btn-analyze-text').addEventListener('click', async () => { |
| const text = document.getElementById('text-input').value; |
| if (!text.trim()) { |
| alert('Please enter some text to analyze.'); |
| return; |
| } |
| |
| const btn = document.getElementById('btn-analyze-text'); |
| btn.innerText = 'Analyzing...'; |
| btn.disabled = true; |
| |
| try { |
| const response = await fetch(`${BACKEND_BASE}/api/analyze`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ |
| text: text, |
| sourceName: 'standalone-browser-client' |
| }) |
| }); |
| |
| if (response.ok) { |
| const result = await response.json(); |
| updateResults(result); |
| } else { |
| alert(`Analysis failed with status code ${response.status}`); |
| } |
| } catch (err) { |
| alert(`Error communicating with backend: ${err.message}`); |
| } finally { |
| btn.innerText = 'Analyze Conversation Text'; |
| btn.disabled = false; |
| } |
| }); |
| |
| |
| document.getElementById('file-input').addEventListener('change', (e) => { |
| const file = e.target.files[0]; |
| if (file) { |
| fileToUpload = file; |
| document.getElementById('file-label').innerText = `${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)`; |
| } |
| }); |
| |
| |
| document.getElementById('btn-upload-audio').addEventListener('click', async () => { |
| if (!fileToUpload) { |
| alert('Please select an audio file first.'); |
| return; |
| } |
| |
| const btn = document.getElementById('btn-upload-audio'); |
| btn.innerText = 'Uploading...'; |
| btn.disabled = true; |
| |
| const container = document.getElementById('job-status-container'); |
| const label = document.getElementById('job-status-label'); |
| const idLabel = document.getElementById('job-id-label'); |
| const timerLabel = document.getElementById('job-timer'); |
| |
| container.classList.remove('hidden'); |
| label.innerText = 'Status: uploading...'; |
| idLabel.innerText = 'Job ID: pending...'; |
| |
| let start = Date.now(); |
| let timer = setInterval(() => { |
| timerLabel.innerText = `${Math.round((Date.now() - start) / 1000)}s elapsed`; |
| }, 1000); |
| |
| try { |
| const formData = new FormData(); |
| formData.append('audio', fileToUpload); |
| |
| const response = await fetch(`${BACKEND_BASE}/api/upload`, { |
| method: 'POST', |
| body: formData |
| }); |
| |
| if (!response.ok) { |
| throw new Error(`Upload failed with status code ${response.status}`); |
| } |
| |
| const data = await response.json(); |
| const jobId = data.job_id; |
| idLabel.innerText = `Job ID: ${jobId}`; |
| label.innerText = `Status: ${data.status}`; |
| |
| |
| while (true) { |
| await new Promise(resolve => setTimeout(resolve, 2000)); |
| const jobRes = await fetch(`${BACKEND_BASE}/api/jobs/${jobId}`); |
| |
| if (!jobRes.ok) { |
| throw new Error(`Failed to fetch job status: ${jobRes.status}`); |
| } |
| |
| const jobData = await jobRes.json(); |
| label.innerText = `Status: ${jobData.status}`; |
| |
| if (jobData.status === 'completed') { |
| updateResults(jobData.result); |
| clearInterval(timer); |
| break; |
| } else if (jobData.status === 'failed') { |
| clearInterval(timer); |
| alert(`Processing failed: ${jobData.error}`); |
| break; |
| } |
| } |
| } catch (err) { |
| clearInterval(timer); |
| alert(`Error: ${err.message}`); |
| label.innerText = 'Status: error'; |
| } finally { |
| btn.innerText = 'Upload & Analyze Audio'; |
| btn.disabled = false; |
| } |
| }); |
| |
| |
| checkHealth(); |
| setInterval(checkHealth, 10000); |
| </script> |
| </body> |
| </html> |
|
|