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-color: #3498db; | |
| --primary-hover: #2980b9; | |
| --secondary-color: #2ecc71; | |
| --secondary-hover: #27ae60; | |
| --danger-color: #e74c3c; | |
| --danger-hover: #c0392b; | |
| --background-color: #f4f7f6; | |
| --text-color: #333; | |
| --border-color: #e0e0e0; | |
| --card-bg: #ffffff; | |
| --shadow: 0 4px 15px rgba(0, 0, 0, 0.08); | |
| --spacing-unit: 1rem; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| html { | |
| scroll-behavior: smooth; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| max-width: 600px; | |
| margin: 0 auto; | |
| padding: var(--spacing-unit); | |
| line-height: 1.6; | |
| background-color: var(--background-color); | |
| color: var(--text-color); | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: calc(var(--spacing-unit) * 2); | |
| } | |
| .header h1 { | |
| font-size: clamp(1.75rem, 7vw, 2.5rem); | |
| color: #2c3e50; | |
| margin-bottom: calc(var(--spacing-unit) * 0.25); | |
| } | |
| .header .subtitle { | |
| font-size: clamp(1rem, 4vw, 1.1rem); | |
| color: #555; | |
| } | |
| .container { | |
| background-color: var(--card-bg); | |
| padding: calc(var(--spacing-unit) * 1.5); | |
| border-radius: 12px; | |
| box-shadow: var(--shadow); | |
| margin-bottom: calc(var(--spacing-unit) * 2); | |
| } | |
| .style-selection h3 { | |
| margin-bottom: var(--spacing-unit); | |
| color: #2c3e50; | |
| font-size: 1.2rem; | |
| text-align: center; | |
| } | |
| .radio-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--spacing-unit); | |
| } | |
| .radio-option label { | |
| display: flex; | |
| align-items: flex-start; | |
| padding: var(--spacing-unit); | |
| border-radius: 8px; | |
| transition: background-color 0.2s, box-shadow 0.2s; | |
| cursor: pointer; | |
| border: 1px solid var(--border-color); | |
| } | |
| .radio-option label:hover { | |
| border-color: var(--primary-color); | |
| } | |
| .radio-option input[type="radio"]:checked + .radio-content-wrapper { | |
| border-color: var(--primary-color); | |
| box-shadow: 0 0 0 2px var(--primary-color); | |
| background-color: #eaf5ff; | |
| } | |
| .radio-option input[type="radio"] { | |
| display: none; | |
| } | |
| .radio-content-wrapper { | |
| display: flex; | |
| align-items: center; | |
| width: 100%; | |
| gap: var(--spacing-unit); | |
| } | |
| .radio-icon { font-size: 1.5rem; } | |
| .radio-label { font-weight: 500; display: block; } | |
| .radio-description { font-size: 0.9rem; color: #666; } | |
| .upload-section { | |
| border: 2px dashed var(--border-color); | |
| padding: calc(var(--spacing-unit) * 2); | |
| text-align: center; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| background-color: #f8f9fa; | |
| margin: calc(var(--spacing-unit) * 1.5) 0; | |
| } | |
| .upload-section:hover { | |
| border-color: var(--primary-color); | |
| background-color: #e8f4fb; | |
| } | |
| .upload-icon { | |
| font-size: 3rem; | |
| margin-bottom: var(--spacing-unit); | |
| color: var(--primary-color); | |
| } | |
| #file-input { display: none; } | |
| #file-preview-area { margin-top: var(--spacing-unit); display: flex; flex-wrap: wrap; gap: var(--spacing-unit); justify-content: center; } | |
| .preview-item { display: flex; flex-direction: column; align-items: center; gap: calc(var(--spacing-unit) * 0.5); padding: calc(var(--spacing-unit) * 0.5); border: 1px solid var(--border-color); border-radius: 8px; background-color: #fdfdfd; } | |
| .preview-item img { width: 80px; height: 80px; border-radius: 4px; object-fit: cover; } | |
| .preview-item .pdf-icon { font-size: 3rem; color: var(--danger-color); line-height: 1; padding: 12px 0; } | |
| .preview-item span { font-size: 0.8rem; color: #555; word-break: break-all; max-width: 80px; text-align: center; } | |
| .button { | |
| width: 100%; | |
| padding: 0.9rem 1rem; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| margin-top: calc(var(--spacing-unit) * 0.5); | |
| background-color: var(--primary-color); | |
| color: white; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| text-decoration: none; | |
| display: inline-block; | |
| text-align:center; | |
| } | |
| .button:hover:not(:disabled) { transform: translateY(-2px); background-color: var(--primary-hover); } | |
| .button:disabled { background-color: #bdc3c7; cursor: not-allowed; box-shadow: none; } | |
| .clear-button { background-color: #7f8c8d; } | |
| .clear-button:hover:not(:disabled) { background-color: #95a5a6; } | |
| .download-button { background-color: var(--secondary-color); } | |
| .download-button:hover:not(:disabled) { background-color: var(--secondary-hover); } | |
| .cooldown-notice { background-color: #fff3cd; border-left: 4px solid #ffeaa7; border-radius: 8px; padding: var(--spacing-unit); margin-bottom: var(--spacing-unit); text-align: center; color: #856404; } | |
| .cooldown-timer { font-weight: bold; } | |
| #solving-container { display: none; margin-top: calc(var(--spacing-unit) * 1.5); } | |
| .status { text-align: center; margin-bottom: var(--spacing-unit); font-weight: bold; color: #2c3e50; font-size: 1.1rem;} | |
| .status.error { color: var(--danger-color); } | |
| .status.completed { color: var(--secondary-color); } | |
| /* NOUVEAU: Message d'upload en cours */ | |
| .upload-notice { | |
| background-color: #fff3cd; | |
| border-left: 4px solid #ffc107; | |
| padding: var(--spacing-unit); | |
| margin: var(--spacing-unit) 0; | |
| border-radius: 8px; | |
| text-align: center; | |
| } | |
| .upload-notice p { margin: 0; font-weight: 600; color: #856404; } | |
| .upload-notice small { color: #6c5d03; } | |
| .processing-notice { | |
| background-color: #eaf5ff; | |
| border-left: 4px solid var(--primary-color); | |
| padding: var(--spacing-unit); | |
| margin: var(--spacing-unit) 0; | |
| border-radius: 8px; | |
| text-align: center; | |
| } | |
| .processing-notice p { margin: 0; font-weight: 600; color: #2c3e50;} | |
| .processing-notice small { color: #555; } | |
| .response-container { display: none; margin-top: calc(var(--spacing-unit) * 1.5); } | |
| #response { background-color: #fdfdfd; padding: var(--spacing-unit); border-radius: 8px; border: 1px solid #eee; min-height: 50px; white-space: pre-wrap; word-wrap: break-word; } | |
| #history-container h2 { text-align:center; margin-bottom: var(--spacing-unit); color: #2c3e50;} | |
| #history-list { list-style: none; padding: 0; display: flex; flex-direction: column; gap: calc(var(--spacing-unit)*0.75);} | |
| .history-item { display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-unit); background: var(--card-bg); border: 1px solid var(--border-color); border-radius: 8px; transition: box-shadow 0.2s; gap: var(--spacing-unit); } | |
| .history-info { flex-grow: 1; min-width: 0; } | |
| .history-filename { font-weight: 500; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | |
| .history-status { font-size: 0.85rem; } | |
| .history-status-pending { color: #f39c12; } .history-status-completed { color: var(--secondary-color); } .history-status-error { color: var(--danger-color); } | |
| .history-actions { flex-shrink: 0; } | |
| .history-actions .button { width: auto; padding: 0.5rem 1rem; font-size: 0.9rem; margin: 0; } | |
| #clear-history-button { background-color: var(--danger-color); margin-top: 1rem; } | |
| #clear-history-button:hover:not(:disabled) { background-color: var(--danger-hover); } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="header"> | |
| <h1>🖼️ Science (Math, Physique, Chimie) 🧪</h1> | |
| <p class="subtitle">Avec Mariam, votre assistante IA</p> | |
| </div> | |
| <div class="container"> | |
| <div class="style-selection"> | |
| <h3>🎨 Choisissez le style de résolution</h3> | |
| <div class="radio-group"> | |
| <div class="radio-option"> | |
| <input type="radio" id="style-light" name="resolution-style" value="light" checked> | |
| <label for="style-light" class="radio-content-wrapper"> | |
| <div class="radio-icon">📝</div> | |
| <div class="radio-content"> | |
| <span class="radio-label">Résolution Light</span> | |
| <div class="radio-description">Simple et épurée, pour une lecture rapide.</div> | |
| </div> | |
| </label> | |
| </div> | |
| <div class="radio-option"> | |
| <input type="radio" id="style-colorful" name="resolution-style" value="colorful"> | |
| <label for="style-colorful" class="radio-content-wrapper"> | |
| <div class="radio-icon">🌈</div> | |
| <div class="radio-content"> | |
| <span class="radio-label">Résolution Colorée</span> | |
| <div class="radio-description">Format riche avec couleurs et mise en page.</div> | |
| </div> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="cooldown-notice" class="cooldown-notice" style="display: none;"> | |
| ⏰ Veuillez attendre <span id="cooldown-timer" class="cooldown-timer"></span> avant une nouvelle soumission. | |
| </div> | |
| <div id="upload-section" class="upload-section"> | |
| <div class="upload-icon">📤</div> | |
| <p><strong>Cliquez ou déposez vos fichiers ici</strong></p> | |
| <small>Images (PNG, JPG...) ou 1 PDF</small> | |
| <input type="file" id="file-input" accept="image/*,application/pdf" multiple> | |
| </div> | |
| <div id="file-preview-area"></div> | |
| <button id="clear-files-button" class="button clear-button" style="display: none;">🗑️ Effacer la sélection</button> | |
| <button id="solve-button" class="button" disabled>🔍 Résoudre</button> | |
| <div id="solving-container"> | |
| <!-- NOUVEAU: Message d'upload en cours --> | |
| <div id="upload-notice" class="upload-notice" style="display: none;"> | |
| <p>📤 Envoi des fichiers en cours...</p> | |
| <small>Veuillez patienter pendant le téléchargement de vos fichiers vers le serveur.</small> | |
| </div> | |
| <div id="processing-notice" class="processing-notice" style="display: none;"> | |
| <p>⏳ La génération a commencé...</p> | |
| <small>Cela peut prendre quelques minutes. Vous pouvez fermer cette page et revenir plus tard, votre résultat apparaîtra dans l'historique ci-dessous.</small> | |
| </div> | |
| <div class="status" id="status"></div> | |
| <div class="response-container" id="response-container"> | |
| <div id="response"></div> | |
| <a id="download-button" class="button download-button" style="display: none;">📥 Télécharger le PDF</a> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="history-container" class="container"> | |
| <h2>Historique des Tâches</h2> | |
| <ul id="history-list"> | |
| <!-- L'historique sera rempli par le JavaScript --> | |
| </ul> | |
| <button id="clear-history-button" class="button">🗑️ Vider l'historique</button> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const uploadSection = document.getElementById('upload-section'); | |
| const fileInput = document.getElementById('file-input'); | |
| const filePreviewArea = document.getElementById('file-preview-area'); | |
| const solveButton = document.getElementById('solve-button'); | |
| const clearFilesButton = document.getElementById('clear-files-button'); | |
| const solvingContainer = document.getElementById('solving-container'); | |
| const responseContainer = document.getElementById('response-container'); | |
| const responseDiv = document.getElementById('response'); | |
| const statusElement = document.getElementById('status'); | |
| const downloadButton = document.getElementById('download-button'); | |
| const cooldownNotice = document.getElementById('cooldown-notice'); | |
| const cooldownTimer = document.getElementById('cooldown-timer'); | |
| const historyList = document.getElementById('history-list'); | |
| const clearHistoryButton = document.getElementById('clear-history-button'); | |
| // NOUVEAU: Références aux nouveaux éléments | |
| const uploadNotice = document.getElementById('upload-notice'); | |
| const processingNotice = document.getElementById('processing-notice'); | |
| let selectedFiles = []; | |
| let cooldownEndTime = 0; | |
| let cooldownInterval = null; | |
| const eventSources = {}; | |
| const getHistory = () => JSON.parse(localStorage.getItem('mariamTaskHistory')) || []; | |
| const saveHistory = (history) => localStorage.setItem('mariamTaskHistory', JSON.stringify(history)); | |
| function renderHistory() { | |
| historyList.innerHTML = ''; | |
| const history = getHistory(); | |
| if (history.length === 0) { | |
| historyList.innerHTML = '<p style="text-align:center; color:#777;">Aucune tâche dans votre historique.</p>'; | |
| clearHistoryButton.style.display = 'none'; | |
| return; | |
| } | |
| clearHistoryButton.style.display = 'block'; | |
| history.sort((a, b) => b.timestamp - a.timestamp).forEach(task => { | |
| const li = document.createElement('li'); | |
| li.classList.add('history-item'); | |
| li.dataset.taskId = task.id; | |
| let statusText = 'En attente...'; | |
| let statusClass = 'history-status-pending'; | |
| if (task.status === 'completed') { | |
| statusText = 'Terminé'; | |
| statusClass = 'history-status-completed'; | |
| } else if (task.status === 'error') { | |
| statusText = 'Erreur'; | |
| statusClass = 'history-status-error'; | |
| } else if (task.status && task.status.startsWith('generating')) { | |
| statusText = 'Génération en cours...'; | |
| } else if (task.status) { | |
| statusText = task.status.charAt(0).toUpperCase() + task.status.slice(1); | |
| } | |
| li.innerHTML = ` | |
| <div class="history-info"> | |
| <span class="history-filename">${task.filename}</span> | |
| <small class="history-status ${statusClass}">${statusText} - ${new Date(task.timestamp).toLocaleString('fr-FR')}</small> | |
| </div> | |
| <div class="history-actions" id="actions-${task.id}"></div> | |
| `; | |
| historyList.appendChild(li); | |
| updateHistoryItemActions(task); | |
| }); | |
| } | |
| function 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="button download-button">Télécharger</a>`; | |
| } else if (task.status === 'error') { | |
| container.innerHTML = `<span style="color:var(--danger-color); font-weight:bold;">Échec</span>`; | |
| } else { | |
| container.innerHTML = `<span style="color:var(--primary-color); font-style:italic;">En cours...</span>`; | |
| } | |
| } | |
| function updateTaskInHistory(taskId, updates) { | |
| let history = getHistory(); | |
| const taskIndex = history.findIndex(t => t.id === taskId); | |
| if (taskIndex > -1) { | |
| history[taskIndex] = { ...history[taskIndex], ...updates }; | |
| saveHistory(history); | |
| renderHistory(); | |
| } | |
| } | |
| function checkHistoryStatus() { | |
| 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) { | |
| 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)); | |
| } | |
| }); | |
| } | |
| function checkCooldownOnLoad() { | |
| const savedCooldown = localStorage.getItem('mariamCooldownEndTime'); | |
| if (savedCooldown && parseInt(savedCooldown) > Date.now()) { | |
| cooldownEndTime = parseInt(savedCooldown); | |
| startCooldownTimer(); | |
| } | |
| } | |
| function startCooldown() { | |
| cooldownEndTime = Date.now() + 15 * 60 * 1000; | |
| localStorage.setItem('mariamCooldownEndTime', cooldownEndTime.toString()); | |
| startCooldownTimer(); | |
| } | |
| function startCooldownTimer() { | |
| cooldownNotice.style.display = 'block'; | |
| solveButton.disabled = true; | |
| if (cooldownInterval) clearInterval(cooldownInterval); | |
| cooldownInterval = setInterval(() => { | |
| const remaining = Math.max(0, cooldownEndTime - Date.now()); | |
| if (remaining <= 0) { | |
| clearInterval(cooldownInterval); | |
| cooldownNotice.style.display = 'none'; | |
| updateButtonsState(); | |
| return; | |
| } | |
| const minutes = Math.floor(remaining / 60000); | |
| const seconds = Math.floor((remaining % 60000) / 1000); | |
| cooldownTimer.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; | |
| }, 1000); | |
| } | |
| const isCooldownActive = () => Date.now() < cooldownEndTime; | |
| const handleFileSelection = (files) => { | |
| const newFiles = Array.from(files); | |
| let pdfSelected = selectedFiles.some(f => f.type === 'application/pdf'); | |
| newFiles.forEach(file => { | |
| if (file.type.startsWith('image/')) { | |
| if (!selectedFiles.some(sf => sf.name === file.name && sf.size === file.size)) selectedFiles.push(file); | |
| } else if (file.type === 'application/pdf') { | |
| if (!pdfSelected) { | |
| selectedFiles = selectedFiles.filter(f => f.type !== 'application/pdf'); | |
| selectedFiles.push(file); | |
| pdfSelected = true; | |
| } | |
| } | |
| }); | |
| updateFilePreviews(); | |
| updateButtonsState(); | |
| }; | |
| uploadSection.addEventListener('click', () => fileInput.click()); | |
| uploadSection.addEventListener('dragover', (e) => { e.preventDefault(); uploadSection.classList.add('hover'); }); | |
| uploadSection.addEventListener('dragleave', (e) => uploadSection.classList.remove('hover')); | |
| uploadSection.addEventListener('drop', (e) => { e.preventDefault(); uploadSection.classList.remove('hover'); if (e.dataTransfer.files.length) handleFileSelection(e.dataTransfer.files); }); | |
| fileInput.addEventListener('change', (e) => { if (e.target.files.length) handleFileSelection(e.target.files); }); | |
| function updateFilePreviews() { | |
| filePreviewArea.innerHTML = ''; | |
| if (selectedFiles.length === 0) return; | |
| selectedFiles.forEach(file => { | |
| const item = document.createElement('div'); | |
| item.className = 'preview-item'; | |
| const name = document.createElement('span'); | |
| name.textContent = file.name.length > 15 ? file.name.substring(0, 12) + "..." : 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>'; | |
| } | |
| item.appendChild(name); | |
| filePreviewArea.appendChild(item); | |
| }); | |
| } | |
| function updateButtonsState() { | |
| const hasFiles = selectedFiles.length > 0; | |
| solveButton.disabled = !hasFiles || isCooldownActive(); | |
| solveButton.textContent = hasFiles ? `🔍 Résoudre (${selectedFiles.length} fichier(s))` : '🔍 Résoudre'; | |
| clearFilesButton.style.display = hasFiles ? 'block' : 'none'; | |
| } | |
| clearFilesButton.addEventListener('click', () => { | |
| selectedFiles = []; | |
| fileInput.value = ''; | |
| updateFilePreviews(); | |
| updateButtonsState(); | |
| solvingContainer.style.display = 'none'; | |
| }); | |
| solveButton.addEventListener('click', () => { | |
| if (selectedFiles.length === 0 || isCooldownActive()) return; | |
| startCooldown(); | |
| solveButton.disabled = true; | |
| solveButton.textContent = '⏳ Envoi en cours...'; | |
| // MODIFIÉ: Affichage immédiat du conteneur avec message d'upload | |
| solvingContainer.style.display = 'block'; | |
| uploadNotice.style.display = 'block'; // Affiche le message d'upload | |
| processingNotice.style.display = 'none'; // Cache le message de traitement | |
| responseContainer.style.display = 'none'; | |
| downloadButton.style.display = 'none'; | |
| statusElement.textContent = ''; // Pas de statut tant que l'upload n'est pas fini | |
| responseDiv.innerHTML = ''; | |
| const formData = new FormData(); | |
| 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 => { | |
| // MODIFIÉ: Masquer le message d'upload et afficher le message de traitement | |
| uploadNotice.style.display = 'none'; | |
| processingNotice.style.display = 'block'; | |
| statusElement.textContent = 'Préparation...'; // Maintenant on peut afficher ce message | |
| 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 = getHistory(); | |
| history.push({ id: task_id, filename: first_filename, status: 'pending', timestamp: Date.now() }); | |
| saveHistory(history); | |
| renderHistory(); | |
| statusElement.textContent = 'Traitement en arrière-plan...'; | |
| listenToTask(task_id); | |
| }) | |
| .catch(error => { | |
| // MODIFIÉ: En cas d'erreur, masquer les messages d'upload/processing | |
| uploadNotice.style.display = 'none'; | |
| processingNotice.style.display = 'none'; | |
| handleError(error.message); | |
| }); | |
| }); | |
| function listenToTask(taskId) { | |
| if (eventSources[taskId]) eventSources[taskId].close(); | |
| const eventSource = new EventSource('/stream/' + taskId); | |
| eventSources[taskId] = eventSource; | |
| eventSource.onmessage = function(event) { | |
| const data = JSON.parse(event.data); | |
| updateTaskInHistory(taskId, { status: data.status, download_url: data.download_url, error: data.error }); | |
| statusElement.textContent = `Statut: ${data.status}`; | |
| if (data.status === 'completed') { | |
| processingNotice.style.display = 'none'; | |
| statusElement.className = 'status completed'; | |
| statusElement.textContent = 'Traitement terminé ! 🎉'; | |
| responseDiv.innerHTML = `<p style="color: #2ecc71; text-align: center;">Votre PDF est prêt.</p>`; | |
| downloadButton.href = data.download_url; | |
| downloadButton.style.display = 'block'; | |
| responseContainer.style.display = 'block'; | |
| eventSource.close(); | |
| } else if (data.status === 'error') { | |
| processingNotice.style.display = 'none'; | |
| handleError(data.error || 'Une erreur inattendue est survenue.', taskId); | |
| eventSource.close(); | |
| } | |
| }; | |
| eventSource.onerror = function() { | |
| eventSource.close(); | |
| checkHistoryStatus(); | |
| }; | |
| } | |
| function handleError(errorMessage, taskId = null) { | |
| statusElement.className = 'status error'; | |
| statusElement.textContent = 'Erreur:'; | |
| responseDiv.innerHTML = `<p style="color:red;">${errorMessage}</p>`; | |
| responseContainer.style.display = 'block'; | |
| downloadButton.style.display = 'none'; | |
| if (taskId) updateTaskInHistory(taskId, { status: 'error', error: errorMessage }); | |
| } | |
| clearHistoryButton.addEventListener('click', () => { | |
| if(confirm("Êtes-vous sûr de vouloir vider tout l'historique ? Cette action est irréversible.")) { | |
| localStorage.removeItem('mariamTaskHistory'); | |
| renderHistory(); | |
| } | |
| }); | |
| checkCooldownOnLoad(); | |
| renderHistory(); | |
| checkHistoryStatus(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |