cyberai-1 commited on
Commit
a018d0d
·
1 Parent(s): 0c42503
Files changed (2) hide show
  1. parfait_model.pth +2 -2
  2. templates/index.html +618 -354
parfait_model.pth CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:681ed1e9855e437851f8028b9dd54e6a6c1101f34f9904a3dbb19c3a4e0e57a9
3
- size 523282
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1ad964d670faae671f200a74dc90713a00af83e18f2114bcddc521b496a29f96
3
+ size 521545
templates/index.html CHANGED
@@ -1,438 +1,663 @@
1
  <!DOCTYPE html>
2
- <html lang="fr">
3
  <head>
4
  <meta charset="UTF-8"/>
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
- <title>Scene Classifier · Parfait</title>
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=Instrument+Sans:wght@300;400;500&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet">
9
  <style>
10
- /* ── Tokens ── */
 
 
11
  :root {
12
- --ink: #0d0d0d;
13
- --paper: #f5f2ec;
14
- --paper2: #edeae2;
15
- --rule: #d4cfc4;
16
- --accent: #c84b2f;
17
- --accent2: #2563eb;
18
- --muted: #8a8070;
19
- --white: #ffffff;
20
- --ff-head: 'Syne', sans-serif;
21
- --ff-body: 'Instrument Sans', sans-serif;
22
- --ff-mono: 'Roboto Mono', monospace;
23
- --r: 8px;
 
 
 
 
 
24
  }
 
25
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
26
- html { font-size: 16px; }
27
 
28
  body {
29
- background: var(--paper);
30
- color: var(--ink);
31
  font-family: var(--ff-body);
32
  min-height: 100vh;
33
- display: grid;
34
- grid-template-rows: auto 1fr auto;
35
  }
36
 
37
- /* ── Header ── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  header {
39
- border-bottom: 2px solid var(--ink);
40
- padding: 1.1rem 2.5rem;
41
  display: flex;
42
- align-items: baseline;
43
- gap: 1.2rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
- .site-name {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  font-family: var(--ff-head);
47
- font-size: 1.4rem;
48
- font-weight: 800;
49
- letter-spacing: -.02em;
50
- }
51
- .site-name span { color: var(--accent); }
52
- .site-sub {
53
- font-family: var(--ff-mono);
54
- font-size: .65rem;
55
- color: var(--muted);
56
- letter-spacing: .1em;
57
- text-transform: uppercase;
58
- }
59
- .badge {
60
- margin-left: auto;
61
- font-family: var(--ff-mono);
62
- font-size: .6rem;
63
- letter-spacing: .1em;
64
- padding: .3rem .75rem;
65
- border: 1.5px solid var(--ink);
66
- border-radius: 99px;
67
- text-transform: uppercase;
68
- }
69
-
70
- /* ── Main layout ── */
71
- main {
72
  display: grid;
73
  grid-template-columns: 1fr 1fr;
74
- max-width: 1100px;
75
- margin: 0 auto;
76
- width: 100%;
77
- gap: 0;
78
- padding: 3rem 2rem;
79
  }
80
 
81
- /* ── Left panel ── */
82
- .left-panel {
83
- padding-right: 3rem;
84
- border-right: 1.5px solid var(--rule);
85
- display: flex;
86
- flex-direction: column;
87
- gap: 2rem;
 
88
  }
89
-
90
- .panel-label {
91
- font-family: var(--ff-mono);
92
- font-size: .6rem;
93
- letter-spacing: .15em;
94
- color: var(--muted);
95
- text-transform: uppercase;
96
- margin-bottom: .5rem;
 
 
 
 
 
 
97
  }
98
 
99
- /* Model selector */
100
- .model-tabs {
101
- display: flex;
102
- gap: .5rem;
103
  }
104
- .tab-btn {
105
- flex: 1;
106
- padding: .9rem 1rem;
107
- border: 1.5px solid var(--rule);
108
  border-radius: var(--r);
109
- background: var(--white);
110
- font-family: var(--ff-head);
111
- font-size: .9rem;
112
- font-weight: 600;
113
  cursor: pointer;
114
  transition: all .2s;
115
- display: flex;
116
- flex-direction: column;
117
- gap: .2rem;
118
- text-align: left;
 
 
 
 
 
 
 
 
 
 
119
  }
120
- .tab-btn .tab-icon { font-size: 1.3rem; }
121
- .tab-btn .tab-fw { font-size: .65rem; font-family: var(--ff-mono); color: var(--muted); font-weight: 400; }
122
- .tab-btn:hover { border-color: var(--ink); }
123
- .tab-btn.active { border-color: var(--ink); background: var(--ink); color: var(--white); }
124
- .tab-btn.active .tab-fw { color: var(--rule); }
125
 
126
- /* Drop zone */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  .drop-zone {
128
- border: 2px dashed var(--rule);
129
  border-radius: var(--r);
130
- min-height: 240px;
131
- display: flex;
132
- flex-direction: column;
133
- align-items: center;
134
- justify-content: center;
135
- gap: 1rem;
136
- cursor: pointer;
137
- transition: border-color .2s, background .2s;
138
- position: relative;
139
- overflow: hidden;
140
- background: var(--white);
141
- }
142
- .drop-zone:hover, .drop-zone.drag { border-color: var(--accent); background: #fdf8f6; }
143
- .drop-zone img {
144
  position: absolute; inset: 0;
145
- width: 100%; height: 100%;
146
- object-fit: cover;
147
- border-radius: calc(var(--r) - 2px);
148
- display: none;
149
- }
150
- .drop-zone.has-img img { display: block; }
151
- .drop-zone.has-img .dz-ph { display: none; }
 
 
 
 
 
 
152
  .dz-ph {
153
- display: flex; flex-direction: column; align-items: center; gap: .75rem;
154
- color: var(--muted); pointer-events: none;
155
  }
156
  .dz-icon {
157
- width: 52px; height: 52px; border: 1.5px solid var(--rule);
158
- border-radius: 50%; display: grid; place-items: center; font-size: 1.3rem;
 
159
  }
160
- .dz-ph p { font-size: .85rem; }
161
- .dz-ph em { font-size: .75rem; font-style: normal; font-family: var(--ff-mono); }
162
  #file-input { display: none; }
163
 
164
- /* Analyse button */
165
- #classify-btn {
166
- width: 100%;
167
- padding: 1.1rem;
168
- background: var(--accent);
169
- color: var(--white);
170
- border: none;
171
- border-radius: var(--r);
172
- font-family: var(--ff-head);
173
- font-size: 1.1rem;
174
- font-weight: 700;
175
- letter-spacing: .03em;
176
- cursor: pointer;
177
- transition: opacity .2s, transform .15s;
178
- position: relative;
179
- overflow: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  }
181
- #classify-btn::after {
182
  content: '';
183
- position: absolute; inset: 0;
184
- background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.12) 50%, transparent 100%);
185
- transform: translateX(-100%);
186
  }
187
- #classify-btn.loading::after {
188
- animation: shimmer 1.2s infinite;
189
  }
