|
<!DOCTYPE html> |
|
<html lang="fr"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Math Solver IA</title> |
|
|
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&family=Roboto+Mono&display=swap" rel="stylesheet"> |
|
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
|
|
|
|
|
|
|
|
<style> |
|
:root { |
|
--primary-color: #3498db; |
|
--secondary-color: #2ecc71; |
|
--accent-color: #e74c3c; |
|
--light-bg: #ecf0f1; |
|
--dark-text: #2c3e50; |
|
--light-text: #7f8c8d; |
|
--border-color: #bdc3c7; |
|
--card-bg: #ffffff; |
|
--shadow: 0 4px 15px rgba(0, 0, 0, 0.1); |
|
--border-radius: 8px; |
|
} |
|
|
|
body { |
|
font-family: 'Poppins', sans-serif; |
|
background-color: var(--light-bg); |
|
color: var(--dark-text); |
|
margin: 0; |
|
padding: 20px; |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
min-height: 100vh; |
|
box-sizing: border-box; |
|
} |
|
|
|
.main-container { |
|
background-color: var(--card-bg); |
|
padding: 30px 40px; |
|
border-radius: var(--border-radius); |
|
box-shadow: var(--shadow); |
|
width: 100%; |
|
max-width: 700px; |
|
text-align: center; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
h1 { |
|
color: var(--primary-color); |
|
font-weight: 600; |
|
margin-bottom: 30px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
h1 i { |
|
margin-right: 10px; |
|
font-size: 1.2em; |
|
} |
|
|
|
.upload-section { |
|
border: 2px dashed var(--border-color); |
|
border-radius: var(--border-radius); |
|
padding: 30px; |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
background-color: #f9fafb; |
|
margin-bottom: 20px; |
|
} |
|
.upload-section:hover { |
|
border-color: var(--primary-color); |
|
background-color: #f0f8ff; |
|
} |
|
.upload-section p { |
|
margin: 0 0 15px 0; |
|
font-size: 1.1em; |
|
color: var(--light-text); |
|
} |
|
.upload-section i { |
|
font-size: 3em; |
|
color: var(--primary-color); |
|
margin-bottom: 15px; |
|
} |
|
#file-input { |
|
display: none; |
|
} |
|
#image-preview { |
|
max-width: 100%; |
|
max-height: 250px; |
|
margin-top: 20px; |
|
border-radius: var(--border-radius); |
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1); |
|
display: none; |
|
} |
|
|
|
.prompt-selector { |
|
margin-bottom: 25px; |
|
text-align: left; |
|
} |
|
.prompt-selector label { |
|
font-weight: 500; |
|
margin-bottom: 8px; |
|
display: block; |
|
color: var(--dark-text); |
|
} |
|
.prompt-selector select { |
|
width: 100%; |
|
padding: 12px; |
|
border-radius: var(--border-radius); |
|
border: 1px solid var(--border-color); |
|
font-size: 1em; |
|
font-family: 'Poppins', sans-serif; |
|
background-color: #fff; |
|
transition: border-color 0.3s ease; |
|
} |
|
.prompt-selector select:focus { |
|
border-color: var(--primary-color); |
|
outline: none; |
|
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); |
|
} |
|
|
|
.button { |
|
background-color: var(--primary-color); |
|
color: white; |
|
border: none; |
|
padding: 12px 25px; |
|
font-size: 1.1em; |
|
font-weight: 500; |
|
border-radius: var(--border-radius); |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
display: inline-flex; |
|
align-items: center; |
|
justify-content: center; |
|
gap: 8px; |
|
} |
|
.button:hover { |
|
background-color: #2980b9; |
|
transform: translateY(-2px); |
|
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.3); |
|
} |
|
.button:disabled { |
|
background-color: #bdc3c7; |
|
cursor: not-allowed; |
|
transform: none; |
|
box-shadow: none; |
|
} |
|
.button.copy-button { |
|
background-color: var(--secondary-color); |
|
} |
|
.button.copy-button:hover { |
|
background-color: #27ae60; |
|
box-shadow: 0 6px 20px rgba(46, 204, 113, 0.3); |
|
} |
|
|
|
#solving-container { |
|
display: none; |
|
margin-top: 30px; |
|
padding: 25px; |
|
background-color: #f9f9f9; |
|
border-radius: var(--border-radius); |
|
border: 1px solid var(--border-color); |
|
} |
|
.status { |
|
font-size: 1.1em; |
|
font-weight: 500; |
|
margin-bottom: 15px; |
|
color: var(--dark-text); |
|
} |
|
.status i { |
|
margin-right: 8px; |
|
} |
|
.status small { |
|
display: block; |
|
font-weight: 400; |
|
color: var(--light-text); |
|
font-size: 0.9em; |
|
margin-top: 5px; |
|
} |
|
.telegram-notice { |
|
background-color: #e3f2fd; |
|
border-left: 4px solid var(--primary-color); |
|
padding: 12px 15px; |
|
margin: 20px 0; |
|
font-size: 0.95em; |
|
border-radius: 4px; |
|
color: var(--dark-text); |
|
} |
|
.telegram-notice i { |
|
margin-right: 8px; |
|
color: var(--primary-color); |
|
} |
|
|
|
.loading-spinner { |
|
border: 4px solid rgba(0, 0, 0, 0.1); |
|
width: 36px; |
|
height: 36px; |
|
border-radius: 50%; |
|
border-left-color: var(--primary-color); |
|
animation: spin 1s ease infinite; |
|
margin: 20px auto; |
|
display: none; |
|
} |
|
@keyframes spin { |
|
0% { transform: rotate(0deg); } |
|
100% { transform: rotate(360deg); } |
|
} |
|
|
|
.response-container { |
|
margin-top: 20px; |
|
padding: 20px; |
|
border: 1px solid var(--border-color); |
|
border-radius: var(--border-radius); |
|
background-color: var(--card-bg); |
|
display: none; |
|
text-align: left; |
|
} |
|
#response { |
|
font-family: 'Roboto Mono', monospace; |
|
background-color: #fdf6e3; |
|
color: #657b83; |
|
padding: 15px; |
|
border-radius: var(--border-radius); |
|
overflow-x: auto; |
|
white-space: pre-wrap; |
|
word-wrap: break-word; |
|
max-height: 400px; |
|
margin-bottom: 15px; |
|
} |
|
#response code { |
|
display: block; |
|
} |
|
|
|
.error-message { |
|
color: var(--accent-color); |
|
background-color: #fdedec; |
|
border: 1px solid var(--accent-color); |
|
padding: 15px; |
|
border-radius: var(--border-radius); |
|
margin-bottom: 15px; |
|
} |
|
.error-message i { |
|
margin-right: 8px; |
|
} |
|
|
|
.footer { |
|
margin-top: 40px; |
|
font-size: 0.9em; |
|
color: var(--light-text); |
|
} |
|
.footer a { |
|
color: var(--primary-color); |
|
text-decoration: none; |
|
} |
|
.footer a:hover { |
|
text-decoration: underline; |
|
} |
|
|
|
|
|
@media (max-width: 600px) { |
|
body { padding: 15px; } |
|
.main-container { padding: 20px; } |
|
h1 { font-size: 1.8em; } |
|
.upload-section { padding: 20px; } |
|
.upload-section p { font-size: 1em; } |
|
.upload-section i { font-size: 2.5em; } |
|
} |
|
|
|
</style> |
|
</head> |
|
<body> |
|
<div class="main-container"> |
|
<h1><i class="fas fa-brain"></i>Math Solver IA</h1> |
|
|
|
<div id="upload-section" class="upload-section"> |
|
<i class="fas fa-cloud-upload-alt"></i> |
|
<p>Cliquez ou glissez-déposez une image ici</p> |
|
<input type="file" id="file-input" accept="image/*"> |
|
<img id="image-preview" src="#" alt="Aperçu de l'image"> |
|
</div> |
|
|
|
<div class="prompt-selector"> |
|
<label for="prompt-type">Style de Correction LaTeX :</label> |
|
<select id="prompt-type" name="prompt-type"> |
|
<option value="refined">Raffiné et Complet (avec tcolorbox, etc.)</option> |
|
<option value="light">Léger et Rapide (LaTeX standard)</option> |
|
</select> |
|
</div> |
|
|
|
<button id="solve-button" class="button" disabled> |
|
<i class="fas fa-magic"></i>Résoudre |
|
</button> |
|
|
|
<div id="solving-container"> |
|
<div class="status" id="status-message"> |
|
<i class="fas fa-hourglass-half"></i>En attente de résolution... |
|
</div> |
|
<div class="loading-spinner" id="loading-spinner"></div> |
|
<div class="telegram-notice"> |
|
<i class="fab fa-telegram-plane"></i>La réponse complète sera également envoyée sur Telegram. |
|
</div> |
|
<div class="response-container" id="response-container"> |
|
<h3><i class="fas fa-file-code"></i>Code LaTeX Généré :</h3> |
|
<div id="response"></div> |
|
<button id="copy-button" class="button copy-button"> |
|
<i class="fas fa-copy"></i>Copier le code |
|
</button> |
|
</div> |
|
<div id="error-display" class="error-message" style="display: none;"> |
|
|
|
</div> |
|
</div> |
|
</div> |
|
|
|
<footer class="footer"> |
|
Propulsé par IA - <a href="#" target="_blank">Mariam-AI</a> © 2024 |
|
</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'); |
|
const responseDiv = document.getElementById('response'); |
|
const copyButton = document.getElementById('copy-button'); |
|
const statusMessageElement = document.getElementById('status-message'); |
|
const loadingSpinner = document.getElementById('loading-spinner'); |
|
const promptTypeSelect = document.getElementById('prompt-type'); |
|
const errorDisplay = document.getElementById('error-display'); |
|
|
|
let selectedFile = null; |
|
|
|
uploadSection.addEventListener('click', () => fileInput.click()); |
|
|
|
['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]); |
|
} |
|
}); |
|
|
|
fileInput.addEventListener('change', (e) => { |
|
if (e.target.files.length) { |
|
handleFileSelection(e.target.files[0]); |
|
} |
|
}); |
|
|
|
function handleFileSelection(file) { |
|
if (!file.type.startsWith('image/')) { |
|
displayError('Veuillez sélectionner un fichier image valide (PNG, JPG, etc.).'); |
|
selectedFile = null; |
|
solveButton.disabled = true; |
|
imagePreview.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'; |
|
}; |
|
reader.readAsDataURL(file); |
|
} |
|
|
|
function displayError(message, details = null) { |
|
let fullMessage = `<i class="fas fa-exclamation-triangle"></i> ${message}`; |
|
if (details) { |
|
fullMessage += `<br><small>Détail: ${escapeHtml(details)}</small>`; |
|
} |
|
errorDisplay.innerHTML = fullMessage; |
|
errorDisplay.style.display = 'block'; |
|
responseContainer.style.display = 'none'; |
|
loadingSpinner.style.display = 'none'; |
|
} |
|
|
|
solveButton.addEventListener('click', () => { |
|
if (!selectedFile) return; |
|
|
|
solveButton.disabled = true; |
|
solvingContainer.style.display = 'block'; |
|
responseContainer.style.display = 'none'; |
|
responseDiv.textContent = ''; |
|
errorDisplay.style.display = 'none'; |
|
loadingSpinner.style.display = 'block'; |
|
updateStatusUI('pending', ''); |
|
|
|
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); |
|
} |
|
|
|
const taskId = data.task_id; |
|
updateStatusUI('pending', taskId); |
|
|
|
const eventSource = new EventSource('/stream/' + taskId); |
|
|
|
eventSource.onmessage = function(event) { |
|
const streamData = JSON.parse(event.data); |
|
|
|
if (streamData.error) { |
|
displayError(streamData.error, streamData.error_detail); |
|
|
|
if (streamData.response) { |
|
responseDiv.textContent = streamData.response; |
|
responseContainer.style.display = 'block'; |
|
} |
|
eventSource.close(); |
|
solveButton.disabled = false; |
|
loadingSpinner.style.display = 'none'; |
|
return; |
|
} |
|
|
|
updateStatusUI(streamData.status, taskId); |
|
|
|
if (streamData.status === 'completed' || streamData.status === 'completed_tex_only' || streamData.status === 'pdf_error') { |
|
responseContainer.style.display = 'block'; |
|
loadingSpinner.style.display = 'none'; |
|
|
|
if (streamData.response) { |
|
responseDiv.textContent = streamData.response; |
|
} |
|
|
|
if (streamData.status === 'pdf_error' && streamData.error_detail) { |
|
|
|
let currentStatus = statusMessageElement.innerHTML; |
|
statusMessageElement.innerHTML = currentStatus + `<br><small style="color:var(--accent-color);"><i class="fas fa-file-pdf"></i> Erreur PDF: ${escapeHtml(streamData.error_detail)}</small>`; |
|
} |
|
|
|
eventSource.close(); |
|
solveButton.disabled = false; |
|
} |
|
}; |
|
|
|
eventSource.onerror = function() { |
|
eventSource.close(); |
|
|
|
fetch('/task/' + taskId) |
|
.then(resp => resp.json()) |
|
.then(taskData => { |
|
updateStatusUI(taskData.status, taskId); |
|
if (taskData.status === 'completed' || taskData.status === 'completed_tex_only' || taskData.status === 'pdf_error') { |
|
responseContainer.style.display = 'block'; |
|
if (taskData.response) { |
|
responseDiv.textContent = taskData.response; |
|
} |
|
if (taskData.status === 'pdf_error' && taskData.error_detail) { |
|
let currentStatus = statusMessageElement.innerHTML; |
|
statusMessageElement.innerHTML = currentStatus + `<br><small style="color:var(--accent-color);"><i class="fas fa-file-pdf"></i> Erreur PDF: ${escapeHtml(taskData.error_detail)}</small>`; |
|
} |
|
} else if (taskData.status === 'error') { |
|
displayError(taskData.error || 'Une erreur inattendue est survenue lors de la récupération de la tâche.', taskData.error_detail); |
|
} else { |
|
|
|
displayError('Connexion perdue avec le serveur. Le traitement peut continuer en arrière-plan.', 'Vérifiez Telegram pour la réponse finale.'); |
|
} |
|
}) |
|
.catch(error => { |
|
displayError('Erreur de connexion lors de la récupération du statut de la tâche.', error.message); |
|
}) |
|
.finally(() => { |
|
solveButton.disabled = false; |
|
loadingSpinner.style.display = 'none'; |
|
}); |
|
}; |
|
}) |
|
.catch(error => { |
|
displayError(error.message || 'Une erreur est survenue lors de la communication avec le serveur.'); |
|
solveButton.disabled = false; |
|
loadingSpinner.style.display = 'none'; |
|
}); |
|
}); |
|
|
|
function updateStatusUI(status, taskId) { |
|
const selectedPromptText = promptTypeSelect.options[promptTypeSelect.selectedIndex].text.split('(')[0].trim(); |
|
let statusMsg = ''; |
|
let iconClass = 'fas fa-hourglass-half'; |
|
|
|
switch(status) { |
|
case 'pending': statusMsg = "En attente de traitement..."; iconClass = 'fas fa-pause-circle'; break; |
|
case 'processing': statusMsg = "L'IA analyse votre image..."; iconClass = 'fas fa-cogs'; break; |
|
case 'generating_latex': statusMsg = "Génération du code LaTeX..."; iconClass = 'fas fa-file-alt'; break; |
|
case 'cleaning_latex': statusMsg = "Nettoyage du code LaTeX..."; iconClass = 'fas fa-broom'; break; |
|
case 'generating_pdf': statusMsg = "Compilation du PDF LaTeX..."; iconClass = 'fas fa-file-pdf'; break; |
|
case 'completed': statusMsg = "Terminé ! PDF et LaTeX générés."; iconClass = 'fas fa-check-circle'; break; |
|
case 'completed_tex_only': statusMsg = "Terminé ! LaTeX généré (PDF non dispo/demandé)."; iconClass = 'fas fa-check-circle'; break; |
|
case 'pdf_error': statusMsg = "Erreur PDF. LaTeX seul généré."; iconClass = 'fas fa-exclamation-circle'; break; |
|
case 'error': statusMsg = "Erreur de traitement."; iconClass = 'fas fa-times-circle'; break; |
|
default: statusMsg = `Statut inconnu: ${status}`; iconClass = 'fas fa-question-circle'; |
|
} |
|
|
|
let taskInfo = ''; |
|
if (taskId) { |
|
taskInfo = ` (Tâche ${taskId.substring(0,8)}, Style: ${selectedPromptText})`; |
|
} |
|
|
|
statusMessageElement.innerHTML = `<i class="${iconClass}"></i> ${statusMsg}${taskInfo}`; |
|
|
|
} |
|
|
|
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 = responseDiv.textContent; |
|
navigator.clipboard.writeText(textToCopy).then(() => { |
|
const originalIcon = copyButton.querySelector('i').className; |
|
const originalText = copyButton.childNodes[1].nodeValue; |
|
copyButton.innerHTML = `<i class="fas fa-check"></i> Copié!`; |
|
setTimeout(() => { |
|
copyButton.innerHTML = `<i class="${originalIcon}"></i>${originalText}`; |
|
}, 2000); |
|
}).catch(err => { |
|
console.error('Erreur de copie: ', err); |
|
displayError('Erreur lors de la copie du texte.', 'Veuillez essayer manuellement.'); |
|
|
|
try { |
|
const range = document.createRange(); |
|
range.selectNodeContents(responseDiv); |
|
window.getSelection().removeAllRanges(); |
|
window.getSelection().addRange(range); |
|
document.execCommand('copy'); |
|
window.getSelection().removeAllRanges(); |
|
|
|
const originalIcon = copyButton.querySelector('i').className; |
|
const originalText = copyButton.childNodes[1].nodeValue; |
|
copyButton.innerHTML = `<i class="fas fa-check"></i> Copié!`; |
|
setTimeout(() => { |
|
copyButton.innerHTML = `<i class="${originalIcon}"></i>${originalText}`; |
|
}, 2000); |
|
} catch (e) { |
|
displayError('Erreur lors de la copie (fallback).', 'Veuillez essayer manuellement.'); |
|
} |
|
}); |
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}); |
|
</script> |
|
</body> |
|
</html> |