Spaces:
Running
Running
<html lang="fr"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Résolution d'exercices avec Gemini</title> | |
<!-- Inclusion de Tailwind CSS via CDN --> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<!-- Inclusion de MathJax pour le rendu LaTeX --> | |
<!-- Il est préférable de charger MathJax v3 --> | |
<script> | |
MathJax = { | |
tex: { | |
inlineMath: [['$', '$'], ['\\(', '\\)']], // Délimiteurs pour les maths en ligne | |
displayMath: [['$$', '$$'], ['\\[', '\\]']], // Délimiteurs pour les maths en affichage | |
processEscapes: true // Traiter les échappements comme \$ | |
}, | |
svg: { | |
fontCache: 'global' // Améliore la performance du rendu SVG | |
} | |
}; | |
</script> | |
<script type="text/javascript" id="MathJax-script" async | |
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"> | |
</script> | |
<!-- Inclusion de highlight.js pour la coloration syntaxique --> | |
<!-- Choisir un thème (ex: github-dark) --> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> | |
<!-- Optionnel: Charger des langages spécifiques si nécessaire --> | |
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script> --> | |
<!-- Styles personnalisés additionnels si besoin (peuvent être intégrés à Tailwind config pour des projets plus gros) --> | |
<style> | |
/* Style pour s'assurer que MathJax n'affecte pas trop la hauteur de ligne */ | |
mjx-container { | |
line-height: normal ; /* Ajuster si nécessaire */ | |
display: inline-block ; /* Pour un meilleur alignement inline */ | |
} | |
/* Amélioration visuelle pour les blocs de code */ | |
pre code.hljs { | |
border-radius: 0.375rem; /* rounded-md */ | |
padding: 1rem; /* p-4 */ | |
} | |
</style> | |
</head> | |
<body class="bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-100 text-gray-800 font-sans min-h-screen flex flex-col items-center py-12 px-4"> | |
<div class="w-full max-w-3xl"> | |
<h1 class="text-4xl font-bold text-center mb-10 text-indigo-700"> | |
Résolution d'exercices avec Gemini | |
</h1> | |
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12"> | |
<!-- Section Mode Normal --> | |
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-200"> | |
<h2 class="text-2xl font-semibold mb-5 text-indigo-600">Mode Normal (Gemini Pro)</h2> | |
<form id="solve-form" enctype="multipart/form-data"> | |
<div class="mb-4"> | |
<label for="image" class="block text-sm font-medium text-gray-700 mb-2"> | |
Téléchargez une image de votre exercice : | |
</label> | |
<input type="file" id="image" name="image" accept="image/*" required | |
class="block w-full text-sm text-gray-500 border border-gray-300 rounded-lg cursor-pointer | |
file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 | |
file:text-sm file:font-semibold file:bg-indigo-100 file:text-indigo-700 | |
hover:file:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> | |
</div> | |
<button type="submit" | |
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-lg | |
focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-opacity-50 | |
transition duration-150 ease-in-out"> | |
Résoudre | |
</button> | |
</form> | |
</div> | |
<!-- Section Mode Rapide --> | |
<div class="bg-white p-6 rounded-xl shadow-lg border border-gray-200"> | |
<h2 class="text-2xl font-semibold mb-5 text-purple-600">Mode Rapide (Gemini Flash)</h2> | |
<form id="solved-form" enctype="multipart/form-data"> | |
<div class="mb-4"> | |
<label for="image2" class="block text-sm font-medium text-gray-700 mb-2"> | |
Téléchargez une image de votre exercice : | |
</label> | |
<input type="file" id="image2" name="image" accept="image/*" required | |
class="block w-full text-sm text-gray-500 border border-gray-300 rounded-lg cursor-pointer | |
file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 | |
file:text-sm file:font-semibold file:bg-purple-100 file:text-purple-700 | |
hover:file:bg-purple-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"> | |
</div> | |
<button type="submit" | |
class="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded-lg | |
focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-opacity-50 | |
transition duration-150 ease-in-out"> | |
Résoudre rapidement | |
</button> | |
</form> | |
</div> | |
</div> | |
<!-- Conteneur de Résultats --> | |
<div id="result-container" class="bg-white p-6 rounded-xl shadow-lg border border-gray-200 w-full min-h-[150px] prose max-w-none prose-indigo"> | |
<!-- "prose" applique des styles par défaut au contenu --> | |
<!-- "max-w-none" empêche prose de limiter la largeur --> | |
<!-- "prose-indigo" adapte les couleurs de prose au thème indigo --> | |
<p class="text-gray-500 italic text-center">Les résultats apparaîtront ici...</p> | |
</div> | |
</div> | |
<!-- Inclusion du script JS personnalisé (mis à jour) --> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
const resultContainer = document.getElementById('result-container'); | |
// Fonction pour créer un élément de code formaté et le colorer | |
function createCodeElement(code, language = null) { | |
const pre = document.createElement('pre'); | |
// Note: La classe 'not-prose' peut être utile si les styles de 'prose' interfèrent | |
pre.className = 'my-4 p-0 bg-transparent not-prose'; // Réinitialise le padding/bg pour que hljs prenne le dessus | |
const codeEl = document.createElement('code'); | |
// Ajouter la classe de langage si spécifié pour highlight.js | |
if (language) { | |
codeEl.className = `language-${language}`; | |
} | |
codeEl.textContent = code; | |
pre.appendChild(codeEl); | |
// Appliquer highlight.js à cet élément spécifique | |
hljs.highlightElement(codeEl); | |
return pre; | |
} | |
// Fonction pour créer un élément de texte/Markdown (sera stylisé par "prose") | |
function createMarkdownElement(text) { | |
const div = document.createElement('div'); | |
div.className = 'markdown-content mb-4'; // prose gère le style général | |
// Simple conversion des sauts de ligne en <br> pour un rendu basique | |
// Une vraie lib Markdown (marked.js, markdown-it) serait mieux pour du vrai Markdown | |
div.innerHTML = text.replace(/\n/g, '<br>'); | |
return div; | |
} | |
// Fonction pour créer un élément spécialisé pour le résultat de code | |
function createResultElement(text) { | |
const div = document.createElement('div'); | |
// Styles Tailwind pour un résultat distinct | |
div.className = 'code-result my-4 p-3 bg-green-50 border-l-4 border-green-500 text-sm text-green-800 rounded-r-lg'; | |
div.innerHTML = text.replace(/\n/g, '<br>'); | |
return div; | |
} | |
// Fonction pour créer un élément image | |
function createImageElement(base64Data, format = 'png') { | |
const img = document.createElement('img'); | |
img.src = `data:image/${format};base64,${base64Data}`; | |
// Styles Tailwind pour les images | |
img.className = 'max-w-full h-auto my-4 border border-gray-300 rounded p-1 shadow-sm mx-auto block'; // Centrer l'image | |
return img; | |
} | |
// Fonction pour créer un indicateur (chargement, réflexion) | |
function createIndicator(text, type = 'loading') { | |
const div = document.createElement('div'); | |
div.dataset.indicatorType = type; // Pour pouvoir le retrouver/supprimer | |
// Styles Tailwind pour les indicateurs | |
div.className = 'indicator my-3 text-center italic text-gray-500'; | |
div.textContent = text; | |
return div; | |
} | |
// Fonction pour créer un message d'erreur | |
function createErrorElement(text) { | |
const div = document.createElement('div'); | |
// Styles Tailwind pour les erreurs | |
div.className = 'error-message my-4 p-4 bg-red-100 border border-red-300 text-red-700 rounded-lg'; | |
div.textContent = 'Erreur: ' + text; | |
return div; | |
} | |
// Fonction pour traiter les données SSE et mettre à jour l'UI | |
function processSseData(jsonData) { | |
if (jsonData.mode === 'thinking') { | |
// Supprimer l'indicateur de chargement s'il existe | |
const loadingIndicator = resultContainer.querySelector('[data-indicator-type="loading"]'); | |
if (loadingIndicator) loadingIndicator.remove(); | |
// Afficher l'indicateur de réflexion (s'il n'existe pas déjà) | |
if (!resultContainer.querySelector('[data-indicator-type="thinking"]')) { | |
resultContainer.appendChild(createIndicator('Gemini réfléchit...', 'thinking')); | |
} | |
} else if (jsonData.mode === 'answering') { | |
// Supprimer l'indicateur de réflexion s'il existe | |
const thinkingIndicator = resultContainer.querySelector('[data-indicator-type="thinking"]'); | |
if (thinkingIndicator) thinkingIndicator.remove(); | |
} | |
if (jsonData.content) { | |
let element; | |
switch(jsonData.type) { | |
case 'text': | |
element = createMarkdownElement(jsonData.content); | |
break; | |
case 'code': | |
// Essayez de détecter le langage si possible (sinon hljs tente de deviner) | |
// Vous pourriez passer le langage depuis le backend si connu | |
element = createCodeElement(jsonData.content); | |
break; | |
case 'result': | |
element = createResultElement(jsonData.content); | |
break; | |
case 'image': | |
element = createImageElement(jsonData.content); | |
break; | |
default: // Traiter comme du texte par défaut | |
element = createMarkdownElement(jsonData.content); | |
} | |
resultContainer.appendChild(element); | |
// Demander à MathJax de re-scanner le conteneur pour le nouveau contenu LaTeX | |
// Utilisation de la nouvelle API MathJax 3 | |
if (typeof MathJax !== 'undefined' && MathJax.typesetPromise) { | |
MathJax.typesetPromise([element]).catch((err) => console.error('MathJax processing error:', err)); | |
} | |
} | |
if (jsonData.error) { | |
resultContainer.appendChild(createErrorElement(jsonData.error)); | |
// Supprimer les indicateurs en cas d'erreur | |
resultContainer.querySelectorAll('.indicator').forEach(el => el.remove()); | |
} | |
} | |
// Fonction pour gérer les événements SSE via Fetch API | |
async function setupFetchStream(url, formData) { | |
// Vider le conteneur de résultats et afficher chargement | |
resultContainer.innerHTML = ''; | |
resultContainer.appendChild(createIndicator('Chargement en cours...', 'loading')); | |
try { | |
const response = await fetch(url, { | |
method: 'POST', | |
body: formData | |
// Pas besoin de 'Content-Type': 'multipart/form-data', le navigateur le met avec FormData | |
}); | |
if (!response.ok) { | |
throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}`); | |
} | |
// Vider à nouveau au cas où la requête prend du temps avant que le stream commence | |
resultContainer.innerHTML = ''; | |
const reader = response.body.getReader(); | |
const decoder = new TextDecoder(); | |
let buffer = ''; // Pour gérer les messages SSE coupés entre les chunks | |
while (true) { | |
const { done, value } = await reader.read(); | |
if (done) break; | |
buffer += decoder.decode(value, { stream: true }); | |
// Traiter les messages complets dans le buffer | |
let boundary = buffer.indexOf('\n\n'); | |
while (boundary !== -1) { | |
const message = buffer.substring(0, boundary); | |
buffer = buffer.substring(boundary + 2); // +2 pour \n\n | |
if (message.startsWith('data: ')) { | |
try { | |
const jsonData = JSON.parse(message.substring(6)); | |
processSseData(jsonData); | |
} catch (e) { | |
console.error('Erreur parsing JSON du SSE:', e, 'Data:', message.substring(6)); | |
} | |
} | |
// Rechercher la prochaine limite | |
boundary = buffer.indexOf('\n\n'); | |
} | |
// Faire défiler vers le bas | |
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); | |
} | |
// Traiter ce qui reste dans le buffer (au cas où le stream se termine sans \n\n final) | |
if (buffer.startsWith('data: ')) { | |
try { | |
const jsonData = JSON.parse(buffer.substring(6)); | |
processSseData(jsonData); | |
} catch (e) { | |
console.error('Erreur parsing JSON du dernier chunk SSE:', e, 'Data:', buffer.substring(6)); | |
} | |
} | |
} catch (error) { | |
console.error('Erreur Fetch Stream:', error); | |
resultContainer.innerHTML = ''; // Nettoyer les indicateurs | |
resultContainer.appendChild(createErrorElement(error.message)); | |
} finally { | |
// Optionnel: Supprimer l'indicateur de chargement final s'il est toujours là | |
const loadingIndicator = resultContainer.querySelector('[data-indicator-type="loading"]'); | |
if (loadingIndicator) loadingIndicator.remove(); | |
} | |
} | |
// Gestion du formulaire pour /solve | |
const solveForm = document.getElementById('solve-form'); | |
if (solveForm) { | |
solveForm.addEventListener('submit', function(e) { | |
e.preventDefault(); | |
const formData = new FormData(solveForm); | |
setupFetchStream('/solve', formData); | |
}); | |
} | |
// Gestion du formulaire pour /solved | |
const solvedForm = document.getElementById('solved-form'); | |
if (solvedForm) { | |
solvedForm.addEventListener('submit', function(e) { | |
e.preventDefault(); | |
const formData = new FormData(solvedForm); | |
setupFetchStream('/solved', formData); | |
}); | |
} | |
}); | |
</script> | |
</body> | |
</html> |