Spaces:
Paused
Paused
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Résolveur d'Images & PDF - Mariam</title> | |
| <style> | |
| :root { | |
| --primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| --primary-solid: #667eea; | |
| --primary-dark: #5a6fd8; | |
| --secondary: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); | |
| --secondary-solid: #f093fb; | |
| --success: #10dc60; | |
| --success-bg: rgba(16, 220, 96, 0.1); | |
| --danger: #f04141; | |
| --danger-bg: rgba(240, 65, 65, 0.1); | |
| --warning: #ffce00; | |
| --warning-bg: rgba(255, 206, 0, 0.1); | |
| --background: #f8fafc; | |
| --surface: #ffffff; | |
| --surface-elevated: #ffffff; | |
| --text-primary: #1a202c; | |
| --text-secondary: #718096; | |
| --text-muted: #a0aec0; | |
| --border: #e2e8f0; | |
| --border-light: #f1f5f9; | |
| --shadow-sm: 0 2px 4px rgba(0,0,0,0.05); | |
| --shadow-md: 0 4px 12px rgba(0,0,0,0.1); | |
| --shadow-lg: 0 8px 25px rgba(0,0,0,0.15); | |
| --radius: 16px; | |
| --radius-sm: 12px; | |
| --radius-lg: 24px; | |
| --spacing: 1rem; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: var(--background); | |
| color: var(--text-primary); | |
| line-height: 1.6; | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| padding: 0; | |
| margin: 0; | |
| min-height: 100vh; | |
| } | |
| .app-container { | |
| max-width: 100%; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Header */ | |
| .header { | |
| background: var(--primary); | |
| color: white; | |
| text-align: center; | |
| padding: 2rem 1rem 1.5rem; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .header::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.1'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E") repeat; | |
| opacity: 0.3; | |
| } | |
| .header-content { | |
| position: relative; | |
| z-index: 2; | |
| } | |
| .header h1 { | |
| font-size: 1.75rem; | |
| font-weight: 800; | |
| margin-bottom: 0.5rem; | |
| text-shadow: 0 2px 4px rgba(0,0,0,0.2); | |
| } | |
| .header .subtitle { | |
| font-size: 1rem; | |
| opacity: 0.9; | |
| font-weight: 500; | |
| } | |
| /* Main Content */ | |
| .main-content { | |
| flex: 1; | |
| padding: 1rem; | |
| padding-bottom: 2rem; | |
| } | |
| .card { | |
| background: var(--surface); | |
| border-radius: var(--radius); | |
| box-shadow: var(--shadow-md); | |
| margin-bottom: 1rem; | |
| overflow: hidden; | |
| border: 1px solid var(--border-light); | |
| } | |
| .card-header { | |
| padding: 1.5rem 1.5rem 1rem; | |
| border-bottom: 1px solid var(--border-light); | |
| } | |
| .card-body { | |
| padding: 1.5rem; | |
| } | |
| .card-title { | |
| font-size: 1.1rem; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| margin-bottom: 1rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| /* Telegram Button */ | |
| .telegram-section { | |
| margin-bottom: 1rem; | |
| } | |
| .telegram-button { | |
| display: block; | |
| background: linear-gradient(135deg, #0088cc 0%, #005599 100%); | |
| color: white; | |
| padding: 1rem 1.5rem; | |
| border-radius: var(--radius); | |
| text-decoration: none; | |
| font-weight: 600; | |
| text-align: center; | |
| box-shadow: var(--shadow-md); | |
| transition: all 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .telegram-button::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); | |
| transition: left 0.5s; | |
| } | |
| .telegram-button:hover::before { | |
| left: 100%; | |
| } | |
| .telegram-button:active { | |
| transform: scale(0.98); | |
| } | |
| /* Style Selection */ | |
| .style-options { | |
| display: grid; | |
| gap: 0.75rem; | |
| } | |
| .style-option { | |
| position: relative; | |
| cursor: pointer; | |
| border-radius: var(--radius-sm); | |
| overflow: hidden; | |
| transition: all 0.3s ease; | |
| } | |
| .style-option input[type="radio"] { | |
| position: absolute; | |
| opacity: 0; | |
| width: 100%; | |
| height: 100%; | |
| cursor: pointer; | |
| z-index: 2; | |
| } | |
| .style-option-content { | |
| padding: 1rem; | |
| border: 2px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| background: var(--surface); | |
| transition: all 0.3s ease; | |
| position: relative; | |
| } | |
| .style-option input[type="radio"]:checked + .style-option-content { | |
| border-color: var(--primary-solid); | |
| background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%); | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-lg); | |
| } | |
| .style-option input[type="radio"]:checked + .style-option-content::after { | |
| content: '✓'; | |
| position: absolute; | |
| top: 0.75rem; | |
| right: 0.75rem; | |
| width: 24px; | |
| height: 24px; | |
| background: var(--primary-solid); | |
| color: white; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 0.8rem; | |
| font-weight: bold; | |
| } | |
| .style-label { | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| margin-bottom: 0.25rem; | |
| display: block; | |
| } | |
| .style-description { | |
| font-size: 0.9rem; | |
| color: var(--text-secondary); | |
| } | |
| /* Upload Section */ | |
| .upload-area { | |
| border: 2px dashed var(--border); | |
| border-radius: var(--radius); | |
| padding: 2rem 1rem; | |
| text-align: center; | |
| background: linear-gradient(135deg, var(--surface) 0%, #f8fafc 100%); | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .upload-area.dragover { | |
| border-color: var(--primary-solid); | |
| background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%); | |
| transform: scale(1.02); | |
| } | |
| .upload-icon { | |
| font-size: 3rem; | |
| margin-bottom: 1rem; | |
| background: var(--primary); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .upload-text { | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| margin-bottom: 0.5rem; | |
| } | |
| .upload-hint { | |
| font-size: 0.9rem; | |
| color: var(--text-secondary); | |
| } | |
| #file-input { | |
| display: none; | |
| } | |
| /* File Previews */ | |
| .file-previews { | |
| margin-top: 1rem; | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); | |
| gap: 0.75rem; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| } | |
| .preview-item { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 0.5rem; | |
| padding: 0.75rem; | |
| background: var(--surface-elevated); | |
| border-radius: var(--radius-sm); | |
| border: 1px solid var(--border); | |
| box-shadow: var(--shadow-sm); | |
| position: relative; | |
| animation: fadeInUp 0.3s ease; | |
| } | |
| @keyframes fadeInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .preview-item img { | |
| width: 60px; | |
| height: 60px; | |
| object-fit: cover; | |
| border-radius: var(--radius-sm); | |
| } | |
| .preview-item .pdf-icon { | |
| font-size: 2.5rem; | |
| color: var(--danger); | |
| } | |
| .preview-filename { | |
| font-size: 0.75rem; | |
| color: var(--text-secondary); | |
| text-align: center; | |
| word-break: break-all; | |
| line-height: 1.2; | |
| } | |
| /* Buttons */ | |
| .btn { | |
| padding: 1rem 1.5rem; | |
| border: none; | |
| border-radius: var(--radius); | |
| font-size: 1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| text-decoration: none; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| min-height: 56px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .btn:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| transform: none ; | |
| } | |
| .btn-primary { | |
| background: var(--primary); | |
| color: white; | |
| box-shadow: var(--shadow-md); | |
| } | |
| .btn-primary:not(:disabled):active { | |
| transform: translateY(1px); | |
| } | |
| .btn-danger { | |
| background: var(--danger); | |
| color: white; | |
| } | |
| .btn-success { | |
| background: var(--success); | |
| color: white; | |
| } | |
| .btn-block { | |
| width: 100%; | |
| margin-bottom: 0.75rem; | |
| } | |
| .btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); | |
| transition: left 0.5s; | |
| } | |
| .btn:hover::before { | |
| left: 100%; | |
| } | |
| /* Cooldown Notice */ | |
| .cooldown-notice { | |
| background: var(--warning-bg); | |
| border: 1px solid var(--warning); | |
| border-radius: var(--radius); | |
| padding: 1rem; | |
| margin-bottom: 1rem; | |
| text-align: center; | |
| display: none; | |
| } | |
| .cooldown-timer { | |
| font-size: 1.2rem; | |
| font-weight: bold; | |
| color: var(--warning); | |
| } | |
| /* Status Container */ | |
| .status-container { | |
| display: none; | |
| animation: slideDown 0.3s ease; | |
| } | |
| @keyframes slideDown { | |
| from { | |
| opacity: 0; | |
| transform: translateY(-20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .status-content { | |
| text-align: center; | |
| padding: 1.5rem; | |
| } | |
| .status-icon { | |
| font-size: 3rem; | |
| margin-bottom: 1rem; | |
| } | |
| .status-text { | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| margin-bottom: 1rem; | |
| } | |
| .status-description { | |
| color: var(--text-secondary); | |
| font-size: 0.9rem; | |
| margin-bottom: 1rem; | |
| } | |
| .status.success .status-icon { color: var(--success); } | |
| .status.error .status-icon { color: var(--danger); } | |
| .status.processing .status-icon { color: var(--primary-solid); } | |
| .status.success .status-text { color: var(--success); } | |
| .status.error .status-text { color: var(--danger); } | |
| .status.processing .status-text { color: var(--primary-solid); } | |
| /* Loading Animation */ | |
| .loading-spinner { | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 2px solid rgba(255,255,255,0.3); | |
| border-radius: 50%; | |
| border-top-color: white; | |
| animation: spin 1s ease-in-out infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| /* History */ | |
| .history-item { | |
| display: flex; | |
| align-items: center; | |
| padding: 1rem; | |
| background: var(--surface); | |
| border-radius: var(--radius-sm); | |
| border: 1px solid var(--border-light); | |
| margin-bottom: 0.75rem; | |
| transition: all 0.3s ease; | |
| } | |
| .history-item:active { | |
| transform: scale(0.98); | |
| box-shadow: var(--shadow-lg); | |
| } | |
| .history-icon { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin-right: 1rem; | |
| font-size: 1.2rem; | |
| } | |
| .history-icon.success { | |
| background: var(--success-bg); | |
| color: var(--success); | |
| } | |
| .history-icon.error { | |
| background: var(--danger-bg); | |
| color: var(--danger); | |
| } | |
| .history-icon.pending { | |
| background: var(--warning-bg); | |
| color: var(--warning); | |
| } | |
| .history-info { | |
| flex: 1; | |
| } | |
| .history-filename { | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| margin-bottom: 0.25rem; | |
| } | |
| .history-meta { | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| } | |
| .history-actions { | |
| margin-left: 0.5rem; | |
| } | |
| .btn-sm { | |
| padding: 0.5rem 1rem; | |
| min-height: 36px; | |
| font-size: 0.9rem; | |
| } | |
| /* Responsive Improvements */ | |
| @media (max-width: 480px) { | |
| .main-content { | |
| padding: 0.75rem; | |
| } | |
| .card-body, | |
| .card-header { | |
| padding: 1rem; | |
| } | |
| .header { | |
| padding: 1.5rem 1rem 1rem; | |
| } | |
| .header h1 { | |
| font-size: 1.5rem; | |
| } | |
| .file-previews { | |
| grid-template-columns: repeat(auto-fit, minmax(70px, 1fr)); | |
| } | |
| } | |
| /* Toast Notifications */ | |
| .toast { | |
| position: fixed; | |
| top: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: var(--surface-elevated); | |
| color: var(--text-primary); | |
| padding: 1rem 1.5rem; | |
| border-radius: var(--radius); | |
| box-shadow: var(--shadow-lg); | |
| border: 1px solid var(--border); | |
| z-index: 1000; | |
| opacity: 0; | |
| transition: all 0.3s ease; | |
| } | |
| .toast.show { | |
| opacity: 1; | |
| transform: translateX(-50%) translateY(0); | |
| } | |
| .toast.success { | |
| background: var(--success-bg); | |
| border-color: var(--success); | |
| color: var(--success); | |
| } | |
| .toast.error { | |
| background: var(--danger-bg); | |
| border-color: var(--danger); | |
| color: var(--danger); | |
| } | |
| /* Smooth Scrolling */ | |
| html { | |
| scroll-behavior: smooth; | |
| } | |
| /* Focus States */ | |
| .btn:focus, | |
| .style-option input:focus + .style-option-content, | |
| .upload-area:focus { | |
| outline: 2px solid var(--primary-solid); | |
| outline-offset: 2px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <header class="header"> | |
| <div class="header-content"> | |
| <h1>🧠 Science avec Mariam</h1> | |
| <p class="subtitle">Votre assistante IA pour Math, Physique & Chimie</p> | |
| </div> | |
| </header> | |
| <main class="main-content"> | |
| <!-- Telegram Section --> | |
| <!-- Style Selection --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3 class="card-title">🎨 Style de résolution</h3> | |
| </div> | |
| <div class="card-body"> | |
| <div class="style-options"> | |
| <div class="style-option"> | |
| <input type="radio" id="style-light" name="resolution-style" value="light"> | |
| <div class="style-option-content"> | |
| <div class="style-label">📝 Résolution Light</div> | |
| <div class="style-description">Format simple et épuré pour une lecture rapide</div> | |
| </div> | |
| </div> | |
| <div class="style-option"> | |
| <input type="radio" id="style-colorful" name="resolution-style" value="colorful" checked> | |
| <div class="style-option-content"> | |
| <div class="style-label">🌈 Résolution Colorée</div> | |
| <div class="style-description">Format richement formaté avec couleurs et mise en page élégante</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Cooldown Notice --> | |
| <div id="cooldown-notice" class="cooldown-notice"> | |
| ⏰ Veuillez attendre <span id="cooldown-timer" class="cooldown-timer">2:00</span> avant de pouvoir soumettre à nouveau. | |
| </div> | |
| <!-- Upload Section --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3 class="card-title">📤 Vos fichiers</h3> | |
| </div> | |
| <div class="card-body"> | |
| <div id="upload-area" class="upload-area" tabindex="0"> | |
| <div class="upload-icon">📱</div> | |
| <div class="upload-text">Toucher pour sélectionner</div> | |
| <div class="upload-hint">Images et PDF acceptés</div> | |
| <input type="file" id="file-input" accept="image/*,application/pdf" multiple> | |
| </div> | |
| <div id="file-previews" class="file-previews"></div> | |
| </div> | |
| </div> | |
| <!-- Action Buttons --> | |
| <button id="clear-files-btn" class="btn btn-danger btn-block" style="display: none;"> | |
| 🗑️ Effacer les fichiers | |
| </button> | |
| <button id="solve-btn" class="btn btn-primary btn-block" disabled> | |
| 🔍 Résoudre | |
| </button> | |
| <!-- Status Container --> | |
| <div id="status-container" class="card status-container"> | |
| <div class="card-body"> | |
| <div class="status-content"> | |
| <div id="status-icon" class="status-icon">⏳</div> | |
| <div id="status-text" class="status-text">Traitement en cours...</div> | |
| <div id="status-description" class="status-description"> | |
| Votre PDF sera disponible ici une fois le traitement terminé. | |
| </div> | |
| <div id="response-container" style="display: none;"> | |
| <div id="response"></div> | |
| <a id="download-btn" class="btn btn-success btn-block" style="display: none;"> | |
| 📥 Télécharger le PDF | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- History --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h3 class="card-title">📋 Historique</h3> | |
| </div> | |
| <div class="card-body"> | |
| <div id="history-list"></div> | |
| <button id="clear-history-btn" class="btn btn-danger btn-block"> | |
| 🗑️ Vider l'historique | |
| </button> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <script> | |
| class MariamApp { | |
| constructor() { | |
| this.selectedFiles = []; | |
| this.cooldownEndTime = 0; | |
| this.cooldownInterval = null; | |
| this.eventSources = {}; | |
| this.initElements(); | |
| this.attachEventListeners(); | |
| this.checkCooldownOnLoad(); | |
| this.renderHistory(); | |
| this.checkHistoryStatus(); | |
| } | |
| initElements() { | |
| this.uploadArea = document.getElementById('upload-area'); | |
| this.fileInput = document.getElementById('file-input'); | |
| this.filePreviews = document.getElementById('file-previews'); | |
| this.solveBtn = document.getElementById('solve-btn'); | |
| this.clearFilesBtn = document.getElementById('clear-files-btn'); | |
| this.statusContainer = document.getElementById('status-container'); | |
| this.statusIcon = document.getElementById('status-icon'); | |
| this.statusText = document.getElementById('status-text'); | |
| this.statusDescription = document.getElementById('status-description'); | |
| this.responseContainer = document.getElementById('response-container'); | |
| this.responseDiv = document.getElementById('response'); | |
| this.downloadBtn = document.getElementById('download-btn'); | |
| this.cooldownNotice = document.getElementById('cooldown-notice'); | |
| this.cooldownTimer = document.getElementById('cooldown-timer'); | |
| this.historyList = document.getElementById('history-list'); | |
| this.clearHistoryBtn = document.getElementById('clear-history-btn'); | |
| } | |
| attachEventListeners() { | |
| // File upload | |
| this.uploadArea.addEventListener('click', () => this.fileInput.click()); | |
| this.uploadArea.addEventListener('dragover', this.handleDragOver.bind(this)); | |
| this.uploadArea.addEventListener('dragleave', this.handleDragLeave.bind(this)); | |
| this.uploadArea.addEventListener('drop', this.handleDrop.bind(this)); | |
| this.fileInput.addEventListener('change', this.handleFileSelect.bind(this)); | |
| // Buttons | |
| this.clearFilesBtn.addEventListener('click', this.clearFiles.bind(this)); | |
| this.solveBtn.addEventListener('click', this.solve.bind(this)); | |
| this.clearHistoryBtn.addEventListener('click', this.clearHistory.bind(this)); | |
| // Style selection | |
| document.querySelectorAll('input[name="resolution-style"]').forEach(input => { | |
| input.addEventListener('change', () => { | |
| // Add haptic feedback on mobile | |
| if (navigator.vibrate) { | |
| navigator.vibrate(50); | |
| } | |
| }); | |
| }); | |
| } | |
| handleDragOver(e) { | |
| e.preventDefault(); | |
| this.uploadArea.classList.add('dragover'); | |
| } | |
| handleDragLeave(e) { | |
| e.preventDefault(); | |
| this.uploadArea.classList.remove('dragover'); | |
| } | |
| handleDrop(e) { | |
| e.preventDefault(); | |
| this.uploadArea.classList.remove('dragover'); | |
| if (e.dataTransfer.files.length) { | |
| this.handleFileSelection(e.dataTransfer.files); | |
| } | |
| } | |
| handleFileSelect(e) { | |
| if (e.target.files.length) { | |
| this.handleFileSelection(e.target.files); | |
| } | |
| } | |
| handleFileSelection(files) { | |
| const newFiles = Array.from(files); | |
| let pdfSelected = this.selectedFiles.some(f => f.type === 'application/pdf'); | |
| newFiles.forEach(file => { | |
| if (file.type.startsWith('image/')) { | |
| if (!this.selectedFiles.some(sf => sf.name === file.name && sf.size === file.size)) { | |
| this.selectedFiles.push(file); | |
| } | |
| } else if (file.type === 'application/pdf') { | |
| if (!pdfSelected) { | |
| this.selectedFiles = this.selectedFiles.filter(f => f.type !== 'application/pdf'); | |
| this.selectedFiles.push(file); | |
| pdfSelected = true; | |
| } | |
| } | |
| }); | |
| this.updateFilePreviews(); | |
| this.updateButtonsState(); | |
| this.showToast('Fichiers ajoutés avec succès', 'success'); | |
| } | |
| updateFilePreviews() { | |
| this.filePreviews.innerHTML = ''; | |
| if (this.selectedFiles.length === 0) return; | |
| this.selectedFiles.forEach(file => { | |
| const item = document.createElement('div'); | |
| item.className = 'preview-item'; | |
| const filename = file.name.length > 10 ? | |
| file.name.substring(0, 8) + "..." : | |
| file.name; | |
| if (file.type.startsWith('image/')) { | |
| const img = document.createElement('img'); | |
| img.src = URL.createObjectURL(file); | |
| item.appendChild(img); | |
| } else { | |
| item.innerHTML = '<div class="pdf-icon">📄</div>'; | |
| } | |
| const filenameEl = document.createElement('div'); | |
| filenameEl.className = 'preview-filename'; | |
| filenameEl.textContent = filename; | |
| item.appendChild(filenameEl); | |
| this.filePreviews.appendChild(item); | |
| }); | |
| } | |
| updateButtonsState() { | |
| const hasFiles = this.selectedFiles.length > 0; | |
| const isCooldown = this.isCooldownActive(); | |
| this.solveBtn.disabled = !hasFiles || isCooldown; | |
| this.solveBtn.innerHTML = hasFiles ? | |
| `🔍 Résoudre (${this.selectedFiles.length})` : | |
| '🔍 Résoudre'; | |
| this.clearFilesBtn.style.display = hasFiles ? 'block' : 'none'; | |
| } | |
| clearFiles() { | |
| this.selectedFiles = []; | |
| this.fileInput.value = ''; | |
| this.updateFilePreviews(); | |
| this.updateButtonsState(); | |
| this.statusContainer.style.display = 'none'; | |
| this.showToast('Fichiers effacés', 'success'); | |
| } | |
| solve() { | |
| if (this.selectedFiles.length === 0 || this.isCooldownActive()) return; | |
| this.startCooldown(); | |
| this.showSolvingStatus(); | |
| const formData = new FormData(); | |
| this.selectedFiles.forEach(file => formData.append('user_files', file)); | |
| formData.append('style', document.querySelector('input[name="resolution-style"]:checked').value); | |
| fetch('/solve', { method: 'POST', body: formData }) | |
| .then(response => { | |
| if (!response.ok) return response.json().then(err => { throw new Error(err.error) }); | |
| return response.json(); | |
| }) | |
| .then(data => { | |
| const { task_id, first_filename } = data; | |
| let history = this.getHistory(); | |
| history.push({ | |
| id: task_id, | |
| filename: first_filename, | |
| status: 'pending', | |
| timestamp: Date.now() | |
| }); | |
| this.saveHistory(history); | |
| this.renderHistory(); | |
| this.updateSolvingStatus('Traitement en arrière-plan...', 'processing'); | |
| this.listenToTask(task_id); | |
| }) | |
| .catch(error => this.handleError(error.message)); | |
| } | |
| showSolvingStatus() { | |
| this.statusContainer.style.display = 'block'; | |
| this.statusContainer.className = 'card status-container'; | |
| this.statusIcon.textContent = '⏳'; | |
| this.statusText.textContent = 'Préparation...'; | |
| this.statusDescription.textContent = 'Envoi de vos fichiers en cours...'; | |
| this.responseContainer.style.display = 'none'; | |
| this.downloadBtn.style.display = 'none'; | |
| this.solveBtn.disabled = true; | |
| this.solveBtn.innerHTML = '<span class="loading-spinner"></span> Traitement...'; | |
| } | |
| updateSolvingStatus(text, type = 'processing') { | |
| this.statusContainer.className = `card status-container status ${type}`; | |
| this.statusText.textContent = text; | |
| switch(type) { | |
| case 'success': | |
| this.statusIcon.textContent = '✅'; | |
| this.statusDescription.textContent = 'Votre PDF est prêt au téléchargement !'; | |
| break; | |
| case 'error': | |
| this.statusIcon.textContent = '❌'; | |
| this.statusDescription.textContent = 'Une erreur est survenue lors du traitement.'; | |
| break; | |
| case 'processing': | |
| this.statusIcon.textContent = '⏳'; | |
| this.statusDescription.textContent = 'Traitement en cours, veuillez patienter...'; | |
| break; | |
| } | |
| } | |
| listenToTask(taskId) { | |
| if (this.eventSources[taskId]) this.eventSources[taskId].close(); | |
| const eventSource = new EventSource('/stream/' + taskId); | |
| this.eventSources[taskId] = eventSource; | |
| eventSource.onmessage = (event) => { | |
| const data = JSON.parse(event.data); | |
| this.updateTaskInHistory(taskId, { | |
| status: data.status, | |
| download_url: data.download_url, | |
| error: data.error | |
| }); | |
| if (data.status === 'completed') { | |
| this.updateSolvingStatus('Traitement terminé ! 🎉', 'success'); | |
| this.responseDiv.innerHTML = '<p style="color: #10dc60; text-align: center; font-weight: 600;">Votre PDF est prêt.</p>'; | |
| this.downloadBtn.href = data.download_url; | |
| this.downloadBtn.style.display = 'block'; | |
| this.responseContainer.style.display = 'block'; | |
| this.showToast('PDF généré avec succès !', 'success'); | |
| eventSource.close(); | |
| // Haptic feedback on mobile | |
| if (navigator.vibrate) { | |
| navigator.vibrate([100, 30, 100, 30, 100]); | |
| } | |
| } else if (data.status === 'error') { | |
| this.handleError(data.error || 'Une erreur inattendue est survenue.', taskId); | |
| eventSource.close(); | |
| } else { | |
| this.updateSolvingStatus(`Statut: ${data.status}`, 'processing'); | |
| } | |
| }; | |
| eventSource.onerror = () => { | |
| eventSource.close(); | |
| this.checkHistoryStatus(); | |
| }; | |
| } | |
| handleError(errorMessage, taskId = null) { | |
| this.updateSolvingStatus('Erreur de traitement', 'error'); | |
| this.responseDiv.innerHTML = `<p style="color: #f04141; text-align: center; font-weight: 600;">${errorMessage}</p>`; | |
| this.responseContainer.style.display = 'block'; | |
| this.downloadBtn.style.display = 'none'; | |
| this.showToast('Erreur: ' + errorMessage, 'error'); | |
| if (taskId) { | |
| this.updateTaskInHistory(taskId, { status: 'error', error: errorMessage }); | |
| } | |
| } | |
| // Cooldown Management | |
| checkCooldownOnLoad() { | |
| const savedCooldown = localStorage.getItem('mariamCooldownEndTime'); | |
| if (savedCooldown && parseInt(savedCooldown) > Date.now()) { | |
| this.cooldownEndTime = parseInt(savedCooldown); | |
| this.startCooldownTimer(); | |
| } | |
| } | |
| startCooldown() { | |
| this.cooldownEndTime = Date.now() + 2 * 60 * 1000; | |
| localStorage.setItem('mariamCooldownEndTime', this.cooldownEndTime.toString()); | |
| this.startCooldownTimer(); | |
| } | |
| startCooldownTimer() { | |
| this.cooldownNotice.style.display = 'block'; | |
| this.solveBtn.disabled = true; | |
| if (this.cooldownInterval) clearInterval(this.cooldownInterval); | |
| this.cooldownInterval = setInterval(() => { | |
| const remaining = Math.max(0, this.cooldownEndTime - Date.now()); | |
| if (remaining <= 0) { | |
| clearInterval(this.cooldownInterval); | |
| this.cooldownNotice.style.display = 'none'; | |
| this.updateButtonsState(); | |
| return; | |
| } | |
| const minutes = Math.floor(remaining / 60000); | |
| const seconds = Math.floor((remaining % 60000) / 1000); | |
| this.cooldownTimer.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; | |
| }, 1000); | |
| } | |
| isCooldownActive() { | |
| return Date.now() < this.cooldownEndTime; | |
| } | |
| // History Management | |
| getHistory() { | |
| return JSON.parse(localStorage.getItem('mariamTaskHistory')) || []; | |
| } | |
| saveHistory(history) { | |
| localStorage.setItem('mariamTaskHistory', JSON.stringify(history)); | |
| } | |
| renderHistory() { | |
| this.historyList.innerHTML = ''; | |
| const history = this.getHistory(); | |
| if (history.length === 0) { | |
| this.historyList.innerHTML = '<p style="text-align:center; color: var(--text-muted); padding: 2rem;">Aucune tâche dans votre historique.</p>'; | |
| this.clearHistoryBtn.style.display = 'none'; | |
| return; | |
| } | |
| this.clearHistoryBtn.style.display = 'block'; | |
| history.sort((a, b) => b.timestamp - a.timestamp).forEach(task => { | |
| const item = document.createElement('div'); | |
| item.className = 'history-item'; | |
| item.dataset.taskId = task.id; | |
| let statusText = 'En attente...'; | |
| let iconClass = 'pending'; | |
| let icon = '⏳'; | |
| if (task.status === 'completed') { | |
| statusText = 'Terminé'; | |
| iconClass = 'success'; | |
| icon = '✅'; | |
| } else if (task.status === 'error') { | |
| statusText = 'Erreur'; | |
| iconClass = 'error'; | |
| icon = '❌'; | |
| } else if (task.status && task.status.startsWith('generating')) { | |
| statusText = 'Génération...'; | |
| icon = '⚙️'; | |
| } | |
| item.innerHTML = ` | |
| <div class="history-icon ${iconClass}">${icon}</div> | |
| <div class="history-info"> | |
| <div class="history-filename">${task.filename}</div> | |
| <div class="history-meta">${statusText} • ${new Date(task.timestamp).toLocaleDateString('fr-FR')}</div> | |
| </div> | |
| <div class="history-actions" id="actions-${task.id}"></div> | |
| `; | |
| this.historyList.appendChild(item); | |
| this.updateHistoryItemActions(task); | |
| }); | |
| } | |
| updateHistoryItemActions(task) { | |
| const container = document.getElementById(`actions-${task.id}`); | |
| if (!container) return; | |
| if (task.status === 'completed' && task.download_url) { | |
| container.innerHTML = `<a href="${task.download_url}" class="btn btn-success btn-sm">📥</a>`; | |
| } else if (task.status === 'error') { | |
| container.innerHTML = `<span style="color: var(--danger); font-size: 0.8rem;">Échec</span>`; | |
| } else { | |
| container.innerHTML = `<span style="color: var(--primary-solid); font-size: 0.8rem;">En cours...</span>`; | |
| } | |
| } | |
| updateTaskInHistory(taskId, updates) { | |
| let history = this.getHistory(); | |
| const taskIndex = history.findIndex(t => t.id === taskId); | |
| if (taskIndex > -1) { | |
| history[taskIndex] = { ...history[taskIndex], ...updates }; | |
| this.saveHistory(history); | |
| this.renderHistory(); | |
| } | |
| } | |
| checkHistoryStatus() { | |
| this.getHistory().forEach(task => { | |
| if (task.status && !['completed', 'error'].includes(task.status)) { | |
| fetch(`/task/${task.id}`) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.status && data.status !== task.status) { | |
| this.updateTaskInHistory(task.id, { | |
| status: data.status, | |
| download_url: data.download_url, | |
| error: data.error | |
| }); | |
| } | |
| }) | |
| .catch(err => console.error(`Could not check status for ${task.id}:`, err)); | |
| } | |
| }); | |
| } | |
| clearHistory() { | |
| if (confirm("Êtes-vous sûr de vouloir vider tout l'historique ? Cette action est irréversible.")) { | |
| localStorage.removeItem('mariamTaskHistory'); | |
| this.renderHistory(); | |
| this.showToast('Historique vidé', 'success'); | |
| } | |
| } | |
| // Toast Notifications | |
| showToast(message, type = 'success') { | |
| // Remove existing toasts | |
| const existingToasts = document.querySelectorAll('.toast'); | |
| existingToasts.forEach(toast => toast.remove()); | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| toast.textContent = message; | |
| document.body.appendChild(toast); | |
| // Show toast | |
| setTimeout(() => toast.classList.add('show'), 100); | |
| // Hide toast | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| } | |
| // Initialize app when DOM is ready | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new MariamApp(); | |
| }); | |
| // Add touch feedback for better mobile experience | |
| document.addEventListener('touchstart', function() {}, {passive: true}); | |
| // Prevent zoom on double tap for better mobile UX | |
| let lastTouchEnd = 0; | |
| document.addEventListener('touchend', function (event) { | |
| const now = (new Date()).getTime(); | |
| if (now - lastTouchEnd <= 300) { | |
| event.preventDefault(); | |
| } | |
| lastTouchEnd = now; | |
| }, false); | |
| // Add pull-to-refresh indication (visual feedback only) | |
| let startY = 0; | |
| document.addEventListener('touchstart', e => { | |
| startY = e.touches[0].pageY; | |
| }); | |
| document.addEventListener('touchmove', e => { | |
| const y = e.touches[0].pageY; | |
| if (document.scrollingElement.scrollTop === 0 && y > startY && (y - startY) > 80) { | |
| document.body.style.paddingTop = '10px'; | |
| } | |
| }); | |
| document.addEventListener('touchend', () => { | |
| document.body.style.paddingTop = ''; | |
| }); | |
| </script> | |
| </body> | |
| </html> |