190
- @keyframes shimmer { to { transform: translateX(100%); } }
191
- #classify-btn:disabled { opacity: .4; cursor: default; }
192
- #classify-btn:not(:disabled):hover { opacity: .88; transform: translateY(-1px); }
193
-
194
- /* ── Right panel ── */
195
- .right-panel {
196
- padding-left: 3rem;
197
- display: flex;
198
- flex-direction: column;
199
- gap: 2.5rem;
200
  }
 
 
 
 
 
201
 
202
- /* Prediction hero */
203
- .pred-hero { display: flex; flex-direction: column; gap: .4rem; }
204
- .pred-waiting {
205
  display: flex; flex-direction: column;
206
- justify-content: center; align-items: flex-start;
207
- min-height: 120px; gap: .5rem;
 
 
 
 
 
 
 
 
 
208
  }
209
- .pred-waiting p { color: var(--muted); font-size: .9rem; line-height: 1.6; }
210
- .pred-waiting strong { color: var(--ink); }
 
 
 
 
 
211
 
212
- .pred-result { display: none; flex-direction: column; gap: .3rem; }
213
- .pred-result.show { display: flex; animation: fadeUp .35s ease; }
214
- @keyframes fadeUp { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:none} }
 
 
 
 
215
 
216
- .pred-cat {
217
- font-family: var(--ff-mono);
218
- font-size: .6rem; letter-spacing: .15em;
219
- color: var(--muted); text-transform: uppercase;
220
  }
221
- .pred-class {
222
  font-family: var(--ff-head);
223
- font-size: 3.5rem;
224
- font-weight: 800;
225
- letter-spacing: -.03em;
226
- line-height: 1;
227
- color: var(--accent);
228
  }
229
- .pred-conf {
230
- font-family: var(--ff-mono);
231
- font-size: .8rem;
232
- color: var(--muted);
233
  }
234
- .pred-conf strong { color: var(--ink); font-size: 1rem; }
235
 
236
- /* Conf bar */
237
  .conf-track {
238
- height: 4px; background: var(--rule);
239
  border-radius: 99px; overflow: hidden; margin-top: .5rem;
240
  }
241
  .conf-fill {
242
  height: 100%;
243
- background: var(--accent);
244
- border-radius: 99px;
245
- width: 0%;
246
- transition: width .8s cubic-bezier(.4,0,.2,1);
247
  }
248
 
249
  /* Divider */
250
- .divider {
251
- height: 1px; background: var(--rule);
252
  }
253
 
254
- /* Prob bars */
255
- .prob-section { display: flex; flex-direction: column; gap: 1rem; }
256
- .prob-row { display: flex; flex-direction: column; gap: .3rem; }
257
- .prob-meta {
258
- display: flex; justify-content: space-between; align-items: baseline;
259
- }
260
  .prob-name {
261
- font-family: var(--ff-body);
262
- font-size: .82rem;
263
- font-weight: 500;
264
- display: flex; align-items: center; gap: .4rem;
265
- }
266
- .prob-name .emoji { font-size: .9rem; }
267
- .prob-pct {
268
- font-family: var(--ff-mono);
269
- font-size: .72rem;
270
- color: var(--muted);
271
  }
 
 
 
272
  .prob-track {
273
- height: 6px; background: var(--paper2);
274
  border-radius: 99px; overflow: hidden;
275
  }
276
  .prob-fill {
277
- height: 100%;
278
- background: var(--rule);
279
- border-radius: 99px;
280
- width: 0%;
281
- transition: width .7s cubic-bezier(.4,0,.2,1);
 
 
282
  }
283
- .prob-row.top .prob-name { color: var(--accent); }
284
- .prob-row.top .prob-pct { color: var(--accent); }
285
- .prob-row.top .prob-fill { background: var(--accent); }
286
 
287
  /* Error */
288
- .pred-error {
289
  display: none; padding: 1rem;
290
- background: #fef2f2; border: 1px solid #fecaca;
291
- border-radius: var(--r); color: #dc2626;
292
- font-size: .85rem; line-height: 1.5;
293
- }
294
- .pred-error.show { display: block; animation: fadeUp .3s ease; }
295
-
296
- /* ── Footer ── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  footer {
298
- border-top: 1.5px solid var(--rule);
299
- padding: 1rem 2.5rem;
300
- display: flex;
301
- align-items: center;
302
- justify-content: space-between;
303
- }
304
- footer p { font-size: .72rem; color: var(--muted); font-family: var(--ff-mono); }
305
- .classes-pills { display: flex; gap: .4rem; flex-wrap: wrap; }
306
- .cpill {
307
- font-family: var(--ff-mono); font-size: .6rem; letter-spacing: .05em;
308
- padding: .2rem .6rem; border: 1px solid var(--rule); border-radius: 99px;
309
- color: var(--muted);
310
  }
 
 
 
 
311
 
312
- /* ── Responsive ── */
313
- @media (max-width: 720px) {
314
- main { grid-template-columns: 1fr; padding: 1.5rem 1rem; }
315
- .left-panel { padding-right: 0; border-right: none; border-bottom: 1.5px solid var(--rule); padding-bottom: 2rem; }
316
- .right-panel { padding-left: 0; }
317
- header { padding: 1rem; flex-wrap: wrap; }
318
  }
319
  </style>
320
  </head>
321
  <body>
322
 
323
- <header>
324
- <div class="site-name">SCENE<span>.</span>AI</div>
325
- <div class="site-sub">Intel Image Classifier · CNN</div>
326
- <div class="badge">by Parfait</div>
327
- </header>
328
-
329
- <main>
330
- <!-- LEFT : inputs -->
331
- <div class="left-panel">
332
-
333
- <div>
334
- <div class="panel-label">01 — Choisir le modèle</div>
335
- <div class="model-tabs">
336
- <button class="tab-btn active" data-fw="pytorch" onclick="selectModel(this)">
337
- <span class="tab-icon">⚡</span>
338
- PyTorch
339
- <span class="tab-fw">CNN_Torch · .pth</span>
340
- </button>
341
- <button class="tab-btn" data-fw="tensorflow" onclick="selectModel(this)">
342
- <span class="tab-icon">🧠</span>
343
- TensorFlow
344
- <span class="tab-fw">CNN_TF · .keras</span>
345
- </button>
346
- </div>
347
- </div>
348
 
349
- <div>
350
- <div class="panel-label">02 — Uploader une image</div>
351
- <div class="drop-zone" id="drop-zone" onclick="document.getElementById('file-input').click()">
352
- <img id="preview" src="" alt="preview"/>
353
- <div class="dz-ph">
354
- <div class="dz-icon">↑</div>
355
- <p>Glisser-déposer ou cliquer</p>
356
- <em>JPG, PNG, WEBP</em>
357
- </div>
358
- </div>
359
- <input type="file" id="file-input" accept="image/*"/>
360
  </div>
361
-
362
- <div>
363
- <div class="panel-label">03 Analyser</div>
364
- <button id="classify-btn" disabled onclick="classify()">
365
- Analyser l'image →
366
- </button>
367
  </div>
 
