Arhamjawad896 commited on
Commit
54a9a57
·
1 Parent(s): ecf8af9

Add Dockerfile, YOLO model, and frontend

Browse files
Files changed (4) hide show
  1. Dockerfile +14 -0
  2. main.html +1751 -0
  3. requirements.txt +14 -0
  4. yolo12x.pt +3 -0
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.13
2
+
3
+ RUN useradd -m -u 1000 user
4
+ USER user
5
+ ENV PATH="/home/user/.local/bin:$PATH"
6
+
7
+ WORKDIR /app
8
+
9
+ COPY --chown=user requirements.txt .
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ COPY --chown=user . /app
13
+
14
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
main.html ADDED
@@ -0,0 +1,1751 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>SeeStuff - AI Object Detection</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
8
+ <style>
9
+ :root {
10
+ --primary: #6366f1;
11
+ --primary-dark: #4f46e5;
12
+ --secondary: #10b981;
13
+ --dark: #1f2937;
14
+ --light: #f9fafb;
15
+ --gray: #9ca3af;
16
+ --danger: #ef4444;
17
+ --success: #22c55e;
18
+ --warning: #f59e0b;
19
+ --info: #3b82f6;
20
+ }
21
+
22
+ * {
23
+ margin: 0;
24
+ padding: 0;
25
+ box-sizing: border-box;
26
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
27
+ }
28
+
29
+ body {
30
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
31
+ color: var(--dark);
32
+ min-height: 100vh;
33
+ padding: 20px;
34
+ }
35
+
36
+ .container {
37
+ max-width: 1200px;
38
+ margin: 0 auto;
39
+ background: rgba(255, 255, 255, 0.95);
40
+ border-radius: 20px;
41
+ box-shadow: 0 15px 50px rgba(0, 0, 0, 0.2);
42
+ overflow: hidden;
43
+ }
44
+
45
+ header {
46
+ background: var(--primary);
47
+ color: white;
48
+ padding: 20px;
49
+ text-align: center;
50
+ position: relative;
51
+ }
52
+
53
+ .logo {
54
+ font-size: 2.5rem;
55
+ font-weight: 700;
56
+ margin-bottom: 10px;
57
+ display: flex;
58
+ align-items: center;
59
+ justify-content: center;
60
+ gap: 10px;
61
+ }
62
+
63
+ .tagline {
64
+ font-size: 1.2rem;
65
+ opacity: 0.9;
66
+ }
67
+
68
+ .main-content {
69
+ display: flex;
70
+ flex-direction: column;
71
+ padding: 30px;
72
+ }
73
+
74
+ .upload-section {
75
+ text-align: center;
76
+ margin-bottom: 30px;
77
+ padding: 30px;
78
+ border: 3px dashed var(--primary);
79
+ border-radius: 15px;
80
+ background: rgba(99, 102, 241, 0.05);
81
+ transition: all 0.3s ease;
82
+ }
83
+
84
+ .upload-section:hover {
85
+ background: rgba(99, 102, 241, 0.1);
86
+ border-color: var(--primary-dark);
87
+ }
88
+
89
+ .upload-icon {
90
+ font-size: 4rem;
91
+ color: var(--primary);
92
+ margin-bottom: 15px;
93
+ }
94
+
95
+ .upload-text {
96
+ font-size: 1.2rem;
97
+ margin-bottom: 20px;
98
+ }
99
+
100
+ .file-input {
101
+ display: none;
102
+ }
103
+
104
+ .btn {
105
+ padding: 12px 25px;
106
+ border: none;
107
+ border-radius: 50px;
108
+ font-size: 1rem;
109
+ font-weight: 600;
110
+ cursor: pointer;
111
+ transition: all 0.3s ease;
112
+ display: inline-flex;
113
+ align-items: center;
114
+ justify-content: center;
115
+ gap: 8px;
116
+ }
117
+
118
+ .btn-primary {
119
+ background: var(--primary);
120
+ color: white;
121
+ }
122
+
123
+ .btn-primary:hover {
124
+ background: var(--primary-dark);
125
+ transform: translateY(-2px);
126
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
127
+ }
128
+
129
+ .btn-secondary {
130
+ background: var(--secondary);
131
+ color: white;
132
+ }
133
+
134
+ .btn-secondary:hover {
135
+ background: #0da271;
136
+ transform: translateY(-2px);
137
+ }
138
+
139
+ .btn-outline {
140
+ background: transparent;
141
+ border: 2px solid var(--primary);
142
+ color: var(--primary);
143
+ }
144
+
145
+ .btn-outline:hover {
146
+ background: var(--primary);
147
+ color: white;
148
+ }
149
+
150
+ .controls {
151
+ display: flex;
152
+ justify-content: center;
153
+ gap: 15px;
154
+ margin-bottom: 30px;
155
+ flex-wrap: wrap;
156
+ }
157
+
158
+ .toggle-container {
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 10px;
162
+ background: #f3f4f6;
163
+ padding: 10px 20px;
164
+ border-radius: 50px;
165
+ }
166
+
167
+ .toggle-switch {
168
+ position: relative;
169
+ display: inline-block;
170
+ width: 60px;
171
+ height: 30px;
172
+ }
173
+
174
+ .toggle-switch input {
175
+ opacity: 0;
176
+ width: 0;
177
+ height: 0;
178
+ }
179
+
180
+ .slider {
181
+ position: absolute;
182
+ cursor: pointer;
183
+ top: 0;
184
+ left: 0;
185
+ right: 0;
186
+ bottom: 0;
187
+ background-color: var(--gray);
188
+ transition: .4s;
189
+ border-radius: 34px;
190
+ }
191
+
192
+ .slider:before {
193
+ position: absolute;
194
+ content: "";
195
+ height: 22px;
196
+ width: 22px;
197
+ left: 4px;
198
+ bottom: 4px;
199
+ background-color: white;
200
+ transition: .4s;
201
+ border-radius: 50%;
202
+ }
203
+
204
+ input:checked + .slider {
205
+ background-color: var(--primary);
206
+ }
207
+
208
+ input:checked + .slider:before {
209
+ transform: translateX(30px);
210
+ }
211
+
212
+ .results-section {
213
+ display: none;
214
+ margin-top: 30px;
215
+ }
216
+
217
+ .scan-info {
218
+ display: flex;
219
+ justify-content: space-between;
220
+ align-items: center;
221
+ margin-bottom: 20px;
222
+ padding-bottom: 15px;
223
+ border-bottom: 2px solid #e5e7eb;
224
+ }
225
+
226
+ .scan-id {
227
+ font-weight: 600;
228
+ color: var(--primary);
229
+ }
230
+
231
+ .scan-date {
232
+ color: var(--gray);
233
+ }
234
+
235
+ .results-grid {
236
+ display: grid;
237
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
238
+ gap: 20px;
239
+ margin-bottom: 30px;
240
+ }
241
+
242
+ .result-card {
243
+ background: white;
244
+ border-radius: 15px;
245
+ overflow: hidden;
246
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
247
+ transition: transform 0.3s ease;
248
+ }
249
+
250
+ .result-card:hover {
251
+ transform: translateY(-5px);
252
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
253
+ }
254
+
255
+ .card-header {
256
+ background: var(--primary);
257
+ color: white;
258
+ padding: 15px;
259
+ font-weight: 600;
260
+ display: flex;
261
+ justify-content: space-between;
262
+ align-items: center;
263
+ }
264
+
265
+ .category-badge {
266
+ background: rgba(255, 255, 255, 0.2);
267
+ padding: 4px 10px;
268
+ border-radius: 20px;
269
+ font-size: 0.8rem;
270
+ }
271
+
272
+ .card-body {
273
+ padding: 20px;
274
+ }
275
+
276
+ .card-description {
277
+ margin-bottom: 15px;
278
+ line-height: 1.5;
279
+ }
280
+
281
+ .card-facts {
282
+ background: #f0f9ff;
283
+ padding: 15px;
284
+ border-radius: 10px;
285
+ margin-bottom: 15px;
286
+ border-left: 4px solid var(--info);
287
+ }
288
+
289
+ .fact-title {
290
+ font-weight: 600;
291
+ margin-bottom: 5px;
292
+ color: var(--info);
293
+ }
294
+
295
+ .similar-objects {
296
+ margin-top: 15px;
297
+ }
298
+
299
+ .similar-title {
300
+ font-weight: 600;
301
+ margin-bottom: 8px;
302
+ color: var(--dark);
303
+ }
304
+
305
+ .similar-list {
306
+ display: flex;
307
+ flex-wrap: wrap;
308
+ gap: 8px;
309
+ }
310
+
311
+ .similar-item {
312
+ background: #f3f4f6;
313
+ padding: 5px 12px;
314
+ border-radius: 20px;
315
+ font-size: 0.9rem;
316
+ }
317
+
318
+ .history-section {
319
+ margin-top: 40px;
320
+ display: none;
321
+ }
322
+
323
+ .section-title {
324
+ font-size: 1.5rem;
325
+ margin-bottom: 20px;
326
+ padding-bottom: 10px;
327
+ border-bottom: 2px solid #e5e7eb;
328
+ display: flex;
329
+ align-items: center;
330
+ gap: 10px;
331
+ }
332
+
333
+ .history-grid {
334
+ display: grid;
335
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
336
+ gap: 20px;
337
+ }
338
+
339
+ .history-card {
340
+ background: white;
341
+ border-radius: 15px;
342
+ overflow: hidden;
343
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
344
+ transition: all 0.3s ease;
345
+ cursor: pointer;
346
+ position: relative;
347
+ }
348
+
349
+ .history-card:hover {
350
+ transform: translateY(-5px);
351
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
352
+ }
353
+
354
+ .history-image {
355
+ width: 100%;
356
+ height: 200px;
357
+ object-fit: cover;
358
+ }
359
+
360
+ .history-content {
361
+ padding: 15px;
362
+ }
363
+
364
+ .history-date {
365
+ font-size: 0.9rem;
366
+ color: var(--gray);
367
+ margin-bottom: 10px;
368
+ }
369
+
370
+ .history-objects {
371
+ display: flex;
372
+ flex-wrap: wrap;
373
+ gap: 8px;
374
+ margin-bottom: 15px;
375
+ }
376
+
377
+ .history-object {
378
+ background: #f0f9ff;
379
+ padding: 4px 10px;
380
+ border-radius: 20px;
381
+ font-size: 0.8rem;
382
+ }
383
+
384
+ .history-actions {
385
+ display: flex;
386
+ justify-content: space-between;
387
+ }
388
+
389
+ .favorite-btn {
390
+ color: var(--gray);
391
+ cursor: pointer;
392
+ transition: color 0.3s ease;
393
+ }
394
+
395
+ .favorite-btn.active {
396
+ color: var(--danger);
397
+ }
398
+
399
+ .export-btn {
400
+ color: var(--primary);
401
+ cursor: pointer;
402
+ }
403
+
404
+ .loading {
405
+ display: none;
406
+ text-align: center;
407
+ padding: 40px;
408
+ }
409
+
410
+ .spinner {
411
+ width: 50px;
412
+ height: 50px;
413
+ border: 5px solid rgba(99, 102, 241, 0.2);
414
+ border-radius: 50%;
415
+ border-top-color: var(--primary);
416
+ animation: spin 1s linear infinite;
417
+ margin: 0 auto 20px;
418
+ }
419
+
420
+ @keyframes spin {
421
+ 0% { transform: rotate(0deg); }
422
+ 100% { transform: rotate(360deg); }
423
+ }
424
+
425
+ .error-message {
426
+ background: #fee2e2;
427
+ color: var(--danger);
428
+ padding: 15px;
429
+ border-radius: 10px;
430
+ margin: 20px 0;
431
+ display: none;
432
+ }
433
+
434
+ .success-message {
435
+ background: #d1fae5;
436
+ color: var(--success);
437
+ padding: 15px;
438
+ border-radius: 10px;
439
+ margin: 20px 0;
440
+ display: none;
441
+ }
442
+
443
+ .image-preview {
444
+ max-width: 100%;
445
+ max-height: 300px;
446
+ border-radius: 10px;
447
+ margin: 20px auto;
448
+ display: none;
449
+ }
450
+
451
+ .tab-container {
452
+ display: flex;
453
+ margin-bottom: 20px;
454
+ border-bottom: 2px solid #e5e7eb;
455
+ }
456
+
457
+ .tab {
458
+ padding: 12px 25px;
459
+ cursor: pointer;
460
+ font-weight: 600;
461
+ color: var(--gray);
462
+ border-bottom: 3px solid transparent;
463
+ transition: all 0.3s ease;
464
+ }
465
+
466
+ .tab.active {
467
+ color: var(--primary);
468
+ border-bottom: 3px solid var(--primary);
469
+ }
470
+
471
+ .tab-content {
472
+ display: none;
473
+ }
474
+
475
+ .tab-content.active {
476
+ display: block;
477
+ }
478
+ .ollama-status {
479
+ display: flex;
480
+ align-items: center;
481
+ gap: 8px;
482
+ padding: 10px 15px;
483
+ border-radius: 8px;
484
+ font-size: 0.9rem;
485
+ font-weight: 500;
486
+ }
487
+
488
+ footer {
489
+ text-align: center;
490
+ padding: 20px;
491
+ color: var(--gray);
492
+ font-size: 0.9rem;
493
+ margin-top: 40px;
494
+ }
495
+
496
+ @media (max-width: 768px) {
497
+ .results-grid, .history-grid {
498
+ grid-template-columns: 1fr;
499
+ }
500
+
501
+ .controls {
502
+ flex-direction: column;
503
+ align-items: center;
504
+ }
505
+
506
+ .scan-info {
507
+ flex-direction: column;
508
+ gap: 10px;
509
+ align-items: flex-start;
510
+ }
511
+ }
512
+ /* Add these to your CSS */
513
+ .realtime-controls {
514
+ display: flex;
515
+ gap: 15px;
516
+ margin-bottom: 20px;
517
+ justify-content: center;
518
+ flex-wrap: wrap;
519
+ }
520
+
521
+ .video-container {
522
+ display: flex;
523
+ justify-content: center;
524
+ margin: 20px 0;
525
+ background: #000;
526
+ border-radius: 10px;
527
+ padding: 10px;
528
+ }
529
+
530
+ .objects-list {
531
+ display: flex;
532
+ flex-wrap: wrap;
533
+ gap: 10px;
534
+ margin-top: 15px;
535
+ }
536
+
537
+ .object-pill {
538
+ background: var(--primary);
539
+ color: white;
540
+ padding: 8px 15px;
541
+ border-radius: 20px;
542
+ font-size: 0.9rem;
543
+ font-weight: 500;
544
+ }
545
+ /* Search & Ask Styles */
546
+ .search-controls {
547
+ margin-bottom: 20px;
548
+ }
549
+
550
+ .search-type-toggle {
551
+ display: flex;
552
+ gap: 10px;
553
+ margin-bottom: 20px;
554
+ justify-content: center;
555
+ }
556
+
557
+ .search-input-container {
558
+ display: flex;
559
+ gap: 10px;
560
+ margin-bottom: 20px;
561
+ }
562
+
563
+ #search-query, #object-name, #question-input {
564
+ flex: 1;
565
+ padding: 12px 15px;
566
+ border: 2px solid #e5e7eb;
567
+ border-radius: 50px;
568
+ font-size: 1rem;
569
+ outline: none;
570
+ }
571
+
572
+ #search-query:focus, #object-name:focus, #question-input:focus {
573
+ border-color: var(--primary);
574
+ }
575
+
576
+ .ask-input {
577
+ display: flex;
578
+ gap: 10px;
579
+ margin-bottom: 20px;
580
+ flex-wrap: wrap;
581
+ }
582
+
583
+ .ask-input input {
584
+ min-width: 200px;
585
+ }
586
+
587
+ .object-links .similar-list {
588
+ display: flex;
589
+ flex-wrap: wrap;
590
+ gap: 8px;
591
+ }
592
+
593
+ .object-links .similar-item {
594
+ background: var(--primary);
595
+ color: white;
596
+ padding: 8px 15px;
597
+ border-radius: 20px;
598
+ font-size: 0.9rem;
599
+ text-decoration: none;
600
+ display: inline-flex;
601
+ align-items: center;
602
+ gap: 5px;
603
+ transition: all 0.3s ease;
604
+ }
605
+
606
+ .object-links .similar-item:hover {
607
+ background: var(--primary-dark);
608
+ transform: translateY(-2px);
609
+ }
610
+ </style>
611
+ </head>
612
+ <body>
613
+ <div class="container">
614
+ <header>
615
+ <div class="logo">
616
+ <i class="fas fa-camera"></i>
617
+ SeeStuff
618
+ </div>
619
+ <div class="tagline">AI-Powered Object Detection & Recognition</div>
620
+ </header>
621
+
622
+ <div class="main-content">
623
+ <div class="tab-container">
624
+ <div class="tab active" data-tab="scan">Scan</div>
625
+ <div class="tab" data-tab="history">History</div>
626
+ <div class="tab" data-tab="realtime">Real-Time</div>
627
+ <div class="tab" data-tab="search">Search & Ask</div>
628
+ </div>
629
+
630
+ <div class="tab-content active" id="scan-tab">
631
+ <div class="upload-section" id="drop-zone">
632
+ <i class="fas fa-cloud-upload-alt upload-icon"></i>
633
+ <div class="upload-text">Drag & drop an image or click to browse</div>
634
+ <button class="btn btn-primary" id="browse-btn">
635
+ <i class="fas fa-folder-open"></i> Browse Files
636
+ </button>
637
+ <input type="file" id="file-input" class="file-input" accept="image/*">
638
+ </div>
639
+
640
+ <img id="image-preview" class="image-preview" alt="Image preview">
641
+
642
+ <div class="controls">
643
+ <div class="toggle-container">
644
+ <span>Offline Mode</span>
645
+ <label class="toggle-switch">
646
+ <input type="checkbox" id="offline-toggle">
647
+ <span class="slider"></span>
648
+ </label>
649
+ </div>
650
+ <div id="ollama-status" class="ollama-status" style="display: none; padding: 10px; border-radius: 8px; background: #f0f9ff; margin: 0 10px;">
651
+ <i class="fas fa-microchip"></i>
652
+ <span id="ollama-status-text">Checking Ollama...</span>
653
+ </div>
654
+ <button class="btn btn-secondary" id="scan-btn" disabled>
655
+ <i class="fas fa-search"></i> Scan Image
656
+ </button>
657
+ </div>
658
+
659
+ <div class="error-message" id="error-message"></div>
660
+ <div class="success-message" id="success-message"></div>
661
+
662
+ <div class="loading" id="loading">
663
+ <div class="spinner"></div>
664
+ <div>Analyzing your image with AI...</div>
665
+ </div>
666
+
667
+ <div class="results-section" id="results-section">
668
+ <div class="scan-info">
669
+ <div>
670
+ <div class="scan-id">Scan ID: <span id="scan-id"></span></div>
671
+ <div class="scan-date">Scanned on: <span id="scan-date"></span></div>
672
+ </div>
673
+ <div>
674
+ <button class="btn btn-outline" id="export-json-btn" onclick="exportScan(currentScanId, 'json')">
675
+ <i class="fas fa-file-export"></i> Export JSON
676
+ </button>
677
+ <button class="btn btn-outline" id="export-csv-btn" onclick="exportScan(currentScanId, 'csv')">
678
+ <i class="fas fa-file-csv"></i> Export CSV
679
+ </button>
680
+ </div>
681
+ </div>
682
+
683
+ <div class="results-grid" id="results-grid">
684
+ <!-- Results will be populated here -->
685
+ </div>
686
+ </div>
687
+ </div>
688
+
689
+ <div class="tab-content" id="history-tab">
690
+ <div class="history-section" id="history-section">
691
+ <h2 class="section-title">
692
+ <i class="fas fa-history"></i> Scan History
693
+ </h2>
694
+ <div class="history-grid" id="history-grid">
695
+ <!-- History items will be populated here -->
696
+ </div>
697
+ </div>
698
+ </div>
699
+ <!-- Add this after the history-tab div -->
700
+ <div class="tab-content" id="realtime-tab">
701
+ <div class="realtime-section">
702
+ <h2 class="section-title">
703
+ <i class="fas fa-video"></i> Real-Time Object Detection
704
+ </h2>
705
+
706
+ <div class="realtime-controls">
707
+ <button class="btn btn-primary" id="start-camera-btn">
708
+ <i class="fas fa-play"></i> Start Camera
709
+ </button>
710
+ <button class="btn btn-secondary" id="stop-camera-btn" disabled>
711
+ <i class="fas fa-stop"></i> Stop Camera
712
+ </button>
713
+ <button class="btn btn-success" id="capture-frame-btn" disabled>
714
+ <i class="fas fa-camera"></i> Capture & Analyze
715
+ </button>
716
+ </div>
717
+
718
+ <div class="video-container">
719
+ <video id="webcam-video" autoplay playsinline muted style="display: none; width: 100%; max-width: 640px; border-radius: 10px;"></video>
720
+ <canvas id="webcam-canvas" style="display: none; width: 100%; max-width: 640px; border-radius: 10px;"></canvas>
721
+ </div>
722
+
723
+ <div class="realtime-results">
724
+ <h3>Detected Objects:</h3>
725
+ <div id="detected-objects-list" class="objects-list">
726
+ <!-- Objects will be populated here -->
727
+ </div>
728
+ </div>
729
+
730
+ <div class="loading" id="realtime-loading" style="display: none;">
731
+ <div class="spinner"></div>
732
+ <div>Processing frame...</div>
733
+ </div>
734
+ </div>
735
+ </div>
736
+ <div class="tab-content" id="search-tab">
737
+ <div class="search-ask-section">
738
+ <h2 class="section-title">
739
+ <i class="fas fa-search"></i> Search & Ask Anything
740
+ </h2>
741
+
742
+ <div class="search-controls">
743
+ <div class="search-type-toggle">
744
+ <button class="btn btn-primary active" data-search-type="text">
745
+ <i class="fas fa-font"></i> Text Search
746
+ </button>
747
+ <button class="btn btn-outline" data-search-type="image">
748
+ <i class="fas fa-image"></i> Image Search
749
+ </button>
750
+ </div>
751
+
752
+ <div class="search-input-container" id="text-search">
753
+ <input type="text" id="search-query" placeholder="Search for objects (e.g., 'red book', 'electronics')">
754
+ <button class="btn btn-secondary" id="search-btn">
755
+ <i class="fas fa-search"></i> Search
756
+ </button>
757
+ </div>
758
+
759
+ <div class="search-input-container" id="image-search" style="display: none;">
760
+ <div class="upload-section" id="search-drop-zone" style="margin: 0;">
761
+ <i class="fas fa-cloud-upload-alt upload-icon"></i>
762
+ <div class="upload-text">Drag & drop an image to search</div>
763
+ <button class="btn btn-outline" id="search-browse-btn">
764
+ <i class="fas fa-folder-open"></i> Browse
765
+ </button>
766
+ <input type="file" id="search-file-input" class="file-input" accept="image/*">
767
+ </div>
768
+ </div>
769
+ </div>
770
+
771
+ <div class="ask-container" style="margin-top: 30px;">
772
+ <h3 class="section-title">
773
+ <i class="fas fa-question-circle"></i> Ask About Any Object
774
+ </h3>
775
+ <div class="ask-input">
776
+ <input type="text" id="object-name" placeholder="Object name (e.g., 'coffee mug')">
777
+ <input type="text" id="question-input" placeholder="Your question (e.g., 'How to clean this?')">
778
+ <button class="btn btn-secondary" id="ask-btn">
779
+ <i class="fas fa-paper-plane"></i> Ask
780
+ </button>
781
+ </div>
782
+ <div class="answer-container" id="answer-container" style="display: none;">
783
+ <div class="card-facts">
784
+ <div class="fact-title">Answer:</div>
785
+ <p id="ai-answer"></p>
786
+ </div>
787
+ </div>
788
+ </div>
789
+
790
+ <div class="search-results" id="search-results" style="margin-top: 30px; display: none;">
791
+ <h3 class="section-title">Search Results</h3>
792
+ <div class="results-grid" id="search-results-grid">
793
+ <!-- Search results will appear here -->
794
+ </div>
795
+ </div>
796
+ </div>
797
+ </div>
798
+ </div>
799
+
800
+ <footer>
801
+ <p>SeeStuff MVP - AI Object Detection powered by YOLOv8, CLIP, and Groq LLM</p>
802
+ </footer>
803
+ </div>
804
+
805
+ <script>
806
+ // DOM Elements
807
+ const API_BASE_URL = 'http://localhost:8000';
808
+ const startCameraBtn = document.getElementById('start-camera-btn');
809
+ const stopCameraBtn = document.getElementById('stop-camera-btn');
810
+ const captureFrameBtn = document.getElementById('capture-frame-btn');
811
+ const webcamVideo = document.getElementById('webcam-video');
812
+ const webcamCanvas = document.getElementById('webcam-canvas');
813
+ const detectedObjectsList = document.getElementById('detected-objects-list');
814
+ const realtimeLoading = document.getElementById('realtime-loading');
815
+ const fileInput = document.getElementById('file-input');
816
+ const browseBtn = document.getElementById('browse-btn');
817
+ const dropZone = document.getElementById('drop-zone');
818
+ const scanBtn = document.getElementById('scan-btn');
819
+ const offlineToggle = document.getElementById('offline-toggle');
820
+ const imagePreview = document.getElementById('image-preview');
821
+ const loading = document.getElementById('loading');
822
+ const errorMessage = document.getElementById('error-message');
823
+ const successMessage = document.getElementById('success-message');
824
+ const resultsSection = document.getElementById('results-section');
825
+ const resultsGrid = document.getElementById('results-grid');
826
+ const scanIdElement = document.getElementById('scan-id');
827
+ const scanDateElement = document.getElementById('scan-date');
828
+ const historySection = document.getElementById('history-section');
829
+ const historyGrid = document.getElementById('history-grid');
830
+ const tabs = document.querySelectorAll('.tab');
831
+ const tabContents = document.querySelectorAll('.tab-content');
832
+
833
+ // State
834
+ let currentFile = null;
835
+ let currentScanId = null;
836
+ let offlineMode = false;
837
+
838
+ // Event Listeners
839
+ browseBtn.addEventListener('click', () => fileInput.click());
840
+ fileInput.addEventListener('change', handleFileSelect);
841
+ dropZone.addEventListener('dragover', handleDragOver);
842
+ dropZone.addEventListener('drop', handleDrop);
843
+ scanBtn.addEventListener('click', handleScan);
844
+ offlineToggle.addEventListener('change', toggleOfflineMode);
845
+ startCameraBtn.addEventListener('click', startCamera);
846
+ stopCameraBtn.addEventListener('click', stopCamera);
847
+ captureFrameBtn.addEventListener('click', captureAndAnalyzeFrame);
848
+
849
+ // Tab switching
850
+ tabs.forEach(tab => {
851
+ tab.addEventListener('click', () => {
852
+ const tabId = tab.getAttribute('data-tab');
853
+
854
+ // Update active tab
855
+ tabs.forEach(t => t.classList.remove('active'));
856
+ tab.classList.add('active');
857
+
858
+ // Show corresponding content
859
+ tabContents.forEach(content => {
860
+ content.classList.remove('active');
861
+ if (content.id === `${tabId}-tab`) {
862
+ content.classList.add('active');
863
+ }
864
+ });
865
+
866
+ // Load history if history tab is selected
867
+ if (tabId === 'history') {
868
+ loadHistory();
869
+ }
870
+ });
871
+ });
872
+
873
+ // Functions
874
+ function handleDragOver(e) {
875
+ e.preventDefault();
876
+ e.stopPropagation();
877
+ dropZone.style.borderColor = '#6366f1';
878
+ dropZone.style.backgroundColor = 'rgba(99, 102, 241, 0.1)';
879
+ }
880
+
881
+ function handleDrop(e) {
882
+ e.preventDefault();
883
+ e.stopPropagation();
884
+ dropZone.style.borderColor = '#6366f1';
885
+ dropZone.style.backgroundColor = 'rgba(99, 102, 241, 0.05)';
886
+
887
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
888
+ handleFile(e.dataTransfer.files[0]);
889
+ }
890
+ }
891
+
892
+ function handleFileSelect(e) {
893
+ if (e.target.files && e.target.files[0]) {
894
+ handleFile(e.target.files[0]);
895
+ }
896
+ }
897
+
898
+ function handleFile(file) {
899
+ if (!file.type.match('image.*')) {
900
+ showError('Please select an image file');
901
+ return;
902
+ }
903
+
904
+ currentFile = file;
905
+ scanBtn.disabled = false;
906
+
907
+ // Preview image
908
+ const reader = new FileReader();
909
+ reader.onload = function(e) {
910
+ imagePreview.src = e.target.result;
911
+ imagePreview.style.display = 'block';
912
+ };
913
+ reader.readAsDataURL(file);
914
+
915
+ hideError();
916
+ showSuccess('Image selected. Click "Scan Image" to analyze.');
917
+ }
918
+
919
+ async function toggleOfflineMode() {
920
+ offlineMode = offlineToggle.checked;
921
+ try {
922
+ const response = await fetch(`${API_BASE_URL}/offline_toggle?mode=${offlineMode}`);
923
+ const data = await response.json();
924
+
925
+ let message = `Offline mode: ${data.offline_mode ? 'ON' : 'OFF'}`;
926
+
927
+ // Show Ollama status if in offline mode
928
+ if (data.offline_mode) {
929
+ const ollamaStatus = document.getElementById('ollama-status');
930
+ const ollamaStatusText = document.getElementById('ollama-status-text');
931
+
932
+ if (data.ollama_available === false) {
933
+ ollamaStatus.style.display = 'flex';
934
+ ollamaStatus.style.background = '#fef3c7';
935
+ ollamaStatusText.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Ollama not available';
936
+ message += ' (Using basic descriptions)';
937
+ } else if (data.ollama_available === true) {
938
+ ollamaStatus.style.display = 'flex';
939
+ ollamaStatus.style.background = '#d1fae5';
940
+ ollamaStatusText.innerHTML = '<i class="fas fa-check-circle"></i> Ollama available';
941
+ message += ' (Using Ollama)';
942
+ }
943
+ } else {
944
+ document.getElementById('ollama-status').style.display = 'none';
945
+ }
946
+
947
+ showSuccess(message);
948
+ } catch (error) {
949
+ showError('Failed to toggle offline mode');
950
+ console.error(error);
951
+ }
952
+ }
953
+ async function checkOllamaStatus() {
954
+ try {
955
+ const response = await fetch(`${API_BASE_URL}/ollama_status`);
956
+ const data = await response.json();
957
+
958
+ const ollamaStatus = document.getElementById('ollama-status');
959
+ const ollamaStatusText = document.getElementById('ollama-status-text');
960
+
961
+ if (data.ollama_available) {
962
+ ollamaStatus.style.display = 'flex';
963
+ ollamaStatus.style.background = '#d1fae5';
964
+ ollamaStatusText.innerHTML = `<i class="fas fa-check-circle"></i> Ollama: ${data.recommended_model || 'Available'}`;
965
+ } else {
966
+ ollamaStatus.style.display = 'flex';
967
+ ollamaStatus.style.background = '#fef3c7';
968
+ ollamaStatusText.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Ollama not installed';
969
+ }
970
+ } catch (error) {
971
+ console.error('Failed to check Ollama status:', error);
972
+ }
973
+ }
974
+
975
+ async function handleScan() {
976
+ if (!currentFile) {
977
+ showError('Please select an image first');
978
+ return;
979
+ }
980
+
981
+ loading.style.display = 'block';
982
+ resultsSection.style.display = 'none';
983
+ hideError();
984
+ hideSuccess();
985
+
986
+ const formData = new FormData();
987
+ formData.append('file', currentFile);
988
+
989
+ try {
990
+ const response = await fetch(`${API_BASE_URL}/scan`, {
991
+ method: 'POST',
992
+ body: formData
993
+ });
994
+
995
+ if (!response.ok) {
996
+ throw new Error(`Server returned ${response.status}: ${response.statusText}`);
997
+ }
998
+
999
+ const data = await response.json();
1000
+ displayResults(data);
1001
+ currentScanId = data.scan_id;
1002
+
1003
+ // Update history if we're on the history tab
1004
+ if (document.querySelector('[data-tab="history"]').classList.contains('active')) {
1005
+ loadHistory();
1006
+ }
1007
+ } catch (error) {
1008
+ showError('Scan failed: ' + error.message);
1009
+ console.error(error);
1010
+ } finally {
1011
+ loading.style.display = 'none';
1012
+ }
1013
+ }
1014
+ async function deleteScan(scanId, element) {
1015
+ if (!confirm('Are you sure you want to delete this scan?')) return;
1016
+
1017
+ try {
1018
+ const response = await fetch(`${API_BASE_URL}/scan/${scanId}`, {
1019
+ method: 'DELETE'
1020
+ });
1021
+
1022
+ if (!response.ok) {
1023
+ throw new Error(`Server returned ${response.status}: ${response.statusText}`);
1024
+ }
1025
+
1026
+ // Remove the card from UI
1027
+ element.closest('.history-card').remove();
1028
+ showSuccess('Scan deleted successfully');
1029
+ } catch (error) {
1030
+ showError('Delete failed: ' + error.message);
1031
+ console.error(error);
1032
+ }
1033
+ }
1034
+
1035
+ function displayResults(data) {
1036
+ resultsSection.style.display = 'block';
1037
+ scanIdElement.textContent = data.scan_id;
1038
+ scanDateElement.textContent = new Date(data.timestamp).toLocaleString();
1039
+
1040
+ // Clear previous results
1041
+ resultsGrid.innerHTML = '';
1042
+
1043
+ // Display each detected object
1044
+ data.objects.forEach(obj => {
1045
+ const card = document.createElement('div');
1046
+ card.className = 'result-card';
1047
+
1048
+ // Generate links HTML if available
1049
+ let linksHTML = '';
1050
+ if (obj.links) {
1051
+ linksHTML = `
1052
+ <div class="object-links" style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #e5e7eb;">
1053
+ <div class="similar-title">Learn more:</div>
1054
+ <div class="similar-list">
1055
+ ${obj.links.wikipedia ? `<a href="${obj.links.wikipedia}" target="_blank" class="similar-item"><i class="fab fa-wikipedia-w"></i> Wikipedia</a>` : ''}
1056
+ ${obj.links.amazon ? `<a href="${obj.links.amazon}" target="_blank" class="similar-item"><i class="fab fa-amazon"></i> Buy</a>` : ''}
1057
+ ${obj.links.youtube ? `<a href="${obj.links.youtube}" target="_blank" class="similar-item"><i class="fab fa-youtube"></i> Videos</a>` : ''}
1058
+ ${obj.links.tutorial ? `<a href="${obj.links.tutorial}" target="_blank" class="similar-item"><i class="fas fa-graduation-cap"></i> Tutorial</a>` : ''}
1059
+ ${obj.links.manual ? `<a href="${obj.links.manual}" target="_blank" class="similar-item"><i class="fas fa-book"></i> Manual</a>` : ''}
1060
+ </div>
1061
+ </div>
1062
+ `;
1063
+ }
1064
+
1065
+ card.innerHTML = `
1066
+ <div class="card-header">
1067
+ ${obj.name}
1068
+ <span class="category-badge">${obj.category}</span>
1069
+ </div>
1070
+ <div class="card-body">
1071
+ <div class="card-description">${obj.description}</div>
1072
+ ${obj.facts ? `
1073
+ <div class="card-facts">
1074
+ <div class="fact-title">Did you know?</div>
1075
+ ${obj.facts}
1076
+ </div>
1077
+ ` : ''}
1078
+ ${obj.similar_objects && obj.similar_objects.length > 0 ? `
1079
+ <div class="similar-objects">
1080
+ <div class="similar-title">Similar objects:</div>
1081
+ <div class="similar-list">
1082
+ ${obj.similar_objects.map(item => `<span class="similar-item">${item}</span>`).join('')}
1083
+ </div>
1084
+ </div>
1085
+ ` : ''}
1086
+ ${linksHTML}
1087
+ </div>
1088
+ `;
1089
+
1090
+ resultsGrid.appendChild(card);
1091
+ });
1092
+
1093
+ showSuccess('Scan completed successfully!');
1094
+ }
1095
+
1096
+ async function loadHistory() {
1097
+ try {
1098
+ const response = await fetch(`${API_BASE_URL}/history`);
1099
+ if (!response.ok) {
1100
+ throw new Error(`Server returned ${response.status}: ${response.statusText}`);
1101
+ }
1102
+
1103
+ const data = await response.json();
1104
+ displayHistory(data);
1105
+ } catch (error) {
1106
+ showError('Failed to load history: ' + error.message);
1107
+ console.error(error);
1108
+ }
1109
+ }
1110
+
1111
+ function displayHistory(scans) {
1112
+ historyGrid.innerHTML = '';
1113
+
1114
+ if (scans.length === 0) {
1115
+ historyGrid.innerHTML = '<p>No scan history yet. Upload an image to get started!</p>';
1116
+ return;
1117
+ }
1118
+
1119
+ scans.forEach(scan => {
1120
+ const card = document.createElement('div');
1121
+ card.className = 'history-card';
1122
+ card.addEventListener('click', () => viewScanDetails(scan.scan_id));
1123
+
1124
+ // Format date
1125
+ const scanDate = new Date(scan.timestamp);
1126
+ const formattedDate = scanDate.toLocaleDateString() + ' ' + scanDate.toLocaleTimeString();
1127
+
1128
+ // Get first 5 objects for preview
1129
+ const previewObjects = scan.objects.slice(0, 5);
1130
+
1131
+ card.innerHTML = `
1132
+ <img src="${API_BASE_URL}/image/${scan.scan_id}" alt="Scan image" class="history-image"> <div class="history-content">
1133
+ <div class="history-date">${formattedDate}</div>
1134
+ <div class="history-objects">
1135
+ ${previewObjects.map(obj => `<span class="history-object">${obj.name}</span>`).join('')}
1136
+ ${scan.objects.length > 5 ? `<span class="history-object">+${scan.objects.length - 5} more</span>` : ''}
1137
+ </div>
1138
+ <div class="history-actions">
1139
+ <i class="fas fa-heart favorite-btn ${scan.favorite ? 'active' : ''}"
1140
+ data-scan-id="${scan.scan_id}"
1141
+ onclick="event.stopPropagation(); toggleFavorite('${scan.scan_id}', this)"></i>
1142
+ <i class="fas fa-download export-btn"
1143
+ onclick="event.stopPropagation(); exportScan('${scan.scan_id}', 'json')"></i>
1144
+ <i class="fas fa-trash delete-btn"
1145
+ onclick="event.stopPropagation(); deleteScan('${scan.scan_id}', this)"></i>
1146
+ </div>
1147
+ </div>
1148
+ `;
1149
+
1150
+ historyGrid.appendChild(card);
1151
+ });
1152
+
1153
+ historySection.style.display = 'block';
1154
+ }
1155
+
1156
+ async function viewScanDetails(scanId) {
1157
+ try {
1158
+ const response = await fetch(`${API_BASE_URL}/scan/${scanId}`);
1159
+ if (!response.ok) {
1160
+ throw new Error(`Server returned ${response.status}: ${response.statusText}`);
1161
+ }
1162
+
1163
+ const data = await response.json();
1164
+
1165
+ // Switch to scan tab
1166
+ document.querySelector('[data-tab="scan"]').click();
1167
+
1168
+ // Display the results
1169
+ displayResults(data);
1170
+ currentScanId = scanId;
1171
+ } catch (error) {
1172
+ showError('Failed to load scan details: ' + error.message);
1173
+ console.error(error);
1174
+ }
1175
+ }
1176
+
1177
+ async function toggleFavorite(scanId, element) {
1178
+ try {
1179
+ const currentlyFavorite = element.classList.contains('active');
1180
+ const response = await fetch(`${API_BASE_URL}/favorite/${scanId}?favorite=${!currentlyFavorite}`, {
1181
+ method: 'POST'
1182
+ });
1183
+
1184
+ if (!response.ok) {
1185
+ throw new Error(`Server returned ${response.status}: ${response.statusText}`);
1186
+ }
1187
+
1188
+ // The response now returns JSON with favorite status
1189
+ const data = await response.json();
1190
+ if (data.favorite) {
1191
+ element.classList.add('active');
1192
+ showSuccess('Scan added to favorites');
1193
+ } else {
1194
+ element.classList.remove('active');
1195
+ showSuccess('Scan removed from favorites');
1196
+ }
1197
+ } catch (error) {
1198
+ showError('Failed to update favorite: ' + error.message);
1199
+ console.error(error);
1200
+ }
1201
+ }
1202
+
1203
+ async function exportScan(scanId, format = 'json') {
1204
+ if (!scanId) {
1205
+ showError('No scan selected for export');
1206
+ return;
1207
+ }
1208
+
1209
+ try {
1210
+ const url = `${API_BASE_URL}/export?scan_id=${scanId}&format=${format}`;
1211
+ const response = await fetch(url);
1212
+ if (!response.ok) {
1213
+ throw new Error(`Server returned ${response.status}: ${response.statusText}`);
1214
+ }
1215
+
1216
+ const blob = await response.blob();
1217
+ const downloadUrl = window.URL.createObjectURL(blob);
1218
+ const a = document.createElement('a');
1219
+ a.href = downloadUrl;
1220
+ a.download = `scan-${scanId}.${format}`;
1221
+ document.body.appendChild(a);
1222
+ a.click();
1223
+ document.body.removeChild(a);
1224
+ window.URL.revokeObjectURL(downloadUrl);
1225
+
1226
+ showSuccess(`Exported as ${format.toUpperCase()}`);
1227
+ } catch (error) {
1228
+ showError('Export failed: ' + error.message);
1229
+ console.error(error);
1230
+ }
1231
+ }
1232
+ function showError(message) {
1233
+ errorMessage.textContent = message;
1234
+ errorMessage.style.display = 'block';
1235
+ setTimeout(hideError, 5000);
1236
+ }
1237
+
1238
+ function hideError() {
1239
+ errorMessage.style.display = 'none';
1240
+ }
1241
+
1242
+ function showSuccess(message) {
1243
+ successMessage.textContent = message;
1244
+ successMessage.style.display = 'block';
1245
+ setTimeout(hideSuccess, 5000);
1246
+ }
1247
+
1248
+ function hideSuccess() {
1249
+ successMessage.style.display = 'none';
1250
+ }
1251
+ let ws = null;
1252
+ let stream = null;
1253
+ let isCameraActive = false;
1254
+
1255
+
1256
+ // Modify the startCamera function to handle errors better
1257
+ async function startCamera() {
1258
+ try {
1259
+ // First check if backend can access camera
1260
+ const cameraAvailable = await checkCameraStatus();
1261
+ if (!cameraAvailable) {
1262
+ showError('No camera available or camera in use by another application');
1263
+ return;
1264
+ }
1265
+
1266
+ // Try to access frontend camera
1267
+ stream = await navigator.mediaDevices.getUserMedia({
1268
+ video: { width: 640, height: 480 }
1269
+ });
1270
+
1271
+ webcamVideo.srcObject = stream;
1272
+ webcamVideo.style.display = 'block';
1273
+
1274
+ // Connect to WebSocket
1275
+ connectWebSocket();
1276
+
1277
+ startCameraBtn.disabled = true;
1278
+ stopCameraBtn.disabled = false;
1279
+ captureFrameBtn.disabled = false;
1280
+ isCameraActive = true;
1281
+
1282
+ showSuccess('Camera started successfully');
1283
+ } catch (error) {
1284
+ showError('Failed to access camera: ' + error.message);
1285
+ console.error('Camera error:', error);
1286
+
1287
+ // Check if it's a permission error
1288
+ if (error.name === 'NotAllowedError') {
1289
+ showError('Camera permission denied. Please allow camera access in your browser settings.');
1290
+ } else if (error.name === 'NotFoundError') {
1291
+ showError('No camera found. Please check if your camera is connected properly.');
1292
+ }
1293
+ }
1294
+ }
1295
+
1296
+
1297
+ function stopCamera() {
1298
+ if (stream) {
1299
+ stream.getTracks().forEach(track => track.stop());
1300
+ stream = null;
1301
+ }
1302
+
1303
+ if (ws) {
1304
+ ws.close();
1305
+ ws = null;
1306
+ }
1307
+
1308
+ webcamVideo.style.display = 'none';
1309
+ webcamCanvas.style.display = 'none';
1310
+ detectedObjectsList.innerHTML = '';
1311
+
1312
+ startCameraBtn.disabled = false;
1313
+ stopCameraBtn.disabled = true;
1314
+ captureFrameBtn.disabled = true;
1315
+ isCameraActive = false;
1316
+
1317
+ showSuccess('Camera stopped');
1318
+ }
1319
+
1320
+ function connectWebSocket() {
1321
+ ws = new WebSocket(`ws://localhost:8001/ws/realtime`);
1322
+
1323
+ ws.onopen = () => {
1324
+ console.log('WebSocket connected');
1325
+ showSuccess('Real-time detection connected');
1326
+ };
1327
+
1328
+ ws.onmessage = (event) => {
1329
+ const data = JSON.parse(event.data);
1330
+ updateDetectedObjects(data.objects);
1331
+
1332
+ // If you want to display the processed frame
1333
+ if (data.frame) {
1334
+ displayProcessedFrame(data.frame);
1335
+ }
1336
+ };
1337
+
1338
+ ws.onclose = () => {
1339
+ console.log('WebSocket disconnected');
1340
+ if (isCameraActive) {
1341
+ showError('Real-time connection lost');
1342
+ }
1343
+ };
1344
+
1345
+ ws.onerror = (error) => {
1346
+ console.error('WebSocket error:', error);
1347
+ showError('Real-time connection error');
1348
+ };
1349
+ }
1350
+
1351
+ function updateDetectedObjects(objects) {
1352
+ detectedObjectsList.innerHTML = '';
1353
+
1354
+ if (objects.length === 0) {
1355
+ detectedObjectsList.innerHTML = '<p>No objects detected</p>';
1356
+ return;
1357
+ }
1358
+
1359
+ objects.forEach(obj => {
1360
+ const pill = document.createElement('div');
1361
+ pill.className = 'object-pill';
1362
+ pill.textContent = obj;
1363
+ detectedObjectsList.appendChild(pill);
1364
+ });
1365
+ }
1366
+
1367
+ function displayProcessedFrame(frameData) {
1368
+ const img = new Image();
1369
+ img.onload = () => {
1370
+ const ctx = webcamCanvas.getContext('2d');
1371
+ webcamCanvas.width = img.width;
1372
+ webcamCanvas.height = img.height;
1373
+ ctx.drawImage(img, 0, 0);
1374
+ webcamCanvas.style.display = 'block';
1375
+ };
1376
+ img.src = 'data:image/jpeg;base64,' + frameData;
1377
+ }
1378
+
1379
+ // Fallback capture method
1380
+ async function captureFallback() {
1381
+ try {
1382
+ // Capture from the video element
1383
+ const canvas = document.createElement('canvas');
1384
+ const ctx = canvas.getContext('2d');
1385
+ canvas.width = webcamVideo.videoWidth;
1386
+ canvas.height = webcamVideo.videoHeight;
1387
+ ctx.drawImage(webcamVideo, 0, 0, canvas.width, canvas.height);
1388
+
1389
+ // Convert to blob and send as form data
1390
+ canvas.toBlob(async (blob) => {
1391
+ const formData = new FormData();
1392
+ formData.append('file', blob, 'capture.jpg');
1393
+
1394
+ const response = await fetch(`${API_BASE_URL}/scan`, {
1395
+ method: 'POST',
1396
+ body: formData
1397
+ });
1398
+
1399
+ if (!response.ok) {
1400
+ throw new Error(`Server returned ${response.status}: ${response.statusText}`);
1401
+ }
1402
+
1403
+ const data = await response.json();
1404
+
1405
+ // Switch to scan tab and show results
1406
+ document.querySelector('[data-tab="scan"]').click();
1407
+ displayResults(data);
1408
+ currentScanId = data.scan_id;
1409
+
1410
+ showSuccess('Frame captured and analyzed successfully!');
1411
+ }, 'image/jpeg', 0.8);
1412
+
1413
+ } catch (error) {
1414
+ showError('Fallback capture also failed: ' + error.message);
1415
+ console.error(error);
1416
+ }
1417
+ }
1418
+
1419
+ async function captureAndAnalyzeFrame() {
1420
+ if (!isCameraActive) {
1421
+ showError('Please start the camera first');
1422
+ return;
1423
+ }
1424
+
1425
+ realtimeLoading.style.display = 'block';
1426
+
1427
+ try {
1428
+ const response = await fetch(`${API_BASE_URL}/realtime/capture`, {
1429
+ method: 'POST'
1430
+ });
1431
+
1432
+ if (!response.ok) {
1433
+ // If capture fails, try with a different endpoint as fallback
1434
+ await captureFallback();
1435
+ return;
1436
+ }
1437
+
1438
+ const data = await response.json();
1439
+
1440
+ // Switch to scan tab and show results
1441
+ document.querySelector('[data-tab="scan"]').click();
1442
+ displayResults(data);
1443
+ currentScanId = data.scan_id;
1444
+
1445
+ showSuccess('Frame captured and analyzed successfully!');
1446
+ } catch (error) {
1447
+ showError('Capture failed: ' + error.message);
1448
+ console.error(error);
1449
+ } finally {
1450
+ realtimeLoading.style.display = 'none';
1451
+ }
1452
+ }
1453
+
1454
+ async function checkCameraStatus() {
1455
+ try {
1456
+ const response = await fetch(`${API_BASE_URL}/camera_status`);
1457
+ const data = await response.json();
1458
+
1459
+ const realtimeTab = document.querySelector('[data-tab="realtime"]');
1460
+
1461
+ if (data.cameras_available) {
1462
+ realtimeTab.style.opacity = "1";
1463
+ realtimeTab.style.cursor = "pointer";
1464
+ showSuccess(`Camera detected: ${data.message}`);
1465
+ } else {
1466
+ realtimeTab.style.opacity = "0.5";
1467
+ realtimeTab.style.cursor = "not-allowed";
1468
+ showError('No camera detected. Real-time features disabled.');
1469
+ }
1470
+
1471
+ return data.cameras_available;
1472
+ } catch (error) {
1473
+ console.error('Failed to check camera status:', error);
1474
+ return false;
1475
+ }
1476
+ }
1477
+ // Search and Ask functionality
1478
+ document.getElementById('search-btn').addEventListener('click', performTextSearch);
1479
+ document.querySelectorAll('[data-search-type]').forEach(btn => {
1480
+ btn.addEventListener('click', (e) => {
1481
+ const searchType = e.target.getAttribute('data-search-type');
1482
+
1483
+ // Update active button
1484
+ document.querySelectorAll('[data-search-type]').forEach(b => {
1485
+ b.classList.remove('active');
1486
+ b.classList.add('btn-outline');
1487
+ });
1488
+ e.target.classList.add('active');
1489
+ e.target.classList.remove('btn-outline');
1490
+ e.target.classList.add('btn-primary');
1491
+
1492
+ // Show appropriate search input
1493
+ document.getElementById('text-search').style.display =
1494
+ searchType === 'text' ? 'flex' : 'none';
1495
+ document.getElementById('image-search').style.display =
1496
+ searchType === 'image' ? 'flex' : 'none';
1497
+ });
1498
+ });
1499
+
1500
+ document.getElementById('ask-btn').addEventListener('click', askQuestion);
1501
+ document.getElementById('search-browse-btn').addEventListener('click', () => {
1502
+ document.getElementById('search-file-input').click();
1503
+ });
1504
+ document.getElementById('search-file-input').addEventListener('change', handleSearchImage);
1505
+
1506
+ // Handle drag and drop for search image
1507
+ const searchDropZone = document.getElementById('search-drop-zone');
1508
+ searchDropZone.addEventListener('dragover', handleDragOver);
1509
+ searchDropZone.addEventListener('drop', (e) => {
1510
+ e.preventDefault();
1511
+ e.stopPropagation();
1512
+ searchDropZone.style.borderColor = '#6366f1';
1513
+ searchDropZone.style.backgroundColor = 'rgba(99, 102, 241, 0.05)';
1514
+
1515
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
1516
+ handleSearchImageFile(e.dataTransfer.files[0]);
1517
+ }
1518
+ });
1519
+
1520
+ async function performTextSearch() {
1521
+ const query = document.getElementById('search-query').value.trim();
1522
+
1523
+ if (!query || query.length < 2) {
1524
+ showError('Please enter at least 2 characters to search');
1525
+ return;
1526
+ }
1527
+
1528
+ try {
1529
+ const response = await fetch(`${API_BASE_URL}/search`, {
1530
+ method: 'POST',
1531
+ headers: {
1532
+ 'Content-Type': 'application/json'
1533
+ },
1534
+ body: JSON.stringify({ query })
1535
+ });
1536
+
1537
+ if (!response.ok) {
1538
+ throw new Error(`Server returned ${response.status}: ${response.statusText}`);
1539
+ }
1540
+
1541
+ const results = await response.json();
1542
+ displaySearchResults(results);
1543
+ } catch (error) {
1544
+ showError('Search failed: ' + error.message);
1545
+ console.error(error);
1546
+ }
1547
+ }
1548
+
1549
+ function handleSearchImage(e) {
1550
+ if (e.target.files && e.target.files[0]) {
1551
+ handleSearchImageFile(e.target.files[0]);
1552
+ }
1553
+ }
1554
+
1555
+ function handleSearchImageFile(file) {
1556
+ if (!file.type.match('image.*')) {
1557
+ showError('Please select an image file');
1558
+ return;
1559
+ }
1560
+
1561
+ // Preview image
1562
+ const reader = new FileReader();
1563
+ reader.onload = function(e) {
1564
+ // You could show a small preview here if desired
1565
+ };
1566
+ reader.readAsDataURL(file);
1567
+
1568
+ // Perform the search
1569
+ performImageSearch(file);
1570
+ }
1571
+
1572
+ async function performImageSearch(file) {
1573
+ const formData = new FormData();
1574
+ formData.append('file', file);
1575
+
1576
+ try {
1577
+ const response = await fetch(`${API_BASE_URL}/search/image`, {
1578
+ method: 'POST',
1579
+ body: formData
1580
+ });
1581
+
1582
+ if (!response.ok) {
1583
+ throw new Error(`Server returned ${response.status}: ${response.statusText}`);
1584
+ }
1585
+
1586
+ const result = await response.json();
1587
+
1588
+ if (result.message && result.message.includes('No matching')) {
1589
+ showError('No matching image found in your history');
1590
+ document.getElementById('search-results').style.display = 'none';
1591
+ } else {
1592
+ // Switch to scan tab and show the result
1593
+ document.querySelector('[data-tab="scan"]').click();
1594
+ displayResults(result);
1595
+ currentScanId = result.scan_id;
1596
+ }
1597
+ } catch (error) {
1598
+ showError('Image search failed: ' + error.message);
1599
+ console.error(error);
1600
+ }
1601
+ }
1602
+
1603
+ async function askQuestion() {
1604
+ const objectName = document.getElementById('object-name').value.trim();
1605
+ const question = document.getElementById('question-input').value.trim();
1606
+
1607
+ if (!objectName || !question) {
1608
+ showError('Please enter both an object name and a question');
1609
+ return;
1610
+ }
1611
+
1612
+ try {
1613
+ const response = await fetch(`${API_BASE_URL}/ask`, {
1614
+ method: 'POST',
1615
+ headers: {
1616
+ 'Content-Type': 'application/json'
1617
+ },
1618
+ body: JSON.stringify({ object_name: objectName, question })
1619
+ });
1620
+
1621
+ if (!response.ok) {
1622
+ throw new Error(`Server returned ${response.status}: ${response.statusText}`);
1623
+ }
1624
+
1625
+ const result = await response.json();
1626
+
1627
+ // Display the answer
1628
+ document.getElementById('ai-answer').textContent = result.answer;
1629
+ document.getElementById('answer-container').style.display = 'block';
1630
+ } catch (error) {
1631
+ showError('Failed to get answer: ' + error.message);
1632
+ console.error(error);
1633
+ }
1634
+ }
1635
+
1636
+ function displaySearchResults(results) {
1637
+ const resultsContainer = document.getElementById('search-results');
1638
+ const resultsGrid = document.getElementById('search-results-grid');
1639
+
1640
+ resultsContainer.style.display = 'block';
1641
+ resultsGrid.innerHTML = '';
1642
+
1643
+ if (results.length === 0) {
1644
+ resultsGrid.innerHTML = '<p>No results found for your search.</p>';
1645
+ return;
1646
+ }
1647
+
1648
+ results.forEach(scan => {
1649
+ const card = document.createElement('div');
1650
+ card.className = 'history-card';
1651
+ card.addEventListener('click', () => viewScanDetails(scan.scan_id));
1652
+
1653
+ // Format date
1654
+ const scanDate = new Date(scan.timestamp);
1655
+ const formattedDate = scanDate.toLocaleDateString() + ' ' + scanDate.toLocaleTimeString();
1656
+
1657
+ // Get first 5 objects for preview
1658
+ const previewObjects = scan.objects.slice(0, 5);
1659
+
1660
+ card.innerHTML = `
1661
+ <img src="${API_BASE_URL}/image/${scan.scan_id}" alt="Scan image" class="history-image">
1662
+ <div class="history-content">
1663
+ <div class="history-date">${formattedDate}</div>
1664
+ <div class="history-objects">
1665
+ ${previewObjects.map(obj => `<span class="history-object">${obj.name}</span>`).join('')}
1666
+ ${scan.objects.length > 5 ? `<span class="history-object">+${scan.objects.length - 5} more</span>` : ''}
1667
+ </div>
1668
+ </div>
1669
+ `;
1670
+
1671
+ resultsGrid.appendChild(card);
1672
+ });
1673
+ }
1674
+ // Add cleanup when switching tabs
1675
+ // Add cleanup when switching tabs
1676
+ tabs.forEach(tab => {
1677
+ tab.addEventListener('click', () => {
1678
+ const tabId = tab.getAttribute('data-tab');
1679
+
1680
+ // Stop camera if switching away from real-time tab
1681
+ if (tabId !== 'realtime' && isCameraActive) {
1682
+ stopCamera();
1683
+ }
1684
+
1685
+ // Update active tab
1686
+ tabs.forEach(t => t.classList.remove('active'));
1687
+ tab.classList.add('active');
1688
+
1689
+ // Show corresponding content
1690
+ tabContents.forEach(content => {
1691
+ content.classList.remove('active');
1692
+ if (content.id === `${tabId}-tab`) {
1693
+ content.classList.add('active');
1694
+ }
1695
+ });
1696
+
1697
+ // Load history if history tab is selected
1698
+ if (tabId === 'history') {
1699
+ loadHistory();
1700
+ }
1701
+
1702
+ // Clear search results if search tab is selected
1703
+ if (tabId === 'search') {
1704
+ document.getElementById('search-results').style.display = 'none';
1705
+ document.getElementById('answer-container').style.display = 'none';
1706
+ }
1707
+ });
1708
+ });
1709
+ // Initialize
1710
+ // Initialize
1711
+ document.addEventListener('DOMContentLoaded', () => {
1712
+ // Check Ollama status on page load
1713
+ checkOllamaStatus();
1714
+
1715
+ // Check camera status
1716
+ checkCameraStatus();
1717
+
1718
+ // Check if we're in offline mode
1719
+ fetch(`${API_BASE_URL}/offline_toggle?mode=false`)
1720
+ .then(response => response.json())
1721
+ .then(data => {
1722
+ offlineMode = data.offline_mode;
1723
+ offlineToggle.checked = offlineMode;
1724
+
1725
+ // Show Ollama status if offline mode is enabled
1726
+ if (offlineMode) {
1727
+ const ollamaStatus = document.getElementById('ollama-status');
1728
+ const ollamaStatusText = document.getElementById('ollama-status-text');
1729
+
1730
+ // We need to check Ollama status again to get the current state
1731
+ checkOllamaStatus();
1732
+ }
1733
+ })
1734
+ .catch(error => {
1735
+ console.error('Failed to get offline mode status:', error);
1736
+ });
1737
+ });
1738
+
1739
+ // Add this to handle page unload
1740
+ window.addEventListener('beforeunload', () => {
1741
+ if (isCameraActive) {
1742
+ stopCamera();
1743
+ }
1744
+
1745
+ if (ws) {
1746
+ ws.close();
1747
+ }
1748
+ });
1749
+ </script>
1750
+ </body>
1751
+ </html>
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn[standard]==0.30.6
3
+ torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
4
+ ultralytics==8.3.2
5
+ clip-by-openai==1.0.0 # For CLIP
6
+ groq==0.9.0
7
+ python-dotenv==1.0.1
8
+ pillow==10.4.0
9
+ opencv-python-headless==4.10.0.84 # Headless for server
10
+ numpy==2.1.1
11
+ imagehash==4.3.1
12
+ aiohttp==3.10.5
13
+ pydantic==2.9.2
14
+ sqlite3 # Built-in
yolo12x.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:682ce8dadee004dbe964950f1bf3eda451671815a6ed62db80b398916b9b7c6f
3
+ size 119322638