Docfile commited on
Commit
1c9f000
·
verified ·
1 Parent(s): 45da2f6

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +374 -273
templates/index.html CHANGED
@@ -3,322 +3,458 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Math Solver IA</title>
7
  <!-- Google Fonts -->
8
- <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&family=Roboto+Mono&display=swap" rel="stylesheet">
9
  <!-- Font Awesome Icons -->
10
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
11
- <!-- KaTeX (si vous décidez de l'utiliser pour autre chose que le code LaTeX brut) -->
12
- <!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/katex.min.css"> -->
13
- <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/katex.min.js"></script> -->
14
- <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/contrib/auto-render.min.js"></script> -->
15
  <style>
16
  :root {
17
- --primary-color: #3498db; /* Bleu principal */
18
- --secondary-color: #2ecc71; /* Vert secondaire */
19
- --accent-color: #e74c3c; /* Rouge pour erreurs/accents */
20
- --light-bg: #ecf0f1; /* Fond clair */
21
- --dark-text: #2c3e50; /* Texte foncé */
22
- --light-text: #7f8c8d; /* Texte plus clair */
23
- --border-color: #bdc3c7;
24
- --card-bg: #ffffff;
25
- --shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
26
- --border-radius: 8px;
 
 
 
 
 
 
 
 
 
27
  }
28
 
29
  body {
30
- font-family: 'Poppins', sans-serif;
31
  background-color: var(--light-bg);
32
- color: var(--dark-text);
33
- margin: 0;
34
- padding: 20px;
35
  display: flex;
36
  flex-direction: column;
37
  align-items: center;
38
  min-height: 100vh;
39
- box-sizing: border-box;
 
40
  }
41
 
42
  .main-container {
43
  background-color: var(--card-bg);
44
- padding: 30px 40px;
45
  border-radius: var(--border-radius);
46
  box-shadow: var(--shadow);
47
  width: 100%;
48
- max-width: 700px;
49
  text-align: center;
50
- transition: all 0.3s ease;
51
  }
52
 
53
  h1 {
54
  color: var(--primary-color);
55
- font-weight: 600;
56
- margin-bottom: 30px;
 
57
  display: flex;
58
  align-items: center;
59
  justify-content: center;
60
  }
61
- h1 i {
62
- margin-right: 10px;
63
- font-size: 1.2em;
 
64
  }
 
 
 
 
 
 
 
65
 
66
  .upload-section {
67
- border: 2px dashed var(--border-color);
68
  border-radius: var(--border-radius);
69
- padding: 30px;
70
  cursor: pointer;
71
  transition: all 0.3s ease;
72
- background-color: #f9fafb;
73
- margin-bottom: 20px;
 
 
74
  }
75
- .upload-section:hover {
76
- border-color: var(--primary-color);
77
- background-color: #f0f8ff;
78
  }
79
- .upload-section p {
80
- margin: 0 0 15px 0;
81
- font-size: 1.1em;
82
- color: var(--light-text);
 
83
  }
84
- .upload-section i {
85
- font-size: 3em;
86
- color: var(--primary-color);
87
  margin-bottom: 15px;
 
88
  }
89
- #file-input {
90
- display: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  }
92
  #image-preview {
93
  max-width: 100%;
94
- max-height: 250px;
95
- margin-top: 20px;
96
  border-radius: var(--border-radius);
97
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
98
  display: none; /* Caché initialement */
 
99
  }
100
 
101
- .prompt-selector {
102
- margin-bottom: 25px;
 
 
 
103
  text-align: left;
 
104
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  .prompt-selector label {
106
  font-weight: 500;
107
  margin-bottom: 8px;
108
  display: block;
109
- color: var(--dark-text);
 
110
  }
111
  .prompt-selector select {
112
  width: 100%;
113
- padding: 12px;
114
  border-radius: var(--border-radius);
115
- border: 1px solid var(--border-color);
116
  font-size: 1em;
117
- font-family: 'Poppins', sans-serif;
118
- background-color: #fff;
119
- transition: border-color 0.3s ease;
 
 
 
 
 
120
  }
121
  .prompt-selector select:focus {
122
- border-color: var(--primary-color);
123
  outline: none;
124
- box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
125
  }
126
 
127
  .button {
128
- background-color: var(--primary-color);
129
  color: white;
130
  border: none;
131
- padding: 12px 25px;
132
- font-size: 1.1em;
133
- font-weight: 500;
 
134
  border-radius: var(--border-radius);
135
  cursor: pointer;
136
- transition: all 0.3s ease;
137
  display: inline-flex;
138
  align-items: center;
139
  justify-content: center;
140
- gap: 8px;
 
 
141
  }
142
  .button:hover {
143
- background-color: #2980b9; /* Bleu plus foncé */
144
- transform: translateY(-2px);
145
- box-shadow: 0 6px 20px rgba(52, 152, 219, 0.3);
 
 
 
146
  }
147
  .button:disabled {
148
- background-color: #bdc3c7; /* Gris pour désactivé */
 
149
  cursor: not-allowed;
150
  transform: none;
151
  box-shadow: none;
 
152
  }
153
- .button.copy-button {
154
- background-color: var(--secondary-color);
 
 
 
 
155
  }
156
  .button.copy-button:hover {
157
- background-color: #27ae60; /* Vert plus foncé */
158
- box-shadow: 0 6px 20px rgba(46, 204, 113, 0.3);
159
  }
160
 
 
161
  #solving-container {
162
  display: none; /* Caché initialement */
163
- margin-top: 30px;
164
- padding: 25px;
165
- background-color: #f9f9f9;
166
  border-radius: var(--border-radius);
167
  border: 1px solid var(--border-color);
168
  }
169
- .status {
170
- font-size: 1.1em;
 
 
 
 
171
  font-weight: 500;
172
- margin-bottom: 15px;
173
- color: var(--dark-text);
 
 
 
174
  }
175
- .status i { /* Icône pour le statut */
176
- margin-right: 8px;
 
 
177
  }
178
- .status small {
179
- display: block;
 
 
180
  font-weight: 400;
181
- color: var(--light-text);
182
- font-size: 0.9em;
183
- margin-top: 5px;
184
- }
185
- .telegram-notice {
186
- background-color: #e3f2fd;
187
- border-left: 4px solid var(--primary-color);
188
- padding: 12px 15px;
189
- margin: 20px 0;
190
- font-size: 0.95em;
191
- border-radius: 4px;
192
- color: var(--dark-text);
193
- }
194
- .telegram-notice i {
195
- margin-right: 8px;
196
- color: var(--primary-color);
197
  }
198
 
