Spaces:
Paused
Paused
| <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> |