Docfile commited on
Commit
7cb4f80
·
verified ·
1 Parent(s): b89224d

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +168 -426
templates/index.html CHANGED
@@ -3,324 +3,149 @@
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> &copy; 2024
322
- </footer>
323
-
324
  <script>
325
  document.addEventListener('DOMContentLoaded', function() {
326
  const uploadSection = document.getElementById('upload-section');
@@ -329,33 +154,29 @@
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
- });
346
- function preventDefaults(e) {
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
  }
@@ -369,16 +190,12 @@
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) => {
@@ -387,86 +204,73 @@
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', () => {
403
  if (!selectedFile) return;
404
 
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);
415
- formData.append('prompt_type', promptTypeSelect.value);
416
 
 
417
  fetch('/solve', {
418
  method: 'POST',
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();
450
  solveButton.disabled = false;
451
- loadingSpinner.style.display = 'none';
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();
472
  solveButton.disabled = false;
@@ -475,129 +279,67 @@
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;
504
- loadingSpinner.style.display = 'none';
505
  });
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, "&amp;")
548
- .replace(/</g, "&lt;")
549
- .replace(/>/g, "&gt;")
550
- .replace(/"/g, "&quot;")
551
- .replace(/'/g, "&#039;");
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>Résolveur d'Images</title>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/katex.min.js"></script>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/contrib/auto-render.min.js"></script>
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/katex.min.css">
 
 
 
 
 
10
  <style>
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  body {
12
+ font-family: Arial, sans-serif;
13
+ max-width: 800px;
14
+ margin: 0 auto;
 
15
  padding: 20px;
16
+ line-height: 1.6;
 
 
 
 
17
  }
18
+ h1 {
 
 
 
 
 
 
 
19
  text-align: center;
20
+ color: #333;
21
  }
22
+ .container {
 
 
 
 
23
  display: flex;
24
+ flex-direction: column;
25
+ gap: 20px;
 
 
 
 
26
  }
 
27
  .upload-section {
28
+ display: flex;
29
+ flex-direction: column;
30
+ align-items: center;
31
+ padding: 20px;
32
+ border: 2px dashed #ccc;
33
+ border-radius: 8px;
34
  cursor: pointer;
35
+ transition: all 0.3s;
 
 
36
  }
37
  .upload-section:hover {
38
+ border-color: #888;
 
 
 
 
 
 
 
 
 
 
 
39
  }
40
  #file-input {
41
  display: none;
42
  }
43
+ .preview-container {
44
+ width: 100%;
45
+ text-align: center;
46
+ margin-top: 10px;
47
+ }
48
  #image-preview {
49
  max-width: 100%;
50
+ max-height: 300px;
51
+ display: none;
 
 
 
 
 
 
 
 
52
  }
53
+ #solving-container {
54
+ display: none;
55
+ background-color: #f5f5f5;
56
+ padding: 20px;
57
+ border-radius: 8px;
58
  }
59
+ .response-container {
60
+ margin-top: 20px;
61
+ padding: 20px;
62
+ border: 1px solid #ddd;
63
+ border-radius: 8px;
 
 
64
  background-color: #fff;
65
+ display: none;
66
  }
67
+ .thinking {
68
+ color: #777;
69
+ font-style: italic;
 
70
  }
 
71
  .button {
72
+ background-color: #4CAF50;
73
  color: white;
74
  border: none;
75
+ padding: 10px 20px;
76
+ text-align: center;
77
+ text-decoration: none;
78
+ display: inline-block;
79
+ font-size: 16px;
80
+ margin: 10px 2px;
81
  cursor: pointer;
82
+ border-radius: 4px;
83
+ transition: background-color 0.3s;
 
 
 
84
  }
85
  .button:hover {
86
+ background-color: #45a049;
 
 
87
  }
88
  .button:disabled {
89
+ background-color: #cccccc;
90
  cursor: not-allowed;
 
 
91
  }
92
+ .copy-button {
93
+ background-color: #2196F3;
94
  }
