aillom-vox-client 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +272 -0
  3. package/dist/AillomVox.d.ts +36 -0
  4. package/dist/AillomVox.js +152 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +18 -0
  7. package/dist/types.d.ts +36 -0
  8. package/dist/types.js +2 -0
  9. package/docs/ASTERISK.md +411 -0
  10. package/docs/PROTOCOL.md +156 -0
  11. package/docs/PROVIDERS.md +40 -0
  12. package/docs/TOOLS.md +314 -0
  13. package/docs/TROUBLESHOOTING.md +86 -0
  14. package/docs/VOICES.md +219 -0
  15. package/docs/providers/AILLOMVOX.md +185 -0
  16. package/docs/providers/AWS.md +32 -0
  17. package/docs/providers/GEMINI.md +33 -0
  18. package/docs/providers/GROK.md +25 -0
  19. package/docs/providers/OPENAI.md +39 -0
  20. package/docs/providers/QWEN.md +27 -0
  21. package/docs/providers/ULTRAVOX.md +29 -0
  22. package/examples/01-basic/app.js +196 -0
  23. package/examples/01-basic/index.html +27 -0
  24. package/examples/02-advanced-dashboard/app.js +465 -0
  25. package/examples/02-advanced-dashboard/index.html +200 -0
  26. package/examples/02-advanced-dashboard/style.css +501 -0
  27. package/examples/03-smart-home/index.html +377 -0
  28. package/examples/04-customer-support/index.html +474 -0
  29. package/examples/sdk-usage.ts +44 -0
  30. package/integrations/n8n-nodes-aillomvox/README.md +56 -0
  31. package/integrations/n8n-nodes-aillomvox/credentials/AillomVoxApi.credentials.ts +29 -0
  32. package/integrations/n8n-nodes-aillomvox/dist/credentials/AillomVoxApi.credentials.js +30 -0
  33. package/integrations/n8n-nodes-aillomvox/dist/nodes/AillomVox/AillomVox.node.js +219 -0
  34. package/integrations/n8n-nodes-aillomvox/dist/nodes/AillomVox/aillomvox.svg +6 -0
  35. package/integrations/n8n-nodes-aillomvox/gulpfile.js +10 -0
  36. package/integrations/n8n-nodes-aillomvox/nodes/AillomVox/AillomVox.node.ts +229 -0
  37. package/integrations/n8n-nodes-aillomvox/nodes/AillomVox/aillomvox.svg +6 -0
  38. package/integrations/n8n-nodes-aillomvox/package-lock.json +11741 -0
  39. package/integrations/n8n-nodes-aillomvox/package.json +56 -0
  40. package/integrations/n8n-nodes-aillomvox/tsconfig.json +32 -0
  41. package/package.json +55 -0
  42. package/src/AillomVox.ts +169 -0
  43. package/src/index.ts +2 -0
  44. package/src/types.ts +50 -0
  45. package/tsconfig.json +23 -0
