// Application Configuration const PRICING_DATABASE = { "apfel_elstar": { name: "Apfel Elstar", price: 2.99, unit: "kg" }, "apfel_gala": { name: "Apfel Gala", price: 2.49, unit: "kg" }, "apfel_granny_smith": { name: "Apfel Granny Smith", price: 2.79, unit: "kg" }, "banane_chiquita": { name: "Banane Chiquita", price: 1.99, unit: "kg" }, "banane_bio": { name: "Bio Banane", price: 2.29, unit: "kg" }, "birne": { name: "Birne Abate Fetel", price: 3.29, unit: "kg" }, "orange": { name: "Orangen", price: 2.19, unit: "kg" }, "zitrone": { name: "Zitronen", price: 0.59, unit: "Stk." }, "limette": { name: "Limetten", price: 0.49, unit: "Stk." }, "erdbeere": { name: "Erdbeeren Schale", price: 2.99, unit: "Stk." }, "blaubeere": { name: "Kulturheidelbeeren", price: 1.99, unit: "Stk." }, "weintraube_hell": { name: "Tafeltrauben hell", price: 3.99, unit: "kg" }, "weintraube_dunkel": { name: "Tafeltrauben dunkel", price: 4.29, unit: "kg" }, "pfirsich": { name: "Pfirsiche", price: 2.99, unit: "kg" }, "tomate": { name: "Rispentomaten", price: 3.49, unit: "kg" }, "gurke": { name: "Salatgurke", price: 0.99, unit: "Stk." }, "kartoffel": { name: "Speisekartoffeln", price: 1.49, unit: "kg" }, "karotte": { name: "Speisemöhren", price: 1.29, unit: "kg" }, "zwiebel_gelb": { name: "Speisezwiebeln", price: 1.19, unit: "kg" }, "zwiebel_rot": { name: "Rote Zwiebeln", price: 1.69, unit: "kg" }, "knoblauch": { name: "Knoblauch", price: 0.89, unit: "Stk." }, "brokkoli": { name: "Brokkoli", price: 1.79, unit: "Stk." }, "paprika_rot": { name: "Paprika rot", price: 3.99, unit: "kg" }, "paprika_gelb": { name: "Paprika gelb", price: 3.99, unit: "kg" }, "paprika_gruen": { name: "Paprika grün", price: 3.49, unit: "kg" }, "champignon": { name: "Champignons weiß", price: 1.89, unit: "Stk." }, "zucchini": { name: "Zucchini", price: 2.19, unit: "kg" }, "avocado": { name: "Avocado Hass", price: 1.29, unit: "Stk." } }; // Application State let activeTab = 'checkout'; let isWebcamActive = false; let webcamStream = null; let simulatedStreamInterval = null; let currentFrameBase64 = null; let activeCameraDeviceId = null; // stores selected camera device ID let isDetecting = false; let autoDetectEnabled = true; let trainingPollInterval = null; // Learn/Anlern-Modus State let learnBackgroundFrame = null; // stores ImageData or Image object for canvas subtraction let learnSelectedClass = null; // selected article key for training let learnSessionImages = []; // list of filenames in the current session let isSerialCapturing = false; // Scanner Emulator State let autoScanEnabled = true; let stableClass = null; let stableCount = 0; const STABILITY_THRESHOLD = 4; // number of frames (approx 0.8s) let lastScannedClass = null; let scanLockActive = false; let articlesDatabase = {}; // Cart State let shoppingCart = []; // DOM Elements const videoEl = document.getElementById("webcam"); const canvasEl = document.getElementById("camera-canvas"); const ctx = canvasEl.getContext("2d"); const learnCanvasEl = document.getElementById("learn-camera-canvas"); const learnCtx = learnCanvasEl ? learnCanvasEl.getContext("2d") : null; const toggleCameraBtn = document.getElementById("toggle-camera-btn"); const triggerScaleBtn = document.getElementById("trigger-scale-btn"); const autoDetectToggle = document.getElementById("auto-detect-toggle"); const cameraTypeBadge = document.getElementById("camera-type-badge"); const quickSelectButtons = document.getElementById("quick-selection-buttons"); const cartList = document.getElementById("cart-list"); const cartSubtotalEl = document.getElementById("cart-subtotal"); const cartTotalEl = document.getElementById("cart-total"); const emptyCartMsg = document.getElementById("empty-cart-msg"); // --- TAB NAVIGATION --- function switchTab(tabName) { activeTab = tabName; document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); document.querySelectorAll('.nav-btn').forEach(el => el.classList.remove('active')); document.getElementById(`tab-${tabName}`).classList.add('active'); document.getElementById(`tab-btn-${tabName}`).classList.add('active'); if (tabName === 'dashboard') { // Start polling training status when entering dashboard startTrainingPolling(); refreshAugmentedPreview(); } else { stopTrainingPolling(); } if (tabName === 'articles') { loadArticlesDatabase(); } if (tabName === 'learn') { checkBackgroundCalibration(); loadLearnArticles(); updateLearnBadge(); } } // --- CAMERA & DETECTION LOGIC --- async function initCameraSelection() { try { const devices = await navigator.mediaDevices.enumerateDevices(); const videoDevices = devices.filter(device => device.kind === 'videoinput'); const selectCheckout = document.getElementById("camera-select"); const selectLearn = document.getElementById("learn-camera-select"); if (selectCheckout && selectLearn) { selectCheckout.innerHTML = ""; selectLearn.innerHTML = ""; if (videoDevices.length === 0) { const opt = document.createElement("option"); opt.value = ""; opt.textContent = "Keine Kamera gefunden"; selectCheckout.appendChild(opt); selectLearn.appendChild(opt.cloneNode(true)); return; } videoDevices.forEach((device, idx) => { const label = device.label || `Kamera ${idx + 1}`; const opt = document.createElement("option"); opt.value = device.deviceId; opt.textContent = label; selectCheckout.appendChild(opt); selectLearn.appendChild(opt.cloneNode(true)); }); // Sync values to current active camera if (activeCameraDeviceId) { selectCheckout.value = activeCameraDeviceId; selectLearn.value = activeCameraDeviceId; } else { activeCameraDeviceId = videoDevices[0].deviceId; selectCheckout.value = activeCameraDeviceId; selectLearn.value = activeCameraDeviceId; } // Hook up change handlers selectCheckout.onchange = (e) => switchCameraDevice(e.target.value); selectLearn.onchange = (e) => switchCameraDevice(e.target.value); } } catch (err) { console.error("Fehler beim Abrufen der Kameras:", err); } } async function switchCameraDevice(deviceId) { activeCameraDeviceId = deviceId; // Sync dropdown values across tabs const selectCheckout = document.getElementById("camera-select"); const selectLearn = document.getElementById("learn-camera-select"); if (selectCheckout) selectCheckout.value = deviceId; if (selectLearn) selectLearn.value = deviceId; if (isWebcamActive) { // Stop current tracks and restart webcam stream with the new device if (webcamStream) { webcamStream.getTracks().forEach(track => track.stop()); webcamStream = null; } await startWebcam(deviceId); } } async function startWebcam(deviceId = null) { try { const targetDeviceId = deviceId || activeCameraDeviceId; const videoConstraints = { width: 640, height: 480 }; if (targetDeviceId) { videoConstraints.deviceId = { exact: targetDeviceId }; } else { videoConstraints.facingMode = "environment"; } webcamStream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints }); videoEl.srcObject = webcamStream; videoEl.style.display = "none"; isWebcamActive = true; // Show camera selector dropdowns const selectCheckout = document.getElementById("camera-select"); const selectLearn = document.getElementById("learn-camera-select"); if (selectCheckout) selectCheckout.style.display = "inline-block"; if (selectLearn) selectLearn.style.display = "inline-block"; cameraTypeBadge.textContent = "LIVE-WEBCAM"; cameraTypeBadge.style.background = "var(--color-primary)"; const learnStreamBadge = document.getElementById("learn-camera-type-badge"); if (learnStreamBadge) { learnStreamBadge.textContent = "LIVE-WEBCAM"; learnStreamBadge.style.background = "var(--color-primary)"; } toggleCameraBtn.innerHTML = ` Kamera deaktivieren `; document.getElementById("stream-status").textContent = "LIVE"; document.getElementById("stream-status").classList.add("live"); // Initialize options list await initCameraSelection(); // Stop simulated stream if active if (simulatedStreamInterval) { clearInterval(simulatedStreamInterval); simulatedStreamInterval = null; } requestAnimationFrame(processWebcamFrame); } catch (err) { console.error("Webcam access denied/unavailable:", err); alert("Webcam konnte nicht gestartet werden. Der simulierte Kamera-Modus wird verwendet."); startSimulatedStream(); } } function stopWebcam() { if (webcamStream) { webcamStream.getTracks().forEach(track => track.stop()); webcamStream = null; } videoEl.srcObject = null; videoEl.style.display = "none"; isWebcamActive = false; // Hide camera selectors when webcam is inactive const selectCheckout = document.getElementById("camera-select"); const selectLearn = document.getElementById("learn-camera-select"); if (selectCheckout) selectCheckout.style.display = "none"; if (selectLearn) selectLearn.style.display = "none"; toggleCameraBtn.innerHTML = ` Webcam aktivieren `; startSimulatedStream(); } function processWebcamFrame() { if (!isWebcamActive) return; if (activeTab === 'checkout') { // Draw current video frame to canvas ctx.drawImage(videoEl, 0, 0, canvasEl.width, canvasEl.height); // Get frame as base64 currentFrameBase64 = canvasEl.toDataURL("image/jpeg", 0.7); // Auto-detect if enabled if (autoDetectEnabled && !isDetecting) { runDetection(); } } else if (activeTab === 'learn') { if (learnCanvasEl && learnCtx) { // Draw current video frame to learn canvas learnCtx.drawImage(videoEl, 0, 0, learnCanvasEl.width, learnCanvasEl.height); // Get frame as base64 currentFrameBase64 = learnCanvasEl.toDataURL("image/jpeg", 0.7); // Draw dynamic green bounding box drawLearnFrameGreenBox(); } } setTimeout(() => { requestAnimationFrame(processWebcamFrame); }, 150); // Limit detection frequency for performance } function startSimulatedStream() { const streamBadge = document.getElementById("camera-type-badge"); const learnStreamBadge = document.getElementById("learn-camera-type-badge"); if (streamBadge) { streamBadge.textContent = "SIMULATIONS-MODUS"; streamBadge.style.background = "var(--color-bg-dark)"; } if (learnStreamBadge) { learnStreamBadge.textContent = "SIMULATIONS-MODUS"; learnStreamBadge.style.background = "var(--color-bg-dark)"; } const streamStatus = document.getElementById("stream-status"); if (streamStatus) { streamStatus.textContent = "Simuliert"; streamStatus.classList.add("live"); } const loadSimulatedFrame = async () => { try { const response = await fetch("/api/simulated_frame"); const data = await response.json(); if (data.status === "success" && data.image) { const img = new Image(); img.onload = () => { if (activeTab === 'checkout') { ctx.drawImage(img, 0, 0, canvasEl.width, canvasEl.height); currentFrameBase64 = data.image; if (autoDetectEnabled && !isDetecting) { runDetection(); } } else if (activeTab === 'learn') { if (learnCanvasEl && learnCtx) { learnCtx.drawImage(img, 0, 0, learnCanvasEl.width, learnCanvasEl.height); currentFrameBase64 = data.image; // Draw dynamic green bounding box drawLearnFrameGreenBox(); } } }; img.src = data.image; } } catch (err) { console.error("Failed to load simulated frame:", err); } }; loadSimulatedFrame(); // First load immediately simulatedStreamInterval = setInterval(loadSimulatedFrame, 2500); // cycle frames every 2.5s } // Perform detection API request async function runDetection() { if (!currentFrameBase64) return; isDetecting = true; try { const response = await fetch("/api/detect", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ image: currentFrameBase64 }) }); const data = await response.json(); if (data.status === "success") { drawDetections(data.predictions); updateQuickButtons(data.predictions); processStabilityAndScan(data.predictions); } } catch (err) { console.error("Detection error:", err); } finally { isDetecting = false; } } // Draw bounding boxes on canvas function drawDetections(predictions) { // Redraw the base frame first (to clear old boxes) const img = new Image(); img.onload = () => { ctx.drawImage(img, 0, 0, canvasEl.width, canvasEl.height); // Draw boxes predictions.forEach(pred => { const [xmin, ymin, xmax, ymax] = pred.box; const w = xmax - xmin; const h = ymax - ymin; const isHighConf = pred.confidence > 0.70; // Box outline ctx.lineWidth = 3; ctx.strokeStyle = isHighConf ? "#10b981" : "#c084fc"; // Green for high conf, purple for low conf ctx.strokeRect(xmin, ymin, w, h); // Label box background ctx.fillStyle = isHighConf ? "rgba(16, 185, 129, 0.85)" : "rgba(168, 85, 247, 0.85)"; const text = `${PRICING_DATABASE[pred.class]?.name || pred.class} (${(pred.confidence * 100).toFixed(0)}%)`; ctx.font = "bold 12px sans-serif"; const textWidth = ctx.measureText(text).width; ctx.fillRect(xmin - 1, ymin - 22, textWidth + 12, 22); // Label text ctx.fillStyle = "#ffffff"; ctx.fillText(text, xmin + 5, ymin - 6); }); }; img.src = currentFrameBase64; } // Update cashier recommendation buttons function updateQuickButtons(predictions) { quickSelectButtons.innerHTML = ""; // Filter predictions with confidence > 0.85 const candidates = predictions.filter(p => p.confidence > 0.85); if (candidates.length === 0) { quickSelectButtons.innerHTML = `

