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

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +550 -248
templates/index.html CHANGED
@@ -3,300 +3,602 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Résolution d'exercices (Maths, Physique et Chimie)</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
 
 
 
 
 
 
 
8
  <style>
9
- /* Custom scrollbar for pre block (optional) */
10
- pre::-webkit-scrollbar {
11
- width: 8px;
12
- height: 8px;
 
 
 
 
 
 
 
13
  }
14
- pre::-webkit-scrollbar-track {
15
- background: #f1f1f1;
16
- border-radius: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  }
18
- pre::-webkit-scrollbar-thumb {
19
- background: #888;
20
- border-radius: 10px;
 
 
 
 
 
21
  }
22
- pre::-webkit-scrollbar-thumb:hover {
23
- background: #555;
 
24
  }
25
- .spinner {
 
26
  border: 4px solid rgba(0, 0, 0, 0.1);
27
  width: 36px;
28
  height: 36px;
29
  border-radius: 50%;
30
- border-left-color: #0ea5e9; /* sky-500 */
31
  animation: spin 1s ease infinite;
 
 
32
  }
33
  @keyframes spin {
34
  0% { transform: rotate(0deg); }
35
  100% { transform: rotate(360deg); }
36
  }
37
- </style>
38
- </head>
39
- <body class="bg-slate-100 text-slate-800 min-h-screen flex flex-col items-center justify-center p-4 font-sans">
40
 
41
- <div class="bg-white p-6 sm:p-8 rounded-xl shadow-2xl w-full max-w-2xl">
42
- <header class="mb-6 sm:mb-8 text-center">
43
- <h1 class="text-2xl sm:text-3xl font-bold text-sky-700">
44
- Résolution d'Exercices
45
- </h1>
46
- <p class="text-sm text-slate-500">Mathématiques, Physique et Chimie</p>
47
- </header>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- <form id="solveForm" class="space-y-6">
50
- <div>
51
- <label for="imageUpload" class="block text-sm font-medium text-gray-700 mb-1">
52
- Télécharger une image de l'exercice :
53
- </label>
54
- <input type="file" id="imageUpload" name="image" accept="image/*" required
55
- class="block w-full text-sm text-slate-500
56
- file:mr-4 file:py-2 file:px-4
57
- file:rounded-full file:border-0
58
- file:text-sm file:font-semibold
59
- file:bg-sky-50 file:text-sky-700
60
- hover:file:bg-sky-100
61
- border border-gray-300 rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-sky-500">
62
- </div>
63
 
64
- <div>
65
- <span class="block text-sm font-medium text-gray-700 mb-2">Choisir le format de la solution :</span>
66
- <div class="space-y-2">
67
- <div class="flex items-center">
68
- <input id="promptRefined" name="prompt_type" type="radio" value="refined" checked
69
- class="h-4 w-4 text-sky-600 border-gray-300 focus:ring-sky-500">
70
- <label for="promptRefined" class="ml-2 block text-sm text-gray-900">
71
- Format Raffiné & Complet <span class="text-xs text-gray-500">(mise en page avancée)</span>
72
- </label>
73
- </div>
74
- <div class="flex items-center">
75
- <input id="promptLight" name="prompt_type" type="radio" value="light"
76
- class="h-4 w-4 text-sky-600 border-gray-300 focus:ring-sky-500">
77
- <label for="promptLight" class="ml-2 block text-sm text-gray-900">
78
- Format simple
79
- </label>
80
- </div>
81
- </div>
82
- </div>
83
 
84
- <button type="submit" id="submitButton"
85
- class="w-full bg-sky-600 hover:bg-sky-700 text-white font-bold py-2.5 px-4 rounded-md
86
- focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2
87
- transition duration-150 ease-in-out
88
- disabled:bg-slate-400 disabled:cursor-not-allowed">
89
- Résoudre l'exercice
90
- </button>
91
- </form>
 
92
 
93
- <div id="statusArea" class="mt-6 space-y-3 hidden">
94
- <div class="flex items-center space-x-3 p-3 rounded-md bg-sky-50 border border-sky-200">
95
- <div id="spinner" class="spinner hidden"></div>
96
- <p id="statusMessage" class="text-sm font-medium text-sky-700"></p>
97
- </div>
98
- <div id="errorMessage" class="p-3 rounded-md bg-red-50 border border-red-200 text-red-700 text-sm hidden"></div>
99
- <div id="errorDetailMessage" class="p-3 rounded-md bg-amber-50 border border-amber-200 text-amber-700 text-sm hidden"></div>
 
 
 
 
100
  </div>
