| <!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 { |
| 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 { |
| flex: 1; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| padding: 0.8rem 0.8rem; |
| } |
| |
| |
| .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); |
| } |
| |
| |
| .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; |
| } |
| |
| |
| .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 { |
| 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; |
| } |
| |
| |
| .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-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; |
| } |
| |
| |
| hr { border: none; border-top: 1px solid var(--border); margin: 0 0 1rem; } |
| |
| |
| .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 { display: none !important; } |
| |
| |
| .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-panel { |
| background: #ffebee; border-radius: var(--radius-card); |
| padding: 0.9rem 1.1rem; color: #c62828; |
| margin-top: 1rem; max-width: 520px; width: 100%; |
| } |
| |
| |
| .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 { |
| 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; } |
| |
| |
| |
| |
| |
| |
| body::-webkit-scrollbar { |
| width: 0px; |
| background: transparent; |
| } |
| |
| |
| body { |
| scrollbar-width: none; |
| } |
| |
| |
| html { |
| scrollbar-width: none; |
| } |
| |
| html::-webkit-scrollbar { |
| width: 0px; |
| background: transparent; |
| } |
| </style> |
| </head> |
| <body> |
|
|
| |
| <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> |
| <div style="width:100%;max-width:520px;display:flex;flex-direction:column;gap:0;"> |
|
|
| |
| <div class="card"> |
| <h1 class="title-main" id="txt-title">GeoClassifier</h1> |
| <p class="subtitle" id="txt-subtitle">Classify natural scenes</p> |
|
|
| |
| <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> |
|
|
| |
| <div class="upload-row"> |
| |
| <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> |
|
|
| |
| <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> |
| |
| <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> |
|
|
| |
| <hr /> |
| <span class="classes-label" id="txt-classes">Possible Classes:</span> |
| <div class="chips-row" id="chips-row"></div> |
| </div> |
|
|
| |
| <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> |
|
|
| |
| <div class="error-panel hidden" id="error-panel"></div> |
|
|
| |
| <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> |
| <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); |
| |
| 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; } |
| |
| |
| 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> |
|
|