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

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +426 -168
templates/index.html CHANGED
@@ -3,149 +3,324 @@
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,29 +329,33 @@
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,12 +369,16 @@
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,73 +387,86 @@
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,67 +475,129 @@
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>
 
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>
325
  document.addEventListener('DOMContentLoaded', function() {
326
  const uploadSection = document.getElementById('upload-section');
 
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
 
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
  };
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
 
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, "&")
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>