368
 
 
 
 
 
 
369
  </div>
370
 
371
- <!-- RIGHT : results -->
372
- <div class="right-panel">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
 
374
- <div class="pred-hero">
375
- <div class="panel-label">Résultat</div>
 
 
 
376
 
377
- <div class="pred-waiting" id="pred-waiting">
378
- <p>Uploadez une image et cliquez sur <strong>Analyser</strong> pour voir la prédiction du modèle CNN sur les 6 catégories Intel.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  </div>
380
 
381
- <div class="pred-result" id="pred-result">
382
- <div class="pred-cat">Classe prédite</div>
383
- <div class="pred-class" id="pred-class-name">—</div>
384
- <div class="pred-conf">Confiance : <strong id="pred-conf-val">—</strong></div>
385
- <div class="conf-track"><div class="conf-fill" id="conf-fill"></div></div>
 
 
 
 
386
  </div>
387
 
388
- <div class="pred-error" id="pred-error"></div>
 
 
 
389
  </div>
390
 
391
- <div class="divider"></div>
 
 
 
 
 
 
 
 
 
 
 
 
392
 
393
- <div class="prob-section" id="prob-section">
394
- <div class="panel-label">Scores par catégorie</div>
395
- <div id="prob-bars">
396
- <!-- generated by JS -->
397
- <div style="color:var(--muted);font-size:.85rem;">Les scores apparaîtront ici après l'analyse.</div>
 
 
 
 
 
 
 
 
398
  </div>
 
 
 
399
  </div>
400
 
401
  </div>
402
- </main>
403
-
404
- <footer>
405
- <p>Intel Image Classification · 6 classes · CNN PyTorch & TensorFlow</p>
406
- <div class="classes-pills">
407
- <span class="cpill">🏙 buildings</span>
408
- <span class="cpill">🌲 forest</span>
409
- <span class="cpill">🧊 glacier</span>
410
- <span class="cpill">⛰ mountain</span>
411
- <span class="cpill">🌊 sea</span>
412
- <span class="cpill">🛣 street</span>
 
413
  </div>
414
- </footer>
 
 
 
 
 
 
415
 
416
  <script>
417
  const EMOJIS = {buildings:"🏙",forest:"🌲",glacier:"🧊",mountain:"⛰",sea:"🌊",street:"🛣"};
418
- let selectedFile = null;
 
419
  let selectedFw = "pytorch";
 
 
 
420
 
421
- /* ── Model tabs ── */
422
- function selectModel(btn) {
 
 
 
 
 
 
 
 
423
  document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
424
  btn.classList.add("active");
425
- selectedFw = btn.dataset.fw;
 
 
 
426
  }
427
 
428
- /* ── File input ── */
 
429
  const fileInput = document.getElementById("file-input");
430
- const dropZone = document.getElementById("drop-zone");
431
- const preview = document.getElementById("preview");
432
- const btn = document.getElementById("classify-btn");
433
-
434
- fileInput.addEventListener("change", () => loadFile(fileInput.files[0]));
435
 
 
 
 
436
  dropZone.addEventListener("dragover", e => { e.preventDefault(); dropZone.classList.add("drag"); });
437
  dropZone.addEventListener("dragleave", () => dropZone.classList.remove("drag"));
438
  dropZone.addEventListener("drop", e => {
@@ -442,30 +667,65 @@
442
  });
443
 
444
  function loadFile(file) {
445
- if (!file) return;
446
  selectedFile = file;
447
- btn.disabled = false;
448
  const reader = new FileReader();
449
  reader.onload = e => {
450
- preview.src = e.target.result;
 
451
  dropZone.classList.add("has-img");
452
  };
453
  reader.readAsDataURL(file);
454
  resetResult();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  }
456
 
457
  /* ── Classify ── */
458
  async function classify() {
459
- if (!selectedFile) return;
460
-
461
  btn.disabled = true;
462
- btn.textContent = "Analyse en cours…";
463
  btn.classList.add("loading");
464
 
465
  const form = new FormData();
466
- form.append("image", selectedFile);
467
  form.append("model", selectedFw);
468
 
 
 
 
 
 
 
469
  try {
470
  const res = await fetch("/predict", { method: "POST", body: form });
471
  const data = await res.json();
@@ -474,70 +734,74 @@
474
  } catch(err) {
475
  showError(err.message);
476
  } finally {
477
- btn.disabled = false;
478
- btn.textContent = "Analyser l'image →";
479
  btn.classList.remove("loading");
 
480
  }
481
  }
482
 
483
- /* ── Display result ── */
484
  function showResult(data) {
485
- document.getElementById("pred-waiting").style.display = "none";
486
- document.getElementById("pred-error").classList.remove("show");
487
 
488
  const pct = Math.round(data.confidence * 100);
489
- document.getElementById("pred-class-name").textContent =
490
  (EMOJIS[data.class] || "") + " " + data.class.charAt(0).toUpperCase() + data.class.slice(1);
491
- document.getElementById("pred-conf-val").textContent = pct + "%";
492
 
493
- const result = document.getElementById("pred-result");
494
- result.classList.add("show");
 
 
495
 
496
  setTimeout(() => {
497
  document.getElementById("conf-fill").style.width = pct + "%";
498
  }, 60);
499
 
500
- // probability bars
501
- const container = document.getElementById("prob-bars");
502
- container.innerHTML = "";
503
- const sorted = Object.entries(data.probabilities).sort((a,b) => b[1]-a[1]);
504
- sorted.forEach(([cls, prob], i) => {
505
- const p = Math.round(prob * 100);
 
506
  const top = cls === data.class;
507
  const row = document.createElement("div");
508
  row.className = "prob-row" + (top ? " top" : "");
509
  row.innerHTML = `
510
  <div class="prob-meta">
511
- <span class="prob-name"><span class="emoji">${EMOJIS[cls]}</span>${cls}</span>
512
  <span class="prob-pct" id="pct-${cls}">0%</span>
513
  </div>
514
  <div class="prob-track">
515
  <div class="prob-fill" id="bar-${cls}"></div>
516
  </div>`;
517
- container.appendChild(row);
518
  setTimeout(() => {
519
  document.getElementById("bar-" + cls).style.width = p + "%";
520
  document.getElementById("pct-" + cls).textContent = p + "%";
521
- }, 80 + i * 40);
522
  });
523
  }
524
 
525
  function showError(msg) {
526
- document.getElementById("pred-waiting").style.display = "none";
527
- document.getElementById("pred-result").classList.remove("show");
528
- const err = document.getElementById("pred-error");
529
- err.textContent = "Erreur : " + msg;
530
  err.classList.add("show");
531
  }
532
 
533
  function resetResult() {
534
- document.getElementById("pred-waiting").style.display = "flex";
535
- document.getElementById("pred-result").classList.remove("show");
536
- document.getElementById("pred-error").classList.remove("show");
537
  document.getElementById("conf-fill").style.width = "0%";
538
- document.getElementById("prob-bars").innerHTML =
539
- "<div style='color:var(--muted);font-size:.85rem;'>Les scores apparaîtront ici après l'analyse.</div>";
540
  }
 
 
 