199
- .loading-spinner { /* NEW spinner */
200
- border: 4px solid rgba(0, 0, 0, 0.1);
201
- width: 36px;
202
- height: 36px;
 
 
203
  border-radius: 50%;
204
- border-left-color: var(--primary-color);
205
- animation: spin 1s ease infinite;
206
- margin: 20px auto;
207
  display: none; /* Caché initialement */
208
  }
209
  @keyframes spin {
210
- 0% { transform: rotate(0deg); }
211
- 100% { transform: rotate(360deg); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  }
213
 
214
  .response-container {
215
- margin-top: 20px;
216
- padding: 20px;
217
  border: 1px solid var(--border-color);
218
  border-radius: var(--border-radius);
219
  background-color: var(--card-bg);
220
  display: none; /* Caché initialement */
221
  text-align: left;
222
  }
223
- #response {
224
- font-family: 'Roboto Mono', monospace; /* Police pour le code */
225
- background-color: #fdf6e3; /* Fond type "Solarized Light" */
226
- color: #657b83; /* Texte pour code */
227
- padding: 15px;
228
- border-radius: var(--border-radius);
229
- overflow-x: auto; /* Défilement horizontal pour le code long */
230
- white-space: pre-wrap; /* Conserve les retours à la ligne et espaces */
231
- word-wrap: break-word; /* S'assure que le texte ne dépasse pas */
232
- max-height: 400px; /* Hauteur max avec défilement */
233
  margin-bottom: 15px;
 
 
 
234
  }
235
- #response code { /* Pas vraiment besoin si #response a déjà white-space: pre */
236
- display: block;
 
237
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
 
239
- .error-message {
240
  color: var(--accent-color);
241
- background-color: #fdedec;
242
- border: 1px solid var(--accent-color);
243
- padding: 15px;
244
  border-radius: var(--border-radius);
245
- margin-bottom: 15px;
 
 
 
 
 
 
 
246
  }
247
- .error-message i {
248
- margin-right: 8px;
 
 
 
 
249
  }
250
 
 
251
  .footer {
252
- margin-top: 40px;
253
- font-size: 0.9em;
254
- color: var(--light-text);
 
255
  }
256
  .footer a {
257
- color: var(--primary-color);
258
  text-decoration: none;
 
259
  }
260
  .footer a:hover {
261
  text-decoration: underline;
262
  }
263
 
264
  /* Responsive adjustments */
265
- @media (max-width: 600px) {
266
  body { padding: 15px; }
267
- .main-container { padding: 20px; }
268
- h1 { font-size: 1.8em; }
 
 
 
 
 
 
 
 
 
 
269
  .upload-section { padding: 20px; }
270
- .upload-section p { font-size: 1em; }
271
- .upload-section i { font-size: 2.5em; }
272
  }
273
 
274
  </style>
275
  </head>
276
  <body>
277
  <div class="main-container">
278
- <h1><i class="fas fa-brain"></i>Math Solver IA</h1>
 
279
 
280
  <div id="upload-section" class="upload-section">
281
- <i class="fas fa-cloud-upload-alt"></i>
282
- <p>Cliquez ou glissez-déposez une image ici</p>
283
- <input type="file" id="file-input" accept="image/*">
284
- <img id="image-preview" src="#" alt="Aperçu de l'image">
 
 
 
 
 
285
  </div>
286
 
287
- <div class="prompt-selector">
288
- <label for="prompt-type">Style de Correction LaTeX :</label>
289
- <select id="prompt-type" name="prompt-type">
290
- <option value="refined">Raffiné et Complet (avec tcolorbox, etc.)</option>
291
- <option value="light">Léger et Rapide (LaTeX standard)</option>
292
- </select>
 
 
 
293
  </div>
294
 
295
  <button id="solve-button" class="button" disabled>
296
- <i class="fas fa-magic"></i>Résoudre
297
  </button>
298
 
299
  <div id="solving-container">
300
- <div class="status" id="status-message">
301
- <i class="fas fa-hourglass-half"></i>En attente de résolution...
 
 
302
  </div>
303
- <div class="loading-spinner" id="loading-spinner"></div>
 
304
  <div class="telegram-notice">
305
- <i class="fab fa-telegram-plane"></i>La réponse complète sera également envoyée sur Telegram.
306
  </div>
307
- <div class="response-container" id="response-container">
308
- <h3><i class="fas fa-file-code"></i>Code LaTeX Généré :</h3>
309
- <div id="response"></div>
 
310
  <button id="copy-button" class="button copy-button">
311
- <i class="fas fa-copy"></i>Copier le code
312
  </button>
313
  </div>
314
- <div id="error-display" class="error-message" style="display: none;">
 
315
  <!-- Les erreurs seront affichées ici -->
316
  </div>
317
  </div>
318
  </div>
319
 
320
  <footer class="footer">
321
- Propulsé par IA - <a href="#" target="_blank">Mariam-AI</a> © 2024
322
  </footer>
323
 
324
  <script>
@@ -326,20 +462,21 @@
326
  const uploadSection = document.getElementById('upload-section');
327
  const fileInput = document.getElementById('file-input');
328
  const imagePreview = document.getElementById('image-preview');
 
329
  const solveButton = document.getElementById('solve-button');
330
  const solvingContainer = document.getElementById('solving-container');
331
- const responseContainer = document.getElementById('response-container');
332
- const responseDiv = document.getElementById('response'); // Renommé pour clarté
333
  const copyButton = document.getElementById('copy-button');
334
- const statusMessageElement = document.getElementById('status-message'); // Renommé
335
- const loadingSpinner = document.getElementById('loading-spinner'); // NOUVEAU
336
  const promptTypeSelect = document.getElementById('prompt-type');
337
- const errorDisplay = document.getElementById('error-display');
338
 
339
  let selectedFile = null;
340
-
341
- uploadSection.addEventListener('click', () => fileInput.click());
342
-
343
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
344
  uploadSection.addEventListener(eventName, preventDefaults, false);
345
  });
@@ -347,20 +484,19 @@
347
  e.preventDefault();
348
  e.stopPropagation();
349
  }
350
-
351
  ['dragenter', 'dragover'].forEach(eventName => {
352
  uploadSection.addEventListener(eventName, () => uploadSection.classList.add('highlight-drag'), false);
353
  });
354
  ['dragleave', 'drop'].forEach(eventName => {
355
  uploadSection.addEventListener(eventName, () => uploadSection.classList.remove('highlight-drag'), false);
356
  });
357
-
358
  uploadSection.addEventListener('drop', (e) => {
359
  if (e.dataTransfer.files.length) {
360
  handleFileSelection(e.dataTransfer.files[0]);
361
  }
362
  });
363
-
 
