Docfile commited on
Commit
1f4ef47
·
verified ·
1 Parent(s): 8df3575

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +253 -365
templates/index.html CHANGED
@@ -3,412 +3,300 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Solveur Expert IA - Maths, Physique, Chimie</title>
7
- <!-- Tailwind CSS Play CDN (v3 - JIT enabled) -->
8
  <script src="https://cdn.tailwindcss.com"></script>
9
- <!-- Google Fonts -->
10
- <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Fira+Code&display=swap" rel="stylesheet">
11
- <!-- Font Awesome Icons -->
12
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
13
  <style>
14
- /* Styles personnalisés complémentaires */
15
- :root {
16
- --primary-color: #2c3e50;
17
- --secondary-color: #1abc9c;
18
- --accent-color: #e74c3c;
19
- --success-color: #27ae60;
20
- --light-secondary-bg: #e8f8f5; /* For upload highlight */
21
  }
22
-
23
- body {
24
- font-family: 'Montserrat', sans-serif;
25
  }
26
-
27
- .font-code {
28
- font-family: 'Fira Code', monospace;
29
  }
30
-
31
- /* Gradients can be defined here or as Tailwind arbitrary values if preferred */
32
- .gradient-primary {
33
- background-image: linear-gradient(to right, var(--secondary-color) 0%, #16a085 100%);
34
  }
35
-
36
- .gradient-secondary {
37
- background-image: linear-gradient(to right, var(--primary-color) 0%, #34495e 100%);
 
 
 
 
38
  }
39
-
40
  @keyframes spin {
41
- to { transform: rotate(360deg); }
42
- }
43
-
44
- .animate-spin-custom {
45
- animation: spin 0.8s linear infinite;
46
- }
47
-
48
- /* This class is added by JS, so define its properties here */
49
- .upload-highlight {
50
- border-color: var(--secondary-color) !important; /* Use !important if Tailwind specificity is an issue */
51
- background-color: var(--light-secondary-bg) !important;
52
  }
53
  </style>
54
- <script>
55
- // Optional: Configure Tailwind if needed (e.g., extending theme colors)
56
- // For this example, we'll rely on arbitrary values for CSS variables,
57
- // which the Play CDN handles well.
58
- // tailwind.config = {
59
- // theme: {
60
- // extend: {
61
- // colors: {
62
- // 'custom-primary': 'var(--primary-color)',
63
- // 'custom-secondary': 'var(--secondary-color)',
64
- // 'custom-accent': 'var(--accent-color)',
65
- // 'custom-success': 'var(--success-color)',
66
- // }
67
- // }
68
- // }
69
- // }
70
- </script>
71
  </head>
72
- <body class="bg-gray-50 text-gray-700 min-h-screen flex flex-col items-center py-6 px-4">
73
- <div class="bg-white rounded-xl shadow-lg w-full max-w-2xl p-6 md:p-8">
74
- <h1 class="flex items-center justify-center text-3xl font-bold text-center mb-2 text-[var(--primary-color)]">
75
- <i class="fas fa-atom text-4xl mr-3 text-[var(--secondary-color)]"></i>
76
- Solveur Expert IA
77
- </h1>
78
- <p class="text-center text-gray-500 mb-8">Solutions LaTeX précises pour Maths, Physique et Chimie.</p>
79
-
80
- <div id="upload-section" class="relative border-2 border-dashed border-gray-300 rounded-xl p-8 mb-6 cursor-pointer transition-all duration-300 bg-gray-50 text-center group hover:border-[var(--secondary-color)] hover:bg-[var(--light-secondary-bg)]">
81
- <div class="upload-content">
82
- <i class="fas fa-file-arrow-up text-5xl mb-4 text-[var(--secondary-color)] transition-transform duration-300 transform group-hover:scale-110 group-hover:-translate-y-1"></i>
83
- <p class="text-lg font-medium mb-1">Déposez l'image de votre exercice ici</p>
84
- <p class="text-sm text-gray-500">ou cliquez pour sélectionner un fichier (PNG, JPG)</p>
85
- </div>
86
- <input type="file" id="file-input" accept="image/png, image/jpeg, image/webp" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
87
- <div id="image-preview-container" class="mt-4">
88
- <img id="image-preview" src="#" alt="Aperçu de l'énoncé" class="hidden max-w-full max-h-64 rounded-lg mx-auto border border-gray-300 shadow-sm">
 
 
 
 
 
 
89
  </div>
90
- </div>
91
 
92
- <div class="bg-gray-50 rounded-xl p-5 mb-6 border border-gray-200">
93
- <h3 class="flex items-center font-semibold text-[var(--primary-color)] mb-4">
94
- <i class="fas fa-cogs mr-2 text-[var(--secondary-color)]"></i>Options de Formatage
95
- </h3>
96
- <div class="prompt-selector">
97
- <label for="prompt-type" class="block font-medium mb-2">Style de la correction LaTeX :</label>
98
- <div class="relative">
99
- <select id="prompt-type" name="prompt-type" class="w-full p-3 pr-10 rounded-xl border-2 border-gray-300 appearance-none bg-white focus:outline-none focus:border-[var(--secondary-color)] focus:ring-2 focus:ring-[var(--secondary-color)]/20 transition-all">
100
- <option value="refined">Format Raffiné & Complet (mise en page avancée)</option>
101
- <option value="light">Format Léger & Essentiel (LaTeX standard)</option>
102
- </select>
103
- <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-gray-700">
104
- <i class="fas fa-chevron-down"></i>
 
 
 
105
  </div>
106
  </div>
107
  </div>
108
- </div>
109
-
110
- <button id="solve-button" class="gradient-primary w-full py-4 px-6 rounded-xl text-white font-semibold text-lg tracking-wide flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-gray-400 disabled:shadow-none transition duration-300 transform hover:translate-y-px disabled:transform-none" disabled>
111
- <i class="fas fa-rocket mr-2"></i>Obtenir la Solution
112
- </button>
113
 
114
- <a href="https://t.me/+ic4zemy1E1k0MzQ0" target="_blank" id="telegram-join-button" class="mt-4 w-full bg-blue-500 hover:bg-blue-600 text-white font-semibold py-3 px-6 rounded-xl flex items-center justify-center transition duration-300 transform hover:translate-y-px">
115
- <i class="fab fa-telegram-plane mr-2"></i>Rejoindre le groupe Telegram
116
- </a>
117
-
118
- <div id="solving-container" class="hidden mt-8">
119
- <div class="text-center mb-5">
120
- <div id="status-message-element" class="flex items-center justify-center flex-wrap text-lg font-medium text-[var(--primary-color)]">
121
- <i class="fas fa-hourglass-start mr-2"></i>Prêt à résoudre votre exercice...
122
- </div>
123
- </div>
124
-
125
- <div id="loading-spinner-element" class="hidden w-10 h-10 mx-auto my-6 border-4 border-gray-300 border-l-[var(--secondary-color)] rounded-full animate-spin-custom"></div>
126
-
127
- <div class="flex items-center bg-blue-50 border-l-4 border-[var(--secondary-color)] p-4 rounded-xl my-6">
128
- <i class="fab fa-telegram text-2xl text-[var(--secondary-color)] mr-3"></i>
129
- <span>Une copie de la solution sera envoyée sur Telegram pour archivage.</span>
130
- </div>
131
 
132
- <div id="response-container-element" class="hidden mt-6 p-6 border border-gray-300 rounded-xl bg-white">
133
- <h3 class="flex items-center font-semibold text-xl text-[var(--primary-color)] mb-4">
134
- <i class="fas fa-file-code mr-3 text-[var(--secondary-color)]"></i>Correction LaTeX Détaillée :
135
- </h3>
136
- <div id="response-output" class="font-code bg-gray-900 text-gray-200 p-5 rounded-xl overflow-x-auto whitespace-pre-wrap break-words max-h-96 mb-5 border border-gray-700"></div>
137
- <button id="copy-button" class="gradient-secondary px-6 py-3 rounded-xl text-white font-medium flex items-center justify-center transition duration-300 transform hover:translate-y-px">
138
- <i class="fas fa-copy mr-2"></i>Copier le code LaTeX
139
- </button>
140
  </div>
 
 
 
141
 
142
- <div id="error-display-element" class="hidden bg-red-50 border-2 border-[var(--accent-color)] text-[var(--accent-color)] p-4 rounded-xl my-5 font-medium">
 
 
 
143
  </div>
 
 
 
144
  </div>
 
145
  </div>
146
 
147
- <footer class="mt-12 mb-5 text-gray-500 text-sm text-center">
148
- Solutions générées par <a href="#" target="_blank" class="text-[var(--secondary-color)] font-medium hover:underline">Mariam IA</a> © 2025 - Précision garantie.
149
  </footer>
150
 
151
  <script>
152
- document.addEventListener('DOMContentLoaded', function() {
153
- const uploadSection = document.getElementById('upload-section');
154
- const fileInput = document.getElementById('file-input');
155
- const imagePreview = document.getElementById('image-preview');
156
- const solveButton = document.getElementById('solve-button');
157
- const solvingContainer = document.getElementById('solving-container');
158
- const responseContainer = document.getElementById('response-container-element');
159
- const responseOutputDiv = document.getElementById('response-output');
160
- const copyButton = document.getElementById('copy-button');
161
- const statusMessageElement = document.getElementById('status-message-element');
162
- const loadingSpinner = document.getElementById('loading-spinner-element');
163
- const promptTypeSelect = document.getElementById('prompt-type');
164
- const errorDisplay = document.getElementById('error-display-element');
165
-
166
- let selectedFile = null;
167
- let currentTaskId = null;
168
-
169
- // --- Gestion du Drag and Drop améliorée ---
170
- ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
171
- uploadSection.addEventListener(eventName, preventDefaults, false);
172
- });
173
- function preventDefaults(e) {
174
- e.preventDefault();
175
- e.stopPropagation();
176
  }
177
-
178
- // Hover effects are now handled by Tailwind's `group hover:` and `hover:` classes directly on #upload-section
179
- // The .upload-highlight class is kept for explicit JS addition if needed elsewhere,
180
- // but for dragenter/dragover, Tailwind's hover states are cleaner.
181
- // If you still want JS to add a class for drag states:
182
- ['dragenter', 'dragover'].forEach(eventName => {
183
- uploadSection.addEventListener(eventName, () => {
184
- uploadSection.classList.add('upload-highlight');
185
- }, false);
186
- });
187
-
188
- ['dragleave', 'drop'].forEach(eventName => {
189
- uploadSection.addEventListener(eventName, () => {
190
- uploadSection.classList.remove('upload-highlight');
191
- }, false);
192
- });
193
-
194
- uploadSection.addEventListener('drop', (e) => {
195
- if (e.dataTransfer.files.length) {
196
- handleFileSelection(e.dataTransfer.files[0]);
197
- }
198
- });
199
-
200
- fileInput.addEventListener('change', (e) => {
201
- if (e.target.files.length) {
202
- handleFileSelection(e.target.files[0]);
203
  }
204
- });
205
-
206
- function handleFileSelection(file) {
207
- const allowedTypes = ['image/png', 'image/jpeg', 'image/webp'];
208
- if (!allowedTypes.includes(file.type)) {
209
- displayError('Format de fichier non supporté.', 'Veuillez utiliser PNG, JPG ou WEBP.');
210
- selectedFile = null;
211
- solveButton.disabled = true;
212
- imagePreview.classList.add('hidden');
213
- return;
214
  }
215
-
216
- selectedFile = file;
217
- solveButton.disabled = false;
218
- errorDisplay.classList.add('hidden');
219
-
220
- const reader = new FileReader();
221
- reader.onload = (e) => {
222
- imagePreview.src = e.target.result;
223
- imagePreview.classList.remove('hidden');
224
- };
225
- reader.readAsDataURL(file);
226
  }
 
227
 
228
- function displayError(message, details = null) {
229
- let fullMessage = `<i class="fas fa-shield-halved mr-2"></i> ${message}`;
230
- if (details) {
231
- fullMessage += `<br><small class="block mt-1 text-red-700 font-normal">${escapeHtml(details)}</small>`;
232
- }
233
- errorDisplay.innerHTML = fullMessage;
234
- errorDisplay.classList.remove('hidden');
235
- responseContainer.classList.add('hidden');
236
- loadingSpinner.classList.add('hidden');
237
- updateStatusUI('error_user', null);
238
  }
 
239
 
240
- solveButton.addEventListener('click', () => {
241
- if (!selectedFile) return;
242
-
243
- solveButton.disabled = true;
244
- solvingContainer.classList.remove('hidden');
245
- responseContainer.classList.add('hidden');
246
- responseOutputDiv.textContent = '';
247
- errorDisplay.classList.add('hidden');
248
- loadingSpinner.classList.remove('hidden');
249
- updateStatusUI('pending', null, 'Préparation de la résolution...');
250
-
251
- const formData = new FormData();
252
- formData.append('image', selectedFile);
253
- formData.append('prompt_type', promptTypeSelect.value);
254
-
255
- fetch('/solve', {
256
- method: 'POST',
257
- body: formData
258
- })
259
- .then(response => {
260
- if (!response.ok) {
261
- return response.json().then(errData => {
262
- throw new Error(errData.error || `Erreur serveur : ${response.status}`);
263
- });
264
- }
265
- return response.json();
266
- })
267
- .then(data => {
268
- if (data.error) {
269
- throw new Error(data.error);
270
- }
271
-
272
- currentTaskId = data.task_id;
273
- updateStatusUI(data.status || 'pending', currentTaskId, "Lancement de l'analyse par l'IA...");
274
-
275
- const eventSource = new EventSource('/stream/' + currentTaskId);
276
-
277
- eventSource.onmessage = function(event) {
278
- const streamData = JSON.parse(event.data);
279
-
280
- if (streamData.error) {
281
- displayError(streamData.error, streamData.error_detail);
282
- if (streamData.response) {
283
- responseOutputDiv.textContent = streamData.response;
284
- responseContainer.classList.remove('hidden');
285
- }
286
- eventSource.close();
287
- solveButton.disabled = false;
288
- loadingSpinner.classList.add('hidden');
289
- return;
290
- }
291
-
292
- updateStatusUI(streamData.status, currentTaskId);
293
-
294
- if (streamData.status === 'completed' || streamData.status === 'completed_tex_only' || streamData.status === 'pdf_error') {
295
- responseContainer.classList.remove('hidden');
296
- loadingSpinner.classList.add('hidden');
297
-
298
- if (streamData.response) {
299
- responseOutputDiv.textContent = streamData.response;
300
- }
301
-
302
- if (streamData.status === 'pdf_error' && streamData.error_detail) {
303
- const statusEl = statusMessageElement.querySelector('.status-text');
304
- if(statusEl) statusEl.innerHTML += `<br><small class="pdf-error-detail block mt-1 text-orange-500"><i class="fas fa-file-invoice mr-1"></i> Erreur PDF: ${escapeHtml(streamData.error_detail)}</small>`;
305
- }
306
-
307
- eventSource.close();
308
- solveButton.disabled = false;
309
- }
310
- };
311
-
312
- eventSource.onerror = function() {
313
- eventSource.close();
314
- fetch('/task/' + currentTaskId)
315
- .then(resp => resp.json())
316
- .then(taskData => {
317
- updateStatusUI(taskData.status, currentTaskId);
318
- if (taskData.status === 'completed' || taskData.status === 'completed_tex_only' || taskData.status === 'pdf_error') {
319
- responseContainer.classList.remove('hidden');
320
- if (taskData.response) {
321
- responseOutputDiv.textContent = taskData.response;
322
- }
323
- if (taskData.status === 'pdf_error' && taskData.error_detail) {
324
- const statusEl = statusMessageElement.querySelector('.status-text');
325
- if(statusEl) statusEl.innerHTML += `<br><small class="pdf-error-detail block mt-1 text-orange-500"><i class="fas fa-file-invoice mr-1"></i> Erreur PDF: ${escapeHtml(taskData.error_detail)}</small>`;
326
- }
327
- } else if (taskData.status === 'error') {
328
- displayError(taskData.error || 'Erreur inattendue lors de la récupération de la tâche.', taskData.error_detail);
329
- } else {
330
- displayError('Connexion interrompue.', 'Le traitement se poursuit en arrière-plan. Vérifiez Telegram.');
331
- }
332
- })
333
- .catch(error => {
334
- displayError('Erreur de récupération du statut.', error.message);
335
- })
336
- .finally(() => {
337
- solveButton.disabled = false;
338
- loadingSpinner.classList.add('hidden');
339
- });
340
- };
341
- })
342
- .catch(error => {
343
- displayError(error.message || 'Erreur de communication serveur.');
344
- solveButton.disabled = false;
345
- loadingSpinner.classList.add('hidden');
346
- });
347
- });
348
-
349
- function updateStatusUI(status, taskId, overrideMessage = null) {
350
- const selectedPromptText = promptTypeSelect.options[promptTypeSelect.selectedIndex].text.split('(')[0].trim();
351
- let statusMsg = overrideMessage || '';
352
- let iconClass = 'fas fa-hourglass-start';
353
- // We use inline style for icon color to ensure CSS variables are applied
354
- let iconColorStyle = "color: var(--primary-color);";
355
-
356
-
357
- if (!overrideMessage) {
358
- switch(status) {
359
- case 'pending': statusMsg = "Lancement de l'analyse par l'IA..."; iconClass = 'fas fa-play-circle'; break;
360
- case 'processing': statusMsg = "L'IA déchiffre votre exercice..."; iconClass = 'fas fa-brain'; iconColorStyle = 'color: var(--secondary-color);'; break;
361
- case 'generating_latex': statusMsg = "Construction de la solution LaTeX..."; iconClass = 'fas fa-scroll'; break;
362
- case 'cleaning_latex': statusMsg = "Peaufinage du code LaTeX..."; iconClass = 'fas fa-magic'; break;
363
- case 'generating_pdf': statusMsg = "Compilation du document PDF final..."; iconClass = 'fas fa-file-pdf'; iconColorStyle = 'color: var(--accent-color);'; break;
364
- case 'completed': statusMsg = "Solution Complète et Précise Générée !"; iconClass = 'fas fa-check-double'; iconColorStyle = 'color: var(--success-color);'; break;
365
- case 'completed_tex_only': statusMsg = "Solution LaTeX Précise Générée ! (PDF non requis/dispo)"; iconClass = 'fas fa-check-circle'; iconColorStyle = 'color: var(--success-color);'; break;
366
- case 'pdf_error': statusMsg = "Solution LaTeX Précise Générée ! (Erreur PDF)"; iconClass = 'fas fa-file-excel'; iconColorStyle = 'color: #f39c12;'; break; // Specific color
367
- case 'error': statusMsg = "Une anomalie technique est survenue."; iconClass = 'fas fa-times-circle'; iconColorStyle = 'color: var(--accent-color);'; break;
368
- case 'error_user': statusMsg = "Veuillez vérifier votre image."; iconClass = 'fas fa-exclamation-triangle'; iconColorStyle = 'color: var(--accent-color);'; break;
369
- default: statusMsg = `Progression: ${status}`; iconClass = 'fas fa-spinner fa-spin';
370
- }
371
  }
372
-
373
- let taskInfoHtml = '';
374
- if (taskId) {
375
- taskInfoHtml = `<span class="text-sm text-gray-500 ml-2">(Tâche ${taskId.substring(0,6)} | Style: ${selectedPromptText})</span>`;
 
 
 
 
 
376
  }
 
377
 
378
- statusMessageElement.innerHTML = `<i class="${iconClass} mr-2" style="${iconColorStyle}"></i> <span class="status-text">${statusMsg}</span> ${taskInfoHtml}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
 
381
- // Fonction corrigée ici !
382
- function escapeHtml(unsafe) {
383
- if (typeof unsafe !== 'string') return '';
384
- return unsafe
385
- .replace(/&/g, "&")
386
- .replace(/</g, "<")
387
- .replace(/>/g, ">")
388
- .replace(/"/g, """)
389
- .replace(/'/g, "'");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  }
391
-
392
- copyButton.addEventListener('click', () => {
393
- const textToCopy = responseOutputDiv.textContent;
394
- navigator.clipboard.writeText(textToCopy).then(() => {
395
- const originalIcon = copyButton.querySelector('i').className;
396
- const originalText = copyButton.childNodes[1] ? copyButton.childNodes[1].nodeValue.trim() : 'Copier le code LaTeX';
397
- copyButton.innerHTML = `<i class="fas fa-check mr-2"></i> Code Copié !`;
398
- copyButton.classList.remove('gradient-secondary');
399
- copyButton.style.backgroundColor = 'var(--success-color)';
400
-
401
- setTimeout(() => {
402
- copyButton.innerHTML = `<i class="${originalIcon} mr-2"></i> ${originalText}`;
403
- copyButton.classList.add('gradient-secondary');
404
- copyButton.style.backgroundColor = ''; // Revert to gradient
405
- }, 2500);
406
- }).catch(err => {
407
- console.error('Erreur de copie: ', err);
408
- displayError('Copie échouée.', 'Veuillez copier manuellement le texte.');
409
- });
410
- });
411
  });
 
412
  </script>
413
  </body>
414
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Résolution d'exercices (Maths, Physique et Chimie)</title>
 
7
  <script src="https://cdn.tailwindcss.com"></script>
 
 
 
 
8
  <style>
9
+ /* Custom scrollbar for pre block (optional) */
10
+ pre::-webkit-scrollbar {
11
+ width: 8px;
12
+ height: 8px;
 
 
 
13
  }
14
+ pre::-webkit-scrollbar-track {
15
+ background: #f1f1f1;
16
+ border-radius: 10px;
17
  }
18
+ pre::-webkit-scrollbar-thumb {
19
+ background: #888;
20
+ border-radius: 10px;
21
  }
22
+ pre::-webkit-scrollbar-thumb:hover {
23
+ background: #555;
 
 
24
  }
25
+ .spinner {
26
+ border: 4px solid rgba(0, 0, 0, 0.1);
27
+ width: 36px;
28
+ height: 36px;
29
+ border-radius: 50%;
30
+ border-left-color: #0ea5e9; /* sky-500 */
31
+ animation: spin 1s ease infinite;
32
  }
 
33
  @keyframes spin {
34
+ 0% { transform: rotate(0deg); }
35
+ 100% { transform: rotate(360deg); }
 
 
 
 
 
 
 
 
 
36
  }
37
  </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  </head>
39
+ <body class="bg-slate-100 text-slate-800 min-h-screen flex flex-col items-center justify-center p-4 font-sans">
40
+
41
+ <div class="bg-white p-6 sm:p-8 rounded-xl shadow-2xl w-full max-w-2xl">
42
+ <header class="mb-6 sm:mb-8 text-center">
43
+ <h1 class="text-2xl sm:text-3xl font-bold text-sky-700">
44
+ Résolution d'Exercices
45
+ </h1>
46
+ <p class="text-sm text-slate-500">Mathématiques, Physique et Chimie</p>
47
+ </header>
48
+
49
+ <form id="solveForm" class="space-y-6">
50
+ <div>
51
+ <label for="imageUpload" class="block text-sm font-medium text-gray-700 mb-1">
52
+ Télécharger une image de l'exercice :
53
+ </label>
54
+ <input type="file" id="imageUpload" name="image" accept="image/*" required
55
+ class="block w-full text-sm text-slate-500
56
+ file:mr-4 file:py-2 file:px-4
57
+ file:rounded-full file:border-0
58
+ file:text-sm file:font-semibold
59
+ file:bg-sky-50 file:text-sky-700
60
+ hover:file:bg-sky-100
61
+ border border-gray-300 rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-sky-500">
62
  </div>
 
63
 
64
+ <div>
65
+ <span class="block text-sm font-medium text-gray-700 mb-2">Choisir le format de la solution :</span>
66
+ <div class="space-y-2">
67
+ <div class="flex items-center">
68
+ <input id="promptRefined" name="prompt_type" type="radio" value="refined" checked
69
+ class="h-4 w-4 text-sky-600 border-gray-300 focus:ring-sky-500">
70
+ <label for="promptRefined" class="ml-2 block text-sm text-gray-900">
71
+ Format Raffiné & Complet <span class="text-xs text-gray-500">(mise en page avancée)</span>
72
+ </label>
73
+ </div>
74
+ <div class="flex items-center">
75
+ <input id="promptLight" name="prompt_type" type="radio" value="light"
76
+ class="h-4 w-4 text-sky-600 border-gray-300 focus:ring-sky-500">
77
+ <label for="promptLight" class="ml-2 block text-sm text-gray-900">
78
+ Format simple
79
+ </label>
80
  </div>
81
  </div>
82
  </div>
 
 
 
 
 
83
 
84
+ <button type="submit" id="submitButton"
85
+ class="w-full bg-sky-600 hover:bg-sky-700 text-white font-bold py-2.5 px-4 rounded-md
86
+ focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2
87
+ transition duration-150 ease-in-out
88
+ disabled:bg-slate-400 disabled:cursor-not-allowed">
89
+ Résoudre l'exercice
90
+ </button>
91
+ </form>
 
 
 
 
 
 
 
 
 
92
 
93
+ <div id="statusArea" class="mt-6 space-y-3 hidden">
94
+ <div class="flex items-center space-x-3 p-3 rounded-md bg-sky-50 border border-sky-200">
95
+ <div id="spinner" class="spinner hidden"></div>
96
+ <p id="statusMessage" class="text-sm font-medium text-sky-700"></p>
 
 
 
 
97
  </div>
98
+ <div id="errorMessage" class="p-3 rounded-md bg-red-50 border border-red-200 text-red-700 text-sm hidden"></div>
99
+ <div id="errorDetailMessage" class="p-3 rounded-md bg-amber-50 border border-amber-200 text-amber-700 text-sm hidden"></div>
100
+ </div>
101
 
102
+ <div id="resultArea" class="mt-6 hidden">
103
+ <h3 class="text-lg font-semibold text-slate-700 mb-2">Code LaTeX Généré :</h3>
104
+ <div class="bg-gray-900 text-gray-100 p-4 rounded-md shadow">
105
+ <pre><code id="latexOutput" class="text-sm whitespace-pre-wrap break-all block max-h-96 overflow-auto"></code></pre>
106
  </div>
107
+ <button id="copyLatexButton" class="mt-3 bg-slate-200 hover:bg-slate-300 text-slate-700 text-sm font-medium py-1.5 px-3 rounded-md transition duration-150 ease-in-out">
108
+ Copier le LaTeX
109
+ </button>
110
  </div>
111
+
112
  </div>
113
 
114
+ <footer class="mt-8 text-center text-xs text-slate-500">
115
+ <p>© 2025 Mariam AI - Solution Propulsée par Mariam</p>
116
  </footer>
117
 
118
  <script>
119
+ const solveForm = document.getElementById('solveForm');
120
+ const submitButton = document.getElementById('submitButton');
121
+ const imageUpload = document.getElementById('imageUpload');
122
+
123
+ const statusArea = document.getElementById('statusArea');
124
+ const spinner = document.getElementById('spinner');
125
+ const statusMessage = document.getElementById('statusMessage');
126
+ const errorMessage = document.getElementById('errorMessage');
127
+ const errorDetailMessage = document.getElementById('errorDetailMessage');
128
+
129
+ const resultArea = document.getElementById('resultArea');
130
+ const latexOutput = document.getElementById('latexOutput');
131
+ const copyLatexButton = document.getElementById('copyLatexButton');
132
+
133
+ let eventSource = null;
134
+
135
+ solveForm.addEventListener('submit', async function(event) {
136
+ event.preventDefault();
137
+
138
+ if (!imageUpload.files || imageUpload.files.length === 0) {
139
+ showError("Veuillez sélectionner un fichier image.");
140
+ return;
 
 
141
  }
142
+
143
+ submitButton.disabled = true;
144
+ showStatus("Initialisation...", true);
145
+ hideError();
146
+ hideResult();
147
+
148
+ const formData = new FormData(solveForm);
149
+
150
+ try {
151
+ const response = await fetch('/solve', {
152
+ method: 'POST',
153
+ body: formData
154
+ });
155
+
156
+ if (!response.ok) {
157
+ const errorData = await response.json().catch(() => ({ error: 'Erreur serveur inconnue' }));
158
+ throw new Error(errorData.error || `Erreur HTTP: ${response.status}`);
 
 
 
 
 
 
 
 
 
159
  }
160
+
161
+ const data = await response.json();
162
+ if (data.task_id) {
163
+ showStatus("Tâche créée. En attente de la réponse...", true);
164
+ startStreaming(data.task_id);
165
+ } else {
166
+ throw new Error("ID de tâche non reçu.");
 
 
 
167
  }
168
+
169
+ } catch (error) {
170
+ showError(`Erreur lors de la soumission : ${error.message}`);
171
+ submitButton.disabled = false;
172
+ hideStatus();
 
 
 
 
 
 
173
  }
174
+ });
175
 
176
+ function startStreaming(taskId) {
177
+ if (eventSource) {
178
+ eventSource.close();
 
 
 
 
 
 
 
179
  }
180
+ eventSource = new EventSource(`/stream/${taskId}`);
181
 
182
+ showStatus("Connexion au flux de progression...", true);
183
+
184
+ eventSource.onmessage = function(event) {
185
+ const data = JSON.parse(event.data);
186
+
187
+ let userFriendlyStatus = data.status;
188
+ switch(data.status) {
189
+ case 'pending': userFriendlyStatus = "En attente de traitement..."; break;
190
+ case 'processing': userFriendlyStatus = "Traitement de l'image..."; break;
191
+ case 'generating_latex': userFriendlyStatus = "Génération du code LaTeX en cours..."; break;
192
+ case 'cleaning_latex': userFriendlyStatus = "Nettoyage du code LaTeX..."; break;
193
+ case 'generating_pdf': userFriendlyStatus = "Génération du PDF en cours..."; break;
194
+ case 'completed': userFriendlyStatus = "Terminé ! Solution générée (LaTeX et PDF)."; break;
195
+ case 'completed_tex_only': userFriendlyStatus = "Terminé ! Solution LaTeX générée (PDF non disponible)."; break;
196
+ case 'pdf_error': userFriendlyStatus = "Erreur lors de la génération du PDF. Le code LaTeX est disponible."; break;
197
+ case 'error': userFriendlyStatus = "Une erreur est survenue."; break;
198
+ }
199
+ showStatus(userFriendlyStatus, !['completed', 'error', 'pdf_error', 'completed_tex_only'].includes(data.status));
200
+
201
+
202
+ if (data.error) {
203
+ showError(data.error);
204
+ } else {
205
+ hideError(); // Hide general error if a specific one is not present in this message
206
+ }
207
+ if (data.error_detail) {
208
+ showErrorDetail(data.error_detail);
209
+ } else {
210
+ hideErrorDetail();
211
+ }
212
+
213
+
214
+ if (data.response) {
215
+ latexOutput.textContent = data.response;
216
+ showResult();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  }
218
+
219
+ if (['completed', 'error', 'pdf_error', 'completed_tex_only'].includes(data.status)) {
220
+ eventSource.close();
221
+ submitButton.disabled = false;
222
+ if (data.status !== 'error' && !data.error) { // If not a critical error, keep status positive
223
+ // Status already set by switch
224
+ } else if (data.error) {
225
+ showStatus("Traitement terminé avec des erreurs.", false);
226
+ }
227
  }
228
+ };
229
 
230
+ eventSource.onerror = function(err) {
231
+ console.error("Erreur EventSource:", err);
232
+ showError("Erreur de connexion au serveur pour les mises à jour. Veuillez réessayer.");
233
+ showStatus("Déconnecté.", false);
234
+ eventSource.close();
235
+ submitButton.disabled = false;
236
+ };
237
+ }
238
+
239
+ function showStatus(message, showSpinner = false) {
240
+ statusArea.classList.remove('hidden');
241
+ statusMessage.textContent = message;
242
+ if (showSpinner) {
243
+ spinner.classList.remove('hidden');
244
+ } else {
245
+ spinner.classList.add('hidden');
246
  }
247
+ }
248
+
249
+ function hideStatus() {
250
+ statusArea.classList.add('hidden');
251
+ spinner.classList.add('hidden');
252
+ }
253
+
254
+ function showError(message) {
255
+ errorMessage.textContent = message;
256
+ errorMessage.classList.remove('hidden');
257
+ statusArea.classList.remove('hidden'); // Ensure status area is visible to show error
258
+ }
259
+ function hideError() {
260
+ errorMessage.classList.add('hidden');
261
+ errorMessage.textContent = '';
262
+ }
263
 
264
+ function showErrorDetail(message) {
265
+ errorDetailMessage.textContent = message;
266
+ errorDetailMessage.classList.remove('hidden');
267
+ statusArea.classList.remove('hidden');
268
+ }
269
+ function hideErrorDetail() {
270
+ errorDetailMessage.classList.add('hidden');
271
+ errorDetailMessage.textContent = '';
272
+ }
273
+
274
+
275
+ function showResult() {
276
+ resultArea.classList.remove('hidden');
277
+ }
278
+ function hideResult() {
279
+ resultArea.classList.add('hidden');
280
+ latexOutput.textContent = '';
281
+ }
282
+
283
+ copyLatexButton.addEventListener('click', () => {
284
+ if (latexOutput.textContent) {
285
+ navigator.clipboard.writeText(latexOutput.textContent)
286
+ .then(() => {
287
+ const originalText = copyLatexButton.textContent;
288
+ copyLatexButton.textContent = 'Copié !';
289
+ setTimeout(() => {
290
+ copyLatexButton.textContent = originalText;
291
+ }, 2000);
292
+ })
293
+ .catch(err => {
294
+ console.error('Erreur de copie: ', err);
295
+ alert('Erreur lors de la copie du texte.');
296
+ });
297
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  });
299
+
300
  </script>
301
  </body>
302
  </html>