Testpdf / templates /index.html
Docfile's picture
Update templates/index.html
8df3575 verified
raw
history blame
23.7 kB
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Solveur Expert IA - Maths, Physique, Chimie</title>
<!-- Tailwind CSS Play CDN (v3 - JIT enabled) -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Fira+Code&display=swap" rel="stylesheet">
<!-- Font Awesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
/* Styles personnalisés complémentaires */
:root {
--primary-color: #2c3e50;
--secondary-color: #1abc9c;
--accent-color: #e74c3c;
--success-color: #27ae60;
--light-secondary-bg: #e8f8f5; /* For upload highlight */
}
body {
font-family: 'Montserrat', sans-serif;
}
.font-code {
font-family: 'Fira Code', monospace;
}
/* Gradients can be defined here or as Tailwind arbitrary values if preferred */
.gradient-primary {
background-image: linear-gradient(to right, var(--secondary-color) 0%, #16a085 100%);
}
.gradient-secondary {
background-image: linear-gradient(to right, var(--primary-color) 0%, #34495e 100%);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.animate-spin-custom {
animation: spin 0.8s linear infinite;
}
/* This class is added by JS, so define its properties here */
.upload-highlight {
border-color: var(--secondary-color) !important; /* Use !important if Tailwind specificity is an issue */
background-color: var(--light-secondary-bg) !important;
}
</style>
<script>
// Optional: Configure Tailwind if needed (e.g., extending theme colors)
// For this example, we'll rely on arbitrary values for CSS variables,
// which the Play CDN handles well.
// tailwind.config = {
// theme: {
// extend: {
// colors: {
// 'custom-primary': 'var(--primary-color)',
// 'custom-secondary': 'var(--secondary-color)',
// 'custom-accent': 'var(--accent-color)',
// 'custom-success': 'var(--success-color)',
// }
// }
// }
// }
</script>
</head>
<body class="bg-gray-50 text-gray-700 min-h-screen flex flex-col items-center py-6 px-4">
<div class="bg-white rounded-xl shadow-lg w-full max-w-2xl p-6 md:p-8">
<h1 class="flex items-center justify-center text-3xl font-bold text-center mb-2 text-[var(--primary-color)]">
<i class="fas fa-atom text-4xl mr-3 text-[var(--secondary-color)]"></i>
Solveur Expert IA
</h1>
<p class="text-center text-gray-500 mb-8">Solutions LaTeX précises pour Maths, Physique et Chimie.</p>
<div id="upload-section" class="relative border-2 border-dashed border-gray-300 rounded-xl p-8 mb-6 cursor-pointer transition-all duration-300 bg-gray-50 text-center group hover:border-[var(--secondary-color)] hover:bg-[var(--light-secondary-bg)]">
<div class="upload-content">
<i class="fas fa-file-arrow-up text-5xl mb-4 text-[var(--secondary-color)] transition-transform duration-300 transform group-hover:scale-110 group-hover:-translate-y-1"></i>
<p class="text-lg font-medium mb-1">Déposez l'image de votre exercice ici</p>
<p class="text-sm text-gray-500">ou cliquez pour sélectionner un fichier (PNG, JPG)</p>
</div>
<input type="file" id="file-input" accept="image/png, image/jpeg, image/webp" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
<div id="image-preview-container" class="mt-4">
<img id="image-preview" src="#" alt="Aperçu de l'énoncé" class="hidden max-w-full max-h-64 rounded-lg mx-auto border border-gray-300 shadow-sm">
</div>
</div>
<div class="bg-gray-50 rounded-xl p-5 mb-6 border border-gray-200">
<h3 class="flex items-center font-semibold text-[var(--primary-color)] mb-4">
<i class="fas fa-cogs mr-2 text-[var(--secondary-color)]"></i>Options de Formatage
</h3>
<div class="prompt-selector">
<label for="prompt-type" class="block font-medium mb-2">Style de la correction LaTeX :</label>
<div class="relative">
<select id="prompt-type" name="prompt-type" class="w-full p-3 pr-10 rounded-xl border-2 border-gray-300 appearance-none bg-white focus:outline-none focus:border-[var(--secondary-color)] focus:ring-2 focus:ring-[var(--secondary-color)]/20 transition-all">
<option value="refined">Format Raffiné & Complet (mise en page avancée)</option>
<option value="light">Format Léger & Essentiel (LaTeX standard)</option>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-gray-700">
<i class="fas fa-chevron-down"></i>
</div>
</div>
</div>
</div>
<button id="solve-button" class="gradient-primary w-full py-4 px-6 rounded-xl text-white font-semibold text-lg tracking-wide flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-gray-400 disabled:shadow-none transition duration-300 transform hover:translate-y-px disabled:transform-none" disabled>
<i class="fas fa-rocket mr-2"></i>Obtenir la Solution
</button>
<a href="https://t.me/+ic4zemy1E1k0MzQ0" target="_blank" id="telegram-join-button" class="mt-4 w-full bg-blue-500 hover:bg-blue-600 text-white font-semibold py-3 px-6 rounded-xl flex items-center justify-center transition duration-300 transform hover:translate-y-px">
<i class="fab fa-telegram-plane mr-2"></i>Rejoindre le groupe Telegram
</a>
<div id="solving-container" class="hidden mt-8">
<div class="text-center mb-5">
<div id="status-message-element" class="flex items-center justify-center flex-wrap text-lg font-medium text-[var(--primary-color)]">
<i class="fas fa-hourglass-start mr-2"></i>Prêt à résoudre votre exercice...
</div>
</div>
<div id="loading-spinner-element" class="hidden w-10 h-10 mx-auto my-6 border-4 border-gray-300 border-l-[var(--secondary-color)] rounded-full animate-spin-custom"></div>
<div class="flex items-center bg-blue-50 border-l-4 border-[var(--secondary-color)] p-4 rounded-xl my-6">
<i class="fab fa-telegram text-2xl text-[var(--secondary-color)] mr-3"></i>
<span>Une copie de la solution sera envoyée sur Telegram pour archivage.</span>
</div>
<div id="response-container-element" class="hidden mt-6 p-6 border border-gray-300 rounded-xl bg-white">
<h3 class="flex items-center font-semibold text-xl text-[var(--primary-color)] mb-4">
<i class="fas fa-file-code mr-3 text-[var(--secondary-color)]"></i>Correction LaTeX Détaillée :
</h3>
<div id="response-output" class="font-code bg-gray-900 text-gray-200 p-5 rounded-xl overflow-x-auto whitespace-pre-wrap break-words max-h-96 mb-5 border border-gray-700"></div>
<button id="copy-button" class="gradient-secondary px-6 py-3 rounded-xl text-white font-medium flex items-center justify-center transition duration-300 transform hover:translate-y-px">
<i class="fas fa-copy mr-2"></i>Copier le code LaTeX
</button>
</div>
<div id="error-display-element" class="hidden bg-red-50 border-2 border-[var(--accent-color)] text-[var(--accent-color)] p-4 rounded-xl my-5 font-medium">
</div>
</div>
</div>
<footer class="mt-12 mb-5 text-gray-500 text-sm text-center">
Solutions générées par <a href="#" target="_blank" class="text-[var(--secondary-color)] font-medium hover:underline">Mariam IA</a> © 2025 - Précision garantie.
</footer>
<script>
document.addEventListener('DOMContentLoaded', function() {
const uploadSection = document.getElementById('upload-section');
const fileInput = document.getElementById('file-input');
const imagePreview = document.getElementById('image-preview');
const solveButton = document.getElementById('solve-button');
const solvingContainer = document.getElementById('solving-container');
const responseContainer = document.getElementById('response-container-element');
const responseOutputDiv = document.getElementById('response-output');
const copyButton = document.getElementById('copy-button');
const statusMessageElement = document.getElementById('status-message-element');
const loadingSpinner = document.getElementById('loading-spinner-element');
const promptTypeSelect = document.getElementById('prompt-type');
const errorDisplay = document.getElementById('error-display-element');
let selectedFile = null;
let currentTaskId = null;
// --- Gestion du Drag and Drop améliorée ---
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadSection.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// Hover effects are now handled by Tailwind's `group hover:` and `hover:` classes directly on #upload-section
// The .upload-highlight class is kept for explicit JS addition if needed elsewhere,
// but for dragenter/dragover, Tailwind's hover states are cleaner.
// If you still want JS to add a class for drag states:
['dragenter', 'dragover'].forEach(eventName => {
uploadSection.addEventListener(eventName, () => {
uploadSection.classList.add('upload-highlight');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
uploadSection.addEventListener(eventName, () => {
uploadSection.classList.remove('upload-highlight');
}, false);
});
uploadSection.addEventListener('drop', (e) => {
if (e.dataTransfer.files.length) {
handleFileSelection(e.dataTransfer.files[0]);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length) {
handleFileSelection(e.target.files[0]);
}
});
function handleFileSelection(file) {
const allowedTypes = ['image/png', 'image/jpeg', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
displayError('Format de fichier non supporté.', 'Veuillez utiliser PNG, JPG ou WEBP.');
selectedFile = null;
solveButton.disabled = true;
imagePreview.classList.add('hidden');
return;
}
selectedFile = file;
solveButton.disabled = false;
errorDisplay.classList.add('hidden');
const reader = new FileReader();
reader.onload = (e) => {
imagePreview.src = e.target.result;
imagePreview.classList.remove('hidden');
};
reader.readAsDataURL(file);
}
function displayError(message, details = null) {
let fullMessage = `<i class="fas fa-shield-halved mr-2"></i> ${message}`;
if (details) {
fullMessage += `<br><small class="block mt-1 text-red-700 font-normal">${escapeHtml(details)}</small>`;
}
errorDisplay.innerHTML = fullMessage;
errorDisplay.classList.remove('hidden');
responseContainer.classList.add('hidden');
loadingSpinner.classList.add('hidden');
updateStatusUI('error_user', null);
}
solveButton.addEventListener('click', () => {
if (!selectedFile) return;
solveButton.disabled = true;
solvingContainer.classList.remove('hidden');
responseContainer.classList.add('hidden');
responseOutputDiv.textContent = '';
errorDisplay.classList.add('hidden');
loadingSpinner.classList.remove('hidden');
updateStatusUI('pending', null, 'Préparation de la résolution...');
const formData = new FormData();
formData.append('image', selectedFile);
formData.append('prompt_type', promptTypeSelect.value);
fetch('/solve', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
return response.json().then(errData => {
throw new Error(errData.error || `Erreur serveur : ${response.status}`);
});
}
return response.json();
})
.then(data => {
if (data.error) {
throw new Error(data.error);
}
currentTaskId = data.task_id;
updateStatusUI(data.status || 'pending', currentTaskId, "Lancement de l'analyse par l'IA...");
const eventSource = new EventSource('/stream/' + currentTaskId);
eventSource.onmessage = function(event) {
const streamData = JSON.parse(event.data);
if (streamData.error) {
displayError(streamData.error, streamData.error_detail);
if (streamData.response) {
responseOutputDiv.textContent = streamData.response;
responseContainer.classList.remove('hidden');
}
eventSource.close();
solveButton.disabled = false;
loadingSpinner.classList.add('hidden');
return;
}
updateStatusUI(streamData.status, currentTaskId);
if (streamData.status === 'completed' || streamData.status === 'completed_tex_only' || streamData.status === 'pdf_error') {
responseContainer.classList.remove('hidden');
loadingSpinner.classList.add('hidden');
if (streamData.response) {
responseOutputDiv.textContent = streamData.response;
}
if (streamData.status === 'pdf_error' && streamData.error_detail) {
const statusEl = statusMessageElement.querySelector('.status-text');
if(statusEl) statusEl.innerHTML += `<br><small class="pdf-error-detail block mt-1 text-orange-500"><i class="fas fa-file-invoice mr-1"></i> Erreur PDF: ${escapeHtml(streamData.error_detail)}</small>`;
}
eventSource.close();
solveButton.disabled = false;
}
};
eventSource.onerror = function() {
eventSource.close();
fetch('/task/' + currentTaskId)
.then(resp => resp.json())
.then(taskData => {
updateStatusUI(taskData.status, currentTaskId);
if (taskData.status === 'completed' || taskData.status === 'completed_tex_only' || taskData.status === 'pdf_error') {
responseContainer.classList.remove('hidden');
if (taskData.response) {
responseOutputDiv.textContent = taskData.response;
}
if (taskData.status === 'pdf_error' && taskData.error_detail) {
const statusEl = statusMessageElement.querySelector('.status-text');
if(statusEl) statusEl.innerHTML += `<br><small class="pdf-error-detail block mt-1 text-orange-500"><i class="fas fa-file-invoice mr-1"></i> Erreur PDF: ${escapeHtml(taskData.error_detail)}</small>`;
}
} else if (taskData.status === 'error') {
displayError(taskData.error || 'Erreur inattendue lors de la récupération de la tâche.', taskData.error_detail);
} else {
displayError('Connexion interrompue.', 'Le traitement se poursuit en arrière-plan. Vérifiez Telegram.');
}
})
.catch(error => {
displayError('Erreur de récupération du statut.', error.message);
})
.finally(() => {
solveButton.disabled = false;
loadingSpinner.classList.add('hidden');
});
};
})
.catch(error => {
displayError(error.message || 'Erreur de communication serveur.');
solveButton.disabled = false;
loadingSpinner.classList.add('hidden');
});
});
function updateStatusUI(status, taskId, overrideMessage = null) {
const selectedPromptText = promptTypeSelect.options[promptTypeSelect.selectedIndex].text.split('(')[0].trim();
let statusMsg = overrideMessage || '';
let iconClass = 'fas fa-hourglass-start';
// We use inline style for icon color to ensure CSS variables are applied
let iconColorStyle = "color: var(--primary-color);";
if (!overrideMessage) {
switch(status) {
case 'pending': statusMsg = "Lancement de l'analyse par l'IA..."; iconClass = 'fas fa-play-circle'; break;
case 'processing': statusMsg = "L'IA déchiffre votre exercice..."; iconClass = 'fas fa-brain'; iconColorStyle = 'color: var(--secondary-color);'; break;
case 'generating_latex': statusMsg = "Construction de la solution LaTeX..."; iconClass = 'fas fa-scroll'; break;
case 'cleaning_latex': statusMsg = "Peaufinage du code LaTeX..."; iconClass = 'fas fa-magic'; break;
case 'generating_pdf': statusMsg = "Compilation du document PDF final..."; iconClass = 'fas fa-file-pdf'; iconColorStyle = 'color: var(--accent-color);'; break;
case 'completed': statusMsg = "Solution Complète et Précise Générée !"; iconClass = 'fas fa-check-double'; iconColorStyle = 'color: var(--success-color);'; break;
case 'completed_tex_only': statusMsg = "Solution LaTeX Précise Générée ! (PDF non requis/dispo)"; iconClass = 'fas fa-check-circle'; iconColorStyle = 'color: var(--success-color);'; break;
case 'pdf_error': statusMsg = "Solution LaTeX Précise Générée ! (Erreur PDF)"; iconClass = 'fas fa-file-excel'; iconColorStyle = 'color: #f39c12;'; break; // Specific color
case 'error': statusMsg = "Une anomalie technique est survenue."; iconClass = 'fas fa-times-circle'; iconColorStyle = 'color: var(--accent-color);'; break;
case 'error_user': statusMsg = "Veuillez vérifier votre image."; iconClass = 'fas fa-exclamation-triangle'; iconColorStyle = 'color: var(--accent-color);'; break;
default: statusMsg = `Progression: ${status}`; iconClass = 'fas fa-spinner fa-spin';
}
}
let taskInfoHtml = '';
if (taskId) {
taskInfoHtml = `<span class="text-sm text-gray-500 ml-2">(Tâche ${taskId.substring(0,6)} | Style: ${selectedPromptText})</span>`;
}
statusMessageElement.innerHTML = `<i class="${iconClass} mr-2" style="${iconColorStyle}"></i> <span class="status-text">${statusMsg}</span> ${taskInfoHtml}`;
}
// Fonction corrigée ici !
function escapeHtml(unsafe) {
if (typeof unsafe !== 'string') return '';
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
copyButton.addEventListener('click', () => {
const textToCopy = responseOutputDiv.textContent;
navigator.clipboard.writeText(textToCopy).then(() => {
const originalIcon = copyButton.querySelector('i').className;
const originalText = copyButton.childNodes[1] ? copyButton.childNodes[1].nodeValue.trim() : 'Copier le code LaTeX';
copyButton.innerHTML = `<i class="fas fa-check mr-2"></i> Code Copié !`;
copyButton.classList.remove('gradient-secondary');
copyButton.style.backgroundColor = 'var(--success-color)';
setTimeout(() => {
copyButton.innerHTML = `<i class="${originalIcon} mr-2"></i> ${originalText}`;
copyButton.classList.add('gradient-secondary');
copyButton.style.backgroundColor = ''; // Revert to gradient
}, 2500);
}).catch(err => {
console.error('Erreur de copie: ', err);
displayError('Copie échouée.', 'Veuillez copier manuellement le texte.');
});
});
});
</script>
</body>
</html>