@@ -0,0 +1,465 @@
1
+ // Advanced Dashboard Client Logic - Universal for All Providers
2
+ const connectBtn = document.getElementById('connectBtn');
3
+ const disconnectBtn = document.getElementById('disconnectBtn');
4
+ const logsContainer = document.getElementById('logsContainer');
5
+ const transcriptContainer = document.getElementById('transcriptContainer');
6
+ const statusBadge = document.getElementById('connectionStatus');
7
+ const micLabel = document.getElementById('micLabel');
8
+ const callTimer = document.getElementById('callTimer');
9
+
10
+ // Inputs
11
+ const inputs = {
12
+ apikey: document.getElementById('apikey'),
13
+ provider: document.getElementById('provider'),
14
+ voice: document.getElementById('voice'),
15
+ language: document.getElementById('language'),
16
+ system_prompt: document.getElementById('system_prompt'),
17
+ sample_rate: document.getElementById('sample_rate'),
18
+ first_message: document.getElementById('first_message'),
19
+ tools: document.getElementById('toolsConfig')
20
+ };
21
+
22
+ let socket;
23
+ let audioContext;
24
+ let processor;
25
+ let mediaStream;
26
+ let callStartTime;
27
+ let timerInterval;
28
+
29
+ // 🎯 ULTRAVOX PATTERN: Track scheduled audio sources for instant barge-in clearing
30
+ let scheduledSources = [];
31
+
32
+ // --- Visualizer Setup ---
33
+ const canvas = document.getElementById('visualizer');
34
+ const ctx = canvas.getContext('2d');
35
+ let analyser;
36
+
37
+ function resizeCanvas() {
38
+ const rect = canvas.parentElement.getBoundingClientRect();
39
+ canvas.width = rect.width;
40
+ canvas.height = rect.height;
41
+ }
42
+
43
+ window.addEventListener('resize', resizeCanvas);
44
+
45
+ function setupVisualizer(stream) {
46
+ if (!audioContext) return;
47
+ resizeCanvas();
48
+ const source = audioContext.createMediaStreamSource(stream);
49
+ analyser = audioContext.createAnalyser();
50
+ analyser.fftSize = 256;
51
+ source.connect(analyser);
52
+ drawVisualizer();
53
+ }
54
+
55
+ function drawVisualizer() {
56
+ if (!analyser) return;
57
+ requestAnimationFrame(drawVisualizer);
58
+
59
+ const bufferLength = analyser.frequencyBinCount;
60
+ const dataArray = new Uint8Array(bufferLength);
61
+ analyser.getByteFrequencyData(dataArray);
62
+
63
+ // Gradient background
64
+ const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
65
+ gradient.addColorStop(0, '#0d1117');
66
+ gradient.addColorStop(1, '#000');
67
+ ctx.fillStyle = gradient;
68
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
69
+
70
+ const barWidth = (canvas.width / bufferLength) * 2;
71
+ let x = 0;
72
+
73
+ for (let i = 0; i < bufferLength; i++) {
74
+ const barHeight = (dataArray[i] / 255) * canvas.height * 0.8;
75
+
76
+ // Gradient bars
77
+ const barGradient = ctx.createLinearGradient(0, canvas.height - barHeight, 0, canvas.height);
78
+ barGradient.addColorStop(0, '#667eea');
79
+ barGradient.addColorStop(1, '#764ba2');
80
+
81
+ ctx.fillStyle = barGradient;
82
+ ctx.fillRect(x, canvas.height - barHeight, barWidth - 1, barHeight);
83
+ x += barWidth;
84
+ }
85
+ }
86
+
87
+ // --- Provider Presets (Universal) ---
88
+ const presets = {
89
+ aillomvox: {
90
+ voice: "Heitor",
91
+ language: "pt-BR",
92
+ sample_rate: "16000",
93
+ system_prompt: "Você é um assistente da Aillom. Seja conciso."
94
+ },
95
+ aws: {
96
+ voice: "matthew",
97
+ language: "pt-BR",
98
+ sample_rate: "16000",
99
+ system_prompt: "You are a helpful assistant. Respond in Portuguese Brazilian."
100
+ },
101
+ openai: {
102
+ voice: "alloy",
103
+ language: "pt-BR",
104
+ sample_rate: "24000",
105
+ system_prompt: "Act as a helpful assistant. Respond in Portuguese Brazilian."
106
+ },
107
+ gemini: {
108
+ voice: "Kore",
109
+ language: "pt-BR",
110
+ sample_rate: "16000",
111
+ system_prompt: "You are a helpful assistant. Respond in Portuguese Brazilian."
112
+ },
113
+ ultravox: {
114
+ voice: "Mark",
115
+ language: "en-US",
116
+ sample_rate: "16000",
117
+ system_prompt: "You are a helpful assistant."
118
+ },
119
+ qwen: {
120
+ voice: "Cherry",
121
+ language: "zh-CN",
122
+ sample_rate: "16000",
123
+ system_prompt: "You are a helpful assistant."
124
+ },
125
+ grok: {
126
+ voice: "Ara",
127
+ language: "en-US",
128
+ sample_rate: "16000",
129
+ system_prompt: "You are a helpful assistant."
130
+ }
131
+ };
132
+
133
+ inputs.provider.addEventListener('change', () => {
134
+ const preset = presets[inputs.provider.value];
135
+ if (preset) {
136
+ inputs.voice.value = preset.voice;
137
+ inputs.language.value = preset.language;
138
+ inputs.sample_rate.value = preset.sample_rate;
139
+ inputs.system_prompt.value = preset.system_prompt;
140
+ }
141
+ });
142
+
143
+ // --- Call Timer ---
144
+ function startTimer() {
145
+ callStartTime = Date.now();
146
+ callTimer.classList.remove('hidden');
147
+ timerInterval = setInterval(updateTimer, 1000);
148
+ }
149
+
150
+ function stopTimer() {
151
+ if (timerInterval) clearInterval(timerInterval);
152
+ callTimer.classList.add('hidden');
153
+ callTimer.textContent = '00:00';
154
+ }
155
+
156
+ function updateTimer() {
157
+ const elapsed = Math.floor((Date.now() - callStartTime) / 1000);
158
+ const mins = Math.floor(elapsed / 60).toString().padStart(2, '0');
159
+ const secs = (elapsed % 60).toString().padStart(2, '0');
160
+ callTimer.textContent = `${mins}:${secs}`;
161
+ }
162
+
163
+ // --- Connect ---
164
+ connectBtn.onclick = async () => {
165
+ const apiKey = inputs.apikey.value.trim();
166
+ if (!apiKey) return log('API Key required!', 'error');
167
+
168
+ updateStatus('CONNECTING', 'connecting');
169
+
170
+ try {
171
+ const sampleRate = parseInt(inputs.sample_rate.value, 10);
172
+ audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate });
173
+
174
+ // Universal WebSocket URL
175
+ const wsUrl = window.location.hostname === 'localhost'
176
+ ? 'ws://localhost:8080/ws'
177
+ : 'wss://vox.aillom.com/ws';
178
+
179
+ socket = new WebSocket(wsUrl);
180
+ socket.binaryType = 'arraybuffer';
181
+
182
+ socket.onopen = async () => {
183
+ log('WebSocket Open. Sending Handshake...', 'info');
184
+ updateStatus('CONNECTED', 'connected');
185
+
186
+ // Parse tools - Universal format that works with all providers
187
+ let tools = [];
188
+ try {
189
+ tools = JSON.parse(inputs.tools.value);
190
+ // Normalize tool format for universal compatibility
191
+ tools = tools.map(tool => ({
192
+ name: tool.name,
193
+ description: tool.description,
194
+ type: tool.type || 'function',
195
+ active: tool.active !== false,
196
+ // Support both 'config' and 'parameters' keys for backwards compatibility
197
+ config: tool.config || tool.parameters || { type: 'object', properties: {} }
198
+ }));
199
+ } catch (e) {
200
+ log('Error parsing Tools JSON: ' + e.message, 'error');
201
+ }
202
+
203
+ // Universal Handshake - works with all providers
204
+ const handshake = {
205
+ type: 'config',
206
+ apikey: apiKey,
207
+ provider: inputs.provider.value,
208
+ voice: inputs.voice.value,
209
+ language: inputs.language.value,
210
+ sample_rate: sampleRate,
211
+ system_prompt: inputs.system_prompt.value,
212
+ first_message: inputs.first_message.value,
213
+ tools: tools
214
+ };
215
+
216
+ socket.send(JSON.stringify(handshake));
217
+ await startAudio();
218
+ startTimer();
219
+ toggleUI(true);
220
+ };
221
+
222
+ socket.onmessage = (event) => {
223
+ if (typeof event.data === 'string') {
224
+ const msg = JSON.parse(event.data);
225
+
226
+ // Handle different message types
227
+ if (msg.type === 'transcript') {
228
+ // Only show FINAL transcripts to avoid spam
229
+ if (msg.final === true) {
230
+ addTranscript(msg.role, msg.text);
231
+ }
232
+ // Log all transcripts for debugging
233
+ if (msg.final) {
234
+ log(`[${msg.role}] ${msg.text}`, 'info');
235
+ }
236
+ } else if (msg.type === 'tool_call') {
237
+ handleToolCall(msg);
238
+ } else if (msg.type === 'playback_clear_buffer') {
239
+ // 🎯 ULTRAVOX PATTERN: Instant barge-in — clear all buffered audio
240
+ clearPlaybackBuffer();
241
+ log('🔇 Barge-in: audio buffer cleared', 'info');
242
+ } else if (msg.type === 'state') {
243
+ // 🎯 ULTRAVOX P1: Conversation state machine
244
+ const stateLabels = { listening: 'LISTENING', thinking: 'THINKING', speaking: 'SPEAKING' };
245
+ updateStatus(stateLabels[msg.state] || msg.state.toUpperCase(), 'connected');
246
+ log(`🔄 State: ${msg.state}`, 'normal');
247
+ } else if (msg.type === 'hangup' || msg.type === 'close') {
248
+ log('Call ended by server', 'normal');
249
+ disconnect();
250
+ } else if (msg.type === 'error') {
251
+ log(`Error: ${msg.message || msg.error}`, 'error');
252
+ } else if (msg.type !== 'audio') {
253
+ log(`RX: ${JSON.stringify(msg)}`, 'normal');
254
+ }
255
+ } else {
256
+ // Binary audio data
257
+ playAudioChunk(event.data);
258
+ }
259
+ };
260
+
261
+ socket.onclose = (e) => {
262
+ log(`Socket Closed: ${e.code}`, 'normal');
263
+ disconnect();
264
+ };
265
+
266
+ socket.onerror = (e) => log('Socket Error', 'error');
267
+
268
+ } catch (e) {
269
+ log(e.message, 'error');
270
+ disconnect();
271
+ }
272
+ };
273
+
274
+ disconnectBtn.onclick = disconnect;
275
+
276
+ function disconnect() {
277
+ clearPlaybackBuffer();
278
+ log('Disconnecting...', 'normal');
279
+ stopTimer();
280
+
281
+ if (socket) {
282
+ socket.onclose = null;
283
+ socket.close();
284
+ }
285
+ if (mediaStream) {
286
+ mediaStream.getTracks().forEach(t => t.stop());
287
+ mediaStream = null;
288
+ }
289
+ if (processor) {
290
+ processor.disconnect();
291
+ processor = null;
292
+ }
293
+ if (audioContext && audioContext.state !== 'closed') audioContext.close();
294
+
295
+ updateStatus('DISCONNECTED', 'disconnected');
296
+ toggleUI(false);
297
+ micLabel.innerHTML = '<i class="fa-solid fa-microphone-slash"></i> MIC OFF';
298
+ micLabel.classList.remove('active');
299
+ }
300
+
301
+ async function startAudio() {
302
+ mediaStream = await navigator.mediaDevices.getUserMedia({
303
+ audio: {
304
+ echoCancellation: true,
305
+ noiseSuppression: true,
306
+ sampleRate: audioContext.sampleRate
307
+ }
308
+ });
309
+
310
+ micLabel.innerHTML = '<i class="fa-solid fa-microphone"></i> MIC ON';
311
+ micLabel.classList.add('active');
312
+
313
+ setupVisualizer(mediaStream);
314
+
315
+ const source = audioContext.createMediaStreamSource(mediaStream);
316
+ processor = audioContext.createScriptProcessor(4096, 1, 1);
317
+
318
+ processor.onaudioprocess = (e) => {
319
+ if (socket.readyState !== WebSocket.OPEN) return;
320
+ const inputData = e.inputBuffer.getChannelData(0);
321
+ socket.send(floatTo16BitPCM(inputData));
322
+ };
323
+
324
+ source.connect(processor);
325
+ processor.connect(audioContext.destination);
326
+ }
327
+
328
+ // Audio playback queue
329
+ let nextStartTime = 0;
330
+
331
+ /**
332
+ * 🎯 ULTRAVOX PATTERN: Clear all buffered/scheduled audio instantly
333
+ * Called when server detects barge-in (user speaking while AI is talking)
334
+ */
335
+ function clearPlaybackBuffer() {
336
+ for (const source of scheduledSources) {
337
+ try { source.stop(); } catch (e) { /* already stopped */ }
338
+ }
339
+ scheduledSources = [];
340
+ nextStartTime = 0;
341
+ }
342
+
343
+ /**
344
+ * 🎯 ULTRAVOX PATTERN: Sequential audio scheduling with tracking
345
+ * Schedules chunks sequentially and tracks sources for cancellation
346
+ */
347
+ function playAudioChunk(arrayBuffer) {
348
+ if (!audioContext || audioContext.state === 'closed') return;
349
+ const float32 = new Float32Array(arrayBuffer.byteLength / 2);
350
+ const view = new DataView(arrayBuffer);
351
+ for (let i = 0; i < float32.length; i++) {
352
+ const s = view.getInt16(i * 2, true);
353
+ float32[i] = s < 0 ? s / 0x8000 : s / 0x7FFF;
354
+ }
355
+
356
+ const buffer = audioContext.createBuffer(1, float32.length, parseInt(inputs.sample_rate.value));
357
+ buffer.getChannelData(0).set(float32);
358
+
359
+ const source = audioContext.createBufferSource();
360
+ source.buffer = buffer;
361
+ source.connect(audioContext.destination);
362
+
363
+ const now = audioContext.currentTime;
364
+ if (nextStartTime < now) nextStartTime = now;
365
+ source.start(nextStartTime);
366
+ nextStartTime += buffer.duration;
367
+
368
+ // Track for cancellation on barge-in
369
+ scheduledSources.push(source);
370
+ source.onended = () => {
371
+ scheduledSources = scheduledSources.filter(s => s !== source);
372
+ };
373
+ }
374
+
375
+ // --- Tool Handling (Universal) ---
376
+ function handleToolCall(msg) {
377
+ // Debug: log the full tool call message
378
+ log(`🛠️ Tool Call: ${msg.name} | Full msg: ${JSON.stringify(msg)}`, 'success');
379
+
380
+ let result = 'Tool executed successfully.';
381
+
382
+ // Get args - handle different formats from different providers
383
+ const args = msg.args || msg.arguments || msg.parameters || {};
384
+
385
+ // Handle known client-side tools
386
+ if (msg.name === 'show_alert') {
387
+ const message = args.message || 'Alert from AI';
388
+ alert(`AI Says: ${message}`);
389
+ result = 'Alert displayed to user.';
390
+ } else if (msg.name === 'change_bg_color') {
391
+ // Accept both 'color' and 'bg_color' since LLMs may use either
392
+ const color = args.color || args.bg_color;
393
+ log(`Received color from args: "${color}"`, 'info');
394
+ if (color) {
395
+ document.body.style.backgroundColor = color === 'black' ? '#0a0c10' : color;
396
+ result = `Background changed to ${color}`;
397
+ log(`Background color changed to: ${color}`, 'success');
398
+ } else {
399
+ result = 'Error: No color specified';
400
+ log('Error: No color in args', 'error');
401
+ }
402
+ } else if (msg.name === 'hangup') {
403
+ result = 'Call ending...';
404
+ setTimeout(disconnect, 500);
405
+ } else {
406
+ // Unknown tool - log it
407
+ log(`Unknown tool: ${msg.name} - Args: ${JSON.stringify(args)}`, 'normal');
408
+ result = `Tool ${msg.name} acknowledged.`;
409
+ }
410
+
411
+ // Send result back to server (works with all providers)
412
+ socket.send(JSON.stringify({
413
+ type: 'tool_result',
414
+ call_id: msg.call_id,
415
+ result: result
416
+ }));
417
+ }
418
+
419
+ // --- Transcript ---
420
+ function addTranscript(role, text) {
421
+ const div = document.createElement('div');
422
+ div.className = `transcript-msg ${role}`;
423
+ div.textContent = text;
424
+ transcriptContainer.appendChild(div);
425
+ transcriptContainer.scrollTop = transcriptContainer.scrollHeight;
426
+ }
427
+
428
+ // --- Utilities ---
429
+ function floatTo16BitPCM(input) {
430
+ const output = new Int16Array(input.length);
431
+ for (let i = 0; i < input.length; i++) {
432
+ const s = Math.max(-1, Math.min(1, input[i]));
433
+ output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
434
+ }
435
+ return output.buffer;
436
+ }
437
+
438
+ function updateStatus(text, className) {
439
+ const icon = className === 'connected' ? 'fa-circle' :
440
+ className === 'connecting' ? 'fa-spinner fa-spin' : 'fa-circle';
441
+ statusBadge.innerHTML = `<i class="fa-solid ${icon}"></i> ${text}`;
442
+ statusBadge.className = `status-badge ${className}`;
443
+ }
444
+
445
+ function toggleUI(connected) {
446
+ connectBtn.classList.toggle('hidden', connected);
447
+ disconnectBtn.classList.toggle('hidden', !connected);
448
+ Object.values(inputs).forEach(i => i.disabled = connected);
449
+ }
450
+
451
+ function log(msg, type = 'normal') {
452
+ const div = document.createElement('div');
453
+ div.className = `log-entry ${type}`;
454
+ const time = new Date().toLocaleTimeString();
455
+ div.textContent = `[${time}] ${msg}`;
456
+ logsContainer.appendChild(div);
457
+ logsContainer.scrollTop = logsContainer.scrollHeight;
458
+ }
459
+
460
+ // --- Clear Buttons ---
461
+ document.getElementById('clearLogsBtn').onclick = () => logsContainer.innerHTML = '';
462
+ document.getElementById('clearTranscriptBtn').onclick = () => transcriptContainer.innerHTML = '';
463
+
464
+ // Initial canvas resize
465
+ setTimeout(resizeCanvas, 100);
@@ -0,0 +1,200 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>AillomVox - Advanced Dashboard</title>
8
+ <link rel="stylesheet" href="style.css">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
11
+ </head>
12
+
13
+ <body>
14
+ <div class="app-container">
15
+ <!-- Sidebar / Configuration Panel -->
16
+ <aside class="config-panel">
17
+ <div class="logo-area">
18
+ <div class="logo-icon">
19
+ <i class="fa-solid fa-wave-square"></i>
20
+ </div>
21
+ <div class="logo-text">
22
+ <h2>AillomVox</h2>
23
+ <span class="version">v2.0 Dashboard</span>
24
+ </div>
25
+ </div>
26
+
27
+ <div class="config-section">
28
+ <h4 class="section-title">Authentication</h4>
29
+ <div class="config-group">
30
+ <label><i class="fa-solid fa-key"></i> API Key</label>
31
+ <input type="password" id="apikey" placeholder="sk-..." autocomplete="off">
32
+ </div>
33
+ </div>
34
+
35
+ <div class="config-section">
36
+ <h4 class="section-title">Provider Settings</h4>
37
+ <div class="config-group">
38
+ <label><i class="fa-solid fa-server"></i> Provider</label>
39
+ <select id="provider">
40
+ <option value="aillomvox" selected>AillomVox (Default)</option>
41
+ <option value="openai">OpenAI Realtime</option>
42
+ <option value="gemini">Google Gemini</option>
43
+ <option value="ultravox">Ultravox</option>
44
+ <option value="aws">AWS Nova Sonic</option>
45
+ <option value="qwen">Qwen (Aliyun)</option>
46
+ <option value="grok">Grok (xAI)</option>
47
+ </select>
48
+ </div>
49
+
50
+ <div class="config-group">
51
+ <label><i class="fa-solid fa-microphone"></i> Voice</label>
52
+ <input type="text" id="voice" value="Heitor">
53
+ </div>
54
+ <div class="config-group">
55
+ <label><i class="fa-solid fa-globe"></i> Language</label>
56
+ <select id="language">
57
+ <option value="pt-BR" selected>Português (Brasil)</option>
58
+ <option value="en-US">English</option>
59
+ <option value="es-ES">Español</option>
60
+ <option value="fr-FR">Français</option>
61
+ <option value="de-DE">Deutsch</option>
62
+ <option value="it-IT">Italiano</option>
63
+ <option value="nl-NL">Nederlands</option>
64
+ <option value="pl-PL">Polski</option>
65
+ <option value="ru-RU">Русский</option>
66
+ <option value="zh-CN">中文</option>
67
+ <option value="ja-JP">日本語</option>
68
+ <option value="ko-KR">한국어</option>
69
+ <option value="hi-IN">हिन्दी</option>
70
+ <option value="he-IL">עברית</option>
71
+ <option value="ar-SA">العربية</option>
72
+ </select>
73
+ </div>
74
+ </div>
75
+
76
+ <div class="config-section">
77
+ <h4 class="section-title">AI Configuration</h4>
78
+ <div class="config-group">
79
+ <label><i class="fa-solid fa-robot"></i> System Prompt</label>
80
+ <textarea id="system_prompt" rows="3">You are a helpful assistant.</textarea>
81
+ </div>
82
+ <div class="config-group">
83
+ <label><i class="fa-solid fa-comment"></i> First Message</label>
84
+ <input type="text" id="first_message" value="Olá! Como posso ajudar?">
85
+ </div>
86
+ </div>
87
+
88
+ <!-- Advanced Toggles -->
89
+ <details class="advanced-section">
90
+ <summary><i class="fa-solid fa-sliders"></i> Advanced Settings</summary>
91
+ <div class="advanced-content">
92
+ <div class="config-group">
93
+ <label><i class="fa-solid fa-wave-square"></i> Sample Rate</label>
94
+ <select id="sample_rate">
95
+ <option value="8000">8000 Hz (Telephony)</option>
96
+ <option value="16000" selected>16000 Hz (Standard)</option>
97
+ <option value="24000">24000 Hz (High Quality)</option>
98
+ </select>
99
+ </div>
100
+ <div class="config-group">
101
+ <label><i class="fa-solid fa-wrench"></i> Custom Tools (JSON)</label>
102
+ <textarea id="toolsConfig" rows="10">[
103
+ {
104
+ "name": "hangup",
105
+ "description": "Ends the call immediately. Use when user says goodbye.",
106
+ "config": { "type": "object", "properties": {} }
107
+ },
108
+ {
109
+ "name": "show_alert",
110
+ "description": "Show a visual alert to the user.",
111
+ "config": {
112
+ "type": "object",
113
+ "properties": {
114
+ "message": { "type": "string", "description": "Message to display" }
115
+ },
116
+ "required": ["message"]
117
+ }
118
+ },
119
+ {
120
+ "name": "change_bg_color",
121
+ "description": "Changes the dashboard background color.",
122
+ "config": {
123
+ "type": "object",
124
+ "properties": {
125
+ "color": { "type": "string", "enum": ["red", "blue", "green", "black"], "description": "Target color" }
126
+ },
127
+ "required": ["color"]
128
+ }
129
+ }
130
+ ]</textarea>
131
+ </div>
132
+ </div>
133
+ </details>
134
+ </aside>
135
+
136
+ <!-- Main Content Area -->
137
+ <main class="main-content">
138
+ <header class="top-bar">
139
+ <div class="header-left">
140
+ <span id="connectionStatus" class="status-badge disconnected">
141
+ <i class="fa-solid fa-circle"></i> DISCONNECTED
142
+ </span>
143
+ <span id="callTimer" class="call-timer hidden">00:00</span>
144
+ </div>
145
+ <div class="header-right">
146
+ <button id="connectBtn" class="btn primary">
147
+ <i class="fa-solid fa-plug"></i> Connect
148
+ </button>
149
+ <button id="disconnectBtn" class="btn danger hidden">
150
+ <i class="fa-solid fa-power-off"></i> Disconnect
151
+ </button>
152
+ </div>
153
+ </header>
154
+
155
+ <div class="content-grid">
156
+ <!-- Visualization Panel -->
157
+ <div class="panel visualization-panel">
158
+ <div class="panel-header">
159
+ <h3><i class="fa-solid fa-chart-bar"></i> Audio Visualizer</h3>
160
+ <span id="micLabel" class="mic-status">
161
+ <i class="fa-solid fa-microphone-slash"></i> MIC OFF
162
+ </span>
163
+ </div>
164
+ <div class="panel-body">
165
+ <canvas id="visualizer"></canvas>
166
+ </div>
167
+ </div>
168
+
169
+ <!-- Transcript Panel -->
170
+ <div class="panel transcript-panel">
171
+ <div class="panel-header">
172
+ <h3><i class="fa-solid fa-comments"></i> Conversation</h3>
173
+ <button id="clearTranscriptBtn" class="btn sm ghost">
174
+ <i class="fa-solid fa-trash"></i>
175
+ </button>
176
+ </div>
177
+ <div class="panel-body">
178
+ <div id="transcriptContainer" class="transcript-list"></div>
179
+ </div>
180
+ </div>
181
+ </div>
182
+
183
+ <!-- Logs Panel -->
184
+ <div class="panel logs-panel">
185
+ <div class="panel-header">
186
+ <h3><i class="fa-solid fa-terminal"></i> Debug Logs</h3>
187
+ <button id="clearLogsBtn" class="btn sm ghost">
188
+ <i class="fa-solid fa-trash"></i>
189
+ </button>
190
+ </div>
191
+ <div class="panel-body">
192
+ <div id="logsContainer" class="logs-list"></div>
193
+ </div>
194
+ </div>
195
+ </main>
196
+ </div>
197
+ <script src="app.js"></script>
198
+ </body>
199
+
200
+ </html>