364
  fileInput.addEventListener('change', (e) => {
365
  if (e.target.files.length) {
366
  handleFileSelection(e.target.files[0]);
@@ -368,35 +504,39 @@
368
  });
369
 
370
  function handleFileSelection(file) {
371
- if (!file.type.startsWith('image/')) {
372
- displayError('Veuillez sélectionner un fichier image valide (PNG, JPG, etc.).');
 
373
  selectedFile = null;
374
  solveButton.disabled = true;
375
  imagePreview.style.display = 'none';
 
376
  return;
377
  }
378
 
379
  selectedFile = file;
380
  solveButton.disabled = false;
381
- errorDisplay.style.display = 'none'; // Cacher les erreurs précédentes
382
 
383
  const reader = new FileReader();
384
  reader.onload = (e) => {
385
  imagePreview.src = e.target.result;
386
  imagePreview.style.display = 'block';
 
387
  };
388
  reader.readAsDataURL(file);
389
  }
390
 
391
  function displayError(message, details = null) {
392
- let fullMessage = `<i class="fas fa-exclamation-triangle"></i> ${message}`;
393
  if (details) {
394
- fullMessage += `<br><small>Détail: ${escapeHtml(details)}</small>`;
395
  }
396
  errorDisplay.innerHTML = fullMessage;
397
  errorDisplay.style.display = 'block';
398
- responseContainer.style.display = 'none'; // Cacher le conteneur de réponse si erreur
399
  loadingSpinner.style.display = 'none';
 
400
  }
401
 
402
  solveButton.addEventListener('click', () => {
@@ -405,10 +545,10 @@
405
  solveButton.disabled = true;
406
  solvingContainer.style.display = 'block';
407
  responseContainer.style.display = 'none';
408
- responseDiv.textContent = ''; // Nettoyer la réponse précédente
409
- errorDisplay.style.display = 'none'; // Cacher les erreurs précédentes
410
- loadingSpinner.style.display = 'block'; // Afficher le spinner
411
- updateStatusUI('pending', ''); // Message initial
412
 
413
  const formData = new FormData();
414
  formData.append('image', selectedFile);
@@ -419,31 +559,30 @@
419
  body: formData
420
  })
421
  .then(response => {
422
- if (!response.ok) { // Gérer les erreurs HTTP (ex: 500, 400)
423
  return response.json().then(errData => {
424
- throw new Error(errData.error || `Erreur serveur: ${response.status}`);
425
  });
426
  }
427
  return response.json();
428
  })
429
  .then(data => {
430
- if (data.error) { // Erreur logique retournée par /solve avant SSE
431
  throw new Error(data.error);
432
  }
433
 
434
- const taskId = data.task_id;
435
- updateStatusUI('pending', taskId); // Mettre à jour avec l'ID
436
 
437
- const eventSource = new EventSource('/stream/' + taskId);
438
 
439
  eventSource.onmessage = function(event) {
440
  const streamData = JSON.parse(event.data);
441
 
442
  if (streamData.error) {
443
  displayError(streamData.error, streamData.error_detail);
444
- // Si une réponse partielle (LaTeX) est disponible malgré l'erreur (ex: erreur PDF)
445
- if (streamData.response) {
446
- responseDiv.textContent = streamData.response; // Utiliser textContent pour le code LaTeX
447
  responseContainer.style.display = 'block';
448
  }
449
  eventSource.close();
@@ -452,20 +591,19 @@
452
  return;
453
  }
454
 
455
- updateStatusUI(streamData.status, taskId);
456
 
457
  if (streamData.status === 'completed' || streamData.status === 'completed_tex_only' || streamData.status === 'pdf_error') {
458
  responseContainer.style.display = 'block';
459
  loadingSpinner.style.display = 'none';
460
 
461
  if (streamData.response) {
462
- responseDiv.textContent = streamData.response; // Utiliser textContent pour le code LaTeX
463
  }
464
 
465
  if (streamData.status === 'pdf_error' && streamData.error_detail) {
466
- // Afficher l'erreur PDF en plus du statut, mais pas comme une erreur bloquante
467
- let currentStatus = statusMessageElement.innerHTML;
468
- statusMessageElement.innerHTML = currentStatus + `<br><small style="color:var(--accent-color);"><i class="fas fa-file-pdf"></i> Erreur PDF: ${escapeHtml(streamData.error_detail)}</small>`;
469
  }
470
 
471
  eventSource.close();
@@ -475,29 +613,27 @@
475
 
476
  eventSource.onerror = function() {
477
  eventSource.close();
478
- // Tenter de récupérer le statut final si SSE échoue
479
- fetch('/task/' + taskId)
480
  .then(resp => resp.json())
481
  .then(taskData => {
482
- updateStatusUI(taskData.status, taskId);
483
  if (taskData.status === 'completed' || taskData.status === 'completed_tex_only' || taskData.status === 'pdf_error') {
484
  responseContainer.style.display = 'block';
485
  if (taskData.response) {
486
- responseDiv.textContent = taskData.response;
487
  }
488
  if (taskData.status === 'pdf_error' && taskData.error_detail) {
489
- let currentStatus = statusMessageElement.innerHTML;
490
- statusMessageElement.innerHTML = currentStatus + `<br><small style="color:var(--accent-color);"><i class="fas fa-file-pdf"></i> Erreur PDF: ${escapeHtml(taskData.error_detail)}</small>`;
491
  }
492
  } else if (taskData.status === 'error') {
493
- displayError(taskData.error || 'Une erreur inattendue est survenue lors de la récupération de la tâche.', taskData.error_detail);
494
  } else {
495
- // Cas où la tâche n'est pas encore terminée et SSE a échoué
496
- displayError('Connexion perdue avec le serveur. Le traitement peut continuer en arrière-plan.', 'Vérifiez Telegram pour la réponse finale.');
497
  }
498
  })
499
  .catch(error => {
500
- displayError('Erreur de connexion lors de la récupération du statut de la tâche.', error.message);
501
  })
502
  .finally(() => {
503
  solveButton.disabled = false;
@@ -506,98 +642,63 @@
506
  };
507
  })
508
  .catch(error => {
509
- displayError(error.message || 'Une erreur est survenue lors de la communication avec le serveur.');
510
  solveButton.disabled = false;
511
  loadingSpinner.style.display = 'none';
512
  });
513
  });
514
 
515
- function updateStatusUI(status, taskId) {
516
  const selectedPromptText = promptTypeSelect.options[promptTypeSelect.selectedIndex].text.split('(')[0].trim();
517
- let statusMsg = '';
518
- let iconClass = 'fas fa-hourglass-half'; // Default icon
519
-
520
- switch(status) {
521
- case 'pending': statusMsg = "En attente de traitement..."; iconClass = 'fas fa-pause-circle'; break;
522
- case 'processing': statusMsg = "L'IA analyse votre image..."; iconClass = 'fas fa-cogs'; break;
523
- case 'generating_latex': statusMsg = "Génération du code LaTeX..."; iconClass = 'fas fa-file-alt'; break;
524
- case 'cleaning_latex': statusMsg = "Nettoyage du code LaTeX..."; iconClass = 'fas fa-broom'; break;
525
- case 'generating_pdf': statusMsg = "Compilation du PDF LaTeX..."; iconClass = 'fas fa-file-pdf'; break;
526
- case 'completed': statusMsg = "Terminé ! PDF et LaTeX générés."; iconClass = 'fas fa-check-circle'; break;
527
- case 'completed_tex_only': statusMsg = "Terminé ! LaTeX généré (PDF non dispo/demandé)."; iconClass = 'fas fa-check-circle'; break;
528
- case 'pdf_error': statusMsg = "Erreur PDF. LaTeX seul généré."; iconClass = 'fas fa-exclamation-circle'; break; // Icône différente pour erreur PDF
529
- case 'error': statusMsg = "Erreur de traitement."; iconClass = 'fas fa-times-circle'; break; // Sera géré par displayError
530
- default: statusMsg = `Statut inconnu: ${status}`; iconClass = 'fas fa-question-circle';
 
 
 
 
531
  }
532
 
533
- let taskInfo = '';
534
  if (taskId) {
535
- taskInfo = ` (Tâche ${taskId.substring(0,8)}, Style: ${selectedPromptText})`;
536
  }
537
 
538
- statusMessageElement.innerHTML = `<i class="${iconClass}"></i> ${statusMsg}${taskInfo}`;
539
- // La petite note sur Telegram est toujours visible, donc pas besoin de la changer ici.
540
  }
541
 
542
  function escapeHtml(unsafe) {
543
- if (typeof unsafe !== 'string') {
544
- return ''; // ou retourner unsafe tel quel si ce n'est pas une chaîne
545
- }
546
- return unsafe
547
- .replace(/&/g, "&")
548
- .replace(/</g, "<")
549
- .replace(/>/g, ">")
550
- .replace(/"/g, """)
551
- .replace(/'/g, "'");
552
  }
553
 
554
  copyButton.addEventListener('click', () => {
555
- const textToCopy = responseDiv.textContent;
556
  navigator.clipboard.writeText(textToCopy).then(() => {
557
  const originalIcon = copyButton.querySelector('i').className;
558
- const originalText = copyButton.childNodes[1].nodeValue; // Texte après l'icône
559
- copyButton.innerHTML = `<i class="fas fa-check"></i> Copié!`;
 
560
  setTimeout(() => {
561
  copyButton.innerHTML = `<i class="${originalIcon}"></i>${originalText}`;
562
- }, 2000);
 
563
  }).catch(err => {
564
  console.error('Erreur de copie: ', err);
565
- displayError('Erreur lors de la copie du texte.', 'Veuillez essayer manuellement.');
566
- // Fallback pour anciens navigateurs (moins fiable)
567
- try {
568
- const range = document.createRange();
569
- range.selectNodeContents(responseDiv);
570
- window.getSelection().removeAllRanges();
571
- window.getSelection().addRange(range);
572
- document.execCommand('copy');
573
- window.getSelection().removeAllRanges();
574
-
575
- const originalIcon = copyButton.querySelector('i').className;
576
- const originalText = copyButton.childNodes[1].nodeValue;
577
- copyButton.innerHTML = `<i class="fas fa-check"></i> Copié!`;
578
- setTimeout(() => {
579
- copyButton.innerHTML = `<i class="${originalIcon}"></i>${originalText}`;
580
- }, 2000);
581
- } catch (e) {
582
- displayError('Erreur lors de la copie (fallback).', 'Veuillez essayer manuellement.');
583
- }
584
  });
585
  });
586
-
587
- // KaTeX rendering (si vous voulez un jour rendre des maths dans l'UI, mais pas pour le code LaTeX brut)
588
- /*
589
- function renderMathInElement(elem, options) {
590
- if (window.renderMathInElement) {
591
- window.renderMathInElement(elem, options);
592
- }
593
- }
594
- renderMathInElement(document.body, {
595
- delimiters: [
596
- {left: '$$', right: '$$', display: true}, {left: '$', right: '$', display: false},
597
- {left: '\\(', right: '\\)', display: false}, {left: '\\[', right: '\\]', display: true}
598
- ]
599
- });
600
- */
601
  });
602
  </script>
603
  </body>
 
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
  <!-- Google Fonts -->
8
+ <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Fira+Code&display=swap" rel="stylesheet">
9
  <!-- Font Awesome Icons -->
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
 
 
 
 
11
  <style>
12
  :root {
13
+ --primary-color: #2c3e50; /* Bleu Nuit - Élégant et Professionnel */
14
+ --secondary-color: #1abc9c; /* Turquoise - Dynamique */
15
+ --accent-color: #e74c3c; /* Rouge Doux - Pour les erreurs subtiles */
16
+ --success-color: #27ae60; /* Vert Succès */
17
+ --light-bg: #f4f6f8; /* Fond Général Très Clair */
18
+ --card-bg: #ffffff; /* Fond des Cartes */
19
+ --text-color: #34495e; /* Texte Principal */
20
+ --subtle-text-color: #7f8c8d; /* Texte Secondaire/Discret */
21
+ --border-color: #dfe4ea; /* Bordures Claires */
22
+ --shadow: 0 8px 25px rgba(44, 62, 80, 0.1); /* Ombre plus douce */
23
+ --border-radius: 12px; /* Rayon de bordure plus arrondi */
24
+ --font-main: 'Montserrat', sans-serif;
25
+ --font-code: 'Fira Code', monospace;
26
+ }
27
+
28
+ * {
29
+ box-sizing: border-box;
30
+ margin: 0;
31
+ padding: 0;
32
  }
33
 
34
  body {
35
+ font-family: var(--font-main);
36
  background-color: var(--light-bg);
37
+ color: var(--text-color);
 
 
38
  display: flex;
39
  flex-direction: column;
40
  align-items: center;
41
  min-height: 100vh;
42
+ padding: 20px;
43
+ line-height: 1.7;
44
  }
45
 
46
  .main-container {
47
  background-color: var(--card-bg);
48
+ padding: 35px 45px;
49
  border-radius: var(--border-radius);
50
  box-shadow: var(--shadow);
51
  width: 100%;
52
+ max-width: 750px;
53
  text-align: center;
54
+ transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
55
  }
56
 
57
  h1 {
58
  color: var(--primary-color);
59
+ font-weight: 700;
60
+ font-size: 2.2em;
61
+ margin-bottom: 10px;
62
  display: flex;
63
  align-items: center;
64
  justify-content: center;
65
  }
66
+ h1 .logo-icon {
67
+ margin-right: 12px;
68
+ font-size: 1.3em;
69
+ color: var(--secondary-color);
70
  }
71
+ .subtitle {
72
+ font-size: 1.1em;
73
+ color: var(--subtle-text-color);
74
+ margin-bottom: 35px;
75
+ font-weight: 400;
76
+ }
77
+
78
 
79
  .upload-section {
80
+ border: 2.5px dashed var(--border-color);
81
  border-radius: var(--border-radius);
82
+ padding: 35px;
83
  cursor: pointer;
84
  transition: all 0.3s ease;
85
+ background-color: #fdfdfe;
86
+ margin-bottom: 30px;
87
+ position: relative; /* Pour le positionnement absolu de l'input */
88
+ overflow: hidden; /* Pour cacher l'input */
89
  }
90
+ .upload-section.highlight-drag { /* Style quand on drag par-dessus */
91
+ border-color: var(--secondary-color);
92
+ background-color: #e8f8f5;
93
  }
94
+ .upload-content {
95
+ display: flex;
96
+ flex-direction: column;
97
+ align-items: center;
98
+ justify-content: center;
99
  }
100
+ .upload-section .upload-icon {
101
+ font-size: 3.5em;
102
+ color: var(--secondary-color);
103
  margin-bottom: 15px;
104
+ transition: transform 0.3s ease;
105
  }
106
+ .upload-section:hover .upload-icon {
107
+ transform: scale(1.1) translateY(-5px);
108
+ }
109
+ .upload-section p {
110
+ margin: 0 0 10px 0;
111
+ font-size: 1.15em;
112
+ font-weight: 500;
113
+ color: var(--text-color);
114
+ }
115
+ .upload-section small {
116
+ font-size: 0.9em;
117
+ color: var(--subtle-text-color);
118
+ }
119
+ #file-input { /* Caché mais accessible pour la sémantique et l'accessibilité */
120
+ position: absolute;
121
+ left: 0;
122
+ top: 0;
123
+ width: 100%;
124
+ height: 100%;
125
+ opacity: 0;
126
+ cursor: pointer;
127
+ }
128
+ #image-preview-container {
129
+ margin-top: 20px;
130
+ text-align: center;
131
  }