Keine Objekte erkannt

Confidence Schwellenwert unter 85%
`; return; } candidates.forEach(pred => { const itemInfo = PRICING_DATABASE[pred.class] || { name: pred.class, price: 1.0, unit: "kg" }; const confPercent = (pred.confidence * 100).toFixed(0); const isHigh = pred.confidence > 0.70; const btn = document.createElement("button"); btn.className = `quick-btn ${isHigh ? 'high-conf' : 'low-conf'}`; btn.onclick = () => sendEanToPos(pred.class); btn.innerHTML = ` ${itemInfo.name} ${confPercent}% Conf `; quickSelectButtons.appendChild(btn); }); } // --- SHOPPING CART LOGIC --- function addToCart(classKey) { const itemInfo = PRICING_DATABASE[classKey]; if (!itemInfo) return; // Simulate weight or piece count let quantity, itemTotal; if (itemInfo.unit === "kg") { // Random weight between 0.150kg and 1.800kg quantity = parseFloat((0.150 + Math.random() * 1.65).toFixed(3)); itemTotal = parseFloat((quantity * itemInfo.price).toFixed(2)); } else { quantity = 1; itemTotal = itemInfo.price; } const cartItem = { key: classKey, name: itemInfo.name, pricePerUnit: itemInfo.price, unit: itemInfo.unit, quantity: quantity, total: itemTotal, timestamp: Date.now() }; shoppingCart.push(cartItem); updateCartUI(); // Scale animation feedback const scaleCard = document.querySelector(".camera-card"); scaleCard.classList.add("scale-flash"); setTimeout(() => scaleCard.classList.remove("scale-flash"), 400); } function deleteCartItem(timestamp) { shoppingCart = shoppingCart.filter(item => item.timestamp !== timestamp); updateCartUI(); } function clearCart() { shoppingCart = []; updateCartUI(); } function checkoutCart() { if (shoppingCart.length === 0) return; const total = cartTotalEl.textContent; alert(`Zahlung erfolgreich abgeschlossen!\nBetrag: ${total}\nKassenzettel gedruckt.`); clearCart(); } function updateCartUI() { cartList.innerHTML = ""; if (shoppingCart.length === 0) { emptyCartMsg.style.display = "flex"; cartSubtotalEl.textContent = "0,00 €"; cartTotalEl.textContent = "0,00 €"; return; } emptyCartMsg.style.display = "none"; let subtotal = 0; shoppingCart.forEach(item => { subtotal += item.total; const row = document.createElement("div"); row.className = "cart-item-row"; const details = document.createElement("div"); details.className = "item-details"; const name = document.createElement("span"); name.className = "item-name"; name.textContent = item.name; const meta = document.createElement("span"); meta.className = "item-meta"; if (item.unit === "kg") { meta.textContent = `${item.quantity.toFixed(3)} kg x ${item.pricePerUnit.toFixed(2)} €/kg`; } else { meta.textContent = `${item.quantity} Stk. x ${item.pricePerUnit.toFixed(2)} €/Stk.`; } details.appendChild(name); details.appendChild(meta); const actions = document.createElement("div"); actions.className = "item-price-actions"; const price = document.createElement("span"); price.className = "item-price"; price.textContent = `${item.total.toFixed(2).replace('.', ',')} €`; const delBtn = document.createElement("button"); delBtn.className = "delete-item-btn"; delBtn.onclick = () => deleteCartItem(item.timestamp); delBtn.innerHTML = ` `; actions.appendChild(price); actions.appendChild(delBtn); row.appendChild(details); row.appendChild(actions); cartList.appendChild(row); }); const tax = subtotal * 0.07; // 7% vat cartSubtotalEl.textContent = `${subtotal.toFixed(2).replace('.', ',')} €`; cartTotalEl.textContent = `${subtotal.toFixed(2).replace('.', ',')} €`; } // --- DEVELOPER DASHBOARD BACKEND CALLS --- // 1. Synthetic dataset trigger async function generateDataset() { try { const response = await fetch("/api/generate_dataset", { method: "POST" }); const data = await response.json(); alert(data.message); startTrainingPolling(); // will start checking generation status } catch (err) { console.error("Failed to generate dataset:", err); } } // 2. Refresh Albumentations augmented preview async function refreshAugmentedPreview() { const previewBox = document.getElementById("aug-preview-box"); previewBox.innerHTML = `
Generiere Vorschau...
`; try { const response = await fetch("/api/augmented_preview"); const data = await response.json(); if (data.status === "success") { previewBox.innerHTML = `Original vs. Augmentiert`; } else { previewBox.innerHTML = `
Fehler: ${data.message}
`; } } catch (err) { previewBox.innerHTML = `
Keine Trainingsbilder vorhanden.
Bitte zuerst "Datensatz generieren" klicken.
`; } } // 3. Trigger model training async function startTraining() { const epochs = parseInt(document.getElementById("param-epochs").value) || 15; const batchSize = parseInt(document.getElementById("param-batch").value) || 8; try { const response = await fetch("/api/train", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ epochs: epochs, batch_size: batchSize }) }); const data = await response.json(); alert(data.message); startTrainingPolling(); } catch (err) { console.error("Failed to start training:", err); } } // Cancel active model training async function cancelTraining() { const cancelBtn = document.getElementById("cancel-training-btn"); if (cancelBtn) cancelBtn.disabled = true; try { const response = await fetch("/api/cancel_training", { method: "POST" }); const data = await response.json(); alert(data.message); } catch (err) { console.error("Failed to cancel training:", err); alert("Fehler beim Abbrechen des Trainings."); } finally { if (cancelBtn) cancelBtn.disabled = false; } } // Poll training API function startTrainingPolling() { if (trainingPollInterval) return; const checkStatus = async () => { try { const response = await fetch("/api/train_status"); const data = await response.json(); updateTrainingStatusUI(data); // If training or data generation completed or failed, we can stop/keep checking depending on state if (data.status === "completed" || data.status === "failed" || data.status === "idle") { // Keep checking periodically but slower, or stop if we want to } } catch (err) { console.error("Status check failed:", err); } }; checkStatus(); trainingPollInterval = setInterval(checkStatus, 1500); } function stopTrainingPolling() { if (trainingPollInterval) { clearInterval(trainingPollInterval); trainingPollInterval = null; } } // Update training UI and plot graphs function updateTrainingStatusUI(data) { const trainingState = document.getElementById("training-state"); const trainingEpoch = document.getElementById("training-epoch"); const progressFill = document.getElementById("training-progress-fill"); const logMsg = document.getElementById("training-log-msg"); const statusBox = document.getElementById("training-status-box"); // Update status labels trainingState.textContent = getGermanState(data.status); trainingEpoch.textContent = data.epoch > 0 ? `Epoche: ${data.epoch}/${data.total_epochs}` : "Epoche: -/-"; progressFill.style.width = `${data.progress}%`; logMsg.textContent = data.message; // Update box glow styling statusBox.className = "training-status-box"; if (data.status === "training") { statusBox.classList.add("active-pulse"); } // Toggle start and cancel buttons based on status const startBtn = document.getElementById("start-training-btn"); const cancelBtn = document.getElementById("cancel-training-btn"); if (startBtn && cancelBtn) { if (data.status === "training") { startBtn.style.display = "none"; cancelBtn.style.display = "inline-block"; } else { startBtn.style.display = "inline-block"; cancelBtn.style.display = "none"; } } // Plot Chart if loss history is available if (data.train_loss && data.train_loss.length > 0) { updateChart(data.train_loss, data.val_loss || []); } } function getGermanState(status) { switch (status) { case "idle": return "Bereit"; case "generating_data": return "Generiere Daten..."; case "training": return "Training läuft..."; case "completed": return "Abgeschlossen"; case "failed": return "Fehlgeschlagen"; default: return status; } } // Plot losses in SVG function updateChart(trainLosses, valLosses) { const svgWidth = 440; // width bounds inside SVG 500 const svgHeight = 150; // height bounds inside SVG 200 if (trainLosses.length === 0) return; // Find maximum loss to scale Y-axis correctly const allLosses = [...trainLosses, ...valLosses]; const maxLoss = Math.max(2.0, ...allLosses); const getSvgCoords = (losses) => { return losses.map((loss, idx) => { const x = 40 + (idx / (losses.length - 1 || 1)) * svgWidth; const y = 170 - (loss / maxLoss) * svgHeight; return `${x.toFixed(1)},${y.toFixed(1)}`; }); }; const trainPoints = getSvgCoords(trainLosses); document.getElementById("train-loss-path").setAttribute("d", "M " + trainPoints.join(" L ")); if (valLosses.length > 0) { const valPoints = getSvgCoords(valLosses); document.getElementById("val-loss-path").setAttribute("d", "M " + valPoints.join(" L ")); } } // 4. Export model to ONNX async function exportModel() { const onnxLog = document.getElementById("onnx-log"); const exportBtn = document.getElementById("export-onnx-btn"); const latencyVal = document.getElementById("onnx-latency"); const latencyStatus = document.getElementById("onnx-latency-status"); onnxLog.textContent = "Starte ONNX Export & Benchmarking..."; exportBtn.disabled = true; try { const response = await fetch("/api/export", { method: "POST" }); const data = await response.json(); exportBtn.disabled = false; if (data.status === "success") { onnxLog.textContent = data.log || "Export erfolgreich!"; // Extract latency from benchmark logs using regex const match = data.log.match(/Average ONNX Runtime inference latency: ([\d\.]+) ms/); if (match && match[1]) { const latency = match[1]; latencyVal.textContent = `${latency} ms`; latencyStatus.textContent = parseFloat(latency) < 50.0 ? "✓ < 50 ms (Flüssige Live-Ansicht)" : "⚠️ > 50 ms (Optimiere Hardware)"; } else { latencyVal.textContent = "< 30 ms"; latencyStatus.textContent = "✓ Inferenz läuft flüssig"; } alert("Modell erfolgreich in ONNX konvertiert und geladen!"); } else { onnxLog.textContent = `Fehler: ${data.message}`; alert(`Export fehlgeschlagen: ${data.message}`); } } catch (err) { exportBtn.disabled = false; onnxLog.textContent = `Fehler bei der Server-Kommunikation.`; alert("Export-Anfrage fehlgeschlagen. Vergewissere dich, dass ein PyTorch Modell (.pt) existiert."); } } async function captureForTraining() { if (!isWebcamActive) { alert("Bitte aktivieren Sie zuerst die Live-Webcam, um ein echtes Foto aufzunehmen."); return; } const classSelect = document.getElementById("capture-class-select"); const selectedClass = classSelect.value; if (!selectedClass) return; const captureBtn = document.getElementById("capture-image-btn"); captureBtn.disabled = true; // Capture current frame from canvas (high quality) const base64Frame = canvasEl.toDataURL("image/jpeg", 0.95); try { const response = await fetch("/api/capture_training_image", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ image: base64Frame, class_name: selectedClass }) }); const data = await response.json(); captureBtn.disabled = false; if (data.status === "success") { // Update display count document.getElementById("capture-count-display").textContent = `Eigene Bilder: ${data.count}`; // Flash camera box as visual feedback const container = document.querySelector(".camera-card"); container.classList.add("scale-flash"); setTimeout(() => container.classList.remove("scale-flash"), 300); console.log(data.message); } else { alert(`Fehler bei Aufnahme: ${data.message}`); } } catch (err) { captureBtn.disabled = false; console.error("Capture request failed:", err); alert("Serverfehler bei Bild-Erfassung."); } } async function initCapturePanel() { const classSelect = document.getElementById("capture-class-select"); if (!classSelect) return; classSelect.innerHTML = ""; Object.keys(PRICING_DATABASE).forEach(key => { const opt = document.createElement("option"); opt.value = key; opt.textContent = PRICING_DATABASE[key].name; if (key === "kartoffel") { opt.selected = true; // default to kartoffel } classSelect.appendChild(opt); }); try { const response = await fetch("/api/capture_count"); const data = await response.json(); document.getElementById("capture-count-display").textContent = `Eigene Bilder: ${data.count}`; } catch (err) { console.error("Failed to load initial capture count:", err); } } // --- VIRTUAL BARCODE SCANNER LOGIC --- // Initial load of EAN database on startup async function initArticles() { try { const response = await fetch("/api/articles"); const data = await response.json(); articlesDatabase = data; // Sync names back to PRICING_DATABASE Object.keys(articlesDatabase).forEach(key => { if (!PRICING_DATABASE[key]) { PRICING_DATABASE[key] = { name: articlesDatabase[key].name, price: 1.99, unit: "Stk." }; } else { PRICING_DATABASE[key].name = articlesDatabase[key].name; } }); } catch (err) { console.error("Fehler beim Initialisieren der Artikeldatenbank:", err); } } // Render articles table rows in Article Management tab function renderArticlesTable() { const tbody = document.getElementById("articles-table-body"); if (!tbody) return; tbody.innerHTML = ""; Object.keys(articlesDatabase).forEach(key => { const article = articlesDatabase[key]; const tr = document.createElement("tr"); tr.style.borderBottom = "1px solid var(--border-glass)"; tr.innerHTML = ` ${key} `; tbody.appendChild(tr); }); } // Load articles table in Article Management tab async function loadArticlesDatabase() { try { const response = await fetch("/api/articles"); const data = await response.json(); articlesDatabase = data; // Sync to PRICING_DATABASE Object.keys(articlesDatabase).forEach(key => { if (!PRICING_DATABASE[key]) { PRICING_DATABASE[key] = { name: articlesDatabase[key].name, price: 1.99, unit: "Stk." }; } else { PRICING_DATABASE[key].name = articlesDatabase[key].name; } }); renderArticlesTable(); } catch (err) { console.error("Fehler beim Laden der Artikeldatenbank:", err); } } // Prompt to add a new article key and EAN code function addNewArticlePrompt() { const key = prompt("Geben Sie einen eindeutigen Systemschlüssel für den Artikel ein (z.B. 'mango', nur Kleinbuchstaben und Unterstriche):"); if (!key) return; // Validate key: lowercase, letters, numbers, underscores const keyRegex = /^[a-z0-9_]+$/; if (!keyRegex.test(key)) { alert("Ungültiger Schlüssel. Bitte nur Kleinbuchstaben (a-z), Zahlen (0-9) und Unterstriche (_) verwenden."); return; } if (PRICING_DATABASE[key] || articlesDatabase[key]) { alert("Dieser Artikel-Schlüssel existiert bereits."); return; } const name = prompt("Geben Sie den Anzeigenamen für den Artikel ein (z.B. 'Mango'):"); if (!name) return; const ean = prompt("Geben Sie den EAN-Barcode für die Tastaturemulation ein (z.B. '4001234000292'):"); if (!ean) return; // Add to local articlesDatabase and PRICING_DATABASE articlesDatabase[key] = { name: name, ean: ean }; PRICING_DATABASE[key] = { name: name, price: 1.99, unit: "Stk." }; // Reload the table and capture dropdown renderArticlesTable(); initCapturePanel(); } // Save articles database async function saveArticlesDatabase() { const tbody = document.getElementById("articles-table-body"); if (!tbody) return; const nameInputs = tbody.querySelectorAll(".article-name-input"); const eanInputs = tbody.querySelectorAll(".article-ean-input"); const updatedArticles = {}; nameInputs.forEach((input, index) => { const key = input.getAttribute("data-key"); const name = input.value.trim(); const ean = eanInputs[index].value.trim(); updatedArticles[key] = { name, ean }; }); try { const response = await fetch("/api/articles", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ articles: updatedArticles }) }); const data = await response.json(); if (data.status === "success") { articlesDatabase = updatedArticles; // Sync updated articles to PRICING_DATABASE Object.keys(updatedArticles).forEach(key => { if (!PRICING_DATABASE[key]) { PRICING_DATABASE[key] = { name: updatedArticles[key].name, price: 1.99, unit: "Stk." }; } else { PRICING_DATABASE[key].name = updatedArticles[key].name; } }); // Update capture dropdown initCapturePanel(); alert("Artikeldatenbank erfolgreich gespeichert!"); } else { alert("Fehler beim Speichern: " + data.message); } } catch (err) { console.error("Fehler beim Speichern der Artikeldatenbank:", err); alert("Serverfehler beim Speichern der Artikeldatenbank."); } } // Play high-pitched scanner beep function playScannerBeep() { try { const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const oscillator = audioCtx.createOscillator(); const gainNode = audioCtx.createGain(); oscillator.connect(gainNode); gainNode.connect(audioCtx.destination); oscillator.type = "sine"; oscillator.frequency.setValueAtTime(1000, audioCtx.currentTime); // 1kHz beep gainNode.gain.setValueAtTime(0, audioCtx.currentTime); gainNode.gain.linearRampToValueAtTime(0.2, audioCtx.currentTime + 0.01); gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.15); oscillator.start(audioCtx.currentTime); oscillator.stop(audioCtx.currentTime + 0.16); } catch (err) { console.error("Audio Context failed to play beep:", err); } } // Add log message to the terminal container function addScanLog(msg) { const logEl = document.getElementById("scanner-log"); if (!logEl) return; const timeStr = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); const logLine = `[${timeStr}] ${msg}`; if (logEl.textContent === "Warte auf Erkennung...") { logEl.textContent = logLine; } else { logEl.textContent += "\\n" + logLine; } logEl.scrollTop = logEl.scrollHeight; } // Send EAN code to backend scanner endpoint async function sendEanToPos(classKey) { const article = articlesDatabase[classKey] || { name: PRICING_DATABASE[classKey]?.name || classKey, ean: "" }; if (!article.ean) { addScanLog(`⚠️ Fehler: Kein EAN-Code für ${article.name} definiert.`); return; } addScanLog(`Scanne ${article.name} (EAN: ${article.ean})...`); try { const response = await fetch("/api/scan_ean", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ class_name: classKey }) }); const data = await response.json(); if (response.ok && data.status === "success") { playScannerBeep(); addScanLog(`✓ EAN ${article.ean} erfolgreich gesendet.`); // Visual flash feedback on scanner card const scannerCard = document.querySelector(".scanner-card"); if (scannerCard) { scannerCard.classList.add("scale-flash"); setTimeout(() => scannerCard.classList.remove("scale-flash"), 400); } } else { addScanLog(`❌ Fehler: ${data.message || "Tastaturemulation fehlgeschlagen"}`); } } catch (err) { console.error("Scan API error:", err); addScanLog(`❌ Serverfehler beim Senden des EAN-Codes.`); } } // Stability filter logic async function processStabilityAndScan(predictions) { const autoScanToggle = document.getElementById("auto-scan-toggle"); const autoScan = autoScanToggle ? autoScanToggle.checked : true; // Find the prediction with the highest confidence let topPrediction = null; if (predictions && predictions.length > 0) { predictions.forEach(p => { if (!topPrediction || p.confidence > topPrediction.confidence) { topPrediction = p; } }); } const scannerIndicator = document.getElementById("scanner-indicator"); const progressWrapper = document.getElementById("stability-progress-wrapper"); const progressFill = document.getElementById("stability-progress-fill"); const STABLE_CONF_THRESHOLD = 0.85; if (topPrediction && topPrediction.confidence >= STABLE_CONF_THRESHOLD) { const detectedClass = topPrediction.class; if (stableClass !== detectedClass) { stableClass = detectedClass; stableCount = 1; } else { stableCount++; } if (progressWrapper && progressFill) { progressWrapper.style.display = "block"; const percent = Math.min(100, (stableCount / STABILITY_THRESHOLD) * 100); progressFill.style.width = `${percent}%`; } if (scannerIndicator) { const articleName = PRICING_DATABASE[detectedClass]?.name || detectedClass; scannerIndicator.textContent = `Erkenne ${articleName}... (${stableCount}/${STABILITY_THRESHOLD})`; scannerIndicator.style.color = "var(--color-accent)"; } if (stableCount >= STABILITY_THRESHOLD) { if (lastScannedClass !== detectedClass && !scanLockActive) { if (autoScan) { await sendEanToPos(detectedClass); } else { addScanLog(`[Auto-Scan aus] ${PRICING_DATABASE[detectedClass]?.name || detectedClass} stabil erkannt.`); } lastScannedClass = detectedClass; scanLockActive = true; } if (progressFill) progressFill.style.width = "0%"; if (progressWrapper) progressWrapper.style.display = "none"; if (scannerIndicator) { scannerIndicator.textContent = `Gesperrt (Waage leeren)`; scannerIndicator.style.color = "var(--color-danger)"; } } } else { const CLEAR_THRESHOLD = 0.15; if (!topPrediction || topPrediction.confidence < CLEAR_THRESHOLD) { stableClass = null; stableCount = 0; scanLockActive = false; lastScannedClass = null; if (progressWrapper) progressWrapper.style.display = "none"; if (progressFill) progressFill.style.width = "0%"; if (scannerIndicator) { scannerIndicator.textContent = "Bereit (Warte auf Objekt)"; scannerIndicator.style.color = "var(--color-success)"; } } else { stableCount = Math.max(0, stableCount - 1); if (progressWrapper && progressFill) { const percent = (stableCount / STABILITY_THRESHOLD) * 100; progressFill.style.width = `${percent}%`; } if (scannerIndicator && stableCount > 0) { scannerIndicator.textContent = `Signal instabil...`; scannerIndicator.style.color = "var(--color-warning)"; } else if (scannerIndicator && !scanLockActive) { scannerIndicator.textContent = "Bereit (Signal instabil)"; scannerIndicator.style.color = "var(--color-success)"; } } } } // Manual scan trigger async function triggerManualScan() { if (stableClass) { addScanLog(`Manueller Scan ausgelöst für ${PRICING_DATABASE[stableClass]?.name || stableClass}...`); await sendEanToPos(stableClass); } else { addScanLog(`⚠️ Kein stabiles Objekt auf der Waage. Manueller Scan abgebrochen.`); alert("Bitte legen Sie ein Produkt auf die Waage, um es zu scannen."); } } // --- LEARN ARTICLE TAB (ANLERN-MODUS) LOGIC --- let learnCurrentBBox = null; // stores {xmin, ymin, xmax, ymax} function computeBBoxFromDiff(currData, bgData, width, height, threshold = 25) { let minX = width, minY = height, maxX = 0, maxY = 0; let changedCount = 0; for (let y = 0; y < height; y += 4) { // sample every 4th pixel for speed for (let x = 0; x < width; x += 4) { const idx = (y * width + x) * 4; const rDiff = Math.abs(currData[idx] - bgData[idx]); const gDiff = Math.abs(currData[idx+1] - bgData[idx+1]); const bDiff = Math.abs(currData[idx+2] - bgData[idx+2]); if (rDiff + gDiff + bDiff > threshold * 3) { if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; changedCount++; } } } // If too few pixels changed, it's probably noise or empty if (changedCount < 100) return null; // Add padding const padding = 15; minX = Math.max(0, minX - padding); minY = Math.max(0, minY - padding); maxX = Math.min(width, maxX + padding); maxY = Math.min(height, maxY + padding); return { xmin: minX, ymin: minY, xmax: maxX, ymax: maxY }; } function drawLearnFrameGreenBox() { if (!learnCanvasEl || !learnCtx || !currentFrameBase64) return; if (learnBackgroundFrame) { try { const width = learnCanvasEl.width; const height = learnCanvasEl.height; // Get current frame image data const currImgData = learnCtx.getImageData(0, 0, width, height); // Calculate bbox const bbox = computeBBoxFromDiff(currImgData.data, learnBackgroundFrame.data, width, height); learnCurrentBBox = bbox; if (bbox) { // Draw green bounding box outline learnCtx.lineWidth = 3; learnCtx.strokeStyle = "#10b981"; // Green learnCtx.strokeRect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin); // Draw label box background learnCtx.fillStyle = "rgba(16, 185, 129, 0.85)"; const labelText = "Ware erkannt (Auto-Crop)"; learnCtx.font = "bold 12px sans-serif"; const textWidth = learnCtx.measureText(labelText).width; learnCtx.fillRect(bbox.xmin - 1, bbox.ymin - 22, textWidth + 12, 22); // Label text learnCtx.fillStyle = "#ffffff"; learnCtx.fillText(labelText, bbox.xmin + 5, bbox.ymin - 6); } } catch (e) { console.error("Error drawing green box:", e); } } else { learnCurrentBBox = null; } } async function calibrateBackground() { if (!currentFrameBase64) { alert("Warte auf Kamerabild..."); return; } try { const response = await fetch("/api/calibrate_background", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ image: currentFrameBase64 }) }); const data = await response.json(); if (data.status === "success") { // Save background frame locally as ImageData for real-time diffing const width = learnCanvasEl.width; const height = learnCanvasEl.height; learnBackgroundFrame = learnCtx.getImageData(0, 0, width, height); // Update UI indicators const statusIndicator = document.getElementById("learn-calibration-indicator"); if (statusIndicator) { statusIndicator.textContent = "Kalibriert"; statusIndicator.style.color = "var(--color-success)"; statusIndicator.classList.add("live"); } const checkbox = document.getElementById("learn-bg-ok-indicator"); if (checkbox) { checkbox.checked = true; } alert(data.message); } else { alert("Kalibrierung fehlgeschlagen: " + data.message); } } catch (err) { console.error("Calibration error:", err); alert("Serverfehler bei Kalibrierung."); } } async function checkBackgroundCalibration() { try { const response = await fetch("/api/has_background"); const data = await response.json(); const statusIndicator = document.getElementById("learn-calibration-indicator"); const checkbox = document.getElementById("learn-bg-ok-indicator"); if (data.has_background) { if (statusIndicator) { statusIndicator.textContent = "Kalibriert"; statusIndicator.style.color = "var(--color-success)"; statusIndicator.classList.add("live"); } if (checkbox) { checkbox.checked = true; } // Load background image from backend and draw it on a hidden canvas to get ImageData const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => { const hiddenCanvas = document.createElement("canvas"); hiddenCanvas.width = learnCanvasEl.width; hiddenCanvas.height = learnCanvasEl.height; const hiddenCtx = hiddenCanvas.getContext("2d"); hiddenCtx.drawImage(img, 0, 0, hiddenCanvas.width, hiddenCanvas.height); learnBackgroundFrame = hiddenCtx.getImageData(0, 0, hiddenCanvas.width, hiddenCanvas.height); }; img.src = "/api/background_image?t=" + Date.now(); // Cache busting } else { learnBackgroundFrame = null; if (statusIndicator) { statusIndicator.textContent = "Nicht kalibriert"; statusIndicator.style.color = "var(--text-muted)"; statusIndicator.classList.remove("live"); } if (checkbox) { checkbox.checked = false; } } } catch (err) { console.error("Failed to check background calibration:", err); } } async function resetBackgroundCalibration() { if (!confirm("Hintergrundkalibrierung wirklich zurücksetzen?")) return; try { const response = await fetch("/api/reset_background", { method: "POST" }); const data = await response.json(); if (data.status === "success") { learnBackgroundFrame = null; const statusIndicator = document.getElementById("learn-calibration-indicator"); if (statusIndicator) { statusIndicator.textContent = "Nicht kalibriert"; statusIndicator.style.color = "var(--text-muted)"; statusIndicator.classList.remove("live"); } const checkbox = document.getElementById("learn-bg-ok-indicator"); if (checkbox) { checkbox.checked = false; } alert(data.message); } } catch (err) { console.error("Reset calibration error:", err); } } function loadLearnArticles() { const listContainer = document.getElementById("learn-articles-list"); if (!listContainer) return; listContainer.innerHTML = ""; // Check if database loaded, if not, wait if (Object.keys(articlesDatabase).length === 0) { setTimeout(loadLearnArticles, 200); return; } Object.keys(articlesDatabase).forEach(key => { const art = articlesDatabase[key]; const item = document.createElement("div"); item.className = "learn-article-item"; item.id = `learn-item-${key}`; if (learnSelectedClass === key) { item.classList.add("selected"); } item.onclick = () => selectLearnArticle(key); item.innerHTML = ` ${art.name} EAN: ${art.ean || 'Keine'} `; listContainer.appendChild(item); }); } function filterLearnArticles() { const query = document.getElementById("learn-search-input").value.toLowerCase().trim(); const items = document.querySelectorAll(".learn-article-item"); items.forEach(item => { const text = item.textContent.toLowerCase(); if (text.includes(query)) { item.style.display = "flex"; } else { item.style.display = "none"; } }); } function selectLearnArticle(classKey) { learnSelectedClass = classKey; document.querySelectorAll(".learn-article-item").forEach(el => el.classList.remove("selected")); const selectedEl = document.getElementById(`learn-item-${classKey}`); if (selectedEl) { selectedEl.classList.add("selected"); } const display = document.getElementById("learn-selected-article-display"); if (display) { const name = articlesDatabase[classKey]?.name || classKey; display.textContent = name; } } function updateLearnBadge() { fetch("/api/capture_count") .then(r => r.json()) .then(data => { const badge = document.getElementById("learn-session-count-badge"); if (badge) badge.textContent = `Eigene Bilder Gesamt: ${data.count}`; const displayCheckout = document.getElementById("capture-count-display"); if (displayCheckout) displayCheckout.textContent = `Eigene Bilder: ${data.count}`; }) .catch(err => console.error(err)); } async function startSerialCapture() { if (!learnSelectedClass) { alert("Bitte wählen Sie zuerst einen Artikel aus der Liste aus."); return; } if (!learnBackgroundFrame) { alert("Bitte führen Sie zuerst die Hintergrund-Kalibrierung durch (Hintergrund speichern)."); return; } if (isSerialCapturing) return; isSerialCapturing = true; // Clear session gallery UI and list const galleryGrid = document.getElementById("learn-gallery-grid"); galleryGrid.innerHTML = ""; learnSessionImages = []; const startBtn = document.getElementById("learn-start-capture-btn"); startBtn.disabled = true; startBtn.innerHTML = `Aufnahme läuft...`; const progressText = document.getElementById("learn-progress-text"); const progressFill = document.getElementById("learn-progress-fill"); let count = 0; const total = 10; const captureNext = async () => { // Sound feedback playScannerBeep(); // Capture frame const frameData = learnCanvasEl.toDataURL("image/jpeg", 0.95); const currentBbox = learnCurrentBBox ? { ...learnCurrentBBox } : null; try { const response = await fetch("/api/capture_training_image", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ image: frameData, class_name: learnSelectedClass, bbox: currentBbox }) }); const data = await response.json(); if (data.status === "success" && data.filename) { learnSessionImages.push(data.filename); addPhotoToGallery(data.filename); } } catch (err) { console.error("Serial capture step failed:", err); } count++; if (progressText) progressText.textContent = `${count} / ${total} Bilder`; if (progressFill) progressFill.style.width = `${(count / total) * 100}%`; if (count >= total) { clearInterval(captureInterval); isSerialCapturing = false; startBtn.disabled = false; startBtn.innerHTML = ` START (SERIENAUFNAHME) `; updateLearnBadge(); alert("Serienaufnahme abgeschlossen! Sie können fehlerhafte Bilder unten in der Galerie löschen."); } }; // Start interval await captureNext(); // First capture immediately const captureInterval = setInterval(captureNext, 1000); } function addPhotoToGallery(filename) { const galleryGrid = document.getElementById("learn-gallery-grid"); const emptyState = document.getElementById("learn-gallery-empty"); if (emptyState) emptyState.remove(); const wrapper = document.createElement("div"); wrapper.className = "gallery-item-wrapper"; wrapper.id = `gallery-item-${filename.replace(".", "_")}`; wrapper.innerHTML = ` ${filename} `; galleryGrid.appendChild(wrapper); } async function deleteSessionImage(filename) { if (!confirm("Dieses Bild aus der Session löschen?")) return; try { const response = await fetch("/api/delete_captured_image", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filename: filename }) }); const data = await response.json(); if (data.status === "success") { // Remove from UI const wrapper = document.getElementById(`gallery-item-${filename.replace(".", "_")}`); if (wrapper) wrapper.remove(); // Remove from list learnSessionImages = learnSessionImages.filter(f => f !== filename); // If empty, show empty state const galleryGrid = document.getElementById("learn-gallery-grid"); if (galleryGrid && galleryGrid.children.length === 0) { galleryGrid.innerHTML = ` `; } updateLearnBadge(); console.log(data.message); } else { alert("Löschen fehlgeschlagen: " + data.message); } } catch (err) { console.error("Delete image error:", err); alert("Serverfehler beim Löschen des Bildes."); } } // --- RESET ALL CAPTURED TRAINING DATA --- async function resetAllCapturedData() { const confirmed = confirm( "⚠️ Alle selbst erfassten Trainingsbilder löschen?\n\n" + "Dies entfernt alle user_capture_* Bilder und Labels sowie die Hintergrundkalibrierung.\n" + "Das vortrainierte Basis-Modell bleibt erhalten.\n\n" + "Dieser Vorgang kann NICHT rückgängig gemacht werden!" ); if (!confirmed) return; const resetBtnLearn = document.getElementById("reset-all-captures-btn"); const resetBtnDash = document.getElementById("dashboard-reset-btn"); if (resetBtnLearn) { resetBtnLearn.disabled = true; resetBtnLearn.textContent = "Wird gelöscht..."; } if (resetBtnDash) { resetBtnDash.disabled = true; resetBtnDash.textContent = "Wird gelöscht..."; } try { const response = await fetch("/api/reset_dataset", { method: "POST" }); const data = await response.json(); if (data.status === "success") { // 1. Clear gallery UI const galleryGrid = document.getElementById("learn-gallery-grid"); if (galleryGrid) { galleryGrid.innerHTML = ` `; } learnSessionImages = []; // 2. Reset background calibration state learnBackgroundFrame = null; learnCurrentBBox = null; const statusIndicator = document.getElementById("learn-calibration-indicator"); if (statusIndicator) { statusIndicator.textContent = "Nicht kalibriert"; statusIndicator.style.color = "var(--text-muted)"; statusIndicator.classList.remove("live"); } const checkbox = document.getElementById("learn-bg-ok-indicator"); if (checkbox) checkbox.checked = false; // 3. Update count badges const badge = document.getElementById("learn-session-count-badge"); if (badge) badge.textContent = "Eigene Bilder Gesamt: 0"; const countDisplay = document.getElementById("capture-count-display"); if (countDisplay) countDisplay.textContent = "Eigene Bilder: 0"; alert("✅ " + data.message); } else { alert("Fehler beim Zurücksetzen: " + (data.detail || data.message)); } } catch (err) { console.error("Reset error:", err); alert("Serverfehler beim Zurücksetzen der Trainingsdaten."); } finally { if (resetBtnLearn) { resetBtnLearn.disabled = false; resetBtnLearn.innerHTML = ` Alle Trainingsbilder löschen `; } if (resetBtnDash) { resetBtnDash.disabled = false; resetBtnDash.innerHTML = ` Eigene Bilder & Kalibrierung zurücksetzen `; } } } // --- RESET TRAINED AI MODEL --- async function resetAIModel() { const confirmed = confirm( "⚠️ Trainiertes KI-Modell wirklich zurücksetzen?\n\n" + "Dies löscht das trainierte PyTorch- und ONNX-Modell vom Server.\n" + "Das System läuft danach im Simulations-Modus, bis Sie ein neues Modell trainieren.\n\n" + "Dieser Vorgang kann NICHT rückgängig gemacht werden!" ); if (!confirmed) return; const resetBtn = document.getElementById("dashboard-reset-model-btn"); if (resetBtn) { resetBtn.disabled = true; resetBtn.textContent = "Zurücksetzen..."; } try { const response = await fetch("/api/reset_model", { method: "POST" }); const data = await response.json(); if (response.ok && data.status === "success") { alert("✅ " + data.message); // Refresh dashboard training status if polling is active if (activeTab === 'dashboard') { const statusResponse = await fetch("/api/train_status"); const statusData = await statusResponse.json(); updateTrainingStatusUI(statusData); } } else { alert("Fehler beim Zurücksetzen des Modells: " + (data.detail || data.message)); } } catch (err) { console.error("Reset model error:", err); alert("Serverfehler beim Zurücksetzen des KI-Modells."); } finally { if (resetBtn) { resetBtn.disabled = false; resetBtn.innerHTML = ` Trainiertes KI-Modell zurücksetzen `; } } } // --- INTERACTIVE EVENT LISTENERS --- toggleCameraBtn.addEventListener("click", () => { if (isWebcamActive) { stopWebcam(); } else { startWebcam(); } }); triggerScaleBtn.addEventListener("click", () => { // Flash scale overlay on manual trigger const overlay = document.querySelector(".camera-card"); overlay.classList.add("scale-flash"); setTimeout(() => overlay.classList.remove("scale-flash"), 400); // Explicitly run detection runDetection(); }); autoDetectToggle.addEventListener("change", (e) => { autoDetectEnabled = e.target.checked; }); // App Startup Initialization window.addEventListener("DOMContentLoaded", () => { // Start in simulated video mode by default startSimulatedStream(); // Fetch articles from database initArticles(); // Read parameters from local values updateCartUI(); // Initialize capture classes initCapturePanel(); // Check background calibration on startup checkBackgroundCalibration(); updateLearnBadge(); });