RehamAAhmed commited on
Commit
43d5325
·
verified ·
1 Parent(s): 2dc49d3

Upload 5 files

Browse files
Files changed (6) hide show
  1. .gitattributes +2 -0
  2. after.mp4 +3 -0
  3. before.mp4 +3 -0
  4. index.html +182 -0
  5. main.js +458 -0
  6. styles.css +843 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ after.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ before.mp4 filter=lfs diff=lfs merge=lfs -text
after.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4b9ff8e1a29973187d89f62bd587ca47b667a077155859120fdb308cdb063e04
3
+ size 3901769
before.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4b9ff8e1a29973187d89f62bd587ca47b667a077155859120fdb308cdb063e04
3
+ size 3901769
index.html ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Apartment Inspector - SAM 2 & Grounding DINO Dashboard</title>
7
+ <!-- Google Fonts -->
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
11
+ <!-- FontAwesome icons -->
12
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
13
+ <!-- Vanilla CSS stylesheet -->
14
+ <link rel="stylesheet" href="/static/css/styles.css">
15
+ </head>
16
+ <body>
17
+ <div class="glass-bg"></div>
18
+
19
+ <div class="app-container">
20
+ <!-- Header -->
21
+ <header class="app-header">
22
+ <div class="logo">
23
+ <i class="fa-solid fa-house-laptop logo-icon"></i>
24
+ <div class="logo-text">
25
+ <h1>Apartment Inspector</h1>
26
+ <span>Smart Asset Damage & Missing Item Evaluator</span>
27
+ </div>
28
+ </div>
29
+ <div class="status-indicator">
30
+ <span class="pulse-dot"></span>
31
+ <span id="backend-status-text">System Ready</span>
32
+ </div>
33
+ </header>
34
+
35
+ <!-- Main Layout Grid -->
36
+ <main class="app-grid">
37
+ <!-- Left Side: Config & Uploads -->
38
+ <section class="card config-card">
39
+ <h2 class="section-title"><i class="fa-solid fa-sliders"></i> Configuration & Upload</h2>
40
+
41
+ <form id="upload-form" enctype="multipart/form-data">
42
+ <!-- Target Inspection Items Prompt -->
43
+ <div class="input-group">
44
+ <label for="prompt-input">
45
+ Target Inspection Items
46
+ <span class="tooltip" data-tooltip="Define what objects to track and compare. Separate by commas.">
47
+ <i class="fa-solid fa-circle-info"></i>
48
+ </span>
49
+ </label>
50
+ <textarea id="prompt-input" name="prompt" rows="3" placeholder="sofa, tv, refrigerator, door, window, bed, microwave, sink">door, sofa, cabinet, refrigerator, tv, table, microwave, chair, toilet, bed, window</textarea>
51
+ </div>
52
+
53
+ <!-- Speed & Resolution Settings -->
54
+ <div class="settings-row">
55
+ <div class="input-group half">
56
+ <label for="speed-mode-select">Speed Mode</label>
57
+ <select id="speed-mode-select" class="styled-select">
58
+ <option value="super-fast">Super Fast (360p)</option>
59
+ <option value="fast" selected>Fast (480p) - Recommended</option>
60
+ <option value="detailed">Detailed (720p)</option>
61
+ </select>
62
+ </div>
63
+ <div class="input-group half">
64
+ <label for="fps-select">Frame Rate (FPS)</label>
65
+ <select id="fps-select" class="styled-select">
66
+ <option value="0.25">0.25 FPS (Ultra Fast)</option>
67
+ <option value="0.5">0.5 FPS (Super Fast)</option>
68
+ <option value="1.0">1.0 FPS (Fast)</option>
69
+ <option value="2.0" selected>2.0 FPS (Normal)</option>
70
+ <option value="3.0">3.0 FPS (Detailed)</option>
71
+ </select>
72
+ </div>
73
+ </div>
74
+
75
+ <!-- Gemini AI Key -->
76
+ <div class="input-group">
77
+ <label for="gemini-key-input">
78
+ Gemini API Key (Optional)
79
+ <span class="tooltip" data-tooltip="Enter your Gemini API Key to enable AI-powered condition analysis and explanations.">
80
+ <i class="fa-solid fa-circle-info"></i>
81
+ </span>
82
+ </label>
83
+ <input type="password" id="gemini-key-input" placeholder="AI final judgment key..." class="styled-input">
84
+ </div>
85
+
86
+ <!-- Upload Zone: Before Video -->
87
+ <div class="upload-box" id="upload-before-zone">
88
+ <input type="file" id="video-before" name="video_before" accept="video/mp4" class="file-input">
89
+ <div class="upload-content">
90
+ <i class="fa-solid fa-cloud-arrow-up upload-icon before-icon"></i>
91
+ <h3>Before Renting Video</h3>
92
+ <p class="file-name-display" id="before-file-name">Drag & drop or click to upload (.mp4)</p>
93
+ </div>
94
+ </div>
95
+
96
+ <!-- Upload Zone: After Video -->
97
+ <div class="upload-box" id="upload-after-zone">
98
+ <input type="file" id="video-after" name="video_after" accept="video/mp4" class="file-input">
99
+ <div class="upload-content">
100
+ <i class="fa-solid fa-cloud-arrow-up upload-icon after-icon"></i>
101
+ <h3>After Renting Video</h3>
102
+ <p class="file-name-display" id="after-file-name">Drag & drop or click to upload (.mp4)</p>
103
+ </div>
104
+ </div>
105
+
106
+ <!-- Action buttons -->
107
+ <div class="actions">
108
+ <button type="submit" id="upload-btn" class="btn btn-primary">
109
+ <i class="fa-solid fa-arrow-up-from-bracket"></i> Upload Videos
110
+ </button>
111
+ <button type="button" id="start-btn" class="btn btn-accent btn-disabled" disabled>
112
+ <i class="fa-solid fa-play"></i> Run Asset Evaluation
113
+ </button>
114
+ <button type="button" id="reset-btn" class="btn btn-danger">
115
+ <i class="fa-solid fa-rotate-left"></i> Reset
116
+ </button>
117
+ </div>
118
+ </form>
119
+
120
+ <!-- Processing Screen / Status details -->
121
+ <div class="processing-overlay hidden" id="processing-view">
122
+ <div class="progress-container">
123
+ <div class="spinner">
124
+ <div class="double-bounce1"></div>
125
+ <div class="double-bounce2"></div>
126
+ </div>
127
+ <h2>Evaluating Apartment Videos</h2>
128
+ <p id="progress-status">Starting up SAM 2 tracker...</p>
129
+
130
+ <div class="progress-bar-bg">
131
+ <div class="progress-bar-fill" id="progress-fill" style="width: 0%;"></div>
132
+ </div>
133
+ <span class="progress-percentage" id="progress-pct">0%</span>
134
+ </div>
135
+ </div>
136
+ </section>
137
+
138
+ <!-- Right Side: Results & Video Outputs -->
139
+ <section class="card results-card">
140
+ <h2 class="section-title"><i class="fa-solid fa-chart-line"></i> Evaluation Report</h2>
141
+
142
+ <!-- Welcome/Placeholder Message -->
143
+ <div class="placeholder-msg" id="results-placeholder">
144
+ <i class="fa-solid fa-wand-magic-sparkles placeholder-icon"></i>
145
+ <h3>No Active Evaluation</h3>
146
+ <p>Upload your apartment condition videos (Before & After renting) on the left panel, configure the asset prompts, and run the evaluation to generate the report.</p>
147
+ </div>
148
+
149
+ <!-- Main results view -->
150
+ <div class="results-content hidden" id="results-view">
151
+
152
+ <!-- Summary Dashboard Banner -->
153
+ <div id="summary-banner" class="summary-banner"></div>
154
+
155
+ <!-- Video streams side by side -->
156
+ <div class="videos-comparison-grid">
157
+ <div class="video-container">
158
+ <h4><i class="fa-solid fa-circle-dot text-success"></i> Before Renting Tracker</h4>
159
+ <video id="out-video-before" controls src="" type="video/mp4"></video>
160
+ </div>
161
+ <div class="video-container">
162
+ <h4><i class="fa-solid fa-circle-dot text-danger"></i> After Renting Tracker</h4>
163
+ <video id="out-video-after" controls src="" type="video/mp4"></video>
164
+ </div>
165
+ </div>
166
+
167
+ <!-- Condition Report Cards -->
168
+ <div class="report-section">
169
+ <h3><i class="fa-solid fa-clipboard-check"></i> Asset Comparison Details</h3>
170
+ <div class="asset-cards-grid" id="asset-cards-container">
171
+ <!-- Populated dynamically by javascript -->
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </section>
176
+ </main>
177
+ </div>
178
+
179
+ <!-- Main JS script -->
180
+ <script src="/static/js/main.js"></script>
181
+ </body>
182
+ </html>
main.js ADDED
@@ -0,0 +1,458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ // DOM Elements
3
+ const uploadForm = document.getElementById('upload-form');
4
+ const promptInput = document.getElementById('prompt-input');
5
+ const beforeInput = document.getElementById('video-before');
6
+ const afterInput = document.getElementById('video-after');
7
+ const beforeZone = document.getElementById('upload-before-zone');
8
+ const afterZone = document.getElementById('upload-after-zone');
9
+ const beforeFileName = document.getElementById('before-file-name');
10
+ const afterFileName = document.getElementById('after-file-name');
11
+
12
+ const speedModeSelect = document.getElementById('speed-mode-select');
13
+ const fpsSelect = document.getElementById('fps-select');
14
+ const geminiKeyInput = document.getElementById('gemini-key-input');
15
+
16
+ const uploadBtn = document.getElementById('upload-btn');
17
+ const startBtn = document.getElementById('start-btn');
18
+ const resetBtn = document.getElementById('reset-btn');
19
+
20
+ const processingView = document.getElementById('processing-view');
21
+ const progressStatus = document.getElementById('progress-status');
22
+ const progressFill = document.getElementById('progress-fill');
23
+ const progressPct = document.getElementById('progress-pct');
24
+ const backendStatusText = document.getElementById('backend-status-text');
25
+
26
+ const resultsPlaceholder = document.getElementById('results-placeholder');
27
+ const resultsView = document.getElementById('results-view');
28
+ const outVideoBefore = document.getElementById('out-video-before');
29
+ const outVideoAfter = document.getElementById('out-video-after');
30
+ const assetCardsContainer = document.getElementById('asset-cards-container');
31
+
32
+ let pollInterval = null;
33
+
34
+ // --- Load saved Gemini API Key ---
35
+ const savedKey = localStorage.getItem('gemini_api_key');
36
+ if (savedKey) {
37
+ geminiKeyInput.value = savedKey;
38
+ }
39
+
40
+ // --- File Drag & Drop Listeners ---
41
+ setupDragAndDrop(beforeZone, beforeInput, beforeFileName, "Before Renting Video Selected");
42
+ setupDragAndDrop(afterZone, afterInput, afterFileName, "After Renting Video Selected");
43
+
44
+ function setupDragAndDrop(zone, input, display, successMsg) {
45
+ zone.addEventListener('click', () => input.click());
46
+
47
+ input.addEventListener('change', (e) => {
48
+ if (input.files.length > 0) {
49
+ display.textContent = `${input.files[0].name} (${(input.files[0].size / 1e6).toFixed(1)} MB)`;
50
+ zone.style.borderColor = 'var(--success)';
51
+ }
52
+ });
53
+
54
+ zone.addEventListener('dragover', (e) => {
55
+ e.preventDefault();
56
+ zone.classList.add('drag-over');
57
+ });
58
+
59
+ zone.addEventListener('dragleave', () => {
60
+ zone.classList.remove('drag-over');
61
+ });
62
+
63
+ zone.addEventListener('drop', (e) => {
64
+ e.preventDefault();
65
+ zone.classList.remove('drag-over');
66
+ if (e.dataTransfer.files.length > 0) {
67
+ input.files = e.dataTransfer.files;
68
+ display.textContent = `${input.files[0].name} (${(input.files[0].size / 1e6).toFixed(1)} MB)`;
69
+ zone.style.borderColor = 'var(--success)';
70
+ }
71
+ });
72
+ }
73
+
74
+ // --- Upload Videos ---
75
+ uploadForm.addEventListener('submit', async (e) => {
76
+ e.preventDefault();
77
+
78
+ if (beforeInput.files.length === 0 || afterInput.files.length === 0) {
79
+ alert('Please select both "Before" and "After" renting videos first.');
80
+ return;
81
+ }
82
+
83
+ const prompt = promptInput.value.trim();
84
+ if (!prompt) {
85
+ alert('Please define target inspection items prompt.');
86
+ return;
87
+ }
88
+
89
+ const formData = new FormData();
90
+ formData.append('video_before', beforeInput.files[0]);
91
+ formData.append('video_after', afterInput.files[0]);
92
+ formData.append('prompt', prompt);
93
+
94
+ try {
95
+ uploadBtn.disabled = true;
96
+ uploadBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Uploading...';
97
+ backendStatusText.textContent = "Uploading videos...";
98
+
99
+ const response = await fetch('/upload', {
100
+ method: 'POST',
101
+ body: formData
102
+ });
103
+
104
+ const data = await response.json();
105
+
106
+ if (!response.ok) {
107
+ throw new Error(data.error || 'Upload failed.');
108
+ }
109
+
110
+ uploadBtn.innerHTML = '<i class="fa-solid fa-check"></i> Uploaded!';
111
+ uploadBtn.style.background = 'var(--success)';
112
+
113
+ // Enable Start button
114
+ startBtn.removeAttribute('disabled');
115
+ startBtn.classList.remove('btn-disabled');
116
+ backendStatusText.textContent = "Ready to analyze";
117
+
118
+ alert('Videos uploaded successfully. You can now start the evaluation.');
119
+
120
+ } catch (error) {
121
+ console.error(error);
122
+ alert(`Error: ${error.message}`);
123
+ uploadBtn.disabled = false;
124
+ uploadBtn.innerHTML = '<i class="fa-solid fa-arrow-up-from-bracket"></i> Upload Videos';
125
+ backendStatusText.textContent = "Upload failed";
126
+ }
127
+ });
128
+
129
+ // --- Start Tracking / Evaluation ---
130
+ startBtn.addEventListener('click', async () => {
131
+ const prompt = promptInput.value.trim();
132
+ const geminiKey = geminiKeyInput.value.trim();
133
+ const speedMode = speedModeSelect.value;
134
+ const fps = fpsSelect.value;
135
+
136
+ // Securely cache Gemini API key in browser
137
+ if (geminiKey) {
138
+ localStorage.setItem('gemini_api_key', geminiKey);
139
+ } else {
140
+ localStorage.removeItem('gemini_api_key');
141
+ }
142
+
143
+ try {
144
+ startBtn.disabled = true;
145
+ startBtn.classList.add('btn-disabled');
146
+ processingView.classList.remove('hidden');
147
+ backendStatusText.textContent = "Running evaluation...";
148
+
149
+ const response = await fetch('/process', {
150
+ method: 'POST',
151
+ headers: {
152
+ 'Content-Type': 'application/json'
153
+ },
154
+ body: JSON.stringify({
155
+ prompt: prompt,
156
+ gemini_api_key: geminiKey,
157
+ speed_mode: speedMode,
158
+ fps: parseFloat(fps)
159
+ })
160
+ });
161
+
162
+ const data = await response.json();
163
+ if (!response.ok) {
164
+ throw new Error(data.error || 'Failed to start process.');
165
+ }
166
+
167
+ // Start polling status
168
+ startStatusPolling();
169
+
170
+ } catch (error) {
171
+ console.error(error);
172
+ alert(`Error: ${error.message}`);
173
+ processingView.classList.add('hidden');
174
+ startBtn.disabled = false;
175
+ startBtn.classList.remove('btn-disabled');
176
+ backendStatusText.textContent = "Error occurred";
177
+ }
178
+ });
179
+
180
+ // --- Reset ---
181
+ resetBtn.addEventListener('click', async () => {
182
+ if (confirm('Are you sure you want to reset and clear all data?')) {
183
+ try {
184
+ await fetch('/reset', { method: 'POST' });
185
+
186
+ // Clear intervals
187
+ clearInterval(pollInterval);
188
+ pollInterval = null;
189
+
190
+ // Reset inputs and displays
191
+ uploadForm.reset();
192
+ beforeInput.value = '';
193
+ afterInput.value = '';
194
+ beforeZone.style.borderColor = 'var(--border-color)';
195
+ afterZone.style.borderColor = 'var(--border-color)';
196
+ beforeFileName.textContent = 'Drag & drop or click to upload (.mp4)';
197
+ afterFileName.textContent = 'Drag & drop or click to upload (.mp4)';
198
+
199
+ // Retain Gemini Key
200
+ const savedKey = localStorage.getItem('gemini_api_key');
201
+ if (savedKey) {
202
+ geminiKeyInput.value = savedKey;
203
+ }
204
+
205
+ uploadBtn.disabled = false;
206
+ uploadBtn.innerHTML = '<i class="fa-solid fa-arrow-up-from-bracket"></i> Upload Videos';
207
+ uploadBtn.style.background = 'var(--primary)';
208
+
209
+ startBtn.disabled = true;
210
+ startBtn.classList.add('btn-disabled');
211
+
212
+ processingView.classList.add('hidden');
213
+ resultsView.classList.add('hidden');
214
+ resultsPlaceholder.classList.remove('hidden');
215
+
216
+ outVideoBefore.src = '';
217
+ outVideoAfter.src = '';
218
+ assetCardsContainer.innerHTML = '';
219
+
220
+ backendStatusText.textContent = "System Ready";
221
+
222
+ } catch (error) {
223
+ console.error(error);
224
+ alert('Reset failed.');
225
+ }
226
+ }
227
+ });
228
+
229
+ // --- Status Polling ---
230
+ function startStatusPolling() {
231
+ if (pollInterval) clearInterval(pollInterval);
232
+
233
+ pollInterval = setInterval(async () => {
234
+ try {
235
+ const response = await fetch('/status');
236
+ const data = await response.json();
237
+
238
+ if (data.error) {
239
+ clearInterval(pollInterval);
240
+ alert(`Evaluation Error: ${data.error}`);
241
+ processingView.classList.add('hidden');
242
+ startBtn.disabled = false;
243
+ startBtn.classList.remove('btn-disabled');
244
+ backendStatusText.textContent = "Analysis Failed";
245
+ return;
246
+ }
247
+
248
+ // Update progress overlay
249
+ progressStatus.textContent = data.status;
250
+ progressFill.style.width = `${data.percent}%`;
251
+ progressPct.textContent = `${data.percent}%`;
252
+
253
+ if (!data.running && data.percent === 100 && data.result) {
254
+ clearInterval(pollInterval);
255
+ pollInterval = null;
256
+ displayResults(data.result);
257
+ }
258
+
259
+ } catch (error) {
260
+ console.error('Polling error:', error);
261
+ }
262
+ }, 1500);
263
+ }
264
+
265
+ // --- Display Results ---
266
+ function displayResults(result) {
267
+ // Hide processing and placeholder
268
+ processingView.classList.add('hidden');
269
+ resultsPlaceholder.classList.add('hidden');
270
+ resultsView.classList.remove('hidden');
271
+ backendStatusText.textContent = "Evaluation Complete";
272
+
273
+ // Render Summary Banner
274
+ const summaryBanner = document.getElementById('summary-banner');
275
+ if (summaryBanner && result.summary) {
276
+ const s = result.summary;
277
+
278
+ // Format elapsed time (e.g. "45.2s" or "1m 15s")
279
+ let timeStr = "";
280
+ if (s.elapsed_time < 60) {
281
+ timeStr = `${s.elapsed_time}s`;
282
+ } else {
283
+ const mins = Math.floor(s.elapsed_time / 60);
284
+ const secs = Math.round(s.elapsed_time % 60);
285
+ timeStr = `${mins}m ${secs}s`;
286
+ }
287
+
288
+ // Compensation judgment logic: if any item is Missing or Damaged, tenant must pay compensation
289
+ const compensationRequired = (s.statuses.Missing || 0) > 0 || (s.statuses["Potential Damage / Altered"] || 0) > 0;
290
+ const verdictTitle = compensationRequired ? "Tenant Must Pay Compensation" : "No Compensation Required";
291
+ const verdictColor = compensationRequired ? "var(--danger)" : "var(--success)";
292
+ const verdictIcon = compensationRequired ? "fa-solid fa-circle-exclamation" : "fa-solid fa-circle-check";
293
+ const verdictDesc = compensationRequired
294
+ ? "Damage or missing assets detected. Tenant is liable for compensation."
295
+ : "All tracked assets are intact. No compensation required.";
296
+
297
+ // Create unique list of all discovered item labels
298
+ const uniqueLabels = Array.from(new Set([...s.before_items, ...s.after_items]));
299
+ const labelsHtml = uniqueLabels.length > 0
300
+ ? uniqueLabels.map(lbl => `<span class="discovered-tag">${lbl}</span>`).join('')
301
+ : '<span class="text-secondary">None</span>';
302
+
303
+ summaryBanner.innerHTML = `
304
+ <div class="summary-card highlight" style="grid-column: span 2; border-color: ${verdictColor}; background: linear-gradient(135deg, rgba(20, 24, 33, 0.85), ${compensationRequired ? 'rgba(255, 82, 82, 0.05)' : 'rgba(0, 219, 139, 0.05)'});">
305
+ <span class="summary-card-val" style="color: ${verdictColor}; font-size: 1.35rem; display: flex; align-items: center; gap: 8px;">
306
+ <i class="${verdictIcon}"></i> ${verdictTitle}
307
+ </span>
308
+ <span class="summary-card-label" style="margin-top: 5px;">AI Final Verdict</span>
309
+ <span style="font-size: 0.82rem; color: var(--text-secondary); margin-top: 4px; line-height: 1.3;">${verdictDesc}</span>
310
+ </div>
311
+ <div class="summary-card">
312
+ <span class="summary-card-val time">${timeStr}</span>
313
+ <span class="summary-card-label">Time Taken</span>
314
+ </div>
315
+ <div class="summary-card">
316
+ <span class="summary-card-val before">${s.total_before}</span>
317
+ <span class="summary-card-label">Assets (Before)</span>
318
+ </div>
319
+ <div class="summary-card">
320
+ <span class="summary-card-val after">${s.total_after}</span>
321
+ <span class="summary-card-label">Assets (After)</span>
322
+ </div>
323
+ <div class="summary-card">
324
+ <span class="summary-card-val missing">${s.statuses.Missing || 0}</span>
325
+ <span class="summary-card-label">Missing Items</span>
326
+ </div>
327
+ <div class="summary-card">
328
+ <span class="summary-card-val" style="color: var(--warning);">${s.statuses["Potential Damage / Altered"] || 0}</span>
329
+ <span class="summary-card-label">Damaged Items</span>
330
+ </div>
331
+ <div class="summary-card">
332
+ <span class="summary-card-val" style="color: var(--success);">${(s.statuses.Intact || 0) + (s.statuses["Fair / Minor Change"] || 0)}</span>
333
+ <span class="summary-card-label">Intact / Fair</span>
334
+ </div>
335
+ <div class="discovered-labels-list">
336
+ <strong><i class="fa-solid fa-list-check"></i> Discovered Items:</strong>
337
+ ${labelsHtml}
338
+ </div>
339
+ `;
340
+ }
341
+
342
+ // Set video sources (add timestamp parameter to bypass browser cache)
343
+ const t = new Date().getTime();
344
+ outVideoBefore.src = `${result.annotated_before}?t=${t}`;
345
+ outVideoAfter.src = `${result.annotated_after}?t=${t}`;
346
+ outVideoBefore.load();
347
+ outVideoAfter.load();
348
+
349
+ // Render Asset Cards
350
+ assetCardsContainer.innerHTML = '';
351
+
352
+ result.report.forEach(item => {
353
+ const card = document.createElement('div');
354
+ card.className = 'asset-card';
355
+
356
+ // Top Row of card: Info, Crops, and Status
357
+ const mainDiv = document.createElement('div');
358
+ mainDiv.className = 'asset-card-main';
359
+
360
+ // 1. Info section
361
+ const infoDiv = document.createElement('div');
362
+ infoDiv.className = 'asset-info';
363
+
364
+ const labelH3 = document.createElement('h3');
365
+ labelH3.className = 'asset-label';
366
+ labelH3.textContent = item.label;
367
+
368
+ const scoreSpan = document.createElement('span');
369
+ scoreSpan.className = 'asset-score';
370
+ scoreSpan.textContent = item.before_crop && item.after_crop ? `Similarity: ${item.similarity}%` : '';
371
+
372
+ infoDiv.appendChild(labelH3);
373
+ infoDiv.appendChild(scoreSpan);
374
+ mainDiv.appendChild(infoDiv);
375
+
376
+ // 2. Before Crop
377
+ const beforeCropDiv = createCropView(item.before_crop, "Before");
378
+ mainDiv.appendChild(beforeCropDiv);
379
+
380
+ // 3. After Crop
381
+ const afterCropDiv = createCropView(item.after_crop, "After");
382
+ mainDiv.appendChild(afterCropDiv);
383
+
384
+ // 4. Status Badge
385
+ const badgeDiv = document.createElement('div');
386
+ const badge = document.createElement('span');
387
+ badge.className = `status-badge ${getBadgeClass(item.status)}`;
388
+ badge.textContent = item.status;
389
+ badgeDiv.appendChild(badge);
390
+ mainDiv.appendChild(badgeDiv);
391
+
392
+ card.appendChild(mainDiv);
393
+
394
+ // Bottom Row of card: Explanation (AI-generated or fallback)
395
+ if (item.explanation) {
396
+ const explanationDiv = document.createElement('div');
397
+ explanationDiv.className = 'asset-explanation';
398
+
399
+ const icon = document.createElement('i');
400
+ // Use robot icon if Gemini was used, sparkles icon if local CV fallback was used
401
+ icon.className = item.ai_evaluated ? 'fa-solid fa-robot explanation-icon' : 'fa-solid fa-wand-magic-sparkles explanation-icon';
402
+
403
+ const textSpan = document.createElement('span');
404
+ textSpan.textContent = item.explanation;
405
+
406
+ explanationDiv.appendChild(icon);
407
+ explanationDiv.appendChild(textSpan);
408
+ card.appendChild(explanationDiv);
409
+ }
410
+
411
+ assetCardsContainer.appendChild(card);
412
+ });
413
+ }
414
+
415
+ function createCropView(src, label) {
416
+ const container = document.createElement('div');
417
+ container.className = 'crop-img-container';
418
+
419
+ const title = document.createElement('span');
420
+ title.textContent = label;
421
+ container.appendChild(title);
422
+
423
+ const frame = document.createElement('div');
424
+ frame.className = 'crop-frame';
425
+
426
+ if (src) {
427
+ const img = document.createElement('img');
428
+ // Add cache-busting timestamp
429
+ img.src = `${src}?t=${new Date().getTime()}`;
430
+ img.alt = label;
431
+ frame.appendChild(img);
432
+ } else {
433
+ const icon = document.createElement('i');
434
+ icon.className = 'fa-solid fa-circle-question crop-missing-icon';
435
+ frame.appendChild(icon);
436
+ }
437
+
438
+ container.appendChild(frame);
439
+ return container;
440
+ }
441
+
442
+ function getBadgeClass(status) {
443
+ switch (status) {
444
+ case 'Intact':
445
+ return 'badge-intact';
446
+ case 'Fair / Minor Change':
447
+ return 'badge-fair';
448
+ case 'Potential Damage / Altered':
449
+ return 'badge-damaged';
450
+ case 'Missing':
451
+ return 'badge-missing';
452
+ case 'New Item':
453
+ return 'badge-new';
454
+ default:
455
+ return '';
456
+ }
457
+ }
458
+ });
styles.css ADDED
@@ -0,0 +1,843 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* CSS Variables for Premium Aesthetics */
2
+ :root {
3
+ --bg-dark: #0a0c10;
4
+ --bg-card: rgba(20, 24, 33, 0.65);
5
+ --border-color: rgba(255, 255, 255, 0.08);
6
+ --border-focus: rgba(124, 77, 255, 0.4);
7
+
8
+ --text-primary: #f1f3f9;
9
+ --text-secondary: #909bb4;
10
+ --text-disabled: #4e5566;
11
+
12
+ --primary: #6c5ce7;
13
+ --primary-hover: #5b4bc4;
14
+ --accent: #00cec9;
15
+ --accent-hover: #00b5b0;
16
+
17
+ --success: #00db8b;
18
+ --warning: #ff9f43;
19
+ --danger: #ff5252;
20
+ --info: #0984e3;
21
+
22
+ --font-main: 'Outfit', sans-serif;
23
+ --font-mono: 'Space Grotesk', sans-serif;
24
+ --radius-sm: 8px;
25
+ --radius-md: 16px;
26
+ --radius-lg: 24px;
27
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
28
+ }
29
+
30
+ /* Reset */
31
+ * {
32
+ margin: 0;
33
+ padding: 0;
34
+ box-sizing: border-box;
35
+ }
36
+
37
+ body {
38
+ background-color: var(--bg-dark);
39
+ color: var(--text-primary);
40
+ font-family: var(--font-main);
41
+ overflow-x: hidden;
42
+ min-height: 100vh;
43
+ display: flex;
44
+ justify-content: center;
45
+ position: relative;
46
+ }
47
+
48
+ /* Glassmorphism Background elements */
49
+ .glass-bg {
50
+ position: absolute;
51
+ width: 600px;
52
+ height: 600px;
53
+ background: radial-gradient(circle, rgba(108, 92, 231, 0.12) 0%, rgba(0, 206, 201, 0.05) 50%, rgba(0,0,0,0) 100%);
54
+ top: -10%;
55
+ right: -10%;
56
+ z-index: -1;
57
+ filter: blur(80px);
58
+ pointer-events: none;
59
+ }
60
+
61
+ /* App Container */
62
+ .app-container {
63
+ width: 100%;
64
+ max-width: 1600px;
65
+ padding: 30px;
66
+ display: flex;
67
+ flex-direction: column;
68
+ gap: 30px;
69
+ z-index: 1;
70
+ }
71
+
72
+ /* Header */
73
+ .app-header {
74
+ display: flex;
75
+ justify-content: space-between;
76
+ align-items: center;
77
+ padding: 10px 20px;
78
+ background: var(--bg-card);
79
+ backdrop-filter: blur(12px);
80
+ border: 1px solid var(--border-color);
81
+ border-radius: var(--radius-md);
82
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
83
+ }
84
+
85
+ .logo {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 15px;
89
+ }
90
+
91
+ .logo-icon {
92
+ font-size: 2.5rem;
93
+ background: linear-gradient(135deg, var(--primary), var(--accent));
94
+ -webkit-background-clip: text;
95
+ -webkit-text-fill-color: transparent;
96
+ }
97
+
98
+ .logo-text h1 {
99
+ font-size: 1.5rem;
100
+ font-weight: 700;
101
+ letter-spacing: -0.5px;
102
+ }
103
+
104
+ .logo-text span {
105
+ font-size: 0.85rem;
106
+ color: var(--text-secondary);
107
+ }
108
+
109
+ .status-indicator {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 10px;
113
+ background: rgba(255, 255, 255, 0.04);
114
+ padding: 8px 16px;
115
+ border-radius: 50px;
116
+ border: 1px solid var(--border-color);
117
+ font-family: var(--font-mono);
118
+ font-size: 0.9rem;
119
+ }
120
+
121
+ .pulse-dot {
122
+ width: 10px;
123
+ height: 10px;
124
+ background-color: var(--success);
125
+ border-radius: 50%;
126
+ box-shadow: 0 0 0 0 rgba(0, 219, 139, 0.7);
127
+ animation: pulse 1.6s infinite;
128
+ }
129
+
130
+ /* Grid Layout */
131
+ .app-grid {
132
+ display: grid;
133
+ grid-template-columns: 450px 1fr;
134
+ gap: 30px;
135
+ align-items: start;
136
+ }
137
+
138
+ @media (max-width: 1100px) {
139
+ .app-grid {
140
+ grid-template-columns: 1fr;
141
+ }
142
+ }
143
+
144
+ /* Cards */
145
+ .card {
146
+ background: var(--bg-card);
147
+ backdrop-filter: blur(12px);
148
+ border: 1px solid var(--border-color);
149
+ border-radius: var(--radius-md);
150
+ padding: 30px;
151
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.25);
152
+ position: relative;
153
+ transition: var(--transition);
154
+ }
155
+
156
+ .card:hover {
157
+ border-color: rgba(255, 255, 255, 0.12);
158
+ }
159
+
160
+ .config-card {
161
+ min-height: 500px;
162
+ }
163
+
164
+ .results-card {
165
+ min-height: 700px;
166
+ display: flex;
167
+ flex-direction: column;
168
+ }
169
+
170
+ .section-title {
171
+ font-size: 1.25rem;
172
+ font-weight: 600;
173
+ margin-bottom: 25px;
174
+ display: flex;
175
+ align-items: center;
176
+ gap: 10px;
177
+ border-bottom: 1px solid var(--border-color);
178
+ padding-bottom: 12px;
179
+ }
180
+
181
+ /* Input Fields */
182
+ .input-group {
183
+ display: flex;
184
+ flex-direction: column;
185
+ gap: 8px;
186
+ margin-bottom: 20px;
187
+ }
188
+
189
+ .input-group label {
190
+ font-size: 0.95rem;
191
+ font-weight: 500;
192
+ color: var(--text-primary);
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 6px;
196
+ }
197
+
198
+ textarea {
199
+ width: 100%;
200
+ background: rgba(0, 0, 0, 0.25);
201
+ border: 1px solid var(--border-color);
202
+ border-radius: var(--radius-sm);
203
+ color: var(--text-primary);
204
+ padding: 12px;
205
+ font-family: var(--font-main);
206
+ font-size: 0.95rem;
207
+ line-height: 1.5;
208
+ resize: vertical;
209
+ transition: var(--transition);
210
+ }
211
+
212
+ textarea:focus {
213
+ outline: none;
214
+ border-color: var(--primary);
215
+ box-shadow: 0 0 0 3px rgba(108, 92, 231, 0.15);
216
+ }
217
+
218
+ /* Upload boxes */
219
+ .upload-box {
220
+ border: 2px dashed var(--border-color);
221
+ border-radius: var(--radius-md);
222
+ padding: 24px;
223
+ text-align: center;
224
+ position: relative;
225
+ margin-bottom: 20px;
226
+ cursor: pointer;
227
+ background: rgba(255, 255, 255, 0.01);
228
+ transition: var(--transition);
229
+ }
230
+
231
+ .upload-box:hover {
232
+ border-color: var(--primary);
233
+ background: rgba(108, 92, 231, 0.02);
234
+ }
235
+
236
+ .upload-box.drag-over {
237
+ border-color: var(--accent);
238
+ background: rgba(0, 206, 201, 0.05);
239
+ }
240
+
241
+ .file-input {
242
+ position: absolute;
243
+ top: 0;
244
+ left: 0;
245
+ width: 100%;
246
+ height: 100%;
247
+ opacity: 0;
248
+ cursor: pointer;
249
+ }
250
+
251
+ .upload-icon {
252
+ font-size: 2.2rem;
253
+ margin-bottom: 12px;
254
+ transition: var(--transition);
255
+ }
256
+
257
+ .before-icon {
258
+ color: var(--accent);
259
+ }
260
+
261
+ .after-icon {
262
+ color: var(--primary);
263
+ }
264
+
265
+ .upload-box:hover .upload-icon {
266
+ transform: translateY(-4px);
267
+ }
268
+
269
+ .upload-box h3 {
270
+ font-size: 1.05rem;
271
+ font-weight: 500;
272
+ margin-bottom: 6px;
273
+ }
274
+
275
+ .upload-box p {
276
+ font-size: 0.85rem;
277
+ color: var(--text-secondary);
278
+ }
279
+
280
+ /* Buttons */
281
+ .actions {
282
+ display: flex;
283
+ flex-direction: column;
284
+ gap: 12px;
285
+ margin-top: 25px;
286
+ }
287
+
288
+ .btn {
289
+ width: 100%;
290
+ padding: 14px;
291
+ border: none;
292
+ border-radius: var(--radius-sm);
293
+ font-family: var(--font-main);
294
+ font-size: 1rem;
295
+ font-weight: 600;
296
+ cursor: pointer;
297
+ display: flex;
298
+ align-items: center;
299
+ justify-content: center;
300
+ gap: 10px;
301
+ transition: var(--transition);
302
+ }
303
+
304
+ .btn-primary {
305
+ background: var(--primary);
306
+ color: #fff;
307
+ }
308
+
309
+ .btn-primary:hover:not(:disabled) {
310
+ background: var(--primary-hover);
311
+ }
312
+
313
+ .btn-accent {
314
+ background: var(--accent);
315
+ color: #0a0c10;
316
+ }
317
+
318
+ .btn-accent:hover:not(:disabled) {
319
+ background: var(--accent-hover);
320
+ }
321
+
322
+ .btn-danger {
323
+ background: transparent;
324
+ border: 1px solid var(--danger);
325
+ color: var(--danger);
326
+ }
327
+
328
+ .btn-danger:hover:not(:disabled) {
329
+ background: rgba(255, 82, 82, 0.1);
330
+ }
331
+
332
+ .btn-disabled, .btn:disabled {
333
+ background: var(--text-disabled) !important;
334
+ color: var(--text-secondary) !important;
335
+ cursor: not-allowed;
336
+ opacity: 0.5;
337
+ }
338
+
339
+ /* Tooltips */
340
+ .tooltip {
341
+ cursor: pointer;
342
+ color: var(--text-secondary);
343
+ font-size: 0.85rem;
344
+ position: relative;
345
+ }
346
+
347
+ /* Processing Overlay Screen */
348
+ .processing-overlay {
349
+ position: absolute;
350
+ top: 0;
351
+ left: 0;
352
+ width: 100%;
353
+ height: 100%;
354
+ background: rgba(10, 12, 16, 0.95);
355
+ border-radius: var(--radius-md);
356
+ z-index: 10;
357
+ display: flex;
358
+ align-items: center;
359
+ justify-content: center;
360
+ padding: 40px;
361
+ animation: fadeIn 0.3s ease-in-out;
362
+ }
363
+
364
+ .progress-container {
365
+ width: 100%;
366
+ max-width: 380px;
367
+ text-align: center;
368
+ display: flex;
369
+ flex-direction: column;
370
+ align-items: center;
371
+ gap: 15px;
372
+ }
373
+
374
+ .progress-container h2 {
375
+ font-size: 1.4rem;
376
+ font-weight: 600;
377
+ }
378
+
379
+ .progress-container p {
380
+ font-size: 0.95rem;
381
+ color: var(--text-secondary);
382
+ min-height: 44px;
383
+ }
384
+
385
+ .progress-bar-bg {
386
+ width: 100%;
387
+ height: 8px;
388
+ background: rgba(255, 255, 255, 0.08);
389
+ border-radius: 10px;
390
+ overflow: hidden;
391
+ margin-top: 10px;
392
+ }
393
+
394
+ .progress-bar-fill {
395
+ height: 100%;
396
+ background: linear-gradient(90deg, var(--accent), var(--primary));
397
+ border-radius: 10px;
398
+ transition: width 0.4s ease;
399
+ }
400
+
401
+ .progress-percentage {
402
+ font-family: var(--font-mono);
403
+ font-size: 1.5rem;
404
+ font-weight: 700;
405
+ color: var(--accent);
406
+ }
407
+
408
+ /* Results Placeholder */
409
+ .placeholder-msg {
410
+ flex: 1;
411
+ display: flex;
412
+ flex-direction: column;
413
+ align-items: center;
414
+ justify-content: center;
415
+ text-align: center;
416
+ max-width: 500px;
417
+ margin: 100px auto;
418
+ gap: 15px;
419
+ }
420
+
421
+ .placeholder-icon {
422
+ font-size: 4rem;
423
+ background: linear-gradient(135deg, var(--accent), var(--primary));
424
+ -webkit-background-clip: text;
425
+ -webkit-text-fill-color: transparent;
426
+ margin-bottom: 10px;
427
+ }
428
+
429
+ .placeholder-msg h3 {
430
+ font-size: 1.4rem;
431
+ font-weight: 600;
432
+ }
433
+
434
+ .placeholder-msg p {
435
+ font-size: 0.95rem;
436
+ color: var(--text-secondary);
437
+ line-height: 1.6;
438
+ }
439
+
440
+ /* Active Results Content */
441
+ .results-content {
442
+ display: flex;
443
+ flex-direction: column;
444
+ gap: 30px;
445
+ animation: fadeIn 0.4s ease-in-out;
446
+ }
447
+
448
+ /* Video side-by-side comparison */
449
+ .videos-comparison-grid {
450
+ display: grid;
451
+ grid-template-columns: 1fr 1fr;
452
+ gap: 20px;
453
+ }
454
+
455
+ @media (max-width: 768px) {
456
+ .videos-comparison-grid {
457
+ grid-template-columns: 1fr;
458
+ }
459
+ }
460
+
461
+ .video-container {
462
+ display: flex;
463
+ flex-direction: column;
464
+ gap: 10px;
465
+ }
466
+
467
+ .video-container h4 {
468
+ font-size: 0.95rem;
469
+ font-weight: 600;
470
+ display: flex;
471
+ align-items: center;
472
+ gap: 8px;
473
+ }
474
+
475
+ .video-container video {
476
+ width: 100%;
477
+ border-radius: var(--radius-sm);
478
+ border: 1px solid var(--border-color);
479
+ background: #000;
480
+ aspect-ratio: 16/9;
481
+ object-fit: contain;
482
+ }
483
+
484
+ /* Report Section */
485
+ .report-section {
486
+ display: flex;
487
+ flex-direction: column;
488
+ gap: 15px;
489
+ }
490
+
491
+ .report-section h3 {
492
+ font-size: 1.2rem;
493
+ font-weight: 600;
494
+ display: flex;
495
+ align-items: center;
496
+ gap: 8px;
497
+ padding-bottom: 8px;
498
+ border-bottom: 1px solid var(--border-color);
499
+ }
500
+
501
+ /* Dynamic Cards Grid */
502
+ .asset-cards-grid {
503
+ display: flex;
504
+ flex-direction: column;
505
+ gap: 16px;
506
+ }
507
+
508
+ /* Glassmorphic asset detail card */
509
+ .asset-card {
510
+ background: rgba(255, 255, 255, 0.02);
511
+ border: 1px solid var(--border-color);
512
+ border-radius: var(--radius-md);
513
+ padding: 20px;
514
+ display: grid;
515
+ grid-template-columns: 1.5fr 1fr 1fr 1.2fr;
516
+ align-items: center;
517
+ gap: 20px;
518
+ transition: var(--transition);
519
+ }
520
+
521
+ .asset-card:hover {
522
+ background: rgba(255, 255, 255, 0.04);
523
+ transform: translateY(-2px);
524
+ border-color: rgba(255, 255, 255, 0.15);
525
+ }
526
+
527
+ .asset-info {
528
+ display: flex;
529
+ flex-direction: column;
530
+ gap: 6px;
531
+ }
532
+
533
+ .asset-label {
534
+ font-size: 1.2rem;
535
+ font-weight: 600;
536
+ text-transform: capitalize;
537
+ }
538
+
539
+ .asset-score {
540
+ font-family: var(--font-mono);
541
+ font-size: 0.9rem;
542
+ color: var(--text-secondary);
543
+ }
544
+
545
+ /* Crop image frame */
546
+ .crop-img-container {
547
+ display: flex;
548
+ flex-direction: column;
549
+ gap: 6px;
550
+ align-items: center;
551
+ }
552
+
553
+ .crop-img-container span {
554
+ font-size: 0.75rem;
555
+ color: var(--text-secondary);
556
+ }
557
+
558
+ .crop-frame {
559
+ width: 90px;
560
+ height: 90px;
561
+ border-radius: var(--radius-sm);
562
+ border: 1px solid var(--border-color);
563
+ background: rgba(0,0,0,0.3);
564
+ display: flex;
565
+ align-items: center;
566
+ justify-content: center;
567
+ overflow: hidden;
568
+ position: relative;
569
+ }
570
+
571
+ .crop-frame img {
572
+ max-width: 100%;
573
+ max-height: 100%;
574
+ object-fit: cover;
575
+ }
576
+
577
+ .crop-missing-icon {
578
+ font-size: 1.8rem;
579
+ color: var(--text-disabled);
580
+ }
581
+
582
+ /* Badge Status Styles */
583
+ .status-badge {
584
+ padding: 8px 16px;
585
+ border-radius: 50px;
586
+ font-size: 0.85rem;
587
+ font-weight: 600;
588
+ text-align: center;
589
+ display: inline-block;
590
+ width: fit-content;
591
+ }
592
+
593
+ .badge-intact {
594
+ background: rgba(0, 219, 139, 0.12);
595
+ color: var(--success);
596
+ border: 1px solid rgba(0, 219, 139, 0.2);
597
+ }
598
+
599
+ .badge-fair {
600
+ background: rgba(255, 159, 67, 0.12);
601
+ color: var(--warning);
602
+ border: 1px solid rgba(255, 159, 67, 0.2);
603
+ }
604
+
605
+ .badge-damaged {
606
+ background: rgba(255, 82, 82, 0.12);
607
+ color: var(--danger);
608
+ border: 1px solid rgba(255, 82, 82, 0.2);
609
+ }
610
+
611
+ .badge-missing {
612
+ background: rgba(255, 82, 82, 0.12);
613
+ color: var(--danger);
614
+ border: 1px solid rgba(255, 82, 82, 0.2);
615
+ text-transform: uppercase;
616
+ letter-spacing: 0.5px;
617
+ }
618
+
619
+ .badge-new {
620
+ background: rgba(9, 132, 227, 0.12);
621
+ color: var(--info);
622
+ border: 1px solid rgba(9, 132, 227, 0.2);
623
+ }
624
+
625
+ /* Spinner Animation */
626
+ .spinner {
627
+ width: 60px;
628
+ height: 60px;
629
+ position: relative;
630
+ margin: 20px auto;
631
+ }
632
+
633
+ .double-bounce1, .double-bounce2 {
634
+ width: 100%;
635
+ height: 100%;
636
+ border-radius: 50%;
637
+ background-color: var(--accent);
638
+ opacity: 0.6;
639
+ position: absolute;
640
+ top: 0;
641
+ left: 0;
642
+ animation: bounce 2.0s infinite ease-in-out;
643
+ }
644
+
645
+ .double-bounce2 {
646
+ background-color: var(--primary);
647
+ animation-delay: -1.0s;
648
+ }
649
+
650
+ /* Helpers */
651
+ .hidden {
652
+ display: none !important;
653
+ }
654
+
655
+ .text-success { color: var(--success); }
656
+ .text-danger { color: var(--danger); }
657
+
658
+ /* Keyframe Animations */
659
+ @keyframes pulse {
660
+ 0% {
661
+ box-shadow: 0 0 0 0 rgba(0, 219, 139, 0.4);
662
+ }
663
+ 70% {
664
+ box-shadow: 0 0 0 10px rgba(0, 219, 139, 0);
665
+ }
666
+ 100% {
667
+ box-shadow: 0 0 0 0 rgba(0, 219, 139, 0);
668
+ }
669
+ }
670
+
671
+ @keyframes bounce {
672
+ 0%, 100% {
673
+ transform: scale(0.0);
674
+ } 50% {
675
+ transform: scale(1.0);
676
+ }
677
+ }
678
+
679
+ @keyframes fadeIn {
680
+ from { opacity: 0; transform: translateY(10px); }
681
+ to { opacity: 1; transform: translateY(0); }
682
+ }
683
+
684
+ /* Settings, Select and Inputs */
685
+ .settings-row {
686
+ display: flex;
687
+ gap: 15px;
688
+ width: 100%;
689
+ }
690
+
691
+ .settings-row .half {
692
+ flex: 1;
693
+ }
694
+
695
+ .styled-select, .styled-input {
696
+ width: 100%;
697
+ background: rgba(0, 0, 0, 0.25);
698
+ border: 1px solid var(--border-color);
699
+ border-radius: var(--radius-sm);
700
+ color: var(--text-primary);
701
+ padding: 10px 12px;
702
+ font-family: var(--font-main);
703
+ font-size: 0.9rem;
704
+ transition: var(--transition);
705
+ }
706
+
707
+ .styled-select:focus, .styled-input:focus {
708
+ outline: none;
709
+ border-color: var(--primary);
710
+ box-shadow: 0 0 0 3px rgba(108, 92, 231, 0.15);
711
+ }
712
+
713
+ /* Card layout update for AI Explanations */
714
+ .asset-card {
715
+ background: rgba(255, 255, 255, 0.02);
716
+ border: 1px solid var(--border-color);
717
+ border-radius: var(--radius-md);
718
+ padding: 20px;
719
+ display: flex;
720
+ flex-direction: column;
721
+ gap: 15px;
722
+ transition: var(--transition);
723
+ }
724
+
725
+ .asset-card-main {
726
+ display: grid;
727
+ grid-template-columns: 1.5fr 1fr 1fr 1.2fr;
728
+ align-items: center;
729
+ gap: 20px;
730
+ width: 100%;
731
+ }
732
+
733
+ .asset-explanation {
734
+ font-size: 0.9rem;
735
+ color: var(--text-secondary);
736
+ border-top: 1px dashed var(--border-color);
737
+ padding-top: 12px;
738
+ margin-top: 2px;
739
+ line-height: 1.5;
740
+ display: flex;
741
+ align-items: flex-start;
742
+ gap: 8px;
743
+ }
744
+
745
+ .explanation-icon {
746
+ color: var(--accent);
747
+ margin-top: 3px;
748
+ font-size: 0.95rem;
749
+ }
750
+
751
+ /* Summary Dashboard Banner styling */
752
+ .summary-banner {
753
+ display: grid;
754
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
755
+ gap: 15px;
756
+ width: 100%;
757
+ margin-bottom: 25px;
758
+ animation: fadeIn 0.5s ease-out;
759
+ }
760
+
761
+ .summary-card {
762
+ background: rgba(255, 255, 255, 0.02);
763
+ border: 1px solid var(--border-color);
764
+ border-radius: var(--radius-md);
765
+ padding: 18px 15px;
766
+ display: flex;
767
+ flex-direction: column;
768
+ align-items: center;
769
+ justify-content: center;
770
+ text-align: center;
771
+ gap: 5px;
772
+ transition: var(--transition);
773
+ }
774
+
775
+ .summary-card:hover {
776
+ transform: translateY(-2px);
777
+ border-color: rgba(255, 255, 255, 0.15);
778
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
779
+ }
780
+
781
+ .summary-card.highlight {
782
+ background: linear-gradient(135deg, rgba(108, 92, 231, 0.06), rgba(0, 206, 201, 0.04));
783
+ border-color: rgba(108, 92, 231, 0.2);
784
+ }
785
+
786
+ .summary-card-val {
787
+ font-size: 1.8rem;
788
+ font-weight: 700;
789
+ color: var(--text-primary);
790
+ font-family: var(--font-mono);
791
+ }
792
+
793
+ .summary-card-val.time {
794
+ color: var(--accent);
795
+ text-shadow: 0 0 10px rgba(0, 206, 201, 0.2);
796
+ }
797
+
798
+ .summary-card-val.before {
799
+ color: var(--success);
800
+ }
801
+
802
+ .summary-card-val.after {
803
+ color: var(--warning);
804
+ }
805
+
806
+ .summary-card-val.missing {
807
+ color: var(--danger);
808
+ }
809
+
810
+ .summary-card-label {
811
+ font-size: 0.75rem;
812
+ font-weight: 600;
813
+ text-transform: uppercase;
814
+ letter-spacing: 1px;
815
+ color: var(--text-secondary);
816
+ }
817
+
818
+ .discovered-labels-list {
819
+ grid-column: 1 / -1;
820
+ background: rgba(255, 255, 255, 0.01);
821
+ border: 1px dashed var(--border-color);
822
+ border-radius: var(--radius-sm);
823
+ padding: 12px 18px;
824
+ text-align: left;
825
+ font-size: 0.85rem;
826
+ color: var(--text-secondary);
827
+ display: flex;
828
+ flex-wrap: wrap;
829
+ align-items: center;
830
+ gap: 8px;
831
+ }
832
+
833
+ .discovered-tag {
834
+ background: rgba(255, 255, 255, 0.05);
835
+ border: 1px solid var(--border-color);
836
+ border-radius: 50px;
837
+ padding: 2px 10px;
838
+ font-size: 0.8rem;
839
+ color: var(--text-primary);
840
+ font-family: var(--font-mono);
841
+ }
842
+
843
+