Ohpdf / templates /index.html
Docfile's picture
Update templates/index.html
f94e218 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 Mathématique IA</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.5/socket.io.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* --- Reset & Variables --- */
:root {
--bg-color: #f4f7fa;
--main-color: #ffffff;
--primary-color: #3b82f6;
--primary-hover: #2563eb;
--text-color: #1f2937;
--text-light: #6b7280;
--border-color: #e5e7eb;
--dark-bg: #111827;
--success-color: #10b981;
--warning-color: #f59e0b;
--error-color: #ef4444;
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--border-radius: 0.75rem;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
line-height: 1.6;
padding: 2rem;
}
/* --- Layout & Main Container --- */
.container {
max-width: 1200px;
margin: 0 auto;
background-color: var(--main-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
overflow: hidden;
}
/* --- Header --- */
.header {
padding: 2.5rem;
text-align: center;
border-bottom: 1px solid var(--border-color);
background: linear-gradient(to top, #ffffff, #f9fafb);
}
.header h1 {
font-size: 2.25rem;
font-weight: 700;
letter-spacing: -0.025em;
color: var(--text-color);
}
.header h1 .icon {
color: var(--primary-color);
}
.header p {
margin-top: 0.5rem;
color: var(--text-light);
font-size: 1.1rem;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
/* --- Upload Section --- */
.main-content {
padding: 2.5rem;
}
.upload-area {
border: 2px dashed var(--border-color);
border-radius: var(--border-radius);
padding: 3rem;
text-align: center;
background-color: #fcfdff;
transition: all 0.3s ease;
}
.upload-area.dragover {
border-color: var(--primary-color);
background-color: #eff6ff;
}
.upload-icon {
font-size: 3rem;
color: var(--primary-color);
margin-bottom: 1rem;
}
.upload-area p {
color: var(--text-light);
margin-bottom: 1.5rem;
}
/* --- Main Button --- */
.btn {
display: inline-block;
background-color: var(--primary-color);
color: white;
padding: 0.8rem 2rem;
border: none;
border-radius: 9999px; /* pill shape */
font-size: 1rem;
font-weight: 500;
cursor: pointer;
text-decoration: none;
transition: background-color 0.2s ease, transform 0.2s ease;
}
.btn:hover {
background-color: var(--primary-hover);
transform: translateY(-2px);
}
/* --- Results & Logs Section --- */
.results-grid {
margin-top: 2.5rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 2rem;
}
.panel {
background-color: #f9fafb;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: 1.5rem;
}
.panel h3 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.75rem;
}
#extractedText {
white-space: pre-wrap;
word-wrap: break-word;
font-family: 'Courier New', monospace;
background-color: var(--main-color);
padding: 1rem;
border-radius: 0.5rem;
max-height: 450px;
overflow-y: auto;
color: var(--text-light);
}
/* --- Logs --- */
#logContainer {
height: 450px;
overflow-y: auto;
background: var(--dark-bg);
color: #d1d5db;
padding: 1rem;
border-radius: 0.5rem;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
}
.log-entry {
margin-bottom: 0.25rem;
padding: 0.2rem 0.5rem;
border-radius: 3px;
}
.log-timestamp { color: #9ca3af; margin-right: 0.5rem; }
.log-info { color: #3b82f6; }
.log-success { color: #10b981; }
.log-warning { color: #f59e0b; }
.log-error { color: #ef4444; font-weight: bold; }
/* --- Status Bar & Download --- */
#status-section {
padding: 1.5rem 2.5rem;
background-color: #f9fafb;
border-top: 1px solid var(--border-color);
text-align: center;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1.25rem;
border-radius: 9999px;
font-weight: 500;
}
.status-processing { background-color: #fef3c7; color: #92400e; }
.status-completed { background-color: #d1fae5; color: #065f46; }
.status-failed { background-color: #fee2e2; color: #991b1b; }
#downloadSection {
margin-top: 1rem;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="container">
<header class="header">
<h1><span class="icon">🧮</span> Résolveur Mathématique IA</h1>
<p>Soumettez une image d'un problème mathématique et laissez nos agents IA générer une solution rigoureuse, étape par étape.</p>
</header>
<main class="main-content">
<!-- Section d'upload corrigée -->
<section id="upload-section">
<div class="upload-area" id="uploadArea">
<div class="upload-icon">📄</div>
<p>Glissez-déposez votre image ici, ou cliquez sur le bouton pour la sélectionner.</p>
<!-- Ce bouton est maintenant le point d'interaction principal pour le clic -->
<button type="button" class="btn" id="selectFileBtn">Choisir un fichier</button>
<!-- L'input de fichier est caché et sera activé par le JS -->
<input type="file" id="fileInput" accept="image/*" class="hidden">
</div>
</section>
<!-- Section pour afficher le statut global, cachée au début -->
<section id="status-section" class="hidden">
<div id="statusBadge" class="status-badge"></div>
<div id="downloadSection" class="hidden">
<a href="#" id="downloadBtn" class="btn">📥 Télécharger la solution</a>
</div>
</section>
<!-- Grille pour les résultats, cachée au début -->
<section id="results-grid" class="results-grid hidden">
<div class="panel">
<h3>📝 Texte extrait de l'image</h3>
<div id="extractedText">Le texte de votre image apparaîtra ici...</div>
</div>
<div class="panel">
<h3>📊 Journal de traitement</h3>
<div id="logContainer"></div>
</div>
</section>
</main>
</div>
<script>
const socket = io();
let currentTaskId = null;
// --- DOM Elements ---
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const selectFileBtn = document.getElementById('selectFileBtn');
const statusSection = document.getElementById('status-section');
const statusBadge = document.getElementById('statusBadge');
const resultsGrid = document.getElementById('results-grid');
const extractedTextEl = document.getElementById('extractedText');
const logContainer = document.getElementById('logContainer');
const downloadSection = document.getElementById('downloadSection');
const downloadBtn = document.getElementById('downloadBtn');
// --- Event Listeners ---
// CORRECTION : Le bouton déclenche l'input de fichier
selectFileBtn.addEventListener('click', () => {
fileInput.click();
});
// L'input de fichier, une fois un fichier choisi, lance l'upload
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFileUpload(e.target.files[0]);
}
});
// Gestion du glisser-déposer (Drag & Drop)
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
uploadArea.addEventListener(eventName, () => uploadArea.classList.add('dragover'), false);
});
['dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, () => uploadArea.classList.remove('dragover'), false);
});
uploadArea.addEventListener('drop', (e) => {
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFileUpload(files[0]);
}
});
// --- Core Functions ---
function resetUI() {
statusSection.classList.add('hidden');
resultsGrid.classList.add('hidden');
downloadSection.classList.add('hidden');
logContainer.innerHTML = '';
extractedTextEl.textContent = 'Le texte de votre image apparaîtra ici...';
currentTaskId = null;
}
function handleFileUpload(file) {
resetUI();
const formData = new FormData();
formData.append('file', file);
updateStatus('processing', '🚀 Envoi et analyse de l\'image...');
statusSection.classList.remove('hidden');
fetch('/upload', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error(`Erreur serveur: ${response.statusText}`);
}
return response.json();
})
.then(data => {
if (data.error) {
throw new Error(data.error);
}
currentTaskId = data.task_id;
extractedTextEl.textContent = data.extracted_text;
resultsGrid.classList.remove('hidden');
updateStatus('processing', '🧠 Résolution en cours...');
})
.catch(error => {
updateStatus('failed', `❌ Erreur critique : ${error.message}`);
console.error('Upload Error:', error);
});
}
function updateStatus(status, message) {
statusBadge.className = `status-badge status-${status}`;
statusBadge.textContent = message;
}
function addLog(timestamp, level, message) {
const logEntry = document.createElement('div');
logEntry.className = `log-entry log-${level}`;
logEntry.innerHTML = `<span class="log-timestamp">[${timestamp}]</span> ${message}`;
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
// --- WebSocket Event Handlers ---
socket.on('log_update', (data) => {
if (data.task_id === currentTaskId) {
addLog(data.log.timestamp, data.log.level, data.log.message);
}
});
socket.on('task_completed', (data) => {
if (data.task_id === currentTaskId) {
updateStatus('completed', '✅ Solution générée avec succès !');
downloadSection.classList.remove('hidden');
downloadBtn.href = `/download/${currentTaskId}`;
}
});
socket.on('task_failed', (data) => {
if (data.task_id === currentTaskId) {
updateStatus('failed', '⚠️ Échec de la résolution (solution partielle disponible)');
downloadSection.classList.remove('hidden');
downloadBtn.href = `/download/${currentTaskId}`;
}
});
socket.on('task_error', (data) => {
if (data.task_id === currentTaskId) {
updateStatus('failed', `❌ Erreur système: ${data.error}`);
}
});
</script>
</body>
</html>