hvanu commited on
Commit
48e528b
·
1 Parent(s): 98e2d95
assets/arrow.svg ADDED
assets/img/.gitattributes ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
2
+ *.png filter=lfs diff=lfs merge=lfs -text
3
+ *.jpg filter=lfs diff=lfs merge=lfs -text
assets/img/AdobeStock_84708957.jpg ADDED

Git LFS Details

  • SHA256: e3b0c21b2504d173ce528238cf7555a5a8b425f111d87dffac7d5632c499f2fd
  • Pointer size: 131 Bytes
  • Size of remote file: 151 kB
assets/img/Deutscher_Personalausweis_(2010_Version).jpg ADDED

Git LFS Details

  • SHA256: 96322638591909e23f7fc7a71dedd1d0de6e5c0589f445079f554b6df94b67bd
  • Pointer size: 131 Bytes
  • Size of remote file: 550 kB
assets/img/Next-Station.jpg ADDED

Git LFS Details

  • SHA256: 6a24e607876e80cc39d3450c8911cf7f74f6733f24dd8541bd3a5b6c1ae80205
  • Pointer size: 131 Bytes
  • Size of remote file: 109 kB
assets/img/computer_vision_textbook_001.jpeg ADDED

Git LFS Details

  • SHA256: f3558c4dcef78d6781543d2c76a78608ab1776eda29453aef2a65d7a7533a81c
  • Pointer size: 131 Bytes
  • Size of remote file: 496 kB
assets/img/confuse.jpeg ADDED

Git LFS Details

  • SHA256: b846e2e3b908d0d75b3e52bda5fd4d64d585d3eb0773519687464eef2c2f4f57
  • Pointer size: 131 Bytes
  • Size of remote file: 190 kB
assets/img/sample_handwritten.png ADDED

Git LFS Details

  • SHA256: 78dd111887a399b0778d3bf11341cf8c823569f3a9a9c1d8cdac3c882de48299
  • Pointer size: 131 Bytes
  • Size of remote file: 262 kB
assets/img/sample_nl_passport.jpg ADDED

Git LFS Details

  • SHA256: 85c831afc216f1c3dcbe06d0676f553955ec5abf154439dceb05f1b877de61ab
  • Pointer size: 131 Bytes
  • Size of remote file: 172 kB
assets/img/sample_tinygpt_screenshot.png ADDED

Git LFS Details

  • SHA256: 75ee7d4c7af3a089840e910defe4e391571e3f40edcba7d4a44bb4326cdb82b1
  • Pointer size: 131 Bytes
  • Size of remote file: 280 kB
assets/img/tire_code.jpeg ADDED

Git LFS Details

  • SHA256: 3a01034682507d27b009527bbeb6f63ff19a4640a25bf2be6b955082afa40a70
  • Pointer size: 131 Bytes
  • Size of remote file: 101 kB
