enterprise-audio-intelligence / test_client.html
Akbub's picture
deploy: Nexus AI v0.2.0 - SAP C4C Lead Creation UI included in fresh frontend build
d1f3f31
Raw
History Blame Contribute Delete
19.3 kB
<!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 -->
<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>
<!-- Main Content Grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Input Panel -->
<div class="space-y-8">
<!-- Text Analysis -->
<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>
<!-- Audio Upload -->
<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>
<!-- Job Status -->
<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>
<!-- Output / Result Panel -->
<div class="space-y-8">
<!-- Quick Insight Metrics -->
<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>
<!-- Details Tab -->
<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>
<!-- Tab: JSON -->
<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>
<!-- Tab: Transcript -->
<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>
<!-- Tab: Alerts -->
<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;
// Check backend health
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';
}
}
// Tab switcher
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';
}
}
// Process result
function updateResults(data) {
// JSON
document.getElementById('json-output').innerText = JSON.stringify(data, null, 2);
// Metrics
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;
// Transcript
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');
}
// Alerts
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');
}
}
// Text analysis handler
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;
}
});
// File selection handler
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)`;
}
});
// Audio upload handler
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}`;
// Polling loop
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;
}
});
// Init
checkHealth();
setInterval(checkHealth, 10000);
</script>
</body>
</html>