|
<!DOCTYPE html> |
|
<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; |
|
--shadow: 0 4px 15px rgba(0,0,0,0.1); --spacing-unit: 1rem; |
|
} |
|
* { box-sizing: border-box; margin: 0; padding: 0; } |
|
body { |
|
font-family: 'Segoe UI', system-ui, sans-serif; max-width: 800px; margin: 0 auto; |
|
padding: calc(var(--spacing-unit) * 2); 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: 2.5rem; color: #2c3e50; margin-bottom: calc(var(--spacing-unit) * 0.5); } |
|
.header .subtitle { font-size: 1.1rem; color: #555; } |
|
.telegram-join-button-container { text-align: center; margin-bottom: calc(var(--spacing-unit) * 2); } |
|
.telegram-button { |
|
display: inline-block; background-color: #0088cc; color: white; |
|
padding: var(--spacing-unit) calc(var(--spacing-unit) * 2); border-radius: 0.5rem; |
|
text-decoration: none; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
|
} |
|
.telegram-button:hover { transform: translateY(-2px); background-color: #006699; } |
|
.container { background-color: white; padding: calc(var(--spacing-unit) * 2); border-radius: 1rem; box-shadow: var(--shadow); margin-bottom: 2rem; } |
|
.style-selection { background-color: #f9f9f9; padding: calc(var(--spacing-unit) * 1.5); border-radius: 0.75rem; border: 1px solid var(--border-color); margin-bottom: calc(var(--spacing-unit) * 1.5); } |
|
.style-selection h3 { margin-bottom: var(--spacing-unit); color: #2c3e50; font-size: 1.2rem; } |
|
.radio-group { display: flex; flex-direction: column; gap: var(--spacing-unit); } |
|
.radio-option { display: flex; align-items: flex-start; padding: calc(var(--spacing-unit) * 0.75); border-radius: 0.5rem; transition: background-color 0.2s; cursor: pointer; border: 1px solid transparent; } |
|
.radio-option:hover { background-color: #f0f4f8; border-color: var(--primary-color); } |
|
.radio-option input[type="radio"] { margin-top: 0.25rem; margin-right: calc(var(--spacing-unit) * 0.75); width: 1.25rem; height: 1.25rem; accent-color: var(--primary-color); } |
|
.radio-content { flex: 1; } |
|
.radio-label { font-weight: 500; margin-bottom: calc(var(--spacing-unit) * 0.25); display: block; } |
|
.radio-description { font-size: 0.9rem; color: #666; } |
|
.upload-section { border: 3px dashed var(--border-color); padding: calc(var(--spacing-unit) * 2); text-align: center; border-radius: 0.75rem; 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: 2.5rem; 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: 0.5rem; background-color: #fdfdfd; } |
|
.preview-item img { max-width: 100px; max-height: 100px; border-radius: 0.25rem; object-fit: cover; } |
|
.preview-item .pdf-icon { font-size: 3rem; color: var(--danger-color); } |
|
.preview-item span { font-size: 0.8rem; color: #555; word-break: break-all; max-width: 100px; text-align: center; } |
|
.button { width: 100%; padding: var(--spacing-unit); border: none; border-radius: 0.5rem; font-size: 1rem; cursor: pointer; transition: all 0.3s ease; margin: var(--spacing-unit) 0; 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; } |
|
.clear-button { background-color: var(--danger-color); margin-top: 0; } |
|
.clear-button:hover:not(:disabled) { background-color: var(--danger-hover); } |
|
.download-button { background-color: var(--secondary-color); } |
|
.download-button:hover:not(:disabled) { background-color: var(--secondary-hover); } |
|
.cooldown-notice { background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 0.5rem; padding: var(--spacing-unit); margin: var(--spacing-unit) 0; text-align: center; color: #856404; font-weight: 500; } |
|
.cooldown-timer { font-size: 1.2rem; color: #d63031; font-weight: bold; } |
|
#solving-container { display: none; background-color: #f9f9f9; padding: calc(var(--spacing-unit) * 1.5); border-radius: 0.75rem; border: 1px solid var(--border-color); margin-top: calc(var(--spacing-unit) * 1.5); } |
|
.status { text-align: center; margin-bottom: var(--spacing-unit); font-weight: bold; color: #2c3e50; } |
|
.status.error { color: #e74c3c; } .status.completed { color: #2ecc71; } |
|
.telegram-notice { background-color: #eaf5ff; border-left: 5px solid var(--primary-color); padding: var(--spacing-unit); margin: var(--spacing-unit) 0; border-radius: 0 0.5rem 0.5rem 0; } |
|
.response-container { display: none; margin-top: calc(var(--spacing-unit) * 1.5); } |
|
#response { background-color: #fdfdfd; padding: var(--spacing-unit); border-radius: 0.5rem; border: 1px solid #eee; min-height: 50px; white-space: pre-wrap; word-wrap: break-word; } |
|
#history-container { margin-top: 2rem; } |
|
#history-container h2 { text-align:center; margin-bottom: 1rem; color: #2c3e50;} |
|
#history-list { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 0.5rem;} |
|
.history-item { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem; background: #fff; border: 1px solid var(--border-color); border-radius: 0.5rem; transition: box-shadow 0.2s; } |
|
.history-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); } |
|
.history-info { display: flex; flex-direction: column; } |
|
.history-filename { font-weight: 500; } |
|
.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 .button { width: auto; padding: 0.5rem 1rem; font-size: 0.9rem; margin: 0; } |
|
#clear-history-button { background-color: var(--danger-color); margin-top: 1rem; } |
|
@media (max-width: 768px) { |
|
body { padding: var(--spacing-unit); } |
|
.header h1 { font-size: 1.75rem; } |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="header"> |
|
<h1>🖼️ Science (Math, Physique, Chimie) 🧠</h1> |
|
<p class="subtitle">Avec Mariam, votre assistante IA</p> |
|
</div> |
|
|
|
<div class="telegram-join-button-container"> |
|
<a href="https://t.me/+ic4zemy1E1k0MzQ0" target="_blank" class="telegram-button"> |
|
🚀 Rejoindre le Groupe Telegram |
|
</a> |
|
</div> |
|
|
|
<div class="container"> |
|
<div class="style-selection"> |
|
<h3>🎨 Choisissez le style de résolution</h3> |
|
<div class="radio-group"> |
|
<div class="radio-option" onclick="selectStyle('light')"> |
|
<input type="radio" id="style-light" name="resolution-style" value="light"> |
|
<div class="radio-content"> |
|
<label class="radio-label" for="style-light">📝 Résolution Light</label> |
|
<div class="radio-description">Format simple et épuré, idéal pour une lecture rapide</div> |
|
</div> |
|
</div> |
|
<div class="radio-option" onclick="selectStyle('colorful')"> |
|
<input type="radio" id="style-colorful" name="resolution-style" value="colorful" checked> |
|
<div class="radio-content"> |
|
<label class="radio-label" for="style-colorful">🌈 Résolution Colorée</label> |
|
<div class="radio-description">Format richement formaté avec couleurs et mise en page élégante</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div id="cooldown-notice" class="cooldown-notice" style="display: none;"> |
|
⏰ Veuillez attendre <span id="cooldown-timer" class="cooldown-timer">2:00</span> avant de pouvoir soumettre à nouveau. |
|
</div> |
|
|
|
<div id="upload-section" class="upload-section"> |
|
<div class="upload-icon">📤</div> |
|
<p>Cliquez ou glissez-déposez vos images et/ou 1 fichier PDF ici</p> |
|
<input type="file" id="file-input" accept="image/*,application/pdf" multiple> |
|
<div id="file-preview-area"></div> |
|
</div> |
|
|
|
<button id="clear-files-button" class="button clear-button" style="display: none;">🗑️ Effacer les fichiers</button> |
|
<button id="solve-button" class="button" disabled>🔍 Résoudre</button> |
|
|
|
<div id="solving-container"> |
|
<div class="status" id="status">En attente de résolution...</div> |
|
<div class="telegram-notice"> |
|
Votre PDF sera disponible au téléchargement ici-même et dans votre historique une fois le traitement terminé. |
|
</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"></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'); |
|
|
|
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)); |
|
} |
|
}); |
|
} |
|
|
|
window.selectStyle = (style) => document.getElementById(`style-${style}`).checked = true; |
|
|
|
function checkCooldownOnLoad() { |
|
const savedCooldown = localStorage.getItem('mariamCooldownEndTime'); |
|
if (savedCooldown && parseInt(savedCooldown) > Date.now()) { |
|
cooldownEndTime = parseInt(savedCooldown); |
|
startCooldownTimer(); |
|
} |
|
} |
|
|
|
function startCooldown() { |
|
cooldownEndTime = Date.now() + 2 * 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 = '⏳ Traitement...'; |
|
|
|
solvingContainer.style.display = 'block'; |
|
responseContainer.style.display = 'none'; |
|
downloadButton.style.display = 'none'; |
|
statusElement.className = 'status'; |
|
statusElement.textContent = 'Préparation...'; |
|
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 => { |
|
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 (ID: ' + task_id.substring(0, 8) + '...)'; |
|
listenToTask(task_id); |
|
}) |
|
.catch(error => 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') { |
|
statusElement.className = 'status completed'; |
|
statusElement.textContent = 'Traitement terminé avec succès ! 🎉'; |
|
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') { |
|
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> |