- Kiosk mode: fullscreen, wake lock, auto-login, context menu block, PWA/Capacitor detection - AI vision: camera photos analyzed by Gemma 3 27B vision model via OpenRouter - AI part classification: auto-suggest name/brand/category when entering part number - Public catalog chatbot: /api/chat endpoint with rate limiting, chat widget on catalog page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
192 lines
5.9 KiB
JavaScript
192 lines
5.9 KiB
JavaScript
// /home/Autopartes/dashboard/chat-public.js
|
|
// Public catalog chatbot — no auth required, calls /api/chat
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
var isOpen = false;
|
|
var isSending = false;
|
|
var history = [];
|
|
|
|
function init() {
|
|
// FAB button
|
|
var fab = document.createElement('button');
|
|
fab.className = 'chat-fab';
|
|
fab.id = 'chatFab';
|
|
fab.title = 'Asistente IA';
|
|
fab.innerHTML = '💬';
|
|
fab.setAttribute('aria-label', 'Abrir asistente IA');
|
|
|
|
// Chat panel
|
|
var panel = document.createElement('div');
|
|
panel.className = 'chat-panel';
|
|
panel.id = 'chatPanel';
|
|
panel.innerHTML =
|
|
'<div class="chat-header">' +
|
|
'<h3>Asistente — Buscar partes</h3>' +
|
|
'<button class="chat-header-close" id="chatClose" aria-label="Cerrar">×</button>' +
|
|
'</div>' +
|
|
'<div class="chat-messages" id="chatMessages">' +
|
|
'<div class="chat-msg ai">Hola, soy el asistente de Nexus Autoparts. Dime que refaccion buscas y te ayudo a encontrarla en el catalogo.</div>' +
|
|
'<div class="chat-typing" id="chatTyping"><span></span><span></span><span></span></div>' +
|
|
'</div>' +
|
|
'<div class="chat-input-area">' +
|
|
'<textarea class="chat-input" id="chatInput" placeholder="Ej: Balatas para Tsuru 2015..." rows="1"></textarea>' +
|
|
'<button class="chat-send-btn" id="chatSend" aria-label="Enviar">▶</button>' +
|
|
'</div>';
|
|
|
|
document.body.appendChild(fab);
|
|
document.body.appendChild(panel);
|
|
|
|
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();
|
|
}
|
|
});
|
|
|
|
document.getElementById('chatInput').addEventListener('input', function () {
|
|
this.style.height = 'auto';
|
|
this.style.height = Math.min(this.scrollHeight, 80) + 'px';
|
|
});
|
|
}
|
|
|
|
function toggleChat() {
|
|
isOpen = !isOpen;
|
|
var panel = document.getElementById('chatPanel');
|
|
var 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 sendMessage() {
|
|
if (isSending) return;
|
|
var input = document.getElementById('chatInput');
|
|
var text = input.value.trim();
|
|
if (!text) return;
|
|
|
|
input.value = '';
|
|
input.style.height = 'auto';
|
|
|
|
addBubble(text, 'user');
|
|
|
|
history.push({ role: 'user', content: text });
|
|
if (history.length > 20) history.splice(0, 2);
|
|
|
|
isSending = true;
|
|
document.getElementById('chatSend').disabled = true;
|
|
showTyping(true);
|
|
|
|
fetch('/api/chat', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
message: text,
|
|
history: history.slice(-10)
|
|
})
|
|
})
|
|
.then(function (resp) { return resp.json(); })
|
|
.then(function (data) {
|
|
if (data.error) {
|
|
addBubble('Error: ' + data.error, 'ai');
|
|
return;
|
|
}
|
|
var aiMsg = data.response || 'Sin respuesta.';
|
|
addBubble(aiMsg, 'ai');
|
|
history.push({ role: 'assistant', content: aiMsg });
|
|
|
|
if (data.search_results && data.search_results.length > 0) {
|
|
addPartResults(data.search_results);
|
|
}
|
|
})
|
|
.catch(function (err) {
|
|
addBubble('Error de conexion: ' + err.message, 'ai');
|
|
})
|
|
.finally(function () {
|
|
isSending = false;
|
|
document.getElementById('chatSend').disabled = false;
|
|
showTyping(false);
|
|
});
|
|
}
|
|
|
|
function addBubble(text, role) {
|
|
var container = document.getElementById('chatMessages');
|
|
var typing = document.getElementById('chatTyping');
|
|
var div = document.createElement('div');
|
|
div.className = 'chat-msg ' + role;
|
|
div.textContent = text;
|
|
container.insertBefore(div, typing);
|
|
scrollToBottom();
|
|
}
|
|
|
|
function addPartResults(parts) {
|
|
var container = document.getElementById('chatMessages');
|
|
var typing = document.getElementById('chatTyping');
|
|
|
|
var wrapper = document.createElement('div');
|
|
wrapper.className = 'chat-parts';
|
|
|
|
parts.slice(0, 8).forEach(function (p) {
|
|
var card = document.createElement('div');
|
|
card.className = 'chat-part-card';
|
|
|
|
var name = p.name_es || p.name_part || '';
|
|
var partNum = p.oem_part_number || p.part_number || '';
|
|
var brand = p.brand || '';
|
|
|
|
card.innerHTML =
|
|
'<div class="part-number">' + esc(partNum) + '</div>' +
|
|
'<div class="part-name">' + esc(name) + (brand ? ' <span style="color:var(--color-text-muted);">(' + esc(brand) + ')</span>' : '') + '</div>';
|
|
|
|
card.style.cursor = 'pointer';
|
|
card.addEventListener('click', function () {
|
|
// Search in catalog
|
|
var searchInput = document.getElementById('searchInput');
|
|
if (searchInput && partNum) {
|
|
searchInput.value = partNum;
|
|
if (typeof window.doSearch === 'function') window.doSearch();
|
|
toggleChat();
|
|
}
|
|
});
|
|
|
|
wrapper.appendChild(card);
|
|
});
|
|
|
|
container.insertBefore(wrapper, typing);
|
|
scrollToBottom();
|
|
}
|
|
|
|
function showTyping(show) {
|
|
var el = document.getElementById('chatTyping');
|
|
if (el) el.classList.toggle('visible', show);
|
|
if (show) scrollToBottom();
|
|
}
|
|
|
|
function scrollToBottom() {
|
|
var el = document.getElementById('chatMessages');
|
|
if (el) el.scrollTop = el.scrollHeight;
|
|
}
|
|
|
|
function esc(s) {
|
|
if (!s) return '';
|
|
var d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|