- Kiosk mode: fullscreen, wake lock, auto-login, context menu block, PWA/Capacitor detection - AI vision: camera photos analyzed by Gemma 3 27B vision model via OpenRouter - AI part classification: auto-suggest name/brand/category when entering part number - Public catalog chatbot: /api/chat endpoint with rate limiting, chat widget on catalog page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -200,8 +200,9 @@
|
||||
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.';
|
||||
// Send image to AI vision model for real analysis
|
||||
var imageData = ev.target.result; // full data URL
|
||||
var photoPrompt = 'Identifica esta parte automotriz y sugiere terminos de busqueda.';
|
||||
history.push({ role: 'user', content: photoPrompt });
|
||||
if (history.length > 20) history.splice(0, 2);
|
||||
|
||||
@@ -209,7 +210,7 @@
|
||||
document.getElementById('chatSend').disabled = true;
|
||||
showTyping(true);
|
||||
|
||||
const token = getToken();
|
||||
var token = getToken();
|
||||
fetch('/pos/api/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -218,6 +219,7 @@
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: photoPrompt,
|
||||
image: imageData,
|
||||
history: history.slice(-10)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -499,6 +499,21 @@ const Config = (() => {
|
||||
// Bind UI events
|
||||
bindEvents();
|
||||
|
||||
// Kiosk mode toggle
|
||||
var kioskToggle = document.getElementById('cfg-kiosk-mode');
|
||||
if (kioskToggle && window.NexusKiosk) {
|
||||
kioskToggle.checked = window.NexusKiosk.isEnabled();
|
||||
kioskToggle.addEventListener('change', function () {
|
||||
if (this.checked) {
|
||||
window.NexusKiosk.enable();
|
||||
toast('Modo Kiosko activado');
|
||||
} else {
|
||||
window.NexusKiosk.disable();
|
||||
toast('Modo Kiosko desactivado');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load real data in parallel
|
||||
loadBranches();
|
||||
loadEmployees();
|
||||
|
||||
@@ -121,7 +121,48 @@
|
||||
|
||||
function showCreateModal() {
|
||||
document.getElementById('createModal').classList.add('is-open');
|
||||
// Attach AI classification on part number blur
|
||||
var pnInput = document.getElementById('newPartNumber');
|
||||
if (pnInput && !pnInput._classifyBound) {
|
||||
pnInput._classifyBound = true;
|
||||
pnInput.addEventListener('blur', function () {
|
||||
var pn = this.value.trim();
|
||||
if (pn.length < 3) return;
|
||||
var nameInput = document.getElementById('newName');
|
||||
// Only auto-classify if name is still empty
|
||||
if (nameInput && nameInput.value.trim()) return;
|
||||
classifyPartNumber(pn);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function classifyPartNumber(partNumber) {
|
||||
var resultEl = document.getElementById('createResult');
|
||||
resultEl.innerHTML = '<span style="color:var(--color-text-muted);">Consultando IA...</span>';
|
||||
apiFetch(API + '/classify/' + encodeURIComponent(partNumber)).then(function (data) {
|
||||
if (!data) return;
|
||||
if (data.name) {
|
||||
document.getElementById('newName').value = data.name;
|
||||
}
|
||||
if (data.brand) {
|
||||
document.getElementById('newBrand').value = data.brand;
|
||||
}
|
||||
// Show suggestion label
|
||||
var parts = [];
|
||||
if (data.name) parts.push(data.name);
|
||||
if (data.brand) parts.push(data.brand);
|
||||
if (data.vehicle) parts.push(data.vehicle);
|
||||
if (data.category) parts.push(data.category);
|
||||
if (parts.length > 0) {
|
||||
resultEl.innerHTML = '<span style="color:var(--color-accent);font-size:var(--text-caption);">Sugerido por IA: ' + esc(parts.join(' | ')) + '</span>';
|
||||
} else {
|
||||
resultEl.innerHTML = '<span style="color:var(--color-text-muted);font-size:var(--text-caption);">IA no pudo identificar este numero de parte</span>';
|
||||
}
|
||||
}).catch(function () {
|
||||
resultEl.innerHTML = '';
|
||||
});
|
||||
}
|
||||
|
||||
function closeCreateModal() {
|
||||
document.getElementById('createModal').classList.remove('is-open');
|
||||
document.getElementById('createResult').innerHTML = '';
|
||||
|
||||
168
pos/static/js/kiosk.js
Normal file
168
pos/static/js/kiosk.js
Normal file
@@ -0,0 +1,168 @@
|
||||
// /home/Autopartes/pos/static/js/kiosk.js
|
||||
// Kiosk mode for Nexus POS — fullscreen, wake lock, auto-login, no right-click
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var STORAGE_KEY = 'pos_kiosk_mode';
|
||||
|
||||
// ─── Detection ───
|
||||
function isPWA() {
|
||||
return window.matchMedia('(display-mode: standalone)').matches
|
||||
|| window.navigator.standalone === true;
|
||||
}
|
||||
|
||||
function isCapacitor() {
|
||||
return typeof window.Capacitor !== 'undefined' && window.Capacitor.isNativePlatform && window.Capacitor.isNativePlatform();
|
||||
}
|
||||
|
||||
function isKioskEnabled() {
|
||||
// Enabled if explicitly set in localStorage, or if running as PWA/Capacitor
|
||||
var pref = localStorage.getItem(STORAGE_KEY);
|
||||
if (pref === 'true') return true;
|
||||
if (pref === 'false') return false;
|
||||
// Auto-detect
|
||||
return isPWA() || isCapacitor();
|
||||
}
|
||||
|
||||
// ─── Fullscreen ───
|
||||
var fullscreenRequested = false;
|
||||
|
||||
function requestFullscreen() {
|
||||
if (fullscreenRequested) return;
|
||||
var el = document.documentElement;
|
||||
var fn = el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen || el.msRequestFullscreen;
|
||||
if (fn) {
|
||||
fn.call(el).catch(function () { /* user may have denied */ });
|
||||
fullscreenRequested = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Wake Lock ───
|
||||
var wakeLock = null;
|
||||
|
||||
async function acquireWakeLock() {
|
||||
if (!('wakeLock' in navigator)) return;
|
||||
try {
|
||||
wakeLock = await navigator.wakeLock.request('screen');
|
||||
wakeLock.addEventListener('release', function () {
|
||||
wakeLock = null;
|
||||
});
|
||||
} catch (e) {
|
||||
// Wake lock may fail if tab not visible
|
||||
}
|
||||
}
|
||||
|
||||
function releaseWakeLock() {
|
||||
if (wakeLock) {
|
||||
wakeLock.release();
|
||||
wakeLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Re-acquire wake lock when page becomes visible again
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.visibilityState === 'visible' && isKioskEnabled() && !wakeLock) {
|
||||
acquireWakeLock();
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Auto-login ───
|
||||
function tryAutoLogin() {
|
||||
var token = localStorage.getItem('pos_token');
|
||||
if (!token) return;
|
||||
|
||||
// Check if we are on the login page
|
||||
var isLoginPage = window.location.pathname.indexOf('/pos/login') !== -1;
|
||||
if (!isLoginPage) return;
|
||||
|
||||
// Validate token by trying to decode expiry (JWT is base64)
|
||||
try {
|
||||
var parts = token.split('.');
|
||||
if (parts.length !== 3) return;
|
||||
var payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
|
||||
var exp = payload.exp;
|
||||
if (exp && (exp * 1000) > Date.now()) {
|
||||
// Token still valid — skip login
|
||||
window.location.href = '/pos/';
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid token, stay on login
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Activate kiosk mode ───
|
||||
function activate() {
|
||||
// Prevent navigation away
|
||||
window.addEventListener('beforeunload', function (e) {
|
||||
if (isKioskEnabled()) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Disable context menu
|
||||
document.addEventListener('contextmenu', function (e) {
|
||||
if (isKioskEnabled()) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Request fullscreen on first user interaction
|
||||
var interactionEvents = ['click', 'touchstart', 'keydown'];
|
||||
function onFirstInteraction() {
|
||||
if (isKioskEnabled()) {
|
||||
requestFullscreen();
|
||||
acquireWakeLock();
|
||||
}
|
||||
interactionEvents.forEach(function (evt) {
|
||||
document.removeEventListener(evt, onFirstInteraction);
|
||||
});
|
||||
}
|
||||
interactionEvents.forEach(function (evt) {
|
||||
document.addEventListener(evt, onFirstInteraction, { once: false });
|
||||
});
|
||||
|
||||
// Auto-login if on login page
|
||||
tryAutoLogin();
|
||||
}
|
||||
|
||||
// ─── Public API ───
|
||||
window.NexusKiosk = {
|
||||
isEnabled: isKioskEnabled,
|
||||
isPWA: isPWA,
|
||||
isCapacitor: isCapacitor,
|
||||
enable: function () {
|
||||
localStorage.setItem(STORAGE_KEY, 'true');
|
||||
requestFullscreen();
|
||||
acquireWakeLock();
|
||||
},
|
||||
disable: function () {
|
||||
localStorage.setItem(STORAGE_KEY, 'false');
|
||||
releaseWakeLock();
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen().catch(function () {});
|
||||
}
|
||||
},
|
||||
toggle: function () {
|
||||
if (isKioskEnabled()) {
|
||||
window.NexusKiosk.disable();
|
||||
} else {
|
||||
window.NexusKiosk.enable();
|
||||
}
|
||||
return isKioskEnabled();
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Init ───
|
||||
if (isKioskEnabled()) {
|
||||
activate();
|
||||
}
|
||||
|
||||
// Also activate if preference changes (e.g. toggled from config)
|
||||
window.addEventListener('storage', function (e) {
|
||||
if (e.key === STORAGE_KEY && e.newValue === 'true') {
|
||||
activate();
|
||||
}
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user