diff --git a/pos/blueprints/chat_bp.py b/pos/blueprints/chat_bp.py index 393ad25..0272bae 100644 --- a/pos/blueprints/chat_bp.py +++ b/pos/blueprints/chat_bp.py @@ -60,29 +60,39 @@ def chat(): effective_query = part_words if effective_query and tenant: - # First: search local inventory - try: - local_results = _search_local_inventory(tenant, effective_query, search_query or '', branch_id) - if local_results: - search_results.extend(local_results) - except Exception: - pass + # Support multiple search queries separated by | (for quotes/cotizaciones) + query_terms = [q.strip() for q in effective_query.split('|') if q.strip()] + seen_part_numbers = set() - # Then: search TecDoc catalog - if master: + for qt in query_terms: + # First: search local inventory try: - catalog_results = catalog_service.smart_search( - master, effective_query, tenant, branch_id, limit=10 - ) - if catalog_results: - # Mark as catalog results and avoid duplicates - local_parts = {r.get('part_number', '') for r in search_results} - for cr in catalog_results: - if cr.get('oem_part_number', '') not in local_parts: - cr['source'] = 'catalog' - search_results.append(cr) + local_results = _search_local_inventory(tenant, qt, qt, branch_id) + if local_results: + for lr in local_results: + pn = lr.get('part_number', '') + if pn not in seen_part_numbers: + seen_part_numbers.add(pn) + search_results.append(lr) except Exception: - pass # search failure is non-fatal + pass + + # Then: search TecDoc catalog + if master: + try: + per_query_limit = max(3, 10 // len(query_terms)) + catalog_results = catalog_service.smart_search( + master, qt, tenant, branch_id, limit=per_query_limit + ) + if catalog_results: + for cr in catalog_results: + pn = cr.get('oem_part_number', '') + if pn not in seen_part_numbers: + seen_part_numbers.add(pn) + cr['source'] = 'catalog' + search_results.append(cr) + except Exception: + pass # search failure is non-fatal except Exception: pass # DB failure is non-fatal for chat diff --git a/pos/services/ai_chat.py b/pos/services/ai_chat.py index c8914c4..2e1bd88 100644 --- a/pos/services/ai_chat.py +++ b/pos/services/ai_chat.py @@ -49,6 +49,43 @@ Reglas OBLIGATORIAS: 4. Nombres mexicanos: Tsuru = TSURU, Aveo = AVEO, Jetta = JETTA, Pointer = POINTER, Chevy = CORSA, Vocho = BEETLE. 5. No preguntes mas info si ya puedes buscar. Si el usuario dice "balatas para Tsuru 2015", busca directo. 6. "message" es breve y directo: "Buscando balatas para Nissan Tsuru 2015..." + +Cuando el usuario describe un SINTOMA del vehiculo (no una parte especifica), diagnostica el problema y sugiere las partes que podrian necesitar reemplazo. + +Ejemplos de sintomas: +- "el carro vibra al frenar" → Discos de freno y/o balatas desgastadas. search_query: "Brake Disc" +- "se calienta el motor" → Termostato, bomba de agua, radiador. search_query: "Thermostat" +- "hace ruido al dar vuelta" → Juntas homocineticas. search_query: "CV Joint" +- "no arranca" → Bateria, alternador, motor de arranque. search_query: "Starter Motor" +- "gasta mucha gasolina" → Filtro de aire, bujias, inyectores. search_query: "Air Filter" +- "huele a gasolina" → Inyectores, bomba de gasolina, mangueras. search_query: "Fuel Pump" +- "se jala a un lado" → Terminales de direccion, rotulas, alineacion. search_query: "Tie Rod End" +- "hace ruido al arrancar" → Banda serpentina, tensor, marcha. search_query: "Serpentine Belt" +- "pierde aceite" → Junta de tapa de valvulas, empaques. search_query: "Gasket" +- "el aire no enfria" → Compresor de AC, gas refrigerante. search_query: "A/C Compressor" + +Si detectas un sintoma, responde con: +1. Diagnostico probable +2. Lista de partes que podrian necesitar reemplazo (en orden de probabilidad) +3. search_query con la parte mas probable + +Cuando el usuario pida una COTIZACION o diga "cotizame", "cuanto cuesta", "precio de": +1. Identifica TODAS las partes necesarias para el trabajo completo +2. Devuelve multiples search_queries separadas por | + +Ejemplo: "cotizame frenos completos para Corolla 2020" +search_query: "Brake Pad|Brake Disc|Brake Fluid|Brake Hose" + +Ejemplo: "servicio completo para Tsuru 2015" +search_query: "Oil Filter|Air Filter|Spark Plug|Coolant|Brake Fluid" + +Ejemplo: "kit de distribucion para Jetta 2018" +search_query: "Timing Belt|Tensioner|Idler Pulley|Water Pump" + +Detecta el idioma del usuario y responde en el mismo idioma. +Si escribe en ingles, responde en ingles. +Si escribe en espanol, responde en espanol. +El search_query SIEMPRE debe ser en ingles (el catalogo TecDoc esta en ingles). """ diff --git a/pos/static/css/chat.css b/pos/static/css/chat.css index a0ced4f..ec6a47e 100644 --- a/pos/static/css/chat.css +++ b/pos/static/css/chat.css @@ -268,6 +268,35 @@ .chat-send-btn:hover { background: var(--color-primary-hover, #e5952f); } .chat-send-btn:disabled { opacity: 0.5; cursor: not-allowed; } +/* ─── Camera Button (Photo identification) ─── */ + +.chat-cam-btn { + width: 38px; + height: 38px; + border-radius: var(--radius-md); + border: 1px solid var(--color-border); + background: var(--color-bg-base); + color: var(--color-text-secondary); + font-size: 1.1rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: background var(--duration-fast) var(--ease-in-out), + color var(--duration-fast) var(--ease-in-out), + border-color var(--duration-fast) var(--ease-in-out); +} + +.chat-cam-btn:hover { + border-color: var(--color-accent); + color: var(--color-accent); +} + +.chat-msg-image img { + border: 1px solid var(--color-border); +} + /* ─── Mic Button (Voice Input) ─── */ .chat-mic-btn { diff --git a/pos/static/js/chat.js b/pos/static/js/chat.js index de3f72c..bb6463c 100644 --- a/pos/static/js/chat.js +++ b/pos/static/js/chat.js @@ -39,6 +39,8 @@
+ + ${hasSpeechAPI ? '' : ''}
@@ -58,6 +60,12 @@ } }); + // 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); @@ -162,6 +170,82 @@ }, 2000); } + // ─── 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 to AI as a text description (vision model placeholder) + const photoPrompt = 'El usuario envio una foto de una parte automotriz. Describe que parte podria ser y sugiere busquedas.'; + history.push({ role: 'user', content: photoPrompt }); + if (history.length > 20) history.splice(0, 2); + + isSending = true; + document.getElementById('chatSend').disabled = true; + showTyping(true); + + const token = getToken(); + fetch('/pos/api/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + }, + body: JSON.stringify({ + message: photoPrompt, + 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');