132
  #image-preview {
133
  max-width: 100%;
134
+ max-height: 280px;
 
135
  border-radius: var(--border-radius);
136
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
137
  display: none; /* Caché initialement */
138
+ border: 1px solid var(--border-color);
139
  }
140
 
141
+ .options-panel {
142
+ background-color: #f8f9fa;
143
+ padding: 20px 25px;
144
+ border-radius: var(--border-radius);
145
+ margin-bottom: 30px;
146
  text-align: left;
147
+ border: 1px solid var(--border-color);
148
  }
149
+ .options-panel h3 {
150
+ font-weight: 600;
151
+ color: var(--primary-color);
152
+ margin-bottom: 15px;
153
+ font-size: 1.2em;
154
+ display: flex;
155
+ align-items: center;
156
+ }
157
+ .options-panel h3 i {
158
+ margin-right: 10px;
159
+ color: var(--secondary-color);
160
+ }
161
+
162
  .prompt-selector label {
163
  font-weight: 500;
164
  margin-bottom: 8px;
165
  display: block;
166
+ color: var(--text-color);
167
+ font-size: 1em;
168
  }
169
  .prompt-selector select {
170
  width: 100%;
171
+ padding: 14px 18px;
172
  border-radius: var(--border-radius);
173
+ border: 1.5px solid var(--border-color);
174
  font-size: 1em;
175
+ font-family: var(--font-main);
176
+ background-color: var(--card-bg);
177
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
178
+ appearance: none; /* Pour styliser la flèche */
179
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%232c3e50' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3E%3C/svg%3E");
180
+ background-repeat: no-repeat;
181
+ background-position: right 18px center;
182
+ background-size: 16px;
183
  }
