Spaces:
Running
Running
<html lang="fr"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Mariam AI - Correcteur d'Exercices</title> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
:root { | |
--primary: #4f46e5; | |
--primary-hover: #4338ca; | |
--primary-light: #eef2ff; | |
--success: #10b981; | |
--success-light: #ecfdf5; | |
--error: #ef4444; | |
--error-light: #fef2f2; | |
--text: #1f2937; | |
--text-light: #6b7280; | |
--bg-light: #f9fafb; | |
--card-bg: #ffffff; | |
--border: #e5e7eb; | |
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
--shadow-md: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
--radius: 0.5rem; | |
} | |
* { | |
box-sizing: border-box; | |
margin: 0; | |
padding: 0; | |
} | |
body { | |
font-family: 'Inter', system-ui, -apple-system, sans-serif; | |
line-height: 1.6; | |
color: var(--text); | |
background-color: var(--bg-light); | |
padding: 1.5rem; | |
} | |
.container { | |
max-width: 1000px; | |
margin: 0 auto; | |
} | |
.header { | |
text-align: center; | |
margin-bottom: 2rem; | |
} | |
h1 { | |
font-size: 2rem; | |
font-weight: 700; | |
margin-bottom: 0.5rem; | |
color: var(--primary); | |
} | |
h2 { | |
font-size: 1.25rem; | |
font-weight: 600; | |
margin-bottom: 1rem; | |
color: var(--text); | |
} | |
h3 { | |
font-size: 1rem; | |
font-weight: 600; | |
margin-bottom: 0.75rem; | |
color: var(--text); | |
} | |
.subheader { | |
color: var(--text-light); | |
font-size: 1rem; | |
} | |
.card { | |
background: var(--card-bg); | |
border-radius: var(--radius); | |
box-shadow: var(--shadow); | |
padding: 1.5rem; | |
margin-bottom: 1.5rem; | |
} | |
.status-checks { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
gap: 1rem; | |
} | |
.status-item { | |
padding: 1rem; | |
border-radius: var(--radius); | |
display: flex; | |
align-items: center; | |
gap: 0.75rem; | |
} | |
.status-success { | |
background-color: var(--success-light); | |
border: 1px solid var(--success); | |
} | |
.status-error { | |
background-color: var(--error-light); | |
border: 1px solid var(--error); | |
} | |
.status-icon { | |
font-size: 1.25rem; | |
} | |
.status-success .status-icon { | |
color: var(--success); | |
} | |
.status-error .status-icon { | |
color: var(--error); | |
} | |
.status-text { | |
flex: 1; | |
} | |
.file-upload { | |
display: flex; | |
flex-direction: column; | |
gap: 1rem; | |
} | |
.file-input-container { | |
position: relative; | |
width: 100%; | |
height: 150px; | |
border: 2px dashed var(--border); | |
border-radius: var(--radius); | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
gap: 0.75rem; | |
padding: 1.5rem; | |
cursor: pointer; | |
overflow: hidden; | |
transition: border-color 0.3s ease, background 0.3s ease; | |
} | |
.file-input-container:hover, .file-input-container.dragover { | |
border-color: var(--primary); | |
background: var(--primary-light); | |
} | |
.file-input { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
opacity: 0; | |
cursor: pointer; | |
} | |
.upload-icon { | |
font-size: 2rem; | |
color: var(--primary); | |
} | |
.upload-label { | |
font-weight: 500; | |
} | |
.upload-hint { | |
font-size: 0.875rem; | |
color: var(--text-light); | |
} | |
.file-preview { | |
display: none; | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
z-index: 1; | |
background: rgba(255, 255, 255, 0.9); | |
align-items: center; | |
justify-content: center; | |
} | |
.file-preview img { | |
max-width: 90%; | |
max-height: 90%; | |
object-fit: contain; | |
} | |
.preview-active .file-preview { | |
display: flex; | |
} | |
.file-name { | |
display: none; | |
position: absolute; | |
bottom: 0; | |
left: 0; | |
right: 0; | |
background: rgba(255, 255, 255, 0.8); | |
padding: 0.5rem; | |
font-size: 0.875rem; | |
text-align: center; | |
word-break: break-all; | |
} | |
.preview-active .file-name { | |
display: block; | |
} | |
.clear-file { | |
display: none; | |
position: absolute; | |
top: 0.5rem; | |
right: 0.5rem; | |
background: white; | |
border-radius: 50%; | |
width: 24px; | |
height: 24px; | |
align-items: center; | |
justify-content: center; | |
cursor: pointer; | |
box-shadow: var(--shadow); | |
z-index: 2; | |
} | |
.preview-active .clear-file { | |
display: flex; | |
} | |
.button { | |
display: inline-flex; | |
align-items: center; | |
justify-content: center; | |
gap: 0.5rem; | |
background-color: var(--primary); | |
color: white; | |
padding: 0.75rem 1.5rem; | |
border: none; | |
border-radius: var(--radius); | |
font-weight: 500; | |
font-size: 1rem; | |
cursor: pointer; | |
transition: background-color 0.3s, transform 0.2s; | |
width: 100%; | |
} | |
.button:hover { | |
background-color: var(--primary-hover); | |
} | |
.button:active { | |
transform: translateY(1px); | |
} | |
.button:disabled { | |
background-color: var(--text-light); | |
cursor: not-allowed; | |
opacity: 0.7; | |
} | |
.button-icon { | |
font-size: 1rem; | |
} | |
.loading { | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
gap: 1rem; | |
padding: 2rem; | |
} | |
.spinner { | |
width: 40px; | |
height: 40px; | |
border: 4px solid rgba(79, 70, 229, 0.2); | |
border-radius: 50%; | |
border-top-color: var(--primary); | |
animation: spin 1s linear infinite; | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
.message { | |
padding: 1rem; | |
border-radius: var(--radius); | |
margin-bottom: 1.5rem; | |
display: flex; | |
align-items: center; | |
gap: 0.75rem; | |
} | |
.message-success { | |
background-color: var(--success-light); | |
border: 1px solid var(--success); | |
color: var(--success); | |
} | |
.message-error { | |
background-color: var(--error-light); | |
border: 1px solid var(--error); | |
color: var(--error); | |
white-space: pre-wrap; | |
} | |
.tab-container { | |
border: 1px solid var(--border); | |
border-radius: var(--radius); | |
overflow: hidden; | |
} | |
.tabs { | |
display: flex; | |
background: var(--bg-light); | |
} | |
.tab { | |
padding: 0.75rem 1.25rem; | |
cursor: pointer; | |
border-bottom: 2px solid transparent; | |
font-weight: 500; | |
color: var(--text-light); | |
transition: all 0.3s ease; | |
} | |
.tab.active { | |
color: var(--primary); | |
border-bottom-color: var(--primary); | |
} | |
.tab-content { | |
display: none; | |
padding: 1.5rem; | |
} | |
.tab-content.active { | |
display: block; | |
} | |
#pdf-viewer { | |
width: 100%; | |
height: 600px; | |
border: 1px solid var(--border); | |
border-radius: var(--radius); | |
} | |
.code-area { | |
background-color: #f8fafc; | |
border: 1px solid var(--border); | |
border-radius: 0.25rem; | |
padding: 1rem; | |
max-height: 400px; | |
overflow-y: auto; | |
} | |
.code-area pre { | |
margin: 0; | |
white-space: pre-wrap; | |
word-wrap: break-word; | |
font-family: Consolas, Monaco, 'Andale Mono', monospace; | |
font-size: 0.875rem; | |
line-height: 1.5; | |
} | |
.download-button { | |
display: inline-flex; | |
align-items: center; | |
justify-content: center; | |
gap: 0.5rem; | |
background-color: var(--primary); | |
color: white; | |
padding: 0.75rem 1.5rem; | |
border: none; | |
border-radius: var(--radius); | |
font-weight: 500; | |
font-size: 1rem; | |
cursor: pointer; | |
transition: background-color 0.3s, transform 0.2s; | |
margin: 1.5rem auto; | |
width: auto; | |
} | |
.hidden { | |
display: none ; | |
} | |
/* Responsive design */ | |
@media (max-width: 768px) { | |
.status-checks { | |
grid-template-columns: 1fr; | |
} | |
.file-upload-container { | |
height: 120px; | |
} | |
.tab { | |
padding: 0.5rem 1rem; | |
font-size: 0.875rem; | |
} | |
#pdf-viewer { | |
height: 400px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="header"> | |
<h1>Mariam AI</h1> | |
<p class="subheader">Correcteur Intelligent d'Exercices Mathématiques/physique/chimie.</p> | |
</div> | |
<div class="card"> | |
<h2>État du système</h2> | |
<div class="status-checks"> | |
<div id="latex-status" class="status-item"> | |
<span class="status-icon"><i class="fas fa-spinner fa-spin"></i></span> | |
<span class="status-text">Vérification de LaTeX...</span> | |
</div> | |
<div id="api-status" class="status-item"> | |
<span class="status-icon"><i class="fas fa-spinner fa-spin"></i></span> | |
<span class="status-text">Vérification de l'API Mariam AI...</span> | |
</div> | |
</div> | |
</div> | |
<div class="card"> | |
<h2>Soumettre un exercice</h2> | |
<div class="file-upload"> | |
<div id="file-input-container" class="file-input-container"> | |
<span class="upload-icon"><i class="fas fa-cloud-upload-alt"></i></span> | |
<span class="upload-label">Déposez votre image ou cliquez pour sélectionner</span> | |
<span class="upload-hint">Formats acceptés: JPG, PNG, GIF</span> | |
<input type="file" id="image-input" class="file-input" accept="image/*"> | |
<div class="file-preview"> | |
<img id="image-preview" src="" alt="Aperçu"> | |
</div> | |
<div class="file-name" id="file-name"></div> | |
<div class="clear-file" id="clear-file"><i class="fas fa-times"></i></div> | |
</div> | |
<button id="process-button" class="button" disabled> | |
<span class="button-icon"><i class="fas fa-magic"></i></span> | |
<span>Générer la solution</span> | |
</button> | |
</div> | |
</div> | |
<div id="loading" class="card loading hidden"> | |
<div class="spinner"></div> | |
<p>Mariam AI analyse l'exercice et génère la solution...</p> | |
<p class="upload-hint">Cette opération peut prendre jusqu'à une minute.</p> | |
</div> | |
<div id="messages" class="message hidden"></div> | |
<div id="results" class="card hidden"> | |
<h2>Résultat de l'analyse</h2> | |
<div class="tab-container"> | |
<div class="tabs"> | |
<div class="tab active" data-tab="pdf">Aperçu PDF</div> | |
<div class="tab" data-tab="latex">Code LaTeX</div> | |
<div class="tab" data-tab="thinking">Processus de réflexion</div> | |
</div> | |
<div id="pdf-tab" class="tab-content active"> | |
<iframe id="pdf-viewer" title="Aperçu du PDF de la solution"></iframe> | |
<div class="download-container"> | |
<button id="download-button" class="download-button"> | |
<i class="fas fa-download"></i> Télécharger le PDF | |
</button> | |
</div> | |
</div> | |
<div id="latex-tab" class="tab-content"> | |
<div class="code-area"> | |
<pre id="latex-output"></pre> | |
</div> | |
</div> | |
<div id="thinking-tab" class="tab-content"> | |
<div class="code-area"> | |
<pre id="thinking-output"></pre> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
// Éléments DOM | |
const latexStatusEl = document.getElementById('latex-status'); | |
const apiStatusEl = document.getElementById('api-status'); | |
const fileInputContainer = document.getElementById('file-input-container'); | |
const imageInput = document.getElementById('image-input'); | |
const imagePreview = document.getElementById('image-preview'); | |
const fileName = document.getElementById('file-name'); | |
const clearFile = document.getElementById('clear-file'); | |
const processButton = document.getElementById('process-button'); | |
const loadingEl = document.getElementById('loading'); | |
const messagesEl = document.getElementById('messages'); | |
const resultsEl = document.getElementById('results'); | |
const pdfViewer = document.getElementById('pdf-viewer'); | |
const downloadButton = document.getElementById('download-button'); | |
const latexOutputEl = document.getElementById('latex-output'); | |
const thinkingOutputEl = document.getElementById('thinking-output'); | |
const tabs = document.querySelectorAll('.tab'); | |
const tabContents = document.querySelectorAll('.tab-content'); | |
let currentPdfBase64 = null; // Stockage des données PDF | |
// --- Gestion des onglets --- | |
tabs.forEach(tab => { | |
tab.addEventListener('click', () => { | |
// Désactiver tous les onglets | |
tabs.forEach(t => t.classList.remove('active')); | |
tabContents.forEach(c => c.classList.remove('active')); | |
// Activer l'onglet sélectionné | |
tab.classList.add('active'); | |
document.getElementById(`${tab.dataset.tab}-tab`).classList.add('active'); | |
}); | |
}); | |
// --- Gestion du chargement des images --- | |
imageInput.addEventListener('change', handleFileSelect); | |
// Gestion du drag and drop | |
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
fileInputContainer.addEventListener(eventName, preventDefaults, false); | |
}); | |
['dragenter', 'dragover'].forEach(eventName => { | |
fileInputContainer.addEventListener(eventName, () => { | |
fileInputContainer.classList.add('dragover'); | |
}, false); | |
}); | |
['dragleave', 'drop'].forEach(eventName => { | |
fileInputContainer.addEventListener(eventName, () => { | |
fileInputContainer.classList.remove('dragover'); | |
}, false); | |
}); | |
fileInputContainer.addEventListener('drop', handleDrop, false); | |
// Gestion du bouton de suppression de fichier | |
clearFile.addEventListener('click', (e) => { | |
e.stopPropagation(); | |
clearFileInput(); | |
}); | |
function preventDefaults(e) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
} | |
function handleDrop(e) { | |
const dt = e.dataTransfer; | |
const files = dt.files; | |
if (files && files[0]) { | |
imageInput.files = files; | |
handleFileSelect(); | |
} | |
} | |
function handleFileSelect() { | |
const file = imageInput.files[0]; | |
if (file) { | |
const reader = new FileReader(); | |
reader.onload = function(e) { | |
imagePreview.src = e.target.result; | |
fileName.textContent = file.name; | |
fileInputContainer.classList.add('preview-active'); | |
processButton.disabled = false; | |
}; | |
reader.readAsDataURL(file); | |
} else { | |
clearFileInput(); | |
} | |
} | |
function clearFileInput() { | |
imageInput.value = ''; | |
imagePreview.src = ''; | |
fileName.textContent = ''; | |
fileInputContainer.classList.remove('preview-active'); | |
processButton.disabled = true; | |
} | |
// --- Fonctions Utilitaires --- | |
function showLoading() { | |
loadingEl.classList.remove('hidden'); | |
processButton.disabled = true; | |
messagesEl.classList.add('hidden'); | |
resultsEl.classList.add('hidden'); | |
pdfViewer.src = 'about:blank'; // Vider l'aperçu PDF | |
latexOutputEl.textContent = ''; | |
thinkingOutputEl.textContent = ''; | |
currentPdfBase64 = null; | |
} | |
function hideLoading() { | |
loadingEl.classList.add('hidden'); | |
processButton.disabled = !imageInput.files[0]; | |
} | |
function showMessage(message, isError = false) { | |
messagesEl.innerHTML = ` | |
<i class="fas ${isError ? 'fa-exclamation-circle' : 'fa-check-circle'}"></i> | |
<div>${message}</div> | |
`; | |
messagesEl.className = `message ${isError ? 'message-error' : 'message-success'}`; | |
messagesEl.classList.remove('hidden'); | |
} | |
function displayResults(data) { | |
resultsEl.classList.remove('hidden'); | |
// Gestion du PDF | |
if (data.pdf_base64) { | |
pdfViewer.src = `data:application/pdf;base64,${data.pdf_base64}`; | |
currentPdfBase64 = data.pdf_base64; | |
downloadButton.classList.remove('hidden'); | |
} else { | |
pdfViewer.src = 'about:blank'; | |
downloadButton.classList.add('hidden'); | |
} | |
// Gestion du LaTeX | |
latexOutputEl.textContent = data.latex || "Aucun code LaTeX disponible."; | |
// Gestion du processus de réflexion | |
thinkingOutputEl.textContent = data.thinking || "Aucun processus de réflexion disponible."; | |
// Activer l'onglet approprié par défaut | |
if (data.pdf_base64) { | |
activateTab('pdf'); | |
} else if (data.latex) { | |
activateTab('latex'); | |
} else if (data.thinking) { | |
activateTab('thinking'); | |
} | |
} | |
function activateTab(tabName) { | |
tabs.forEach(t => t.classList.remove('active')); | |
tabContents.forEach(c => c.classList.remove('active')); | |
document.querySelector(`.tab[data-tab="${tabName}"]`).classList.add('active'); | |
document.getElementById(`${tabName}-tab`).classList.add('active'); | |
} | |
// --- Vérifications Initiales --- | |
async function checkStatus() { | |
try { | |
const latexRes = await fetch('/check-latex'); | |
const latexData = await latexRes.json(); | |
latexStatusEl.innerHTML = ` | |
<span class="status-icon"><i class="fas ${latexData.success ? 'fa-check-circle' : 'fa-times-circle'}"></i></span> | |
<span class="status-text">${latexData.message}</span> | |
`; | |
latexStatusEl.className = `status-item ${latexData.success ? 'status-success' : 'status-error'}`; | |
} catch (error) { | |
latexStatusEl.innerHTML = ` | |
<span class="status-icon"><i class="fas fa-times-circle"></i></span> | |
<span class="status-text">Erreur lors de la vérification de LaTeX: ${error}</span> | |
`; | |
latexStatusEl.className = 'status-item status-error'; | |
} | |
try { | |
const apiRes = await fetch('/check-api'); | |
const apiData = await apiRes.json(); | |
apiStatusEl.innerHTML = ` | |
<span class="status-icon"><i class="fas ${apiData.success ? 'fa-check-circle' : 'fa-times-circle'}"></i></span> | |
<span class="status-text">${apiData.message}</span> | |
`; | |
apiStatusEl.className = `status-item ${apiData.success ? 'status-success' : 'status-error'}`; | |
} catch (error) { | |
apiStatusEl.innerHTML = ` | |
<span class="status-icon"><i class="fas fa-times-circle"></i></span> | |
<span class="status-text">Erreur lors de la vérification de l'API: ${error}</span> | |
`; | |
apiStatusEl.className = 'status-item status-error'; | |
} | |
} | |
// --- Traitement de l'Image --- | |
processButton.addEventListener('click', async () => { | |
const file = imageInput.files[0]; | |
if (!file) { | |
showMessage('Veuillez sélectionner un fichier image.', true); | |
return; | |
} | |
showLoading(); | |
const formData = new FormData(); | |
formData.append('image', file); | |
try { | |
const response = await fetch('/process', { | |
method: 'POST', | |
body: formData | |
}); | |
const data = await response.json(); | |
hideLoading(); | |
if (data.success) { | |
showMessage('Solution générée avec succès !'); | |
displayResults(data); | |
} else { | |
showMessage(`Erreur : ${data.message}`, true); | |
// Afficher le LaTeX/Thinking même en cas d'erreur de compilation | |
if(data.latex || data.thinking) { | |
displayResults(data); | |
} | |
} | |
} catch (error) { | |
hideLoading(); | |
showMessage(`Erreur de communication avec le serveur : ${error}`, true); | |
console.error("Fetch Error:", error); | |
} | |
}); | |
// --- Téléchargement du PDF --- | |
downloadButton.addEventListener('click', async () => { | |
if (!currentPdfBase64) { | |
showMessage('Aucune donnée PDF à télécharger.', true); | |
return; | |
} | |
try { | |
const response = await fetch('/download-pdf', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ pdf_data: currentPdfBase64 }), | |
}); | |
if (response.ok) { | |
const blob = await response.blob(); | |
const url = window.URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.style.display = 'none'; | |
a.href = url; | |
a.download = response.headers.get('Content-Disposition')?.split('filename=')[1]?.replaceAll('"', '') || 'solution_mariam_ai.pdf'; | |
document.body.appendChild(a); | |
a.click(); | |
window.URL.revokeObjectURL(url); | |
a.remove(); | |
showMessage('Téléchargement démarré.'); | |
} else { | |
let errorMsg = `Échec du téléchargement (code ${response.status}).`; | |
try { | |
const errorData = await response.json(); | |
if(errorData.message) errorMsg += ` Raison: ${errorData.message}`; | |
} catch(e) { /* Ignorer l'erreur si la réponse n'est pas JSON */ } | |
showMessage(errorMsg, true); | |
} | |
} catch (error) { | |
showMessage(`Erreur lors de la tentative de téléchargement : ${error}`, true); | |
console.error("Download Error:", error); | |
} | |
}); | |
// Exécuter les vérifications au chargement | |
checkStatus(); | |
}); | |
</script> | |
</body> | |
</html> |