Spaces:
Sleeping
Sleeping
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>YELY | Démo module IA pompiste</title> | |
| <style> | |
| :root { | |
| --bg: #0f172a; | |
| --panel: #1e293b; | |
| --panel-2: #273449; | |
| --border: #334155; | |
| --text: #e2e8f0; | |
| --muted: #94a3b8; | |
| --accent: #f59e0b; | |
| --ok-bg: #052e1f; | |
| --ok-border: #10b981; | |
| --ok-text: #6ee7b7; | |
| --ko-bg: #3b0d0d; | |
| --ko-border: #ef4444; | |
| --ko-text: #fca5a5; | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| margin: 0; | |
| min-height: 100vh; | |
| font-family: -apple-system, "Segoe UI", Roboto, Arial, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| display: flex; | |
| justify-content: center; | |
| padding: 2rem 1rem; | |
| } | |
| .app { width: 100%; max-width: 480px; } | |
| .brand { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; margin-bottom: 0.25rem; } | |
| .brand-left { display: flex; align-items: center; gap: 0.6rem; } | |
| .brand .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--accent); } | |
| .brand h1 { font-size: 1.3rem; margin: 0; } | |
| .brand a { color: var(--muted); font-size: 0.85rem; text-decoration: none; border: 1px solid var(--border); padding: 0.4rem 0.7rem; border-radius: 8px; } | |
| .brand a:hover { border-color: var(--accent); color: var(--text); } | |
| .subtitle { color: var(--muted); font-size: 0.9rem; margin: 0 0 1.5rem; } | |
| .card { | |
| background: var(--panel); | |
| border: 1px solid var(--border); | |
| border-radius: 14px; | |
| padding: 1.25rem; | |
| margin-bottom: 1rem; | |
| } | |
| label { display: block; font-size: 0.85rem; color: var(--muted); margin: 0.9rem 0 0.3rem; } | |
| label:first-child { margin-top: 0; } | |
| input, select { | |
| width: 100%; | |
| padding: 0.65rem 0.75rem; | |
| border-radius: 8px; | |
| border: 1px solid var(--border); | |
| background: var(--panel-2); | |
| color: var(--text); | |
| font-size: 1rem; | |
| } | |
| input[type="file"] { padding: 0.5rem; } | |
| #preview { | |
| display: none; | |
| width: 100%; | |
| max-height: 260px; | |
| object-fit: contain; | |
| border-radius: 10px; | |
| margin-top: 0.75rem; | |
| border: 1px solid var(--border); | |
| } | |
| button { | |
| width: 100%; | |
| margin-top: 1.25rem; | |
| padding: 0.85rem; | |
| border: none; | |
| border-radius: 10px; | |
| background: var(--accent); | |
| color: #1a1200; | |
| font-size: 1.05rem; | |
| font-weight: 700; | |
| cursor: pointer; | |
| } | |
| button:disabled { background: #4b5563; color: #9ca3af; cursor: not-allowed; } | |
| .status { font-size: 0.85rem; color: var(--muted); text-align: center; margin-top: 0.75rem; min-height: 1.2em; } | |
| .banner { | |
| display: none; | |
| border-radius: 12px; | |
| padding: 1rem 1.1rem; | |
| font-weight: 700; | |
| font-size: 1.05rem; | |
| margin-bottom: 1rem; | |
| border: 1px solid; | |
| } | |
| .banner.ok { display: block; background: var(--ok-bg); border-color: var(--ok-border); color: var(--ok-text); } | |
| .banner.ko { display: block; background: var(--ko-bg); border-color: var(--ko-border); color: var(--ko-text); } | |
| .rows { display: flex; flex-direction: column; gap: 0.6rem; } | |
| .row { display: flex; justify-content: space-between; align-items: baseline; padding: 0.5rem 0; border-bottom: 1px solid var(--border); } | |
| .row:last-child { border-bottom: none; } | |
| .row .k { color: var(--muted); font-size: 0.85rem; } | |
| .row .v { font-weight: 700; font-size: 1.15rem; } | |
| .row .v.small { font-size: 0.95rem; font-weight: 500; } | |
| details { margin-top: 0.5rem; } | |
| summary { cursor: pointer; color: var(--muted); font-size: 0.8rem; } | |
| pre { background: #000; color: #a3e635; padding: 0.75rem; border-radius: 8px; overflow-x: auto; font-size: 0.75rem; } | |
| .hint { color: var(--muted); font-size: 0.78rem; margin-top: 0.4rem; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <div class="brand"> | |
| <div class="brand-left"><span class="dot"></span><h1>YELY, verification pompiste</h1></div> | |
| <a href="stats.html">Statistiques</a> | |
| </div> | |
| <p class="subtitle">Démo du module IA : photo du terminal -> lecture automatique -> vérification de cohérence.</p> | |
| <div id="banner" class="banner"></div> | |
| <div id="resultCard" class="card" style="display:none;"> | |
| <div class="rows" id="resultRows"></div> | |
| <details> | |
| <summary>Détails techniques</summary> | |
| <pre id="rawJson"></pre> | |
| </details> | |
| </div> | |
| <form id="form" class="card"> | |
| <label>Photo du terminal de pompe</label> | |
| <input type="file" id="image" accept="image/*" capture="environment" required /> | |
| <img id="preview" alt="Aperçu" /> | |
| <label>Prix du litre (FCFA)</label> | |
| <input type="number" id="fuelPrice" value="700" step="0.01" /> | |
| <label>ID chauffeur (optionnel | simulate le scan QR)</label> | |
| <input type="text" id="driverId" placeholder="chauffeur-42" /> | |
| <label>ID pompiste / station (optionnel)</label> | |
| <input type="text" id="pompisteId" placeholder="pompiste-7" /> | |
| <details style="margin-top: 0.9rem;"> | |
| <summary>Configuration (URL de l'API)</summary> | |
| <label>URL du service</label> | |
| <input type="text" id="apiUrl" /> | |
| </details> | |
| <button type="submit" id="submitBtn">Analyser la photo</button> | |
| <div class="status" id="status"></div> | |
| </form> | |
| </div> | |
| <script src="config.js"></script> | |
| <script> | |
| const form = document.getElementById('form'); | |
| const submitBtn = document.getElementById('submitBtn'); | |
| const statusEl = document.getElementById('status'); | |
| const banner = document.getElementById('banner'); | |
| const resultCard = document.getElementById('resultCard'); | |
| const resultRows = document.getElementById('resultRows'); | |
| const rawJson = document.getElementById('rawJson'); | |
| const imageInput = document.getElementById('image'); | |
| const preview = document.getElementById('preview'); | |
| const apiUrlInput = document.getElementById('apiUrl'); | |
| apiUrlInput.value = localStorage.getItem('yely_api_url') || window.YELY_API_URL || ''; | |
| imageInput.addEventListener('change', () => { | |
| if (imageInput.files.length > 0) { | |
| preview.src = URL.createObjectURL(imageInput.files[0]); | |
| preview.style.display = 'block'; | |
| } else { | |
| preview.style.display = 'none'; | |
| } | |
| }); | |
| function fmt(v) { | |
| if (v === null || v === undefined || v === '') return '—'; | |
| if (typeof v === 'boolean') return v ? 'Oui' : 'Non'; | |
| return String(v); | |
| } | |
| function row(label, value, small) { | |
| const div = document.createElement('div'); | |
| div.className = 'row'; | |
| div.innerHTML = `<span class="k">${label}</span><span class="v${small ? ' small' : ''}">${value}</span>`; | |
| return div; | |
| } | |
| function renderSuccess(data) { | |
| resultRows.innerHTML = ''; | |
| resultRows.appendChild(row('Litres détectés', fmt(data.detected_liters))); | |
| resultRows.appendChild(row('Montant détecté', fmt(data.detected_amount))); | |
| resultRows.appendChild(row('Prix du litre', fmt(data.fuel_price))); | |
| resultRows.appendChild(row('Montant calculé', fmt(data.calculated_amount))); | |
| resultRows.appendChild(row('Litres calculés', fmt(data.calculated_liters))); | |
| resultRows.appendChild(row('Cohérent', fmt(data.is_consistent))); | |
| resultRows.appendChild(row('Confiance', fmt(data.confidence_score), true)); | |
| resultRows.appendChild(row('Qualité image', fmt(data.image_quality), true)); | |
| resultCard.style.display = 'block'; | |
| rawJson.textContent = JSON.stringify(data, null, 2); | |
| } | |
| form.addEventListener('submit', async (event) => { | |
| event.preventDefault(); | |
| if (imageInput.files.length === 0) return; | |
| const apiUrl = apiUrlInput.value.trim(); | |
| localStorage.setItem('yely_api_url', apiUrl); | |
| if (!apiUrl) { | |
| statusEl.textContent = "Renseigne l'URL de l'API dans « Configuration »."; | |
| return; | |
| } | |
| const formData = new FormData(); | |
| formData.append('image', imageInput.files[0]); | |
| formData.append('fuel_price', document.getElementById('fuelPrice').value); | |
| const driverId = document.getElementById('driverId').value.trim(); | |
| const pompisteId = document.getElementById('pompisteId').value.trim(); | |
| if (driverId) formData.append('driver_id', driverId); | |
| if (pompisteId) formData.append('pompiste_id', pompisteId); | |
| submitBtn.disabled = true; | |
| submitBtn.textContent = 'Analyse en cours…'; | |
| banner.className = 'banner'; | |
| banner.textContent = ''; | |
| resultCard.style.display = 'none'; | |
| statusEl.textContent = 'Envoi de la photo…'; | |
| const t0 = performance.now(); | |
| try { | |
| const res = await fetch(apiUrl, { method: 'POST', body: formData }); | |
| const elapsed = ((performance.now() - t0) / 1000).toFixed(1); | |
| const data = await res.json(); | |
| statusEl.textContent = `Réponse en ${elapsed}s (HTTP ${res.status})`; | |
| const success = res.ok && data.success === true; | |
| banner.className = 'banner ' + (success ? 'ok' : 'ko'); | |
| banner.textContent = (success ? '✅ ' : '⛔ ') + (data.message || (success ? 'Validé.' : 'Bloqué.')); | |
| renderSuccess(data); | |
| } catch (err) { | |
| statusEl.textContent = ''; | |
| banner.className = 'banner ko'; | |
| banner.textContent = "⛔ Impossible de contacter l'API (" + err.message + ")."; | |
| } finally { | |
| submitBtn.disabled = false; | |
| submitBtn.textContent = 'Analyser la photo'; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |