Spaces:
Running
Running
<html lang="fr"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Math Solver - Version Gratuite</title> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet"> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/atom-one-dark.min.css" rel="stylesheet"> | |
<!-- PAS BESOIN de marked.js pour cette approche --> | |
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.3.0/marked.min.js"></script> --> | |
<!-- CSS pour KaTeX --> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.8/katex.min.css"> | |
<style> | |
/* --- Styles CSS (inchangés par rapport à la version précédente) --- */ | |
:root { | |
--primary-color: #4a6fa5; | |
--secondary-color: #166088; | |
--accent-color: #4fc3f7; | |
--background-color: #f8f9fa; | |
--text-color: #333; | |
--box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
--code-bg: #2c323c; | |
--output-bg: #f1f8f9; | |
} | |
body { | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
line-height: 1.6; | |
margin: 0; | |
padding: 0; | |
background-color: var(--background-color); | |
color: var(--text-color); | |
} | |
.container { | |
max-width: 1000px; | |
margin: 0 auto; | |
padding: 20px; | |
} | |
header { | |
text-align: center; | |
padding: 20px 0; | |
margin-bottom: 30px; | |
} | |
.logo { | |
font-size: 2.5rem; | |
font-weight: bold; | |
color: var(--primary-color); | |
margin-bottom: 10px; | |
} | |
.subtitle { | |
font-size: 1.2rem; | |
color: var(--secondary-color); | |
margin-bottom: 20px; | |
} | |
.content-box { | |
background-color: white; | |
border-radius: 10px; | |
box-shadow: var(--box-shadow); | |
padding: 30px; | |
margin-bottom: 30px; | |
/* text-align: center; enlevé pour que le contenu dynamique ne soit pas centré par défaut */ | |
} | |
/* Ajustement pour le contenu généré */ | |
.content-box > h1, .content-box > p:not(#uploadStatus) { | |
text-align: center; | |
} | |
.content-box .feature-list { | |
text-align: left; /* Garder le texte des features aligné à gauche */ | |
max-width: 600px; | |
margin: 30px auto; | |
} | |
.content-box .feature-list h2 { | |
text-align: center; | |
} | |
.content-box .upload-section { | |
text-align: center; | |
margin-top: 30px; /* Ajouter de l'espace avant */ | |
} | |
.content-box .upgrade-section { | |
text-align: center; | |
margin-top: 30px; | |
padding: 20px; | |
border-top: 1px solid #ddd; | |
} | |
h1 { | |
color: var(--primary-color); | |
margin-top: 0; | |
} | |
.feature-list { | |
list-style-type: none; | |
padding: 0; | |
} | |
.feature-list li { | |
padding: 10px 0; | |
margin-bottom: 10px; | |
display: flex; | |
align-items: center; | |
} | |
.feature-list i { | |
color: var(--accent-color); | |
margin-right: 10px; | |
font-size: 1.2rem; | |
width: 20px; /* Donner une largeur fixe pour l'alignement */ | |
text-align: center; | |
} | |
.feature-list .fa-times-circle { | |
color: #aaa; /* Griser l'icône aussi */ | |
} | |
.cta-button { | |
display: inline-block; | |
background-color: var(--primary-color); | |
color: white; | |
padding: 12px 25px; | |
border-radius: 5px; | |
text-decoration: none; | |
font-weight: bold; | |
transition: all 0.3s ease; | |
margin: 10px; /* Ajustement marge */ | |
border: none; | |
cursor: pointer; | |
} | |
.cta-button:hover { | |
background-color: var(--secondary-color); | |
transform: translateY(-2px); | |
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); | |
} | |
footer { | |
text-align: center; | |
padding: 20px 0; | |
color: #666; | |
font-size: 0.9rem; | |
} | |
#solutionOutput { | |
margin-top: 30px; | |
text-align: left; | |
display: none; /* Initialement caché */ | |
} | |
#solutionOutput h3 { | |
text-align: center; /* Centrer le titre "Solution :" */ | |
color: var(--primary-color); | |
margin-bottom: 20px; | |
} | |
#solution { | |
background: transparent; /* Le fond sera sur les divs enfants */ | |
padding: 0; | |
border-radius: 8px; | |
text-align: left; | |
line-height: 1.8; | |
font-size: 16px; | |
box-shadow: var(--box-shadow); | |
overflow: hidden; /* Pour que les border-radius s'appliquent aux enfants */ | |
margin-top: 20px; /* Espace entre indicateur et contenu */ | |
} | |
/* Style de base pour tous les blocs dans #solution */ | |
/* Utiliser 'section' pour les blocs pour plus de sémantique */ | |
#solution > section { | |
padding: 20px; | |
margin: 0; | |
border-radius: 0; /* Sera ajusté pour first/last child */ | |
overflow-x: auto; /* Permet le scroll horizontal si contenu trop large */ | |
background-color: #f9f9f9; /* Fond par défaut */ | |
border-bottom: 1px solid #eee; /* Séparateur par défaut */ | |
/* Ajout pour débordement horizontal */ | |
word-wrap: break-word; /* Pour casser les mots longs si nécessaire */ | |
overflow-wrap: break-word; /* Standard CSS */ | |
} | |
.code-section { | |
background-color: transparent ; /* Le fond est géré par header/content */ | |
padding: 0 ; | |
border-bottom: none ; /* Le content aura la bordure */ | |
} | |
.output-section { | |
background-color: var(--output-bg) ; | |
border-left: 4px solid var(--secondary-color); /* Distinction visuelle */ | |
padding-left: calc(20px - 4px); | |
} | |
.step-section { | |
background-color: #ffffff; /* Fond blanc pour les étapes normales */ | |
border-left: 4px solid var(--primary-color); | |
padding-left: calc(20px - 4px); /* Compenser la bordure */ | |
font-size: 16px; | |
line-height: 1.8; | |
} | |
/* --- Amélioration du rendu KaTeX --- */ | |
.step-section .katex { | |
font-size: 1.1em; /* Légèrement plus grand que le texte normal */ | |
line-height: normal; /* Laisser KaTeX gérer */ | |
text-indent: 0; | |
/* text-rendering: optimizeLegibility; */ /* Peut améliorer mais coûteux */ | |
color: #333; /* Assurer la couleur du texte */ | |
/* display: inline-block; */ /* Peut causer des problèmes de wrapping */ | |
/* white-space: normal; */ /* Assurer le retour à la ligne si nécessaire */ | |
} | |
.step-section .katex-display { | |
display: block; /* Comportement block pour $$...$$ */ | |
text-align: center; /* Centrer les équations display */ | |
margin: 1em 0; /* Espacement vertical */ | |
overflow-x: auto; /* Permet le scroll horizontal si équation trop large */ | |
overflow-y: hidden; | |
padding: 0.5em 0.2em; /* Un peu d'espace interne, même horizontal */ | |
background-color: #fdfdfd; /* Fond très légèrement différent pour les blocs */ | |
border-radius: 4px; | |
} | |
/* Conteneur interne créé par KaTeX */ | |
.step-section .katex-display > .katex { | |
display: inline-block; /* Permet le centrage et évite la pleine largeur */ | |
text-align: initial; /* Texte de l'équation aligné à gauche par défaut */ | |
max-width: 100%; | |
/* white-space: nowrap; */ /* Empêche le retour à la ligne DANS l'équation, utiliser scroll */ | |
} | |
/* --- Fin Amélioration KaTeX --- */ | |
.code-header { | |
background-color: #343a40; | |
color: white; | |
padding: 10px 15px; | |
font-size: 14px; | |
font-family: 'Courier New', monospace; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.code-content { | |
margin: 0; | |
padding: 15px; | |
background-color: var(--code-bg); | |
color: #e6e6e6; | |
overflow-x: auto; | |
font-family: 'Courier New', monospace; | |
font-size: 14px; | |
line-height: 1.5; | |
border-bottom: 1px solid #444; /* Bordure sous le code */ | |
} | |
/* --- Règles pour les coins arrondis (simplifié) --- */ | |
#solution > section:first-child, | |
#solution > section.code-section:first-child .code-header { | |
border-top-left-radius: 8px; | |
border-top-right-radius: 8px; | |
} | |
#solution > section:last-child, | |
#solution > section.code-section:last-child .code-content { | |
border-bottom-left-radius: 8px; | |
border-bottom-right-radius: 8px; | |
border-bottom: none; /* Pas de bordure en bas du dernier élément */ | |
} | |
#solution > section:only-child { | |
border-radius: 8px; | |
} | |
/* --- Indicateurs --- */ | |
.thinking-indicator, .executing-indicator, .answering-indicator { | |
display: flex; | |
align-items: center; | |
justify-content: center; /* Centrer le contenu */ | |
padding: 12px 15px; | |
margin: 0 auto 20px auto; /* Centrer l'indicateur lui-même */ | |
border-radius: 8px; | |
font-size: 0.95rem; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | |
max-width: 400px; /* Limiter la largeur */ | |
} | |
.thinking-indicator { background-color: #e3f2fd; color: #1565c0; border: 1px solid #bbdefb; } | |
.executing-indicator { background-color: #ede7f6; color: #5e35b1; border: 1px solid #d1c4e9; } | |
.answering-indicator { background-color: #e8f5e9; color: #2e7d32; border: 1px solid #c8e6c9; } | |
.indicator-icon { | |
margin-right: 12px; | |
animation: pulse 1.5s infinite ease-in-out; | |
font-size: 1.1rem; | |
} | |
@keyframes pulse { | |
0% { opacity: 0.6; transform: scale(1); } | |
50% { opacity: 1; transform: scale(1.05); } | |
100% { opacity: 0.6; transform: scale(1); } | |
} | |
/* --- Classe pour les erreurs --- */ | |
.error-message { | |
background-color: #fff0f0 ; | |
color: red ; | |
border-color: red ; | |
font-weight: bold; | |
} | |
.error-message code { /* Code dans les messages d'erreur */ | |
background-color: #fdd; | |
color: #c00; | |
padding: 2px 4px; | |
border-radius: 3px; | |
font-family: monospace; | |
} | |
/* Styles spécifiques pour le contenu sans Markdown (peut nécessiter ajustement) */ | |
.step-section p { /* Ajouter marge basique pour les paragraphes si pas de Markdown */ | |
margin-bottom: 1em; | |
} | |
.step-section p:last-child { | |
margin-bottom: 0; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<header> | |
<div class="logo">Math Solver</div> | |
<div class="subtitle">La solution intelligente pour vos problèmes mathématiques</div> | |
</header> | |
<div class="content-box"> | |
<h1>Version Gratuite</h1> | |
<p>Vous utilisez actuellement la version gratuite de Math Solver qui vous permet de résoudre 3 problèmes par jour.</p> | |
<div class="feature-list"> | |
<h2>Fonctionnalités disponibles :</h2> | |
<!-- Liste des fonctionnalités --> | |
<ul> | |
<li><i class="fas fa-check-circle"></i> Résolution de problèmes mathématiques basiques</li> | |
<li><i class="fas fa-check-circle"></i> 3 résolutions gratuites par jour</li> | |
<li><i class="fas fa-check-circle"></i> Explication des étapes de résolution</li> | |
<li><i class="fas fa-times-circle"></i> <span style="color: #999;">Mode d'exécution de code avancé (version Pro)</span></li> | |
<li><i class="fas fa-times-circle"></i> <span style="color: #999;">Résolutions illimitées (version Pro)</span></li> | |
<li><i class="fas fa-times-circle"></i> <span style="color: #999;">Support prioritaire (version Pro)</span></li> | |
</ul> | |
</div> | |
<div class="upload-section"> | |
<h2>Soumettez votre problème</h2> | |
<form id="imageForm" enctype="multipart/form-data"> | |
<input type="file" id="imageInput" name="image" accept="image/*" style="display: none;"> | |
<button type="button" id="uploadButton" class="cta-button" onclick="document.getElementById('imageInput').click()"> | |
<i class="fas fa-upload"></i> Télécharger une image | |
</button> | |
<p id="uploadStatus" style="margin-top: 10px; font-size: 0.9em; color: #555; min-height: 1.2em;"></p> | |
</form> | |
<div id="imagePreview" style="display: none; margin: 20px auto; max-width: 400px; max-height: 300px; overflow: hidden; border: 1px solid #ddd; border-radius: 8px;"> | |
<img id="preview" style="display: block; width: 100%; height: auto; border-radius: 8px;"> | |
</div> | |
<button id="solveButton" class="cta-button" style="display: none; background-color: var(--secondary-color);"> | |
<i class="fas fa-calculator"></i> Résoudre ce problème | |
</button> | |
</div> | |
<!-- Section pour afficher la solution --> | |
<div id="solutionOutput"> <!-- Initialement caché via CSS --> | |
<h3>Solution :</h3> | |
<div id="loadingIndicator" class="thinking-indicator" style="display: none;"> | |
<i class="fas fa-brain indicator-icon"></i> | |
<span>Je réfléchis au problème...</span> | |
</div> | |
<!-- Container pour les blocs de contenu dynamiques --> | |
<div id="solution"> | |
<!-- Le contenu (steps, code, output) sera ajouté ici par JS --> | |
</div> | |
</div> | |
<div class="upgrade-section"> | |
<h2>Besoin de plus de puissance ?</h2> | |
<p>Passez à la version Pro pour des fonctionnalités avancées et des résolutions illimitées.</p> | |
<a href="#" class="cta-button">Passer à la version Pro</a> | |
</div> | |
</div> | |
<footer> | |
<p>© 2025 Math Solver. Tous droits réservés.</p> | |
</footer> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/python.min.js"></script> | |
<!-- KaTeX Scripts --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.8/katex.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.8/contrib/auto-render.min.js"></script> | |
<script> | |
// Configuration de KaTeX (options globales) | |
const katexOptions = { | |
delimiters: [ | |
{left: '$$', right: '$$', display: true}, | |
{left: '$', right: '$', display: false}, | |
{left: '\\(', right: '\\)', display: false}, | |
{left: '\\[', right: '\\]', display: true} | |
], | |
throwOnError: false, // Ne pas bloquer toute la page en cas d'erreur LaTeX | |
strict: (errorCode, errorMsg, token) => { | |
// Log plus détaillé des erreurs KaTeX pour le débogage | |
// console.warn(`KaTeX ${errorCode}: ${errorMsg}`, token); | |
if (errorCode === 'unicodeTextInMathMode') { | |
return 'ignore'; // Permet certains caractères unicode si besoin | |
} | |
return 'warn'; // Affiche les autres erreurs dans la console sans planter | |
}, | |
// IMPORTANT: Pour autoriser le rendu dans différentes situations | |
// trust: true peut être nécessaire si le contenu vient d'une source | |
// non sûre et utilise des commandes spécifiques, mais essayons sans d'abord. | |
// trust: true, | |
output: 'htmlAndMathml' // Améliore l'accessibilité | |
}; | |
// Fonction pour déclencher le rendu KaTeX sur un élément spécifique | |
// En utilisant setTimeout pour s'assurer que le DOM est mis à jour | |
function triggerKatexRender(element) { | |
// Utiliser setTimeout pour décaler légèrement l'exécution | |
setTimeout(() => { | |
try { | |
if (window.renderMathInElement) { | |
window.renderMathInElement(element, katexOptions); | |
} else { | |
console.error("renderMathInElement n'est pas disponible. Vérifiez le chargement de KaTeX auto-render."); | |
} | |
} catch (e) { | |
console.error('Erreur lors du rendu KaTeX sur élément:', e, element); | |
} | |
}, 0); // 0 ms de délai suffit généralement | |
} | |
document.getElementById('imageInput').addEventListener('change', function(event) { | |
const file = event.target.files[0]; | |
if (file) { | |
const reader = new FileReader(); | |
reader.onload = function(e) { | |
document.getElementById('preview').src = e.target.result; | |
document.getElementById('imagePreview').style.display = 'block'; | |
document.getElementById('solveButton').style.display = 'inline-block'; | |
document.getElementById('uploadStatus').textContent = `Image sélectionnée : ${file.name}`; | |
document.getElementById('solutionOutput').style.display = 'none'; | |
document.getElementById('solution').innerHTML = ''; | |
} | |
reader.readAsDataURL(file); | |
} | |
}); | |
document.getElementById('solveButton').addEventListener('click', function() { | |
const formData = new FormData(document.getElementById('imageForm')); | |
const solutionOutputDiv = document.getElementById('solutionOutput'); | |
const loadingIndicator = document.getElementById('loadingIndicator'); | |
const solutionContainer = document.getElementById('solution'); | |
solutionOutputDiv.style.display = 'block'; | |
loadingIndicator.style.display = 'flex'; | |
solutionContainer.innerHTML = ''; | |
loadingIndicator.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
fetch('/solved', { // Endpoint Flask | |
method: 'POST', | |
body: formData | |
}) | |
.then(response => { | |
if (!response.ok) { | |
return response.text().then(text => { | |
throw new Error(`Erreur serveur: ${response.status} ${response.statusText}\n${text || '(Aucun détail)'}`); | |
}); | |
} | |
const reader = response.body.getReader(); | |
const decoder = new TextDecoder(); | |
let buffer = ''; | |
function processStream({ done, value }) { | |
if (done) { | |
loadingIndicator.style.display = 'none'; | |
// Passe finale de rendu KaTeX pour être sûr (peut-être redondant mais sûr) | |
triggerKatexRender(solutionContainer); | |
return; | |
} | |
buffer += decoder.decode(value, { stream: true }); | |
const messages = buffer.split(/\r?\n\r?\n/); | |
buffer = messages.pop(); | |
messages.forEach(message => { | |
if (message.startsWith('data: ')) { | |
try { | |
const data = JSON.parse(message.substring(6)); | |
// --- Gérer les mises à jour de mode --- | |
if (data.mode) { | |
const modes = { | |
thinking: { icon: 'fa-brain', text: 'Je réfléchis...', class: 'thinking-indicator' }, | |
answering: { icon: 'fa-pencil-alt', text: 'Je rédige la réponse...', class: 'answering-indicator' }, | |
executing_code: { icon: 'fa-play', text: 'Exécution du code...', class: 'executing-indicator' }, | |
code_result: { icon: 'fa-terminal', text: 'Traitement des résultats...', class: 'executing-indicator' } | |
}; | |
const modeInfo = modes[data.mode] || { icon: 'fa-sync-alt fa-spin', text: 'Traitement...', class: 'thinking-indicator' }; | |
loadingIndicator.className = `${modeInfo.class}`; | |
loadingIndicator.innerHTML = `<i class="fas ${modeInfo.icon} indicator-icon"></i><span>${modeInfo.text}</span>`; | |
loadingIndicator.style.display = 'flex'; | |
} | |
// --- Gérer les blocs de contenu --- | |
if (data.content !== undefined && data.content !== null) { // Vérifier existence et non-nullité | |
loadingIndicator.style.display = 'none'; | |
const content = data.content; | |
// Créer un élément conteneur générique (section) | |
const blockElement = document.createElement('section'); | |
// Analyser le fragment HTML reçu pour détecter le type de bloc | |
const tempDiv = document.createElement('div'); | |
tempDiv.innerHTML = content; | |
const codeSection = tempDiv.querySelector('.code-section'); | |
const outputSection = tempDiv.querySelector('.output-section'); | |
if (codeSection) { | |
// C'est une section de code Python dédiée | |
// Remplacer le contenu de blockElement par codeSection | |
blockElement.outerHTML = codeSection.outerHTML; | |
// Il faut récupérer la référence au nouvel élément ajouté | |
const addedCodeSection = solutionContainer.appendChild(blockElement.cloneNode(true)); | |
addedCodeSection.querySelectorAll('pre code').forEach((block) => { | |
hljs.highlightElement(block); | |
}); | |
// Déclencher KaTeX au cas où il y aurait du LaTeX dans les commentaires du code (?) | |
// triggerKatexRender(addedCodeSection); // Probablement pas nécessaire pour le code pur | |
} else if (outputSection) { | |
// C'est une section de sortie de code | |
blockElement.outerHTML = outputSection.outerHTML; | |
const addedOutputSection = solutionContainer.appendChild(blockElement.cloneNode(true)); | |
// Déclencher KaTeX si la sortie peut contenir du LaTeX | |
triggerKatexRender(addedOutputSection); | |
} else { | |
// C'est une section d'étape (texte + LaTeX) | |
// **Approche simplifiée : injecter le HTML brut** | |
blockElement.className = 'step-section'; // Appliquer la classe pour le style | |
blockElement.innerHTML = content; // Injecter directement | |
solutionContainer.appendChild(blockElement); | |
// **Déclencher le rendu KaTeX sur ce bloc spécifique** | |
triggerKatexRender(blockElement); | |
} | |
} | |
// --- Gérer les erreurs spécifiques --- | |
if (data.error) { | |
const errorElement = document.createElement('section'); | |
errorElement.className = 'step-section error-message'; // Utiliser classe d'erreur | |
// Utiliser innerHTML pour interpréter d'éventuelles balises simples (gras, etc.) | |
// ou juste textContent si l'erreur est toujours du texte brut. | |
errorElement.innerHTML = `<strong>Erreur :</strong> ${data.error || 'Une erreur inconnue est survenue.'}`; | |
solutionContainer.appendChild(errorElement); | |
loadingIndicator.style.display = 'none'; | |
} | |
} catch (e) { | |
console.error('Erreur analyse JSON ou traitement message SSE:', e, message); | |
const errorElement = document.createElement('section'); | |
errorElement.className = 'step-section error-message'; | |
errorElement.style.color = 'orange'; // Erreur de parsing client | |
errorElement.style.backgroundColor = '#fff8e1'; | |
errorElement.style.borderColor = 'orange'; | |
errorElement.textContent = `Erreur de traitement du message reçu: ${message.substring(0, 150)}... (Voir console pour détails)`; | |
solutionContainer.appendChild(errorElement); | |
loadingIndicator.style.display = 'none'; | |
} | |
} | |
}); | |
// Défiler vers le bas pour voir le nouveau contenu au fur et à mesure | |
solutionContainer.lastChild?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
// Continuer à lire le flux | |
return reader.read().then(processStream); | |
} | |
// Démarrer le traitement du flux | |
reader.read().then(processStream); | |
}) | |
.catch(error => { | |
console.error('Erreur Fetch ou connexion:', error); | |
const errorElement = document.createElement('section'); | |
errorElement.className = 'step-section error-message'; | |
errorElement.innerHTML = `<strong>Erreur de connexion ou serveur :</strong> ${error.message || error}`; | |
solutionContainer.appendChild(errorElement); | |
loadingIndicator.style.display = 'none'; | |
errorElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
}); | |
}); | |
// Premier rendu KaTeX au chargement (pour contenu statique éventuel) | |
document.addEventListener('DOMContentLoaded', function() { | |
// Inutile de le faire sur tout le body si le contenu est dynamique | |
// Mais on peut le laisser au cas où il y aurait du LaTeX statique | |
// triggerKatexRender(document.body); | |
console.log("DOM chargé, KaTeX auto-render prêt."); | |
}); | |
</script> | |
</body> | |
</html> |