|
<!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> |
|
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
|
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Fira+Code&display=swap" rel="stylesheet"> |
|
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> |
|
<style> |
|
|
|
:root { |
|
--primary-color: #2c3e50; |
|
--secondary-color: #1abc9c; |
|
--accent-color: #e74c3c; |
|
--success-color: #27ae60; |
|
--light-secondary-bg: #e8f8f5; |
|
} |
|
|
|
body { |
|
font-family: 'Montserrat', sans-serif; |
|
} |
|
|
|
.font-code { |
|
font-family: 'Fira Code', monospace; |
|
} |
|
|
|
|
|
.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; |
|
} |
|
|
|
|
|
.upload-highlight { |
|
border-color: var(--secondary-color) !important; |
|
background-color: var(--light-secondary-bg) !important; |
|
} |
|
</style> |
|
<script> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</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> |