Testpdf / templates /index.html
Docfile's picture
Update templates/index.html
5111edf verified
<!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;
--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 {
/* Améliore la fluidité du défilement */
scroll-behavior: smooth;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
max-width: 600px; /* Largeur max plus adaptée pour du contenu centré */
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 {
/* MODIFIÉ: Typographie réactive pour s'adapter à toutes les tailles d'écrans */
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;
}
/* <!-- SUPPRIMÉ: Le conteneur du bouton Telegram a été retiré --> */
.container {
background-color: var(--card-bg);
padding: calc(var(--spacing-unit) * 1.5);
border-radius: 12px; /* Bords plus arrondis pour un look moderne */
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);
}
/* MODIFIÉ: Amélioration sémantique et UX des boutons radio */
.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"] {
/* Caché car on stylise le label à la place */
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; /* MODIFIÉ: Plus haut pour être plus facile à taper */
border: none;
border-radius: 8px;
font-size: 1.1rem; /* Police plus grande */
font-weight: 600; /* Police plus affirmée */
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; } /* Couleur moins agressive pour une action secondaire */
.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); }
/* MODIFIÉ: Le message de patience pour l'utilisateur */
.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; } /* Permet au nom de fichier de se couper correctement */
.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>
<!-- SUPPRIMÉ : Le bouton pour rejoindre le groupe Telegram a été enlevé. -->
<div class="container">
<div class="style-selection">
<h3>🎨 Choisissez le style de résolution</h3>
<div class="radio-group">
<!-- MODIFIÉ: Structure sémantique et accessible des boutons radio -->
<div class="radio-option">
<input type="radio" id="style-light" name="resolution-style" value="light">
<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é, pour une lecture rapide.</div>
</div>
</label>
</div>
<div class="radio-option">
<input type="radio" id="style-colorful" name="resolution-style" value="colorful" checked>
<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">
<!-- MODIFIÉ: Message clair pour informer l'utilisateur de l'attente -->
<div class="processing-notice">
<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>
// Le JavaScript reste identique car il est déjà bien conçu pour gérer cette logique.
// Les modifications sont purement dans le HTML et le CSS.
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));
}
});
}
// La fonction selectStyle n'est plus nécessaire grâce à l'utilisation des <label>
// 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...';
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é ! 🎉';
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>