541
  </script>
542
  </body>
543
  </html>
 
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>SCENEIQ Intel Scene Classifier</title>
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Rajdhani:wght@400;500;600;700&family=Exo+2:wght@300;400;600&display=swap" rel="stylesheet">
9
  <style>
10
+ /* ══════════════════════════════════════════════════════════════════
11
+ TOKENS
12
+ ══════════════════════════════════════════════════════════════════ */
13
  :root {
14
+ --g0: #000000;
15
+ --g1: #050d05;
16
+ --g2: #0a150a;
17
+ --g3: #0f1f0f;
18
+ --g4: #1a2e1a;
19
+ --green: #00ff41;
20
+ --green2: #00cc33;
21
+ --green3: #008f1f;
22
+ --green-dim:#004d11;
23
+ --green-glow: rgba(0,255,65,0.15);
24
+ --border: rgba(0,255,65,0.18);
25
+ --border2: rgba(0,255,65,0.35);
26
+ --muted: rgba(0,255,65,0.45);
27
+ --ff-mono: 'Share Tech Mono', monospace;
28
+ --ff-head: 'Rajdhani', sans-serif;
29
+ --ff-body: 'Exo 2', sans-serif;
30
+ --r: 4px;
31
  }
32
+
33
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
34
+ html { font-size: 16px; scroll-behavior: smooth; }
35
 
36
  body {
37
+ background: var(--g0);
38
+ color: var(--green);
39
  font-family: var(--ff-body);
40
  min-height: 100vh;
41
+ overflow-x: hidden;
42
+ cursor: default;
43
  }
44
 
45
+ /* ── Grid overlay ── */
46
+ body::before {
47
+ content: '';
48
+ position: fixed; inset: 0; z-index: 0; pointer-events: none;
49
+ background-image:
50
+ linear-gradient(rgba(0,255,65,0.03) 1px, transparent 1px),
51
+ linear-gradient(90deg, rgba(0,255,65,0.03) 1px, transparent 1px);
52
+ background-size: 40px 40px;
53
+ }
54
+ /* Scanline overlay */
55
+ body::after {
56
+ content: '';
57
+ position: fixed; inset: 0; z-index: 0; pointer-events: none;
58
+ background: repeating-linear-gradient(
59
+ 0deg,
60
+ transparent,
61
+ transparent 2px,
62
+ rgba(0,0,0,0.07) 2px,
63
+ rgba(0,0,0,0.07) 4px
64
+ );
65
+ }
66
+
67
+ /* ══════════════════════════════════════════════════════════════════
68
+ LAYOUT
69
+ ══════════════════════════════════════════════════════════════════ */
70
+ .wrapper {
71
+ position: relative; z-index: 1;
72
+ max-width: 1140px;
73
+ margin: 0 auto;
74
+ padding: 0 2rem;
75
+ }
76
+
77
+ /* ── HEADER ── */
78
  header {
79
+ border-bottom: 1px solid var(--border);
80
+ padding: 1.4rem 0;
81
  display: flex;
82
+ align-items: center;
83
+ justify-content: space-between;
84
+ animation: fadeIn .6s ease both;
85
+ }
86
+ .logo {
87
+ display: flex; align-items: center; gap: 1rem;
88
+ }
89
+ .logo-icon {
90
+ width: 38px; height: 38px;
91
+ border: 1.5px solid var(--green);
92
+ display: grid; place-items: center;
93
+ font-size: 1.1rem;
94
+ box-shadow: 0 0 14px var(--green-glow), inset 0 0 8px var(--green-glow);
95
+ animation: pulse-box 3s ease-in-out infinite;
96
+ }
97
+ @keyframes pulse-box {
98
+ 0%,100% { box-shadow: 0 0 10px var(--green-glow), inset 0 0 6px var(--green-glow); }
99
+ 50% { box-shadow: 0 0 22px rgba(0,255,65,.3), inset 0 0 14px rgba(0,255,65,.2); }
100
+ }
101
+ .logo-text {
102
+ font-family: var(--ff-head);
103
+ font-size: 1.8rem; font-weight: 700;
104
+ letter-spacing: .15em;
105
+ text-shadow: 0 0 20px rgba(0,255,65,.6);
106
+ }
107
+ .logo-text span { color: var(--green3); }
108
+ .header-right {
109
+ display: flex; align-items: center; gap: 1.5rem;
110
  }
111
+ .status-dot {
112
+ display: flex; align-items: center; gap: .45rem;
113
+ font-family: var(--ff-mono); font-size: .65rem; color: var(--muted);
114
+ }
115
+ .dot {
116
+ width: 6px; height: 6px; border-radius: 50%;
117
+ background: var(--green);
118
+ box-shadow: 0 0 8px var(--green);
119
+ animation: blink 2s step-end infinite;
120
+ }
121
+ @keyframes blink { 0%,100%{opacity:1} 50%{opacity:.2} }
122
+ .version {
123
+ font-family: var(--ff-mono); font-size: .6rem;
124
+ color: var(--green-dim); letter-spacing: .1em;
125
+ }
126
+
127
+ /* ── HERO ── */
128
+ .hero {
129
+ padding: 3rem 0 2rem;
130
+ animation: fadeIn .7s ease .1s both;
131
+ }
132
+ .hero-label {
133
+ font-family: var(--ff-mono); font-size: .62rem;
134
+ color: var(--green3); letter-spacing: .2em;
135
+ text-transform: uppercase; margin-bottom: .6rem;
136
+ }
137
+ .hero-title {
138
  font-family: var(--ff-head);
139
+ font-size: clamp(2.2rem, 5vw, 3.4rem);
140
+ font-weight: 700; letter-spacing: -.01em;
141
+ line-height: 1.05;
142
+ text-shadow: 0 0 40px rgba(0,255,65,.3);
143
+ }
144
+ .hero-title em {
145
+ font-style: normal; color: var(--green2);
146
+ text-shadow: 0 0 30px rgba(0,204,51,.6);
147
+ }
148
+ .hero-sub {
149
+ margin-top: .8rem;
150
+ font-size: .9rem; color: var(--muted);
151
+ font-family: var(--ff-mono); letter-spacing: .04em;
152
+ }
153
+
154
+ /* ── MAIN GRID ── */
155
+ .main-grid {
 
 
 
 
 
 
 
 
156
  display: grid;
157
  grid-template-columns: 1fr 1fr;
158
+ gap: 1.5rem;
159
+ padding-bottom: 4rem;
160
+ animation: fadeIn .8s ease .2s both;
 
 
161
  }
162
 
163
+ /* ── PANEL BASE ── */
164
+ .panel {
165
+ background: var(--g2);
166
+ border: 1px solid var(--border);
167
+ border-radius: var(--r);
168
+ padding: 1.75rem;
169
+ position: relative;
170
+ overflow: hidden;
171
  }
