File size: 23,744 Bytes
79914b8
 
 
 
 
1c9f000
ad494e0
 
45da2f6
1c9f000
45da2f6
1c9f000
79914b8
10970c2
45da2f6
10970c2
 
 
 
ad494e0
1c9f000
10970c2
79914b8
10970c2
45da2f6
10970c2
 
 
45da2f6
10970c2
ad494e0
10970c2
ad494e0
79914b8
10970c2
 
ad494e0
79914b8
10970c2
45da2f6
1c9f000
 
10970c2
 
 
1c9f000
10970c2
ad494e0
10970c2
ad494e0
 
45da2f6
79914b8
ad494e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79914b8
10970c2
 
ae43fd8
 
10970c2
 
 
45da2f6
ad494e0
1c9f000
ae43fd8
10970c2
 
1c9f000
10970c2
 
 
1c9f000
45da2f6
 
10970c2
ae43fd8
 
10970c2
1c9f000
10970c2
 
ae43fd8
10970c2
 
 
 
 
 
 
1c9f000
79914b8
 
10970c2
 
45da2f6
ae43fd8
 
 
 
79914b8
10970c2
 
ae43fd8
10970c2
1c9f000
45da2f6
1c9f000
ae43fd8
10970c2
ae43fd8
 
10970c2
35fdca9
1c9f000
10970c2
ae43fd8
 
10970c2
ad494e0
10970c2
 
45da2f6
 
1c9f000
ae43fd8
79914b8
 
43f84cb
 
ad494e0
ae43fd8
45da2f6
 
79914b8
 
 
 
 
 
 
1c9f000
 
79914b8
1c9f000
 
45da2f6
1c9f000
79914b8
 
10970c2
1c9f000
10970c2
45da2f6
 
 
 
79914b8
45da2f6
 
10970c2
ad494e0
 
 
 
45da2f6
10970c2
ad494e0
10970c2
79914b8
10970c2
45da2f6
10970c2
 
 
79914b8
10970c2
79914b8
 
 
 
 
10970c2
79914b8
 
 
 
 
 
 
1c9f000
 
 
45da2f6
 
10970c2
79914b8
 
 
 
 
10970c2
79914b8
 
 
 
10970c2
79914b8
 
43f84cb
45da2f6
 
10970c2
45da2f6
10970c2
45da2f6
 
10970c2
 
 
 
45da2f6
79914b8
 
 
 
 
10970c2
 
1c9f000
10970c2
 
1c9f000
79914b8
 
 
45da2f6
79914b8
 
 
 
35fdca9
45da2f6
1c9f000
45da2f6
1c9f000
45da2f6
 
 
 
35fdca9
1c9f000
35fdca9
79914b8
 
1c9f000
 
35fdca9
1c9f000
79914b8
 
45da2f6
79914b8
45da2f6
 
10970c2
1c9f000
10970c2
45da2f6
35fdca9
 
10970c2
35fdca9
79914b8
 
1c9f000
45da2f6
 
10970c2
 
35fdca9
45da2f6
1c9f000
45da2f6
 
 
1c9f000
10970c2
45da2f6
35fdca9
79914b8
 
 
 
 
 
 
10970c2
45da2f6
35fdca9
1c9f000
45da2f6
10970c2
45da2f6
1c9f000
45da2f6
 
1c9f000
10970c2
45da2f6
35fdca9
1c9f000
45da2f6
1c9f000
35fdca9
 
 
1c9f000
35fdca9
 
 
10970c2
35fdca9
79914b8
35fdca9
 
1c9f000
79914b8
10970c2
79914b8
 
45da2f6
1c9f000
45da2f6
1c9f000
 
ad494e0
 
 
1c9f000
 
 
 
ad494e0
1c9f000
10970c2
ad494e0
 
 
 
 
 
10970c2
1c9f000
45da2f6
 
1c9f000
45da2f6
10970c2
45da2f6
 
ad494e0
45da2f6
 
8df3575
45da2f6
1c9f000
8df3575
 
 
 
 
 