184
  .prompt-selector select:focus {
185
+ border-color: var(--secondary-color);
186
  outline: none;
187
+ box-shadow: 0 0 0 3px rgba(26, 188, 156, 0.2);
188
  }
189
 
190
  .button {
191
+ background-image: linear-gradient(to right, var(--secondary-color) 0%, #16a085 100%);
192
  color: white;
193
  border: none;
194
+ padding: 15px 30px;
195
+ font-size: 1.15em;
196
+ font-weight: 600;
197
+ letter-spacing: 0.5px;
198
  border-radius: var(--border-radius);
199
  cursor: pointer;
200
+ transition: all 0.35s cubic-bezier(0.25, 0.8, 0.25, 1);
201
  display: inline-flex;
202
  align-items: center;
203
  justify-content: center;
204
+ gap: 10px;
205
+ box-shadow: 0 4px 15px rgba(26, 188, 156, 0.2);
206
+ text-transform: uppercase;
207
  }
208
  .button:hover {
209
+ transform: translateY(-3px) scale(1.02);
210
+ box-shadow: 0 7px 20px rgba(26, 188, 156, 0.3);
211
+ }
212
+ .button:active {
213
+ transform: translateY(-1px) scale(0.98);
214
+ box-shadow: 0 2px 10px rgba(26, 188, 156, 0.2);
215
  }
216
  .button:disabled {
217
+ background-image: none;
218
+ background-color: #bdc3c7;
219
  cursor: not-allowed;
220
  transform: none;
221
  box-shadow: none;
222
+ color: #7f8c8d;
223
  }
224
+ .button.copy-button { /* Style un peu différent pour le bouton copier */
225
+ background-image: linear-gradient(to right, var(--primary-color) 0%, #34495e 100%);
226
+ box-shadow: 0 4px 15px rgba(44, 62, 80, 0.2);
227
+ font-size: 1em;
228
+ padding: 12px 25px;
229
+ text-transform: none;
230
  }
231
  .button.copy-button:hover {
232
+ box-shadow: 0 7px 20px rgba(44, 62, 80, 0.3);
 
233
  }
234
 
235
+
236
  #solving-container {
237
  display: none; /* Caché initialement */
238
+ margin-top: 35px;
239
+ padding: 30px;
240
+ background-color: #fbfcfd;
241
  border-radius: var(--border-radius);
242
  border: 1px solid var(--border-color);
243
  }
244
+ .status-section {
245
+ margin-bottom: 20px;
246
+ text-align: center;
247
+ }
248
+ .status-message {
249
+ font-size: 1.2em;
250
  font-weight: 500;
251
+ color: var(--primary-color);
252
+ display: flex;
253
+ align-items: center;
254
+ justify-content: center;
255
+ flex-wrap: wrap;
256
  }
257
+ .status-message i {
258
+ margin-right: 10px;
259
+ font-size: 1.3em;
260
+ transition: color 0.3s ease;
261
  }
262
+ .status-message .task-info {
263
+ font-size: 0.85em;
264
+ color: var(--subtle-text-color);
265
+ margin-left: 8px;
266
  font-weight: 400;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  }
268
 
269
+ .loading-spinner {
270
+ width: 40px;
271
+ height: 40px;
272
+ margin: 25px auto;
273
+ border: 5px solid rgba(44, 62, 80, 0.15);
274
+ border-left-color: var(--secondary-color);
275
  border-radius: 50%;
276
+ animation: spin 0.8s linear infinite;
 
 
277
  display: none; /* Caché initialement */
278
  }
279
  @keyframes spin {
280
+ to { transform: rotate(360deg); }
281
+ }
282
+
283
+ .telegram-notice {
284
+ background-color: #eaf2f8; /* Bleu très clair */
285
+ border-left: 5px solid var(--secondary-color);
286
+ padding: 15px 20px;
287
+ margin: 25px 0;
288
+ font-size: 1em;
289
+ border-radius: var(--border-radius);
290
+ color: var(--text-color);
291
+ display: flex;
292
+ align-items: center;
293
+ }
294
+ .telegram-notice i {
295
+ margin-right: 12px;
296
+ color: var(--secondary-color);
297
+ font-size: 1.4em;
298
  }
299
 
300
  .response-container {
301
+ margin-top: 25px;
302
+ padding: 25px;
303
  border: 1px solid var(--border-color);
304
  border-radius: var(--border-radius);
305
  background-color: var(--card-bg);
306
  display: none; /* Caché initialement */
307
  text-align: left;
308
  }
309
+ .response-container h3 {
310
+ font-weight: 600;
311
+ color: var(--primary-color);
 
 
 
 
 
 
 
312
  margin-bottom: 15px;
313
+ font-size: 1.25em;
314
+ display: flex;
315
+ align-items: center;
316
  }
317
+ .response-container h3 i {
318
+ margin-right: 10px;
319
+ color: var(--secondary-color);
320
  }
321
+ #response-output { /* Renommé pour plus de clarté */
322
+ font-family: var(--font-code);
323
+ background-color: #2d2d2d; /* Fond sombre pour code */
324
+ color: #d4d4d4; /* Texte clair pour code */
325
+ padding: 20px;
326
+ border-radius: var(--border-radius);
327
+ overflow-x: auto;
328
+ white-space: pre-wrap;
329
+ word-wrap: break-word;
330
+ max-height: 450px;
331
+ margin-bottom: 20px;
332
+ border: 1px solid #444;
333
+ line-height: 1.6;
334
+ }
335
+ /* Style pour les commentaires dans le code LaTeX (si possible à faire via JS) */
336
+ /* .latex-comment { color: #6a9955; font-style: italic; } */
337
 
