Spaces:
Runtime error
Runtime error
Upload main.js with huggingface_hub
Browse files
main.js
CHANGED
@@ -1,7 +1,31 @@
|
|
|
|
1 |
let recognition = null;
|
2 |
let isProcessingSpeech = false;
|
3 |
let isPlayingAudio = false;
|
4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
|
6 |
// Elementos del DOM
|
7 |
const chatBox = document.getElementById('chatBox');
|
@@ -12,373 +36,726 @@ const modeloSelect = document.getElementById('modeloSelect');
|
|
12 |
const vozSelect = document.getElementById('vozSelect');
|
13 |
const configForm = document.getElementById('configForm');
|
14 |
const statusLabel = document.getElementById('statusLabel');
|
|
|
15 |
|
16 |
-
//
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
try {
|
27 |
-
|
|
|
|
|
|
|
|
|
28 |
method: 'POST',
|
29 |
headers: {
|
30 |
'Content-Type': 'application/json'
|
31 |
},
|
32 |
-
body: JSON.stringify(
|
|
|
|
|
|
|
|
|
|
|
33 |
});
|
34 |
-
|
|
|
|
|
|
|
|
|
35 |
const data = await response.json();
|
36 |
-
if (data.success) {
|
37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
} else {
|
39 |
-
|
40 |
}
|
|
|
41 |
} catch (error) {
|
42 |
-
|
|
|
|
|
|
|
|
|
43 |
}
|
44 |
-
}
|
45 |
-
}
|
46 |
|
47 |
-
//
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
|
58 |
-
|
59 |
-
|
60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
61 |
}
|
62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
63 |
}
|
64 |
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
|
|
|
|
69 |
method: 'POST',
|
70 |
headers: {
|
71 |
'Content-Type': 'application/json'
|
72 |
},
|
73 |
-
body: JSON.stringify({
|
74 |
});
|
75 |
|
76 |
-
|
77 |
-
|
78 |
-
alert('Error al cambiar el modelo de voz: ' + data.error);
|
79 |
}
|
80 |
-
});
|
81 |
-
}
|
82 |
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
}
|
99 |
-
|
|
|
|
|
|
|
|
|
|
|
100 |
}
|
101 |
-
});
|
102 |
-
|
103 |
-
// Función para verificar si el texto contiene palabras de parada
|
104 |
-
function containsStopWord(text) {
|
105 |
-
const lowerText = text.toLowerCase();
|
106 |
-
return STOP_WORDS.some(word => lowerText.includes(word));
|
107 |
}
|
108 |
|
109 |
-
//
|
110 |
-
async function
|
111 |
try {
|
112 |
-
|
113 |
-
|
114 |
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
121 |
|
122 |
-
|
123 |
-
const
|
|
|
|
|
124 |
|
125 |
-
|
126 |
-
|
|
|
127 |
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
|
|
|
|
|
|
133 |
|
134 |
-
|
|
|
|
|
|
|
135 |
|
|
|
136 |
} catch (error) {
|
137 |
-
console.error('Error
|
138 |
-
|
139 |
-
updateStatus('Error al reproducir audio. Escuchando...');
|
140 |
-
if (!isProcessingSpeech) {
|
141 |
-
recognition.start();
|
142 |
-
}
|
143 |
}
|
144 |
}
|
145 |
|
146 |
-
// Función para
|
147 |
-
function
|
148 |
-
if (!
|
149 |
-
console.error('Error: texto o remitente faltante', { text, sender });
|
150 |
-
return;
|
151 |
-
}
|
152 |
|
153 |
-
const
|
154 |
-
|
155 |
-
console.error('Error crítico: No se encontró el elemento chatBox');
|
156 |
-
return;
|
157 |
-
}
|
158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
159 |
try {
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
iconSpan.className = 'message-icon';
|
167 |
-
iconSpan.textContent = sender === 'user' ? '👤' : '🤖';
|
168 |
-
|
169 |
-
const textSpan = document.createElement('span');
|
170 |
-
textSpan.className = 'message-text';
|
171 |
-
textSpan.textContent = text;
|
172 |
|
173 |
-
|
174 |
-
|
175 |
-
|
|
|
176 |
|
177 |
-
//
|
178 |
-
|
|
|
|
|
|
|
|
|
179 |
|
180 |
-
|
181 |
} catch (error) {
|
182 |
-
console.error('Error
|
|
|
183 |
}
|
184 |
}
|
185 |
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
191 |
}
|
192 |
|
193 |
-
// Función para
|
194 |
-
function
|
195 |
-
const
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
}
|
204 |
|
205 |
-
// Función para
|
206 |
-
async function
|
207 |
try {
|
208 |
-
|
|
|
209 |
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
|
|
|
|
|
|
|
|
|
|
217 |
|
218 |
-
|
219 |
-
|
220 |
-
|
|
|
|
|
|
|
|
|
|
|
221 |
}
|
222 |
-
} else {
|
223 |
-
const errorMessage = data.error || 'Error desconocido';
|
224 |
-
console.error('Error en la respuesta:', errorMessage);
|
225 |
-
addMessage('Lo siento, hubo un error: ' + errorMessage, 'bot');
|
226 |
}
|
227 |
} catch (error) {
|
228 |
-
console.error('Error
|
229 |
-
|
230 |
}
|
231 |
}
|
232 |
|
233 |
-
//
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
252 |
}
|
253 |
-
};
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
|
|
|
|
|
|
|
|
|
|
259 |
}
|
260 |
-
};
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
console.log('Palabra de parada detectada, deteniendo audio...');
|
276 |
-
const audio = document.querySelector('audio');
|
277 |
-
if (audio) {
|
278 |
-
audio.pause();
|
279 |
-
audio.currentTime = 0;
|
280 |
-
}
|
281 |
-
isPlayingAudio = false;
|
282 |
-
updateStatus('Audio detenido. Escuchando...');
|
283 |
-
continue;
|
284 |
-
}
|
285 |
-
|
286 |
-
if (!isProcessingSpeech) {
|
287 |
-
isProcessingSpeech = true;
|
288 |
-
updateStatus('Procesando...');
|
289 |
-
|
290 |
-
try {
|
291 |
-
console.log('Enviando texto al servidor:', finalTranscript);
|
292 |
-
const response = await fetch('/procesar_voz', {
|
293 |
-
method: 'POST',
|
294 |
-
headers: {
|
295 |
-
'Content-Type': 'application/json'
|
296 |
-
},
|
297 |
-
body: JSON.stringify({ texto: finalTranscript })
|
298 |
-
});
|
299 |
-
|
300 |
-
const data = await response.json();
|
301 |
-
console.log('Respuesta recibida:', data);
|
302 |
-
await processServerResponse(data, finalTranscript);
|
303 |
-
|
304 |
-
} catch (error) {
|
305 |
-
console.error('Error al procesar voz:', error);
|
306 |
-
updateStatus('Error de conexión. Escuchando...');
|
307 |
-
addMessage('Lo siento, hubo un error de conexión.', 'bot');
|
308 |
-
}
|
309 |
-
|
310 |
-
isProcessingSpeech = false;
|
311 |
-
}
|
312 |
-
} else {
|
313 |
-
interimTranscript = transcript;
|
314 |
-
updateStatus(`Escuchando: ${interimTranscript}`);
|
315 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
316 |
}
|
317 |
-
};
|
318 |
-
|
319 |
-
// Iniciar reconocimiento
|
320 |
-
recognition.start();
|
321 |
-
updateStatus('Escuchando...');
|
322 |
-
|
323 |
-
} else {
|
324 |
-
console.error('El reconocimiento de voz no está soportado en este navegador');
|
325 |
-
alert('Tu navegador no soporta el reconocimiento de voz. Por favor, usa Chrome.');
|
326 |
-
updateStatus('Reconocimiento de voz no soportado');
|
327 |
}
|
328 |
-
}
|
329 |
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
addMessage(text, 'user');
|
336 |
-
|
337 |
-
try {
|
338 |
-
const response = await fetch('/procesar_voz', {
|
339 |
-
method: 'POST',
|
340 |
-
headers: {
|
341 |
-
'Content-Type': 'application/json'
|
342 |
-
},
|
343 |
-
body: JSON.stringify({ texto: text })
|
344 |
-
});
|
345 |
-
|
346 |
-
const data = await response.json();
|
347 |
-
await processServerResponse(data, text);
|
348 |
-
|
349 |
-
textInput.value = '';
|
350 |
-
updateStatus('Esperando activación...');
|
351 |
-
} catch (error) {
|
352 |
-
console.error('Error:', error);
|
353 |
-
updateStatus('Error de conexión');
|
354 |
-
}
|
355 |
}
|
356 |
-
});
|
357 |
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
if (!chatBox) {
|
367 |
-
console.error('Error crítico: No se encontró el elemento chatBox');
|
368 |
-
return;
|
369 |
}
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
374 |
}
|
375 |
-
|
376 |
-
//
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
|
|
|
|
|
|
|
|
384 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// Variables globales
|
2 |
let recognition = null;
|
3 |
let isProcessingSpeech = false;
|
4 |
let isPlayingAudio = false;
|
5 |
+
let currentMode = 'soporte';
|
6 |
+
let currentModel = 'gemini';
|
7 |
+
let currentTTS = 'EDGE';
|
8 |
+
let isListening = false;
|
9 |
+
let sessionId = null;
|
10 |
+
let shouldRestartRecognition = true;
|
11 |
+
|
12 |
+
// Variables globales para audio
|
13 |
+
let currentAudio = null;
|
14 |
+
let isPlaying = false;
|
15 |
+
let audioQueue = [];
|
16 |
+
let isProcessingQueue = false;
|
17 |
+
|
18 |
+
// Variables para control de interrupción
|
19 |
+
let lastInterruptTime = 0;
|
20 |
+
const INTERRUPT_COOLDOWN = 1000; // 1 segundo entre interrupciones
|
21 |
+
const ENERGY_THRESHOLD = 1.5; // Umbral de energía para detectar interrupción
|
22 |
+
let audioContext = null;
|
23 |
+
let analyser = null;
|
24 |
+
let microphoneStream = null;
|
25 |
+
|
26 |
+
const STOP_WORDS = ['alto', 'detente', 'permiteme', 'callate', 'silencio', 'calla', 'para', 'espera'];
|
27 |
+
const GREETING_WORDS = ['hola', 'buenas', 'buenos días', 'buenas tardes', 'buenas noches'];
|
28 |
+
const INTEREST_WORDS = ['acepto', 'me interesa', 'dime más', 'cuéntame más', 'quiero saber más'];
|
29 |
|
30 |
// Elementos del DOM
|
31 |
const chatBox = document.getElementById('chatBox');
|
|
|
36 |
const vozSelect = document.getElementById('vozSelect');
|
37 |
const configForm = document.getElementById('configForm');
|
38 |
const statusLabel = document.getElementById('statusLabel');
|
39 |
+
const startRecordingButton = document.getElementById('startRecording');
|
40 |
|
41 |
+
// Comandos de voz
|
42 |
+
const VOICE_COMMANDS = {
|
43 |
+
stop: ['para', 'detente', 'silencio', 'cállate', 'espera', 'stop', 'alto', 'basta', 'suficiente', 'ya', 'shh', 'sh'],
|
44 |
+
greet: ['hola', 'buenas', 'buenos días', 'buenas tardes', 'buenas noches'],
|
45 |
+
interest: ['acepto', 'me interesa', 'dime más', 'cuéntame más', 'quiero saber más']
|
46 |
+
};
|
47 |
+
|
48 |
+
// Funciones de utilidad
|
49 |
+
const Utils = {
|
50 |
+
checkCommand(text, type) {
|
51 |
+
return VOICE_COMMANDS[type].some(cmd => text.toLowerCase().includes(cmd));
|
52 |
+
},
|
53 |
+
|
54 |
+
updateStatus(text, type = 'info') {
|
55 |
+
if (statusLabel) {
|
56 |
+
statusLabel.textContent = text;
|
57 |
+
statusLabel.classList.remove('error', 'success');
|
58 |
+
if (type === 'error' || type === 'success') {
|
59 |
+
statusLabel.classList.add(type);
|
60 |
+
}
|
61 |
+
}
|
62 |
+
}
|
63 |
+
};
|
64 |
|
65 |
+
// Funciones de chat
|
66 |
+
const ChatManager = {
|
67 |
+
addMessage(text, sender) {
|
68 |
+
if (!text || !sender || !chatBox) return;
|
69 |
+
|
70 |
+
try {
|
71 |
+
console.log('Agregando mensaje al chat:', { text, sender });
|
72 |
+
|
73 |
+
const messageDiv = document.createElement('div');
|
74 |
+
messageDiv.className = `message ${sender}-message`;
|
75 |
+
|
76 |
+
const iconSpan = document.createElement('span');
|
77 |
+
iconSpan.className = 'message-icon';
|
78 |
+
iconSpan.textContent = sender === 'user' ? '👤' : '🤖';
|
79 |
+
|
80 |
+
const textSpan = document.createElement('span');
|
81 |
+
textSpan.className = 'message-text';
|
82 |
+
textSpan.textContent = text;
|
83 |
+
|
84 |
+
messageDiv.appendChild(iconSpan);
|
85 |
+
messageDiv.appendChild(textSpan);
|
86 |
+
chatBox.appendChild(messageDiv);
|
87 |
+
|
88 |
+
// Hacer scroll al último mensaje
|
89 |
+
chatBox.scrollTop = chatBox.scrollHeight;
|
90 |
+
|
91 |
+
console.log('Mensaje agregado exitosamente');
|
92 |
+
} catch (error) {
|
93 |
+
console.error('Error al agregar mensaje:', error);
|
94 |
+
}
|
95 |
+
},
|
96 |
+
|
97 |
+
async sendMessage(text) {
|
98 |
+
if (!text) return;
|
99 |
+
|
100 |
try {
|
101 |
+
Utils.updateStatus('Procesando...');
|
102 |
+
this.addMessage(text, 'user');
|
103 |
+
textInput.value = '';
|
104 |
+
|
105 |
+
const response = await fetch('/chat', {
|
106 |
method: 'POST',
|
107 |
headers: {
|
108 |
'Content-Type': 'application/json'
|
109 |
},
|
110 |
+
body: JSON.stringify({
|
111 |
+
mensaje: text,
|
112 |
+
mode: currentMode,
|
113 |
+
model: currentModel,
|
114 |
+
tts: currentTTS
|
115 |
+
})
|
116 |
});
|
117 |
+
|
118 |
+
if (!response.ok) {
|
119 |
+
throw new Error(`Error HTTP: ${response.status}`);
|
120 |
+
}
|
121 |
+
|
122 |
const data = await response.json();
|
123 |
+
if (data.success && data.texto) {
|
124 |
+
this.addMessage(data.texto, 'bot');
|
125 |
+
|
126 |
+
if (data.audio) {
|
127 |
+
try {
|
128 |
+
const audioType = data.audio_type || 'mp3';
|
129 |
+
await AudioManager.playAudio(data.audio, audioType);
|
130 |
+
} catch (audioError) {
|
131 |
+
console.error('Error reproduciendo audio:', audioError);
|
132 |
+
handleAudioError();
|
133 |
+
}
|
134 |
+
}
|
135 |
} else {
|
136 |
+
throw new Error(data.error || 'Error desconocido');
|
137 |
}
|
138 |
+
|
139 |
} catch (error) {
|
140 |
+
console.error('Error:', error);
|
141 |
+
Utils.updateStatus('Error de conexión', 'error');
|
142 |
+
this.addMessage('Lo siento, hubo un error al procesar tu mensaje.', 'bot');
|
143 |
+
} finally {
|
144 |
+
Utils.updateStatus('Escuchando...');
|
145 |
}
|
146 |
+
}
|
147 |
+
};
|
148 |
|
149 |
+
// Gestión de audio
|
150 |
+
const AudioManager = {
|
151 |
+
async playAudio(base64Audio, audioType = 'wav') {
|
152 |
+
if (!base64Audio) return;
|
153 |
+
|
154 |
+
try {
|
155 |
+
if (isPlayingAudio) {
|
156 |
+
audioQueue.push({ base64Audio, audioType });
|
157 |
+
return;
|
158 |
+
}
|
159 |
+
|
160 |
+
isPlayingAudio = true;
|
161 |
+
Utils.updateStatus('Reproduciendo audio...');
|
162 |
+
|
163 |
+
const audioBlob = await fetch(`data:audio/${audioType};base64,${base64Audio}`)
|
164 |
+
.then(res => res.blob());
|
165 |
+
|
166 |
+
currentAudio = new Audio(URL.createObjectURL(audioBlob));
|
167 |
+
|
168 |
+
currentAudio.onended = () => {
|
169 |
+
isPlayingAudio = false;
|
170 |
+
URL.revokeObjectURL(currentAudio.src);
|
171 |
+
currentAudio = null;
|
172 |
+
Utils.updateStatus('Escuchando...');
|
173 |
+
this.processQueue();
|
174 |
+
};
|
175 |
+
|
176 |
+
await currentAudio.play();
|
177 |
+
|
178 |
+
} catch (error) {
|
179 |
+
console.error('Error reproduciendo audio:', error);
|
180 |
+
this.handleError();
|
181 |
+
}
|
182 |
+
},
|
183 |
+
|
184 |
+
async processQueue() {
|
185 |
+
if (isProcessingQueue || audioQueue.length === 0) return;
|
186 |
+
|
187 |
+
isProcessingQueue = true;
|
188 |
+
const nextAudio = audioQueue.shift();
|
189 |
+
await this.playAudio(nextAudio.base64Audio, nextAudio.audioType);
|
190 |
+
isProcessingQueue = false;
|
191 |
+
},
|
192 |
+
|
193 |
+
handleError() {
|
194 |
+
isPlayingAudio = false;
|
195 |
+
if (currentAudio) {
|
196 |
+
URL.revokeObjectURL(currentAudio.src);
|
197 |
+
currentAudio = null;
|
198 |
+
}
|
199 |
+
Utils.updateStatus('Error de audio', 'error');
|
200 |
+
},
|
201 |
+
|
202 |
+
stop() {
|
203 |
+
if (currentAudio) {
|
204 |
+
currentAudio.pause();
|
205 |
+
URL.revokeObjectURL(currentAudio.src);
|
206 |
+
currentAudio = null;
|
207 |
+
}
|
208 |
+
isPlayingAudio = false;
|
209 |
+
audioQueue = [];
|
210 |
+
Utils.updateStatus('Audio detenido');
|
211 |
+
}
|
212 |
+
};
|
213 |
|
214 |
+
// Gestión de voz
|
215 |
+
const VoiceManager = {
|
216 |
+
async processCommand(text) {
|
217 |
+
if (!text) return false;
|
218 |
+
|
219 |
+
const currentTime = Date.now();
|
220 |
+
|
221 |
+
if (Utils.checkCommand(text, 'stop')) {
|
222 |
+
if (currentTime - lastInterruptTime > INTERRUPT_COOLDOWN) {
|
223 |
+
lastInterruptTime = currentTime;
|
224 |
+
AudioManager.stop();
|
225 |
+
Utils.updateStatus('Audio detenido por comando de voz');
|
226 |
+
return true;
|
227 |
+
}
|
228 |
}
|
229 |
+
|
230 |
+
if (Utils.checkCommand(text, 'greet') || Utils.checkCommand(text, 'interest')) {
|
231 |
+
if (currentTime - lastInterruptTime > INTERRUPT_COOLDOWN) {
|
232 |
+
lastInterruptTime = currentTime;
|
233 |
+
|
234 |
+
if (isPlayingAudio) {
|
235 |
+
AudioManager.stop();
|
236 |
+
await new Promise(resolve => setTimeout(resolve, 300));
|
237 |
+
}
|
238 |
+
|
239 |
+
ChatManager.addMessage(text, 'user');
|
240 |
+
await this.processVoiceResponse(text);
|
241 |
+
return true;
|
242 |
+
}
|
243 |
+
}
|
244 |
+
|
245 |
+
return false;
|
246 |
+
},
|
247 |
+
|
248 |
+
async processVoiceResponse(text) {
|
249 |
+
try {
|
250 |
+
shouldRestartRecognition = false;
|
251 |
+
if (recognition && isListening) {
|
252 |
+
recognition.stop();
|
253 |
+
}
|
254 |
+
|
255 |
+
const response = await fetch('/procesar_voz', {
|
256 |
+
method: 'POST',
|
257 |
+
headers: { 'Content-Type': 'application/json' },
|
258 |
+
body: JSON.stringify({
|
259 |
+
texto: text,
|
260 |
+
mode: currentMode,
|
261 |
+
model: currentModel,
|
262 |
+
tts: currentTTS
|
263 |
+
})
|
264 |
+
});
|
265 |
+
|
266 |
+
if (!response.ok) throw new Error(`Error HTTP: ${response.status}`);
|
267 |
+
|
268 |
+
const data = await response.json();
|
269 |
+
if (data.success && data.texto) {
|
270 |
+
ChatManager.addMessage(data.texto, 'bot');
|
271 |
+
if (data.audio) {
|
272 |
+
await AudioManager.playAudio(data.audio, data.audio_type || 'wav');
|
273 |
+
}
|
274 |
+
}
|
275 |
+
} catch (error) {
|
276 |
+
console.error('Error procesando voz:', error);
|
277 |
+
Utils.updateStatus('Error procesando voz', 'error');
|
278 |
+
} finally {
|
279 |
+
shouldRestartRecognition = true;
|
280 |
+
if (!isListening) {
|
281 |
+
await this.startRecognition();
|
282 |
+
}
|
283 |
+
}
|
284 |
+
},
|
285 |
+
|
286 |
+
async startRecognition() {
|
287 |
+
if (!recognition || !isListening) {
|
288 |
+
try {
|
289 |
+
await this.initializeSpeechRecognition();
|
290 |
+
isListening = true;
|
291 |
+
startRecordingButton.classList.add('active');
|
292 |
+
Utils.updateStatus('Escuchando...');
|
293 |
+
} catch (error) {
|
294 |
+
console.error('Error iniciando reconocimiento:', error);
|
295 |
+
Utils.updateStatus('Error de micrófono', 'error');
|
296 |
+
}
|
297 |
+
}
|
298 |
+
},
|
299 |
+
|
300 |
+
async pauseRecognition() {
|
301 |
+
if (recognition && isListening) {
|
302 |
+
recognition.stop();
|
303 |
+
isListening = false;
|
304 |
+
startRecordingButton.classList.remove('active');
|
305 |
+
Utils.updateStatus('Reconocimiento pausado');
|
306 |
+
}
|
307 |
+
},
|
308 |
+
|
309 |
+
async initializeSpeechRecognition() {
|
310 |
+
if (!('webkitSpeechRecognition' in window)) {
|
311 |
+
throw new Error('Reconocimiento de voz no soportado');
|
312 |
+
}
|
313 |
+
|
314 |
+
recognition = new webkitSpeechRecognition();
|
315 |
+
recognition.continuous = true;
|
316 |
+
recognition.interimResults = true;
|
317 |
+
recognition.lang = 'es-ES';
|
318 |
+
|
319 |
+
recognition.onstart = () => {
|
320 |
+
isListening = true;
|
321 |
+
Utils.updateStatus('Escuchando...');
|
322 |
+
};
|
323 |
+
|
324 |
+
recognition.onend = () => {
|
325 |
+
isListening = false;
|
326 |
+
if (shouldRestartRecognition) {
|
327 |
+
recognition.start();
|
328 |
+
}
|
329 |
+
};
|
330 |
+
|
331 |
+
recognition.onresult = async (event) => {
|
332 |
+
if (isProcessingSpeech) return;
|
333 |
+
|
334 |
+
const results = Array.from(event.results);
|
335 |
+
const lastResult = results[results.length - 1];
|
336 |
+
|
337 |
+
if (lastResult.isFinal) {
|
338 |
+
const text = lastResult[0].transcript.trim();
|
339 |
+
if (text) {
|
340 |
+
isProcessingSpeech = true;
|
341 |
+
await this.processCommand(text);
|
342 |
+
isProcessingSpeech = false;
|
343 |
+
}
|
344 |
+
}
|
345 |
+
};
|
346 |
+
|
347 |
+
recognition.onerror = (event) => {
|
348 |
+
console.error('Error de reconocimiento:', event.error);
|
349 |
+
Utils.updateStatus('Error de reconocimiento', 'error');
|
350 |
+
};
|
351 |
+
|
352 |
+
recognition.start();
|
353 |
+
}
|
354 |
+
};
|
355 |
+
|
356 |
+
// Funciones de modo y modelo
|
357 |
+
function changeMode(mode) {
|
358 |
+
currentMode = mode;
|
359 |
+
fetch('/cambiar_modo', {
|
360 |
+
method: 'POST',
|
361 |
+
headers: {
|
362 |
+
'Content-Type': 'application/json'
|
363 |
+
},
|
364 |
+
body: JSON.stringify({ mode })
|
365 |
+
})
|
366 |
+
.then(response => response.json())
|
367 |
+
.then(data => {
|
368 |
+
if (data.success) {
|
369 |
+
ChatManager.addMessage(`Modo cambiado a: ${mode}`, 'bot');
|
370 |
+
} else {
|
371 |
+
console.error('Error cambiando modo:', data.error);
|
372 |
+
}
|
373 |
+
})
|
374 |
+
.catch(error => console.error('Error:', error));
|
375 |
+
}
|
376 |
+
|
377 |
+
function changeModel(model) {
|
378 |
+
currentModel = model;
|
379 |
+
fetch('/cambiar_modelo', {
|
380 |
+
method: 'POST',
|
381 |
+
headers: {
|
382 |
+
'Content-Type': 'application/json'
|
383 |
+
},
|
384 |
+
body: JSON.stringify({ model })
|
385 |
+
})
|
386 |
+
.then(response => response.json())
|
387 |
+
.then(data => {
|
388 |
+
if (data.success) {
|
389 |
+
ChatManager.addMessage(`Modelo cambiado a: ${model}`, 'bot');
|
390 |
+
} else {
|
391 |
+
console.error('Error cambiando modelo:', data.error);
|
392 |
+
}
|
393 |
+
})
|
394 |
+
.catch(error => console.error('Error:', error));
|
395 |
}
|
396 |
|
397 |
+
async function changeTTS(model) {
|
398 |
+
console.log("Cambiando modelo TTS a:", model);
|
399 |
+
Utils.updateStatus("Cambiando voz...");
|
400 |
+
|
401 |
+
try {
|
402 |
+
const response = await fetch('/cambiar_tts', {
|
403 |
method: 'POST',
|
404 |
headers: {
|
405 |
'Content-Type': 'application/json'
|
406 |
},
|
407 |
+
body: JSON.stringify({ model: model })
|
408 |
});
|
409 |
|
410 |
+
if (!response.ok) {
|
411 |
+
throw new Error('Error en la respuesta del servidor');
|
|
|
412 |
}
|
|
|
|
|
413 |
|
414 |
+
const data = await response.json();
|
415 |
+
|
416 |
+
let voiceName;
|
417 |
+
switch(model) {
|
418 |
+
case 'EDGE':
|
419 |
+
voiceName = "Jorge (MX)";
|
420 |
+
break;
|
421 |
+
case 'EDGE_ES':
|
422 |
+
voiceName = "Álvaro (ES)";
|
423 |
+
break;
|
424 |
+
case 'VITS':
|
425 |
+
voiceName = "VITS (ES)";
|
426 |
+
break;
|
427 |
+
default:
|
428 |
+
voiceName = model;
|
429 |
}
|
430 |
+
|
431 |
+
Utils.updateStatus(`Voz cambiada a ${voiceName}`);
|
432 |
+
console.log("Modelo TTS cambiado exitosamente");
|
433 |
+
} catch (error) {
|
434 |
+
console.error("Error al cambiar el modelo de voz:", error);
|
435 |
+
Utils.updateStatus("Error al cambiar la voz");
|
436 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
437 |
}
|
438 |
|
439 |
+
// Reconocimiento de voz
|
440 |
+
async function checkMicrophonePermissions() {
|
441 |
try {
|
442 |
+
// Verificar si ya tenemos permisos
|
443 |
+
const permissionStatus = await navigator.permissions.query({ name: 'microphone' });
|
444 |
|
445 |
+
if (permissionStatus.state === 'granted') {
|
446 |
+
return true;
|
447 |
+
} else if (permissionStatus.state === 'prompt') {
|
448 |
+
// Solicitar permisos explícitamente
|
449 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
450 |
+
stream.getTracks().forEach(track => track.stop());
|
451 |
+
return true;
|
452 |
+
} else if (permissionStatus.state === 'denied') {
|
453 |
+
Utils.updateStatus('Permisos de micrófono denegados. Por favor, habilítalos en la configuración del navegador.', 'error');
|
454 |
+
ChatManager.addMessage('❌ El micrófono está bloqueado. Para habilitarlo:\n1. Haz clic en el icono 🔒 o 🎤 en la barra de direcciones\n2. Selecciona "Permitir" para el micrófono\n3. Recarga la página', 'bot');
|
455 |
+
return false;
|
456 |
}
|
457 |
+
} catch (error) {
|
458 |
+
console.error('Error verificando permisos:', error);
|
459 |
+
return false;
|
460 |
+
}
|
461 |
+
}
|
462 |
+
|
463 |
+
// Función para inicializar el análisis de audio
|
464 |
+
async function setupAudioAnalysis() {
|
465 |
+
try {
|
466 |
+
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
467 |
+
analyser = audioContext.createAnalyser();
|
468 |
+
analyser.fftSize = 256;
|
469 |
|
470 |
+
// Configurar filtros para reducir eco
|
471 |
+
const lowpass = audioContext.createBiquadFilter();
|
472 |
+
lowpass.type = 'lowpass';
|
473 |
+
lowpass.frequency.value = 2000;
|
474 |
|
475 |
+
const highpass = audioContext.createBiquadFilter();
|
476 |
+
highpass.type = 'highpass';
|
477 |
+
highpass.frequency.value = 85;
|
478 |
|
479 |
+
// Obtener stream del micrófono
|
480 |
+
microphoneStream = await navigator.mediaDevices.getUserMedia({
|
481 |
+
audio: {
|
482 |
+
echoCancellation: true,
|
483 |
+
noiseSuppression: true,
|
484 |
+
autoGainControl: false
|
485 |
+
}
|
486 |
+
});
|
487 |
|
488 |
+
const source = audioContext.createMediaStreamSource(microphoneStream);
|
489 |
+
source.connect(highpass);
|
490 |
+
highpass.connect(lowpass);
|
491 |
+
lowpass.connect(analyser);
|
492 |
|
493 |
+
return true;
|
494 |
} catch (error) {
|
495 |
+
console.error('Error configurando análisis de audio:', error);
|
496 |
+
return false;
|
|
|
|
|
|
|
|
|
497 |
}
|
498 |
}
|
499 |
|
500 |
+
// Función para detectar interrupción basada en energía de audio
|
501 |
+
function detectInterruption() {
|
502 |
+
if (!analyser) return false;
|
|
|
|
|
|
|
503 |
|
504 |
+
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
505 |
+
analyser.getByteFrequencyData(dataArray);
|
|
|
|
|
|
|
506 |
|
507 |
+
// Calcular energía promedio
|
508 |
+
const average = dataArray.reduce((a, b) => a + b) / dataArray.length;
|
509 |
+
const normalizedEnergy = average / 128; // Normalizar a un rango de 0-1
|
510 |
+
|
511 |
+
return normalizedEnergy > ENERGY_THRESHOLD;
|
512 |
+
}
|
513 |
+
|
514 |
+
// Funciones de sesión
|
515 |
+
async function initSession() {
|
516 |
try {
|
517 |
+
const response = await fetch('/get_session_data');
|
518 |
+
const data = await response.json();
|
519 |
+
sessionId = data.id;
|
520 |
+
currentMode = data.mode;
|
521 |
+
currentModel = data.model;
|
522 |
+
currentTTS = data.tts;
|
|
|
|
|
|
|
|
|
|
|
|
|
523 |
|
524 |
+
// Actualizar selectores con valores de la sesión
|
525 |
+
if (modoSelect) modoSelect.value = currentMode;
|
526 |
+
if (modeloSelect) modeloSelect.value = currentModel;
|
527 |
+
if (vozSelect) vozSelect.value = currentTTS;
|
528 |
|
529 |
+
// Cargar historial de chat
|
530 |
+
if (data.chat_history && data.chat_history.length > 0) {
|
531 |
+
data.chat_history.forEach(msg => {
|
532 |
+
ChatManager.addMessage(msg.text, msg.sender);
|
533 |
+
});
|
534 |
+
}
|
535 |
|
536 |
+
return sessionId;
|
537 |
} catch (error) {
|
538 |
+
console.error('Error inicializando sesión:', error);
|
539 |
+
return null;
|
540 |
}
|
541 |
}
|
542 |
|
543 |
+
async function updateAudioState(isPlaying, currentAudio = null) {
|
544 |
+
try {
|
545 |
+
await fetch('/update_audio_state', {
|
546 |
+
method: 'POST',
|
547 |
+
headers: {
|
548 |
+
'Content-Type': 'application/json'
|
549 |
+
},
|
550 |
+
body: JSON.stringify({
|
551 |
+
is_playing: isPlaying,
|
552 |
+
current_audio: currentAudio
|
553 |
+
})
|
554 |
+
});
|
555 |
+
} catch (error) {
|
556 |
+
console.error('Error actualizando estado del audio:', error);
|
557 |
+
}
|
558 |
}
|
559 |
|
560 |
+
// Función para obtener el mensaje de presentación según el modo
|
561 |
+
function getMensajePresentacion(modo) {
|
562 |
+
const presentaciones = {
|
563 |
+
'soporte': '¡Hola! Soy tu asistente de soporte técnico. Estoy aquí para ayudarte con cualquier problema de PC o Android. ¿En qué puedo ayudarte?',
|
564 |
+
'seguros': '¡Hola! Soy tu asesor de seguros personal. Te ayudaré a encontrar la mejor protección para ti y tu familia. ¿Qué tipo de seguro te interesa?',
|
565 |
+
'creditos': '¡Hola! Soy tu asesor financiero. Estoy aquí para ayudarte a obtener el crédito que necesitas con las mejores condiciones. ¿Cuánto necesitas?',
|
566 |
+
'cobranza': '¡Hola! Soy tu gestor de cobranza. Estoy aquí para ayudarte a regularizar tu situación y encontrar la mejor solución para ti. ¿Cómo puedo ayudarte?',
|
567 |
+
'encuestas': '¡Hola! Soy tu encuestador profesional. Me gustaría conocer tu opinión sobre temas importantes. ¿Estás listo para comenzar?'
|
568 |
+
};
|
569 |
+
return presentaciones[modo] || '¡Hola! ¿En qué puedo ayudarte?';
|
570 |
}
|
571 |
|
572 |
+
// Función para iniciar el modo con presentación
|
573 |
+
async function iniciarModoConPresentacion(modo) {
|
574 |
try {
|
575 |
+
Utils.updateStatus('Iniciando modo...');
|
576 |
+
const mensajePresentacion = getMensajePresentacion(modo);
|
577 |
|
578 |
+
const response = await fetch('/cambiar_modo', {
|
579 |
+
method: 'POST',
|
580 |
+
headers: {
|
581 |
+
'Content-Type': 'application/json'
|
582 |
+
},
|
583 |
+
body: JSON.stringify({ mode: modo })
|
584 |
+
});
|
585 |
+
|
586 |
+
const data = await response.json();
|
587 |
+
if (data.success) {
|
588 |
+
currentMode = modo;
|
589 |
+
ChatManager.addMessage(mensajePresentacion, 'bot');
|
590 |
|
591 |
+
if (data.audio) {
|
592 |
+
try {
|
593 |
+
const audioType = data.audio_type || 'wav';
|
594 |
+
await AudioManager.playAudio(data.audio, audioType);
|
595 |
+
} catch (error) {
|
596 |
+
console.error('Error reproduciendo audio:', error);
|
597 |
+
handleAudioError();
|
598 |
+
}
|
599 |
}
|
|
|
|
|
|
|
|
|
600 |
}
|
601 |
} catch (error) {
|
602 |
+
console.error('Error iniciando modo:', error);
|
603 |
+
Utils.updateStatus('Error al iniciar modo');
|
604 |
}
|
605 |
}
|
606 |
|
607 |
+
// Event Listeners
|
608 |
+
document.addEventListener('DOMContentLoaded', async () => {
|
609 |
+
console.log('Página cargada, inicializando...');
|
610 |
+
|
611 |
+
// Inicializar sesión
|
612 |
+
await initSession();
|
613 |
+
|
614 |
+
// Inicializar reconocimiento de voz
|
615 |
+
await VoiceManager.startRecognition();
|
616 |
+
|
617 |
+
// Iniciar modo inicial con presentación
|
618 |
+
await iniciarModoConPresentacion(currentMode);
|
619 |
+
|
620 |
+
// Iniciar reconocimiento automáticamente
|
621 |
+
setTimeout(() => {
|
622 |
+
if (startRecordingButton && !isListening && !isPlayingAudio) {
|
623 |
+
console.log('Iniciando reconocimiento automático...');
|
624 |
+
VoiceManager.startRecognition();
|
625 |
+
Utils.updateStatus('Escuchando...');
|
626 |
+
}
|
627 |
+
}, 1000);
|
628 |
+
|
629 |
+
// Botón de envío de texto
|
630 |
+
if (sendTextButton) {
|
631 |
+
sendTextButton.addEventListener('click', () => {
|
632 |
+
const text = textInput.value.trim();
|
633 |
+
if (text) {
|
634 |
+
ChatManager.sendMessage(text);
|
635 |
}
|
636 |
+
});
|
637 |
+
}
|
638 |
+
|
639 |
+
// Input de texto (Enter)
|
640 |
+
if (textInput) {
|
641 |
+
textInput.addEventListener('keypress', (e) => {
|
642 |
+
if (e.key === 'Enter') {
|
643 |
+
const text = textInput.value.trim();
|
644 |
+
if (text) {
|
645 |
+
ChatManager.sendMessage(text);
|
646 |
+
}
|
647 |
}
|
648 |
+
});
|
649 |
+
}
|
650 |
+
|
651 |
+
// Selectores
|
652 |
+
if (modoSelect) {
|
653 |
+
modoSelect.addEventListener('change', async function() {
|
654 |
+
try {
|
655 |
+
Utils.updateStatus('Cambiando modo...');
|
656 |
|
657 |
+
// Detener cualquier audio actual
|
658 |
+
if (currentAudio && isPlaying) {
|
659 |
+
currentAudio.pause();
|
660 |
+
currentAudio.currentTime = 0;
|
661 |
+
currentAudio = null;
|
662 |
+
isPlaying = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
663 |
}
|
664 |
+
|
665 |
+
// Iniciar nuevo modo con presentación
|
666 |
+
await iniciarModoConPresentacion(this.value);
|
667 |
+
|
668 |
+
} catch (error) {
|
669 |
+
console.error('Error en cambio de modo:', error);
|
670 |
+
Utils.updateStatus('Error al cambiar modo');
|
671 |
+
this.value = currentMode; // Revertir al modo anterior
|
672 |
+
} finally {
|
673 |
+
setTimeout(() => Utils.updateStatus('Escuchando...'), 2000);
|
674 |
}
|
675 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
676 |
}
|
|
|
677 |
|
678 |
+
if (modeloSelect) {
|
679 |
+
modeloSelect.addEventListener('change', async (e) => {
|
680 |
+
currentModel = e.target.value;
|
681 |
+
changeModel(currentModel);
|
682 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
683 |
}
|
|
|
684 |
|
685 |
+
if (vozSelect) {
|
686 |
+
vozSelect.addEventListener('change', async (e) => {
|
687 |
+
const newVoice = e.target.value;
|
688 |
+
console.log('Seleccionada nueva voz:', newVoice);
|
689 |
+
Utils.updateStatus('Cambiando voz...');
|
690 |
+
currentTTS = newVoice;
|
691 |
+
changeTTS(newVoice);
|
692 |
+
});
|
|
|
|
|
|
|
693 |
}
|
694 |
+
|
695 |
+
// Botón de grabación
|
696 |
+
if (startRecordingButton) {
|
697 |
+
startRecordingButton.addEventListener('click', async () => {
|
698 |
+
if (!isListening) {
|
699 |
+
await VoiceManager.startRecognition();
|
700 |
+
Utils.updateStatus('Escuchando...');
|
701 |
+
} else {
|
702 |
+
await VoiceManager.pauseRecognition();
|
703 |
+
Utils.updateStatus('Reconocimiento pausado');
|
704 |
+
}
|
705 |
+
});
|
706 |
}
|
707 |
+
|
708 |
+
// Manejar visibilidad de la página
|
709 |
+
document.addEventListener('visibilitychange', () => {
|
710 |
+
if (document.hidden) {
|
711 |
+
if (isListening) {
|
712 |
+
VoiceManager.pauseRecognition();
|
713 |
+
}
|
714 |
+
} else {
|
715 |
+
if (!isListening && !isPlayingAudio) {
|
716 |
+
setTimeout(VoiceManager.startRecognition, 500);
|
717 |
+
}
|
718 |
+
}
|
719 |
+
});
|
720 |
});
|
721 |
+
|
722 |
+
// Agregar al inicio del archivo, después de las variables globales
|
723 |
+
const permissionModal = `
|
724 |
+
<div class="permission-modal" id="permissionModal">
|
725 |
+
<div class="permission-content">
|
726 |
+
<h4>Permisos de Micrófono</h4>
|
727 |
+
<p>Para usar el chat por voz, necesitamos acceso a tu micrófono.</p>
|
728 |
+
<div class="alert alert-warning">
|
729 |
+
<small>⚠️ Si el sitio no es seguro (HTTPS), el navegador puede bloquear el acceso al micrófono.</small>
|
730 |
+
</div>
|
731 |
+
<div class="permission-buttons">
|
732 |
+
<button class="btn btn-primary" onclick="requestMicrophonePermission()">Permitir Micrófono</button>
|
733 |
+
<button class="btn btn-secondary" onclick="closePermissionModal()">Cancelar</button>
|
734 |
+
</div>
|
735 |
+
</div>
|
736 |
+
</div>`;
|
737 |
+
|
738 |
+
// Agregar después de document.addEventListener('DOMContentLoaded'...
|
739 |
+
document.body.insertAdjacentHTML('beforeend', permissionModal);
|
740 |
+
|
741 |
+
// Funciones para el modal de permisos
|
742 |
+
function showPermissionModal() {
|
743 |
+
document.getElementById('permissionModal').classList.add('show');
|
744 |
+
}
|
745 |
+
|
746 |
+
function closePermissionModal() {
|
747 |
+
document.getElementById('permissionModal').classList.remove('show');
|
748 |
+
}
|
749 |
+
|
750 |
+
async function requestMicrophonePermission() {
|
751 |
+
try {
|
752 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
753 |
+
stream.getTracks().forEach(track => track.stop());
|
754 |
+
closePermissionModal();
|
755 |
+
await VoiceManager.initializeSpeechRecognition();
|
756 |
+
} catch (error) {
|
757 |
+
console.error('Error al solicitar permisos:', error);
|
758 |
+
Utils.updateStatus('Error: No se pudo acceder al micrófono', 'error');
|
759 |
+
ChatManager.addMessage('❌ No se pudo acceder al micrófono. Por favor, verifica los permisos en tu navegador.', 'bot');
|
760 |
+
}
|
761 |
+
}
|