Cambios implementados: 1. Lazy loading de imágenes: - catalog.js: loading="lazy" decoding="async" en part cards y detail panel - inventory.js: lazy loading en imagen de detalle de item 2. Minificación de assets: - scripts/minify-assets.sh: minifica JS (terser) y CSS para POS y Dashboard - 25 archivos .min.js + 5 .min.css generados en pos/static/ - 14 archivos .min.js + 8 .min.css generados en dashboard/ 3. Nginx auto-serve minified: - try_files $1.min.js antes de servir .js original - try_files $1.min.css antes de servir .css original - Transparente para los templates HTML (cero cambios en HTML) 4. Cache warming script: - scripts/warm_vehicle_cache.py: pobla Redis con vehicle info por batches - Mitiga DISTINCT ON + 4 JOINs sobre 2B filas - Corre en background, procesa ~1.5M parts Tests: 73/73 pasando
1 line
8.3 KiB
JavaScript
1 line
8.3 KiB
JavaScript
!function(){"use strict";let e=!1,t=!1,n=!1,a=null;const s=[],i="webkitSpeechRecognition"in window||"SpeechRecognition"in window;function c(){const e=document.createElement("button");e.className="chat-fab",e.id="chatFab",e.title="Asistente IA",e.innerHTML="💬",e.setAttribute("aria-label","Abrir asistente IA");const t=document.createElement("div");t.className="chat-panel",t.id="chatPanel",t.innerHTML=`\n <div class="chat-header">\n <h3>Asistente IA — Buscar partes</h3>\n <button class="chat-header-close" id="chatClose" aria-label="Cerrar">×</button>\n </div>\n <div class="chat-messages" id="chatMessages">\n <div class="chat-msg ai">Hola, soy el asistente de Nexus. Dime que refaccion necesitas y te ayudo a encontrarla.</div>\n <div class="chat-typing" id="chatTyping">\n <span></span><span></span><span></span>\n </div>\n </div>\n <div class="chat-input-area">\n <textarea class="chat-input" id="chatInput" placeholder="Ej: Balatas para Tsuru 2015..." rows="1"></textarea>\n <input type="file" id="chatImageInput" accept="image/*" capture="environment" style="display:none;">\n <button class="chat-cam-btn" id="chatCam" aria-label="Enviar foto de parte" title="Identificar parte por foto">📷</button>\n ${i?'<button class="chat-mic-btn" id="chatMic" aria-label="Entrada por voz" title="Entrada por voz">🎤</button>':""}\n <button class="chat-send-btn" id="chatSend" aria-label="Enviar">▶</button>\n </div>\n `,document.body.appendChild(e),document.body.appendChild(t),e.addEventListener("click",l),document.getElementById("chatClose").addEventListener("click",l),document.getElementById("chatSend").addEventListener("click",u),document.getElementById("chatInput").addEventListener("keydown",(function(e){"Enter"!==e.key||e.shiftKey||(e.preventDefault(),u())})),document.getElementById("chatCam").addEventListener("click",(function(){document.getElementById("chatImageInput").click()})),document.getElementById("chatImageInput").addEventListener("change",r),i&&document.getElementById("chatMic").addEventListener("click",o),document.getElementById("chatInput").addEventListener("input",(function(){this.style.height="auto",this.style.height=Math.min(this.scrollHeight,80)+"px"}))}function o(){n?function(){a&&(a.abort(),a=null);n=!1;const e=document.getElementById("chatMic");e&&e.classList.remove("listening")}():function(){const e=window.SpeechRecognition||window.webkitSpeechRecognition;if(!e)return;a=new e,a.lang="es-MX",a.continuous=!1,a.interimResults=!0;const t=document.getElementById("chatInput"),s=document.getElementById("chatMic"),i=t.placeholder;a.onstart=function(){n=!0,s.classList.add("listening"),t.placeholder="Escuchando...",t.value=""},a.onresult=function(e){let n="",a="";for(let t=e.resultIndex;t<e.results.length;t++)e.results[t].isFinal?a+=e.results[t][0].transcript:n+=e.results[t][0].transcript;t.value=a||n},a.onend=function(){n=!1,s.classList.remove("listening"),t.placeholder=i,a=null,t.value.trim()&&u()},a.onerror=function(e){n=!1,s.classList.remove("listening"),t.placeholder=i,a=null,"no-speech"!==e.error&&"audio-capture"!==e.error&&"not-allowed"!==e.error||function(e){const t=document.createElement("div");t.className="chat-voice-toast",t.textContent=e,document.body.appendChild(t),setTimeout((function(){t.classList.add("visible")}),10),setTimeout((function(){t.classList.remove("visible"),setTimeout((function(){t.remove()}),300)}),2e3)}("No se detecto voz")},a.start()}()}function r(e){const n=e.target.files&&e.target.files[0];if(!n)return;if(e.target.value="",!n.type.startsWith("image/"))return void m("Solo se permiten imagenes.","ai");if(n.size>5242880)return void m("La imagen es muy grande (max 5MB).","ai");const a=new FileReader;a.onload=function(e){const n=document.getElementById("chatMessages"),a=document.getElementById("chatTyping"),i=document.createElement("div");i.className="chat-msg user chat-msg-image",i.innerHTML='<img src="'+e.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>',n.insertBefore(i,a),y();var c=e.target.result,o="Identifica esta parte automotriz y sugiere terminos de busqueda.";s.push({role:"user",content:o}),s.length>20&&s.splice(0,2),t=!0,document.getElementById("chatSend").disabled=!0,g(!0);var r=d();fetch("/pos/api/chat",{method:"POST",headers:{"Content-Type":"application/json",Authorization:"Bearer "+r},body:JSON.stringify({message:o,image:c,history:s.slice(-10)})}).then((function(e){return e.json()})).then((function(e){const t=e.response||"No pude identificar la parte. Intenta describirla con texto.";m(t,"ai"),s.push({role:"assistant",content:t}),e.vehicle&&e.vehicle.brand_id&&p(e.vehicle),e.search_results&&e.search_results.length>0&&h(e.search_results)})).catch((function(e){m("Error al procesar imagen: "+e.message,"ai")})).finally((function(){t=!1,document.getElementById("chatSend").disabled=!1,g(!1)}))},a.readAsDataURL(n)}function l(){e=!e;const t=document.getElementById("chatPanel"),n=document.getElementById("chatFab");e?(t.classList.add("open"),n.style.display="none",document.getElementById("chatInput").focus()):(t.classList.remove("open"),n.style.display="flex")}function d(){return window.__pos&&window.__pos.token?window.__pos.token:localStorage.getItem("pos_token")||""}async function u(){if(t)return;const e=document.getElementById("chatInput"),n=e.value.trim();if(n){e.value="",e.style.height="auto",m(n,"user"),s.push({role:"user",content:n}),s.length>20&&s.splice(0,2),t=!0,document.getElementById("chatSend").disabled=!0,g(!0);try{const e=d(),t=await fetch("/pos/api/chat",{method:"POST",headers:{"Content-Type":"application/json",Authorization:"Bearer "+e},body:JSON.stringify({message:n,history:s.slice(-10)})}),a=await t.json();if(!t.ok)return void m("Error: "+(a.error||t.statusText),"ai");const i=a.response||"Sin respuesta.";m(i,"ai"),s.push({role:"assistant",content:i}),a.vehicle&&a.vehicle.brand_id&&p(a.vehicle),a.search_results&&a.search_results.length>0&&h(a.search_results)}catch(e){m("Error de conexion: "+e.message,"ai")}finally{t=!1,document.getElementById("chatSend").disabled=!1,g(!1)}}}function m(e,t){const n=document.getElementById("chatMessages"),a=document.getElementById("chatTyping"),s=document.createElement("div");s.className="chat-msg "+t,s.textContent=e,n.insertBefore(s,a),y()}function p(e){const t=document.getElementById("chatMessages"),n=document.getElementById("chatTyping"),a=document.createElement("div");a.className="chat-vehicle-banner";let s="<strong>"+f(e.brand||"")+" "+f(e.model||"")+"</strong>";e.year&&(s+=" "+e.year),e.mye_options&&e.mye_options.length>0&&(s+="<br>Motorizaciones encontradas:",e.mye_options.forEach((function(e){s+="<br>• "+f(e.engine),e.trim&&(s+=" ("+f(e.trim)+")")}))),a.innerHTML=s,t.insertBefore(a,n),y()}function h(e){const t=document.getElementById("chatMessages"),n=document.getElementById("chatTyping"),a=document.createElement("div");a.className="chat-parts",e.slice(0,8).forEach((function(e){const t=document.createElement("div");t.className="chat-part-card";const n="local"===e.source,s=e.local_stock||0,i=s>0?"in-stock":"",c=s>0?s+" en stock":"Sin stock local",o=e.name_es||e.name_part||"",r=e.oem_part_number||e.part_number||"",d=e.brand||"",u=e.price_1?"$"+parseFloat(e.price_1).toFixed(2):"",m=n?'<span style="background:var(--color-success);color:#fff;padding:1px 6px;border-radius:4px;font-size:0.65rem;margin-left:6px;">MI INVENTARIO</span>':'<span style="background:var(--color-primary);color:#fff;padding:1px 6px;border-radius:4px;font-size:0.65rem;margin-left:6px;">CATÁLOGO</span>';t.innerHTML='<div class="part-number">'+f(r)+m+(u?" — "+u:"")+'</div><div class="part-name">'+f(o)+(d?' <span style="color:var(--color-text-muted);">('+f(d)+")</span>":"")+'</div><div class="part-stock '+i+'">'+f(c)+"</div>",t.addEventListener("click",(function(){e.id_part&&"function"==typeof window.openPartDetail&&(window.openPartDetail(e.id_part),l())})),a.appendChild(t)})),t.insertBefore(a,n),y()}function g(e){const t=document.getElementById("chatTyping");t&&t.classList.toggle("visible",e),e&&y()}function y(){const e=document.getElementById("chatMessages");e&&(e.scrollTop=e.scrollHeight)}function f(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",c):c()}(); |