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.
- package/LICENSE +15 -0
- package/README.md +272 -0
- package/dist/AillomVox.d.ts +36 -0
- package/dist/AillomVox.js +152 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.js +2 -0
- package/docs/ASTERISK.md +411 -0
- package/docs/PROTOCOL.md +156 -0
- package/docs/PROVIDERS.md +40 -0
- package/docs/TOOLS.md +314 -0
- package/docs/TROUBLESHOOTING.md +86 -0
- package/docs/VOICES.md +219 -0
- package/docs/providers/AILLOMVOX.md +185 -0
- package/docs/providers/AWS.md +32 -0
- package/docs/providers/GEMINI.md +33 -0
- package/docs/providers/GROK.md +25 -0
- package/docs/providers/OPENAI.md +39 -0
- package/docs/providers/QWEN.md +27 -0
- package/docs/providers/ULTRAVOX.md +29 -0
- package/examples/01-basic/app.js +196 -0
- package/examples/01-basic/index.html +27 -0
- package/examples/02-advanced-dashboard/app.js +465 -0
- package/examples/02-advanced-dashboard/index.html +200 -0
- package/examples/02-advanced-dashboard/style.css +501 -0
- package/examples/03-smart-home/index.html +377 -0
- package/examples/04-customer-support/index.html +474 -0
- package/examples/sdk-usage.ts +44 -0
- package/integrations/n8n-nodes-aillomvox/README.md +56 -0
- package/integrations/n8n-nodes-aillomvox/credentials/AillomVoxApi.credentials.ts +29 -0
- package/integrations/n8n-nodes-aillomvox/dist/credentials/AillomVoxApi.credentials.js +30 -0
- package/integrations/n8n-nodes-aillomvox/dist/nodes/AillomVox/AillomVox.node.js +219 -0
- package/integrations/n8n-nodes-aillomvox/dist/nodes/AillomVox/aillomvox.svg +6 -0
- package/integrations/n8n-nodes-aillomvox/gulpfile.js +10 -0
- package/integrations/n8n-nodes-aillomvox/nodes/AillomVox/AillomVox.node.ts +229 -0
- package/integrations/n8n-nodes-aillomvox/nodes/AillomVox/aillomvox.svg +6 -0
- package/integrations/n8n-nodes-aillomvox/package-lock.json +11741 -0
- package/integrations/n8n-nodes-aillomvox/package.json +56 -0
- package/integrations/n8n-nodes-aillomvox/tsconfig.json +32 -0
- package/package.json +55 -0
- package/src/AillomVox.ts +169 -0
- package/src/index.ts +2 -0
- package/src/types.ts +50 -0
- 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>
|