// 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 = `
`;
} 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 = `
`;
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 = `
Noch keine Aufnahmen in dieser Session
Wähle ein Produkt und starte die Serienaufnahme
`;
}
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 = `
Noch keine Aufnahmen in dieser Session
Wähle ein Produkt und starte die Serienaufnahme
`;
}
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();
});