Testpdf / templates /index.html
Docfile's picture
Update templates/index.html
f96a4ec verified
<!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 & PDF - 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;
--danger-color: #e74c3c;
--danger-hover: #c0392b;
--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;
}
#file-preview-area {
margin-top: var(--spacing-unit);
display: flex;
flex-wrap: wrap;
gap: var(--spacing-unit);
justify-content: center;
}
.preview-item {
display: flex;
flex-direction: column;
align-items: center;
gap: calc(var(--spacing-unit) * 0.5);
padding: calc(var(--spacing-unit) * 0.5);
border: 1px solid var(--border-color);
border-radius: 0.5rem;
background-color: #fdfdfd;
}
.preview-item img {
max-width: 100px;
max-height: 100px;
border-radius: 0.25rem;
object-fit: cover;
}
.preview-item .pdf-icon {
font-size: 3rem;
color: var(--danger-color);
}
.preview-item span {
font-size: 0.8rem;
color: #555;
word-break: break-all;
max-width: 100px;
text-align: center;
}
.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;
}
.clear-button {
background-color: var(--danger-color);
margin-top: 0;
}
.clear-button:hover:not(:disabled) {
background-color: var(--danger-hover);
}
.copy-button {
background-color: var(--secondary-color);
}
.copy-button:hover:not(:disabled) {
background-color: var(--secondary-hover);
}
.cooldown-notice {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 0.5rem;
padding: var(--spacing-unit);
margin: var(--spacing-unit) 0;
text-align: center;
color: #856404;
font-weight: 500;
}
.cooldown-timer {
font-size: 1.2rem;
color: #d63031;
font-weight: bold;
}
#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;
}
.preview-item img {
max-width: 80px;
max-height: 80px;
}
.preview-item .pdf-icon {
font-size: 2.5rem;
}
}
</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="cooldown-notice" class="cooldown-notice" style="display: none;">
⏰ Veuillez attendre <span id="cooldown-timer" class="cooldown-timer">2:00</span> avant de pouvoir soumettre à nouveau.
</div>
<div id="upload-section" class="upload-section">
<div class="upload-icon">📤</div>
<p>Cliquez ou glissez-déposez vos images et/ou 1 fichier PDF ici</p>
<input type="file" id="file-input" accept="image/*,application/pdf" multiple>
<div id="file-preview-area">
<!-- Les aperçus des fichiers seront ajoutés ici -->
</div>
</div>
<button id="clear-files-button" class="button clear-button" style="display: none;">🗑️ Effacer les fichiers</button>
<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 filePreviewArea = document.getElementById('file-preview-area');
const solveButton = document.getElementById('solve-button');
const clearFilesButton = document.getElementById('clear-files-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');
const cooldownNotice = document.getElementById('cooldown-notice');
const cooldownTimer = document.getElementById('cooldown-timer');
let selectedFiles = [];
let cooldownEndTime = 0;
let cooldownInterval = null;
// Vérifier le cooldown au chargement de la page
checkCooldownOnLoad();
window.selectStyle = function(style) {
document.getElementById(`style-${style}`).checked = true;
};
function checkCooldownOnLoad() {
const savedCooldownEndTime = localStorage.getItem('mariamCooldownEndTime');
if (savedCooldownEndTime) {
const endTime = parseInt(savedCooldownEndTime);
const now = Date.now();
if (now < endTime) {
cooldownEndTime = endTime;
startCooldownTimer();
} else {
localStorage.removeItem('mariamCooldownEndTime');
}
}
}
function startCooldown() {
cooldownEndTime = Date.now() + (2 * 60 * 1000); // 2 minutes
localStorage.setItem('mariamCooldownEndTime', cooldownEndTime.toString());
startCooldownTimer();
}
function startCooldownTimer() {
cooldownNotice.style.display = 'block';
solveButton.disabled = true;
cooldownInterval = setInterval(() => {
const now = Date.now();
const remainingTime = Math.max(0, cooldownEndTime - now);
if (remainingTime <= 0) {
clearInterval(cooldownInterval);
cooldownNotice.style.display = 'none';
localStorage.removeItem('mariamCooldownEndTime');
updateButtonsState(); // Réactiver le bouton si des fichiers sont sélectionnés
return;
}
const minutes = Math.floor(remainingTime / 60000);
const seconds = Math.floor((remainingTime % 60000) / 1000);
cooldownTimer.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
}, 1000);
}
function isCooldownActive() {
return Date.now() < cooldownEndTime;
}
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);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length) {
handleFileSelection(e.target.files);
}
});
function handleFileSelection(files) {
const newFiles = Array.from(files);
let pdfAlreadySelected = selectedFiles.some(f => f.type === 'application/pdf');
newFiles.forEach(file => {
if (file.type.startsWith('image/')) {
if (!selectedFiles.some(sf => sf.name === file.name && sf.size === file.size)) {
selectedFiles.push(file);
}
} else if (file.type === 'application/pdf') {
if (!pdfAlreadySelected) {
selectedFiles = selectedFiles.filter(f => f.type !== 'application/pdf');
selectedFiles.push(file);
pdfAlreadySelected = true;
} else {
alert('Vous ne pouvez sélectionner qu\'un seul fichier PDF.');
}
} else {
alert(`Le fichier "${file.name}" n'est pas une image ou un PDF valide et sera ignoré.`);
}
});
updateFilePreviews();
updateButtonsState();
solvingContainer.style.display = 'none';
responseContainer.style.display = 'none';
}
function updateFilePreviews() {
filePreviewArea.innerHTML = '';
if (selectedFiles.length === 0) {
filePreviewArea.style.display = 'none';
return;
}
filePreviewArea.style.display = 'flex';
selectedFiles.forEach(file => {
const previewItem = document.createElement('div');
previewItem.classList.add('preview-item');
const fileNameSpan = document.createElement('span');
fileNameSpan.textContent = file.name.length > 15 ? file.name.substring(0,12) + "..." : file.name;
if (file.type.startsWith('image/')) {
const img = document.createElement('img');
const reader = new FileReader();
reader.onload = (e) => {
img.src = e.target.result;
};
reader.readAsDataURL(file);
previewItem.appendChild(img);
} else if (file.type === 'application/pdf') {
const pdfIcon = document.createElement('div');
pdfIcon.classList.add('pdf-icon');
pdfIcon.textContent = '📄';
previewItem.appendChild(pdfIcon);
}
previewItem.appendChild(fileNameSpan);
filePreviewArea.appendChild(previewItem);
});
}
function updateButtonsState() {
if (selectedFiles.length > 0 && !isCooldownActive()) {
solveButton.disabled = false;
solveButton.textContent = `🔍 Résoudre (${selectedFiles.length} fichier(s))`;
clearFilesButton.style.display = 'block';
} else if (selectedFiles.length > 0 && isCooldownActive()) {
solveButton.disabled = true;
solveButton.textContent = `🔍 Résoudre (${selectedFiles.length} fichier(s))`;
clearFilesButton.style.display = 'block';
} else {
solveButton.disabled = true;
solveButton.textContent = '🔍 Résoudre';
clearFilesButton.style.display = 'none';
}
}
clearFilesButton.addEventListener('click', () => {
selectedFiles = [];
fileInput.value = '';
updateFilePreviews();
updateButtonsState();
solvingContainer.style.display = 'none';
responseContainer.style.display = 'none';
});
solveButton.addEventListener('click', () => {
if (selectedFiles.length === 0 || isCooldownActive()) return;
const selectedStyle = document.querySelector('input[name="resolution-style"]:checked').value;
// Démarrer le cooldown immédiatement
startCooldown();
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();
selectedFiles.forEach(file => {
formData.append('user_files', file);
});
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);
eventSource.close();
return;
}
updateStatus(data);
if (data.status === 'completed' || data.status === 'error') {
eventSource.close();
}
};
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 vos fichiers... <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 = '<p style="color: #2ecc71; font-size: 1.2rem; text-align: center; font-weight: bold;">📄 Votre PDF est disponible sur Telegram</p>';
showResponse();
break;
case 'error':
handleError(data.error || 'Une erreur inattendue est survenue.');
break;
}
}
function showResponse() {
responseContainer.style.display = 'block';
loadingText.style.display = 'none';
}
function handleEventSourceError(taskId) {
fetch('/task/' + taskId)
.then(response => {
if (!response.ok) {
throw new Error(`Erreur serveur lors de la récupération de la tâche: ${response.status}`);
}
return response.json();
})
.then(taskData => {
if (taskData.status === 'completed' && taskData.response) {
updateStatus({
status: 'completed',
response: taskData.response
});
} else if (taskData.status === 'error' || taskData.error) {
handleError(taskData.error || 'Une erreur est survenue lors du traitement de la tâche.');
} else {
handleError('La connexion au flux a été perdue. Vérifiez Telegram ou réessayez plus tard.');
}
})
.catch((err) => {
console.error("Erreur lors de la récupération de l'état de la tâche:", err);
handleError('La connexion au flux a été perdue et la récupération de l\'état final a échoué. La réponse pourrait être sur Telegram.');
});
}
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>