circuitglyph / index.html
Shehrin Sayed
Fix mobile responsiveness for header and logo
917f609
Raw
History Blame Contribute Delete
18.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>CircuitGlyph β€” Neural Network-Based Electronic Circuit Classifier v1.0.0</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet"/>
<style>
:root {
--bg: #000000;
--panel: #0e1018;
--border: #3b415e;
--accent: #033c42;
--accent2: #c49a1b;
--accent3: #00ff9d;
--text: #c8d0e8;
--text-dim: #8c6a0e;
--danger: #b55667;
--glow: 0 0 24px rgba(0,229,255,0.18);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg); color: var(--text);
font-family: 'Syne', sans-serif; min-height: 100vh; overflow-x: hidden;
}
body::before {
content: ''; position: fixed; inset: 0; z-index: 0;
background-image:
linear-gradient(rgba(51, 109, 116, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,229,255,0.03) 1px, transparent 1px);
background-size: 40px 40px; pointer-events: none;
}
.orb { position: fixed; border-radius: 50%; pointer-events: none; z-index: 0; filter: blur(90px); opacity: 0.18; }
.orb-1 { width: 500px; height: 500px; background: var(--accent); top: -120px; left: -100px; }
.orb-2 { width: 400px; height: 400px; background: var(--accent); bottom: -80px; right: -60px; }
.app { position: relative; z-index: 1; max-width: 960px; margin: 0 auto; padding: 48px 24px 80px; }
header { display: flex; align-items: center; gap: 16px; margin-bottom: 56px; }
.logo-mark {
width: 140px; height: 110px; border: 3px solid var(--accent2); border-radius: 12px;
display: grid; place-items: center; box-shadow: var(--glow); flex-shrink: 0;
}
.logo-text h1 { font-size: 3em; font-weight: 800; letter-spacing: -0.02em; color: var(--accent2); }
.logo-text p {
font-family: 'Space Mono', monospace; font-size: 0.68rem;
color: var(--text-dim); letter-spacing: 0.1em; text-transform: uppercase;
}
.main-grid { display: grid; grid-template-columns: 2fr 2fr; gap: 20px; margin-bottom: 20px; }
@media (max-width: 680px) { .main-grid { grid-template-columns: 2fr; } }
.panel { background: var(--panel); border: 1px solid var(--border); border-radius: 14px; overflow: hidden; }
.panel-header {
padding: 12px 18px; border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 8px;
}
.panel-header .dot { width: 8px; height: 8px; border-radius: 50%; }
.dot-cyan { background: var(--accent); }
.dot-purple { background: var(--accent2); }
.dot-green { background: var(--accent3); }
.panel-title {
font-family: 'Space Mono', monospace; font-size: 0.7rem;
letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-dim);
}
/* Upload zone */
.upload-zone {
padding: 28px 28px; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 14px;
cursor: pointer; min-height: 260px; transition: background 0.2s; position: relative;
}
.upload-zone.drag-over { background: rgba(0,229,255,0.06); }
.upload-zone input[type=file] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.upload-icon {
width: 56px; height: 56px; border: 1.5px dashed rgba(0, 229, 255, 0.691);
border-radius: 12px; display: grid; place-items: center; color: var(--accent); transition: all 0.2s;
}
.upload-zone:hover .upload-icon, .upload-zone.drag-over .upload-icon
{ border-color: var(--accent); box-shadow: var(--glow); }
.upload-label { text-align: center; }
.upload-label strong { display: block; font-size: 0.95rem; color: #fff; margin-bottom: 4px; }
.upload-label span { font-family: 'Space Mono', monospace; font-size: 0.68rem; color: var(--accent3); }
#preview-img { max-width: 100%; max-height: 210px; border-radius: 8px; object-fit: contain; display: none; }
#file-name {
font-family: 'Space Mono', monospace; font-size: 0.7rem; color: var(--accent); display: none;
padding: 4px 10px; background: rgba(0,229,255,0.07); border-radius: 4px;
word-break: break-all; text-align: center;
}
/* Analyze button */
.analyze-btn {
width: 100%; padding: 14px 0;
background: linear-gradient(55deg, var(--accent), var(--accent));
border: none; border-radius: 10px; color: #fff;
font-family: 'Syne', sans-serif; font-size: 1rem; font-weight: 700;
letter-spacing: 0.03em; cursor: pointer; transition: opacity 0.2s, transform 0.1s;
display: flex; align-items: center; justify-content: center; gap: 10px;
}
.analyze-btn:hover:not(:disabled) { opacity: 0.9; transform: translateY(-1px); }
.analyze-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* Processed image panel */
.proc-zone {
padding: 20px; min-height: 260px; display: flex; align-items: center; justify-content: center;
}
#proc-img { max-width: 100%; max-height: 310px; border-radius: 8px; object-fit: contain; display: none; }
.proc-placeholder {
font-family: 'Space Mono', monospace; font-size: 0.75rem;
color: var(--text-dim); text-align: center; line-height: 1.8;
}
/* Results */
.result-body { padding: 24px 22px; min-height: 160px; }
#typewriter-output {
font-family: 'Space Mono', monospace; font-size: 0.82rem;
line-height: 1.85; color: var(--text); white-space: pre-wrap; min-height: 40px;
}
.cursor {
display: inline-block; width: 2px; height: 1em; background: var(--accent);
vertical-align: middle; animation: blink 0.8s step-end infinite; margin-left: 2px;
}
@keyframes blink { 50% { opacity: 0; } }
/* Confidence bars */
.conf-bars { margin-top: 20px; display: none; }
.conf-bars.show { display: block; }
.conf-title {
font-family: 'Space Mono', monospace; font-size: 0.65rem;
letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-dim); margin-bottom: 12px;
}
.bar-row { display: grid; grid-template-columns: 180px 1fr 60px; align-items: center; gap: 12px; margin-bottom: 8px; }
.bar-label { font-family: 'Space Mono', monospace; font-size: 0.68rem; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bar-track { background: rgba(255,255,255,0.06); border-radius: 4px; height: 6px; overflow: hidden; }
.bar-fill {
height: 100%; border-radius: 4px;
background: linear-gradient(90deg, var(--accent2), var(--accent));
width: 0; transition: width 0.8s cubic-bezier(0.16,1,0.3,1);
}
.bar-pct { font-family: 'Space Mono', monospace; font-size: 0.68rem; color: var(--accent3); text-align: right; }
/* Status */
.status-row {
display: flex; align-items: center; gap: 10px;
padding: 12px 18px; border-top: 1px solid var(--border);
}
.spinner {
width: 14px; height: 14px; border: 2px solid rgba(0,229,255,0.15);
border-top-color: var(--accent); border-radius: 50%;
animation: spin 0.7s linear infinite; display: none;
}
@keyframes spin { to { transform: rotate(360deg); } }
.spinner.active { display: block; }
.status-text { font-family: 'Space Mono', monospace; font-size: 0.68rem; color: var(--text-dim); }
.status-text.error { color: var(--danger); }
.status-text.ok { color: var(--accent3); }
.models-strip { display: flex; gap: 8px; flex-wrap: wrap; padding: 0 18px 14px; }
.model-tag {
font-family: 'Space Mono', monospace; font-size: 0.6rem;
letter-spacing: 0.1em; text-transform: uppercase; padding: 3px 9px; border-radius: 4px;
}
.model-tag.eff { color: var(--accent3); background: rgba(123,92,255,0.1); border: 1px solid rgba(123,92,255,0.2); }
.model-tag.vit { color: var(--accent3); background: rgba(0,229,255,0.07); border: 1px solid rgba(0,229,255,0.18); }
footer {
margin-top: 52px; text-align: center;
font-family: 'Space Mono', monospace; font-size: 0.62rem;
color: var(--text-dim); letter-spacing: 0.08em; line-height: 2;
}
footer span { color: var(--accent2); }
@media (max-width: 480px) {
.app { padding: 24px 14px 60px; }
header { flex-direction: column; align-items: center; text-align: center; gap: 12px; margin-bottom: 32px; }
.logo-mark { width: 90px; height: 72px; }
.logo-mark img { width: 76px; height: 58px; }
.logo-text h1 { font-size: 1.8em; }
.logo-text p { font-size: 0.58rem; }
.bar-row { grid-template-columns: 120px 1fr 48px; }
}
</style>
</head>
<body>
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
<div class="app">
<header>
<div class="logo-mark">
<img src="https://ssayed122-circuitglyph.hf.space/logo.png" alt="CircuitGlyph logo" width="120" height="90" />
</div>
<div class="logo-text">
<h1>CircuitGlyph</h1>
<p>Neural Network-Based Electronic Circuit Classification System &nbsp;Β·&nbsp; v1.0.0</p>
</div>
</header>
<div class="main-grid">
<!-- Left: Upload -->
<div>
<div class="panel" style="margin-bottom:16px">
<div class="panel-header">
<div class="dot dot-cyan"></div>
<span class="panel-title">Input Image</span>
</div>
<div class="upload-zone" id="drop-zone">
<input type="file" id="file-input" accept="image/*"/>
<div class="upload-icon">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
</div>
<img id="preview-img" alt="Preview"/>
<div class="upload-label">
<strong>Drop your circuit image here</strong>
<span>or click to browse β€” PNG, JPG, BMP</span>
</div>
<div id="file-name"></div>
</div>
</div>
<button class="analyze-btn" id="analyze-btn" disabled>
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">
<!-- input layer -->
<circle cx="2" cy="3" r="2"/>
<circle cx="2" cy="12" r="2"/>
<circle cx="2" cy="21" r="2"/>
<!-- hidden layer -->
<circle cx="12" cy="3" r="2"/>
<circle cx="12" cy="12" r="2"/>
<circle cx="12" cy="21" r="2"/>
<!-- output layer -->
<circle cx="22" cy="10" r="2"/>
<circle cx="22" cy="14" r="2"/>
<!-- connections -->
<line x1="3.5" y1="3" x2="10.5" y2="3"/>
<line x1="3.5" y1="12" x2="10.5" y2="12"/>
<line x1="3.5" y1="21" x2="10.5" y2="21"/>
<line x1="13.5" y1="3" x2="19" y2="10"/>
<line x1="13.5" y1="12" x2="19" y2="10"/>
<line x1="13.5" y1="12" x2="19" y2="14"/>
<line x1="13.5" y1="21" x2="19" y2="14"/>
</svg>
Predict Functionality
</button>
</div>
<!-- Right: Processed image -->
<div>
<div class="panel" style="height:100%">
<div class="panel-header">
<div class="dot dot-purple"></div>
<span class="panel-title">Preprocessed Circuit Image</span>
</div>
<div class="proc-zone">
<img id="proc-img" alt="Processed"/>
<div class="proc-placeholder" id="proc-placeholder">
Cleaned, binarized, &amp; square-padded<br>circuit image will appear here
</div>
</div>
</div>
</div>
</div>
<!-- Results panel -->
<div class="panel">
<div class="panel-header">
<div class="dot dot-green"></div>
<span class="panel-title">Prediction Report</span>
</div>
<div class="models-strip">
<span class="model-tag vit">ViT</span>
<span class="model-tag eff">EfficientNet</span>
</div>
<div class="result-body">
<div id="typewriter-output">Awaiting image input…</div>
<div class="conf-bars" id="conf-bars">
<div class="conf-title">Top-5 Predictions</div>
<div id="bar-container"></div>
</div>
</div>
<div class="status-row">
<div class="spinner" id="spinner"></div>
<span class="status-text" id="status-text">Ready</span>
</div>
</div>
</div>
<footer>CircuitGlyph &nbsp;Β·&nbsp; <span>v1.0.0</span> &nbsp;Β·&nbsp; 51 circuit classes<br style="margin-bottom:6px"/>Developed by <span>Shehrin Sayed, Ph.D.</span> &nbsp;Β·&nbsp; CopyrightΒ© 2026. All rights reserved.</footer>
<script>
let selectedFile = null;
const fileInput = document.getElementById('file-input');
const previewImg = document.getElementById('preview-img');
const fileNameEl = document.getElementById('file-name');
const uploadLabel = document.querySelector('.upload-label');
const uploadIcon = document.querySelector('.upload-icon');
const analyzeBtn = document.getElementById('analyze-btn');
const procImg = document.getElementById('proc-img');
const procPH = document.getElementById('proc-placeholder');
const twOutput = document.getElementById('typewriter-output');
const confBars = document.getElementById('conf-bars');
const barContainer = document.getElementById('bar-container');
const spinner = document.getElementById('spinner');
const statusText = document.getElementById('status-text');
const dropZone = document.getElementById('drop-zone');
// ─── File selection ────────────────────────────────────────────────────────
fileInput.addEventListener('change', e => {
const f = e.target.files[0];
if (f) handleFile(f);
});
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
dropZone.addEventListener('drop', e => {
e.preventDefault(); dropZone.classList.remove('drag-over');
const f = e.dataTransfer.files[0];
if (f && f.type.startsWith('image/')) handleFile(f);
});
function handleFile(f) {
selectedFile = f;
previewImg.src = URL.createObjectURL(f);
previewImg.style.display = 'block';
uploadLabel.style.display = 'none';
uploadIcon.style.display = 'none';
fileNameEl.textContent = f.name;
fileNameEl.style.display = 'block';
analyzeBtn.disabled = false;
setStatus('Selected image loaded β€” click Predict Functionality', '');
twOutput.innerHTML = 'Selected image loaded. Click <strong style="color:var(--accent)">Predict Functionality</strong> to classify…';
confBars.classList.remove('show');
}
// ─── Analyze ────────────────────────────────────────────────────────────────
analyzeBtn.addEventListener('click', () => {
if (!selectedFile) return;
runAnalysis(selectedFile);
});
async function runAnalysis(file) {
analyzeBtn.disabled = true;
spinner.classList.add('active');
setStatus('Sending to Python backend…', '');
twOutput.innerHTML = '';
confBars.classList.remove('show');
procImg.style.display = 'none';
procPH.style.display = 'block';
try {
const formData = new FormData();
formData.append('image', file);
setStatus('Preprocessing + running inference…', '');
const response = await fetch('/analyze', { method: 'POST', body: formData });
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || `Server error ${response.status}`);
}
// Show processed image returned from backend
if (data.processed_img) {
procImg.src = 'data:image/png;base64,' + data.processed_img;
procImg.style.display = 'block';
procPH.style.display = 'none';
}
setStatus('Rendering results…', '');
spinner.classList.remove('active');
// Typewrite the pred_string exactly as Python produces it
typewrite(data.pred_string, 22, () => {
renderBars(data.top5);
confBars.classList.add('show');
setStatus('Analysis complete', 'ok');
});
} catch(err) {
spinner.classList.remove('active');
setStatus('Error: ' + err.message, 'error');
twOutput.innerHTML = `<span style="color:var(--danger)">βœ— ${err.message}</span>`;
analyzeBtn.disabled = false;
}
}
// ─── Typewriter ──────────────────────────────────────────────────────────────
function typewrite(text, speed, onDone) {
twOutput.innerHTML = '';
const cursor = document.createElement('span');
cursor.className = 'cursor';
twOutput.appendChild(cursor);
let i = 0;
const id = setInterval(() => {
if (i >= text.length) {
clearInterval(id);
cursor.remove();
if (onDone) onDone();
return;
}
cursor.insertAdjacentText('beforebegin', text[i++]);
}, speed);
}
// ─── Confidence bars ─────────────────────────────────────────────────────────
function renderBars(top5) {
barContainer.innerHTML = '';
top5.forEach(item => {
const pct = (item.conf * 100).toFixed(1);
const row = document.createElement('div');
row.className = 'bar-row';
row.innerHTML = `
<span class="bar-label">${item.name}</span>
<div class="bar-track"><div class="bar-fill" data-pct="${item.conf*100}"></div></div>
<span class="bar-pct">${pct}%</span>`;
barContainer.appendChild(row);
});
requestAnimationFrame(() => {
document.querySelectorAll('.bar-fill').forEach(el => {
el.style.width = el.dataset.pct + '%';
});
});
}
function setStatus(msg, cls) {
statusText.textContent = msg;
statusText.className = 'status-text' + (cls ? ' ' + cls : '');
}
</script>
</body>
</html>