45da2f6
79914b8
 
1c9f000
45da2f6
 
ad494e0
10970c2
 
ad494e0
10970c2
45da2f6
ad494e0
10970c2
ad494e0
1c9f000
45da2f6
 
1c9f000
45da2f6
79914b8
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Solveur Expert IA - Maths, Physique, Chimie</title>
    <!-- Tailwind CSS Play CDN (v3 - JIT enabled) -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- Google Fonts -->
    <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Fira+Code&display=swap" rel="stylesheet">
    <!-- Font Awesome Icons -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
    <style>
        /* Styles personnalisés complémentaires */
        :root {
            --primary-color: #2c3e50;
            --secondary-color: #1abc9c;
            --accent-color: #e74c3c;
            --success-color: #27ae60;
            --light-secondary-bg: #e8f8f5; /* For upload highlight */
        }
        
        body {
            font-family: 'Montserrat', sans-serif;
        }
        
        .font-code {
            font-family: 'Fira Code', monospace;
        }
        
        /* Gradients can be defined here or as Tailwind arbitrary values if preferred */
        .gradient-primary {
            background-image: linear-gradient(to right, var(--secondary-color) 0%, #16a085 100%);
        }
        
        .gradient-secondary {
            background-image: linear-gradient(to right, var(--primary-color) 0%, #34495e 100%);
        }
        
        @keyframes spin {
            to { transform: rotate(360deg); }
        }
        
        .animate-spin-custom {
            animation: spin 0.8s linear infinite;
        }
        
        /* This class is added by JS, so define its properties here */
        .upload-highlight {
            border-color: var(--secondary-color) !important; /* Use !important if Tailwind specificity is an issue */
            background-color: var(--light-secondary-bg) !important;
        }
    </style>
    <script>
        // Optional: Configure Tailwind if needed (e.g., extending theme colors)
        // For this example, we'll rely on arbitrary values for CSS variables,
        // which the Play CDN handles well.
        // tailwind.config = {
        //   theme: {
        //     extend: {
        //       colors: {
        //         'custom-primary': 'var(--primary-color)',
        //         'custom-secondary': 'var(--secondary-color)',
        //         'custom-accent': 'var(--accent-color)',
        //         'custom-success': 'var(--success-color)',
        //       }
        //     }
        //   }
        // }
    </script>
</head>
<body class="bg-gray-50 text-gray-700 min-h-screen flex flex-col items-center py-6 px-4">
    <div class="bg-white rounded-xl shadow-lg w-full max-w-2xl p-6 md:p-8">
        <h1 class="flex items-center justify-center text-3xl font-bold text-center mb-2 text-[var(--primary-color)]">
            <i class="fas fa-atom text-4xl mr-3 text-[var(--secondary-color)]"></i>
            Solveur Expert IA
        </h1>
        <p class="text-center text-gray-500 mb-8">Solutions LaTeX précises pour Maths, Physique et Chimie.</p>
        
        <div id="upload-section" class="relative border-2 border-dashed border-gray-300 rounded-xl p-8 mb-6 cursor-pointer transition-all duration-300 bg-gray-50 text-center group hover:border-[var(--secondary-color)] hover:bg-[var(--light-secondary-bg)]">
            <div class="upload-content">
                <i class="fas fa-file-arrow-up text-5xl mb-4 text-[var(--secondary-color)] transition-transform duration-300 transform group-hover:scale-110 group-hover:-translate-y-1"></i>
                <p class="text-lg font-medium mb-1">Déposez l'image de votre exercice ici</p>
                <p class="text-sm text-gray-500">ou cliquez pour sélectionner un fichier (PNG, JPG)</p>
            </div>
            <input type="file" id="file-input" accept="image/png, image/jpeg, image/webp" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
            <div id="image-preview-container" class="mt-4">
                <img id="image-preview" src="#" alt="Aperçu de l'énoncé" class="hidden max-w-full max-h-64 rounded-lg mx-auto border border-gray-300 shadow-sm">
            </div>
        </div>

        <div class="bg-gray-50 rounded-xl p-5 mb-6 border border-gray-200">
            <h3 class="flex items-center font-semibold text-[var(--primary-color)] mb-4">
                <i class="fas fa-cogs mr-2 text-[var(--secondary-color)]"></i>Options de Formatage
            </h3>
            <div class="prompt-selector">
                <label for="prompt-type" class="block font-medium mb-2">Style de la correction LaTeX :</label>
                <div class="relative">
                    <select id="prompt-type" name="prompt-type" class="w-full p-3 pr-10 rounded-xl border-2 border-gray-300 appearance-none bg-white focus:outline-none focus:border-[var(--secondary-color)] focus:ring-2 focus:ring-[var(--secondary-color)]/20 transition-all">
                        <option value="refined">Format Raffiné & Complet (mise en page avancée)</option>
                        <option value="light">Format Léger & Essentiel (LaTeX standard)</option>
                    </select>
                    <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-gray-700">
                        <i class="fas fa-chevron-down"></i>
                    </div>
                </div>
            </div>
        </div>
        
        <button id="solve-button" class="gradient-primary w-full py-4 px-6 rounded-xl text-white font-semibold text-lg tracking-wide flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-gray-400 disabled:shadow-none transition duration-300 transform hover:translate-y-px disabled:transform-none" disabled>
            <i class="fas fa-rocket mr-2"></i>Obtenir la Solution
        </button>

        <a href="https://t.me/+ic4zemy1E1k0MzQ0" target="_blank" id="telegram-join-button" class="mt-4 w-full bg-blue-500 hover:bg-blue-600 text-white font-semibold py-3 px-6 rounded-xl flex items-center justify-center transition duration-300 transform hover:translate-y-px">
            <i class="fab fa-telegram-plane mr-2"></i>Rejoindre le groupe Telegram
        </a>
        
        <div id="solving-container" class="hidden mt-8">
            <div class="text-center mb-5">
                <div id="status-message-element" class="flex items-center justify-center flex-wrap text-lg font-medium text-[var(--primary-color)]">
                    <i class="fas fa-hourglass-start mr-2"></i>Prêt à résoudre votre exercice...
                </div>
            </div>
            
            <div id="loading-spinner-element" class="hidden w-10 h-10 mx-auto my-6 border-4 border-gray-300 border-l-[var(--secondary-color)] rounded-full animate-spin-custom"></div>
            
            <div class="flex items-center bg-blue-50 border-l-4 border-[var(--secondary-color)] p-4 rounded-xl my-6">
                <i class="fab fa-telegram text-2xl text-[var(--secondary-color)] mr-3"></i>
                <span>Une copie de la solution sera envoyée sur Telegram pour archivage.</span>
            </div>

            <div id="response-container-element" class="hidden mt-6 p-6 border border-gray-300 rounded-xl bg-white">
                <h3 class="flex items-center font-semibold text-xl text-[var(--primary-color)] mb-4">
                    <i class="fas fa-file-code mr-3 text-[var(--secondary-color)]"></i>Correction LaTeX Détaillée :
                </h3>
                <div id="response-output" class="font-code bg-gray-900 text-gray-200 p-5 rounded-xl overflow-x-auto whitespace-pre-wrap break-words max-h-96 mb-5 border border-gray-700"></div>
                <button id="copy-button" class="gradient-secondary px-6 py-3 rounded-xl text-white font-medium flex items-center justify-center transition duration-300 transform hover:translate-y-px">
                    <i class="fas fa-copy mr-2"></i>Copier le code LaTeX
                </button>
            </div>

            <div id="error-display-element" class="hidden bg-red-50 border-2 border-[var(--accent-color)] text-[var(--accent-color)] p-4 rounded-xl my-5 font-medium">
            </div>
        </div>
    </div>

    <footer class="mt-12 mb-5 text-gray-500 text-sm text-center">
        Solutions générées par <a href="#" target="_blank" class="text-[var(--secondary-color)] font-medium hover:underline">Mariam IA</a> © 2025 - Précision garantie.
    </footer>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            const uploadSection = document.getElementById('upload-section');
            const fileInput = document.getElementById('file-input');
            const imagePreview = document.getElementById('image-preview');
            const solveButton = document.getElementById('solve-button');
            const solvingContainer = document.getElementById('solving-container');
            const responseContainer = document.getElementById('response-container-element');
            const responseOutputDiv = document.getElementById('response-output');
            const copyButton = document.getElementById('copy-button');
            const statusMessageElement = document.getElementById('status-message-element');
            const loadingSpinner = document.getElementById('loading-spinner-element');
            const promptTypeSelect = document.getElementById('prompt-type');
            const errorDisplay = document.getElementById('error-display-element');
            
            let selectedFile = null;
            let currentTaskId = null;

            // --- Gestion du Drag and Drop améliorée ---
            ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
                uploadSection.addEventListener(eventName, preventDefaults, false);
            });
            function preventDefaults(e) {
                e.preventDefault();
                e.stopPropagation();
            }
            
            // Hover effects are now handled by Tailwind's `group hover:` and `hover:` classes directly on #upload-section
            // The .upload-highlight class is kept for explicit JS addition if needed elsewhere,
            // but for dragenter/dragover, Tailwind's hover states are cleaner.
            // If you still want JS to add a class for drag states:
            ['dragenter', 'dragover'].forEach(eventName => {
                uploadSection.addEventListener(eventName, () => {
                    uploadSection.classList.add('upload-highlight'); 
                }, false);
            });
            
            ['dragleave', 'drop'].forEach(eventName => {
                uploadSection.addEventListener(eventName, () => {
                    uploadSection.classList.remove('upload-highlight');
                }, false);
            });
            
            uploadSection.addEventListener('drop', (e) => {
                if (e.dataTransfer.files.length) {
                    handleFileSelection(e.dataTransfer.files[0]);
                }
            });
            
            fileInput.addEventListener('change', (e) => {
                if (e.target.files.length) {
                    handleFileSelection(e.target.files[0]);
                }
            });
            
            function handleFileSelection(file) {
                const allowedTypes = ['image/png', 'image/jpeg', 'image/webp'];
                if (!allowedTypes.includes(file.type)) {
                    displayError('Format de fichier non supporté.', 'Veuillez utiliser PNG, JPG ou WEBP.');
                    selectedFile = null;
                    solveButton.disabled = true;
                    imagePreview.classList.add('hidden');
                    return;
                }
                
                selectedFile = file;
                solveButton.disabled = false;
                errorDisplay.classList.add('hidden');
                
                const reader = new FileReader();
                reader.onload = (e) => {
                    imagePreview.src = e.target.result;
                    imagePreview.classList.remove('hidden');
                };
                reader.readAsDataURL(file);
            }

            function displayError(message, details = null) {
                let fullMessage = `<i class="fas fa-shield-halved mr-2"></i> ${message}`;
                if (details) {
                    fullMessage += `<br><small class="block mt-1 text-red-700 font-normal">${escapeHtml(details)}</small>`;
                }
                errorDisplay.innerHTML = fullMessage;
                errorDisplay.classList.remove('hidden');
                responseContainer.classList.add('hidden');
                loadingSpinner.classList.add('hidden');
                updateStatusUI('error_user', null);
            }
            
            solveButton.addEventListener('click', () => {
                if (!selectedFile) return;
                
                solveButton.disabled = true;
                solvingContainer.classList.remove('hidden');
                responseContainer.classList.add('hidden');
                responseOutputDiv.textContent = '';
                errorDisplay.classList.add('hidden');
                loadingSpinner.classList.remove('hidden');
                updateStatusUI('pending', null, 'Préparation de la résolution...');
                
                const formData = new FormData();
                formData.append('image', selectedFile);
                formData.append('prompt_type', promptTypeSelect.value);
                
                fetch('/solve', {
                    method: 'POST',
                    body: formData
                })
                .then(response => {
                    if (!response.ok) {
                        return response.json().then(errData => {
                            throw new Error(errData.error || `Erreur serveur : ${response.status}`);
                        });
                    }
                    return response.json();
                })
                .then(data => {
                    if (data.error) {
                        throw new Error(data.error);
                    }
                    
                    currentTaskId = data.task_id;
                    updateStatusUI(data.status || 'pending', currentTaskId, "Lancement de l'analyse par l'IA...");
                    
                    const eventSource = new EventSource('/stream/' + currentTaskId);
                    
                    eventSource.onmessage = function(event) {
                        const streamData = JSON.parse(event.data);
                        
                        if (streamData.error) {
                            displayError(streamData.error, streamData.error_detail);
                            if (streamData.response) {
                                responseOutputDiv.textContent = streamData.response;
                                responseContainer.classList.remove('hidden');
                            }
                            eventSource.close();
                            solveButton.disabled = false;
                            loadingSpinner.classList.add('hidden');
                            return;
                        }
                        
                        updateStatusUI(streamData.status, currentTaskId);
                        
                        if (streamData.status === 'completed' || streamData.status === 'completed_tex_only' || streamData.status === 'pdf_error') {
                            responseContainer.classList.remove('hidden');
                            loadingSpinner.classList.add('hidden');
                            
                            if (streamData.response) {
                                responseOutputDiv.textContent = streamData.response;
                            }

                            if (streamData.status === 'pdf_error' && streamData.error_detail) {
                                const statusEl = statusMessageElement.querySelector('.status-text');
                                if(statusEl) statusEl.innerHTML += `<br><small class="pdf-error-detail block mt-1 text-orange-500"><i class="fas fa-file-invoice mr-1"></i> Erreur PDF: ${escapeHtml(streamData.error_detail)}</small>`;
                            }
                            
                            eventSource.close();
                            solveButton.disabled = false;
                        }
                    };
                    
                    eventSource.onerror = function() {
                        eventSource.close();
                        fetch('/task/' + currentTaskId)
                            .then(resp => resp.json())
                            .then(taskData => {
                                updateStatusUI(taskData.status, currentTaskId);
                                if (taskData.status === 'completed' || taskData.status === 'completed_tex_only' || taskData.status === 'pdf_error') {
                                    responseContainer.classList.remove('hidden');
                                    if (taskData.response) {
                                        responseOutputDiv.textContent = taskData.response;
                                    }
                                    if (taskData.status === 'pdf_error' && taskData.error_detail) {
                                       const statusEl = statusMessageElement.querySelector('.status-text');
                                       if(statusEl) statusEl.innerHTML += `<br><small class="pdf-error-detail block mt-1 text-orange-500"><i class="fas fa-file-invoice mr-1"></i> Erreur PDF: ${escapeHtml(taskData.error_detail)}</small>`;
                                    }
                                } else if (taskData.status === 'error') {
                                    displayError(taskData.error || 'Erreur inattendue lors de la récupération de la tâche.', taskData.error_detail);
                                } else {
                                    displayError('Connexion interrompue.', 'Le traitement se poursuit en arrière-plan. Vérifiez Telegram.');
                                }
                            })
                            .catch(error => {
                                displayError('Erreur de récupération du statut.', error.message);
                            })
                            .finally(() => {
                                solveButton.disabled = false;
                                loadingSpinner.classList.add('hidden');
                            });
                    };
                })
                .catch(error => {
                    displayError(error.message || 'Erreur de communication serveur.');
                    solveButton.disabled = false;
                    loadingSpinner.classList.add('hidden');
                });
            });

            function updateStatusUI(status, taskId, overrideMessage = null) {
                const selectedPromptText = promptTypeSelect.options[promptTypeSelect.selectedIndex].text.split('(')[0].trim();
                let statusMsg = overrideMessage || '';
                let iconClass = 'fas fa-hourglass-start';
                // We use inline style for icon color to ensure CSS variables are applied
                let iconColorStyle = "color: var(--primary-color);";


                if (!overrideMessage) {
                    switch(status) {
                        case 'pending': statusMsg = "Lancement de l'analyse par l'IA..."; iconClass = 'fas fa-play-circle'; break;
                        case 'processing': statusMsg = "L'IA déchiffre votre exercice..."; iconClass = 'fas fa-brain'; iconColorStyle = 'color: var(--secondary-color);'; break;
                        case 'generating_latex': statusMsg = "Construction de la solution LaTeX..."; iconClass = 'fas fa-scroll'; break;
                        case 'cleaning_latex': statusMsg = "Peaufinage du code LaTeX..."; iconClass = 'fas fa-magic'; break;
                        case 'generating_pdf': statusMsg = "Compilation du document PDF final..."; iconClass = 'fas fa-file-pdf'; iconColorStyle = 'color: var(--accent-color);'; break;
                        case 'completed': statusMsg = "Solution Complète et Précise Générée !"; iconClass = 'fas fa-check-double'; iconColorStyle = 'color: var(--success-color);'; break;
                        case 'completed_tex_only': statusMsg = "Solution LaTeX Précise Générée ! (PDF non requis/dispo)"; iconClass = 'fas fa-check-circle'; iconColorStyle = 'color: var(--success-color);'; break;
                        case 'pdf_error': statusMsg = "Solution LaTeX Précise Générée ! (Erreur PDF)"; iconClass = 'fas fa-file-excel'; iconColorStyle = 'color: #f39c12;'; break; // Specific color
                        case 'error': statusMsg = "Une anomalie technique est survenue."; iconClass = 'fas fa-times-circle'; iconColorStyle = 'color: var(--accent-color);'; break;
                        case 'error_user': statusMsg = "Veuillez vérifier votre image."; iconClass = 'fas fa-exclamation-triangle'; iconColorStyle = 'color: var(--accent-color);'; break;
                        default: statusMsg = `Progression: ${status}`; iconClass = 'fas fa-spinner fa-spin';
                    }
                }
                
                let taskInfoHtml = '';
                if (taskId) {
                    taskInfoHtml = `<span class="text-sm text-gray-500 ml-2">(Tâche ${taskId.substring(0,6)} | Style: ${selectedPromptText})</span>`;
                }

                statusMessageElement.innerHTML = `<i class="${iconClass} mr-2" style="${iconColorStyle}"></i> <span class="status-text">${statusMsg}</span> ${taskInfoHtml}`;
            }

            // Fonction corrigée ici !
            function escapeHtml(unsafe) {
                if (typeof unsafe !== 'string') return '';
                return unsafe
                    .replace(/&/g, "&")
                    .replace(/</g, "<")
                    .replace(/>/g, ">")
                    .replace(/"/g, """)
                    .replace(/'/g, "'");
            }
            
            copyButton.addEventListener('click', () => {
                const textToCopy = responseOutputDiv.textContent;
                navigator.clipboard.writeText(textToCopy).then(() => {
                    const originalIcon = copyButton.querySelector('i').className;
                    const originalText = copyButton.childNodes[1] ? copyButton.childNodes[1].nodeValue.trim() : 'Copier le code LaTeX';
                    copyButton.innerHTML = `<i class="fas fa-check mr-2"></i> Code Copié !`;
                    copyButton.classList.remove('gradient-secondary');
                    copyButton.style.backgroundColor = 'var(--success-color)';
                    
                    setTimeout(() => {
                        copyButton.innerHTML = `<i class="${originalIcon} mr-2"></i> ${originalText}`;
                        copyButton.classList.add('gradient-secondary');
                        copyButton.style.backgroundColor = ''; // Revert to gradient
                    }, 2500);
                }).catch(err => {
                    console.error('Erreur de copie: ', err);
                    displayError('Copie échouée.', 'Veuillez copier manuellement le texte.');
                });
            });
        });
    </script>
</body>
</html>