| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
| var VOICE_ENDPOINTS = { |
| engage: '/api/v1/voice/engage', |
| audio: '/api/v1/voice/audio', |
| health: '/api/v1/voice/health', |
| }; |
| var API_KEY = 'sVlunn0LMQZNAkRYqZB-f1-Ye7rgzjB_E3b1gNxnUV8'; |
| var RECORDING_MIME_TYPES = [ |
| 'audio/webm;codecs=opus', |
| 'audio/webm', |
| 'audio/ogg;codecs=opus', |
| 'audio/mp4', |
| ]; |
|
|
| |
| |
| |
|
|
| var appState = { |
| sessionId: null, |
| mediaRecorder: null, |
| audioStream: null, |
| audioChunks: [], |
| recordingMimeType: '', |
| isRecording: false, |
| isProcessing: false, |
| currentAudio: null, |
| extractedIntel: { |
| upi_ids: [], |
| bank_accounts: [], |
| ifsc_codes: [], |
| phone_numbers: [], |
| phishing_links: [], |
| }, |
| turnCount: 0, |
| }; |
|
|
| |
| |
| |
|
|
| document.addEventListener('DOMContentLoaded', function () { |
| checkVoiceHealth(); |
| setInterval(checkVoiceHealth, 30000); |
| }); |
|
|
| |
| |
| |
|
|
| async function checkVoiceHealth() { |
| var dot = document.getElementById('healthDot'); |
| var label = document.getElementById('healthLabel'); |
| try { |
| var response = await fetch(VOICE_ENDPOINTS.health, { |
| headers: { 'x-api-key': API_KEY }, |
| }); |
| var data = await response.json(); |
| dot.className = 'health-dot ' + data.status; |
| label.textContent = data.status.charAt(0).toUpperCase() + data.status.slice(1); |
| } catch (_err) { |
| dot.className = 'health-dot unhealthy'; |
| label.textContent = 'Offline'; |
| } |
| } |
|
|
| |
| |
| |
|
|
| function toggleRecording() { |
| if (appState.isProcessing) return; |
|
|
| if (appState.isRecording) { |
| stopAndSend(); |
| } else { |
| startRecording(); |
| } |
| } |
|
|
| function getSupportedMimeType() { |
| for (var i = 0; i < RECORDING_MIME_TYPES.length; i++) { |
| if (MediaRecorder.isTypeSupported(RECORDING_MIME_TYPES[i])) { |
| return RECORDING_MIME_TYPES[i]; |
| } |
| } |
| return ''; |
| } |
|
|
| async function startRecording() { |
| if (appState.isRecording || appState.isProcessing) return; |
|
|
| if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { |
| showError('Your browser does not support audio recording.'); |
| return; |
| } |
|
|
| |
| stopCurrentAudio(); |
|
|
| try { |
| var stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
| var mimeType = getSupportedMimeType(); |
| var options = mimeType ? { mimeType: mimeType } : {}; |
|
|
| appState.recordingMimeType = mimeType || 'audio/webm'; |
| appState.audioChunks = []; |
| appState.audioStream = stream; |
| appState.mediaRecorder = new MediaRecorder(stream, options); |
|
|
| appState.mediaRecorder.addEventListener('dataavailable', function (event) { |
| if (event.data && event.data.size > 0) { |
| appState.audioChunks.push(event.data); |
| } |
| }); |
|
|
| appState.mediaRecorder.addEventListener('stop', function () { |
| |
| if (appState.audioStream) { |
| appState.audioStream.getTracks().forEach(function (t) { t.stop(); }); |
| appState.audioStream = null; |
| } |
| handleRecordingComplete(); |
| }); |
|
|
| appState.mediaRecorder.start(); |
| appState.isRecording = true; |
| setUIState('recording'); |
| hideError(); |
| } catch (err) { |
| if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') { |
| showError('Microphone access denied. Please allow microphone permission.'); |
| } else if (err.name === 'NotFoundError') { |
| showError('No microphone detected. Please connect a microphone.'); |
| } else { |
| showError('Failed to start recording: ' + err.message); |
| } |
| } |
| } |
|
|
| function stopAndSend() { |
| if (!appState.isRecording || !appState.mediaRecorder) return; |
| appState.mediaRecorder.stop(); |
| appState.isRecording = false; |
| } |
|
|
| function handleRecordingComplete() { |
| if (appState.audioChunks.length === 0) { |
| showError('No audio captured. Please try again.'); |
| setUIState('ready'); |
| return; |
| } |
|
|
| var blob = new Blob(appState.audioChunks, { type: appState.recordingMimeType }); |
| var ext = appState.recordingMimeType.indexOf('webm') !== -1 ? 'webm' |
| : appState.recordingMimeType.indexOf('ogg') !== -1 ? 'ogg' |
| : appState.recordingMimeType.indexOf('mp4') !== -1 ? 'mp4' |
| : 'webm'; |
|
|
| var file = new File([blob], 'recording.' + ext, { type: appState.recordingMimeType }); |
| sendAudioToAPI(file); |
| } |
|
|
| |
| |
| |
|
|
| function uploadAudio() { |
| if (appState.isRecording || appState.isProcessing) return; |
| document.getElementById('audioFileInput').click(); |
| } |
|
|
| function handleFileSelect(event) { |
| var files = event.target.files; |
| if (!files || files.length === 0) return; |
|
|
| var file = files[0]; |
| if (!file.type.startsWith('audio/') && file.type !== 'application/octet-stream') { |
| showError('Please select a valid audio file.'); |
| event.target.value = ''; |
| return; |
| } |
| if (file.size > 25 * 1024 * 1024) { |
| showError('File too large. Maximum 25 MB.'); |
| event.target.value = ''; |
| return; |
| } |
|
|
| sendAudioToAPI(file); |
| event.target.value = ''; |
| } |
|
|
| |
| |
| |
|
|
| async function sendAudioToAPI(audioFile) { |
| if (appState.isProcessing) return; |
|
|
| appState.isProcessing = true; |
| setUIState('processing'); |
|
|
| clearWelcome(); |
| addMessage('system', 'Listening...'); |
|
|
| var formData = new FormData(); |
| formData.append('audio_file', audioFile); |
|
|
| var languageSelect = document.getElementById('languageSelect'); |
| var language = languageSelect ? languageSelect.value : 'auto'; |
| if (language && language !== 'auto') { |
| formData.append('language', language); |
| } |
| if (appState.sessionId) { |
| formData.append('session_id', appState.sessionId); |
| } |
|
|
| try { |
| var startTime = Date.now(); |
| var response = await fetch(VOICE_ENDPOINTS.engage, { |
| method: 'POST', |
| headers: { 'x-api-key': API_KEY }, |
| body: formData, |
| }); |
|
|
| var elapsed = Date.now() - startTime; |
|
|
| if (!response.ok) { |
| var errorBody = null; |
| try { errorBody = await response.json(); } catch (_e) { } |
| var errorMsg = (errorBody && errorBody.detail) |
| ? (typeof errorBody.detail === 'string' ? errorBody.detail : errorBody.detail.message || JSON.stringify(errorBody.detail)) |
| : 'Server returned HTTP ' + response.status; |
| throw new Error(errorMsg); |
| } |
|
|
| var data = await response.json(); |
| handleAPIResponse(data, elapsed); |
|
|
| } catch (err) { |
| removeLastSystemMessage(); |
| addMessage('system', 'Error: ' + err.message); |
| showError(err.message); |
| } finally { |
| appState.isProcessing = false; |
| setUIState('ready'); |
| } |
| } |
|
|
| |
| |
| |
|
|
| function handleAPIResponse(data, elapsedMs) { |
| appState.sessionId = data.session_id; |
| appState.turnCount = data.turn_count || 0; |
| hideError(); |
| removeLastSystemMessage(); |
|
|
| updateSessionDisplay(); |
| updateResponseTime(elapsedMs, data.processing_time_ms); |
|
|
| |
| if (data.transcription && data.transcription.text) { |
| addMessage('user', data.transcription.text, { |
| language: data.transcription.language, |
| confidence: data.transcription.confidence, |
| }); |
| } |
|
|
| |
| |
| var audioUrl = null; |
| if (data.ai_reply_audio_url) { |
| audioUrl = data.ai_reply_audio_url.startsWith('http') |
| ? data.ai_reply_audio_url |
| : data.ai_reply_audio_url; |
| } |
|
|
| addMessage('ai', data.ai_reply_text, { |
| audioUrl: audioUrl, |
| turnCount: data.turn_count, |
| scamType: data.scam_type, |
| }); |
|
|
| |
| if (audioUrl) { |
| autoPlayAudio(audioUrl); |
| } |
|
|
| updateTranscriptionMetadata(data.transcription); |
| updateDetection(data); |
| updateVoiceFraud(data.voice_fraud); |
| updateIntelligence(data.extracted_intelligence); |
| } |
|
|
| |
| |
| |
| function autoPlayAudio(url) { |
| stopCurrentAudio(); |
|
|
| var audio = new Audio(url); |
| appState.currentAudio = audio; |
|
|
| audio.addEventListener('ended', function () { |
| appState.currentAudio = null; |
| }); |
|
|
| audio.addEventListener('error', function () { |
| appState.currentAudio = null; |
| }); |
|
|
| |
| var playPromise = audio.play(); |
| if (playPromise && playPromise.catch) { |
| playPromise.catch(function () { |
| |
| }); |
| } |
| } |
|
|
| function stopCurrentAudio() { |
| if (appState.currentAudio) { |
| try { |
| appState.currentAudio.pause(); |
| appState.currentAudio.currentTime = 0; |
| } catch (_e) { } |
| appState.currentAudio = null; |
| } |
| } |
|
|
| |
| |
| |
|
|
| function clearWelcome() { |
| var welcome = document.getElementById('conversationWelcome'); |
| if (welcome) welcome.remove(); |
| } |
|
|
| function removeLastSystemMessage() { |
| var container = document.getElementById('conversationContainer'); |
| var msgs = container.querySelectorAll('.message.system'); |
| if (msgs.length > 0) { |
| msgs[msgs.length - 1].remove(); |
| } |
| } |
|
|
| function addMessage(type, text, meta) { |
| meta = meta || {}; |
| var container = document.getElementById('conversationContainer'); |
|
|
| var msgEl = document.createElement('div'); |
| msgEl.className = 'message ' + type; |
|
|
| if (type === 'system') { |
| msgEl.innerHTML = '<div class="message-bubble">' + escapeHtml(text) + '</div>'; |
| container.appendChild(msgEl); |
| container.scrollTop = container.scrollHeight; |
| return; |
| } |
|
|
| var avatar = type === 'user' ? '🦹' : '🤖'; |
| var senderName = type === 'user' ? 'You' : 'AI Agent'; |
|
|
| var metaHtml = ''; |
| if (type === 'user' && meta.language) { |
| metaHtml = '<div class="message-meta">' |
| + '<span>' + escapeHtml(meta.language) + '</span>' |
| + '<span>' + formatPercent(meta.confidence) + '</span>' |
| + '</div>'; |
| } |
| if (type === 'ai' && meta.turnCount) { |
| metaHtml = '<div class="message-meta">' |
| + '<span>Turn ' + meta.turnCount + '</span>' |
| + (meta.scamType ? '<span>' + escapeHtml(meta.scamType) + '</span>' : '') |
| + '</div>'; |
| } |
|
|
| |
| var audioHtml = ''; |
| if (type === 'ai' && meta.audioUrl) { |
| audioHtml = '<div class="ai-audio-player">' |
| + '<audio controls preload="auto" src="' + escapeAttr(meta.audioUrl) + '"></audio>' |
| + '</div>'; |
| } |
|
|
| msgEl.innerHTML = '<div class="message-avatar">' + avatar + '</div>' |
| + '<div class="message-body">' |
| + '<div class="message-sender">' + escapeHtml(senderName) + '</div>' |
| + '<div class="message-bubble">' + escapeHtml(text) + '</div>' |
| + audioHtml |
| + metaHtml |
| + '</div>'; |
|
|
| container.appendChild(msgEl); |
| container.scrollTop = container.scrollHeight; |
| } |
|
|
| |
| |
| |
|
|
| function setUIState(state) { |
| var btn = document.getElementById('btnTalk'); |
| var icon = document.getElementById('talkButtonIcon'); |
| var label = document.getElementById('talkButtonLabel'); |
| var statusEl = document.getElementById('recordingStatus'); |
| var statusLabel = document.getElementById('recordingLabel'); |
| var uploadBtn = document.getElementById('btnUploadAudio'); |
|
|
| btn.classList.remove('recording', 'processing'); |
| statusEl.className = 'recording-status ' + state; |
|
|
| if (state === 'ready') { |
| icon.innerHTML = '🎤'; |
| label.textContent = 'Tap to Talk'; |
| statusLabel.textContent = 'Ready'; |
| btn.disabled = false; |
| uploadBtn.disabled = false; |
| } else if (state === 'recording') { |
| btn.classList.add('recording'); |
| icon.innerHTML = '⏹'; |
| label.textContent = 'Tap to Send'; |
| statusLabel.textContent = 'Recording...'; |
| btn.disabled = false; |
| uploadBtn.disabled = true; |
| } else if (state === 'processing') { |
| btn.classList.add('processing'); |
| icon.innerHTML = '⏳'; |
| label.textContent = 'Thinking...'; |
| statusLabel.textContent = 'Processing...'; |
| btn.disabled = true; |
| uploadBtn.disabled = true; |
| } |
| } |
|
|
| |
| |
| |
|
|
| function updateTranscriptionMetadata(transcription) { |
| if (!transcription) return; |
| setText('metaTranscription', transcription.text || '-'); |
| setText('metaASRLanguage', transcription.language || '-'); |
| setText('metaASRConfidence', formatPercent(transcription.confidence)); |
| } |
|
|
| function updateDetection(data) { |
| var container = document.getElementById('detectionResult'); |
| var isScam = data.scam_detected; |
| var confidence = data.scam_confidence || 0; |
| var percent = (confidence * 100).toFixed(0); |
|
|
| container.className = 'detection-result ' + (isScam ? 'scam' : 'safe'); |
| container.innerHTML = '<div class="detection-badge">' |
| + (isScam ? 'SCAM DETECTED' : 'SAFE') |
| + '</div>' |
| + '<div class="confidence-bar"><div class="confidence-fill" style="width:' + percent + '%"></div></div>' |
| + '<div class="detection-details">' |
| + 'Confidence: ' + percent + '%' |
| + (data.scam_type ? ' | Type: ' + escapeHtml(data.scam_type) : '') |
| + ' | Turn: ' + (data.turn_count || 0) |
| + '</div>'; |
| } |
|
|
| function updateVoiceFraud(fraud) { |
| var section = document.getElementById('voiceFraudSection'); |
| var container = document.getElementById('voiceFraudResult'); |
| if (!fraud) { section.style.display = 'none'; return; } |
|
|
| section.style.display = 'block'; |
| var riskClass = fraud.risk_level === 'high' ? 'high-risk' |
| : fraud.risk_level === 'medium' ? 'medium-risk' : 'low-risk'; |
|
|
| container.className = 'fraud-result ' + riskClass; |
| container.innerHTML = '<div class="metadata-grid">' |
| + '<div class="metadata-card"><span class="metadata-label">Synthetic</span>' |
| + '<span class="metadata-value ' + (fraud.is_synthetic ? 'danger' : 'success') + '">' |
| + (fraud.is_synthetic ? 'Yes' : 'No') + '</span></div>' |
| + '<div class="metadata-card"><span class="metadata-label">Confidence</span>' |
| + '<span class="metadata-value">' + formatPercent(fraud.confidence) + '</span></div>' |
| + '<div class="metadata-card full-width"><span class="metadata-label">Risk</span>' |
| + '<span class="metadata-value ' + riskClass.replace('-risk', '') + '">' |
| + fraud.risk_level.toUpperCase() + '</span></div></div>'; |
| } |
|
|
| function updateIntelligence(intel) { |
| if (!intel) return; |
| mergeIntelArray('upi_ids', intel.upi_ids); |
| mergeIntelArray('bank_accounts', intel.bank_accounts); |
| mergeIntelArray('ifsc_codes', intel.ifsc_codes); |
| mergeIntelArray('phone_numbers', intel.phone_numbers); |
| mergeIntelArray('phishing_links', intel.phishing_links); |
|
|
| renderIntelValue('intelUPI', appState.extractedIntel.upi_ids); |
| renderIntelValue('intelBank', appState.extractedIntel.bank_accounts); |
| renderIntelValue('intelPhone', appState.extractedIntel.phone_numbers); |
| renderIntelValue('intelLinks', appState.extractedIntel.phishing_links); |
|
|
| var conf = intel.extraction_confidence; |
| var confEl = document.getElementById('intelConfidence'); |
| if (confEl) { |
| confEl.textContent = conf != null ? formatPercent(conf) : '-'; |
| confEl.className = 'intel-value' + (conf > 0 ? ' has-data' : ''); |
| } |
| } |
|
|
| function mergeIntelArray(key, incoming) { |
| if (!incoming || !incoming.length) return; |
| var set = {}; |
| appState.extractedIntel[key].forEach(function (v) { set[v] = true; }); |
| incoming.forEach(function (v) { set[v] = true; }); |
| appState.extractedIntel[key] = Object.keys(set); |
| } |
|
|
| function renderIntelValue(elementId, values) { |
| var el = document.getElementById(elementId); |
| if (!el) return; |
| if (values && values.length > 0) { |
| el.textContent = values.join(', '); |
| el.className = 'intel-value has-data'; |
| } else { |
| el.textContent = '-'; |
| el.className = 'intel-value'; |
| } |
| } |
|
|
| |
| |
| |
|
|
| function updateSessionDisplay() { |
| var el = document.getElementById('sessionIdDisplay'); |
| el.value = appState.sessionId || ''; |
| } |
|
|
| function updateResponseTime(clientMs, serverMs) { |
| var el = document.getElementById('responseTime'); |
| var parts = []; |
| if (clientMs != null) parts.push('RTT: ' + clientMs + 'ms'); |
| if (serverMs != null) parts.push('Server: ' + serverMs + 'ms'); |
| el.textContent = parts.length > 0 ? parts.join(' | ') : '-'; |
| } |
|
|
| function newSession() { |
| stopCurrentAudio(); |
| appState.sessionId = null; |
| appState.turnCount = 0; |
| appState.extractedIntel = { |
| upi_ids: [], bank_accounts: [], ifsc_codes: [], |
| phone_numbers: [], phishing_links: [], |
| }; |
|
|
| var container = document.getElementById('conversationContainer'); |
| container.innerHTML = '<div id="conversationWelcome" class="conversation-welcome">' |
| + '<div class="welcome-icon">🎤</div>' |
| + '<h3>Voice Honeypot Testing</h3>' |
| + '<p>Tap the microphone to speak. When you stop, the AI will ' |
| + 'transcribe your words, detect scam intent, and reply with voice automatically.</p>' |
| + '</div>'; |
|
|
| updateSessionDisplay(); |
| document.getElementById('detectionResult').className = 'detection-result'; |
| document.getElementById('detectionResult').innerHTML = '<div class="no-data">Send audio to see detection results</div>'; |
| setText('metaTranscription', '-'); |
| setText('metaASRLanguage', '-'); |
| setText('metaASRConfidence', '-'); |
| document.getElementById('voiceFraudSection').style.display = 'none'; |
| renderIntelValue('intelUPI', []); |
| renderIntelValue('intelBank', []); |
| renderIntelValue('intelPhone', []); |
| renderIntelValue('intelLinks', []); |
| var confEl = document.getElementById('intelConfidence'); |
| if (confEl) { confEl.textContent = '-'; confEl.className = 'intel-value'; } |
| document.getElementById('responseTime').textContent = '-'; |
| hideError(); |
| } |
|
|
| |
| |
| |
|
|
| function showError(message) { |
| var banner = document.getElementById('errorBanner'); |
| banner.textContent = message; |
| banner.classList.add('visible'); |
| } |
|
|
| function hideError() { |
| var banner = document.getElementById('errorBanner'); |
| banner.classList.remove('visible'); |
| banner.textContent = ''; |
| } |
|
|
| |
| |
| |
|
|
| function escapeHtml(text) { |
| var div = document.createElement('div'); |
| div.textContent = text || ''; |
| return div.innerHTML; |
| } |
|
|
| function escapeAttr(text) { |
| return (text || '').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>'); |
| } |
|
|
| function formatPercent(value) { |
| if (value == null) return '-'; |
| return (value * 100).toFixed(1) + '%'; |
| } |
|
|
| function setText(id, text) { |
| var el = document.getElementById(id); |
| if (el) el.textContent = text; |
| } |
|
|