95
+ .copy-button:hover {
96
+ background-color: #0b7dda;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  }
98
  .telegram-notice {
99
  background-color: #e3f2fd;
100
+ border-left: 4px solid #2196F3;
101
+ padding: 10px;
102
+ margin: 15px 0;
103
+ font-size: 14px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  }
105
+ .loading {
106
+ text-align: center;
107
+ font-style: italic;
108
+ margin: 10px 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  }
110
+ .status {
111
+ text-align: center;
112
+ margin-bottom: 10px;
113
+ font-weight: bold;
114
  }
115
+ .status small {
116
+ font-weight: normal;
117
+ color: #666;
118
+ font-size: 0.85em;
 
 
 
 
 
119
  }
 
120
  </style>
121
  </head>
122
  <body>
123
+ <h1>Résolveur d'Images</h1>
124
+
125
+ <div class="container">
126
  <div id="upload-section" class="upload-section">
 
127
  <p>Cliquez ou glissez-déposez une image ici</p>
128
  <input type="file" id="file-input" accept="image/*">
129
+ <div class="preview-container">
130
+ <img id="image-preview" src="#" alt="Aperçu de l'image">
131
+ </div>
 
 
 
 
 
 
132
  </div>
133
 
134
+ <button id="solve-button" class="button" disabled>Résoudre</button>
 
 
135
 
136
  <div id="solving-container">
137
+ <div class="status" id="status">En attente de résolution...</div>
 
 
 
138
  <div class="telegram-notice">
139
+ La réponse complète sera également envoyée sous forme de fichier texte sur Telegram.
140
  </div>
141
+ <div class="loading" id="loading-text">Traitement en cours...</div>
142
  <div class="response-container" id="response-container">
 
143
  <div id="response"></div>
144
+ <button id="copy-button" class="button copy-button">Copier la réponse</button>
 
 
 
 
 
145
  </div>
146
  </div>
147
  </div>
148
 
 
 
 
 
149
  <script>
150
  document.addEventListener('DOMContentLoaded', function() {
151
  const uploadSection = document.getElementById('upload-section');
 
154
  const solveButton = document.getElementById('solve-button');
155
  const solvingContainer = document.getElementById('solving-container');
156
  const responseContainer = document.getElementById('response-container');
157
+ const response = document.getElementById('response');
158
  const copyButton = document.getElementById('copy-button');
159
+ const statusElement = document.getElementById('status');
160
+ const loadingText = document.getElementById('loading-text');
 
 
161
 
162
  let selectedFile = null;
163
 
164
+ // Événements pour l'upload d'image
165
  uploadSection.addEventListener('click', () => fileInput.click());
166
 
167
+ uploadSection.addEventListener('dragover', (e) => {
 
 
 
168
  e.preventDefault();
169
+ uploadSection.style.borderColor = '#2196F3';
 
 
 
 
170
  });
171
+
172
+ uploadSection.addEventListener('dragleave', () => {
173
+ uploadSection.style.borderColor = '#ccc';
174
  });
175
 
176
  uploadSection.addEventListener('drop', (e) => {
177
+ e.preventDefault();
178
+ uploadSection.style.borderColor = '#ccc';
179
+
180
  if (e.dataTransfer.files.length) {
181
  handleFileSelection(e.dataTransfer.files[0]);
182
  }
 
190
 
191
  function handleFileSelection(file) {
192
  if (!file.type.startsWith('image/')) {
193
+ alert('Veuillez sélectionner une image valide');
 
 
 
194
  return;
195
  }
196
 
197
  selectedFile = file;
198
  solveButton.disabled = false;
 
199
 
200
  const reader = new FileReader();
201
  reader.onload = (e) => {
 
204
  };
205
  reader.readAsDataURL(file);
206
  }
 
 
 
 
 
 
 
 
 
 
 
207
 
208
+ // Événement pour résoudre l'image
209
  solveButton.addEventListener('click', () => {
210
  if (!selectedFile) return;
211
 
212
  solveButton.disabled = true;
213
  solvingContainer.style.display = 'block';
214
  responseContainer.style.display = 'none';
215
+ statusElement.textContent = 'En attente de résolution...';
216
+ loadingText.style.display = 'block';
217
+ response.innerHTML = '';
 
218
 
219
  const formData = new FormData();
220
  formData.append('image', selectedFile);
 
221
 
222
+ // Soumettre l'image pour traitement en arrière-plan
223
  fetch('/solve', {
224
  method: 'POST',
225
  body: formData
226
  })
227
+ .then(response => response.json())
 
 
 
 
 
 
 
228
  .then(data => {
229
+ if (data.error) {
230
  throw new Error(data.error);
231
  }
232
 
233
  const taskId = data.task_id;
234
+ statusElement.textContent = 'Traitement en arrière-plan (ID: ' + taskId + ')';
235
 
236
+ // Création d'une connexion SSE pour suivre le progrès
237
  const eventSource = new EventSource('/stream/' + taskId);
238
+ let fullResponse = '';
239
 
240
  eventSource.onmessage = function(event) {
241
+ const data = JSON.parse(event.data);
242
 
243
+ if (data.error) {
244
+ statusElement.textContent = 'Erreur:';
245
+ response.innerHTML = data.error;
246
+ responseContainer.style.display = 'block';
247
+ loadingText.style.display = 'none';
 
 
248
  eventSource.close();
249
  solveButton.disabled = false;
 
250
  return;
251
  }
252
 
253
+ if (data.status === 'pending') {
254
+ statusElement.textContent = 'En attente de traitement...';
255
+ } else if (data.status === 'processing') {
256
+ statusElement.textContent = 'Gemini traite votre image...';
257
+ statusElement.innerHTML += '<br><small>La réponse sera également envoyée sur Telegram</small>';
258
+ } else if (data.status === 'completed') {
259
+ statusElement.textContent = 'Traitement terminé!';
260
  responseContainer.style.display = 'block';
261
+ loadingText.style.display = 'none';
262
 
263
+ fullResponse = data.response;
264
+ response.innerHTML = fullResponse;
265
+ renderMathInElement(response);
266
+
267
+ eventSource.close();
268
+ solveButton.disabled = false;
269
+ } else if (data.status === 'error') {
270
+ statusElement.textContent = 'Erreur:';
271
+ response.innerHTML = data.error || 'Une erreur inattendue est survenue';
272
+ responseContainer.style.display = 'block';
273
+ loadingText.style.display = 'none';
274
 
275
  eventSource.close();
276
  solveButton.disabled = false;
 
279
 
280
  eventSource.onerror = function() {
281
  eventSource.close();
282
+ // Essayer de r��cupérer le statut via une requête GET normale
283
  fetch('/task/' + taskId)
284
+ .then(response => response.json())
285
  .then(taskData => {
286
+ if (taskData.status === 'completed') {
287
+ statusElement.textContent = 'Traitement terminé!';
288
  responseContainer.style.display = 'block';
289
+ loadingText.style.display = 'none';
290
+
291
+ response.innerHTML = taskData.response;
292
+ renderMathInElement(response);
 
 
 
293
  } else if (taskData.status === 'error') {
294
+ throw new Error(taskData.error || 'Une erreur inattendue est survenue');
 
 
 
295
  }
296
  })
297
  .catch(error => {
298
+ statusElement.textContent = 'Erreur de connexion:';
299
+ response.innerHTML = 'La connexion a été perdue, mais le traitement continue en arrière-plan. La réponse sera envoyée sur Telegram.';
300
+ responseContainer.style.display = 'block';
301
+ loadingText.style.display = 'none';
302
  })
303
  .finally(() => {
304
  solveButton.disabled = false;
 
305
  });
306
  };
307
  })
308
  .catch(error => {
309
+ statusElement.textContent = 'Erreur:';
310
+ response.innerHTML = error.message || 'Une erreur est survenue lors de la communication avec le serveur.';
311
+ responseContainer.style.display = 'block';
312
+ loadingText.style.display = 'none';
313
  solveButton.disabled = false;
 
314
  });
315
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
 
317
+ // Événement pour copier la réponse
318
  copyButton.addEventListener('click', () => {
319
+ const range = document.createRange();
320
+ range.selectNode(response);
321
+ window.getSelection().removeAllRanges();
322
+ window.getSelection().addRange(range);
323
+ document.execCommand('copy');
324
+ window.getSelection().removeAllRanges();
325
+
326
+ copyButton.textContent = 'Copié!';
327
+ setTimeout(() => {
328
+ copyButton.textContent = 'Copier la réponse';
329
+ }, 2000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  });
331
+ });
332
 
333
+ // Rendu des formules LaTeX
334
+ document.addEventListener('DOMContentLoaded', function() {
 
 
 
 
 
335
  renderMathInElement(document.body, {
336
  delimiters: [
337
+ {left: '$$', right: '$$', display: true},
338
+ {left: '$', right: '$', display: false},
339
+ {left: '\\(', right: '\\)', display: false},
340
+ {left: '\\[', right: '\\]', display: true}
341
  ]
342
  });
 
343
  });
344
  </script>
345
  </body>