172
+ .panel::before {
173
+ content: '';
174
+ position: absolute; top: 0; left: 0; right: 0;
175
+ height: 2px;
176
+ background: linear-gradient(90deg, transparent, var(--green3), transparent);
177
+ }
178
+ .panel-title {
179
+ font-family: var(--ff-mono); font-size: .6rem;
180
+ letter-spacing: .2em; color: var(--green3);
181
+ text-transform: uppercase; margin-bottom: 1.4rem;
182
+ display: flex; align-items: center; gap: .6rem;
183
+ }
184
+ .panel-title::after {
185
+ content: ''; flex: 1; height: 1px; background: var(--border);
186
  }
187
 
188
+ /* ── MODEL SELECTOR ── */
189
+ .model-grid {
190
+ display: grid; grid-template-columns: 1fr 1fr; gap: .75rem;
191
+ margin-bottom: 1.5rem;
192
  }
193
+ .model-card {
194
+ border: 1px solid var(--border);
 
 
195
  border-radius: var(--r);
196
+ padding: 1rem .9rem;
 
 
 
197
  cursor: pointer;
198
  transition: all .2s;
199
+ background: var(--g1);
200
+ display: flex; flex-direction: column; gap: .3rem;
201
+ }
202
+ .model-card:hover { border-color: var(--green3); background: var(--g3); }
203
+ .model-card.active {
204
+ border-color: var(--green);
205
+ background: rgba(0,255,65,.06);
206
+ box-shadow: 0 0 16px rgba(0,255,65,.12), inset 0 0 12px rgba(0,255,65,.04);
207
+ }
208
+ .model-card.active .mc-name { color: var(--green); }
209
+ .mc-icon { font-size: 1.2rem; }
210
+ .mc-name {
211
+ font-family: var(--ff-head); font-weight: 600; font-size: 1rem;
212
+ color: var(--green2); transition: color .2s;
213
  }
214
+ .mc-sub { font-family: var(--ff-mono); font-size: .58rem; color: var(--muted); }
 
 
 
 
215
 
216
+ /* ── INPUT TABS ── */
217
+ .input-tabs {
218
+ display: flex; gap: 0; margin-bottom: 1rem;
219
+ border: 1px solid var(--border); border-radius: var(--r); overflow: hidden;
220
+ }
221
+ .tab-btn {
222
+ flex: 1; padding: .55rem; background: var(--g1);
223
+ border: none; color: var(--muted); font-family: var(--ff-mono);
224
+ font-size: .65rem; letter-spacing: .1em; cursor: pointer;
225
+ transition: all .2s; text-transform: uppercase;
226
+ }
227
+ .tab-btn:hover { background: var(--g3); color: var(--green2); }
228
+ .tab-btn.active {
229
+ background: rgba(0,255,65,.08); color: var(--green);
230
+ box-shadow: inset 0 -2px 0 var(--green);
231
+ }
232
+
233
+ /* ── DROP ZONE ── */
234
  .drop-zone {
235
+ border: 1.5px dashed var(--border2);
236
  border-radius: var(--r);
237
+ min-height: 190px;
238
+ display: flex; flex-direction: column;
239
+ align-items: center; justify-content: center;
240
+ gap: .75rem; cursor: pointer;
241
+ transition: all .25s; background: var(--g1);
242
+ position: relative; overflow: hidden;
243
+ }
244
+ .drop-zone:hover, .drop-zone.drag {
245
+ border-color: var(--green);
246
+ background: rgba(0,255,65,.04);
247
+ box-shadow: 0 0 20px rgba(0,255,65,.08);
248
+ }
249
+ .drop-zone.has-img .dz-ph { display: none; }
250
+ #preview-img {
251
  position: absolute; inset: 0;
252
+ width: 100%; height: 100%; object-fit: cover;
253
+ display: none; border-radius: calc(var(--r) - 1px);
254
+ opacity: .85;
255
+ }
256
+ .drop-zone.has-img #preview-img { display: block; }
257
+ .dz-corner {
258
+ position: absolute; width: 14px; height: 14px;
259
+ border-color: var(--green3); border-style: solid;
260
+ }
261
+ .dz-corner.tl { top: 8px; left: 8px; border-width: 1.5px 0 0 1.5px; }
262
+ .dz-corner.tr { top: 8px; right: 8px; border-width: 1.5px 1.5px 0 0; }
263
+ .dz-corner.bl { bottom: 8px; left: 8px; border-width: 0 0 1.5px 1.5px; }
264
+ .dz-corner.br { bottom: 8px; right: 8px; border-width: 0 1.5px 1.5px 0; }
265
  .dz-ph {
266
+ display: flex; flex-direction: column; align-items: center; gap: .6rem;
267
+ pointer-events: none;
268
  }
269
  .dz-icon {
270
+ width: 48px; height: 48px; border: 1px solid var(--border2);
271
+ border-radius: 50%; display: grid; place-items: center;
272
+ font-size: 1.3rem; color: var(--green3);
273
  }
274
+ .dz-ph p { font-family: var(--ff-mono); font-size: .72rem; color: var(--muted); }
275
+ .dz-ph em { font-family: var(--ff-mono); font-size: .6rem; color: var(--green-dim); font-style: normal; }
276
  #file-input { display: none; }
277
 
278
+ /* ── URL INPUT ── */
279
+ .url-input-wrap { display: none; flex-direction: column; gap: .6rem; }
280
+ .url-input-wrap.show { display: flex; }
281
+ .url-field {
282
+ display: flex; align-items: center;
283
+ border: 1px solid var(--border2); border-radius: var(--r);
284
+ background: var(--g1); overflow: hidden;
285
+ }
286
+ .url-prefix {
287
+ font-family: var(--ff-mono); font-size: .65rem;
288
+ color: var(--green3); padding: 0 .7rem; white-space: nowrap;
289
+ border-right: 1px solid var(--border);
290
+ }
291
+ .url-input {
292
+ flex: 1; background: transparent; border: none; outline: none;
293
+ color: var(--green); font-family: var(--ff-mono); font-size: .75rem;
294
+ padding: .75rem .8rem;
295
+ caret-color: var(--green);
296
+ }
297
+ .url-input::placeholder { color: var(--green-dim); }
298
+ .url-load-btn {
299
+ padding: .65rem .9rem;
300
+ background: rgba(0,255,65,.1); border: none;
301
+ border-left: 1px solid var(--border);
302
+ color: var(--green2); font-family: var(--ff-mono); font-size: .65rem;
303
+ cursor: pointer; transition: background .2s; white-space: nowrap;
304
+ }
305
+ .url-load-btn:hover { background: rgba(0,255,65,.2); }
306
+ .url-preview {
307
+ width: 100%; height: 140px; object-fit: cover;
308
+ border-radius: var(--r); border: 1px solid var(--border);
309
+ display: none;
310
+ }
311
+ .url-preview.show { display: block; }
312
+
313
+ /* ── CLASSIFY BTN ── */
314
+ .classify-btn {
315
+ width: 100%; margin-top: 1rem;
316
+ padding: 1rem;
317
+ background: linear-gradient(135deg, var(--green3), var(--green-dim));
318
+ border: 1px solid var(--green3); border-radius: var(--r);
319
+ color: var(--green); font-family: var(--ff-head);
320
+ font-size: 1.1rem; font-weight: 700; letter-spacing: .1em;
321
+ cursor: pointer; position: relative; overflow: hidden;
322
+ transition: all .2s; text-transform: uppercase;
323
  }