338
+ .error-display {
339
  color: var(--accent-color);
340
+ background-color: #fbecec;
341
+ border: 1.5px solid var(--accent-color);
342
+ padding: 18px;
343
  border-radius: var(--border-radius);
344
+ margin: 20px 0;
345
+ font-weight: 500;
346
+ display: flex;
347
+ align-items: center;
348
+ }
349
+ .error-display i {
350
+ margin-right: 12px;
351
+ font-size: 1.3em;
352
  }
353
+ .error-display small {
354
+ display: block;
355
+ font-weight: 400;
356
+ color: #c0392b; /* Rouge plus foncé pour détails */
357
+ font-size: 0.9em;
358
+ margin-top: 5px;
359
  }
360
 
361
+
362
  .footer {
363
+ margin-top: 50px;
364
+ padding-bottom: 20px;
365
+ font-size: 0.95em;
366
+ color: var(--subtle-text-color);
367
  }
368
  .footer a {
369
+ color: var(--secondary-color);
370
  text-decoration: none;
371
+ font-weight: 500;
372
  }
373
  .footer a:hover {
374
  text-decoration: underline;
375
  }
376
 
377
  /* Responsive adjustments */
378
+ @media (max-width: 768px) {
379
  body { padding: 15px; }
380
+ .main-container { padding: 25px 30px; max-width: 95%; }
381
+ h1 { font-size: 1.9em; }
382
+ .subtitle { font-size: 1em; margin-bottom: 25px; }
383
+ .upload-section { padding: 25px; }
384
+ .upload-section p { font-size: 1.05em; }
385
+ .upload-section .upload-icon { font-size: 3em; }
386
+ .options-panel { padding: 15px 20px; }
387
+ .button { padding: 14px 25px; font-size: 1.05em; }
388
+ }
389
+ @media (max-width: 480px) {
390
+ h1 { font-size: 1.7em; }
391
+ .subtitle { font-size: 0.95em; }
392
  .upload-section { padding: 20px; }
393
+ .main-container { padding: 20px; }
 
394
  }
395
 
396
  </style>
397
  </head>
398
  <body>
399
  <div class="main-container">
400
+ <h1><i class="fas fa-atom logo-icon"></i>Solveur Expert IA</h1>
401
+ <p class="subtitle">Solutions LaTeX précises pour Maths, Physique et Chimie en Terminale</p>
402
 
403
  <div id="upload-section" class="upload-section">
404
+ <div class="upload-content">
405
+ <i class="fas fa-file-arrow-up upload-icon"></i>
406
+ <p>Déposez l'image de votre exercice ici</p>
407
+ <small>ou cliquez pour sélectionner un fichier (PNG, JPG)</small>
408
+ </div>
409
+ <input type="file" id="file-input" accept="image/png, image/jpeg, image/webp">
410
+ <div id="image-preview-container">
411
+ <img id="image-preview" src="#" alt="Aperçu de l'énoncé">
412
+ </div>
413
  </div>
414
 
415
+ <div class="options-panel">
416
+ <h3><i class="fas fa-cogs"></i>Options de Formatage</h3>
417
+ <div class="prompt-selector">
418
+ <label for="prompt-type">Style de la correction LaTeX :</label>
419
+ <select id="prompt-type" name="prompt-type">
420
+ <option value="refined">Format Raffiné & Complet (mise en page avancée)</option>
421
+ <option value="light">Format Léger & Essentiel (LaTeX standard)</option>
422
+ </select>
423
+ </div>
424
  </div>