101
 
102
- <div id="resultArea" class="mt-6 hidden">
103
- <h3 class="text-lg font-semibold text-slate-700 mb-2">Code LaTeX Généré :</h3>
104
- <div class="bg-gray-900 text-gray-100 p-4 rounded-md shadow">
105
- <pre><code id="latexOutput" class="text-sm whitespace-pre-wrap break-all block max-h-96 overflow-auto"></code></pre>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  </div>
107
- <button id="copyLatexButton" class="mt-3 bg-slate-200 hover:bg-slate-300 text-slate-700 text-sm font-medium py-1.5 px-3 rounded-md transition duration-150 ease-in-out">
108
- Copier le LaTeX
109
- </button>
110
  </div>
111
-
112
  </div>
113
 
114
- <footer class="mt-8 text-center text-xs text-slate-500">
115
- <p>© 2025 Mariam AI - Solution Propulsée par Mariam</p>
116
  </footer>
117
 
118
  <script>
119
- const solveForm = document.getElementById('solveForm');
120
- const submitButton = document.getElementById('submitButton');
121
- const imageUpload = document.getElementById('imageUpload');
122
-
123
- const statusArea = document.getElementById('statusArea');
124
- const spinner = document.getElementById('spinner');
125
- const statusMessage = document.getElementById('statusMessage');
126
- const errorMessage = document.getElementById('errorMessage');
127
- const errorDetailMessage = document.getElementById('errorDetailMessage');
128
-
129
- const resultArea = document.getElementById('resultArea');
130
- const latexOutput = document.getElementById('latexOutput');
131
- const copyLatexButton = document.getElementById('copyLatexButton');
132
-
133
- let eventSource = null;
134
-
135
- solveForm.addEventListener('submit', async function(event) {
136
- event.preventDefault();
137
-
138
- if (!imageUpload.files || imageUpload.files.length === 0) {
139
- showError("Veuillez sélectionner un fichier image.");
140
- return;
 
 
141
  }
142
 
143
- submitButton.disabled = true;
144
- showStatus("Initialisation...", true);
145
- hideError();
146
- hideResult();
147
-
148
- const formData = new FormData(solveForm);
149
-
150
- try {
151
- const response = await fetch('/solve', {
152
- method: 'POST',
153
- body: formData
154
- });
155
-
156
- if (!response.ok) {
157
- const errorData = await response.json().catch(() => ({ error: 'Erreur serveur inconnue' }));
158
- throw new Error(errorData.error || `Erreur HTTP: ${response.status}`);
159
  }
160
-
161
- const data = await response.json();
162
- if (data.task_id) {
163
- showStatus("Tâche créée. En attente de la réponse...", true);
164
- startStreaming(data.task_id);
165
- } else {
166
- throw new Error("ID de tâche non reçu.");
167
  }
168
-
169
- } catch (error) {
170
- showError(`Erreur lors de la soumission : ${error.message}`);
171
- submitButton.disabled = false;
172
- hideStatus();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  }
174
- });
175
 