324
+ .classify-btn::before {
325
  content: '';
326
+ position: absolute; top: -2px; left: -100%; width: 60%; height: calc(100% + 4px);
327
+ background: linear-gradient(90deg, transparent, rgba(0,255,65,.18), transparent);
328
+ transform: skewX(-15deg);
329
  }
330
+ .classify-btn.loading::before {
331
+ animation: sweep 1.2s linear infinite;
332
  }
333
+ @keyframes sweep { to { left: 150%; } }
334
+ .classify-btn:not(:disabled):hover {
335
+ background: linear-gradient(135deg, var(--green2), var(--green3));
336
+ box-shadow: 0 0 24px rgba(0,255,65,.3);
337
+ transform: translateY(-1px);
 
 
 
 
 
338
  }
339
+ .classify-btn:disabled { opacity: .35; cursor: default; }
340
+
341
+ /* ══════════════════════════════════════════════════════════════════
342
+ RIGHT PANEL — RESULTS
343
+ ══════════════════════════════════════════════════════════════════ */
344
 
345
+ /* Waiting state */
346
+ .result-waiting {
 
347
  display: flex; flex-direction: column;
348
+ align-items: flex-start; justify-content: center;
349
+ min-height: 200px; gap: .6rem;
350
+ padding: 1rem 0;
351
+ }
352
+ .result-waiting .rw-title {
353
+ font-family: var(--ff-head); font-size: 1rem; font-weight: 600;
354
+ color: var(--green3);
355
+ }
356
+ .result-waiting .rw-sub {
357
+ font-family: var(--ff-mono); font-size: .7rem;
358
+ color: var(--green-dim); line-height: 1.7;
359
  }
360
+ .terminal-cursor {
361
+ display: inline-block; width: 8px; height: 14px;
362
+ background: var(--green); margin-left: 2px;
363
+ animation: blink-c .8s step-end infinite;
364
+ vertical-align: middle;
365
+ }
366
+ @keyframes blink-c { 0%,100%{opacity:1} 50%{opacity:0} }
367
 
368
+ /* Result */
369
+ .result-content { display: none; flex-direction: column; gap: 1.4rem; }
370
+ .result-content.show { display: flex; animation: scanIn .4s ease; }
371
+ @keyframes scanIn {
372
+ from { opacity: 0; clip-path: inset(0 0 100% 0); }
373
+ to { opacity: 1; clip-path: inset(0 0 0% 0); }
374
+ }
375
 
376
+ .rc-header { display: flex; flex-direction: column; gap: .25rem; }
377
+ .rc-label {
378
+ font-family: var(--ff-mono); font-size: .6rem;
379
+ letter-spacing: .2em; color: var(--green3);
380
  }
381
+ .rc-class {
382
  font-family: var(--ff-head);
383
+ font-size: 3rem; font-weight: 700;
384
+ letter-spacing: -.01em; line-height: 1;
385
+ color: var(--green);
386
+ text-shadow: 0 0 30px rgba(0,255,65,.5);
 
387
  }
388
+ .rc-conf {
389
+ font-family: var(--ff-mono); font-size: .78rem;
390
+ color: var(--muted); margin-top: .1rem;
 
391
  }
392
+ .rc-conf strong { color: var(--green2); font-size: .9rem; }
393
 
394
+ /* Main confidence bar */
395
  .conf-track {
396
+ height: 3px; background: var(--g4);
397
  border-radius: 99px; overflow: hidden; margin-top: .5rem;
398
  }
399
  .conf-fill {
400
  height: 100%;
401
+ background: linear-gradient(90deg, var(--green3), var(--green));
402
+ border-radius: 99px; width: 0%;
403
+ transition: width 1s cubic-bezier(.4,0,.2,1);
404
+ box-shadow: 0 0 8px var(--green);
405
  }
406
 
407
  /* Divider */
408
+ .rc-divider {
409
+ height: 1px; background: var(--border);
410
  }
411
 
412
+ /* Probability bars */
413
+ .prob-list { display: flex; flex-direction: column; gap: .85rem; }
414
+ .prob-row { display: flex; flex-direction: column; gap: .22rem; }
415
+ .prob-meta { display: flex; justify-content: space-between; align-items: baseline; }
 
 
416
  .prob-name {
417
+ font-family: var(--ff-body); font-size: .8rem;
418
+ font-weight: 400; color: var(--muted);
419
+ display: flex; align-items: center; gap: .4rem; text-transform: capitalize;
 
 
 
 
 
 
 
420
  }
421
+ .prob-row.top .prob-name { color: var(--green); font-weight: 600; }
422
+ .prob-pct { font-family: var(--ff-mono); font-size: .68rem; color: var(--green-dim); }
423
+ .prob-row.top .prob-pct { color: var(--green2); }
424
  .prob-track {
425
+ height: 4px; background: var(--g4);
426
  border-radius: 99px; overflow: hidden;
427
  }
428
  .prob-fill {
429
+ height: 100%; background: var(--green-dim);
430
+ border-radius: 99px; width: 0%;
431
+ transition: width .8s cubic-bezier(.4,0,.2,1);
432
+ }
433
+ .prob-row.top .prob-fill {
434
+ background: linear-gradient(90deg, var(--green3), var(--green));
435
+ box-shadow: 0 0 6px rgba(0,255,65,.4);
436
  }
 
 
 
437
 
438
  /* Error */
439
+ .result-error {
440
  display: none; padding: 1rem;
441
+ border: 1px solid rgba(255,50,50,.3);
442
+ border-radius: var(--r); background: rgba(255,0,0,.05);
443
+ font-family: var(--ff-mono); font-size: .75rem; color: #ff4444;
444
+ line-height: 1.6;
445
+ }
446
+ .result-error.show { display: block; animation: fadeIn .3s ease; }
447
+
448
+ /* ── CLASSES STRIP ── */
449
+ .classes-strip {
450
+ border-top: 1px solid var(--border);
451
+ padding: 1rem 0;
452
+ display: flex; align-items: center; gap: 1.2rem;
453
+ flex-wrap: wrap;
454
+ animation: fadeIn .9s ease .3s both;
455
+ }
456
+ .cs-label { font-family: var(--ff-mono); font-size: .55rem; color: var(--green-dim); letter-spacing: .15em; }
457
+ .cs-pills { display: flex; gap: .5rem; flex-wrap: wrap; }
458
+ .cs-pill {
459
+ font-family: var(--ff-mono); font-size: .6rem;
460
+ padding: .25rem .65rem;
461
+ border: 1px solid var(--border); border-radius: 2px;
462
+ color: var(--green-dim);
463
+ transition: all .2s; cursor: default;
464
+ }
465
+ .cs-pill:hover { border-color: var(--green3); color: var(--green2); }
466
+
467
+ /* ── FOOTER ── */
468
  footer {
469
+ border-top: 1px solid var(--border);
470
+ padding: 1rem 0;
471
+ display: flex; justify-content: space-between; align-items: center;
472
+ animation: fadeIn 1s ease .4s both;
 
 
 
 
 
 
 
 
473
  }