425
 
426
  <button id="solve-button" class="button" disabled>
427
+ <i class="fas fa-rocket"></i>Obtenir la Solution
428
  </button>
429
 
430
  <div id="solving-container">
431
+ <div class="status-section">
432
+ <div class="status-message" id="status-message-element">
433
+ <i class="fas fa-hourglass-start"></i>Prêt à résoudre votre exercice...
434
+ </div>
435
  </div>
436
+ <div class="loading-spinner" id="loading-spinner-element"></div>
437
+
438
  <div class="telegram-notice">
439
+ <i class="fab fa-telegram"></i>Une copie de la solution sera envoyée sur Telegram pour archivage.
440
  </div>
441
+
442
+ <div class="response-container" id="response-container-element">
443
+ <h3><i class="fas fa-file-code"></i>Correction LaTeX Détaillée :</h3>
444
+ <div id="response-output"></div>
445
  <button id="copy-button" class="button copy-button">
446
+ <i class="fas fa-copy"></i>Copier le code LaTeX
447
  </button>
448
  </div>
449
+
450
+ <div id="error-display-element" class="error-display" style="display: none;">
451
  <!-- Les erreurs seront affichées ici -->
452
  </div>
453
  </div>
454
  </div>
455
 
456
  <footer class="footer">
457
+ Solutions générées par <a href="#" target="_blank">Mariam IA</a> © 2025 - Précision garantie.
458
  </footer>
459
 
460
  <script>
 
462
  const uploadSection = document.getElementById('upload-section');
463
  const fileInput = document.getElementById('file-input');
464
  const imagePreview = document.getElementById('image-preview');
465
+ const imagePreviewContainer = document.getElementById('image-preview-container');
466
  const solveButton = document.getElementById('solve-button');
467
  const solvingContainer = document.getElementById('solving-container');
468
+ const responseContainer = document.getElementById('response-container-element');
469
+ const responseOutputDiv = document.getElementById('response-output');
470
  const copyButton = document.getElementById('copy-button');
471
+ const statusMessageElement = document.getElementById('status-message-element');
472
+ const loadingSpinner = document.getElementById('loading-spinner-element');
473
  const promptTypeSelect = document.getElementById('prompt-type');
474
+ const errorDisplay = document.getElementById('error-display-element');
475
 
476
  let selectedFile = null;
477
+ let currentTaskId = null; // Pour suivre l'ID de la tâche en cours
478
+
479
+ // --- Drag and Drop Logic ---
480
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
481
  uploadSection.addEventListener(eventName, preventDefaults, false);
482
  });
 
484
  e.preventDefault();
485
  e.stopPropagation();
486
  }
 
487
  ['dragenter', 'dragover'].forEach(eventName => {
488
  uploadSection.addEventListener(eventName, () => uploadSection.classList.add('highlight-drag'), false);
489
  });
490
  ['dragleave', 'drop'].forEach(eventName => {
491
  uploadSection.addEventListener(eventName, () => uploadSection.classList.remove('highlight-drag'), false);
492
  });
 
493
  uploadSection.addEventListener('drop', (e) => {
494
  if (e.dataTransfer.files.length) {
495
  handleFileSelection(e.dataTransfer.files[0]);
496
  }
497
  });
