// State const state = { token: localStorage.getItem('homebox_token') || '', locations: [], labels: [], selectedLabels: [], // [{id, name, isNew}] capturedImage: null, stream: null, }; // DOM Elements const elements = { // Screens loginScreen: document.getElementById('login-screen'), mainScreen: document.getElementById('main-screen'), // Login loginForm: document.getElementById('login-form'), username: document.getElementById('username'), password: document.getElementById('password'), loginError: document.getElementById('login-error'), // Camera cameraPreview: document.getElementById('camera-preview'), photoCanvas: document.getElementById('photo-canvas'), photoPreview: document.getElementById('photo-preview'), captureBtn: document.getElementById('capture-btn'), retakeBtn: document.getElementById('retake-btn'), // Status statusSection: document.getElementById('status-section'), statusText: document.getElementById('status-text'), // Form formSection: document.getElementById('form-section'), itemName: document.getElementById('item-name'), itemDescription: document.getElementById('item-description'), itemLocation: document.getElementById('item-location'), itemQuantity: document.getElementById('item-quantity'), selectedTags: document.getElementById('selected-tags'), tagInput: document.getElementById('tag-input'), tagSuggestions: document.getElementById('tag-suggestions'), // More details itemManufacturer: document.getElementById('item-manufacturer'), itemModel: document.getElementById('item-model'), itemSerial: document.getElementById('item-serial'), itemPrice: document.getElementById('item-price'), itemPurchaseFrom: document.getElementById('item-purchase-from'), itemNotes: document.getElementById('item-notes'), itemInsured: document.getElementById('item-insured'), saveBtn: document.getElementById('save-btn'), // Success/Error successSection: document.getElementById('success-section'), newItemBtn: document.getElementById('new-item-btn'), errorSection: document.getElementById('error-section'), errorText: document.getElementById('error-text'), retryBtn: document.getElementById('retry-btn'), }; // API calls async function api(endpoint, options = {}) { const headers = { 'Content-Type': 'application/json', ...(state.token && { Authorization: `Bearer ${state.token}` }), ...options.headers, }; const response = await fetch(`/api${endpoint}`, { ...options, headers, }); if (!response.ok) { const error = await response.json().catch(() => ({ detail: 'Request failed' })); throw new Error(error.detail || `HTTP ${response.status}`); } return response.json(); } // Login async function login(username, password) { const data = await api('/login', { method: 'POST', body: JSON.stringify({ username, password }), }); state.token = data.token; localStorage.setItem('homebox_token', data.token); return data; } // Fetch locations and labels async function fetchMetadata() { const [locations, labels] = await Promise.all([ api('/locations'), api('/labels'), ]); state.locations = locations; state.labels = labels; populateLocationDropdown(); } function populateLocationDropdown() { elements.itemLocation.innerHTML = ''; state.locations.forEach(loc => { const option = document.createElement('option'); option.value = loc.id; option.textContent = loc.name; elements.itemLocation.appendChild(option); }); } // Camera functions async function startCamera() { try { state.stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 }, }, audio: false, }); elements.cameraPreview.srcObject = state.stream; } catch (err) { showError('Camera access denied. Please enable camera permissions.'); } } function stopCamera() { if (state.stream) { state.stream.getTracks().forEach(track => track.stop()); state.stream = null; } } function capturePhoto() { const video = elements.cameraPreview; const canvas = elements.photoCanvas; canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0); state.capturedImage = canvas.toDataURL('image/jpeg', 0.85); elements.photoPreview.src = state.capturedImage; // Switch to preview mode elements.cameraPreview.classList.add('hidden'); elements.photoPreview.classList.remove('hidden'); elements.captureBtn.classList.add('hidden'); elements.retakeBtn.classList.remove('hidden'); stopCamera(); analyzeImage(); } function retakePhoto() { state.capturedImage = null; elements.photoPreview.classList.add('hidden'); elements.cameraPreview.classList.remove('hidden'); elements.retakeBtn.classList.add('hidden'); elements.captureBtn.classList.remove('hidden'); elements.formSection.classList.add('hidden'); elements.statusSection.classList.add('hidden'); startCamera(); } // AI Analysis async function analyzeImage() { showStatus('Analyzing image...'); try { // Send image with available locations and labels for AI to choose from const result = await api('/analyze', { method: 'POST', body: JSON.stringify({ image: state.capturedImage, locations: state.locations.map(l => ({ id: l.id, name: l.name })), labels: state.labels.map(l => ({ id: l.id, name: l.name })), }), }); // Populate form elements.itemName.value = result.name; elements.itemDescription.value = result.description; // Set location (AI returns exact name from our list) const matchedLocation = state.locations.find( loc => loc.name.toLowerCase() === result.suggested_location.toLowerCase() ); if (matchedLocation) { elements.itemLocation.value = matchedLocation.id; } else if (state.locations.length > 0) { elements.itemLocation.value = state.locations[0].id; } // Add labels - existing ones from Homebox state.selectedLabels = []; (result.existing_labels || []).forEach(labelName => { const label = state.labels.find(l => l.name.toLowerCase() === labelName.toLowerCase()); if (label) { addLabel({ id: label.id, name: label.name, isNew: false }); } }); // Add new label suggestions from AI (result.new_labels || []).forEach(labelName => { addLabel({ id: null, name: labelName, isNew: true }); }); hideStatus(); elements.formSection.classList.remove('hidden'); } catch (err) { hideStatus(); showError(`Analysis failed: ${err.message}`); } } // Labels function addLabel(label) { const name = label.name.trim().toLowerCase(); if (!name || state.selectedLabels.some(l => l.name.toLowerCase() === name)) return; state.selectedLabels.push({ id: label.id || null, name: label.name, isNew: label.isNew || !label.id, }); renderLabels(); } function removeLabel(labelName) { state.selectedLabels = state.selectedLabels.filter(l => l.name.toLowerCase() !== labelName.toLowerCase()); renderLabels(); } function renderLabels() { elements.selectedTags.innerHTML = state.selectedLabels.map(label => ` ${label.name}${label.isNew ? ' (new)' : ''} × `).join(''); } function showTagSuggestions(query) { if (!query) { elements.tagSuggestions.classList.add('hidden'); return; } const selectedNames = state.selectedLabels.map(l => l.name.toLowerCase()); const matches = state.labels.filter(label => label.name.toLowerCase().includes(query.toLowerCase()) && !selectedNames.includes(label.name.toLowerCase()) ).slice(0, 5); if (matches.length === 0) { elements.tagSuggestions.classList.add('hidden'); return; } elements.tagSuggestions.innerHTML = matches.map(label => `