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 IA</title> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.0/es5/tex-mml-chtml.js"></script> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> | |
<style> | |
body { | |
font-family: 'Arial', sans-serif; | |
background-color: #f8f9fa; | |
padding-bottom: 50px; | |
} | |
.container { | |
max-width: 800px; | |
margin: 0 auto; | |
} | |
.header { | |
text-align: center; | |
padding: 30px 0; | |
} | |
.header h1 { | |
color: #0d6efd; | |
font-weight: bold; | |
} | |
.upload-container { | |
background-color: #ffffff; | |
border-radius: 10px; | |
padding: 30px; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
margin-bottom: 30px; | |
} | |
.preview-container { | |
margin-top: 20px; | |
text-align: center; | |
} | |
#imagePreview { | |
max-width: 100%; | |
max-height: 400px; | |
display: none; | |
margin: 0 auto; | |
border-radius: 5px; | |
border: 1px solid #dee2e6; | |
} | |
.drop-zone { | |
border: 2px dashed #0d6efd; | |
border-radius: 5px; | |
padding: 50px 20px; | |
text-align: center; | |
cursor: pointer; | |
margin-bottom: 20px; | |
transition: all 0.3s; | |
} | |
.drop-zone:hover { | |
background-color: #f1f8ff; | |
} | |
.drop-zone i { | |
font-size: 48px; | |
color: #0d6efd; | |
margin-bottom: 15px; | |
} | |
.drop-zone-active { | |
background-color: #e8f4ff; | |
border-color: #0d6efd; | |
} | |
#response { | |
background-color: #ffffff; | |
border-radius: 10px; | |
padding: 20px; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
min-height: 100px; | |
white-space: pre-wrap; | |
font-size: 16px; | |
line-height: 1.6; | |
display: none; | |
} | |
.btn-primary { | |
background-color: #0d6efd; | |
border-color: #0d6efd; | |
padding: 10px 20px; | |
font-weight: 600; | |
} | |
.btn-primary:hover { | |
background-color: #0b5ed7; | |
border-color: #0a58ca; | |
} | |
.btn-secondary { | |
background-color: #6c757d; | |
border-color: #6c757d; | |
padding: 10px 20px; | |
font-weight: 600; | |
} | |
.btn-group { | |
margin-bottom: 20px; | |
} | |
#loading { | |
display: none; | |
text-align: center; | |
margin: 20px 0; | |
} | |
.spinner-border { | |
width: 3rem; | |
height: 3rem; | |
} | |
.thinking-indicator { | |
display: inline-block; | |
padding: 5px 15px; | |
background-color: #e6f7ff; | |
border-radius: 20px; | |
color: #1890ff; | |
font-weight: 500; | |
margin-bottom: 15px; | |
} | |
.response-header { | |
margin-bottom: 15px; | |
font-weight: bold; | |
font-size: 18px; | |
} | |
.model-selector { | |
margin-bottom: 20px; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="header"> | |
<h1>Résolveur d'exercices par IA</h1> | |
<p class="lead">Téléchargez une image de votre exercice pour obtenir une solution détaillée</p> | |
</div> | |
<div class="upload-container"> | |
<div class="model-selector"> | |
<label class="form-label">Choisissez un modèle :</label> | |
<div class="btn-group" role="group"> | |
<input type="radio" class="btn-check" name="modelOption" id="modelPro" value="pro" checked> | |
<label class="btn btn-outline-primary" for="modelPro">Gemini Pro (précis mais plus lent)</label> | |
<input type="radio" class="btn-check" name="modelOption" id="modelFlash" value="flash"> | |
<label class="btn btn-outline-primary" for="modelFlash">Gemini Flash (plus rapide)</label> | |
</div> | |
</div> | |
<div class="drop-zone" id="dropZone"> | |
<i class="fas fa-cloud-upload-alt"></i> | |
<p>Déposez votre image ici ou cliquez pour choisir un fichier</p> | |
<input type="file" id="fileInput" accept="image/*" style="display: none;"> | |
</div> | |
<div class="preview-container"> | |
<img id="imagePreview" alt="Aperçu de l'image"> | |
</div> | |
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-3"> | |
<button class="btn btn-secondary me-md-2" id="resetBtn" type="button" disabled>Réinitialiser</button> | |
<button class="btn btn-primary" id="submitBtn" type="button" disabled>Résoudre l'exercice</button> | |
</div> | |
</div> | |
<div id="loading"> | |
<div class="spinner-border text-primary" role="status"> | |
<span class="visually-hidden">Chargement...</span> | |
</div> | |
<p class="mt-2" id="processingText">Traitement en cours...</p> | |
</div> | |
<div id="response"></div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
const dropZone = document.getElementById('dropZone'); | |
const fileInput = document.getElementById('fileInput'); | |
const imagePreview = document.getElementById('imagePreview'); | |
const submitBtn = document.getElementById('submitBtn'); | |
const resetBtn = document.getElementById('resetBtn'); | |
const loading = document.getElementById('loading'); | |
const processingText = document.getElementById('processingText'); | |
const response = document.getElementById('response'); | |
const modelPro = document.getElementById('modelPro'); | |
const modelFlash = document.getElementById('modelFlash'); | |
let selectedFile = null; | |
// Fonctions pour la zone de dépôt | |
dropZone.addEventListener('click', () => { | |
fileInput.click(); | |
}); | |
dropZone.addEventListener('dragover', (e) => { | |
e.preventDefault(); | |
dropZone.classList.add('drop-zone-active'); | |
}); | |
dropZone.addEventListener('dragleave', () => { | |
dropZone.classList.remove('drop-zone-active'); | |
}); | |
dropZone.addEventListener('drop', (e) => { | |
e.preventDefault(); | |
dropZone.classList.remove('drop-zone-active'); | |
if (e.dataTransfer.files.length) { | |
handleFile(e.dataTransfer.files[0]); | |
} | |
}); | |
fileInput.addEventListener('change', () => { | |
if (fileInput.files.length) { | |
handleFile(fileInput.files[0]); | |
} | |
}); | |
function handleFile(file) { | |
if (!file.type.match('image.*')) { | |
alert('Veuillez sélectionner une image'); | |
return; | |
} | |
selectedFile = file; | |
const reader = new FileReader(); | |
reader.onload = (e) => { | |
imagePreview.src = e.target.result; | |
imagePreview.style.display = 'block'; | |
submitBtn.disabled = false; | |
resetBtn.disabled = false; | |
}; | |
reader.readAsDataURL(file); | |
} | |
// Réinitialiser | |
resetBtn.addEventListener('click', () => { | |
selectedFile = null; | |
fileInput.value = ''; | |
imagePreview.src = ''; | |
imagePreview.style.display = 'none'; | |
submitBtn.disabled = true; | |
resetBtn.disabled = true; | |
response.style.display = 'none'; | |
response.innerHTML = ''; | |
}); | |
// Soumettre l'image | |
submitBtn.addEventListener('click', () => { | |
if (!selectedFile) { | |
alert('Veuillez sélectionner une image'); | |
return; | |
} | |
// Préparer les données | |
const formData = new FormData(); | |
formData.append('image', selectedFile); | |
// Afficher le chargement | |
loading.style.display = 'block'; | |
processingText.textContent = 'Traitement en cours...'; | |
response.style.display = 'none'; | |
submitBtn.disabled = true; | |
// Déterminer l'endpoint en fonction du modèle choisi | |
const endpoint = modelPro.checked ? '/solve' : '/solved'; | |
// Initialiser SSE | |
const eventSource = new EventSource(endpoint); | |
// Créer FormData et effectuer la requête | |
const xhr = new XMLHttpRequest(); | |
xhr.open('POST', endpoint, true); | |
xhr.onload = function() { | |
// Gestion manuelle non nécessaire car nous utilisons SSE | |
}; | |
xhr.onerror = function() { | |
alert('Erreur lors de l\'envoi de l\'image'); | |
loading.style.display = 'none'; | |
submitBtn.disabled = false; | |
}; | |
xhr.send(formData); | |
// Configurer EventSource pour la réponse en streaming | |
const sse = new EventSource(endpoint); | |
response.innerHTML = ''; | |
response.style.display = 'block'; | |
let isThinking = false; | |
let thinkingIndicator = null; | |
sse.onmessage = function(event) { | |
const data = JSON.parse(event.data); | |
if (data.mode === "thinking" && !isThinking) { | |
isThinking = true; | |
processingText.textContent = "L'IA réfléchit..."; | |
// Ajouter l'indicateur de réflexion | |
if (!thinkingIndicator) { | |
thinkingIndicator = document.createElement('div'); | |
thinkingIndicator.className = 'thinking-indicator'; | |
thinkingIndicator.innerHTML = '<i class="fas fa-brain"></i> L\'IA réfléchit...'; | |
response.appendChild(thinkingIndicator); | |
} | |
} | |
if (data.mode === "answering") { | |
isThinking = false; | |
processingText.textContent = "L'IA répond..."; | |
// Supprimer l'indicateur de réflexion s'il existe | |
if (thinkingIndicator) { | |
thinkingIndicator.remove(); | |
thinkingIndicator = null; | |
// Ajouter un en-tête de réponse | |
const responseHeader = document.createElement('div'); | |
responseHeader.className = 'response-header'; | |
responseHeader.textContent = 'Solution :'; | |
response.appendChild(responseHeader); | |
} | |
} | |
if (data.content) { | |
const contentDiv = document.createElement('div'); | |
contentDiv.innerHTML = data.content; | |
response.appendChild(contentDiv); | |
// Rendre les équations LaTeX | |
if (window.MathJax) { | |
MathJax.typesetPromise([contentDiv]); | |
} | |
} | |
if (data.error) { | |
const errorDiv = document.createElement('div'); | |
errorDiv.className = 'alert alert-danger'; | |
errorDiv.textContent = `Erreur: ${data.error}`; | |
response.appendChild(errorDiv); | |
sse.close(); | |
loading.style.display = 'none'; | |
submitBtn.disabled = false; | |
} | |
}; | |
sse.onerror = function() { | |
console.error('Erreur SSE'); | |
sse.close(); | |
loading.style.display = 'none'; | |
submitBtn.disabled = false; | |
// Afficher un message d'erreur seulement si aucune réponse n'a été reçue | |
if (response.children.length === 0 || (thinkingIndicator && response.children.length === 1)) { | |
const errorDiv = document.createElement('div'); | |
errorDiv.className = 'alert alert-danger'; | |
errorDiv.textContent = 'La connexion a été perdue ou une erreur est survenue.'; | |
response.appendChild(errorDiv); | |
} | |
}; | |
sse.onopen = function() { | |
console.log('Connexion SSE établie'); | |
}; | |
}); | |
}); | |
</script> | |
</body> | |
</html> |