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:
@@ -60,25 +60,35 @@ def chat():
|
||||
effective_query = part_words
|
||||
|
||||
if effective_query and tenant:
|
||||
# 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()
|
||||
|
||||
for qt in query_terms:
|
||||
# First: search local inventory
|
||||
try:
|
||||
local_results = _search_local_inventory(tenant, effective_query, search_query or '', branch_id)
|
||||
local_results = _search_local_inventory(tenant, qt, qt, branch_id)
|
||||
if local_results:
|
||||
search_results.extend(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
|
||||
|
||||
# Then: search TecDoc catalog
|
||||
if master:
|
||||
try:
|
||||
per_query_limit = max(3, 10 // len(query_terms))
|
||||
catalog_results = catalog_service.smart_search(
|
||||
master, effective_query, tenant, branch_id, limit=10
|
||||
master, qt, tenant, branch_id, limit=per_query_limit
|
||||
)
|
||||
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:
|
||||
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:
|
||||
|
||||
@@ -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).
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">📷</button>
|
||||
${hasSpeechAPI ? '<button class="chat-mic-btn" id="chatMic" aria-label="Entrada por voz" title="Entrada por voz">🎤</button>' : ''}
|
||||
<button class="chat-send-btn" id="chatSend" aria-label="Enviar">▶</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');
|
||||
|
||||
Reference in New Issue
Block a user