|
<!DOCTYPE html> |
|
<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 !important; |
|
} |
|
|
|
|
|
@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', () => { |
|
|
|
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; |
|
|
|
|
|
tabs.forEach(tab => { |
|
tab.addEventListener('click', () => { |
|
|
|
tabs.forEach(t => t.classList.remove('active')); |
|
tabContents.forEach(c => c.classList.remove('active')); |
|
|
|
|
|
tab.classList.add('active'); |
|
document.getElementById(`${tab.dataset.tab}-tab`).classList.add('active'); |
|
}); |
|
}); |
|
|
|
|
|
imageInput.addEventListener('change', handleFileSelect); |
|
|
|
|
|
['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); |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
function showLoading() { |
|
loadingEl.classList.remove('hidden'); |
|
processButton.disabled = true; |
|
messagesEl.classList.add('hidden'); |
|
resultsEl.classList.add('hidden'); |
|
pdfViewer.src = 'about:blank'; |
|
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'); |
|
|
|
|
|
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'); |
|
} |
|
|
|
|
|
latexOutputEl.textContent = data.latex || "Aucun code LaTeX disponible."; |
|
|
|
|
|
thinkingOutputEl.textContent = data.thinking || "Aucun processus de réflexion disponible."; |
|
|
|
|
|
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'); |
|
} |
|
|
|
|
|
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'; |
|
} |
|
} |
|
|
|
|
|
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); |
|
|
|
if(data.latex || data.thinking) { |
|
displayResults(data); |
|
} |
|
} |
|
|
|
} catch (error) { |
|
hideLoading(); |
|
showMessage(`Erreur de communication avec le serveur : ${error}`, true); |
|
console.error("Fetch Error:", error); |
|
} |
|
}); |
|
|
|
|
|
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) { } |
|
showMessage(errorMsg, true); |
|
} |
|
} catch (error) { |
|
showMessage(`Erreur lors de la tentative de téléchargement : ${error}`, true); |
|
console.error("Download Error:", error); |
|
} |
|
}); |
|
|
|
|
|
checkStatus(); |
|
}); |
|
</script> |
|
</body> |
|
</html> |