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
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

View File

@@ -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).
"""

View File

@@ -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 {

View File

@@ -39,6 +39,8 @@
</div>
<div class="chat-input-area">
<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>' : ''}
<button class="chat-send-btn" id="chatSend" aria-label="Enviar">&#9654;</button>
</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)
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 = '<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() {
isOpen = !isOpen;
const panel = document.getElementById('chatPanel');