// /home/Autopartes/pos/static/js/chat.js // AI Chat Widget for Nexus POS — natural language parts lookup (function () { 'use strict'; // ─── State ─── let isOpen = false; let isSending = false; let isListening = false; let recognition = null; let ttsEnabled = true; let ttsUtterance = null; const history = []; // conversation history for AI context const hasSpeechAPI = ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window); const hasTTS = ('speechSynthesis' in window); // ─── Build DOM ─── function init() { // FAB button const fab = document.createElement('button'); fab.className = 'chat-fab'; fab.id = 'chatFab'; fab.title = 'Asistente IA'; fab.innerHTML = '💬'; // speech bubble emoji fab.setAttribute('aria-label', 'Abrir asistente IA'); // Chat panel const panel = document.createElement('div'); panel.className = 'chat-panel'; panel.id = 'chatPanel'; panel.innerHTML = `

Asistente IA — Buscar partes

${hasTTS ? '' : ''}
Hola, soy el asistente de Nexus. Dime que refaccion necesitas y te ayudo a encontrarla.
${hasSpeechAPI ? '' : ''}
`; document.body.appendChild(fab); document.body.appendChild(panel); // Events fab.addEventListener('click', toggleChat); document.getElementById('chatClose').addEventListener('click', toggleChat); document.getElementById('chatSend').addEventListener('click', sendMessage); document.getElementById('chatInput').addEventListener('keydown', function (e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); // Camera button — identify part by photo document.getElementById('chatCam').addEventListener('click', function () { document.getElementById('chatImageInput').click(); }); document.getElementById('chatImageInput').addEventListener('change', handleImageUpload); // Mic button (only if Speech API available) if (hasSpeechAPI) { document.getElementById('chatMic').addEventListener('click', toggleVoice); } // TTS toggle if (hasTTS) { document.getElementById('chatTtsToggle').addEventListener('click', toggleTTS); } // Stop TTS when closing document.getElementById('chatClose').addEventListener('click', function () { if (hasTTS) stopSpeaking(); }); // Auto-resize textarea document.getElementById('chatInput').addEventListener('input', function () { this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 80) + 'px'; }); } // ─── Voice Input (Web Speech API) ─── function toggleVoice() { if (isListening) { stopVoice(); return; } startVoice(); } function startVoice() { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) return; recognition = new SpeechRecognition(); recognition.lang = 'es-MX'; recognition.continuous = false; recognition.interimResults = true; const input = document.getElementById('chatInput'); const micBtn = document.getElementById('chatMic'); const savedPlaceholder = input.placeholder; recognition.onstart = function () { isListening = true; micBtn.classList.add('listening'); input.placeholder = 'Escuchando...'; input.value = ''; }; recognition.onresult = function (e) { let interim = ''; let finalTranscript = ''; for (let i = e.resultIndex; i < e.results.length; i++) { if (e.results[i].isFinal) { finalTranscript += e.results[i][0].transcript; } else { interim += e.results[i][0].transcript; } } if (finalTranscript) { input.value = finalTranscript; } else { input.value = interim; } }; recognition.onend = function () { isListening = false; micBtn.classList.remove('listening'); input.placeholder = savedPlaceholder; recognition = null; // Auto-send if we got text if (input.value.trim()) { sendMessage(); } }; recognition.onerror = function (e) { isListening = false; micBtn.classList.remove('listening'); input.placeholder = savedPlaceholder; recognition = null; if (e.error === 'no-speech' || e.error === 'audio-capture' || e.error === 'not-allowed') { showVoiceToast('No se detecto voz'); } }; recognition.start(); } function stopVoice() { if (recognition) { recognition.abort(); recognition = null; } isListening = false; const micBtn = document.getElementById('chatMic'); if (micBtn) micBtn.classList.remove('listening'); } function showVoiceToast(msg) { const toast = document.createElement('div'); toast.className = 'chat-voice-toast'; toast.textContent = msg; document.body.appendChild(toast); setTimeout(function () { toast.classList.add('visible'); }, 10); setTimeout(function () { toast.classList.remove('visible'); setTimeout(function () { toast.remove(); }, 300); }, 2000); } // ─── TTS (Text-to-Speech) ─── function toggleTTS() { ttsEnabled = !ttsEnabled; const btn = document.getElementById('chatTtsToggle'); if (btn) { btn.classList.toggle('off', !ttsEnabled); btn.setAttribute('title', ttsEnabled ? 'Desactivar lectura de respuestas' : 'Activar lectura de respuestas'); } if (!ttsEnabled) stopSpeaking(); } function speak(text) { if (!hasTTS || !ttsEnabled || !text) return; stopSpeaking(); ttsUtterance = new SpeechSynthesisUtterance(text); ttsUtterance.lang = 'es-MX'; ttsUtterance.rate = 1.1; ttsUtterance.pitch = 1; window.speechSynthesis.speak(ttsUtterance); } function stopSpeaking() { if (hasTTS && window.speechSynthesis.speaking) { window.speechSynthesis.cancel(); } ttsUtterance = null; } // ─── Image Upload (Part identification placeholder) ─── function handleImageUpload(e) { const file = e.target.files && e.target.files[0]; if (!file) return; // Reset input so the same file can be selected again e.target.value = ''; // Validate file type and size (max 5MB) if (!file.type.startsWith('image/')) { addBubble('Solo se permiten imagenes.', 'ai'); return; } if (file.size > 5 * 1024 * 1024) { addBubble('La imagen es muy grande (max 5MB).', 'ai'); return; } // Show image thumbnail in chat const reader = new FileReader(); reader.onload = function (ev) { const container = document.getElementById('chatMessages'); const typing = document.getElementById('chatTyping'); const div = document.createElement('div'); div.className = 'chat-msg user chat-msg-image'; div.innerHTML = 'Foto de parte' + 'Identificar esta parte'; container.insertBefore(div, typing); scrollToBottom(); // Send image to AI vision model for real analysis var imageData = ev.target.result; // full data URL var photoPrompt = 'Identifica esta parte automotriz y sugiere terminos de busqueda.'; history.push({ role: 'user', content: photoPrompt }); if (history.length > 20) history.splice(0, 2); isSending = true; document.getElementById('chatSend').disabled = true; showTyping(true); var token = getToken(); fetch('/pos/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ message: photoPrompt, image: imageData, history: history.slice(-10) }) }) .then(function (resp) { return resp.json(); }) .then(function (data) { const aiMsg = data.response || 'No pude identificar la parte. Intenta describirla con texto.'; addBubble(aiMsg, 'ai'); history.push({ role: 'assistant', content: aiMsg }); if (data.vehicle && data.vehicle.brand_id) { addVehicleBanner(data.vehicle); } if (data.search_results && data.search_results.length > 0) { addPartResults(data.search_results); } }) .catch(function (err) { addBubble('Error al procesar imagen: ' + err.message, 'ai'); }) .finally(function () { isSending = false; document.getElementById('chatSend').disabled = false; showTyping(false); }); }; reader.readAsDataURL(file); } function toggleChat() { isOpen = !isOpen; const panel = document.getElementById('chatPanel'); const fab = document.getElementById('chatFab'); if (isOpen) { panel.classList.add('open'); fab.style.display = 'none'; document.getElementById('chatInput').focus(); } else { panel.classList.remove('open'); fab.style.display = 'flex'; } } function getToken() { // app-init.js stores token in window.__pos or localStorage if (window.__pos && window.__pos.token) return window.__pos.token; return localStorage.getItem('pos_token') || ''; } // ─── Send message ─── async function sendMessage() { if (isSending) return; const input = document.getElementById('chatInput'); const text = input.value.trim(); if (!text) return; input.value = ''; input.style.height = 'auto'; // Add user bubble addBubble(text, 'user'); // Keep history for context (last 10 exchanges) history.push({ role: 'user', content: text }); if (history.length > 20) history.splice(0, 2); // Show typing isSending = true; document.getElementById('chatSend').disabled = true; showTyping(true); try { const token = getToken(); const resp = await fetch('/pos/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ message: text, history: history.slice(-10) }) }); const data = await resp.json(); if (!resp.ok) { addBubble('Error: ' + (data.error || resp.statusText), 'ai'); return; } // AI response bubble const aiMsg = data.response || 'Sin respuesta.'; addBubble(aiMsg, 'ai'); history.push({ role: 'assistant', content: aiMsg }); speak(aiMsg); // Vehicle info if (data.vehicle && data.vehicle.brand_id) { addVehicleBanner(data.vehicle); } // Search results if (data.search_results && data.search_results.length > 0) { addPartResults(data.search_results); } } catch (err) { addBubble('Error de conexion: ' + err.message, 'ai'); } finally { isSending = false; document.getElementById('chatSend').disabled = false; showTyping(false); } } // ─── DOM helpers ─── function addBubble(text, role) { const container = document.getElementById('chatMessages'); const typing = document.getElementById('chatTyping'); const div = document.createElement('div'); div.className = 'chat-msg ' + role; div.textContent = text; container.insertBefore(div, typing); scrollToBottom(); } function addVehicleBanner(vehicle) { const container = document.getElementById('chatMessages'); const typing = document.getElementById('chatTyping'); const div = document.createElement('div'); div.className = 'chat-vehicle-banner'; let html = '' + esc(vehicle.brand || '') + ' ' + esc(vehicle.model || '') + ''; if (vehicle.year) html += ' ' + vehicle.year; if (vehicle.mye_options && vehicle.mye_options.length > 0) { html += '
Motorizaciones encontradas:'; vehicle.mye_options.forEach(function (opt) { html += '
• ' + esc(opt.engine); if (opt.trim) html += ' (' + esc(opt.trim) + ')'; }); } div.innerHTML = html; container.insertBefore(div, typing); scrollToBottom(); } function addPartResults(parts) { const container = document.getElementById('chatMessages'); const typing = document.getElementById('chatTyping'); const wrapper = document.createElement('div'); wrapper.className = 'chat-parts'; parts.slice(0, 8).forEach(function (p) { const card = document.createElement('div'); card.className = 'chat-part-card'; const isLocal = p.source === 'local'; const stockQty = p.local_stock || 0; const stockClass = stockQty > 0 ? 'in-stock' : ''; const stockText = stockQty > 0 ? (stockQty + ' en stock') : 'Sin stock local'; const name = p.name_es || p.name_part || ''; const partNum = p.oem_part_number || p.part_number || ''; const brand = p.brand || ''; const priceText = p.price_1 ? ('$' + parseFloat(p.price_1).toFixed(2)) : ''; const sourceTag = isLocal ? 'MI INVENTARIO' : 'CATÁLOGO'; card.innerHTML = '
' + esc(partNum) + sourceTag + (priceText ? ' — ' + priceText : '') + '
' + '
' + esc(name) + (brand ? ' (' + esc(brand) + ')' : '') + '
' + '
' + esc(stockText) + '
'; // Click to open detail (if catalog page has a detail function) card.addEventListener('click', function () { if (p.id_part && typeof window.openPartDetail === 'function') { window.openPartDetail(p.id_part); toggleChat(); } }); wrapper.appendChild(card); }); container.insertBefore(wrapper, typing); scrollToBottom(); } function showTyping(show) { const el = document.getElementById('chatTyping'); if (el) el.classList.toggle('visible', show); if (show) scrollToBottom(); } function scrollToBottom() { const el = document.getElementById('chatMessages'); if (el) el.scrollTop = el.scrollHeight; } function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } // ─── Init when DOM ready ─── if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();