assets/obj-detect.svg ADDED
assets/style.css ADDED
@@ -0,0 +1,441 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --background: 0 0% 100%;
3
+ --foreground: 222.2 84% 4.9%;
4
+ --card: 0 0% 100%;
5
+ --card-foreground: 222.2 84% 4.9%;
6
+ --popover: 0 0% 100%;
7
+ --popover-foreground: 222.2 84% 4.9%;
8
+ --primary: 221.2 83.2% 53.3%;
9
+ --primary-foreground: 210 40% 98%;
10
+ --secondary: 210 40% 96.1%;
11
+ --secondary-foreground: 222.2 47.4% 11.2%;
12
+ --muted: 210 40% 96.1%;
13
+ --muted-foreground: 215.4 16.3% 46.9%;
14
+ --accent: 210 40% 96.1%;
15
+ --accent-foreground: 222.2 47.4% 11.2%;
16
+ --destructive: 0 84.2% 60.2%;
17
+ --destructive-foreground: 210 40% 98%;
18
+ --border: 214.3 31.8% 91.4%;
19
+ --input: 214.3 31.8% 91.4%;
20
+ --ring: 221.2 83.2% 53.3%;
21
+ --radius: 0.5rem;
22
+ }
23
+
24
+
25
+ * {
26
+ margin: 0;
27
+ padding: 0;
28
+ box-sizing: border-box;
29
+ }
30
+
31
+ body {
32
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
33
+ Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans",
34
+ "Helvetica Neue", sans-serif;
35
+ background-color: hsl(var(--background));
36
+ color: hsl(var(--foreground));
37
+ line-height: 1.5;
38
+ min-height: 100vh;
39
+ transition: background-color 0.2s;
40
+ overflow-y: scroll;
41
+ }
42
+
43
+ .container {
44
+ max-width: 1400px;
45
+ margin: 0 auto;
46
+ padding: 1rem;
47
+ }
48
+
49
+ header {
50
+ display: flex;
51
+ justify-content: space-between;
52
+ align-items: center;
53
+ padding: 1rem 0;
54
+ border-bottom: 1px solid hsl(var(--border));
55
+ margin-bottom: 2rem;
56
+ }
57
+
58
+ h1 {
59
+ font-size: 1.5rem;
60
+ font-weight: 600;
61
+ }
62
+
63
+ .grid {
64
+ display: grid;
65
+ gap: 1.5rem;
66
+ }
67
+
68
+ .grid-cols-2 {
69
+ grid-template-columns: repeat(2, minmax(0, 1fr));
70
+ }
71
+
72
+ .mb-2 {
73
+ margin-bottom: 2rem;
74
+ }
75
+
76
+ .card {
77
+ background-color: hsl(var(--card));
78
+ border: 1px solid hsl(var(--border));
79
+ border-radius: var(--radius);
80
+ padding: 1.5rem;
81
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
82
+ }
83
+
84
+ .card-header {
85
+ margin-bottom: 1rem;
86
+ }
87
+
88
+ .card-title {
89
+ font-size: 1.25rem;
90
+ font-weight: 600;
91
+ margin-bottom: 0.5rem;
92
+ }
93
+
94
+ .card-description {
95
+ color: hsl(var(--muted-foreground));
96
+ font-size: 0.875rem;
97
+ }
98
+
99
+ .preview-area {
100
+ position: relative;
101
+ width: 100%;
102
+ height: 300px;
103
+ border: 2px dashed hsl(var(--border));
104
+ border-radius: var(--radius);
105
+ display: flex;
106
+ flex-direction: column;
107
+ justify-content: center;
108
+ align-items: center;
109
+ overflow: hidden;
110
+ margin-bottom: 1rem;
111
+ }
112
+
113
+ .text-output-area {
114
+ width: 100%;
115
+ height: 500px;
116
+ border: 2px dashed hsl(var(--border));
117
+ border-radius: var(--radius);
118
+ display: flex;
119
+ flex-direction: column;
120
+ justify-content: center;
121
+ align-items: center;
122
+ overflow: scroll;
123
+ }
124
+
125
+ .preview-area.has-image {
126
+ border: none;
127
+ }
128
+
129
+ .preview-area img {
130
+ max-width: 100%;
131
+ max-height: 100%;
132
+ object-fit: contain;
133
+ }
134
+
135
+ .preview-placeholder {
136
+ text-align: center;
137
+ color: hsl(var(--muted-foreground));
138
+ }
139
+
140
+ .preview-placeholder svg {
141
+ width: 3rem;
142
+ height: 3rem;
143
+ margin-bottom: 1rem;
144
+ color: hsl(var(--muted-foreground));
145
+ }
146
+
147
+ .btn {
148
+ display: inline-flex;
149
+ align-items: center;
150
+ justify-content: center;
151
+ border-radius: var(--radius);
152
+ font-size: 0.875rem;
153
+ font-weight: 500;
154
+ transition: all 0.2s;
155
+ padding: 0.5rem 1rem;
156
+ cursor: pointer;
157
+ border: 1px solid transparent;
158
+ }
159
+
160
+ .btn-primary {
161
+ background-color: hsl(var(--primary));
162
+ color: hsl(var(--primary-foreground));
163
+ }
164
+
165
+ .btn-primary:hover {
166
+ background-color: hsl(var(--primary) / 0.9);
167
+ }
168
+
169
+ .btn-secondary {
170
+ background-color: hsl(var(--secondary));
171
+ color: hsl(var(--secondary-foreground));
172
+ border-color: hsl(var(--border));
173
+ }
174
+
175
+ .btn-secondary:hover {
176
+ background-color: hsl(var(--secondary) / 0.9);
177
+ }
178
+
179
+ .btn-group {
180
+ display: flex;
181
+ gap: 0.5rem;
182
+ }
183
+
184
+ .tabs {
185
+ display: flex;
186
+ border-bottom: 1px solid hsl(var(--border));
187
+ margin-bottom: 1rem;
188
+ }
189
+
190
+ .tab {
191
+ padding: 0.5rem 1rem;
192
+ cursor: pointer;
193
+ border-bottom: 2px solid transparent;
194
+ font-size: 0.875rem;
195
+ font-weight: 500;
196
+ color: hsl(var(--muted-foreground));
197
+ }
198
+
199
+ .tab.active {
200
+ color: hsl(var(--primary));
201
+ border-bottom-color: hsl(var(--primary));
202
+ }
203
+
204
+ .results {
205
+ margin-top: 1rem;
206
+ border-top: 1px solid hsl(var(--border));
207
+ padding-top: 1rem;
208
+ }
209
+
210
+ .result-item {
211
+ padding: 0.5rem;
212
+ border-radius: var(--radius);
213
+ margin-bottom: 0.5rem;
214
+ background-color: hsl(var(--secondary));
215
+ }
216
+
217
+ .result-label {
218
+ font-weight: 500;
219
+ }
220
+
221
+ .result-confidence {
222
+ color: hsl(var(--muted-foreground));
223
+ font-size: 0.75rem;
224
+ }
225
+
226
+ .bounding-box {
227
+ position: absolute;
228
+ border: 2px solid hsl(var(--primary));
229
+ background-color: hsl(var(--primary) / 0.2);
230
+ }
231
+
232
+
233
+ /* Updated tabs styling for collapsible applications */
234
+ .applications-container {
235
+ display: flex;
236
+ flex-direction: column;
237
+ gap: 1rem;
238
+ margin-bottom: 1rem;
239
+ }
240
+
241
+ .application-tab {
242
+ border: 1px solid hsl(var(--border));
243
+ border-radius: var(--radius);
244
+ overflow: hidden;
245
+ }
246
+
247
+ .application-header {
248
+ display: flex;
249
+ justify-content: space-between;
250
+ align-items: center;
251
+ padding: 0.75rem 1rem;
252
+ background-color: hsl(var(--secondary));
253
+ cursor: pointer;
254
+ user-select: none;
255
+ }
256
+
257
+ .application-header:hover {
258
+ background-color: hsl(var(--secondary) / 0.9);
259
+ }
260
+
261
+ .application-title {
262
+ font-weight: 500;
263
+ display: flex;
264
+ align-items: center;
265
+ gap: 0.5rem;
266
+ }
267
+
268
+ .application-icon {
269
+ width: 1.25rem;
270
+ height: 1.25rem;
271
+ }
272
+
273
+ .application-content {
274
+ padding: 0 1rem;
275
+ max-height: 0;
276
+ overflow: hidden;
277
+ transition: max-height 0.1s ease-out;
278
+ }
279
+
280
+ .application-tab.active .application-content {
281
+ padding: 1rem;
282
+ max-height: 2000px;
283
+ transition: max-height 0.2s ease-in;
284
+ }
285
+
286
+ .application-arrow {
287
+ transition: transform 0.2s ease;
288
+ }
289
+
290
+ .application-tab.active .application-arrow {
291
+ transform: rotate(180deg);
292
+ }
293
+
294
+ /* -- */
295
+
296
+ @media (max-width: 768px) {
297
+ .grid-cols-2 {
298
+ grid-template-columns: 1fr;
299
+ }
300
+ }
301
+
302
+
303
+ .input-group {
304
+ margin-bottom: 4px;
305
+ }
306
+
307
+ .input-group label {
308
+ display: block;
309
+ margin-bottom: 4px;
310
+ font-weight: 500;
311
+ font-size: 0.875rem;
312
+ color: var(--foreground);
313
+ }
314
+
315
+ .input-group input {
316
+ width: 100%;
317
+ padding: 10px 12px;
318
+ font-size: 0.9375rem;
319
+ border: 1px solid hsl(var(--border));
320
+ border-radius: var(--radius);
321
+ transition: var(--transition);
322
+ box-shadow: var(--shadow-sm);
323
+ }
324
+
325
+ .input-group input::placeholder {
326
+ color: #9ca3af; /* Placeholder text color */
327
+ }
328
+
329
+ .input-group input:focus {
330
+ outline: none;
331
+ border-color: #6b7280;
332
+ box-shadow: var(--shadow-focus), var(--shadow-sm);
333
+ }
334
+
335
+ .helper-text {
336
+ margin-top: 6px;
337
+ font-size: 0.75rem;
338
+ color: #6b7280; /* Muted gray for helper text */
339
+ }
340
+
341
+ .hidden {
342
+ display: none;
343
+ }
344
+
345
+ .confidence-slider input[type="range"] {
346
+ width: 100%;
347
+ appearance: none;
348
+ height: 6px;
349
+ background: hsl(var(--border));
350
+ border-radius: var(--radius);
351
+ outline: none;
352
+ transition: background 0.2s;
353
+ }
354
+
355
+ .confidence-slider input[type="range"]::-webkit-slider-thumb {
356
+ appearance: none;
357
+ width: 16px;
358
+ height: 16px;
359
+ border-radius: 50%;
360
+ background: hsl(var(--primary));
361
+ cursor: pointer;
362
+ transition: background 0.2s;
363
+ }
364
+
365
+ .confidence-slider input[type="range"]::-webkit-slider-thumb:hover {
366
+ background: hsl(var(--primary) / 0.9);
367
+ }
368
+
369
+ /* Sample images collapsible section */
370
+ .sample-images-container {
371
+ border-top: 1px solid hsl(var(--border));
372
+ margin-top: 1rem;
373
+ }
374
+
375
+ .collapsible-header {
376
+ display: flex;
377
+ justify-content: space-between;
378
+ align-items: center;
379
+ padding: 0.75rem 0;
380
+ cursor: pointer;
381
+ font-weight: 500;
382
+ user-select: none;
383
+ }
384
+
385
+ .collapsible-header:hover {
386
+ color: hsl(var(--primary));
387
+ }
388
+
389
+ .transition-transform {
390
+ transition: transform 0.2s ease;
391
+ }
392
+
393
+ .rotate-180 {
394
+ transform: rotate(180deg);
395
+ }
396
+
397
+ .sample-images-content {
398
+ padding-bottom: 0.5rem;
399
+ }
400
+
401
+ .sample-image-grid {
402
+ display: flex;
403
+ flex-wrap: wrap;
404
+ gap: 1rem;
405
+ }
406
+
407
+ .sample-image {
408
+ cursor: pointer;
409
+ text-align: center;
410
+ transition: transform 0.2s;
411
+ }
412
+
413
+ .sample-image:hover {
414
+ transform: scale(1.05);
415
+ }
416
+
417
+ .sample-image-thumb {
418
+ width: 80px;
419
+ height: 80px;
420
+ border-radius: var(--radius);
421
+ overflow: hidden;
422
+ border: 1px solid hsl(var(--border));
423
+ margin-bottom: 0.25rem;
424
+ }
425
+
426
+ .sample-image-thumb img {
427
+ width: 100%;
428
+ height: 100%;
429
+ object-fit: cover;
430
+ }
431
+
432
+ .sample-image p {
433
+ font-size: 0.75rem;
434
+ margin-top: 0.25rem;
435
+ }
436
+
437
+ .mt-2 {
438
+ margin-top: 2rem;
439
+ }
440
+
441
+
assets/text.svg ADDED
index.html ADDED
@@ -0,0 +1,992 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>ML Vision Playground</title>
7
+ <script
8
+ defer
9
+ src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"
10
+ ></script>
11
+ <link rel="stylesheet" href="assets/style.css" />
12
+ </head>
13
+
14
+ <body x-data="app()" :class="{'dark': darkMode}">
15
+ <div class="container">
16
+ <header>
17
+ <h1>ML Vision Playground</h1>
18
+ </header>
19
+
20
+ <div class="applications-container mb-2">
21
+ <!-- Object Detection Application -->
22
+
23
+ <!-- Object detection -->
24
+ <div
25
+ class="application-tab"
26
+ :class="{'active': activeApplication === 'objdet'}"
27
+ >
28
+ <div class="application-header" @click="toggleApplication('objdet')">
29
+ <div class="application-title">
30
+ <img src="assets/obj-detect.svg" height="20px" />
31
+ Object detection
32
+ </div>
33
+ <img
34
+ src="assets/arrow.svg"
35
+ height="20px"
36
+ class="application-arrow"
37
+ />
38
+ </div>
39
+
40
+ <div class="application-content">
41
+ <!-- api creds -->
42
+ <div class="application-meta-input card mb-2">
43
+ <div class="form-container">
44
+ <div class="input-group">
45
+ <label for="url"
46
+ >URL
47
+ <span class="helper-text"
48
+ >Enter the base URL for the API.</span
49
+ ></label
50
+ >
51
+ <input
52
+ type="text"
53
+ id="url"
54
+ placeholder="https://example.com/api"
55
+ />
56
+ </div>
57
+ <div class="input-group">
58
+ <label for="apiKey"
59
+ >API Key
60
+ <span class="helper-text"
61
+ >Your secret API key.
62
+ </span></label
63
+ >
64
+ <input
65
+ type="text"
66
+ id="apiKey"
67
+ placeholder="DPT23exmplg4FJfgfFREsada0lMUgJs0kcC9wxd"
68
+ />
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="grid grid-cols-2">
74
+ <!-- Input Card -->
75
+ <div class="card">
76
+ <div class="card-header">
77
+ <h2 class="card-title">Input Image</h2>
78
+ <p class="card-description">
79
+ Upload or capture an image to analyze
80
+ </p>
81
+ </div>
82
+
83
+ <div class="tabs">
84
+ <div
85
+ @click="activeTab = 'upload'"
86
+ :class="{'active': activeTab === 'upload'}"
87
+ class="tab"
88
+ >
89
+ Upload
90
+ </div>
91
+ <div
92
+ @click="activeTab = 'camera'"
93
+ :class="{'active': activeTab === 'camera'}"
94
+ class="tab"
95
+ >
96
+ Camera
97
+ </div>
98
+ </div>
99
+
100
+ <div
101
+ x-show="imageUrl"
102
+ class="preview-image-container"
103
+ style="max-width: 100%; height: auto"
104
+ >
105
+ <canvas
106
+ id="previewCanvas"
107
+ x-ref="previewCanvas"
108
+ class="preview-image"
109
+ style="max-width: 100%; height: auto"
110
+ x-effect="imageUrl && (() => {
111
+ const canvas = $refs.previewCanvas;
112
+ const img = new Image();
113
+ img.onload = function() {
114
+ canvas.width = img.width;
115
+ canvas.height = img.height;
116
+ canvas.getContext('2d').drawImage(img, 0, 0);
117
+ };
118
+ img.src = imageUrl;
119
+ })()"
120
+ ></canvas>
121
+ </div>
122
+ <!-- <canvas id="canvas" x-ref="canvas"></canvas> -->
123
+
124
+ <div
125
+ x-show="activeTab === 'upload' && !imageUrl"
126
+ class="upload-tab"
127
+ >
128
+ <div
129
+ @click="$refs.fileInput.click()"
130
+ @dragover.prevent="dragOver = true"
131
+ @dragleave="dragOver = false"
132
+ @drop.prevent="handleDrop($event)"
133
+ :class="{'has-image': imageUrl, 'border-primary': dragOver}"
134
+ class="preview-area"
135
+ >
136
+ <input
137
+ type="file"
138
+ @change="handleFileSelect"
139
+ x-ref="fileInput"
140
+ accept="image/*"
141
+ class="hidden"
142
+ />
143
+
144
+ <template x-if="!imageUrl">
145
+ <div class="preview-placeholder">
146
+ <svg
147
+ xmlns="http://www.w3.org/2000/svg"
148
+ fill="none"
149
+ viewBox="0 0 24 24"
150
+ stroke="currentColor"
151
+ >
152
+ <path
153
+ stroke-linecap="round"
154
+ stroke-linejoin="round"
155
+ stroke-width="2"
156
+ d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
157
+ />
158
+ </svg>
159
+ <p>Click to upload or drag and drop</p>
160
+ <p class="text-sm">Supports JPG, PNG, WEBP</p>
161
+ </div>
162
+ </template>
163
+ </div>
164
+ <!-- <div class="btn-group">
165
+ <button @click="clearImage" x-show="imageUrl" class="btn btn-secondary">
166
+ Clear
167
+ </button>
168
+ </div> -->
169
+ </div>
170
+
171
+ <div
172
+ x-show="activeTab === 'camera' && !imageUrl"
173
+ class="camera-tab"
174
+ >
175
+ <div class="preview-area">
176
+ <video
177
+ x-ref="video"
178
+ autoplay
179
+ playsinline
180
+ class="hidden"
181
+ ></video>
182
+ <canvas x-ref="canvas" class="hidden"></canvas>
183
+
184
+ <div class="preview-placeholder">
185
+ <svg
186
+ xmlns="http://www.w3.org/2000/svg"
187
+ fill="none"
188
+ viewBox="0 0 24 24"
189
+ stroke="currentColor"
190
+ >
191
+ <path
192
+ stroke-linecap="round"
193
+ stroke-linejoin="round"
194
+ stroke-width="2"
195
+ d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
196
+ />
197
+ <path
198
+ stroke-linecap="round"
199
+ stroke-linejoin="round"
200
+ stroke-width="2"
201
+ d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
202
+ />
203
+ </svg>
204
+ <p>Camera Feed</p>
205
+ </div>
206
+ </div>
207
+
208
+ <div class="btn-group">
209
+ <button @click="startCamera" class="btn btn-primary">
210
+ Start Camera
211
+ </button>
212
+ <button
213
+ @click="captureImage"
214
+ class="btn btn-primary"
215
+ :disabled="!cameraActive"
216
+ >
217
+ Capture
218
+ </button>
219
+ <button
220
+ @click="stopCamera"
221
+ class="btn btn-secondary"
222
+ :disabled="!cameraActive"
223
+ >
224
+ Stop
225
+ </button>
226
+ </div>
227
+ </div>
228
+
229
+ <div class="btn-group">
230
+ <button
231
+ @click="clearImage"
232
+ x-show="imageUrl"
233
+ class="btn btn-secondary"
234
+ >
235
+ Clear
236
+ </button>
237
+ </div>
238
+
239
+ <!-- Collapsible section to select sample images from thumbnails -->
240
+ <div class="sample-images-container mt-2">
241
+ <div
242
+ class="collapsible-header"
243
+ @click="sampleImagesOpen = !sampleImagesOpen"
244
+ >
245
+ <span>Sample Images</span>
246
+ <img
247
+ src="assets/arrow.svg"
248
+ height="20px"
249
+ :class="{'rotate-180': sampleImagesOpen}"
250
+ />
251
+ </div>
252
+
253
+ <div
254
+ x-show="sampleImagesOpen"
255
+ x-transition
256
+ class="sample-images-content"
257
+ >
258
+ <div class="sample-image-grid">
259
+ <template x-for="(image, type) in sampleData" :key="type">
260
+ <span @click="loadSample(type)" class="sample-image">
261
+ <img
262
+ :src="image.url"
263
+ alt=""
264
+ width="80px"
265
+ style="max-height: 100px"
266
+ />
267
+ </span>
268
+ </template>
269
+ </div>
270
+ </div>
271
+ </div>
272
+
273
+ <!-- Confidence Interval Slider -->
274
+ <div class="confidence-slider mt-2">
275
+ <label
276
+ for="confidence-slider"
277
+ class="block text-sm font-medium text-gray-700"
278
+ >Confidence Interval</label
279
+ >
280
+ <input
281
+ id="confidence-slider"
282
+ type="range"
283
+ min="0"
284
+ max="100"
285
+ step="1"
286
+ x-model="confidenceThreshold"
287
+ />
288
+ <p class="text-sm mt-1">
289
+ Selected: <span x-text="confidenceThreshold"></span>%
290
+ </p>
291
+ </div>
292
+
293
+ <!-- Predict Button -->
294
+ <div class="mt-2">
295
+ <button
296
+ @click="predictImage()"
297
+ class="btn btn-primary w-100"
298
+ :disabled="!imageUrl"
299
+ >
300
+ Predict
301
+ </button>
302
+ </div>
303
+ </div>
304
+
305
+ <!-- Processing Card -->
306
+ <!-- Processing Card - results -->
307
+ <div class="card">
308
+ <div class="card-header">
309
+ <h2 class="card-title">Output</h2>
310
+ <p class="card-description">
311
+ Select an application to analyze your image
312
+ </p>
313
+ </div>
314
+
315
+ <div class="application-content">
316
+ <!-- Preview content same as before -->
317
+ <span id="objdet-status-display"></span>
318
+ <textarea
319
+ id="objdet-output-display"
320
+ class="text-output-area"
321
+ ></textarea>
322
+
323
+ <div class="btn-group mt-2">
324
+ <button
325
+ @click="clearResults"
326
+ class="btn btn-secondary"
327
+ :disabled="!results.length"
328
+ >
329
+ Clear Results
330
+ </button>
331
+ </div>
332
+ </div>
333
+ </div>
334
+ </div>
335
+ </div>
336
+
337
+ <!-- end -->
338
+ </div>
339
+
340
+ <!-- OCR -->
341
+ <div
342
+ class="application-tab"
343
+ :class="{'active': activeApplication === 'ocr'}"
344
+ >
345
+ <div class="application-header" @click="toggleApplication('ocr')">
346
+ <div class="application-title">
347
+ <img src="assets/text.svg" height="20px" />
348
+ Text Recognition (OCR)
349
+ </div>
350
+ <img
351
+ src="assets/arrow.svg"
352
+ height="20px"
353
+ class="application-arrow"
354
+ />
355
+ </div>
356
+ <div class="application-content">
357
+ <!-- api creds -->
358
+ <div class="application-meta-input card mb-2">
359
+ <div class="form-container">
360
+ <div class="input-group">
361
+ <!-- Add the loader component here -->
362
+
363
+ <label for="url-ocr"
364
+ >URL
365
+ <span class="helper-text"
366
+ >Enter the base URL for the API.</span
367
+ ></label
368
+ >
369
+ <input
370
+ type="text"
371
+ id="url-ocr"
372
+ placeholder="https://example.com/api"
373
+ />
374
+ </div>
375
+ <div class="input-group">
376
+ <label for="apiKey-ocr"
377
+ >API Key
378
+ <span class="helper-text"
379
+ >Your secret API key.
380
+ </span></label
381
+ >
382
+ <input
383
+ type="text"
384
+ id="apiKey-ocr"
385
+ placeholder="DPT23exmplg4FJfgfFREsada0lMUgJs0kcC9wxd"
386
+ />
387
+ </div>
388
+ </div>
389
+ </div>
390
+
391
+ <!-- app -->
392
+ <div class="grid grid-cols-2">
393
+ <!-- Input Card -->
394
+ <div class="card">
395
+ <div class="card-header">
396
+ <h2 class="card-title">Input Image</h2>
397
+ <p class="card-description">
398
+ Upload or capture an image to analyze
399
+ </p>
400
+ </div>
401
+
402
+ <div class="tabs">
403
+ <div
404
+ @click="activeTab = 'upload'"
405
+ :class="{'active': activeTab === 'upload'}"
406
+ class="tab"
407
+ >
408
+ Upload
409
+ </div>
410
+ <div
411
+ @click="activeTab = 'camera'"
412
+ :class="{'active': activeTab === 'camera'}"
413
+ class="tab"
414
+ >
415
+ Camera
416
+ </div>
417
+ </div>
418
+
419
+ <div
420
+ x-show="imageUrlOcr"
421
+ class="preview-image-container"
422
+ style="max-width: 100%; height: auto"
423
+ >
424
+ <canvas
425
+ id="previewCanvasOcr"
426
+ x-ref="previewCanvasOcr"
427
+ class="preview-image"
428
+ style="max-width: 100%; height: auto"
429
+ x-effect="imageUrlOcr && (() => {
430
+ const canvas = $refs.previewCanvasOcr;
431
+ const img = new Image();
432
+ img.onload = function() {
433
+ canvas.width = img.width;
434
+ canvas.height = img.height;
435
+ canvas.getContext('2d').drawImage(img, 0, 0);
436
+ };
437
+ img.src = imageUrlOcr;
438
+ })()"
439
+ ></canvas>
440
+ </div>
441
+ <!-- <canvas id="canvas" x-ref="canvas"></canvas> -->
442
+
443
+ <div
444
+ x-show="activeTab === 'upload' && !imageUrlOcr"
445
+ class="upload-tab"
446
+ >
447
+ <div
448
+ @click="$refs.fileInput.click()"
449
+ @dragover.prevent="dragOver = true"
450
+ @dragleave="dragOver = false"
451
+ @drop.prevent="handleDrop($event)"
452
+ :class="{'has-image': imageUrlOcr, 'border-primary': dragOver}"
453
+ class="preview-area"
454
+ >
455
+ <input
456
+ type="file"
457
+ @change="handleFileSelect"
458
+ x-ref="fileInput"
459
+ accept="image/*"
460
+ class="hidden"
461
+ />
462
+
463
+ <template x-if="!imageUrlOcr">
464
+ <div class="preview-placeholder">
465
+ <svg
466
+ xmlns="http://www.w3.org/2000/svg"
467
+ fill="none"
468
+ viewBox="0 0 24 24"
469
+ stroke="currentColor"
470
+ >
471
+ <path
472
+ stroke-linecap="round"
473
+ stroke-linejoin="round"
474
+ stroke-width="2"
475
+ d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
476
+ />
477
+ </svg>
478
+ <p>Click to upload or drag and drop</p>
479
+ <p class="text-sm">Supports JPG, PNG, WEBP</p>
480
+ </div>
481
+ </template>
482
+ </div>
483
+ <!-- <div class="btn-group">
484
+ <button @click="clearImage" x-show="imageUrl" class="btn btn-secondary">
485
+ Clear
486
+ </button>
487
+ </div> -->
488
+ </div>
489
+
490
+ <div
491
+ x-show="activeTab === 'camera' && !imageUrlOcr"
492
+ class="camera-tab"
493
+ >
494
+ <div class="preview-area">
495
+ <video
496
+ x-ref="video"
497
+ autoplay
498
+ playsinline
499
+ class="hidden"
500
+ ></video>
501
+ <canvas x-ref="canvas" class="hidden"></canvas>
502
+
503
+ <div class="preview-placeholder">
504
+ <svg
505
+ xmlns="http://www.w3.org/2000/svg"
506
+ fill="none"
507
+ viewBox="0 0 24 24"
508
+ stroke="currentColor"
509
+ >
510
+ <path
511
+ stroke-linecap="round"
512
+ stroke-linejoin="round"
513
+ stroke-width="2"
514
+ d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
515
+ />
516
+ <path
517
+ stroke-linecap="round"
518
+ stroke-linejoin="round"
519
+ stroke-width="2"
520
+ d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
521
+ />
522
+ </svg>
523
+ <p>Camera Feed</p>
524
+ </div>
525
+ </div>
526
+
527
+ <div class="btn-group">
528
+ <button @click="startCamera" class="btn btn-primary">
529
+ Start Camera
530
+ </button>
531
+ <button
532
+ @click="captureImage"
533
+ class="btn btn-primary"
534
+ :disabled="!cameraActive"
535
+ >
536
+ Capture
537
+ </button>
538
+ <button
539
+ @click="stopCamera"
540
+ class="btn btn-secondary"
541
+ :disabled="!cameraActive"
542
+ >
543
+ Stop
544
+ </button>
545
+ </div>
546
+ </div>
547
+
548
+ <div class="btn-group">
549
+ <button
550
+ @click="clearImage"
551
+ x-show="imageUrlOcr"
552
+ class="btn btn-secondary"
553
+ >
554
+ Clear
555
+ </button>
556
+ </div>
557
+
558
+ <!-- Collapsible section to select sample images from thumbnails -->
559
+ <div class="sample-images-container mt-2">
560
+ <div
561
+ class="collapsible-header"
562
+ @click="sampleImagesOpen = !sampleImagesOpen"
563
+ >
564
+ <span>Sample Images</span>
565
+ <img
566
+ src="assets/arrow.svg"
567
+ height="20px"
568
+ :class="{'rotate-180': sampleImagesOpen}"
569
+ />
570
+ </div>
571
+
572
+ <div
573
+ x-show="sampleImagesOpen"
574
+ x-transition
575
+ class="sample-images-content"
576
+ >
577
+ <div class="sample-image-grid">
578
+ <template
579
+ x-for="(image, type) in sampleDataOcr"
580
+ :key="type"
581
+ >
582
+ <span @click="loadSampleOcr(type)" class="sample-image">
583
+ <img
584
+ :src="image.url"
585
+ alt=""
586
+ width="80px"
587
+ style="max-height: 100px"
588
+ />
589
+ </span>
590
+ </template>
591
+ </div>
592
+ </div>
593
+ </div>
594
+
595
+ <!-- Confidence Interval Slider -->
596
+ <div class="confidence-slider mt-2">
597
+ <label
598
+ for="confidence-slider"
599
+ class="block text-sm font-medium text-gray-700"
600
+ >Confidence Interval</label
601
+ >
602
+ <input
603
+ id="confidence-slider"
604
+ type="range"
605
+ min="0"
606
+ max="100"
607
+ step="1"
608
+ x-model="confidenceThreshold"
609
+ />
610
+ <p class="text-sm mt-1">
611
+ Selected: <span x-text="confidenceThreshold"></span>%
612
+ </p>
613
+ </div>
614
+
615
+ <!-- Predict Button -->
616
+ <div class="mt-2">
617
+ <button
618
+ @click="predictOcr()"
619
+ class="btn btn-primary w-100"
620
+ :disabled="!imageUrlOcr"
621
+ >
622
+ Predict
623
+ </button>
624
+ </div>
625
+ </div>
626
+
627
+ <!-- Processing Card -->
628
+ <!-- Processing Card - results -->
629
+ <div class="card">
630
+ <div class="card-header">
631
+ <h2 class="card-title">Output</h2>
632
+ <p class="card-description">
633
+ Select an application to analyze your image
634
+ </p>
635
+ </div>
636
+
637
+ <div class="application-content">
638
+ <!-- Preview content same as before -->
639
+ <span id="ocr-status-display"></span>
640
+ <textarea
641
+ id="ocr-output-display"
642
+ class="text-output-area"
643
+ ></textarea>
644
+
645
+ <div class="btn-group mt-2">
646
+ <button
647
+ @click="clearResults"
648
+ class="btn btn-secondary"
649
+ >
650
+ Clear Results
651
+ </button>
652
+ </div>
653
+ </div>
654
+ </div>
655
+ </div>
656
+ </div>
657
+ </div>
658
+ </div>
659
+
660
+ <script>
661
+ // ocr
662
+ // detection / yolov11
663
+ // whisper
664
+ // medical (yolov11)
665
+ //
666
+ $ = (id) => document.getElementById(id);
667
+
668
+ function fetchWrapper(url, statusElPrefix, options = {}) {
669
+ // Get or create UI elements
670
+ const statusSpan = $(`${statusElPrefix}-status-display`);
671
+ const jsonTextarea = $(`${statusElPrefix}-output-display`);
672
+
673
+ // Update status to loading
674
+ statusSpan.textContent = "Loading...";
675
+ statusSpan.className = "status loading";
676
+
677
+ // Clear previous results
678
+ jsonTextarea.value = "";
679
+
680
+ // Make the fetch request
681
+ return fetch(url, options)
682
+ .then((response) => {
683
+ // Display status code
684
+ statusSpan.textContent = `Status: ${response.status} ${response.statusText}`;
685
+ statusSpan.className = response.ok
686
+ ? "status success"
687
+ : "status error";
688
+
689
+ // Check if response is JSON
690
+ const contentType = response.headers.get("content-type");
691
+ if (contentType && contentType.includes("application/json")) {
692
+ return response.json().then((data) => {
693
+ // Format and display JSON
694
+ jsonTextarea.value = JSON.stringify(data, null, 2);
695
+ drawBoundingBoxesObjdet(data);
696
+ return data; // Return data for further processing
697
+ });
698
+ } else {
699
+ // Handle non-JSON responses
700
+ return response.text().then((text) => {
701
+ jsonTextarea.value = text;
702
+ return text; // Return text for further processing
703
+ });
704
+ }
705
+ })
706
+ .catch((error) => {
707
+ // Handle network errors
708
+ statusSpan.textContent = `Error: ${error.message}`;
709
+ statusSpan.className = "status error";
710
+ jsonTextarea.value = `Failed to fetch: ${error.message}`;
711
+ console.error("Fetch error:", error);
712
+ throw error; // Re-throw to allow caller to handle error
713
+ });
714
+ }
715
+
716
+ // Draw bounding boxes on the image
717
+ function drawBoundingBoxesObjdet(data) {
718
+ // Check if we have predictions to display
719
+ if (!data || !data.detections || !data.detections.length) return;
720
+
721
+ // Get canvas and create context for drawing
722
+ const canvasPreview = $("previewCanvas");
723
+
724
+ const ctx = canvasPreview.getContext("2d");
725
+
726
+
727
+ // Draw each bounding box
728
+ for (const box of data.detections) {
729
+
730
+
731
+ // Set styling for the box
732
+ ctx.strokeStyle = "rgba(255, 0, 0, 0.8)";
733
+ ctx.lineWidth = 2;
734
+
735
+ // Draw the rectangle
736
+ ctx.strokeRect(
737
+ box.x_min,
738
+ box.y_min,
739
+ box.x_max - box.x_min,
740
+ box.y_max - box.y_min
741
+ );
742
+
743
+ // Add label text with confidence
744
+ const label = `${box.class_name} (${Math.round(
745
+ box.confidence * 100
746
+ )}%)`;
747
+ ctx.fillStyle = "rgba(255, 0, 0, 0.8)";
748
+ // Scale font based on image dimensions (640px is the base)
749
+ const scale = Math.max(1, Math.min(2, canvasPreview.width / 640));
750
+ ctx.font = `${Math.round(14 * scale)}px Arial`;
751
+ const textWidth = ctx.measureText(label).width;
752
+ ctx.fillRect(box.x_min, box.y_min - 20, textWidth + 10, 20);
753
+
754
+ ctx.fillStyle = "white";
755
+ ctx.fillText(label, box.x_min + 5, box.y_min - 5);
756
+ };
757
+
758
+
759
+ }
760
+
761
+ document.addEventListener("alpine:init", () => {
762
+ Alpine.data("app", () => ({
763
+ darkMode: false,
764
+ activeTab: "upload",
765
+ activeModel: "object",
766
+ activeApplication: "object",
767
+ imageUrl: null,
768
+ imageUrlOcr: null,
769
+ dragOver: false,
770
+ cameraActive: false,
771
+ boundingBoxes: [],
772
+ results: [],
773
+ confidenceThreshold: 50, // Default confidence threshold,
774
+ sampleImagesOpen: false,
775
+
776
+ toggleApplication(app) {
777
+ this.activeApplication =
778
+ this.activeApplication === app ? null : app;
779
+ this.clearResults();
780
+ },
781
+
782
+ predictOcr() {
783
+ console.log("predictOcr called");
784
+ if (!this.imageUrlOcr) {
785
+ console.log("no imageUrlOcr");
786
+ return;
787
+ }
788
+
789
+ const baseUrl = $("url-ocr").value;
790
+ const apiKey = $("apiKey-ocr").value;
791
+ const imageUrlOcr = this.imageUrlOcr;
792
+
793
+ if (!baseUrl) {
794
+ alert("Please enter an API URL");
795
+ return;
796
+ }
797
+ if (!apiKey) {
798
+ alert("Please enter an API Key");
799
+ return;
800
+ }
801
+
802
+ // Fetch the blob first, then make the API call
803
+ fetch(imageUrlOcr)
804
+ .then((response) => response.blob())
805
+ .then((blob) => {
806
+ // Convert blob to base64
807
+ const reader = new FileReader();
808
+ reader.readAsDataURL(blob);
809
+ reader.onloadend = () => {
810
+ // Extract the base64 data (remove the data:image/xyz;base64, prefix)
811
+ const base64data = reader.result.split(",")[1];
812
+
813
+ // use fetchWrapper to make the API call using application/json body
814
+ fetchWrapper(`/api/req`, "ocr", {
815
+ method: "POST",
816
+ headers: {
817
+ "Content-Type": "application/json",
818
+ Authorization: `Bearer ${apiKey}`,
819
+ "x-req-path": `${baseUrl}`,
820
+ },
821
+ body: JSON.stringify({
822
+ instances: [],
823
+ image_base64: base64data,
824
+ }),
825
+ });
826
+ };
827
+ })
828
+ .catch((error) => {
829
+ console.error("Error preparing image:", error);
830
+ alert("Error preparing image for upload");
831
+ });
832
+ },
833
+
834
+ predictImage() {
835
+ if (!this.imageUrl) return;
836
+
837
+ const baseUrl = $("url").value;
838
+ const apiKey = $("apiKey").value;
839
+ const imageUrl = this.imageUrl;
840
+
841
+ if (!baseUrl) {
842
+ alert("Please enter an API URL");
843
+ return;
844
+ }
845
+ if (!apiKey) {
846
+ alert("Please enter an API Key");
847
+ return;
848
+ }
849
+
850
+ // Fetch the blob first, then make the API call
851
+ fetch(imageUrl)
852
+ .then((response) => response.blob())
853
+ .then((blob) => {
854
+ // use fetchWrapper to make the API call using application/octet-stream body
855
+ fetchWrapper(`/api/req`, "objdet", {
856
+ method: "POST",
857
+ headers: {
858
+ "Content-Type": "application/octet-stream",
859
+ Authorization: `Bearer ${apiKey}`,
860
+ "x-req-path": `${baseUrl}`,
861
+ },
862
+ body: blob,
863
+ });
864
+ })
865
+ .catch((error) => {
866
+ console.error("Error preparing image:", error);
867
+ alert("Error preparing image for upload");
868
+ });
869
+ },
870
+
871
+ // Sample data for demo purposes
872
+ sampleData: {
873
+ catdog: {
874
+ url: "https://images.unsplash.com/photo-1606098216818-40939b7c98ad?w=600",
875
+ },
876
+ document: {
877
+ url: "https://images.unsplash.com/photo-1546410531-bb4caa6b424d?w=600",
878
+ },
879
+ food: {
880
+ url: "https://images.unsplash.com/photo-1512621776951-a57141f2eefd?w=600",
881
+ },
882
+ people: {
883
+ url: "https://images.unsplash.com/photo-1527529482837-4698179dc6ce?w=600",
884
+ },
885
+ people2: {
886
+ url: "assets/img/AdobeStock_84708957.jpg",
887
+ },
888
+ confuse: {
889
+ url: "assets/img/confuse.jpeg",
890
+ },
891
+ },
892
+
893
+ sampleDataOcr: {
894
+ ausweis: {
895
+ url: "assets/img/Deutscher_Personalausweis_(2010_Version).jpg",
896
+ },
897
+ textbook: { url: "assets/img/computer_vision_textbook_001.jpeg" },
898
+ handwritten: { url: "assets/img/sample_handwritten.png" },
899
+ nlpass: { url: "assets/img/sample_nl_passport.jpg" },
900
+ },
901
+
902
+ handleFileSelect(e) {
903
+ const file = e.target.files[0];
904
+ if (file?.type.match("image.*")) {
905
+ this.imageUrl = URL.createObjectURL(file);
906
+ this.clearResults();
907
+ }
908
+ },
909
+
910
+ handleDrop(e) {
911
+ this.dragOver = false;
912
+ const file = e.dataTransfer.files[0];
913
+ if (file?.type.match("image.*")) {
914
+ this.imageUrl = URL.createObjectURL(file);
915
+ this.clearResults();
916
+ }
917
+ },
918
+
919
+ clearImage() {
920
+ if (this.imageUrl) {
921
+ URL.revokeObjectURL(this.imageUrl);
922
+ this.imageUrl = null;
923
+ }
924
+ this.clearResults();
925
+ },
926
+
927
+ async startCamera() {
928
+ try {
929
+ const stream = await navigator.mediaDevices.getUserMedia({
930
+ video: true,
931
+ });
932
+ this.$refs.video.srcObject = stream;
933
+ this.$refs.video.classList.remove("hidden");
934
+ this.cameraActive = true;
935
+ } catch (err) {
936
+ console.error("Error accessing camera:", err);
937
+ alert("Could not access camera. Please check permissions.");
938
+ }
939
+ },
940
+
941
+ captureImage() {
942
+ const video = this.$refs.video;
943
+ const canvas = this.$refs.canvas;
944
+ canvas.width = video.videoWidth;
945
+ canvas.height = video.videoHeight;
946
+ canvas.getContext("2d").drawImage(video, 0, 0);
947
+
948
+ this.imageUrl = canvas.toDataURL("image/png");
949
+ this.clearResults();
950
+ },
951
+
952
+ stopCamera() {
953
+ const stream = this.$refs.video.srcObject;
954
+ if (stream) {
955
+ stream.getTracks().forEach((track) => track.stop());
956
+ this.$refs.video.srcObject = null;
957
+ this.$refs.video.classList.add("hidden");
958
+ this.cameraActive = false;
959
+ }
960
+ },
961
+
962
+ loadSample(type) {
963
+ this.imageUrl = this.sampleData[type].url;
964
+ this.clearResults();
965
+ },
966
+ loadSampleOcr(type) {
967
+ this.imageUrlOcr = this.sampleDataOcr[type].url;
968
+ // this.clearResults();
969
+ },
970
+
971
+ processImage(appname) {
972
+ if (!this.imageUrl) return;
973
+
974
+ this.clearResults();
975
+
976
+ // API call
977
+ },
978
+
979
+ clearResults() {
980
+ const jsonTextareaOcr = $(`ocr-output-display`);
981
+ const jsonTextareaObjdect = $(`objdet-output-display`);
982
+
983
+ // Clear previous results
984
+ jsonTextareaOcr.value = "";
985
+ jsonTextareaObjdect.value = "";
986
+ },
987
+ }));
988
+ });
989
+ </script>
990
+ </div>
991
+ </body>
992
+ </html>
main.go CHANGED
@@ -134,7 +134,13 @@ func main() {
134
  })
135
 
136
  // Serve static files from ./frontend, no directory listing
137
- r.StaticFS("/app", gin.Dir("./frontend/", false))
 
 
 
 
 
 
138
 
139
  // Route to serve parsed HTML template partials
140
  r.GET("/partials", func(c *gin.Context) {
 
134
  })
135
 
136
  // Serve static files from ./frontend, no directory listing
137
+ r.StaticFS("/assets", gin.Dir("./assets", false))
138
+
139
+ r.GET("/", func(c *gin.Context) {
140
+ // Load HTML templates from ./frontend folder
141
+ // Render the template by name
142
+ c.HTML(http.StatusOK, "index.html", gin.H{})
143
+ })
144
 
145
  // Route to serve parsed HTML template partials
146
  r.GET("/partials", func(c *gin.Context) {