474
+ footer p { font-family: var(--ff-mono); font-size: .6rem; color: var(--green-dim); }
475
+
476
+ /* ── ANIMATIONS ── */
477
+ @keyframes fadeIn { from{opacity:0; transform:translateY(8px)} to{opacity:1;transform:none} }
478
 
479
+ /* ── RESPONSIVE ── */
480
+ @media (max-width: 740px) {
481
+ .main-grid { grid-template-columns: 1fr; }
482
+ .hero-title { font-size: 2rem; }
483
+ .model-grid { grid-template-columns: 1fr 1fr; }
 
484
  }
485
  </style>
486
  </head>
487
  <body>
488
 
489
+ <div class="wrapper">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
 
491
+ <!-- HEADER -->
492
+ <header>
493
+ <div class="logo">
494
+ <div class="logo-icon">⬡</div>
495
+ <div class="logo-text">SCENE<span>IQ</span></div>
 
 
 
 
 
 
496
  </div>
497
+ <div class="header-right">
498
+ <div class="status-dot"><div class="dot"></div> SYSTEM ONLINE</div>
499
+ <div class="version">v4.0 · PARFAIT</div>
 
 
 
500
  </div>
501
+ </header>
502
 
503
+ <!-- HERO -->
504
+ <div class="hero">
505
+ <div class="hero-label">// Intel Image Classification · CNN</div>
506
+ <h1 class="hero-title">Scene Recognition<br><em>Powered by Neural Networks</em></h1>
507
+ <p class="hero-sub">Upload an image or provide a URL — select your model — get instant predictions across 6 scene categories.</p>
508
  </div>
509
 
510
+ <!-- MAIN GRID -->
511
+ <div class="main-grid">
512
+
513
+ <!-- LEFT : inputs -->
514
+ <div class="panel">
515
+ <div class="panel-title">01 // MODEL_SELECT</div>
516
+
517
+ <div class="model-grid">
518
+ <div class="model-card active" data-fw="pytorch" onclick="selectModel(this)">
519
+ <span class="mc-icon">⚡</span>
520
+ <span class="mc-name">PyTorch</span>
521
+ <span class="mc-sub">CNN_Torch · .pth</span>
522
+ </div>
523
+ <div class="model-card" data-fw="tensorflow" onclick="selectModel(this)">
524
+ <span class="mc-icon">🧠</span>
525
+ <span class="mc-name">TensorFlow</span>
526
+ <span class="mc-sub">CNN_TF · .keras</span>
527
+ </div>
528
+ </div>
529
+
530
+ <div class="panel-title">02 // INPUT_IMAGE</div>
531
 
532
+ <!-- Tabs: File / URL -->
533
+ <div class="input-tabs">
534
+ <button class="tab-btn active" onclick="switchTab('file', this)">↑ FILE_UPLOAD</button>
535
+ <button class="tab-btn" onclick="switchTab('url', this)">⬡ IMAGE_URL</button>
536
+ </div>
537
 
538
+ <!-- File drop zone -->
539
+ <div id="tab-file">
540
+ <div class="drop-zone" id="drop-zone" onclick="document.getElementById('file-input').click()">
541
+ <div class="dz-corner tl"></div>
542
+ <div class="dz-corner tr"></div>
543
+ <div class="dz-corner bl"></div>
544
+ <div class="dz-corner br"></div>
545
+ <img id="preview-img" src="" alt="preview"/>
546
+ <div class="dz-ph">
547
+ <div class="dz-icon">↑</div>
548
+ <p>Drop image here or click</p>
549
+ <em>JPG · PNG · WEBP · GIF</em>
550
+ </div>
551
+ </div>
552
+ <input type="file" id="file-input" accept="image/*"/>
553
  </div>
554
 
555
+ <!-- URL input -->
556
+ <div id="tab-url" class="url-input-wrap">
557
+ <div class="url-field">
558
+ <span class="url-prefix">URL://</span>
559
+ <input type="text" class="url-input" id="url-input"
560
+ placeholder="https://example.com/image.jpg"/>
561
+ <button class="url-load-btn" onclick="loadFromUrl()">LOAD</button>
562
+ </div>
563
+ <img id="url-preview" class="url-preview" src="" alt="URL preview"/>
564
  </div>
565
 
566
+ <div class="panel-title" style="margin-top:1.2rem;">03 // ANALYZE</div>
567
+ <button class="classify-btn" id="classify-btn" disabled onclick="classify()">
568
+ RUN CLASSIFICATION →
569
+ </button>
570
  </div>
571
 
572
+ <!-- RIGHT : results -->
573
+ <div class="panel">
574
+ <div class="panel-title">04 // SCAN_RESULTS</div>
575
+
576
+ <!-- Waiting -->
577
+ <div class="result-waiting" id="result-waiting">
578
+ <div class="rw-title">AWAITING INPUT</div>
579
+ <div class="rw-sub">
580
+ &gt; select_model()<br>
581
+ &gt; load_image() <span class="terminal-cursor"></span><br>
582
+ &gt; predict()
583
+ </div>
584
+ </div>
585
 
586
+ <!-- Results -->
587
+ <div class="result-content" id="result-content">
588
+ <div class="rc-header">
589
+ <div class="rc-label">// PREDICTED_CLASS</div>
590
+ <div class="rc-class" id="rc-class"></div>
591
+ <div class="rc-conf">confidence : <strong id="rc-conf">—</strong></div>
592
+ <div class="conf-track"><div class="conf-fill" id="conf-fill"></div></div>
593
+ </div>
594
+ <div class="rc-divider"></div>
595
+ <div>
596
+ <div class="panel-title" style="margin-bottom:.9rem;">// CLASS_SCORES</div>
597
+ <div class="prob-list" id="prob-list"></div>
598
+ </div>
599
  </div>
600
+
601
+ <!-- Error -->
602
+ <div class="result-error" id="result-error"></div>
603
  </div>
604
 
605
  </div>
606
+
607
+ <!-- CLASSES STRIP -->
608
+ <div class="classes-strip">
609
+ <span class="cs-label">CLASSES</span>
610
+ <div class="cs-pills">
611
+ <span class="cs-pill">🏙 buildings</span>
612
+ <span class="cs-pill">🌲 forest</span>
613
+ <span class="cs-pill">🧊 glacier</span>
614
+ <span class="cs-pill">⛰ mountain</span>
615
+ <span class="cs-pill">🌊 sea</span>
616
+ <span class="cs-pill">🛣 street</span>
617
+ </div>
618
  </div>
619
+
620
+ <footer>
621
+ <p>SCENEIQ · Intel Image Classification · CNN PyTorch &amp; TensorFlow</p>
622
+ <p>by PARFAIT · seed=42 · reproducible</p>
623
+ </footer>
624
+
625
+ </div><!-- /wrapper -->
626
 
