|
<!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 IMO - Version 2</title> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.4/socket.io.js"></script> |
|
<style> |
|
:root { |
|
--primary: #6366f1; |
|
--primary-dark: #4f46e5; |
|
--success: #10b981; |
|
--warning: #f59e0b; |
|
--error: #ef4444; |
|
--bg-light: #f8fafc; |
|
--text-dark: #1e293b; |
|
--border: #e2e8f0; |
|
} |
|
|
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
} |
|
|
|
body { |
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
background: var(--bg-light); |
|
color: var(--text-dark); |
|
line-height: 1.6; |
|
} |
|
|
|
.app-container { |
|
min-height: 100vh; |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
|
|
.navbar { |
|
background: white; |
|
border-bottom: 1px solid var(--border); |
|
padding: 1rem 0; |
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1); |
|
} |
|
|
|
.nav-content { |
|
max-width: 1200px; |
|
margin: 0 auto; |
|
padding: 0 2rem; |
|
display: flex; |
|
align-items: center; |
|
gap: 1rem; |
|
} |
|
|
|
.logo { |
|
font-size: 1.5rem; |
|
font-weight: bold; |
|
color: var(--primary); |
|
} |
|
|
|
.main-content { |
|
flex: 1; |
|
max-width: 1200px; |
|
margin: 0 auto; |
|
padding: 2rem; |
|
width: 100%; |
|
} |
|
|
|
.card { |
|
background: white; |
|
border-radius: 12px; |
|
padding: 2rem; |
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1); |
|
border: 1px solid var(--border); |
|
margin-bottom: 2rem; |
|
} |
|
|
|
.upload-area { |
|
text-align: center; |
|
padding: 3rem 2rem; |
|
} |
|
|
|
.file-input-wrapper { |
|
position: relative; |
|
display: inline-block; |
|
margin: 2rem 0; |
|
} |
|
|
|
.file-input { |
|
position: absolute; |
|
opacity: 0; |
|
width: 100%; |
|
height: 100%; |
|
cursor: pointer; |
|
} |
|
|
|
.upload-btn { |
|
background: var(--primary); |
|
color: white; |
|
padding: 1rem 2rem; |
|
border: none; |
|
border-radius: 8px; |
|
font-size: 1rem; |
|
font-weight: 500; |
|
cursor: pointer; |
|
transition: all 0.2s; |
|
display: inline-flex; |
|
align-items: center; |
|
gap: 0.5rem; |
|
} |
|
|
|
.upload-btn:hover { |
|
background: var(--primary-dark); |
|
transform: translateY(-1px); |
|
} |
|
|
|
.drop-zone { |
|
border: 2px dashed var(--border); |
|
border-radius: 12px; |
|
padding: 3rem; |
|
margin: 2rem 0; |
|
transition: all 0.3s; |
|
cursor: pointer; |
|
} |
|
|
|
.drop-zone:hover { |
|
border-color: var(--primary); |
|
background: rgba(99, 102, 241, 0.05); |
|
} |
|
|
|
.drop-zone.active { |
|
border-color: var(--primary); |
|
background: rgba(99, 102, 241, 0.1); |
|
} |
|
|
|
.status-indicator { |
|
display: inline-flex; |
|
align-items: center; |
|
gap: 0.5rem; |
|
padding: 0.5rem 1rem; |
|
border-radius: 6px; |
|
font-weight: 500; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
.status-processing { |
|
background: rgba(245, 158, 11, 0.1); |
|
color: var(--warning); |
|
} |
|
|
|
.status-success { |
|
background: rgba(16, 185, 129, 0.1); |
|
color: var(--success); |
|
} |
|
|
|
.status-error { |
|
background: rgba(239, 68, 68, 0.1); |
|
color: var(--error); |
|
} |
|
|
|
.content-grid { |
|
display: grid; |
|
grid-template-columns: 1fr 1fr; |
|
gap: 2rem; |
|
margin-top: 2rem; |
|
} |
|
|
|
.text-preview { |
|
background: #f1f5f9; |
|
border-radius: 8px; |
|
padding: 1.5rem; |
|
max-height: 400px; |
|
overflow-y: auto; |
|
white-space: pre-wrap; |
|
font-family: 'Courier New', monospace; |
|
font-size: 0.9rem; |
|
} |
|
|
|
.logs { |
|
background: #0f172a; |
|
color: #e2e8f0; |
|
border-radius: 8px; |
|
padding: 1rem; |
|
max-height: 400px; |
|
overflow-y: auto; |
|
font-family: 'Courier New', monospace; |
|
font-size: 0.85rem; |
|
} |
|
|
|
.log-entry { |
|
padding: 0.25rem 0; |
|
border-bottom: 1px solid rgba(255,255,255,0.1); |
|
} |
|
|
|
.log-timestamp { |
|
color: #64748b; |
|
} |
|
|
|
.log-info { color: #3b82f6; } |
|
.log-success { color: var(--success); } |
|
.log-warning { color: var(--warning); } |
|
.log-error { color: var(--error); } |
|
|
|
.download-area { |
|
text-align: center; |
|
padding: 2rem; |
|
background: rgba(16, 185, 129, 0.05); |
|
border-radius: 12px; |
|
border: 1px solid rgba(16, 185, 129, 0.2); |
|
} |
|
|
|
.download-btn { |
|
background: var(--success); |
|
color: white; |
|
padding: 1rem 2rem; |
|
border: none; |
|
border-radius: 8px; |
|
font-size: 1rem; |
|
cursor: pointer; |
|
text-decoration: none; |
|
display: inline-flex; |
|
align-items: center; |
|
gap: 0.5rem; |
|
transition: all 0.2s; |
|
} |
|
|
|
.download-btn:hover { |
|
background: #059669; |
|
} |
|
|
|
.hidden { |
|
display: none; |
|
} |
|
|
|
.spinner { |
|
width: 20px; |
|
height: 20px; |
|
border: 2px solid transparent; |
|
border-top: 2px solid currentColor; |
|
border-radius: 50%; |
|
animation: spin 1s linear infinite; |
|
} |
|
|
|
@keyframes spin { |
|
to { transform: rotate(360deg); } |
|
} |
|
|
|
@media (max-width: 768px) { |
|
.content-grid { |
|
grid-template-columns: 1fr; |
|
} |
|
.main-content { |
|
padding: 1rem; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="app-container"> |
|
<nav class="navbar"> |
|
<div class="nav-content"> |
|
<div class="logo">🧮 Math Solver IMO</div> |
|
<div style="margin-left: auto;"> |
|
<span style="font-size: 0.9rem; color: #64748b;">Résolveur de problèmes mathématiques</span> |
|
</div> |
|
</div> |
|
</nav> |
|
|
|
<main class="main-content"> |
|
|
|
<div class="card"> |
|
<div class="upload-area"> |
|
<h2 style="margin-bottom: 1rem;">📸 Uploader votre problème mathématique</h2> |
|
<p style="color: #64748b; margin-bottom: 2rem;"> |
|
Prenez une photo ou uploadez une image de votre problème mathématique |
|
</p> |
|
|
|
|
|
<div class="drop-zone" id="dropZone"> |
|
<div> |
|
<div style="font-size: 3rem; margin-bottom: 1rem;">📁</div> |
|
<h3>Glissez votre image ici</h3> |
|
<p style="color: #64748b; margin: 1rem 0;">ou</p> |
|
|
|
<div class="file-input-wrapper"> |
|
<input type="file" class="file-input" id="fileInput" accept="image/*"> |
|
<button class="upload-btn"> |
|
<span>📷</span> |
|
Choisir une image |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<p style="font-size: 0.85rem; color: #64748b;"> |
|
Formats supportés: PNG, JPG, JPEG, GIF, BMP, TIFF |
|
</p> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="card" id="statusCard" style="display: none;"> |
|
<div id="statusIndicator"></div> |
|
<div id="statusMessage"></div> |
|
</div> |
|
|
|
|
|
<div class="content-grid" id="contentGrid" style="display: none;"> |
|
<div class="card"> |
|
<h3 style="margin-bottom: 1rem;">📝 Texte extrait</h3> |
|
<div class="text-preview" id="extractedText"></div> |
|
</div> |
|
|
|
<div class="card"> |
|
<h3 style="margin-bottom: 1rem;">📊 Logs en temps réel</h3> |
|
<div class="logs" id="logsContainer"></div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="card hidden" id="downloadCard"> |
|
<div class="download-area"> |
|
<h3 style="margin-bottom: 1rem;">✅ Solution prête !</h3> |
|
<p style="margin-bottom: 2rem; color: #64748b;"> |
|
Votre problème mathématique a été résolu avec succès |
|
</p> |
|
<a class="download-btn" id="downloadLink"> |
|
<span>📥</span> |
|
Télécharger la solution |
|
</a> |
|
</div> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<script> |
|
class MathSolverApp { |
|
constructor() { |
|
this.socket = io(); |
|
this.currentTaskId = null; |
|
this.initializeElements(); |
|
this.setupEventListeners(); |
|
this.setupSocketEvents(); |
|
} |
|
|
|
initializeElements() { |
|
this.dropZone = document.getElementById('dropZone'); |
|
this.fileInput = document.getElementById('fileInput'); |
|
this.statusCard = document.getElementById('statusCard'); |
|
this.statusIndicator = document.getElementById('statusIndicator'); |
|
this.statusMessage = document.getElementById('statusMessage'); |
|
this.contentGrid = document.getElementById('contentGrid'); |
|
this.extractedText = document.getElementById('extractedText'); |
|
this.logsContainer = document.getElementById('logsContainer'); |
|
this.downloadCard = document.getElementById('downloadCard'); |
|
this.downloadLink = document.getElementById('downloadLink'); |
|
} |
|
|
|
setupEventListeners() { |
|
|
|
this.fileInput.addEventListener('change', (e) => { |
|
if (e.target.files.length > 0) { |
|
this.handleFile(e.target.files[0]); |
|
} |
|
}); |
|
|
|
|
|
this.dropZone.addEventListener('dragover', (e) => { |
|
e.preventDefault(); |
|
this.dropZone.classList.add('active'); |
|
}); |
|
|
|
this.dropZone.addEventListener('dragleave', (e) => { |
|
e.preventDefault(); |
|
this.dropZone.classList.remove('active'); |
|
}); |
|
|
|
this.dropZone.addEventListener('drop', (e) => { |
|
e.preventDefault(); |
|
this.dropZone.classList.remove('active'); |
|
|
|
const files = e.dataTransfer.files; |
|
if (files.length > 0) { |
|
this.handleFile(files[0]); |
|
} |
|
}); |
|
|
|
|
|
this.dropZone.addEventListener('click', () => { |
|
this.fileInput.click(); |
|
}); |
|
} |
|
|
|
setupSocketEvents() { |
|
this.socket.on('log_update', (data) => { |
|
if (data.task_id === this.currentTaskId) { |
|
this.addLog(data.log); |
|
} |
|
}); |
|
|
|
this.socket.on('task_completed', (data) => { |
|
if (data.task_id === this.currentTaskId) { |
|
this.showSuccess('Solution générée avec succès !'); |
|
this.showDownload(); |
|
} |
|
}); |
|
|
|
this.socket.on('task_failed', (data) => { |
|
if (data.task_id === this.currentTaskId) { |
|
this.showError('Échec de la résolution (solution partielle disponible)'); |
|
this.showDownload(); |
|
} |
|
}); |
|
|
|
this.socket.on('task_error', (data) => { |
|
if (data.task_id === this.currentTaskId) { |
|
this.showError(`Erreur: ${data.error}`); |
|
} |
|
}); |
|
} |
|
|
|
async handleFile(file) { |
|
if (!this.isValidFile(file)) { |
|
this.showError('Type de fichier non supporté'); |
|
return; |
|
} |
|
|
|
const formData = new FormData(); |
|
formData.append('file', file); |
|
|
|
this.showProcessing('Upload en cours...'); |
|
|
|
try { |
|
const response = await fetch('/upload', { |
|
method: 'POST', |
|
body: formData |
|
}); |
|
|
|
const data = await response.json(); |
|
|
|
if (data.error) { |
|
this.showError(data.error); |
|
} else { |
|
this.currentTaskId = data.task_id; |
|
this.extractedText.textContent = data.extracted_text; |
|
this.contentGrid.style.display = 'grid'; |
|
this.showProcessing('Résolution en cours...'); |
|
} |
|
} catch (error) { |
|
this.showError('Erreur lors de l\'upload'); |
|
console.error('Upload error:', error); |
|
} |
|
} |
|
|
|
isValidFile(file) { |
|
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/bmp', 'image/tiff']; |
|
return allowedTypes.includes(file.type); |
|
} |
|
|
|
showProcessing(message) { |
|
this.statusCard.style.display = 'block'; |
|
this.statusIndicator.innerHTML = ` |
|
<div class="status-indicator status-processing"> |
|
<div class="spinner"></div> |
|
En cours |
|
</div> |
|
`; |
|
this.statusMessage.textContent = message; |
|
} |
|
|
|
showSuccess(message) { |
|
this.statusIndicator.innerHTML = ` |
|
<div class="status-indicator status-success"> |
|
✅ Terminé |
|
</div> |
|
`; |
|
this.statusMessage.textContent = message; |
|
} |
|
|
|
showError(message) { |
|
this.statusIndicator.innerHTML = ` |
|
<div class="status-indicator status-error"> |
|
❌ Erreur |
|
</div> |
|
`; |
|
this.statusMessage.textContent = message; |
|
} |
|
|
|
addLog(log) { |
|
const logEntry = document.createElement('div'); |
|
logEntry.className = 'log-entry'; |
|
logEntry.innerHTML = ` |
|
<span class="log-timestamp">[${log.timestamp}]</span> |
|
<span class="log-${log.level}">${log.message}</span> |
|
`; |
|
|
|
this.logsContainer.appendChild(logEntry); |
|
this.logsContainer.scrollTop = this.logsContainer.scrollHeight; |
|
} |
|
|
|
showDownload() { |
|
this.downloadCard.classList.remove('hidden'); |
|
this.downloadLink.href = `/download/${this.currentTaskId}`; |
|
} |
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
new MathSolverApp(); |
|
}); |
|
</script> |
|
</body> |
|
</html> |