cyberai-1
Add
e48e5be
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>SCENEIQ — Intel Scene Classifier | Enhanced Readability</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<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">
<style>
/* ══ TOKENS — SCALED FOR READABILITY ══ */
:root {
--g0:#000000; --g1:#071007; --g2:#0f1a0f; --g3:#152215;
--g4:#1f331f;
--green:#00ff41; --green2:#00dd33; --green3:#00aa22;
--green-dim:#00661a; --green-glow:rgba(0,255,65,0.2);
--border:rgba(0,255,65,0.22); --border2:rgba(0,255,65,0.45);
--muted:rgba(0,255,65,0.55);
--ff-mono:'Share Tech Mono',monospace;
--ff-head:'Rajdhani',sans-serif;
--ff-body:'Exo 2',sans-serif;
--r:8px;
}
*, *::before, *::after { box-sizing:border-box; margin:0; padding:0; }
html, body {
height: 100vh;
overflow: hidden;
font-size: 18px; /* base boost for readability */
}
body {
background: var(--g0);
color: var(--green);
font-family: var(--ff-body);
display: flex;
flex-direction: column;
line-height: 1.45;
}
/* ── Background layers (enhanced depth) ── */
#plexus-bg {
position: fixed; inset: 0; z-index: 0; pointer-events: none;
}
body::before {
content: ''; position: fixed; inset: 0; z-index: 0; pointer-events: none;
background-image:
linear-gradient(rgba(0,255,65,0.045) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,255,65,0.045) 1px, transparent 1px);
background-size: 40px 40px;
}
body::after {
content: ''; position: fixed; inset: 0; z-index: 0; pointer-events: none;
background: repeating-linear-gradient(
0deg, transparent, transparent 2px,
rgba(0,0,0,0.1) 2px, rgba(0,0,0,0.1) 5px
);
}
/* ══ WRAPPER — full height, spacious inner layout ══ */
.wrapper {
width: 100%;
max-width: 1400px;
margin: 0 auto;
padding: 0 1.5rem;
height: 100vh;
display: flex;
flex-direction: column;
gap: 0.25rem;
position: relative;
z-index: 2;
}
/* ══ HEADER — bold, crisp ══ */
header {
flex-shrink: 0;
border-bottom: 2px solid var(--border);
padding: 1.2rem 0 0.8rem 0;
display: flex; align-items: center; justify-content: space-between;
animation: fadeIn .5s ease both;
}
.logo { display:flex; align-items:center; gap:1rem; }
.logo-icon {
width:50px; height:50px; border:1.8px solid var(--green);
display:grid; place-items:center; font-size:1.4rem;
box-shadow: 0 0 18px var(--green-glow), inset 0 0 12px var(--green-glow);
animation: pulse-box 3s ease-in-out infinite;
background: rgba(0,255,65,0.02);
}
@keyframes pulse-box {
0%,100%{box-shadow:0 0 14px var(--green-glow),inset 0 0 8px var(--green-glow);}
50%{box-shadow:0 0 28px rgba(0,255,65,.4),inset 0 0 18px rgba(0,255,65,.2);}
}
.logo-text {
font-family:var(--ff-head); font-size:2.5rem; font-weight:700;
letter-spacing:.12em; text-shadow:0 0 22px rgba(0,255,65,.7);
}
.logo-text span { color:var(--green3); }
.header-right { display:flex; align-items:center; gap:1.6rem; }
.status-dot {
display:flex; align-items:center; gap:.9rem;
font-family:var(--ff-mono); font-size:0.85rem; color:var(--muted);
letter-spacing: 0.5px;
}
.dot {
width:9px; height:9px; border-radius:50%;
background:var(--green); box-shadow:0 0 12px var(--green);
animation:blink 2s step-end infinite;
}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.2}}
.version { font-family:var(--ff-mono); font-size:.85rem; color:var(--green-dim); letter-spacing:.12em; }
/* ── CLASSES BAR — bigger pills, readable ── */
.classes-bar {
flex-shrink: 0;
display: flex; align-items: center; gap: 1rem;
padding: 0.7rem 0;
border-bottom: 1px solid var(--border);
animation: fadeIn .6s ease .1s both;
}
.cs-label {
font-family:var(--ff-mono); font-size:0.85rem;
color:var(--green-dim); letter-spacing:.2em;
white-space:nowrap; font-weight:500;
}
.cs-pills { display:flex; gap:0.6rem; flex-wrap:wrap; }
.cs-pill {
font-family:var(--ff-mono); font-size:0.85rem;
padding:0.3rem 0.9rem;
border:1px solid var(--border); border-radius:20px;
color:var(--green-dim); transition:all .2s; cursor:default;
background: rgba(0,20,0,0.4);
backdrop-filter: blur(1px);
}
.cs-pill:hover { border-color:var(--green2); color:var(--green); background:rgba(0,255,65,0.08); }
/* ══ MAIN GRID — generous spacing, bigger cards ══ */
.main-grid {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
padding: 1rem 0 0.75rem 0;
animation: fadeIn .7s ease .15s both;
}
/* ══ PANELS — elevated, more padding, readable containers ══ */
.panel {
background: rgba(10, 21, 10, 0.92);
backdrop-filter: blur(2px);
border: 1px solid var(--border);
border-radius: var(--r);
padding: 1.5rem 1.6rem;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 8px 20px rgba(0,0,0,0.6), 0 0 0 1px rgba(0,255,65,0.05) inset;
transition: box-shadow 0.2s;
}
.panel:hover {
box-shadow: 0 10px 28px rgba(0,0,0,0.7), 0 0 0 1px rgba(0,255,65,0.15) inset;
}
.panel::before {
content:''; position:absolute; top:0; left:0; right:0; height:3px;
background:linear-gradient(90deg,transparent,var(--green2),transparent);
}
.panel-title {
flex-shrink: 0;
font-family:var(--ff-mono); font-size:0.9rem;
letter-spacing:.25em; color:var(--green3);
text-transform:uppercase; margin-bottom:1rem;
display:flex; align-items:center; gap:.8rem;
font-weight: 500;
}
.panel-title::after { content:''; flex:1; height:1.5px; background:var(--border); }
/* scrollable zones — smooth, no visible scrollbar */
.panel-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: var(--green3) var(--g2);
padding-right: 3px;
}
.panel-scroll::-webkit-scrollbar { width: 5px; }
.panel-scroll::-webkit-scrollbar-track { background: var(--g2); border-radius: 4px; }
.panel-scroll::-webkit-scrollbar-thumb { background: var(--green-dim); border-radius: 4px; }
/* ── MODEL SELECTOR (larger cards) ── */
.model-grid {
display:grid; grid-template-columns:1fr 1fr; gap:1rem;
margin-bottom:1.4rem;
}
.model-card {
border:1.5px solid var(--border); border-radius:12px;
padding:0.9rem 0.8rem; cursor:pointer; transition:all .2s;
background:var(--g1);
display:flex; flex-direction:column; gap:0.4rem;
}
.model-card:hover { border-color:var(--green2); background:rgba(0,255,65,0.08); transform:translateY(-2px); }
.model-card.active {
border-color:var(--green); background:rgba(0,255,65,.1);
box-shadow:0 0 20px rgba(0,255,65,.18),inset 0 0 12px rgba(0,255,65,.06);
}
.model-card.active .mc-name { color:var(--green); text-shadow:0 0 5px var(--green-glow);}
.mc-icon { font-size:1.6rem; }
.mc-name { font-family:var(--ff-head); font-weight:700; font-size:1.3rem; color:var(--green2); transition:color .2s; }
.mc-sub { font-family:var(--ff-mono); font-size:0.75rem; color:var(--muted); letter-spacing: -0.2px;}
/* ── INPUT TABS (bigger click area) ── */
.input-tabs {
display:flex; margin-bottom:1rem;
border:1px solid var(--border); border-radius:12px; overflow:hidden;
}
.tab-btn {
flex:1; padding:0.7rem 0.5rem; background:var(--g1);
border:none; color:var(--muted); font-family:var(--ff-mono);
font-size:0.85rem; letter-spacing:.1em; cursor:pointer;
transition:all .2s; text-transform:uppercase; font-weight:500;
}
.tab-btn:hover { background:var(--g3); color:var(--green2); }
.tab-btn.active {
background:rgba(0,255,65,.12); color:var(--green);
box-shadow:inset 0 -3px 0 var(--green);
}
/* ── DROP ZONE — taller, clearer text ── */
.drop-zone {
border:2px dashed var(--border2); border-radius:16px;
height: 210px;
display:flex; flex-direction:column;
align-items:center; justify-content:center;
gap:.7rem; cursor:pointer;
transition:all .25s; background:var(--g1);
position:relative; overflow:hidden;
}
.drop-zone:hover,.drop-zone.drag {
border-color:var(--green); background:rgba(0,255,65,.08);
box-shadow:0 0 28px rgba(0,255,65,.12);
}
.drop-zone.has-img .dz-ph { display:none; }
#preview-img {
position:absolute; inset:0; width:100%; height:100%;
object-fit:cover; display:none;
border-radius:14px; opacity:.9;
}
.drop-zone.has-img #preview-img { display:block; }
.dz-corner { position:absolute; width:14px; height:14px; border-color:var(--green2); border-style:solid; }
.dz-corner.tl{top:10px;left:10px;border-width:2px 0 0 2px;}
.dz-corner.tr{top:10px;right:10px;border-width:2px 2px 0 0;}
.dz-corner.bl{bottom:10px;left:10px;border-width:0 0 2px 2px;}
.dz-corner.br{bottom:10px;right:10px;border-width:0 2px 2px 0;}
.dz-ph { display:flex; flex-direction:column; align-items:center; gap:.5rem; pointer-events:none; }
.dz-icon {
width:48px; height:48px; border:1.5px solid var(--border2);
border-radius:50%; display:grid; place-items:center;
font-size:1.5rem; color:var(--green2);
}
.dz-ph p { font-family:var(--ff-mono); font-size:0.95rem; color:var(--muted); font-weight:500; }
.dz-ph em { font-family:var(--ff-mono); font-size:0.75rem; color:var(--green-dim); font-style:normal; }
#file-input { display:none; }
/* ── URL INPUT — bigger controls ── */
.url-input-wrap { display:none; flex-direction:column; gap:.8rem; }
.url-input-wrap.show { display:flex; }
.url-field {
display:flex; align-items:center;
border:1.5px solid var(--border2); border-radius:12px;
background:var(--g1); overflow:hidden;
}
.url-prefix { font-family:var(--ff-mono); font-size:0.85rem; color:var(--green2); padding:0 0.8rem; white-space:nowrap; border-right:1.5px solid var(--border); }
.url-input {
flex:1; background:transparent; border:none; outline:none;
color:var(--green); font-family:var(--ff-mono); font-size:0.9rem;
padding:0.8rem 1rem; caret-color:var(--green);
}
.url-input::placeholder { color:var(--green-dim); font-size:0.8rem; }
.url-load-btn {
padding:0.7rem 1rem; background:rgba(0,255,65,.12); border:none;
border-left:1.5px solid var(--border);
color:var(--green2); font-family:var(--ff-mono); font-size:0.85rem;
cursor:pointer; transition:background .2s; white-space:nowrap;
font-weight:600;
}
.url-load-btn:hover { background:rgba(0,255,65,.25); }
.url-preview {
width:100%; height:130px; object-fit:cover;
border-radius:12px; border:1px solid var(--border); display:none;
}
.url-preview.show { display:block; }
/* ── CLASSIFY BTN — bold, interactive ── */
.classify-btn {
flex-shrink: 0;
width:100%; margin-top:1rem;
padding:0.9rem 0.5rem;
background:linear-gradient(135deg,var(--green3),var(--green-dim));
border:1.5px solid var(--green3); border-radius:60px;
color:var(--green); font-family:var(--ff-head);
font-size:1.3rem; font-weight:700; letter-spacing:.12em;
cursor:pointer; position:relative; overflow:hidden;
transition:all .25s; text-transform:uppercase;
box-shadow:0 2px 8px rgba(0,0,0,0.3);
}
.classify-btn::before {
content:''; position:absolute; top:-2px; left:-100%;
width:60%; height:calc(100% + 4px);
background:linear-gradient(90deg,transparent,rgba(0,255,65,.25),transparent);
transform:skewX(-15deg);
}
.classify-btn.loading::before { animation:sweep 1.2s linear infinite; }
@keyframes sweep { to { left:150%; } }
.classify-btn:not(:disabled):hover {
background:linear-gradient(135deg,var(--green2),var(--green3));
box-shadow:0 0 30px rgba(0,255,65,.45); transform:translateY(-2px);
border-color: var(--green);
}
.classify-btn:disabled { opacity:.4; cursor:not-allowed; transform: none; }
/* ══ RIGHT PANEL — results, big class display, crisp probabilities ══ */
.result-panel-inner {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
padding-right: 4px;
}
.result-panel-inner::-webkit-scrollbar { width: 5px; }
.result-waiting {
display:flex; flex-direction:column;
align-items:flex-start; justify-content:center;
flex:1; gap:.8rem; padding:0.6rem 0;
}
.result-waiting .rw-title { font-family:var(--ff-head); font-size:1.5rem; font-weight:600; color:var(--green2); letter-spacing:-0.2px; }
.result-waiting .rw-sub { font-family:var(--ff-mono); font-size:1rem; color:var(--green-dim); line-height:1.9; }
.terminal-cursor {
display:inline-block; width:9px; height:18px;
background:var(--green); margin-left:4px;
animation:blink-c .8s step-end infinite; vertical-align:middle;
}
@keyframes blink-c{0%,100%{opacity:1}50%{opacity:0}}
.result-content { display:none; flex-direction:column; gap:1.4rem; }
.result-content.show { display:flex; animation:scanIn .45s ease; }
@keyframes scanIn {
from{opacity:0;clip-path:inset(0 0 100% 0)}
to{opacity:1;clip-path:inset(0 0 0% 0)}
}
.rc-header { display:flex; flex-direction:column; gap:.5rem; }
.rc-label { font-family:var(--ff-mono); font-size:0.7rem; letter-spacing:.25em; color:var(--green3); font-weight:500; }
.rc-class {
font-family:var(--ff-head); font-size:4rem; font-weight:800;
letter-spacing:-0.02em; line-height:1.1; color:var(--green);
text-shadow:0 0 28px rgba(0,255,65,.7);
word-break: break-word;
}
.rc-conf { font-family:var(--ff-mono); font-size:1rem; color:var(--muted); margin-top:.2rem; }
.rc-conf strong { color:var(--green2); font-size:1.2rem; }
.conf-track {
height:8px; background:var(--g4); border-radius:40px; overflow:hidden; margin-top:.6rem;
}
.conf-fill {
height:100%;
background:linear-gradient(90deg,var(--green3),var(--green));
border-radius:40px; width:0%;
transition:width 1s cubic-bezier(.2,.9,.3,1.1);
box-shadow:0 0 12px var(--green);
}
.rc-divider { height:1.5px; background:var(--border); flex-shrink:0; margin:.2rem 0; }
.prob-list { display:flex; flex-direction:column; gap:1rem; margin-top:0.3rem; }
.prob-row { display:flex; flex-direction:column; gap:.3rem; }
.prob-meta { display:flex; justify-content:space-between; align-items:baseline; }
.prob-name {
font-family:var(--ff-body); font-size:1rem; font-weight:500;
color:var(--muted); display:flex; align-items:center; gap:.5rem; text-transform:capitalize;
}
.prob-row.top .prob-name { color:var(--green); font-weight:700; text-shadow:0 0 3px rgba(0,255,65,0.3);}
.prob-pct { font-family:var(--ff-mono); font-size:0.85rem; color:var(--green-dim); font-weight:500; }
.prob-row.top .prob-pct { color:var(--green2); font-weight:600; }
.prob-track { height:7px; background:var(--g4); border-radius:40px; overflow:hidden; }
.prob-fill {
height:100%; background:var(--green-dim);
border-radius:40px; width:0%;
transition:width .8s cubic-bezier(.3,.8,.4,1);
}
.prob-row.top .prob-fill {
background:linear-gradient(90deg,var(--green3),var(--green));
box-shadow:0 0 10px rgba(0,255,65,.5);
}
.result-error {
display:none; padding:1rem;
border:1px solid rgba(255,70,70,.5); border-radius:12px;
background:rgba(255,30,30,.08);
font-family:var(--ff-mono); font-size:0.85rem; color:#ff7a7a; line-height:1.7;
}
.result-error.show { display:block; animation:fadeIn .3s ease; }
/* ══ FOOTER BAR — readable, relaxed ══ */
.footer-bar {
flex-shrink: 0;
border-top: 1.5px solid var(--border);
padding: 0.5rem 0;
display: flex; justify-content: space-between; align-items: center;
animation: fadeIn .8s ease .25s both;
}
.footer-bar p { font-family:var(--ff-mono); font-size:0.8rem; color:var(--green-dim); letter-spacing:0.3px; }
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
</style>
</head>
<body>
<canvas id="plexus-bg"></canvas>
<div class="wrapper">
<!-- HEADER -->
<header>
<div class="logo">
<div class="logo-icon"></div>
<div class="logo-text">SCENE<span>IQ</span></div>
</div>
<div class="header-right">
<div class="status-dot"><div class="dot"></div> SYSTEM ONLINE</div>
<div class="version">v4.0 · PARFAIT</div>
</div>
</header>
<!-- CLASSES BAR -->
<div class="classes-bar">
<span class="cs-label">CLASSES //</span>
<div class="cs-pills">
<span class="cs-pill">🏙 buildings</span>
<span class="cs-pill">🌲 forest</span>
<span class="cs-pill">🧊 glacier</span>
<span class="cs-pill">⛰ mountain</span>
<span class="cs-pill">🌊 sea</span>
<span class="cs-pill">🛣 street</span>
</div>
</div>
<!-- MAIN GRID -->
<div class="main-grid">
<!-- LEFT : inputs -->
<div class="panel">
<div class="panel-title">01 // MODEL_SELECT</div>
<div class="panel-scroll">
<div class="model-grid">
<div class="model-card active" data-fw="pytorch" onclick="selectModel(this)">
<span class="mc-icon"></span>
<span class="mc-name">PyTorch</span>
<span class="mc-sub">CNN_Torch · .pth</span>
</div>
<div class="model-card" data-fw="tensorflow" onclick="selectModel(this)">
<span class="mc-icon">🧠</span>
<span class="mc-name">TensorFlow</span>
<span class="mc-sub">CNN_TF · .keras</span>
</div>
</div>
<div class="panel-title">02 // INPUT_IMAGE</div>
<div class="input-tabs">
<button class="tab-btn active" onclick="switchTab('file', this)">↑ FILE_UPLOAD</button>
<button class="tab-btn" onclick="switchTab('url', this)">⬡ IMAGE_URL</button>
</div>
<!-- File drop zone -->
<div id="tab-file">
<div class="drop-zone" id="drop-zone" onclick="document.getElementById('file-input').click()">
<div class="dz-corner tl"></div><div class="dz-corner tr"></div>
<div class="dz-corner bl"></div><div class="dz-corner br"></div>
<img id="preview-img" src="" alt="preview"/>
<div class="dz-ph">
<div class="dz-icon"></div>
<p>Drop image here or click</p>
<em>JPG · PNG · WEBP · GIF</em>
</div>
</div>
<input type="file" id="file-input" accept="image/*"/>
</div>
<!-- URL input -->
<div id="tab-url" class="url-input-wrap">
<div class="url-field">
<span class="url-prefix">URL://</span>
<input type="text" class="url-input" id="url-input"
placeholder="https://example.com/image.jpg"/>
<button class="url-load-btn" onclick="loadFromUrl()">LOAD</button>
</div>
<img id="url-preview" class="url-preview" src="" alt="URL preview"/>
</div>
<div class="panel-title" style="margin-top:1rem;">03 // ANALYZE</div>
</div><!-- /panel-scroll -->
<button class="classify-btn" id="classify-btn" disabled onclick="classify()">
RUN CLASSIFICATION →
</button>
</div>
<!-- RIGHT : results -->
<div class="panel">
<div class="panel-title">04 // SCAN_RESULTS</div>
<div class="result-panel-inner">
<div class="result-waiting" id="result-waiting">
<div class="rw-title">AWAITING INPUT</div>
<div class="rw-sub">
&gt; select_model()<br>
&gt; load_image() <span class="terminal-cursor"></span><br>
&gt; predict()
</div>
</div>
<div class="result-content" id="result-content">
<div class="rc-header">
<div class="rc-label">// PREDICTED_CLASS</div>
<div class="rc-class" id="rc-class"></div>
<div class="rc-conf">confidence : <strong id="rc-conf"></strong></div>
<div class="conf-track"><div class="conf-fill" id="conf-fill"></div></div>
</div>
<div class="rc-divider"></div>
<div>
<div class="panel-title" style="margin-bottom:.8rem;">// CLASS_SCORES</div>
<div class="prob-list" id="prob-list"></div>
</div>
</div>
<div class="result-error" id="result-error"></div>
</div><!-- /result-panel-inner -->
</div>
</div><!-- /main-grid -->
<!-- FOOTER BAR -->
<div class="footer-bar">
<p>SCENEIQ · Intel Image Classification · CNN PyTorch &amp; TensorFlow</p>
<p>by PARFAIT · seed=42 · reproducible</p>
</div>
</div><!-- /wrapper -->
<script>
const EMOJIS = {buildings:"🏙",forest:"🌲",glacier:"🧊",mountain:"⛰",sea:"🌊",street:"🛣"};
const CLASSES = ["buildings","forest","glacier","mountain","sea","street"];
let selectedFw = "pytorch";
let selectedFile = null;
let urlImageReady = false;
let currentTab = "file";
function selectModel(card) {
document.querySelectorAll(".model-card").forEach(c => c.classList.remove("active"));
card.classList.add("active");
selectedFw = card.dataset.fw;
}
function switchTab(tab, btn) {
currentTab = tab;
document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
document.getElementById("tab-file").style.display = tab === "file" ? "block" : "none";
document.getElementById("tab-url").classList.toggle("show", tab === "url");
updateBtn();
}
const dropZone = document.getElementById("drop-zone");
const fileInput = document.getElementById("file-input");
fileInput.addEventListener("change", () => { if (fileInput.files[0]) loadFile(fileInput.files[0]); });
dropZone.addEventListener("dragover", e => { e.preventDefault(); dropZone.classList.add("drag"); });
dropZone.addEventListener("dragleave", () => dropZone.classList.remove("drag"));
dropZone.addEventListener("drop", e => {
e.preventDefault(); dropZone.classList.remove("drag");
const f = e.dataTransfer.files[0];
if (f && f.type.startsWith("image/")) loadFile(f);
});
function loadFile(file) {
selectedFile = file;
const reader = new FileReader();
reader.onload = e => {
document.getElementById("preview-img").src = e.target.result;
dropZone.classList.add("has-img");
};
reader.readAsDataURL(file);
resetResult(); updateBtn();
}
function loadFromUrl() {
const url = document.getElementById("url-input").value.trim();
if (!url) return;
const preview = document.getElementById("url-preview");
preview.onload = () => { preview.classList.add("show"); urlImageReady = true; resetResult(); updateBtn(); };
preview.onerror = () => { preview.classList.remove("show"); urlImageReady = false; updateBtn(); };
preview.src = url;
}
document.getElementById("url-input").addEventListener("keydown", e => { if (e.key === "Enter") loadFromUrl(); });
function updateBtn() {
const ready = (currentTab === "file" && selectedFile) || (currentTab === "url" && urlImageReady);
document.getElementById("classify-btn").disabled = !ready;
}
async function classify() {
const btn = document.getElementById("classify-btn");
btn.disabled = true; btn.textContent = "SCANNING…"; btn.classList.add("loading");
const form = new FormData();
form.append("model", selectedFw);
if (currentTab === "file" && selectedFile) form.append("image", selectedFile);
else form.append("image_url", document.getElementById("url-input").value.trim());
try {
const res = await fetch("/predict", { method: "POST", body: form });
const data = await res.json();
if (data.error) throw new Error(data.error);
showResult(data);
} catch(err) {
showError(err.message);
} finally {
btn.textContent = "RUN CLASSIFICATION →"; btn.classList.remove("loading"); btn.disabled = false;
}
}
function showResult(data) {
document.getElementById("result-waiting").style.display = "none";
document.getElementById("result-error").classList.remove("show");
const pct = Math.round(data.confidence * 100);
document.getElementById("rc-class").textContent =
(EMOJIS[data.class]||"") + " " + data.class.charAt(0).toUpperCase() + data.class.slice(1);
document.getElementById("rc-conf").textContent = pct + "%";
const content = document.getElementById("result-content");
content.classList.remove("show"); void content.offsetWidth; content.classList.add("show");
setTimeout(() => { document.getElementById("conf-fill").style.width = pct + "%"; }, 60);
const list = document.getElementById("prob-list");
list.innerHTML = "";
const sorted = [...CLASSES].sort((a,b) => data.probabilities[b]-data.probabilities[a]);
sorted.forEach((cls, i) => {
const p = Math.round(data.probabilities[cls] * 100);
const top = cls === data.class;
const row = document.createElement("div");
row.className = "prob-row" + (top ? " top" : "");
row.innerHTML = `
<div class="prob-meta">
<span class="prob-name">${EMOJIS[cls]||""} ${cls}</span>
<span class="prob-pct" id="pct-${cls}">0%</span>
</div>
<div class="prob-track"><div class="prob-fill" id="bar-${cls}"></div></div>`;
list.appendChild(row);
setTimeout(() => {
document.getElementById("bar-"+cls).style.width = p+"%";
document.getElementById("pct-"+cls).textContent = p+"%";
}, 100 + i*50);
});
}
function showError(msg) {
document.getElementById("result-waiting").style.display = "none";
document.getElementById("result-content").classList.remove("show");
const err = document.getElementById("result-error");
err.textContent = "ERROR > " + msg; err.classList.add("show");
}
function resetResult() {
document.getElementById("result-waiting").style.display = "flex";
document.getElementById("result-content").classList.remove("show");
document.getElementById("result-error").classList.remove("show");
document.getElementById("conf-fill").style.width = "0%";
}
document.getElementById("tab-file").style.display = "block";
</script>
<!-- PLEXUS BACKGROUND (enhanced) -->
<script>
(function(){
const canvas = document.getElementById('plexus-bg');
const ctx = canvas.getContext('2d');
const NODE_COLOR='rgba(0,255,65,', LINE_COLOR='rgba(0,255,65,';
const MAX_DIST=160, MOUSE_DIST=220, ATTRACT_FORCE=0.012, NODE_COUNT=110, SPEED=0.4;
let W, H, nodes=[], mouse={x:-9999,y:-9999};
window.addEventListener('mousemove', e=>{mouse.x=e.clientX;mouse.y=e.clientY;});
window.addEventListener('mouseleave',()=>{mouse.x=-9999;mouse.y=-9999;});
function resize(){W=canvas.width=window.innerWidth;H=canvas.height=window.innerHeight;}
function Node(){this.x=Math.random()*W;this.y=Math.random()*H;this.vx=(Math.random()-.5)*SPEED;this.vy=(Math.random()-.5)*SPEED;this.r=Math.random()*2+1.5;}
function init(){nodes=[];for(let i=0;i<NODE_COUNT;i++)nodes.push(new Node());}
function draw(){
ctx.clearRect(0,0,W,H);
for(const n of nodes){
const dxM=mouse.x-n.x,dyM=mouse.y-n.y,dM=Math.sqrt(dxM*dxM+dyM*dyM);
if(dM<MOUSE_DIST&&dM>0){const f=(1-dM/MOUSE_DIST)*ATTRACT_FORCE;n.vx+=dxM/dM*f;n.vy+=dyM/dM*f;}
const spd=Math.sqrt(n.vx*n.vx+n.vy*n.vy),maxSpd=SPEED*3;
if(spd>maxSpd){n.vx=(n.vx/spd)*maxSpd;n.vy=(n.vy/spd)*maxSpd;}
if(dM>=MOUSE_DIST){n.vx*=.996;n.vy*=.996;const s=Math.sqrt(n.vx*n.vx+n.vy*n.vy);if(s<SPEED*.3){n.vx+=(Math.random()-.5)*.06;n.vy+=(Math.random()-.5)*.06;}}
n.x+=n.vx;n.y+=n.vy;
if(n.x<0||n.x>W)n.vx*=-1;if(n.y<0||n.y>H)n.vy*=-1;
}
for(let i=0;i<nodes.length;i++)for(let j=i+1;j<nodes.length;j++){
const a=nodes[i],b=nodes[j],dx=a.x-b.x,dy=a.y-b.y,d=Math.sqrt(dx*dx+dy*dy);
if(d<MAX_DIST){const al=(1-d/MAX_DIST)*.28;ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.strokeStyle=LINE_COLOR+al+')';ctx.lineWidth=.9;ctx.stroke();}
}
for(const n of nodes){
const dxM=mouse.x-n.x,dyM=mouse.y-n.y,dM=Math.sqrt(dxM*dxM+dyM*dyM);
if(dM<MOUSE_DIST){const al=(1-dM/MOUSE_DIST)*.35;ctx.beginPath();ctx.moveTo(mouse.x,mouse.y);ctx.lineTo(n.x,n.y);ctx.strokeStyle=LINE_COLOR+al+')';ctx.lineWidth=.7;ctx.stroke();}
}
if(mouse.x>0){
const g=ctx.createRadialGradient(mouse.x,mouse.y,0,mouse.x,mouse.y,22);
g.addColorStop(0,'rgba(0,255,65,0.45)');g.addColorStop(1,'rgba(0,255,65,0)');
ctx.beginPath();ctx.arc(mouse.x,mouse.y,22,0,Math.PI*2);ctx.fillStyle=g;ctx.fill();
ctx.beginPath();ctx.arc(mouse.x,mouse.y,3,0,Math.PI*2);ctx.fillStyle='rgba(0,255,65,0.95)';ctx.fill();
}
for(const n of nodes){
const g=ctx.createRadialGradient(n.x,n.y,0,n.x,n.y,n.r*6);
g.addColorStop(0,NODE_COLOR+'0.35)');g.addColorStop(1,NODE_COLOR+'0)');
ctx.beginPath();ctx.arc(n.x,n.y,n.r*6,0,Math.PI*2);ctx.fillStyle=g;ctx.fill();
ctx.beginPath();ctx.arc(n.x,n.y,n.r,0,Math.PI*2);ctx.fillStyle=NODE_COLOR+'0.9)';ctx.fill();
}
requestAnimationFrame(draw);
}
resize();init();draw();
window.addEventListener('resize',()=>{resize();init();});
})();
</script>
</body>
</html>
```