498
+ // --- Fin Drag and Drop ---
499
+
500
  fileInput.addEventListener('change', (e) => {
501
  if (e.target.files.length) {
502
  handleFileSelection(e.target.files[0]);
 
504
  });
505
 
506
  function handleFileSelection(file) {
507
+ const allowedTypes = ['image/png', 'image/jpeg', 'image/webp'];
508
+ if (!allowedTypes.includes(file.type)) {
509
+ displayError('Format de fichier non supporté.', 'Veuillez utiliser PNG, JPG ou WEBP.');
510
  selectedFile = null;
511
  solveButton.disabled = true;
512
  imagePreview.style.display = 'none';
513
+ imagePreviewContainer.style.display = 'none';
514
  return;
515
  }
516
 
517
  selectedFile = file;
518
  solveButton.disabled = false;
519
+ errorDisplay.style.display = 'none';
520
 
521
  const reader = new FileReader();
522
  reader.onload = (e) => {
523
  imagePreview.src = e.target.result;
524
  imagePreview.style.display = 'block';
525
+ imagePreviewContainer.style.display = 'block';
526
  };
527
  reader.readAsDataURL(file);
528
  }
529
 
530
  function displayError(message, details = null) {
531
+ let fullMessage = `<i class="fas fa-shield-halved"></i> ${message}`;
532
  if (details) {
533
+ fullMessage += `<br><small>${escapeHtml(details)}</small>`;
534
  }
535
  errorDisplay.innerHTML = fullMessage;
536
  errorDisplay.style.display = 'block';
537
+ responseContainer.style.display = 'none';
538
  loadingSpinner.style.display = 'none';
539
+ updateStatusUI('error_user', null); // Statut spécifique pour erreurs d'input utilisateur
540
  }
541
 
542
  solveButton.addEventListener('click', () => {
 
545
  solveButton.disabled = true;
546
  solvingContainer.style.display = 'block';
547
  responseContainer.style.display = 'none';
548
+ responseOutputDiv.textContent = '';
549
+ errorDisplay.style.display = 'none';
550
+ loadingSpinner.style.display = 'block';
551
+ updateStatusUI('pending', null, 'Préparation de la résolution...');
552
 
553
  const formData = new FormData();
554
  formData.append('image', selectedFile);
 
559
  body: formData
560
  })
561
  .then(response => {
562
+ if (!response.ok) {
563
  return response.json().then(errData => {
564
+ throw new Error(errData.error || `Erreur serveur : ${response.status}`);
565
  });
566
  }
567
  return response.json();
568
  })
569
  .then(data => {
570
+ if (data.error) {
571
  throw new Error(data.error);
572
  }
573
 
574
+ currentTaskId = data.task_id;
575
+ updateStatusUI(data.status || 'pending', currentTaskId, "Lancement de l'analyse par l'IA...");
576
 
577
+ const eventSource = new EventSource('/stream/' + currentTaskId);
578
 
579
  eventSource.onmessage = function(event) {
580
  const streamData = JSON.parse(event.data);
581
 
582
  if (streamData.error) {
583
  displayError(streamData.error, streamData.error_detail);
584
+ if (streamData.response) { // Afficher LaTeX partiel si dispo
585
+ responseOutputDiv.textContent = streamData.response;
 
586
  responseContainer.style.display = 'block';
587
  }
588
  eventSource.close();
 
591
  return;
592
  }
593
 
594
+ updateStatusUI(streamData.status, currentTaskId);
595
 
596
  if (streamData.status === 'completed' || streamData.status === 'completed_tex_only' || streamData.status === 'pdf_error') {
597
  responseContainer.style.display = 'block';
598
  loadingSpinner.style.display = 'none';
599
 
600
  if (streamData.response) {
601
+ responseOutputDiv.textContent = streamData.response;
602
  }
603
 
604
  if (streamData.status === 'pdf_error' && streamData.error_detail) {
605
+ const statusEl = statusMessageElement.querySelector('.status-text');
606
+ if(statusEl) statusEl.innerHTML += `<br><small class="pdf-error-detail"><i class="fas fa-file-invoice"></i> Erreur PDF: ${escapeHtml(streamData.error_detail)}</small>`;
 
607
  }
608
 
609
  eventSource.close();
 
613
 
614
  eventSource.onerror = function() {
615
  eventSource.close();
616
+ fetch('/task/' + currentTaskId) // Utiliser currentTaskId
 
617
  .then(resp => resp.json())
618
  .then(taskData => {
619
+ updateStatusUI(taskData.status, currentTaskId);
620
  if (taskData.status === 'completed' || taskData.status === 'completed_tex_only' || taskData.status === 'pdf_error') {
621
  responseContainer.style.display = 'block';
622
  if (taskData.response) {
623
+ responseOutputDiv.textContent = taskData.response;
624
  }
625
  if (taskData.status === 'pdf_error' && taskData.error_detail) {
626
+ const statusEl = statusMessageElement.querySelector('.status-text');
627
+ if(statusEl) statusEl.innerHTML += `<br><small class="pdf-error-detail"><i class="fas fa-file-invoice"></i> Erreur PDF: ${escapeHtml(taskData.error_detail)}</small>`;
628
  }
629
  } else if (taskData.status === 'error') {
630
+ displayError(taskData.error || 'Erreur inattendue lors de la récupération de la tâche.', taskData.error_detail);
631
  } else {
632
+ displayError('Connexion interrompue.', 'Le traitement se poursuit en arrière-plan. Vérifiez Telegram.');
 
633
  }
634
  })
635
  .catch(error => {
636
+ displayError('Erreur de récupération du statut.', error.message);
637
  })
638
  .finally(() => {
639
  solveButton.disabled = false;
 
642
  };
643
  })
644
  .catch(error => {
645
+ displayError(error.message || 'Erreur de communication serveur.');
646
  solveButton.disabled = false;
647
  loadingSpinner.style.display = 'none';
648
  });
649
  });
650
 
651
+ function updateStatusUI(status, taskId, overrideMessage = null) {
652
  const selectedPromptText = promptTypeSelect.options[promptTypeSelect.selectedIndex].text.split('(')[0].trim();
653
+ let statusMsg = overrideMessage || '';
654
+ let iconClass = 'fas fa-hourglass-start';
655
+ let iconColor = 'var(--primary-color)';
656
+
657
+ if (!overrideMessage) {
658
+ switch(status) {
659
+ case 'pending': statusMsg = "Lancement de l'analyse par l'IA..."; iconClass = 'fas fa-play-circle'; break;
660
+ case 'processing': statusMsg = "L'IA déchiffre votre exercice..."; iconClass = 'fas fa-brain'; iconColor = 'var(--secondary-color)'; break;
661
+ case 'generating_latex': statusMsg = "Construction de la solution LaTeX..."; iconClass = 'fas fa-scroll'; break;
662
+ case 'cleaning_latex': statusMsg = "Peaufinage du code LaTeX..."; iconClass = 'fas fa-magic-wand-sparkles'; break;
663
+ case 'generating_pdf': statusMsg = "Compilation du document PDF final..."; iconClass = 'fas fa-file-pdf'; iconColor = '#e74c3c'; break; // Rouge pour PDF
664
+ case 'completed': statusMsg = "Solution Complète et Précise Générée !"; iconClass = 'fas fa-check-double'; iconColor = 'var(--success-color)'; break;
665
+ case 'completed_tex_only': statusMsg = "Solution LaTeX Précise Générée ! (PDF non requis/dispo)"; iconClass = 'fas fa-check-circle'; iconColor = 'var(--success-color)'; break;
666
+ case 'pdf_error': statusMsg = "Solution LaTeX Précise Générée ! (Erreur PDF)"; iconClass = 'fas fa-file-excel'; iconColor = '#f39c12'; break; // Orange pour erreur PDF
667
+ case 'error': statusMsg = "Une anomalie technique est survenue."; iconClass = 'fas fa-times-circle'; iconColor = 'var(--accent-color)'; break;
668
+ case 'error_user': statusMsg = "Veuillez vérifier votre image."; iconClass = 'fas fa-exclamation-triangle'; iconColor = 'var(--accent-color)'; break;
669
+ default: statusMsg = `Progression: ${status}`; iconClass = 'fas fa-spinner fa-spin'; // spinner pour statut inconnu
670
+ }
671
  }
672
 
673
+ let taskInfoHtml = '';
674
  if (taskId) {
675
+ taskInfoHtml = `<span class="task-info">(Tâche ${taskId.substring(0,6)} | Style: ${selectedPromptText})</span>`;
676
  }
677
 
678
+ statusMessageElement.innerHTML = `<i class="${iconClass}" style="color:${iconColor};"></i> <span class="status-text">${statusMsg}</span> ${taskInfoHtml}`;
 
679
  }
680
 
681
  function escapeHtml(unsafe) {
682
+ if (typeof unsafe !== 'string') return '';
683
+ return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
 
 
 
 
 
 
 
684
  }
685
 
686
  copyButton.addEventListener('click', () => {
687
+ const textToCopy = responseOutputDiv.textContent;
688
  navigator.clipboard.writeText(textToCopy).then(() => {
689
  const originalIcon = copyButton.querySelector('i').className;
690
+ const originalText = copyButton.childNodes[1] ? copyButton.childNodes[1].nodeValue : ' Copier le code LaTeX'; // Safer access
691
+ copyButton.innerHTML = `<i class="fas fa-check"></i> Code Copié !`;
692
+ copyButton.style.backgroundColor = 'var(--success-color)'; // Feedback visuel
693
  setTimeout(() => {
694
  copyButton.innerHTML = `<i class="${originalIcon}"></i>${originalText}`;
695
+ copyButton.style.backgroundColor = ''; // Reset style
696
+ }, 2500);
697
  }).catch(err => {
698
  console.error('Erreur de copie: ', err);
699
+ displayError('Copie échouée.', 'Veuillez copier manuellement le texte.');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
700
  });
701
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702
  });
703
  </script>
704
  </body>