feat(pos/chat): add 4 AI chatbot improvements — symptom diagnosis, smart quotes, photo ID, multilanguage

1. Symptom diagnosis: AI now detects vehicle symptoms and suggests probable parts
2. Smart quotations: "cotizame frenos completos" returns multiple search_queries (pipe-separated), backend searches each term and deduplicates
3. Photo identification: camera button in chat widget uploads image and sends placeholder prompt (ready for vision model)
4. Multilanguage: AI detects user language and responds accordingly, search_query always in English

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 08:00:41 +00:00
parent fd31864cea
commit 39f2aaf98f
4 changed files with 180 additions and 20 deletions

View File

@@ -60,29 +60,39 @@ def chat():
effective_query = part_words effective_query = part_words
if effective_query and tenant: if effective_query and tenant:
# First: search local inventory # Support multiple search queries separated by | (for quotes/cotizaciones)
try: query_terms = [q.strip() for q in effective_query.split('|') if q.strip()]
local_results = _search_local_inventory(tenant, effective_query, search_query or '', branch_id) seen_part_numbers = set()
if local_results:
search_results.extend(local_results)
except Exception:
pass
# Then: search TecDoc catalog for qt in query_terms:
if master: # First: search local inventory
try: try:
catalog_results = catalog_service.smart_search( local_results = _search_local_inventory(tenant, qt, qt, branch_id)
master, effective_query, tenant, branch_id, limit=10 if local_results:
) for lr in local_results:
if catalog_results: pn = lr.get('part_number', '')
# Mark as catalog results and avoid duplicates if pn not in seen_part_numbers:
local_parts = {r.get('part_number', '') for r in search_results} seen_part_numbers.add(pn)
for cr in catalog_results: search_results.append(lr)
if cr.get('oem_part_number', '') not in local_parts:
cr['source'] = 'catalog'
search_results.append(cr)
except Exception: 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: except Exception:
pass # DB failure is non-fatal for chat pass # DB failure is non-fatal for chat

View File

@@ -49,6 +49,43 @@ Reglas OBLIGATORIAS:
4. Nombres mexicanos: Tsuru = TSURU, Aveo = AVEO, Jetta = JETTA, Pointer = POINTER, Chevy = CORSA, Vocho = BEETLE. 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. 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..." 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).
""" """

View File

@@ -268,6 +268,35 @@
.chat-send-btn:hover { background: var(--color-primary-hover, #e5952f); } .chat-send-btn:hover { background: var(--color-primary-hover, #e5952f); }
.chat-send-btn:disabled { opacity: 0.5; cursor: not-allowed; } .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) ─── */ /* ─── Mic Button (Voice Input) ─── */
.chat-mic-btn { .chat-mic-btn {

View File

@@ -39,6 +39,8 @@
</div> </div>
<div class="chat-input-area"> <div class="chat-input-area">
<textarea class="chat-input" id="chatInput" placeholder="Ej: Balatas para Tsuru 2015..." rows="1"></textarea> <textarea class="chat-input" id="chatInput" placeholder="Ej: Balatas para Tsuru 2015..." rows="1"></textarea>
<input type="file" id="chatImageInput" accept="image/*" capture="environment" style="display:none;">
<button class="chat-cam-btn" id="chatCam" aria-label="Enviar foto de parte" title="Identificar parte por foto">&#128247;</button>
${hasSpeechAPI ? '<button class="chat-mic-btn" id="chatMic" aria-label="Entrada por voz" title="Entrada por voz">&#127908;</button>' : ''} ${hasSpeechAPI ? '<button class="chat-mic-btn" id="chatMic" aria-label="Entrada por voz" title="Entrada por voz">&#127908;</button>' : ''}
<button class="chat-send-btn" id="chatSend" aria-label="Enviar">&#9654;</button> <button class="chat-send-btn" id="chatSend" aria-label="Enviar">&#9654;</button>
</div> </div>
@@ -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) // Mic button (only if Speech API available)
if (hasSpeechAPI) { if (hasSpeechAPI) {
document.getElementById('chatMic').addEventListener('click', toggleVoice); document.getElementById('chatMic').addEventListener('click', toggleVoice);
@@ -162,6 +170,82 @@
}, 2000); }, 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 = '<img src="' + ev.target.result + '" alt="Foto de parte" style="max-width:180px;max-height:140px;border-radius:8px;display:block;margin-bottom:4px;">'
+ '<span>Identificar esta parte</span>';
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() { function toggleChat() {
isOpen = !isOpen; isOpen = !isOpen;
const panel = document.getElementById('chatPanel'); const panel = document.getElementById('chatPanel');