danielle2035's picture
add
46433f5
Raw
History Blame Contribute Delete
28.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GeoClassifier</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--primary: #2e7d32;
--primary-dark: #1b5e20;
--primary-light: #e8f5e9;
--bg: #f0f2f5;
--surface: #ffffff;
--text: #1f2937;
--text-sec: #9ca3af;
--border: #e5e7eb;
--radius-card: 16px;
--radius-btn: 10px;
--shadow: 0 4px 24px rgba(0,0,0,0.08);
}
[data-theme="dark"] {
--bg: #111827;
--surface: #1f2937;
--text: #f9fafb;
--text-sec: #9ca3af;
--border: #374151;
--primary-light: #1a3a1c;
--shadow: 0 4px 24px rgba(0,0,0,0.5);
}
body {
font-family: 'Inter', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
transition: background 0.3s, color 0.3s;
}
/* ── HEADER ── */
header {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 0 1.75rem;
display: flex;
align-items: center;
height: 60px;
gap: 0.6rem;
}
.brand-icon { color: var(--primary); display: flex; align-items: center; }
.brand-title {
font-weight: 800;
font-size: 1.05rem;
flex: 1;
letter-spacing: -0.01em;
}
.lang-btns { display: flex; align-items: center; gap: 2px; margin-right: 2px; }
.lang-btn {
background: none; border: none; cursor: pointer;
font-size: 0.78rem; font-weight: 600;
padding: 4px 7px; border-radius: 6px;
color: var(--text-sec);
font-family: inherit;
transition: color 0.2s;
letter-spacing: 0.03em;
}
.lang-btn.active { color: var(--text); }
.lang-btn:hover { color: var(--text); }
.icon-btn {
background: none; border: none; cursor: pointer;
color: var(--text); padding: 6px; border-radius: 50%;
font-size: 1.1rem; transition: background 0.2s;
display: flex; align-items: center; justify-content: center;
}
.icon-btn:hover { background: rgba(128,128,128,0.1); }
/* ── MAIN ── */
main {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 0.8rem 0.8rem;
}
/* ── CARD ── */
.card {
background: var(--surface);
border-radius: var(--radius-card);
box-shadow: var(--shadow);
padding: 1.2rem 1.2rem 1.2rem;
width: 100%;
max-width: 520px;
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-8px);
}
/* ── TYPOGRAPHY ── */
.title-main {
font-size: 1.9rem;
font-weight: 800;
text-align: center;
letter-spacing: -0.02em;
margin-bottom: 0.25rem;
}
.subtitle {
text-align: center;
color: var(--text-sec);
font-size: 0.86rem;
margin-bottom: 1.4rem;
}
/* ── FIELDSET-STYLE MODEL SELECTOR ── */
.model-fieldset {
position: relative;
border: 1px solid var(--border);
border-radius: var(--radius-btn);
margin-bottom: 1.75rem;
}
.model-fieldset legend {
position: absolute;
top: -0.6rem;
left: 1rem;
font-size: 0.78rem;
color: var(--text-sec);
background: var(--surface);
padding: 0 4px;
font-weight: 500;
}
.model-fieldset select {
width: 100%;
padding: 0.85rem 1rem 0.85rem 1rem;
border: none;
background: transparent;
color: var(--text);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
outline: none;
appearance: none;
cursor: pointer;
}
.model-fieldset .select-arrow {
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
color: var(--text-sec);
}
/* ── UPLOAD ROW ── */
.upload-row {
display: flex;
gap: 0.9rem;
align-items: flex-start;
margin-bottom: 1.2rem;
flex-wrap: wrap;
}
.upload-col { flex: 1 1 220px; }
.col-label {
font-size: 0.9rem;
font-weight: 500;
color: var(--text);
margin-bottom: 0.55rem;
display: block;
}
/* outlined button */
.btn-outlined {
display: flex; align-items: center; justify-content: center; gap: 0.45rem;
width: 100%; padding: 0.72rem 1rem;
border: 1.5px solid var(--border);
border-radius: var(--radius-btn);
background: transparent;
color: var(--primary);
font-family: inherit; font-weight: 600; font-size: 0.92rem;
cursor: pointer; text-decoration: none;
transition: background 0.2s;
}
.btn-outlined:hover { background: var(--primary-light); }
input[type="file"] { display: none; }
/* URL input with label */
.url-field {
position: relative;
border: 1px solid var(--border);
border-radius: var(--radius-btn);
margin-top: 0.75rem;
}
.url-field label {
position: absolute;
top: -0.58rem;
left: 0.85rem;
font-size: 0.72rem;
color: var(--text-sec);
background: var(--surface);
padding: 0 4px;
}
.url-field input {
width: 100%;
padding: 0.82rem 0.9rem 0.72rem;
border: none;
background: transparent;
color: var(--text);
font-family: inherit;
font-size: 0.87rem;
outline: none;
}
/* ── DIVIDER ── */
hr { border: none; border-top: 1px solid var(--border); margin: 0 0 1rem; }
/* ── CHIPS ── */
.classes-label {
font-size: 0.88rem;
color: var(--text-sec);
margin-bottom: 0.6rem;
display: block;
}
.chips-row { display: flex; flex-wrap: wrap; gap: 0.45rem; }
.chip {
display: inline-block; padding: 4px 14px;
border: 1px solid var(--border); border-radius: 999px;
font-size: 0.83rem; color: var(--text);
}
/* ── HIDDEN ── */
.hidden { display: none !important; }
/* ── PREVIEW CARD ── */
.preview-card {
width: 100%; max-width: 520px;
background: var(--surface);
border-radius: var(--radius-card);
box-shadow: var(--shadow);
padding: 0.95rem;
text-align: center;
margin-top: 0.9rem;
transition: transform 0.3s ease;
}
.preview-card:hover {
transform: translateY(-8px);
}
.preview-img {
width: 100%; max-height: 170px; object-fit: contain;
border-radius: 10px; display: block; margin: 0 auto;
}
.linear-progress {
width: 100%; height: 4px; background: var(--border);
border-radius: 2px; overflow: hidden; margin-top: 0.75rem;
}
.linear-progress-inner {
height: 100%; width: 40%; background: var(--primary);
border-radius: 2px; animation: indeterminate 1.4s infinite ease-in-out;
}
@keyframes indeterminate {
0% { transform: translateX(-100%); }
100% { transform: translateX(350%); }
}
.classify-btn {
margin-top: 1rem;
padding: 0.7rem 2rem;
background: var(--primary);
color: #fff;
border: none; border-radius: var(--radius-btn);
font-family: inherit; font-weight: 600; font-size: 0.95rem;
cursor: pointer; transition: background 0.2s;
}
.classify-btn:hover { background: var(--primary-dark); }
.classify-btn:disabled { opacity: 0.55; cursor: not-allowed; }
/* ── ERROR ── */
.error-panel {
background: #ffebee; border-radius: var(--radius-card);
padding: 0.9rem 1.1rem; color: #c62828;
margin-top: 1rem; max-width: 520px; width: 100%;
}
/* ── RESULT ── */
.result-card {
width: 100%; max-width: 520px;
background: var(--surface);
border-radius: var(--radius-card);
box-shadow: var(--shadow);
padding: 1.1rem;
text-align: center;
margin-top: 0.9rem;
transition: transform 0.3s ease;
}
.result-card:hover {
transform: translateY(-8px);
}
.result-header { font-size: 1rem; font-weight: 700; }
.result-chip {
display: inline-block; padding: 7px 22px;
border-radius: 999px; font-size: 1.05rem; font-weight: 700; margin-top: 0.65rem;
}
.chip-success { background: #e8f5e9; color: #2e7d32; }
.chip-error { background: #ffebee; color: #c62828; }
.result-conf { margin-top: 0.5rem; }
.result-btns { display: flex; justify-content: center; gap: 1rem; margin-top: 1.25rem; flex-wrap: wrap; }
.btn-result-outlined {
display: inline-flex; align-items: center; gap: 0.4rem;
padding: 0.6rem 1.4rem;
border: 1.5px solid var(--border); border-radius: var(--radius-btn);
background: transparent; color: var(--primary);
font-family: inherit; font-weight: 600; font-size: 0.9rem; cursor: pointer;
}
.btn-result-outlined:hover { background: var(--primary-light); }
.progress-track {
width: 100%; height: 8px; background: var(--border);
border-radius: 4px; overflow: hidden; margin-top: 0.5rem;
}
.progress-bar { height: 100%; background: var(--primary); border-radius: 4px; }
.prob-labels { display: flex; justify-content: space-between; margin-bottom: 3px; font-size: 0.85rem; font-weight: 600; }
.prob-row { margin-bottom: 0.85rem; }
.details-panel { overflow: hidden; max-height: 0; transition: max-height 0.35s ease; }
.details-panel.open { max-height: 600px; }
/* ── FOOTER ── */
footer {
border-top: 1px solid var(--border);
background: var(--surface);
padding: 0.75rem 1rem;
display: flex; align-items: center;
justify-content: space-between; flex-wrap: wrap; gap: 0.5rem;
}
footer p { font-size: 0.82rem; color: var(--text-sec); }
.footer-icons { display: flex; gap: 0.25rem; }
/* ── HIDE SCROLLBAR (keep scroll working) ── */
/* Chrome, Safari, Edge */
body::-webkit-scrollbar {
width: 0px;
background: transparent;
}
/* Firefox */
body {
scrollbar-width: none;
}
/* optionnel: si ton scroll est sur un container */
html {
scrollbar-width: none;
}
html::-webkit-scrollbar {
width: 0px;
background: transparent;
}
</style>
</head>
<body>
<!-- HEADER -->
<header>
<span class="brand-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="currentColor">
<path d="M2 18l4-7 5 4 4-8 5 9H2z"/>
</svg>
</span>
<span class="brand-title" id="header-title">GeoClassifier</span>
<div class="lang-btns">
<button class="lang-btn active" data-lang="en" onclick="setLang('en')">en</button>
<button class="lang-btn" data-lang="fr" onclick="setLang('fr')">fr</button>
<button class="lang-btn" data-lang="wo" onclick="setLang('wo')">wo</button>
</div>
<button class="icon-btn" onclick="toggleTheme()" title="Toggle theme">
<svg id="icon-dark" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.389 5.389 0 0 1-4.4 2.26 5.403 5.403 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1z"/>
</svg>
<svg id="icon-light" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" class="hidden">
<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0a.996.996 0 0 0 0-1.41l-1.06-1.06zm1.06-12.37l-1.06 1.06a.996.996 0 0 0 0 1.41c.39.39 1.03.39 1.41 0l1.06-1.06a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0zM7.05 18.36l-1.06 1.06a.996.996 0 0 0 0 1.41c.39.39 1.03.39 1.41 0l1.06-1.06a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0z"/>
</svg>
</button>
</header>
<!-- MAIN -->
<main>
<div style="width:100%;max-width:520px;display:flex;flex-direction:column;gap:0;">
<!-- INPUT CARD -->
<div class="card">
<h1 class="title-main" id="txt-title">GeoClassifier</h1>
<p class="subtitle" id="txt-subtitle">Classify natural scenes</p>
<!-- Model selector -->
<div class="model-fieldset">
<legend id="txt-selectModel">Select Model</legend>
<select id="model-choice" name="model_choice">
<option value="pytorch">PyTorch CNN</option>
<option value="tensorflow">TensorFlow CNN</option>
</select>
<span class="select-arrow">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M7 10l5 5 5-5z"/>
</svg>
</span>
</div>
<!-- Upload + URL row -->
<div class="upload-row">
<!-- Upload col -->
<div class="upload-col">
<span class="col-label" id="txt-upload">Upload Image</span>
<label class="btn-outlined" for="image">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"/>
</svg>
<span id="txt-upload-btn">Upload Image</span>
</label>
<input type="file" id="image" name="image" accept="image/*" />
</div>
<!-- URL col -->
<div class="upload-col">
<span class="col-label" id="txt-urlBtn">Load image from URL</span>
<button class="btn-outlined" onclick="toggleUrlInput()">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"/>
</svg>
<span id="txt-urlBtn-btn">Load image from URL</span>
</button>
<!-- URL input: shown by default to match screenshot -->
<div id="url-input-wrap" class="url-field hidden">
<label for="image-url">URL</label>
<input type="text" id="image-url" placeholder="https://..." />
</div>
</div>
</div>
<!-- Possible classes -->
<hr />
<span class="classes-label" id="txt-classes">Possible Classes:</span>
<div class="chips-row" id="chips-row"></div>
</div>
<!-- PREVIEW CARD -->
<div class="preview-card hidden" id="preview-card">
<img id="preview-img" class="preview-img" src="" alt="preview" />
<div class="linear-progress hidden" id="loading-bar">
<div class="linear-progress-inner"></div>
</div>
<div>
<button class="classify-btn" id="classify-btn" onclick="submitClassify()">
<span id="txt-classify">Classify</span>
</button>
</div>
</div>
<!-- ERROR -->
<div class="error-panel hidden" id="error-panel"></div>
<!-- RESULT CARD -->
<div class="result-card hidden" id="result-card">
<p class="result-header" id="txt-result">Result</p>
<div id="result-chip-wrap"></div>
<p class="result-conf" id="result-conf"></p>
<div class="details-panel" id="details-panel">
<div style="margin-top:1rem; text-align:left;" id="prob-bars"></div>
</div>
<div class="result-btns">
<button class="btn-result-outlined" onclick="resetAll()">
<span id="txt-reset">Reset</span>
</button>
<button class="btn-result-outlined" onclick="toggleDetails()">
<span id="txt-details">Details</span>
<svg id="expand-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="transition:transform 0.2s">
<path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/>
</svg>
</button>
</div>
</div>
</div>
</main>
<!-- FOOTER -->
<footer>
<p>© 2026 GeoClassifier By <strong>N.Tsemo Danielle</strong></p>
<div class="footer-icons">
<button class="icon-btn" title="GitHub" onclick="window.open('https://github.com/nguemtchuengdanielle/')">
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
</svg>
</button>
<button class="icon-btn" title="LinkedIn" style="color:#0077b5;" onclick="window.open('https://linkedin.com/in/danielle-tsemo3')">
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</button>
</div>
</footer>
<script>
const TEXTS = {
en: {
title: "GeoClassifier", subtitle: "Classify natural scenes",
upload: "Upload Image", urlBtn: "Load image from URL",
classify: "Classify", result: "Result", confidence: "Confidence",
selectModel: "Select Model", processing: "Processing...",
selectImage: "Please provide an image", classes: "Possible Classes:",
reset: "Reset", details: "Details", hideDetails: "Hide", unknown: "Image not recognized",
},
fr: {
title: "GeoClassifier", subtitle: "Classifiez des scènes naturelles",
upload: "Télécharger Image", urlBtn: "Charger image depuis URL",
classify: "Classer", result: "Résultat", confidence: "Confiance",
selectModel: "Choisir modèle", processing: "Traitement...",
selectImage: "Veuillez fournir une image", classes: "Classes possibles :",
reset: "Réinitialiser", details: "Détails", hideDetails: "Masquer", unknown: "Image non reconnue",
},
wo: {
title: "GeoClassifier", subtitle: "Jëfandikoo IA ngir xool nataal yi",
upload: "Yeb Nataal bi", urlBtn: "Yeb nataal ci URL",
classify: "Wone", result: "Njëg", confidence: "Loo xam ne",
selectModel: "Tànn modil", processing: "Di liggéey...",
selectImage: "Tànnal ab nataal", classes: "Yëgël yi :",
reset: "Tàkku", details: "Xam ci kanam", hideDetails: "Planque", unknown: "Nataal xamul",
}
};
const CLASS_LABELS = {
buildings: { en: "Buildings", fr: "Bâtiments", wo: "Kër yi" },
forest: { en: "Forest", fr: "Forêt", wo: "Géej bu wees" },
glacier: { en: "Glacier", fr: "Glacier", wo: "Dëkk bu sedd" },
mountain: { en: "Mountain", fr: "Montagne", wo: "Tund bi" },
sea: { en: "Sea", fr: "Mer", wo: "Géej bi" },
street: { en: "Street", fr: "Rue", wo: "Yoon bi" },
};
let lang = 'en';
let darkMode = false;
let detailsOpen = false;
let lastResult = null;
function t(key) { return TEXTS[lang][key] || key; }
function getLabel(cls) { return CLASS_LABELS[cls]?.[lang] ?? cls; }
function setLang(l) {
lang = l;
document.querySelectorAll('.lang-btn').forEach(b => b.classList.toggle('active', b.dataset.lang === l));
updateTexts();
buildChips();
if (lastResult) renderResult(lastResult);
}
function updateTexts() {
document.getElementById('header-title').textContent = t('title');
document.getElementById('txt-title').textContent = t('title');
document.getElementById('txt-subtitle').textContent = t('subtitle');
document.getElementById('txt-upload').textContent = t('upload');
document.getElementById('txt-upload-btn').textContent = t('upload');
document.getElementById('txt-urlBtn').textContent = t('urlBtn');
document.getElementById('txt-urlBtn-btn').textContent = t('urlBtn');
document.getElementById('txt-selectModel').textContent = t('selectModel');
document.getElementById('txt-classify').textContent = t('classify');
document.getElementById('txt-result').textContent = t('result');
document.getElementById('txt-reset').textContent = t('reset');
document.getElementById('txt-classes').textContent = t('classes');
document.getElementById('txt-details').textContent = detailsOpen ? t('hideDetails') : t('details');
}
function buildChips() {
document.getElementById('chips-row').innerHTML =
Object.keys(CLASS_LABELS).map(cls => `<span class="chip">${getLabel(cls)}</span>`).join('');
}
function toggleTheme() {
darkMode = !darkMode;
document.documentElement.setAttribute('data-theme', darkMode ? 'dark' : '');
document.getElementById('icon-dark').classList.toggle('hidden', darkMode);
document.getElementById('icon-light').classList.toggle('hidden', !darkMode);
// update URL field and model fieldset background for dark
document.querySelectorAll('.url-field label, .model-fieldset legend').forEach(el => {
el.style.background = darkMode ? '#1f2937' : '#ffffff';
});
}
const fileInput = document.getElementById('image');
const urlInput = document.getElementById('image-url');
fileInput.addEventListener('change', () => {
const file = fileInput.files[0];
if (!file) return;
urlInput.value = '';
showPreview(URL.createObjectURL(file));
clearResult();
});
urlInput.addEventListener('input', () => {
if (urlInput.value.trim()) {
fileInput.value = '';
showPreview(urlInput.value.trim());
clearResult();
}
});
function showPreview(src) {
const img = document.getElementById('preview-img');
const card = document.getElementById('preview-card');
img.src = src;
card.classList.remove('hidden');
}
function toggleUrlInput() {
const wrap = document.getElementById('url-input-wrap');
wrap.classList.toggle('hidden');
if (wrap.classList.contains('hidden')) {
urlInput.value = '';
}
}
function resetAll() {
fileInput.value = '';
urlInput.value = '';
document.getElementById('preview-card').classList.add('hidden');
document.getElementById('result-card').classList.add('hidden');
document.getElementById('error-panel').classList.add('hidden');
detailsOpen = false;
lastResult = null;
}
function clearResult() {
document.getElementById('result-card').classList.add('hidden');
document.getElementById('error-panel').classList.add('hidden');
}
function toggleDetails() {
detailsOpen = !detailsOpen;
document.getElementById('details-panel').classList.toggle('open', detailsOpen);
document.getElementById('txt-details').textContent = detailsOpen ? t('hideDetails') : t('details');
document.getElementById('expand-icon').style.transform = detailsOpen ? 'rotate(180deg)' : '';
}
function submitClassify() {
const file = fileInput.files[0];
const modelChoice = document.getElementById('model-choice').value;
const urlVal = urlInput.value.trim();
if (!file && !urlVal) { showError(t('selectImage')); return; }
// Validation basique de l'URL
if (!file && urlVal) {
try {
const url = new URL(urlVal);
if (!['http:', 'https:', 'data:'].includes(url.protocol)) {
showError('URL invalide. Utilisez http://, https:// ou data:');
return;
}
} catch (e) {
showError('URL invalide. Vérifiez le format.');
return;
}
}
const formData = new FormData();
if (file) {
formData.append('image', file);
} else {
formData.append('image_url', urlVal);
}
formData.append('model_choice', modelChoice);
setProcessing(true);
clearResult();
fetch('/predict', { method: 'POST', body: formData })
.then(r => r.ok ? r.json() : r.text().then(txt => { throw new Error(txt || 'Server error'); }))
.then(data => { lastResult = data; renderResult(data); })
.catch(err => showError('Error: ' + err.message))
.finally(() => setProcessing(false));
}
function renderResult(data) {
document.getElementById('result-card').classList.remove('hidden');
const isUnknown = !data.predicted_class || data.predicted_class === 'unknown';
document.getElementById('result-chip-wrap').innerHTML =
`<span class="result-chip ${isUnknown ? 'chip-error' : 'chip-success'}">${isUnknown ? t('unknown') : getLabel(data.predicted_class)}</span>`;
document.getElementById('result-conf').innerHTML = `${t('confidence')}: <strong>${data.confidence}</strong>`;
const barsWrap = document.getElementById('prob-bars');
barsWrap.innerHTML = '';
if (Array.isArray(data.probabilities)) {
data.probabilities.forEach(item => {
const pct = (item[1] * 100).toFixed(0);
const div = document.createElement('div');
div.className = 'prob-row';
div.innerHTML = `
<div class="prob-labels"><span>${getLabel(item[0])}</span><span>${pct}%</span></div>
<div class="progress-track"><div class="progress-bar" style="width:${pct}%"></div></div>`;
barsWrap.appendChild(div);
});
}
detailsOpen = false;
document.getElementById('details-panel').classList.remove('open');
document.getElementById('txt-details').textContent = t('details');
document.getElementById('expand-icon').style.transform = '';
}
function setProcessing(on) {
document.getElementById('classify-btn').disabled = on;
document.getElementById('txt-classify').textContent = on ? t('processing') : t('classify');
document.getElementById('loading-bar').classList.toggle('hidden', !on);
}
function showError(msg) {
const panel = document.getElementById('error-panel');
panel.textContent = msg;
panel.classList.remove('hidden');
}
buildChips();
updateTexts();
</script>
</body>
</html>