176
- function startStreaming(taskId) {
177
- if (eventSource) {
178
- eventSource.close();
 
 
 
 
 
 
179
  }
180
- eventSource = new EventSource(`/stream/${taskId}`);
181
 
182
- showStatus("Connexion au flux de progression...", true);
183
-
184
- eventSource.onmessage = function(event) {
185
- const data = JSON.parse(event.data);
186
-
187
- let userFriendlyStatus = data.status;
188
- switch(data.status) {
189
- case 'pending': userFriendlyStatus = "En attente de traitement..."; break;
190
- case 'processing': userFriendlyStatus = "Traitement de l'image..."; break;
191
- case 'generating_latex': userFriendlyStatus = "Génération du code LaTeX en cours..."; break;
192
- case 'cleaning_latex': userFriendlyStatus = "Nettoyage du code LaTeX..."; break;
193
- case 'generating_pdf': userFriendlyStatus = "Génération du PDF en cours..."; break;
194
- case 'completed': userFriendlyStatus = "Terminé ! Solution générée (LaTeX et PDF)."; break;
195
- case 'completed_tex_only': userFriendlyStatus = "Terminé ! Solution LaTeX générée (PDF non disponible)."; break;
196
- case 'pdf_error': userFriendlyStatus = "Erreur lors de la génération du PDF. Le code LaTeX est disponible."; break;
197
- case 'error': userFriendlyStatus = "Une erreur est survenue."; break;
198
- }
199
- showStatus(userFriendlyStatus, !['completed', 'error', 'pdf_error', 'completed_tex_only'].includes(data.status));
200
-
201
-
202
- if (data.error) {
203
- showError(data.error);
204
- } else {
205
- hideError(); // Hide general error if a specific one is not present in this message
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  }
207
- if (data.error_detail) {
208
- showErrorDetail(data.error_detail);
209
- } else {
210
- hideErrorDetail();
211
  }
212
 
 
 
 
213
 
214
- if (data.response) {
215
- latexOutput.textContent = data.response;
216
- showResult();
217
  }
218
-
219
- if (['completed', 'error', 'pdf_error', 'completed_tex_only'].includes(data.status)) {
220
- eventSource.close();
221
- submitButton.disabled = false;
222
- if (data.status !== 'error' && !data.error) { // If not a critical error, keep status positive
223
- // Status already set by switch
224
- } else if (data.error) {
225
- showStatus("Traitement terminé avec des erreurs.", false);
226
- }
227
- }
228
- };
229
-
230
- eventSource.onerror = function(err) {
231
- console.error("Erreur EventSource:", err);
232
- showError("Erreur de connexion au serveur pour les mises à jour. Veuillez réessayer.");
233
- showStatus("Déconnecté.", false);
234
- eventSource.close();
235
- submitButton.disabled = false;
236
- };
237
- }
238
-
239
- function showStatus(message, showSpinner = false) {
240
- statusArea.classList.remove('hidden');
241
- statusMessage.textContent = message;
242
- if (showSpinner) {
243
- spinner.classList.remove('hidden');
244
- } else {
245
- spinner.classList.add('hidden');
246
  }
247
- }
248
-
249
- function hideStatus() {
250
- statusArea.classList.add('hidden');
251
- spinner.classList.add('hidden');
252
- }
253
-
254
- function showError(message) {
255
- errorMessage.textContent = message;
256
- errorMessage.classList.remove('hidden');
257
- statusArea.classList.remove('hidden'); // Ensure status area is visible to show error
258
- }
259
- function hideError() {
260
- errorMessage.classList.add('hidden');
261
- errorMessage.textContent = '';
262
- }
263
-
264
- function showErrorDetail(message) {
265
- errorDetailMessage.textContent = message;
266
- errorDetailMessage.classList.remove('hidden');
267
- statusArea.classList.remove('hidden');
268
- }
269
- function hideErrorDetail() {
270
- errorDetailMessage.classList.add('hidden');
271
- errorDetailMessage.textContent = '';
272
- }
273
-
274
-
275
- function showResult() {
276
- resultArea.classList.remove('hidden');
277
- }
278
- function hideResult() {
279
- resultArea.classList.add('hidden');
280
- latexOutput.textContent = '';
281
- }
282
-
283
- copyLatexButton.addEventListener('click', () => {
284
- if (latexOutput.textContent) {
285
- navigator.clipboard.writeText(latexOutput.textContent)
286
- .then(() => {
287
- const originalText = copyLatexButton.textContent;
288
- copyLatexButton.textContent = 'Copié !';
289
- setTimeout(() => {
290
- copyLatexButton.textContent = originalText;
291
  }, 2000);
292
- })
293
- .catch(err => {
294
- console.error('Erreur de copie: ', err);
295
- alert('Erreur lors de la copie du texte.');
296
- });
 
 
 
 
 
 
 
297
  }
 
 
 
 
 
 
 
298
  });
299
-
300
  </script>
301
  </body>
302
  </html>
 
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');
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
+ });
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
  }
362
+ });
363
+
364
+ fileInput.addEventListener('change', (e) => {
365
+ if (e.target.files.length) {
366
+ handleFileSelection(e.target.files[0]);
 
 
367
  }
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', () => {
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;
473
+ }
474
+ };
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>
604
  </html>