|
<!DOCTYPE html> |
|
<html lang="fr"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Résolveur d'Images - Mariam</title> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/katex.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/contrib/auto-render.min.js"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/katex.min.css"> |
|
<style> |
|
:root { |
|
--primary-color: #3498db; |
|
--primary-hover: #2980b9; |
|
--secondary-color: #2ecc71; |
|
--secondary-hover: #27ae60; |
|
--background-color: #f4f7f6; |
|
--text-color: #333; |
|
--border-color: #e0e0e0; |
|
--shadow: 0 4px 15px rgba(0,0,0,0.1); |
|
--spacing-unit: 1rem; |
|
} |
|
|
|
* { |
|
box-sizing: border-box; |
|
margin: 0; |
|
padding: 0; |
|
} |
|
|
|
body { |
|
font-family: 'Segoe UI', system-ui, sans-serif; |
|
max-width: 800px; |
|
margin: 0 auto; |
|
padding: calc(var(--spacing-unit) * 2); |
|
line-height: 1.6; |
|
background-color: var(--background-color); |
|
color: var(--text-color); |
|
} |
|
|
|
.header { |
|
text-align: center; |
|
margin-bottom: calc(var(--spacing-unit) * 2); |
|
} |
|
|
|
.header h1 { |
|
font-size: 2.5rem; |
|
color: #2c3e50; |
|
margin-bottom: calc(var(--spacing-unit) * 0.5); |
|
} |
|
|
|
.header .subtitle { |
|
font-size: 1.1rem; |
|
color: #555; |
|
} |
|
|
|
.telegram-join-button-container { |
|
text-align: center; |
|
margin-bottom: calc(var(--spacing-unit) * 2); |
|
} |
|
|
|
.telegram-button { |
|
display: inline-block; |
|
background-color: #0088cc; |
|
color: white; |
|
padding: var(--spacing-unit) calc(var(--spacing-unit) * 2); |
|
border-radius: 0.5rem; |
|
text-decoration: none; |
|
transition: all 0.3s ease; |
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
|
} |
|
|
|
.telegram-button:hover { |
|
transform: translateY(-2px); |
|
background-color: #006699; |
|
} |
|
|
|
.container { |
|
background-color: white; |
|
padding: calc(var(--spacing-unit) * 2); |
|
border-radius: 1rem; |
|
box-shadow: var(--shadow); |
|
} |
|
|
|
.style-selection { |
|
background-color: #f9f9f9; |
|
padding: calc(var(--spacing-unit) * 1.5); |
|
border-radius: 0.75rem; |
|
border: 1px solid var(--border-color); |
|
margin-bottom: calc(var(--spacing-unit) * 1.5); |
|
} |
|
|
|
.style-selection h3 { |
|
margin-bottom: var(--spacing-unit); |
|
color: #2c3e50; |
|
font-size: 1.2rem; |
|
} |
|
|
|
.radio-group { |
|
display: flex; |
|
flex-direction: column; |
|
gap: var(--spacing-unit); |
|
} |
|
|
|
.radio-option { |
|
display: flex; |
|
align-items: flex-start; |
|
padding: calc(var(--spacing-unit) * 0.75); |
|
border-radius: 0.5rem; |
|
transition: background-color 0.2s; |
|
cursor: pointer; |
|
border: 1px solid transparent; |
|
} |
|
|
|
.radio-option:hover { |
|
background-color: #f0f4f8; |
|
border-color: var(--primary-color); |
|
} |
|
|
|
.radio-option input[type="radio"] { |
|
margin-top: 0.25rem; |
|
margin-right: calc(var(--spacing-unit) * 0.75); |
|
width: 1.25rem; |
|
height: 1.25rem; |
|
accent-color: var(--primary-color); |
|
} |
|
|
|
.radio-content { |
|
flex: 1; |
|
} |
|
|
|
.radio-label { |
|
font-weight: 500; |
|
margin-bottom: calc(var(--spacing-unit) * 0.25); |
|
display: block; |
|
} |
|
|
|
.radio-description { |
|
font-size: 0.9rem; |
|
color: #666; |
|
} |
|
|
|
.upload-section { |
|
border: 3px dashed var(--border-color); |
|
padding: calc(var(--spacing-unit) * 2); |
|
text-align: center; |
|
border-radius: 0.75rem; |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
background-color: #f8f9fa; |
|
margin: calc(var(--spacing-unit) * 1.5) 0; |
|
} |
|
|
|
.upload-section:hover { |
|
border-color: var(--primary-color); |
|
background-color: #e8f4fb; |
|
} |
|
|
|
.upload-icon { |
|
font-size: 2.5rem; |
|
margin-bottom: var(--spacing-unit); |
|
color: var(--primary-color); |
|
} |
|
|
|
#file-input { |
|
display: none; |
|
} |
|
|
|
.preview-container { |
|
margin-top: var(--spacing-unit); |
|
} |
|
|
|
#image-preview { |
|
max-width: 100%; |
|
max-height: 300px; |
|
display: none; |
|
border-radius: 0.5rem; |
|
border: 1px solid var(--border-color); |
|
} |
|
|
|
.button { |
|
width: 100%; |
|
padding: var(--spacing-unit); |
|
border: none; |
|
border-radius: 0.5rem; |
|
font-size: 1rem; |
|
cursor: pointer; |
|
transition: all 0.3s ease; |
|
margin: var(--spacing-unit) 0; |
|
background-color: var(--primary-color); |
|
color: white; |
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
|
} |
|
|
|
.button:hover:not(:disabled) { |
|
transform: translateY(-2px); |
|
background-color: var(--primary-hover); |
|
} |
|
|
|
.button:disabled { |
|
background-color: #bdc3c7; |
|
cursor: not-allowed; |
|
} |
|
|
|
.copy-button { |
|
background-color: var(--secondary-color); |
|
} |
|
|
|
.copy-button:hover { |
|
background-color: var(--secondary-hover); |
|
} |
|
|
|
#solving-container { |
|
display: none; |
|
background-color: #f9f9f9; |
|
padding: calc(var(--spacing-unit) * 1.5); |
|
border-radius: 0.75rem; |
|
border: 1px solid var(--border-color); |
|
margin-top: calc(var(--spacing-unit) * 1.5); |
|
} |
|
|
|
.status { |
|
text-align: center; |
|
margin-bottom: var(--spacing-unit); |
|
font-weight: bold; |
|
color: #2c3e50; |
|
} |
|
|
|
.status.error { color: #e74c3c; } |
|
.status.completed { color: #2ecc71; } |
|
|
|
.telegram-notice { |
|
background-color: #eaf5ff; |
|
border-left: 5px solid var(--primary-color); |
|
padding: var(--spacing-unit); |
|
margin: var(--spacing-unit) 0; |
|
border-radius: 0 0.5rem 0.5rem 0; |
|
} |
|
|
|
.response-container { |
|
display: none; |
|
margin-top: calc(var(--spacing-unit) * 1.5); |
|
padding: calc(var(--spacing-unit) * 1.5); |
|
background-color: white; |
|
border-radius: 0.75rem; |
|
border: 1px solid var(--border-color); |
|
} |
|
|
|
#response { |
|
background-color: #fdfdfd; |
|
padding: var(--spacing-unit); |
|
border-radius: 0.5rem; |
|
border: 1px solid #eee; |
|
min-height: 50px; |
|
white-space: pre-wrap; |
|
word-wrap: break-word; |
|
} |
|
|
|
.loading { |
|
text-align: center; |
|
font-style: italic; |
|
color: #555; |
|
margin: var(--spacing-unit) 0; |
|
} |
|
|
|
.loading::before { |
|
content: "⏳ "; |
|
} |
|
|
|
@media (max-width: 768px) { |
|
:root { |
|
--spacing-unit: 0.875rem; |
|
} |
|
|
|
body { |
|
padding: var(--spacing-unit); |
|
} |
|
|
|
.header h1 { |
|
font-size: 1.75rem; |
|
} |
|
|
|
.container { |
|
padding: var(--spacing-unit); |
|
} |
|
|
|
.radio-option { |
|
padding: calc(var(--spacing-unit) * 0.5); |
|
} |
|
|
|
.radio-content { |
|
font-size: 0.95rem; |
|
} |
|
|
|
.radio-description { |
|
font-size: 0.85rem; |
|
} |
|
|
|
.upload-section { |
|
padding: var(--spacing-unit); |
|
} |
|
|
|
.telegram-button { |
|
padding: calc(var(--spacing-unit) * 0.75) var(--spacing-unit); |
|
font-size: 0.95rem; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="header"> |
|
<h1>🖼️ Science (Math, Physique, Chimie) 🧠</h1> |
|
<p class="subtitle">Avec Mariam, votre assistante IA</p> |
|
</div> |
|
|
|
<div class="telegram-join-button-container"> |
|
<a href="https://t.me/+ic4zemy1E1k0MzQ0" target="_blank" class="telegram-button"> |
|
🚀 Rejoindre le Groupe Telegram pour obtenir le PDF |
|
</a> |
|
</div> |
|
|
|
<div class="container"> |
|
<div class="style-selection"> |
|
<h3>🎨 Choisissez le style de résolution</h3> |
|
<div class="radio-group"> |
|
<div class="radio-option" onclick="selectStyle('light')"> |
|
<input type="radio" id="style-light" name="resolution-style" value="light"> |
|
<div class="radio-content"> |
|
<label class="radio-label" for="style-light">📝 Résolution Light</label> |
|
<div class="radio-description">Format simple et épuré, idéal pour une lecture rapide</div> |
|
</div> |
|
</div> |
|
|
|
<div class="radio-option" onclick="selectStyle('colorful')"> |
|
<input type="radio" id="style-colorful" name="resolution-style" value="colorful" checked> |
|
<div class="radio-content"> |
|
<label class="radio-label" for="style-colorful">🌈 Résolution Colorée</label> |
|
<div class="radio-description">Format richement formaté avec couleurs, boîtes et mise en page élégante</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div id="upload-section" class="upload-section"> |
|
<div class="upload-icon">📤</div> |
|
<p>Cliquez ou glissez-déposez une image ici</p> |
|
<input type="file" id="file-input" accept="image/*"> |
|
<div class="preview-container"> |
|
<img id="image-preview" src="#" alt="Aperçu de l'image"> |
|
</div> |
|
</div> |
|
|
|
<button id="solve-button" class="button" disabled>🔍 Résoudre</button> |
|
|
|
<div id="solving-container"> |
|
<div class="status" id="status">En attente de résolution...</div> |
|
<div class="telegram-notice"> |
|
La réponse complète sera également envoyée sous forme de fichier texte sur notre groupe Telegram. |
|
</div> |
|
<div class="loading" id="loading-text">Traitement en cours...</div> |
|
<div class="response-container" id="response-container"> |
|
<h3>Réponse de Mariam :</h3> |
|
<div id="response"></div> |
|
<button id="copy-button" class="button copy-button">📋 Copier la réponse</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
const uploadSection = document.getElementById('upload-section'); |
|
const fileInput = document.getElementById('file-input'); |
|
const imagePreview = document.getElementById('image-preview'); |
|
const solveButton = document.getElementById('solve-button'); |
|
const solvingContainer = document.getElementById('solving-container'); |
|
const responseContainer = document.getElementById('response-container'); |
|
const responseDiv = document.getElementById('response'); |
|
const copyButton = document.getElementById('copy-button'); |
|
const statusElement = document.getElementById('status'); |
|
const loadingText = document.getElementById('loading-text'); |
|
|
|
let selectedFile = null; |
|
|
|
window.selectStyle = function(style) { |
|
document.getElementById(`style-${style}`).checked = true; |
|
}; |
|
|
|
uploadSection.addEventListener('click', () => fileInput.click()); |
|
|
|
uploadSection.addEventListener('dragover', (e) => { |
|
e.preventDefault(); |
|
uploadSection.style.borderColor = 'var(--primary-color)'; |
|
uploadSection.style.backgroundColor = '#e8f4fb'; |
|
}); |
|
|
|
uploadSection.addEventListener('dragleave', () => { |
|
uploadSection.style.borderColor = 'var(--border-color)'; |
|
uploadSection.style.backgroundColor = '#f8f9fa'; |
|
}); |
|
|
|
uploadSection.addEventListener('drop', (e) => { |
|
e.preventDefault(); |
|
uploadSection.style.borderColor = 'var(--border-color)'; |
|
uploadSection.style.backgroundColor = '#f8f9fa'; |
|
|
|
if (e.dataTransfer.files.length) { |
|
handleFileSelection(e.dataTransfer.files[0]); |
|
} |
|
}); |
|
|
|
fileInput.addEventListener('change', (e) => { |
|
if (e.target.files.length) { |
|
handleFileSelection(e.target.files[0]); |
|
} |
|
}); |
|
|
|
function handleFileSelection(file) { |
|
if (!file.type.startsWith('image/')) { |
|
alert('Veuillez sélectionner une image valide (format PNG, JPG, GIF, etc.)'); |
|
return; |
|
} |
|
|
|
selectedFile = file; |
|
solveButton.disabled = false; |
|
solveButton.textContent = '🔍 Résoudre'; |
|
|
|
const reader = new FileReader(); |
|
reader.onload = (e) => { |
|
imagePreview.src = e.target.result; |
|
imagePreview.style.display = 'block'; |
|
}; |
|
reader.readAsDataURL(file); |
|
|
|
solvingContainer.style.display = 'none'; |
|
responseContainer.style.display = 'none'; |
|
} |
|
|
|
solveButton.addEventListener('click', () => { |
|
if (!selectedFile) return; |
|
|
|
const selectedStyle = document.querySelector('input[name="resolution-style"]:checked').value; |
|
|
|
solveButton.disabled = true; |
|
solveButton.textContent = '⏳ Traitement...'; |
|
solvingContainer.style.display = 'block'; |
|
responseContainer.style.display = 'none'; |
|
statusElement.className = 'status'; |
|
statusElement.textContent = 'Préparation de la requête...'; |
|
loadingText.style.display = 'block'; |
|
responseDiv.innerHTML = ''; |
|
|
|
const formData = new FormData(); |
|
formData.append('image', selectedFile); |
|
formData.append('style', selectedStyle); |
|
|
|
fetch('/solve', { |
|
method: 'POST', |
|
body: formData |
|
}) |
|
.then(response => { |
|
if (!response.ok) { |
|
return response.json().then(err => { throw new Error(err.error || `Erreur Serveur: ${response.status}`) }); |
|
} |
|
return response.json(); |
|
}) |
|
.then(data => { |
|
if (data.error) { |
|
throw new Error(data.error); |
|
} |
|
|
|
const taskId = data.task_id; |
|
statusElement.textContent = 'Traitement en arrière-plan (ID: ' + taskId + ')'; |
|
|
|
const eventSource = new EventSource('/stream/' + taskId); |
|
|
|
eventSource.onmessage = function(event) { |
|
const data = JSON.parse(event.data); |
|
|
|
if (data.error) { |
|
handleError(data.error); |
|
return; |
|
} |
|
|
|
updateStatus(data); |
|
}; |
|
|
|
eventSource.onerror = function() { |
|
eventSource.close(); |
|
handleEventSourceError(taskId); |
|
}; |
|
}) |
|
.catch(error => { |
|
handleError(error.message); |
|
}); |
|
}); |
|
|
|
function handleError(errorMessage) { |
|
statusElement.className = 'status error'; |
|
statusElement.textContent = 'Erreur:'; |
|
responseDiv.innerHTML = `<p style="color:red;">${errorMessage}</p>`; |
|
showResponse(); |
|
} |
|
|
|
function updateStatus(data) { |
|
switch(data.status) { |
|
case 'pending': |
|
statusElement.textContent = 'En file d\'attente...'; |
|
break; |
|
case 'processing': |
|
statusElement.innerHTML = '<span class="thinking">Mariam</span> traite votre image... <br><small>La réponse sera également envoyée sur Telegram.</small>'; |
|
break; |
|
case 'completed': |
|
statusElement.className = 'status completed'; |
|
statusElement.textContent = 'Traitement terminé avec succès ! 🎉'; |
|
responseDiv.innerHTML = data.response; |
|
renderMathInElement(responseDiv); |
|
showResponse(); |
|
break; |
|
case 'error': |
|
handleError(data.error || 'Une erreur inattendue est survenue.'); |
|
break; |
|
} |
|
} |
|
|
|
function showResponse() { |
|
responseContainer.style.display = 'block'; |
|
loadingText.style.display = 'none'; |
|
solveButton.disabled = false; |
|
solveButton.textContent = '🔍 Résoudre'; |
|
} |
|
|
|
function handleEventSourceError(taskId) { |
|
fetch('/task/' + taskId) |
|
.then(response => response.json()) |
|
.then(taskData => { |
|
if (taskData.status === 'completed') { |
|
updateStatus({ |
|
status: 'completed', |
|
response: taskData.response |
|
}); |
|
} else if (taskData.status === 'error' || taskData.error) { |
|
handleError(taskData.error || 'Une erreur est survenue.'); |
|
} else { |
|
handleError('La connexion au flux a été perdue. La réponse sera envoyée sur Telegram.'); |
|
} |
|
}) |
|
.catch(() => { |
|
handleError('La connexion au flux a été perdue et la récupération a échoué.'); |
|
}); |
|
} |
|
|
|
copyButton.addEventListener('click', () => { |
|
const textToCopy = responseDiv.innerText || responseDiv.textContent; |
|
navigator.clipboard.writeText(textToCopy) |
|
.then(() => { |
|
copyButton.textContent = '✅ Copié!'; |
|
setTimeout(() => { |
|
copyButton.textContent = '📋 Copier la réponse'; |
|
}, 2000); |
|
}) |
|
.catch(() => { |
|
|
|
const range = document.createRange(); |
|
range.selectNode(responseDiv); |
|
window.getSelection().removeAllRanges(); |
|
window.getSelection().addRange(range); |
|
try { |
|
document.execCommand('copy'); |
|
copyButton.textContent = '✅ Copié!'; |
|
} catch (e) { |
|
copyButton.textContent = '❌ Erreur de copie'; |
|
} |
|
window.getSelection().removeAllRanges(); |
|
setTimeout(() => { |
|
copyButton.textContent = '📋 Copier la réponse'; |
|
}, 2000); |
|
}); |
|
}); |
|
|
|
renderMathInElement(document.body, { |
|
delimiters: [ |
|
{left: '$$', right: '$$', display: true}, |
|
{left: '$', right: '$', display: false}, |
|
{left: '\\(', right: '\\)', display: false}, |
|
{left: '\\[', right: '\\]', display: true} |
|
], |
|
throwOnError: false |
|
}); |
|
}); |
|
</script> |
|
</body> |
|
</html> |
|
|