627
  <script>
628
  const EMOJIS = {buildings:"🏙",forest:"🌲",glacier:"🧊",mountain:"⛰",sea:"🌊",street:"🛣"};
629
+ const CLASSES = ["buildings","forest","glacier","mountain","sea","street"];
630
+
631
  let selectedFw = "pytorch";
632
+ let selectedFile = null;
633
+ let urlImageReady = false;
634
+ let currentTab = "file";
635
 
636
+ /* ── Model selector ── */
637
+ function selectModel(card) {
638
+ document.querySelectorAll(".model-card").forEach(c => c.classList.remove("active"));
639
+ card.classList.add("active");
640
+ selectedFw = card.dataset.fw;
641
+ }
642
+
643
+ /* ── Tab switcher ── */
644
+ function switchTab(tab, btn) {
645
+ currentTab = tab;
646
  document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
647
  btn.classList.add("active");
648
+ document.getElementById("tab-file").style.display = tab === "file" ? "block" : "none";
649
+ const urlWrap = document.getElementById("tab-url");
650
+ urlWrap.classList.toggle("show", tab === "url");
651
+ updateBtn();
652
  }
653
 
654
+ /* ── File drag & drop ── */
655
+ const dropZone = document.getElementById("drop-zone");
656
  const fileInput = document.getElementById("file-input");
 
 
 
 
 
657
 
658
+ fileInput.addEventListener("change", () => {
659
+ if (fileInput.files[0]) loadFile(fileInput.files[0]);
660
+ });
661
  dropZone.addEventListener("dragover", e => { e.preventDefault(); dropZone.classList.add("drag"); });
662
  dropZone.addEventListener("dragleave", () => dropZone.classList.remove("drag"));
663
  dropZone.addEventListener("drop", e => {
 
667
  });
668
 
669
  function loadFile(file) {
 
670
  selectedFile = file;
 
671
  const reader = new FileReader();
672
  reader.onload = e => {
673
+ const img = document.getElementById("preview-img");
674
+ img.src = e.target.result;
675
  dropZone.classList.add("has-img");
676
  };
677
  reader.readAsDataURL(file);
678
  resetResult();
679
+ updateBtn();
680
+ }
681
+
682
+ /* ── URL load ── */
683
+ function loadFromUrl() {
684
+ const url = document.getElementById("url-input").value.trim();
685
+ if (!url) return;
686
+ const preview = document.getElementById("url-preview");
687
+ preview.onload = () => {
688
+ preview.classList.add("show");
689
+ urlImageReady = true;
690
+ resetResult();
691
+ updateBtn();
692
+ };
693
+ preview.onerror = () => {
694
+ preview.classList.remove("show");
695
+ urlImageReady = false;
696
+ updateBtn();
697
+ };
698
+ preview.src = url;
699
+ }
700
+
701
+ // Allow Enter key on URL input
702
+ document.getElementById("url-input").addEventListener("keydown", e => {
703
+ if (e.key === "Enter") loadFromUrl();
704
+ });
705
+
706
+ /* ── Update classify button ── */
707
+ function updateBtn() {
708
+ const ready = (currentTab === "file" && selectedFile) ||
709
+ (currentTab === "url" && urlImageReady);
710
+ document.getElementById("classify-btn").disabled = !ready;
711
  }
712
 
713
  /* ── Classify ── */
714
  async function classify() {
715
+ const btn = document.getElementById("classify-btn");
 
716
  btn.disabled = true;
717
+ btn.textContent = "SCANNING…";
718
  btn.classList.add("loading");
719
 
720
  const form = new FormData();
 
721
  form.append("model", selectedFw);
722
 
723
+ if (currentTab === "file" && selectedFile) {
724
+ form.append("image", selectedFile);
725
+ } else {
726
+ form.append("image_url", document.getElementById("url-input").value.trim());
727
+ }
728
+
729
  try {
730
  const res = await fetch("/predict", { method: "POST", body: form });
731
  const data = await res.json();
 
734
  } catch(err) {
735
  showError(err.message);
736
  } finally {
737
+ btn.textContent = "RUN CLASSIFICATION →";
 
738
  btn.classList.remove("loading");
739
+ btn.disabled = false;
740
  }
741
  }
742
 
743
+ /* ── Show result ── */
744
  function showResult(data) {
745
+ document.getElementById("result-waiting").style.display = "none";
746
+ document.getElementById("result-error").classList.remove("show");
747
 
748
  const pct = Math.round(data.confidence * 100);
749
+ document.getElementById("rc-class").textContent =
750
  (EMOJIS[data.class] || "") + " " + data.class.charAt(0).toUpperCase() + data.class.slice(1);
751
+ document.getElementById("rc-conf").textContent = pct + "%";
752
 
753
+ const content = document.getElementById("result-content");
754
+ content.classList.remove("show");
755
+ void content.offsetWidth; // force reflow for animation
756
+ content.classList.add("show");
757
 
758
  setTimeout(() => {
759
  document.getElementById("conf-fill").style.width = pct + "%";
760
  }, 60);
761
 
762
+ // Probability bars
763
+ const list = document.getElementById("prob-list");
764
+ list.innerHTML = "";
765
+ const sorted = [...CLASSES].sort((a,b) => data.probabilities[b] - data.probabilities[a]);
766
+
767
+ sorted.forEach((cls, i) => {
768
+ const p = Math.round(data.probabilities[cls] * 100);
769
  const top = cls === data.class;
770
  const row = document.createElement("div");
771
  row.className = "prob-row" + (top ? " top" : "");
772
  row.innerHTML = `
773
  <div class="prob-meta">
774
+ <span class="prob-name">${EMOJIS[cls] || ""} ${cls}</span>
775
  <span class="prob-pct" id="pct-${cls}">0%</span>
776
  </div>
777
  <div class="prob-track">
778
  <div class="prob-fill" id="bar-${cls}"></div>
779
  </div>`;
780
+ list.appendChild(row);
781
  setTimeout(() => {
782
  document.getElementById("bar-" + cls).style.width = p + "%";
783
  document.getElementById("pct-" + cls).textContent = p + "%";
784
+ }, 100 + i * 50);
785
  });
786
  }
787
 
788
  function showError(msg) {
789
+ document.getElementById("result-waiting").style.display = "none";
790
+ document.getElementById("result-content").classList.remove("show");
791
+ const err = document.getElementById("result-error");
792
+ err.textContent = "ERROR > " + msg;
793
  err.classList.add("show");
794
  }
795
 
796
  function resetResult() {
797
+ document.getElementById("result-waiting").style.display = "flex";
798
+ document.getElementById("result-content").classList.remove("show");
799
+ document.getElementById("result-error").classList.remove("show");
800
  document.getElementById("conf-fill").style.width = "0%";
 
 
801
  }
802
+
803
+ // Init
804
+ document.getElementById("tab-file").style.display = "block";
805
  </script>
806
  </body>
807
  </html>