document.addEventListener('DOMContentLoaded', () => { // DOM Elements const uploadForm = document.getElementById('upload-form'); const promptInput = document.getElementById('prompt-input'); const beforeInput = document.getElementById('video-before'); const afterInput = document.getElementById('video-after'); const beforeZone = document.getElementById('upload-before-zone'); const afterZone = document.getElementById('upload-after-zone'); const beforeFileName = document.getElementById('before-file-name'); const afterFileName = document.getElementById('after-file-name'); const speedModeSelect = document.getElementById('speed-mode-select'); const fpsSelect = document.getElementById('fps-select'); const geminiKeyInput = document.getElementById('gemini-key-input'); const uploadBtn = document.getElementById('upload-btn'); const startBtn = document.getElementById('start-btn'); const resetBtn = document.getElementById('reset-btn'); const processingView = document.getElementById('processing-view'); const progressStatus = document.getElementById('progress-status'); const progressFill = document.getElementById('progress-fill'); const progressPct = document.getElementById('progress-pct'); const backendStatusText = document.getElementById('backend-status-text'); const resultsPlaceholder = document.getElementById('results-placeholder'); const resultsView = document.getElementById('results-view'); const outVideoBefore = document.getElementById('out-video-before'); const outVideoAfter = document.getElementById('out-video-after'); const assetCardsContainer = document.getElementById('asset-cards-container'); let pollInterval = null; // --- Load saved Gemini API Key --- const savedKey = localStorage.getItem('gemini_api_key'); if (savedKey) { geminiKeyInput.value = savedKey; } // --- File Drag & Drop Listeners --- setupDragAndDrop(beforeZone, beforeInput, beforeFileName, "Before Renting Video Selected"); setupDragAndDrop(afterZone, afterInput, afterFileName, "After Renting Video Selected"); function setupDragAndDrop(zone, input, display, successMsg) { zone.addEventListener('click', () => input.click()); input.addEventListener('change', (e) => { if (input.files.length > 0) { display.textContent = `${input.files[0].name} (${(input.files[0].size / 1e6).toFixed(1)} MB)`; zone.style.borderColor = 'var(--success)'; } }); zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.classList.add('drag-over'); }); zone.addEventListener('dragleave', () => { zone.classList.remove('drag-over'); }); zone.addEventListener('drop', (e) => { e.preventDefault(); zone.classList.remove('drag-over'); if (e.dataTransfer.files.length > 0) { input.files = e.dataTransfer.files; display.textContent = `${input.files[0].name} (${(input.files[0].size / 1e6).toFixed(1)} MB)`; zone.style.borderColor = 'var(--success)'; } }); } // --- Upload Videos --- uploadForm.addEventListener('submit', async (e) => { e.preventDefault(); if (beforeInput.files.length === 0 || afterInput.files.length === 0) { alert('Please select both "Before" and "After" renting videos first.'); return; } const prompt = promptInput.value.trim(); if (!prompt) { alert('Please define target inspection items prompt.'); return; } const formData = new FormData(); formData.append('video_before', beforeInput.files[0]); formData.append('video_after', afterInput.files[0]); formData.append('prompt', prompt); try { uploadBtn.disabled = true; uploadBtn.innerHTML = ' Uploading...'; backendStatusText.textContent = "Uploading videos..."; const response = await fetch('/upload', { method: 'POST', body: formData }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Upload failed.'); } uploadBtn.innerHTML = ' Uploaded!'; uploadBtn.style.background = 'var(--success)'; // Enable Start button startBtn.removeAttribute('disabled'); startBtn.classList.remove('btn-disabled'); backendStatusText.textContent = "Ready to analyze"; alert('Videos uploaded successfully. You can now start the evaluation.'); } catch (error) { console.error(error); alert(`Error: ${error.message}`); uploadBtn.disabled = false; uploadBtn.innerHTML = ' Upload Videos'; backendStatusText.textContent = "Upload failed"; } }); // --- Start Tracking / Evaluation --- startBtn.addEventListener('click', async () => { const prompt = promptInput.value.trim(); const geminiKey = geminiKeyInput.value.trim(); const speedMode = speedModeSelect.value; const fps = fpsSelect.value; // Securely cache Gemini API key in browser if (geminiKey) { localStorage.setItem('gemini_api_key', geminiKey); } else { localStorage.removeItem('gemini_api_key'); } try { startBtn.disabled = true; startBtn.classList.add('btn-disabled'); processingView.classList.remove('hidden'); backendStatusText.textContent = "Running evaluation..."; const response = await fetch('/process', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: prompt, gemini_api_key: geminiKey, speed_mode: speedMode, fps: parseFloat(fps) }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Failed to start process.'); } // Start polling status startStatusPolling(); } catch (error) { console.error(error); alert(`Error: ${error.message}`); processingView.classList.add('hidden'); startBtn.disabled = false; startBtn.classList.remove('btn-disabled'); backendStatusText.textContent = "Error occurred"; } }); // --- Reset --- resetBtn.addEventListener('click', async () => { if (confirm('Are you sure you want to reset and clear all data?')) { try { await fetch('/reset', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ force: true }) }); // Clear intervals clearInterval(pollInterval); pollInterval = null; // Reset inputs and displays uploadForm.reset(); beforeInput.value = ''; afterInput.value = ''; beforeZone.style.borderColor = 'var(--border-color)'; afterZone.style.borderColor = 'var(--border-color)'; beforeFileName.textContent = 'Drag & drop or click to upload (.mp4)'; afterFileName.textContent = 'Drag & drop or click to upload (.mp4)'; // Retain Gemini Key const savedKey = localStorage.getItem('gemini_api_key'); if (savedKey) { geminiKeyInput.value = savedKey; } uploadBtn.disabled = false; uploadBtn.innerHTML = ' Upload Videos'; uploadBtn.style.background = 'var(--primary)'; startBtn.disabled = true; startBtn.classList.add('btn-disabled'); processingView.classList.add('hidden'); resultsView.classList.add('hidden'); resultsPlaceholder.classList.remove('hidden'); outVideoBefore.src = ''; outVideoAfter.src = ''; assetCardsContainer.innerHTML = ''; backendStatusText.textContent = "System Ready"; } catch (error) { console.error(error); alert('Reset failed.'); } } }); // --- Status Polling --- function startStatusPolling() { if (pollInterval) clearInterval(pollInterval); pollInterval = setInterval(async () => { try { const response = await fetch('/status'); const data = await response.json(); if (data.error) { clearInterval(pollInterval); alert(`Evaluation Error: ${data.error}`); processingView.classList.add('hidden'); startBtn.disabled = false; startBtn.classList.remove('btn-disabled'); backendStatusText.textContent = "Analysis Failed"; return; } // Update progress overlay progressStatus.textContent = data.status; progressFill.style.width = `${data.percent}%`; progressPct.textContent = `${data.percent}%`; if (!data.running && data.percent === 100 && data.result) { clearInterval(pollInterval); pollInterval = null; displayResults(data.result); } } catch (error) { console.error('Polling error:', error); } }, 1500); } // --- Display Results --- function displayResults(result) { // Hide processing and placeholder processingView.classList.add('hidden'); resultsPlaceholder.classList.add('hidden'); resultsView.classList.remove('hidden'); backendStatusText.textContent = "Evaluation Complete"; // Render Summary Banner const summaryBanner = document.getElementById('summary-banner'); if (summaryBanner && result.summary) { const s = result.summary; // Format elapsed time (e.g. "45.2s" or "1m 15s") let timeStr = ""; if (s.elapsed_time < 60) { timeStr = `${s.elapsed_time}s`; } else { const mins = Math.floor(s.elapsed_time / 60); const secs = Math.round(s.elapsed_time % 60); timeStr = `${mins}m ${secs}s`; } // Compensation judgment logic: if any item is Missing or Damaged, tenant must pay compensation const compensationRequired = (s.statuses.Missing || 0) > 0 || (s.statuses["Potential Damage / Altered"] || 0) > 0; const verdictTitle = compensationRequired ? "Tenant Must Pay Compensation" : "No Compensation Required"; const verdictColor = compensationRequired ? "var(--danger)" : "var(--success)"; const verdictIcon = compensationRequired ? "fa-solid fa-circle-exclamation" : "fa-solid fa-circle-check"; const verdictDesc = compensationRequired ? "Damage or missing assets detected. Tenant is liable for compensation." : "All tracked assets are intact. No compensation required."; // Create unique list of all discovered item labels const uniqueLabels = Array.from(new Set([...s.before_items, ...s.after_items])); const labelsHtml = uniqueLabels.length > 0 ? uniqueLabels.map(lbl => `${lbl}`).join('') : 'None'; summaryBanner.innerHTML = `
${verdictTitle} AI Final Verdict ${verdictDesc}
${timeStr} Time Taken
${s.total_before} Assets (Before)
${s.total_after} Assets (After)
${s.statuses.Missing || 0} Missing Items
${s.statuses["Potential Damage / Altered"] || 0} Damaged Items
${(s.statuses.Intact || 0) + (s.statuses["Fair / Minor Change"] || 0)} Intact / Fair
Discovered Items: ${labelsHtml}
`; } // Set video sources (add timestamp parameter to bypass browser cache) const t = new Date().getTime(); outVideoBefore.src = `${result.annotated_before}?t=${t}`; outVideoAfter.src = `${result.annotated_after}?t=${t}`; outVideoBefore.load(); outVideoAfter.load(); // Render Asset Cards assetCardsContainer.innerHTML = ''; result.report.forEach(item => { const card = document.createElement('div'); card.className = 'asset-card'; // Top Row of card: Info, Crops, and Status const mainDiv = document.createElement('div'); mainDiv.className = 'asset-card-main'; // 1. Info section const infoDiv = document.createElement('div'); infoDiv.className = 'asset-info'; const labelH3 = document.createElement('h3'); labelH3.className = 'asset-label'; labelH3.textContent = item.label; const scoreSpan = document.createElement('span'); scoreSpan.className = 'asset-score'; scoreSpan.textContent = item.before_crop && item.after_crop ? `Similarity: ${item.similarity}%` : ''; infoDiv.appendChild(labelH3); infoDiv.appendChild(scoreSpan); mainDiv.appendChild(infoDiv); // 2. Before Crop const beforeCropDiv = createCropView(item.before_crop, "Before"); mainDiv.appendChild(beforeCropDiv); // 3. After Crop const afterCropDiv = createCropView(item.after_crop, "After"); mainDiv.appendChild(afterCropDiv); // 4. Status Badge const badgeDiv = document.createElement('div'); const badge = document.createElement('span'); badge.className = `status-badge ${getBadgeClass(item.status)}`; badge.textContent = item.status; badgeDiv.appendChild(badge); mainDiv.appendChild(badgeDiv); card.appendChild(mainDiv); // Bottom Row of card: Explanation (AI-generated or fallback) if (item.explanation) { const explanationDiv = document.createElement('div'); explanationDiv.className = 'asset-explanation'; const icon = document.createElement('i'); // Use robot icon if Gemini was used, sparkles icon if local CV fallback was used icon.className = item.ai_evaluated ? 'fa-solid fa-robot explanation-icon' : 'fa-solid fa-wand-magic-sparkles explanation-icon'; const textSpan = document.createElement('span'); textSpan.textContent = item.explanation; explanationDiv.appendChild(icon); explanationDiv.appendChild(textSpan); card.appendChild(explanationDiv); } assetCardsContainer.appendChild(card); }); } function createCropView(src, label) { const container = document.createElement('div'); container.className = 'crop-img-container'; const title = document.createElement('span'); title.textContent = label; container.appendChild(title); const frame = document.createElement('div'); frame.className = 'crop-frame'; if (src) { const img = document.createElement('img'); // Add cache-busting timestamp img.src = `${src}?t=${new Date().getTime()}`; img.alt = label; frame.appendChild(img); } else { const icon = document.createElement('i'); icon.className = 'fa-solid fa-circle-question crop-missing-icon'; frame.appendChild(icon); } container.appendChild(frame); return container; } function getBadgeClass(status) { switch (status) { case 'Intact': return 'badge-intact'; case 'Fair / Minor Change': return 'badge-fair'; case 'Potential Damage / Altered': return 'badge-damaged'; case 'Missing': return 'badge-missing'; case 'New Item': return 'badge-new'; default: return ''; } } });