Testpdf / templates /index.html
Docfile's picture
Update templates/index.html
1c9f000 verified
raw
history blame
30.8 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>
<!-- 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>
:root {
--primary-color: #2c3e50; /* Bleu Nuit - Élégant et Professionnel */
--secondary-color: #1abc9c; /* Turquoise - Dynamique */
--accent-color: #e74c3c; /* Rouge Doux - Pour les erreurs subtiles */
--success-color: #27ae60; /* Vert Succès */
--light-bg: #f4f6f8; /* Fond Général Très Clair */
--card-bg: #ffffff; /* Fond des Cartes */
--text-color: #34495e; /* Texte Principal */
--subtle-text-color: #7f8c8d; /* Texte Secondaire/Discret */
--border-color: #dfe4ea; /* Bordures Claires */
--shadow: 0 8px 25px rgba(44, 62, 80, 0.1); /* Ombre plus douce */
--border-radius: 12px; /* Rayon de bordure plus arrondi */
--font-main: 'Montserrat', sans-serif;
--font-code: 'Fira Code', monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-main);
background-color: var(--light-bg);
color: var(--text-color);
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 20px;
line-height: 1.7;
}
.main-container {
background-color: var(--card-bg);
padding: 35px 45px;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
width: 100%;
max-width: 750px;
text-align: center;
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
h1 {
color: var(--primary-color);
font-weight: 700;
font-size: 2.2em;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: center;
}
h1 .logo-icon {
margin-right: 12px;
font-size: 1.3em;
color: var(--secondary-color);
}
.subtitle {
font-size: 1.1em;
color: var(--subtle-text-color);
margin-bottom: 35px;
font-weight: 400;
}
.upload-section {
border: 2.5px dashed var(--border-color);
border-radius: var(--border-radius);
padding: 35px;
cursor: pointer;
transition: all 0.3s ease;
background-color: #fdfdfe;
margin-bottom: 30px;
position: relative; /* Pour le positionnement absolu de l'input */
overflow: hidden; /* Pour cacher l'input */
}
.upload-section.highlight-drag { /* Style quand on drag par-dessus */
border-color: var(--secondary-color);
background-color: #e8f8f5;
}
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.upload-section .upload-icon {
font-size: 3.5em;
color: var(--secondary-color);
margin-bottom: 15px;
transition: transform 0.3s ease;
}
.upload-section:hover .upload-icon {
transform: scale(1.1) translateY(-5px);
}
.upload-section p {
margin: 0 0 10px 0;
font-size: 1.15em;
font-weight: 500;
color: var(--text-color);
}
.upload-section small {
font-size: 0.9em;
color: var(--subtle-text-color);
}
#file-input { /* Caché mais accessible pour la sémantique et l'accessibilité */
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
#image-preview-container {
margin-top: 20px;
text-align: center;
}
#image-preview {
max-width: 100%;
max-height: 280px;
border-radius: var(--border-radius);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
display: none; /* Caché initialement */
border: 1px solid var(--border-color);
}
.options-panel {
background-color: #f8f9fa;
padding: 20px 25px;
border-radius: var(--border-radius);
margin-bottom: 30px;
text-align: left;
border: 1px solid var(--border-color);
}
.options-panel h3 {
font-weight: 600;
color: var(--primary-color);
margin-bottom: 15px;
font-size: 1.2em;
display: flex;
align-items: center;
}
.options-panel h3 i {
margin-right: 10px;
color: var(--secondary-color);
}
.prompt-selector label {
font-weight: 500;
margin-bottom: 8px;
display: block;
color: var(--text-color);
font-size: 1em;
}
.prompt-selector select {
width: 100%;
padding: 14px 18px;
border-radius: var(--border-radius);
border: 1.5px solid var(--border-color);
font-size: 1em;
font-family: var(--font-main);
background-color: var(--card-bg);
transition: border-color 0.3s ease, box-shadow 0.3s ease;
appearance: none; /* Pour styliser la flèche */
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%232c3e50' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 18px center;
background-size: 16px;
}
.prompt-selector select:focus {
border-color: var(--secondary-color);
outline: none;
box-shadow: 0 0 0 3px rgba(26, 188, 156, 0.2);
}
.button {
background-image: linear-gradient(to right, var(--secondary-color) 0%, #16a085 100%);
color: white;
border: none;
padding: 15px 30px;
font-size: 1.15em;
font-weight: 600;
letter-spacing: 0.5px;
border-radius: var(--border-radius);
cursor: pointer;
transition: all 0.35s cubic-bezier(0.25, 0.8, 0.25, 1);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
box-shadow: 0 4px 15px rgba(26, 188, 156, 0.2);
text-transform: uppercase;
}
.button:hover {
transform: translateY(-3px) scale(1.02);
box-shadow: 0 7px 20px rgba(26, 188, 156, 0.3);
}
.button:active {
transform: translateY(-1px) scale(0.98);
box-shadow: 0 2px 10px rgba(26, 188, 156, 0.2);
}
.button:disabled {
background-image: none;
background-color: #bdc3c7;
cursor: not-allowed;
transform: none;
box-shadow: none;
color: #7f8c8d;
}
.button.copy-button { /* Style un peu différent pour le bouton copier */
background-image: linear-gradient(to right, var(--primary-color) 0%, #34495e 100%);
box-shadow: 0 4px 15px rgba(44, 62, 80, 0.2);
font-size: 1em;
padding: 12px 25px;
text-transform: none;
}
.button.copy-button:hover {
box-shadow: 0 7px 20px rgba(44, 62, 80, 0.3);
}
#solving-container {
display: none; /* Caché initialement */
margin-top: 35px;
padding: 30px;
background-color: #fbfcfd;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
}
.status-section {
margin-bottom: 20px;
text-align: center;
}
.status-message {
font-size: 1.2em;
font-weight: 500;
color: var(--primary-color);
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.status-message i {
margin-right: 10px;
font-size: 1.3em;
transition: color 0.3s ease;
}
.status-message .task-info {
font-size: 0.85em;
color: var(--subtle-text-color);
margin-left: 8px;
font-weight: 400;
}
.loading-spinner {
width: 40px;
height: 40px;
margin: 25px auto;
border: 5px solid rgba(44, 62, 80, 0.15);
border-left-color: var(--secondary-color);
border-radius: 50%;
animation: spin 0.8s linear infinite;
display: none; /* Caché initialement */
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.telegram-notice {
background-color: #eaf2f8; /* Bleu très clair */
border-left: 5px solid var(--secondary-color);
padding: 15px 20px;
margin: 25px 0;
font-size: 1em;
border-radius: var(--border-radius);
color: var(--text-color);
display: flex;
align-items: center;
}
.telegram-notice i {
margin-right: 12px;
color: var(--secondary-color);
font-size: 1.4em;
}
.response-container {
margin-top: 25px;
padding: 25px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background-color: var(--card-bg);
display: none; /* Caché initialement */
text-align: left;
}
.response-container h3 {
font-weight: 600;
color: var(--primary-color);
margin-bottom: 15px;
font-size: 1.25em;
display: flex;
align-items: center;
}
.response-container h3 i {
margin-right: 10px;
color: var(--secondary-color);
}
#response-output { /* Renommé pour plus de clarté */
font-family: var(--font-code);
background-color: #2d2d2d; /* Fond sombre pour code */
color: #d4d4d4; /* Texte clair pour code */
padding: 20px;
border-radius: var(--border-radius);
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 450px;
margin-bottom: 20px;
border: 1px solid #444;
line-height: 1.6;
}
/* Style pour les commentaires dans le code LaTeX (si possible à faire via JS) */
/* .latex-comment { color: #6a9955; font-style: italic; } */
.error-display {
color: var(--accent-color);
background-color: #fbecec;
border: 1.5px solid var(--accent-color);
padding: 18px;
border-radius: var(--border-radius);
margin: 20px 0;
font-weight: 500;
display: flex;
align-items: center;
}
.error-display i {
margin-right: 12px;
font-size: 1.3em;
}
.error-display small {
display: block;
font-weight: 400;
color: #c0392b; /* Rouge plus foncé pour détails */
font-size: 0.9em;
margin-top: 5px;
}
.footer {
margin-top: 50px;
padding-bottom: 20px;
font-size: 0.95em;
color: var(--subtle-text-color);
}
.footer a {
color: var(--secondary-color);
text-decoration: none;
font-weight: 500;
}
.footer a:hover {
text-decoration: underline;
}
/* Responsive adjustments */
@media (max-width: 768px) {
body { padding: 15px; }
.main-container { padding: 25px 30px; max-width: 95%; }
h1 { font-size: 1.9em; }
.subtitle { font-size: 1em; margin-bottom: 25px; }
.upload-section { padding: 25px; }
.upload-section p { font-size: 1.05em; }
.upload-section .upload-icon { font-size: 3em; }
.options-panel { padding: 15px 20px; }
.button { padding: 14px 25px; font-size: 1.05em; }
}
@media (max-width: 480px) {
h1 { font-size: 1.7em; }
.subtitle { font-size: 0.95em; }
.upload-section { padding: 20px; }
.main-container { padding: 20px; }
}
</style>
</head>
<body>
<div class="main-container">
<h1><i class="fas fa-atom logo-icon"></i>Solveur Expert IA</h1>
<p class="subtitle">Solutions LaTeX précises pour Maths, Physique et Chimie en Terminale</p>
<div id="upload-section" class="upload-section">
<div class="upload-content">
<i class="fas fa-file-arrow-up upload-icon"></i>
<p>Déposez l'image de votre exercice ici</p>
<small>ou cliquez pour sélectionner un fichier (PNG, JPG)</small>
</div>
<input type="file" id="file-input" accept="image/png, image/jpeg, image/webp">
<div id="image-preview-container">
<img id="image-preview" src="#" alt="Aperçu de l'énoncé">
</div>
</div>
<div class="options-panel">
<h3><i class="fas fa-cogs"></i>Options de Formatage</h3>
<div class="prompt-selector">
<label for="prompt-type">Style de la correction LaTeX :</label>
<select id="prompt-type" name="prompt-type">
<option value="refined">Format Raffiné & Complet (mise en page avancée)</option>
<option value="light">Format Léger & Essentiel (LaTeX standard)</option>
</select>
</div>
</div>
<button id="solve-button" class="button" disabled>
<i class="fas fa-rocket"></i>Obtenir la Solution
</button>
<div id="solving-container">
<div class="status-section">
<div class="status-message" id="status-message-element">
<i class="fas fa-hourglass-start"></i>Prêt à résoudre votre exercice...
</div>
</div>
<div class="loading-spinner" id="loading-spinner-element"></div>
<div class="telegram-notice">
<i class="fab fa-telegram"></i>Une copie de la solution sera envoyée sur Telegram pour archivage.
</div>
<div class="response-container" id="response-container-element">
<h3><i class="fas fa-file-code"></i>Correction LaTeX Détaillée :</h3>
<div id="response-output"></div>
<button id="copy-button" class="button copy-button">
<i class="fas fa-copy"></i>Copier le code LaTeX
</button>
</div>
<div id="error-display-element" class="error-display" style="display: none;">
<!-- Les erreurs seront affichées ici -->
</div>
</div>
</div>
<footer class="footer">
Solutions générées par <a href="#" target="_blank">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 imagePreviewContainer = document.getElementById('image-preview-container');
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; // Pour suivre l'ID de la tâche en cours
// --- Drag and Drop Logic ---
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadSection.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
uploadSection.addEventListener(eventName, () => uploadSection.classList.add('highlight-drag'), false);
});
['dragleave', 'drop'].forEach(eventName => {
uploadSection.addEventListener(eventName, () => uploadSection.classList.remove('highlight-drag'), false);
});
uploadSection.addEventListener('drop', (e) => {
if (e.dataTransfer.files.length) {
handleFileSelection(e.dataTransfer.files[0]);
}
});
// --- Fin Drag and Drop ---
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.style.display = 'none';
imagePreviewContainer.style.display = 'none';
return;
}
selectedFile = file;
solveButton.disabled = false;
errorDisplay.style.display = 'none';
const reader = new FileReader();
reader.onload = (e) => {
imagePreview.src = e.target.result;
imagePreview.style.display = 'block';
imagePreviewContainer.style.display = 'block';
};
reader.readAsDataURL(file);
}
function displayError(message, details = null) {
let fullMessage = `<i class="fas fa-shield-halved"></i> ${message}`;
if (details) {
fullMessage += `<br><small>${escapeHtml(details)}</small>`;
}
errorDisplay.innerHTML = fullMessage;
errorDisplay.style.display = 'block';
responseContainer.style.display = 'none';
loadingSpinner.style.display = 'none';
updateStatusUI('error_user', null); // Statut spécifique pour erreurs d'input utilisateur
}
solveButton.addEventListener('click', () => {
if (!selectedFile) return;
solveButton.disabled = true;
solvingContainer.style.display = 'block';
responseContainer.style.display = 'none';
responseOutputDiv.textContent = '';
errorDisplay.style.display = 'none';
loadingSpinner.style.display = 'block';
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) { // Afficher LaTeX partiel si dispo
responseOutputDiv.textContent = streamData.response;
responseContainer.style.display = 'block';
}
eventSource.close();
solveButton.disabled = false;
loadingSpinner.style.display = 'none';
return;
}
updateStatusUI(streamData.status, currentTaskId);
if (streamData.status === 'completed' || streamData.status === 'completed_tex_only' || streamData.status === 'pdf_error') {
responseContainer.style.display = 'block';
loadingSpinner.style.display = 'none';
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"><i class="fas fa-file-invoice"></i> Erreur PDF: ${escapeHtml(streamData.error_detail)}</small>`;
}
eventSource.close();
solveButton.disabled = false;
}
};
eventSource.onerror = function() {
eventSource.close();
fetch('/task/' + currentTaskId) // Utiliser 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.style.display = 'block';
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"><i class="fas fa-file-invoice"></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.style.display = 'none';
});
};
})
.catch(error => {
displayError(error.message || 'Erreur de communication serveur.');
solveButton.disabled = false;
loadingSpinner.style.display = 'none';
});
});
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';
let iconColor = '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'; iconColor = '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-wand-sparkles'; break;
case 'generating_pdf': statusMsg = "Compilation du document PDF final..."; iconClass = 'fas fa-file-pdf'; iconColor = '#e74c3c'; break; // Rouge pour PDF
case 'completed': statusMsg = "Solution Complète et Précise Générée !"; iconClass = 'fas fa-check-double'; iconColor = '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'; iconColor = 'var(--success-color)'; break;
case 'pdf_error': statusMsg = "Solution LaTeX Précise Générée ! (Erreur PDF)"; iconClass = 'fas fa-file-excel'; iconColor = '#f39c12'; break; // Orange pour erreur PDF
case 'error': statusMsg = "Une anomalie technique est survenue."; iconClass = 'fas fa-times-circle'; iconColor = 'var(--accent-color)'; break;
case 'error_user': statusMsg = "Veuillez vérifier votre image."; iconClass = 'fas fa-exclamation-triangle'; iconColor = 'var(--accent-color)'; break;
default: statusMsg = `Progression: ${status}`; iconClass = 'fas fa-spinner fa-spin'; // spinner pour statut inconnu
}
}
let taskInfoHtml = '';
if (taskId) {
taskInfoHtml = `<span class="task-info">(Tâche ${taskId.substring(0,6)} | Style: ${selectedPromptText})</span>`;
}
statusMessageElement.innerHTML = `<i class="${iconClass}" style="color:${iconColor};"></i> <span class="status-text">${statusMsg}</span> ${taskInfoHtml}`;
}
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 : ' Copier le code LaTeX'; // Safer access
copyButton.innerHTML = `<i class="fas fa-check"></i> Code Copié !`;
copyButton.style.backgroundColor = 'var(--success-color)'; // Feedback visuel
setTimeout(() => {
copyButton.innerHTML = `<i class="${originalIcon}"></i>${originalText}`;
copyButton.style.backgroundColor = ''; // Reset style
}, 2500);
}).catch(err => {
console.error('Erreur de copie: ', err);
displayError('Copie échouée.', 'Veuillez copier manuellement le texte.');
});
});
});
</script>
</body>
</html>