Maths / templates /math.html
Docfile's picture
Create math.html
e17df7c verified
raw
history blame
26.1 kB
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Assistant intelligent pour la résolution détaillée de problèmes mathématiques">
<title>Mariam - Résolution de Problèmes Mathématiques</title>
<!-- Preload des ressources critiques -->
<link rel="preload" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" as="style">
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" as="style">
<!-- CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* Base styles */
:root {
--primary-color: #3b82f6;
--primary-dark: #2563eb;
--error-color: #ef4444;
--success-color: #22c55e;
--border-color: #d1d5db;
}
body {
font-family: 'Poppins', system-ui, sans-serif;
background-color: #f9fafb;
}
/* Optimized dropzone */
.dropzone {
border: 3px dashed var(--primary-color);
transition: all 0.2s ease;
border-radius: 1rem;
padding: 2rem;
background-color: rgba(59, 130, 246, 0.05);
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.dropzone:hover, .dropzone.drag-active {
border-color: var(--primary-dark);
background-color: rgba(59, 130, 246, 0.1);
transform: scale(1.01);
}
/* Enhanced button styles */
.btn {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: var(--primary-dark);
transform: translateY(-1px);
}
.btn-danger {
background-color: var(--error-color);
color: white;
}
.btn-danger:hover {
background-color: #dc2626;
transform: translateY(-1px);
}
/* Optimized math content display */
.math-content {
font-size: 1.1em;
line-height: 1.6;
overflow-x: auto;
opacity: 0;
transition: opacity 0.3s ease;
}
.math-content.visible {
opacity: 1;
}
.math-content p {
margin-bottom: 1rem;
white-space: pre-wrap;
}
/* Enhanced form controls */
.form-input {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid var(--border-color);
border-radius: 0.5rem;
transition: all 0.2s ease;
}
.form-input:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
outline: none;
}
/* Improved loading animation */
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.loading-spinner {
width: 3rem;
height: 3rem;
border: 4px solid var(--primary-color);
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Responsive adjustments */
@media (max-width: 640px) {
.container {
padding: 1rem;
}
.btn {
width: 100%;
justify-content: center;
}
.dropzone {
padding: 1.5rem;
}
}
/* Enhanced accessibility */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Toast notifications */
.toast {
position: fixed;
bottom: 1rem;
right: 1rem;
padding: 1rem;
border-radius: 0.5rem;
background: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 50;
max-width: 24rem;
transform: translateY(100%);
opacity: 0;
transition: all 0.3s ease;
}
.toast.show {
transform: translateY(0);
opacity: 1;
}
</style>
</head>
<body>
<div class="container mx-auto px-4 py-8 max-w-4xl">
<!-- Header -->
<header class="text-center mb-12">
<h1 class="text-4xl font-bold mb-4">
<span class="bg-gradient-to-r from-blue-500 to-blue-700 text-transparent bg-clip-text">Mariam</span>
- Résolution de Problèmes Mathématiques
</h1>
<p class="text-gray-600 text-lg">Assistant intelligent pour des solutions mathématiques détaillées</p>
</header>
<!-- Main Form -->
<form id="uploadForm" class="space-y-6">
<div id="dropzone" class="dropzone">
<input type="file" id="fileInput" class="sr-only" accept="image/*">
<div class="text-center">
<i class="fas fa-cloud-upload-alt text-5xl text-blue-500 mb-4"></i>
<p class="text-lg text-gray-700">
Glissez votre image ici ou <button type="button" class="text-blue-500 font-semibold hover:text-blue-700">parcourez vos fichiers</button>
</p>
<p class="text-sm text-gray-500 mt-2">Formats acceptés: PNG, JPG, JPEG</p>
</div>
</div>
<div class="space-y-4">
<div>
<label for="customInstruction" class="block text-gray-700 font-medium mb-2">
Instruction personnalisée (optionnel)
</label>
<input type="text" id="customInstruction" class="form-input"
placeholder="Exemple : Résoudre en utilisant le théorème de Pythagore">
</div>
<div class="flex flex-col sm:flex-row gap-4">
<select id="modelChoice" class="form-input">
<option value="mariam's">Mariam's (Ultra performant)</option>
<option value="qwen2">Qwen2 (lent et performant)</option>
</select>
<button type="submit" class="btn btn-primary" disabled>
<i class="fas fa-paper-plane"></i>
<span>Analyser l'image</span>
</button>
</div>
</div>
</form>
<!-- Loading State -->
<div id="loading" class="hidden mt-8 text-center">
<div class="loading-spinner mx-auto mb-4"></div>
<p class="text-gray-600 font-medium">Analyse en cours...</p>
</div>
<!-- Response Display -->
<div id="response" class="hidden mt-8">
<div class="bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-semibold text-blue-600 mb-4">
Solution (Modèle: <span id="modelName"></span>)
</h2>
<div id="latexContent" class="math-content"></div>
</div>
</div>
<!-- Saved Responses -->
<section id="savedResponsesSection" class="mt-8">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-semibold text-blue-600">Réponses Sauvegardées</h2>
<button id="clearSavedResponses" class="btn btn-danger">
<i class="fas fa-trash-alt"></i>
<span>Effacer Tout</span>
</button>
</div>
<div id="savedResponses" class="space-y-4"></div>
</section>
<!-- Toast Container -->
<div id="toastContainer"></div>
</div>
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11" defer></script>
<script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js" defer></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Initialize MathJax
window.MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']],
processEscapes: true,
macros: {
R: "{\\mathbb{R}}",
N: "{\\mathbb{N}}",
Z: "{\\mathbb{Z}}",
vecv: ["\\begin{pmatrix}#1\\\\#2\\\\#3\\end{pmatrix}", 3]
}
},
svg: {
fontCache: 'global'
}
};
// Load MathJax dynamically
const loadMathJax = () => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
script.async = true;
document.head.appendChild(script);
return new Promise(resolve => script.onload = resolve);
};
// Toast notification system
const showToast = (message, type = 'info') => {
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `
<div class="flex items-center">
<i class="fas fa-${type === 'error' ? 'exclamation-circle' : 'info-circle'} mr-2"></i>
<p>${message}</p>
</div>
`;
document.getElementById('toastContainer').appendChild(toast);
requestAnimationFrame(() => toast.classList.add('show'));
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
};
// Enhanced file handling
const handleFile = file => {
if (!file.type.startsWith('image/')) {
showToast('Veuillez sélectionner une image valide', 'error');
return false;
}
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
showToast('L\'image est trop volumineuse. Maximum 10MB.', 'error');
return false;
}
return true;
};
// Initialize the application
const init = async () => {
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
const form = document.getElementById('uploadForm');
const submitBtn = form.querySelector('button[type="submit"]');
// Enhanced drag and drop
dropzone.addEventListener('dragenter', e => {
e.preventDefault();
dropzone.classList.add('drag-active');
});
dropzone.addEventListener('dragleave', e => {
e.preventDefault();
dropzone.classList.remove('drag-active');
});
dropzone.addEventListener('dragover', e => {
e.preventDefault();
});
dropzone.addEventListener('drop', e => {
e.preventDefault();
dropzone.classList.remove('drag-active');
const file = e.dataTransfer.files[0];
if (handleFile(file)) {
fileInput.files = e.dataTransfer.files;
handleFileSelect(file);
}
});
// File selection
const handleFileSelect = file => {
const reader = new FileReader();
reader.onload = e => {
const preview = dropzone.querySelector('img') || document.createElement('img');
preview.src = e.target.result;
preview.className = 'max-h-48 mx-auto mt-4 rounded-lg';
if (!dropzone.querySelector('img')) {
dropzone.appendChild(preview);
}
submitBtn.disabled = false;
};
reader.readAsDataURL(file);
};
fileInput.addEventListener('change', e => {
const file = e.target.files[0];
if (handleFile(file)) {
handleFileSelect(file);
}
});
// Form submission
form.addEventListener('submit', async e => {
e.preventDefault();
if (!fileInput.files.length) {
showToast('Veuillez sélectionner une image', 'error');
return;
}
const formData = new FormData(form);
submitBtn.disabled = true;
try {
document.getElementById('loading').classList.remove('hidden');
document.getElementById('response').classList.add('hidden');
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'Erreur serveur');
await renderMathContent(data.result);
document.getElementById('modelName').textContent = data.model;
await saveResponse(data.result, data.model);
// Gestion des graphiques générés
if (data.image_paths?.length) {
const imageContainer = document.createElement('div');
imageContainer.className = 'mt-4 grid gap-4 grid-cols-1 sm:grid-cols-2';
for (const path of data.image_paths) {
const figure = document.createElement('figure');
figure.className = 'relative group';
const img = document.createElement('img');
img.src = `/temp/${path.split('/').pop()}`;
img.className = 'w-full h-auto rounded-lg shadow-md transition-transform hover:scale-105';
img.loading = 'lazy';
figure.appendChild(img);
imageContainer.appendChild(figure);
}
document.getElementById('latexContent').appendChild(imageContainer);
}
showToast('Analyse terminée avec succès', 'success');
} catch (error) {
console.error('Erreur:', error);
showToast(error.message || 'Une erreur est survenue', 'error');
} finally {
document.getElementById('loading').classList.add('hidden');
submitBtn.disabled = false;
}
});
// Optimized MathJax rendering
const renderMathContent = async (text) => {
const latexContent = document.getElementById('latexContent');
try {
latexContent.innerHTML = '';
latexContent.classList.remove('visible');
// Utilisation de marked avec des options de sécurité
const htmlContent = marked.parse(text, {
breaks: true,
gfm: true,
sanitize: true
});
latexContent.innerHTML = htmlContent;
await MathJax.typesetPromise([latexContent]);
document.getElementById('response').classList.remove('hidden');
requestAnimationFrame(() => latexContent.classList.add('visible'));
} catch (error) {
console.error('Erreur de rendu:', error);
showToast('Erreur lors du rendu mathématique', 'error');
latexContent.innerHTML = `
<div class="text-red-600 mb-4">Une erreur s'est produite lors du rendu. Voici le texte brut :</div>
<pre class="bg-gray-100 p-4 rounded-lg overflow-x-auto">${text}</pre>
`;
}
};
// Enhanced local storage management
const saveResponse = async (response, model) => {
try {
const timestamp = Date.now();
const key = `response-${timestamp}-${model}`;
await localforage.setItem(key, {
content: response,
model,
timestamp,
id: key
});
await loadSavedResponses();
} catch (error) {
console.error('Erreur de sauvegarde:', error);
showToast('Erreur lors de la sauvegarde', 'error');
}
};
const loadSavedResponses = async () => {
const container = document.getElementById('savedResponses');
container.innerHTML = '';
try {
const keys = await localforage.keys();
const responses = await Promise.all(
keys.map(async key => {
const data = await localforage.getItem(key);
return { ...data, key };
})
);
// Tri par date décroissante
responses.sort((a, b) => b.timestamp - a.timestamp);
for (const response of responses) {
const element = createResponseElement(response);
container.appendChild(element);
}
if (responses.length === 0) {
container.innerHTML = `
<div class="text-center text-gray-500 py-8">
<i class="fas fa-inbox text-4xl mb-4"></i>
<p>Aucune réponse sauvegardée</p>
</div>
`;
}
} catch (error) {
console.error('Erreur de chargement:', error);
showToast('Erreur lors du chargement des réponses', 'error');
}
};
const createResponseElement = (response) => {
const element = document.createElement('div');
element.className = 'bg-white rounded-lg shadow-md overflow-hidden';
const header = document.createElement('div');
header.className = 'flex items-center justify-between p-4 bg-gray-50';
const date = new Date(response.timestamp).toLocaleString('fr-FR', {
dateStyle: 'medium',
timeStyle: 'short'
});
header.innerHTML = `
<div class="flex items-center space-x-2">
<span class="font-medium text-gray-700">${date}</span>
<span class="text-sm text-gray-500">(${response.model})</span>
</div>
<div class="flex space-x-2">
<button class="btn-toggle p-2 rounded-full hover:bg-gray-200 transition-colors">
<i class="fas fa-chevron-down"></i>
</button>
<button class="btn-delete p-2 rounded-full hover:bg-red-100 text-red-500 transition-colors">
<i class="fas fa-trash-alt"></i>
</button>
</div>
`;
const content = document.createElement('div');
content.className = 'p-4 math-content hidden';
content.innerHTML = marked.parse(response.content);
element.appendChild(header);
element.appendChild(content);
// Event listeners
header.querySelector('.btn-toggle').addEventListener('click', async (e) => {
const icon = e.currentTarget.querySelector('i');
icon.classList.toggle('fa-chevron-down');
icon.classList.toggle('fa-chevron-up');
content.classList.toggle('hidden');
if (!content.classList.contains('hidden')) {
await MathJax.typesetPromise([content]);
}
});
header.querySelector('.btn-delete').addEventListener('click', async () => {
if (await confirmDelete()) {
try {
await localforage.removeItem(response.key);
element.remove();
showToast('Réponse supprimée', 'success');
} catch (error) {
console.error('Erreur de suppression:', error);
showToast('Erreur lors de la suppression', 'error');
}
}
});
return element;
};
const confirmDelete = () => {
return Swal.fire({
title: 'Confirmer la suppression',
text: 'Cette action est irréversible',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#6b7280',
confirmButtonText: 'Supprimer',
cancelButtonText: 'Annuler'
}).then(result => result.isConfirmed);
};
// Initialize
await loadMathJax();
await loadSavedResponses();
// Clear all responses
document.getElementById('clearSavedResponses').addEventListener('click', async () => {
const result = await Swal.fire({
title: 'Tout effacer ?',
text: 'Cette action supprimera toutes vos réponses sauvegardées',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#6b7280',
confirmButtonText: 'Tout effacer',
cancelButtonText: 'Annuler'
});
if (result.isConfirmed) {
try {
await localforage.clear();
await loadSavedResponses();
showToast('Toutes les réponses ont été supprimées', 'success');
} catch (error) {
console.error('Erreur de suppression:', error);
showToast('Erreur lors de la suppression', 'error');
}
}
});
};
// Start the application
init().catch(console.error);
});
</script>
</body>
</html>