Docfile commited on
Commit
c7c58b1
·
verified ·
1 Parent(s): 12c36e4

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +912 -272
templates/index.html CHANGED
@@ -3,218 +3,658 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Mariam AI!</title>
7
 
8
- <!-- Tailwind CSS via CDN (inclut le plugin Forms) -->
9
- <script src="https://cdn.tailwindcss.com?plugins=forms"></script>
 
 
 
 
 
 
 
 
10
 
11
- <!-- Favicon (Emoji simple) -->
12
- <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🤖</text></svg>">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  <style>
15
- html, body {
16
- min-height: 100vh;
17
- margin: 0;
18
- padding: 0;
19
  }
 
20
  body {
21
- display: flex;
22
- flex-direction: column;
23
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
24
  }
25
- #chat-container {
26
- display: flex;
27
- flex-direction: column;
28
- flex-grow: 1;
29
- min-height: 0;
30
- }
31
- #chat-messages {
32
- flex-grow: 1;
33
- overflow-y: auto;
34
- min-height: 0;
35
  }
 
 
36
  ::-webkit-scrollbar {
37
- width: 8px;
 
38
  }
 
39
  ::-webkit-scrollbar-track {
40
- background: #f1f1f1;
41
- border-radius: 10px;
42
  }
 
43
  ::-webkit-scrollbar-thumb {
44
- background: #a8a8a8;
45
- border-radius: 10px;
 
 
 
 
46
  }
 
47
  ::-webkit-scrollbar-thumb:hover {
48
- background: #7a7a7a;
49
- }
50
- /* Styles pour le rendu Markdown dans le chat */
51
- #chat-messages .prose code:not(pre code) {
52
- background-color: #e5e7eb;
53
- padding: 0.2em 0.4em;
54
- font-size: 85%;
55
- border-radius: 4px;
56
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
57
- }
58
- #chat-messages .prose pre {
59
- background-color: #f3f4f6;
60
- padding: 0.8em 1em;
61
- border-radius: 6px;
62
- overflow-x: auto;
63
- font-size: 0.875rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  }
65
- #chat-messages .prose pre code {
66
- background-color: transparent;
67
- padding: 0;
68
- font-size: inherit;
69
- border-radius: 0;
70
- color: inherit;
71
  }
72
- #chat-messages .prose blockquote {
73
- border-left-color: #9ca3af;
74
- color: #4b5563;
 
 
75
  }
76
- #chat-messages .prose strong {
77
- color: #1f2937;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  }
79
- #chat-messages .prose a {
80
- color: #2563eb;
81
- text-decoration: underline;
82
- text-decoration-color: #93c5fd;
83
- transition: color 0.2s ease;
84
  }
85
- #chat-messages .prose a:hover {
86
- color: #1d4ed8;
87
- text-decoration-color: #60a5fa;
 
 
 
 
 
88
  }
89
- #history-loading {
90
- padding: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  text-align: center;
92
- color: #6b7280;
93
- font-style: italic;
 
 
 
 
 
 
 
 
 
 
94
  }
95
- /* Zone de prévisualisation des images */
96
- #file-preview {
97
- margin-top: 8px;
 
98
  }
99
- #file-preview img {
100
- max-width: 100%;
101
- max-height: 150px;
102
- border: 1px solid #ddd;
103
- border-radius: 4px;
104
- padding: 4px;
 
 
 
 
105
  }
106
- /* Bouton de copie (positionné en haut à droite de la bulle de réponse) */
 
 
 
 
 
 
 
 
 
 
107
  .copy-btn {
108
  position: absolute;
109
- top: 4px;
110
- right: 4px;
111
- background: rgba(255, 255, 255, 0.8);
112
- border: none;
113
- border-radius: 4px;
114
- padding: 2px 4px;
115
- cursor: pointer;
116
- font-size: 0.75rem;
117
- display: none;
118
  }
119
- /* Afficher le bouton au survol de la bulle */
120
- .message-wrapper:hover .copy-btn {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  display: block;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  }
123
  </style>
124
  </head>
125
- <body class="bg-gray-100 flex flex-col min-h-screen">
126
- <!-- En-tête fixe -->
127
- <header class="bg-gradient-to-r from-cyan-500 to-blue-500 text-white p-3 sm:p-4 shadow-md flex justify-between items-center sticky top-0 z-10 flex-shrink-0">
128
- <h1 class="text-xl sm:text-2xl font-bold">Mariam AI!</h1>
129
- <form action="/clear" method="POST" id="clear-form">
130
- <button type="submit" title="Effacer la conversation actuelle" class="bg-red-500 hover:bg-red-600 text-white text-xs font-semibold py-1 px-3 rounded-full transition duration-200 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-opacity-75">
131
- Effacer
132
- </button>
133
- </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  </header>
135
 
136
- <!-- Conteneur Principal du Chat -->
137
- <div id="chat-container" class="max-w-4xl w-full mx-auto bg-white shadow-xl rounded-b-lg flex flex-col flex-grow">
138
- <!-- Zone d'affichage des messages -->
139
- <div id="chat-messages" class="flex-grow overflow-y-auto p-4 sm:p-6 space-y-4 scroll-smooth">
140
- <div id="history-loading">
141
- <div class="flex justify-center items-center space-x-2">
142
- <svg class="animate-spin h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
 
143
  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
144
  <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
145
  </svg>
146
- <span>Chargement de l'historique...</span>
147
  </div>
148
  </div>
149
- <div id="loading-indicator" class="text-center text-gray-500 italic py-4" style="display: none;">
150
- <div class="flex justify-center items-center space-x-2">
151
- <svg class="animate-spin h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
152
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
153
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
154
- </svg>
155
- <span>Mariam réfléchit...</span>
 
 
 
 
 
 
156
  </div>
157
  </div>
158
- </div>
159
-
160
- <!-- Zone d'erreur -->
161
- <div id="error-message" class="bg-red-100 border-l-4 border-red-500 text-red-700 px-4 py-2 rounded mx-4 my-2 flex-shrink-0" role="alert" style="display: none;">
162
- <p class="font-bold mr-2">Erreur:</p>
163
- <p id="error-text">Le message d'erreur détaillé ira ici.</p>
164
- </div>
165
-
166
- <!-- Barre d'options, d'upload et de prévisualisation -->
167
- <div class="bg-gray-50 border-t border-gray-200 px-4 py-2 flex-shrink-0">
168
- <div class="flex items-center justify-between text-sm">
169
- <label for="web_search_toggle" class="flex items-center space-x-2 cursor-pointer text-gray-600 hover:text-gray-800 select-none" title="Activer/Désactiver la recherche web pour le prochain message">
170
- <input type="checkbox" id="web_search_toggle" name="web_search" value="true" class="form-checkbox h-4 w-4 rounded text-blue-600 focus:ring-blue-500 focus:ring-offset-0 border-gray-300">
171
- <span class="hidden sm:inline">Recherche Web</span>
172
- <span class="sm:hidden">Web</span>
173
- </label>
174
-
175
- <label for="advanced_reasoning_toggle" class="flex items-center space-x-2 cursor-pointer text-purple-600 hover:text-purple-800 select-none" title="Utiliser le raisonnement avancé Pro - 1 fois/min)">
176
- <input type="checkbox" id="advanced_reasoning_toggle" name="advanced_reasoning" value="true" class="form-checkbox h-4 w-4 rounded text-purple-600 focus:ring-purple-500 focus:ring-offset-0 border-gray-300">
177
- <span class="hidden sm:inline">Raisonnement Avancé</span>
178
- <span class="sm:hidden">Avancé</span>
179
- <span id="advanced-cooldown-timer" class="text-xs text-gray-500 ml-1" style="display: none;"></span>
180
- </label>
181
-
182
- <div class="flex items-center space-x-2">
183
- <label for="file_upload" class="cursor-pointer text-blue-600 hover:text-blue-700 font-medium flex items-center" title="Joindre un fichier (txt, pdf, png, jpg)">
184
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
185
- <path stroke-linecap="round" stroke-linejoin="round" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
186
- </svg>
187
- <span class="hidden sm:inline">Fichier</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  <input type="file" id="file_upload" name="file" class="hidden" accept=".txt,.pdf,.png,.jpg,.jpeg">
 
189
  </label>
190
- <span id="file-name" class="text-gray-500 text-xs truncate max-w-[150px]" title=""></span>
191
- <button id="clear-file" class="text-red-500 hover:text-red-700 text-xs p-0.5 rounded focus:outline-none focus:ring-1 focus:ring-red-400" title="Retirer le fichier" style="display: none;">
192
- <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
193
- <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
194
- </svg>
195
- </button>
 
196
  </div>
197
  </div>
198
- <!-- Zone de prévisualisation des images -->
199
- <div id="file-preview"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  </div>
 
201
 
202
- <!-- Formulaire d'entrée du message -->
203
- <form id="chat-form" class="bg-gray-100 p-3 sm:p-4 border-t border-gray-200 rounded-b-lg flex-shrink-0">
204
- <div class="flex items-center space-x-2 sm:space-x-3">
205
- <input type="text" id="prompt" name="prompt" class="flex-grow form-input px-4 py-2 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-400 shadow-sm text-sm sm:text-base" placeholder="Posez votre question à Mariam..." autocomplete="off">
206
- <button type="submit" id="send-button" title="Envoyer le message" class="bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed text-white font-bold p-2 rounded-full transition duration-200 flex items-center justify-center shadow-md w-10 h-10 flex-shrink-0 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75">
207
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
208
- <path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" />
209
- </svg>
210
- </button>
211
- </div>
212
- </form>
213
- </div>
214
-
215
- <!-- Script JavaScript pour l'interaction -->
216
  <script>
217
  document.addEventListener('DOMContentLoaded', () => {
 
218
  const chatForm = document.getElementById('chat-form');
219
  const promptInput = document.getElementById('prompt');
220
  const chatMessages = document.getElementById('chat-messages');
@@ -222,134 +662,212 @@
222
  const historyLoadingIndicator = document.getElementById('history-loading');
223
  const errorMessageDiv = document.getElementById('error-message');
224
  const errorTextP = document.getElementById('error-text');
 
225
  const webSearchToggle = document.getElementById('web_search_toggle');
226
  const fileUpload = document.getElementById('file_upload');
 
227
  const fileNameSpan = document.getElementById('file-name');
228
  const clearFileButton = document.getElementById('clear-file');
229
  const filePreview = document.getElementById('file-preview');
 
230
  const sendButton = document.getElementById('send-button');
231
  const clearForm = document.getElementById('clear-form');
232
  const advancedToggle = document.getElementById('advanced_reasoning_toggle');
233
  const advancedCooldownTimerSpan = document.getElementById('advanced-cooldown-timer');
 
234
 
 
235
  const API_CHAT_ENDPOINT = '/api/chat';
236
  const API_HISTORY_ENDPOINT = '/api/history';
237
  const CLEAR_ENDPOINT = '/clear';
238
 
239
- function scrollToBottom() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  setTimeout(() => {
241
- chatMessages.scrollTop = chatMessages.scrollHeight;
 
 
 
242
  }, 50);
243
  }
244
 
245
- let advancedToggleCooldownEndTime = 0; // Timestamp when cooldown ends
246
- const COOLDOWN_DURATION = 60 * 1000; // 60 seconds in milliseconds
247
-
248
  function showLoading(show) {
249
- const currentlyLoading = loadingIndicator.style.display !== 'none';
250
- if (show && !currentlyLoading) {
251
- loadingIndicator.style.display = 'block';
252
  chatMessages.appendChild(loadingIndicator);
253
  scrollToBottom();
254
- } else if (!show && currentlyLoading) {
255
- loadingIndicator.style.display = 'none';
256
  }
 
257
  sendButton.disabled = show;
258
  promptInput.disabled = show;
259
  fileUpload.disabled = show;
260
  clearFileButton.disabled = show;
261
- // Don't disable advanced toggle during loading, only during cooldown
262
  }
263
 
 
264
  function displayError(message) {
265
  errorTextP.textContent = message || "Une erreur inconnue est survenue.";
266
- errorMessageDiv.style.display = 'flex';
267
  errorMessageDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
268
  }
 
 
 
 
 
269
 
270
- function addMessageToChat(role, text, isHtml = false) {
271
- errorMessageDiv.style.display = 'none';
 
 
272
  const messageWrapper = document.createElement('div');
273
- messageWrapper.classList.add('message-wrapper', 'flex', role === 'user' ? 'justify-end' : 'justify-start', 'mb-4');
274
-
275
- const bubbleDiv = document.createElement('div');
276
- bubbleDiv.classList.add('p-3', 'rounded-lg', 'max-w-[85%]', 'sm:max-w-[75%]', 'shadow-md', 'relative');
277
-
278
  if (role === 'user') {
279
- bubbleDiv.classList.add('bg-blue-500', 'text-white', 'rounded-br-none');
280
- const paragraph = document.createElement('p');
281
- paragraph.classList.add('text-sm', 'sm:text-base', 'break-words');
282
- paragraph.textContent = text;
283
- bubbleDiv.appendChild(paragraph);
 
 
 
284
  } else {
285
- bubbleDiv.classList.add('bg-gray-100', 'text-gray-800', 'rounded-bl-none', 'border', 'border-gray-200');
286
- const proseDiv = document.createElement('div');
287
- proseDiv.classList.add('prose', 'prose-sm', 'sm:prose-base', 'max-w-none', 'text-gray-800', 'prose-headings:font-semibold', 'prose-headings:text-gray-800', 'prose-a:text-blue-600', 'prose-a:no-underline', 'hover:prose-a:underline', 'prose-strong:text-gray-800', 'prose-code:text-red-600', 'prose-code:font-mono', 'prose-blockquote:text-gray-600', 'break-words');
288
- if (isHtml) {
289
- proseDiv.innerHTML = text;
290
- } else {
291
- proseDiv.textContent = text;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  }
293
- bubbleDiv.appendChild(proseDiv);
294
-
295
- // Ajout du bouton de copie
296
- const copyBtn = document.createElement('button');
297
- copyBtn.textContent = 'Copier';
298
- copyBtn.classList.add('copy-btn');
299
- copyBtn.title = "Copier la réponse";
300
  copyBtn.addEventListener('click', (e) => {
301
  e.stopPropagation();
302
  e.preventDefault();
303
- // Copier le texte sans le bouton
304
- let textToCopy = '';
305
- if (isHtml) {
306
- textToCopy = proseDiv.innerText;
307
- } else {
308
- textToCopy = text;
309
- }
310
- navigator.clipboard.writeText(textToCopy)
311
  .then(() => {
312
- copyBtn.textContent = 'Copié';
313
  setTimeout(() => {
314
- copyBtn.textContent = 'Copier';
315
  }, 2000);
316
- })
317
- .catch(err => {
318
- console.error('Erreur lors de la copie :', err);
319
  });
320
  });
321
- bubbleDiv.appendChild(copyBtn);
322
  }
323
-
324
- messageWrapper.appendChild(bubbleDiv);
325
- chatMessages.insertBefore(messageWrapper, loadingIndicator);
326
  if (historyLoadingIndicator.parentNode !== chatMessages) {
327
  scrollToBottom();
328
  }
329
  }
330
 
 
 
 
 
 
 
 
 
 
 
 
331
  function startAdvancedCooldownTimer() {
332
- advancedToggle.disabled = true; // Disable toggle during cooldown
333
  advancedToggleCooldownEndTime = Date.now() + COOLDOWN_DURATION;
334
 
335
  const updateTimer = () => {
336
- const now = Date.now();
337
- if (now >= advancedToggleCooldownEndTime) {
338
- clearInterval(intervalId);
339
- advancedCooldownTimerSpan.style.display = 'none';
340
- advancedToggle.disabled = false;
341
- advancedToggleCooldownEndTime = 0; // Reset cooldown end time
342
- } else {
343
- const remainingSeconds = Math.ceil((advancedToggleCooldownEndTime - now) / 1000);
344
- advancedCooldownTimerSpan.textContent = `(${remainingSeconds}s)`;
345
- advancedCooldownTimerSpan.style.display = 'inline';
346
- }
347
  };
348
 
349
  const intervalId = setInterval(updateTimer, 1000);
350
- updateTimer(); // Initial call to display immediately
351
  }
352
 
 
353
  async function loadChatHistory() {
354
  historyLoadingIndicator.style.display = 'block';
355
  try {
@@ -359,44 +877,49 @@
359
  try {
360
  const errData = await response.json();
361
  errorMsg = errData.error || errorMsg;
362
- } catch (e) { }
363
  throw new Error(errorMsg);
364
  }
 
365
  const data = await response.json();
366
  if (data.success && Array.isArray(data.history)) {
 
367
  chatMessages.innerHTML = '';
368
  chatMessages.appendChild(loadingIndicator);
369
- loadingIndicator.style.display = 'none';
 
370
  if (data.history.length === 0) {
371
- addMessageToChat('assistant', "Bonjour ! Comment puis-je vous aider aujourd'hui ?");
 
372
  } else {
 
373
  data.history.forEach(message => {
374
  const isAssistantHtml = message.role === 'assistant';
375
  addMessageToChat(message.role, message.text, isAssistantHtml);
376
  });
377
  }
378
- scrollToBottom();
 
379
  } else {
380
  throw new Error(data.error || "Format de réponse de l'historique invalide.");
381
  }
382
  } catch (error) {
383
  chatMessages.innerHTML = '';
384
  chatMessages.appendChild(loadingIndicator);
385
- loadingIndicator.style.display = 'none';
386
  displayError(`Impossible de charger l'historique: ${error.message}`);
387
  } finally {
388
- if (historyLoadingIndicator.parentNode === chatMessages) {
389
- historyLoadingIndicator.remove();
390
- }
391
  promptInput.focus();
392
  }
393
  }
394
 
 
395
  function clearFileInput() {
396
  fileUpload.value = '';
397
- fileNameSpan.textContent = '';
398
- fileNameSpan.title = '';
399
- clearFileButton.style.display = 'none';
400
  filePreview.innerHTML = '';
401
  }
402
 
@@ -404,28 +927,62 @@
404
  if (fileUpload.files.length > 0) {
405
  const file = fileUpload.files[0];
406
  const name = file.name;
407
- fileNameSpan.textContent = name.length > 20 ? name.substring(0, 17) + '...' : name;
 
 
408
  fileNameSpan.title = name;
409
- clearFileButton.style.display = 'inline-block';
410
- // Si le fichier est une image, afficher la prévisualisation
 
411
  if (file.type.startsWith('image/')) {
412
  const reader = new FileReader();
413
  reader.onload = (e) => {
414
- filePreview.innerHTML = `<img src="${e.target.result}" alt="Prévisualisation de l'image">`;
 
 
 
 
 
 
415
  };
416
  reader.readAsDataURL(file);
417
  } else {
418
- filePreview.innerHTML = '';
 
 
 
 
 
 
 
 
 
 
419
  }
420
  } else {
421
  clearFileInput();
422
  }
423
  });
424
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  clearFileButton.addEventListener('click', () => {
426
  clearFileInput();
427
  });
428
 
 
429
  chatForm.addEventListener('submit', async (e) => {
430
  e.preventDefault();
431
  const prompt = promptInput.value.trim();
@@ -438,52 +995,59 @@
438
  promptInput.focus();
439
  return;
440
  }
441
- errorMessageDiv.style.display = 'none';
 
442
 
443
- // --- Cooldown Check for Advanced Reasoning ---
444
  if (useAdvanced) {
445
- const now = Date.now();
446
- if (now < advancedToggleCooldownEndTime) {
447
- const remainingSeconds = Math.ceil((advancedToggleCooldownEndTime - now) / 1000);
448
- displayError(`Le raisonnement avancé est disponible dans ${remainingSeconds} seconde(s).`);
449
- return; // Stop submission
450
- }
451
- // If check passes, cooldown will be started AFTER successful submission trigger
452
  }
453
- // --- End Cooldown Check ---
454
 
 
455
  let userMessageText = prompt;
456
  if (file && file.name) {
457
- userMessageText = prompt ? `[${file.name}] ${prompt}` : `[${file.name}]`;
458
  }
 
459
  addMessageToChat('user', userMessageText);
 
 
460
  const formData = new FormData();
461
  formData.append('prompt', prompt);
462
  formData.append('web_search', useWebSearch);
 
463
  if (file) {
464
- formData.append('file', file);
465
  }
466
- // Add the advanced reasoning flag
467
  formData.append('advanced_reasoning', useAdvanced);
468
 
 
469
  showLoading(true);
470
  promptInput.value = '';
471
  clearFileInput();
472
 
473
- // Reset toggles for next message (Web Search stays, Advanced resets and starts cooldown if used)
474
- // webSearchToggle.checked = false; // Decide if you want this to reset
475
- advancedToggle.checked = false; // Always reset advanced toggle
476
- if (useAdvanced) startAdvancedCooldownTimer(); // Start cooldown *now* as we are sending the request
477
 
478
  try {
479
  const response = await fetch(API_CHAT_ENDPOINT, {
480
  method: 'POST',
481
  body: formData,
482
  });
 
483
  const data = await response.json();
 
484
  if (!response.ok) {
485
  throw new Error(data.error || `Erreur serveur: ${response.status}`);
486
  }
 
487
  if (data.success && data.message) {
488
  addMessageToChat('assistant', data.message, true);
489
  } else {
@@ -491,19 +1055,51 @@
491
  }
492
  } catch (error) {
493
  displayError(error.message);
494
- if (useAdvanced) { /* Maybe revert cooldown if API fails? */ } // Optional: more complex error handling
495
  } finally {
496
  showLoading(false);
497
  promptInput.focus();
498
  }
499
  });
500
 
 
501
  clearForm.addEventListener('submit', async (e) => {
502
  e.preventDefault();
503
- if (confirm("Êtes-vous sûr de vouloir effacer toute la conversation ?")) {
504
- const originalButtonText = e.target.querySelector('button').textContent;
505
- e.target.querySelector('button').textContent = '...';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
  e.target.querySelector('button').disabled = true;
 
507
  try {
508
  const response = await fetch(CLEAR_ENDPOINT, {
509
  method: 'POST',
@@ -511,27 +1107,71 @@
511
  'X-Requested-With': 'XMLHttpRequest'
512
  }
513
  });
 
514
  const data = await response.json();
 
515
  if (response.ok && data.success) {
516
  chatMessages.innerHTML = '';
517
  chatMessages.appendChild(loadingIndicator);
518
- loadingIndicator.style.display = 'none';
519
- addMessageToChat('assistant', "Conversation effacée. Comment puis-je vous aider ?");
520
- errorMessageDiv.style.display = 'none';
 
521
  } else {
522
  throw new Error(data.error || "Impossible d'effacer côté serveur.");
523
  }
524
  } catch (error) {
525
  displayError(`Erreur lors de l'effacement du chat: ${error.message}`);
526
  } finally {
527
- e.target.querySelector('button').textContent = originalButtonText;
528
  e.target.querySelector('button').disabled = false;
529
  promptInput.focus();
530
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
531
  }
532
  });
 
 
 
 
 
 
 
 
 
533
 
 
534
  loadChatHistory();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
  });
536
  </script>
537
  </body>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Mariam AI</title>
7
 
8
+ <!-- Tailwind CSS via CDN -->
9
+ <script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
10
+
11
+ <!-- Font Awesome pour les icônes -->
12
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
13
+
14
+ <!-- Google Fonts -->
15
+ <link rel="preconnect" href="https://fonts.googleapis.com">
16
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
17
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
18
 
19
+ <!-- Favicon (Emoji amélioré) -->
20
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>✨</text></svg>">
21
+
22
+ <script>
23
+ // Configuration avancée de Tailwind
24
+ tailwind.config = {
25
+ darkMode: 'class',
26
+ theme: {
27
+ extend: {
28
+ fontFamily: {
29
+ sans: ['Inter', 'system-ui', 'sans-serif'],
30
+ mono: ['"JetBrains Mono"', 'monospace'],
31
+ },
32
+ colors: {
33
+ primary: {
34
+ 50: '#f0f9ff',
35
+ 100: '#e0f2fe',
36
+ 200: '#bae6fd',
37
+ 300: '#7dd3fc',
38
+ 400: '#38bdf8',
39
+ 500: '#0ea5e9',
40
+ 600: '#0284c7',
41
+ 700: '#0369a1',
42
+ 800: '#075985',
43
+ 900: '#0c4a6e',
44
+ },
45
+ secondary: {
46
+ 50: '#f8fafc',
47
+ 100: '#f1f5f9',
48
+ 200: '#e2e8f0',
49
+ 300: '#cbd5e1',
50
+ 400: '#94a3b8',
51
+ 500: '#64748b',
52
+ 600: '#475569',
53
+ 700: '#334155',
54
+ 800: '#1e293b',
55
+ 900: '#0f172a',
56
+ },
57
+ accent: {
58
+ 50: '#fdf4ff',
59
+ 100: '#fae8ff',
60
+ 200: '#f5d0fe',
61
+ 300: '#f0abfc',
62
+ 400: '#e879f9',
63
+ 500: '#d946ef',
64
+ 600: '#c026d3',
65
+ 700: '#a21caf',
66
+ 800: '#86198f',
67
+ 900: '#701a75',
68
+ }
69
+ },
70
+ animation: {
71
+ 'bounce-slow': 'bounce 2s infinite',
72
+ 'pulse-slow': 'pulse 3s infinite',
73
+ 'typing': 'typing 1.2s steps(3) infinite',
74
+ },
75
+ keyframes: {
76
+ typing: {
77
+ '0%': { width: '0.15em' },
78
+ '50%': { width: '0.7em' },
79
+ '100%': { width: '0.15em' },
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ </script>
86
 
87
  <style>
88
+ /* Base styles */
89
+ html {
90
+ scroll-behavior: smooth;
 
91
  }
92
+
93
  body {
94
+ font-family: 'Inter', sans-serif;
95
+ transition: background-color 0.3s ease, color 0.3s ease;
 
96
  }
97
+
98
+ /* Layout */
99
+ .chat-layout {
100
+ min-height: calc(100vh - 64px);
101
+ display: grid;
102
+ grid-template-rows: 1fr auto;
 
 
 
 
103
  }
104
+
105
+ /* Scrollbar styling */
106
  ::-webkit-scrollbar {
107
+ width: 5px;
108
+ height: 5px;
109
  }
110
+
111
  ::-webkit-scrollbar-track {
112
+ background: transparent;
 
113
  }
114
+
115
  ::-webkit-scrollbar-thumb {
116
+ background: #cbd5e1;
117
+ border-radius: 5px;
118
+ }
119
+
120
+ .dark ::-webkit-scrollbar-thumb {
121
+ background: #475569;
122
  }
123
+
124
  ::-webkit-scrollbar-thumb:hover {
125
+ background: #94a3b8;
126
+ }
127
+
128
+ .dark ::-webkit-scrollbar-thumb:hover {
129
+ background: #64748b;
130
+ }
131
+
132
+ /* Message bubbles */
133
+ .message-bubble {
134
+ position: relative;
135
+ max-width: 85%;
136
+ border-radius: 1rem;
137
+ padding: 0.875rem 1rem;
138
+ line-height: 1.5;
139
+ animation: message-fade-in 0.3s ease-out;
140
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
141
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
142
+ }
143
+
144
+ .message-bubble:hover {
145
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05), 0 1px 3px rgba(0, 0, 0, 0.1);
146
+ }
147
+
148
+ .dark .message-bubble {
149
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
150
+ }
151
+
152
+ .dark .message-bubble:hover {
153
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2), 0 1px 3px rgba(0, 0, 0, 0.3);
154
+ }
155
+
156
+ .user-message {
157
+ border-bottom-right-radius: 0.125rem;
158
+ align-self: flex-end;
159
+ background: linear-gradient(to bottom right, #3b82f6, #2563eb);
160
+ color: white;
161
  }
162
+
163
+ .assistant-message {
164
+ border-bottom-left-radius: 0.125rem;
165
+ align-self: flex-start;
 
 
166
  }
167
+
168
+ .dark .assistant-message {
169
+ background-color: #1e293b;
170
+ color: #e2e8f0;
171
+ border-color: #334155;
172
  }
173
+
174
+ /* Message bubble arrow */
175
+ .user-message::after {
176
+ content: '';
177
+ position: absolute;
178
+ bottom: -0.5rem;
179
+ right: 0.125rem;
180
+ width: 0.75rem;
181
+ height: 0.75rem;
182
+ background: #2563eb;
183
+ clip-path: polygon(0 0, 100% 0, 100% 100%);
184
+ }
185
+
186
+ .assistant-message::after {
187
+ content: '';
188
+ position: absolute;
189
+ bottom: -0.5rem;
190
+ left: 0.125rem;
191
+ width: 0.75rem;
192
+ height: 0.75rem;
193
+ background: #f8fafc;
194
+ clip-path: polygon(0 0, 100% 0, 0 100%);
195
+ }
196
+
197
+ .dark .assistant-message::after {
198
+ background: #1e293b;
199
+ }
200
+
201
+ /* Animations */
202
+ @keyframes message-fade-in {
203
+ from {
204
+ opacity: 0;
205
+ transform: translateY(10px);
206
+ }
207
+ to {
208
+ opacity: 1;
209
+ transform: translateY(0);
210
+ }
211
+ }
212
+
213
+ @keyframes pulse-fade {
214
+ 0%, 100% {
215
+ opacity: 0.5;
216
+ }
217
+ 50% {
218
+ opacity: 1;
219
+ }
220
  }
221
+
222
+ .typing-indicator {
223
+ display: inline-flex;
224
+ align-items: center;
225
+ margin-left: 0.5rem;
226
  }
227
+
228
+ .typing-dot {
229
+ width: 0.5rem;
230
+ height: 0.5rem;
231
+ border-radius: 50%;
232
+ background-color: currentColor;
233
+ opacity: 0.7;
234
+ margin: 0 0.1rem;
235
  }
236
+
237
+ .typing-dot:nth-child(1) {
238
+ animation: pulse-fade 1.2s 0s infinite;
239
+ }
240
+
241
+ .typing-dot:nth-child(2) {
242
+ animation: pulse-fade 1.2s 0.2s infinite;
243
+ }
244
+
245
+ .typing-dot:nth-child(3) {
246
+ animation: pulse-fade 1.2s 0.4s infinite;
247
+ }
248
+
249
+ /* Style spécifique pour le mode sombre */
250
+ .dark body {
251
+ background-color: #0f172a;
252
+ color: #e2e8f0;
253
+ }
254
+
255
+ /* Tooltip custom */
256
+ .tooltip {
257
+ position: relative;
258
+ }
259
+
260
+ .tooltip .tooltip-text {
261
+ visibility: hidden;
262
+ width: max-content;
263
+ max-width: 200px;
264
+ background-color: #1e293b;
265
+ color: #f8fafc;
266
  text-align: center;
267
+ border-radius: 6px;
268
+ padding: 0.375rem 0.625rem;
269
+ position: absolute;
270
+ z-index: 1;
271
+ bottom: 125%;
272
+ left: 50%;
273
+ transform: translateX(-50%);
274
+ opacity: 0;
275
+ transition: opacity 0.3s, visibility 0.3s;
276
+ font-size: 0.75rem;
277
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
278
+ pointer-events: none;
279
  }
280
+
281
+ .dark .tooltip .tooltip-text {
282
+ background-color: #475569;
283
+ color: #f1f5f9;
284
  }
285
+
286
+ .tooltip .tooltip-text::after {
287
+ content: "";
288
+ position: absolute;
289
+ top: 100%;
290
+ left: 50%;
291
+ margin-left: -5px;
292
+ border-width: 5px;
293
+ border-style: solid;
294
+ border-color: #1e293b transparent transparent transparent;
295
  }
296
+
297
+ .dark .tooltip .tooltip-text::after {
298
+ border-color: #475569 transparent transparent transparent;
299
+ }
300
+
301
+ .tooltip:hover .tooltip-text {
302
+ visibility: visible;
303
+ opacity: 1;
304
+ }
305
+
306
+ /* Bouton de copie */
307
  .copy-btn {
308
  position: absolute;
309
+ top: 0.5rem;
310
+ right: 0.5rem;
311
+ opacity: 0;
312
+ transition: opacity 0.2s ease, background-color 0.2s ease;
313
+ z-index: 2;
314
+ }
315
+
316
+ .message-bubble:hover .copy-btn {
317
+ opacity: 1;
318
  }
319
+
320
+ /* File preview */
321
+ .file-preview {
322
+ max-width: 300px;
323
+ margin: 0.5rem auto;
324
+ position: relative;
325
+ overflow: hidden;
326
+ border-radius: 0.5rem;
327
+ transition: transform 0.2s ease;
328
+ }
329
+
330
+ .file-preview:hover {
331
+ transform: scale(1.02);
332
+ }
333
+
334
+ .file-preview img {
335
+ width: 100%;
336
+ height: auto;
337
  display: block;
338
+ object-fit: cover;
339
+ }
340
+
341
+ /* Chip style */
342
+ .chip {
343
+ display: inline-flex;
344
+ align-items: center;
345
+ background: #e0f2fe;
346
+ border-radius: 9999px;
347
+ padding: 0.25rem 0.75rem;
348
+ font-size: 0.75rem;
349
+ font-weight: 500;
350
+ color: #0369a1;
351
+ transition: all 0.2s ease;
352
+ }
353
+
354
+ .chip .chip-icon {
355
+ margin-right: 0.25rem;
356
+ }
357
+
358
+ .chip .chip-close {
359
+ margin-left: 0.25rem;
360
+ cursor: pointer;
361
+ opacity: 0.7;
362
+ transition: opacity 0.2s ease;
363
+ }
364
+
365
+ .chip .chip-close:hover {
366
+ opacity: 1;
367
+ }
368
+
369
+ .dark .chip {
370
+ background: #0c4a6e;
371
+ color: #7dd3fc;
372
+ }
373
+
374
+ /* Switch toggle style */
375
+ .toggle-switch {
376
+ position: relative;
377
+ display: inline-block;
378
+ width: 2.5rem;
379
+ height: 1.25rem;
380
+ }
381
+
382
+ .toggle-switch input {
383
+ opacity: 0;
384
+ width: 0;
385
+ height: 0;
386
+ }
387
+
388
+ .toggle-slider {
389
+ position: absolute;
390
+ cursor: pointer;
391
+ top: 0;
392
+ left: 0;
393
+ right: 0;
394
+ bottom: 0;
395
+ background-color: #cbd5e1;
396
+ transition: .4s;
397
+ border-radius: 1.25rem;
398
+ }
399
+
400
+ .toggle-slider:before {
401
+ position: absolute;
402
+ content: "";
403
+ height: 0.875rem;
404
+ width: 0.875rem;
405
+ left: 0.25rem;
406
+ bottom: 0.1875rem;
407
+ background-color: white;
408
+ transition: .4s;
409
+ border-radius: 50%;
410
+ }
411
+
412
+ input:checked + .toggle-slider {
413
+ background-color: #0ea5e9;
414
+ }
415
+
416
+ input:focus + .toggle-slider {
417
+ box-shadow: 0 0 1px #0ea5e9;
418
+ }
419
+
420
+ input:checked + .toggle-slider:before {
421
+ transform: translateX(1.125rem);
422
+ }
423
+
424
+ .dark .toggle-slider {
425
+ background-color: #475569;
426
+ }
427
+
428
+ .dark input:checked + .toggle-slider {
429
+ background-color: #38bdf8;
430
+ }
431
+
432
+ /* Input styling */
433
+ .chat-input {
434
+ transition: all 0.3s ease;
435
+ border-color: #e2e8f0;
436
+ }
437
+
438
+ .chat-input:focus {
439
+ border-color: #38bdf8;
440
+ box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2);
441
+ }
442
+
443
+ .dark .chat-input {
444
+ background-color: #1e293b;
445
+ color: #f1f5f9;
446
+ border-color: #334155;
447
+ }
448
+
449
+ .dark .chat-input:focus {
450
+ border-color: #38bdf8;
451
+ box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2);
452
+ }
453
+
454
+ /* Code block styling */
455
+ pre {
456
+ position: relative;
457
+ background-color: #f8fafc;
458
+ border-radius: 0.5rem;
459
+ margin: 1rem 0;
460
+ padding: 1.25rem 1rem;
461
+ overflow-x: auto;
462
+ }
463
+
464
+ .dark pre {
465
+ background-color: #1e293b;
466
+ color: #e2e8f0;
467
+ }
468
+
469
+ code {
470
+ font-family: 'JetBrains Mono', monospace;
471
+ font-size: 0.875rem;
472
+ }
473
+
474
+ /* Code block copy button */
475
+ .code-copy-btn {
476
+ position: absolute;
477
+ top: 0.5rem;
478
+ right: 0.5rem;
479
+ opacity: 0;
480
+ transition: opacity 0.2s ease;
481
+ }
482
+
483
+ pre:hover .code-copy-btn {
484
+ opacity: 0.7;
485
+ }
486
+
487
+ pre:hover .code-copy-btn:hover {
488
+ opacity: 1;
489
  }
490
  </style>
491
  </head>
492
+ <body class="bg-gray-50 text-gray-900 antialiased">
493
+ <!-- Header -->
494
+ <header class="bg-gradient-to-r from-primary-600 to-primary-800 text-white py-3 px-4 shadow-md sticky top-0 z-10">
495
+ <div class="max-w-4xl mx-auto flex justify-between items-center">
496
+ <!-- Logo & Title -->
497
+ <div class="flex items-center space-x-2">
498
+ <span class="text-2xl">✨</span>
499
+ <h1 class="text-xl font-bold">Mariam AI</h1>
500
+ </div>
501
+
502
+ <!-- Actions -->
503
+ <div class="flex items-center space-x-2 sm:space-x-4">
504
+ <!-- Thème sombre/clair -->
505
+ <button id="theme-toggle" class="p-2 rounded-full hover:bg-primary-700/50 transition-colors duration-200 tooltip" aria-label="Changer de thème">
506
+ <i class="fa-solid fa-moon dark:hidden"></i>
507
+ <i class="fa-solid fa-sun hidden dark:inline"></i>
508
+ <span class="tooltip-text">Mode clair/sombre</span>
509
+ </button>
510
+
511
+ <!-- Bouton d'effacement -->
512
+ <form action="/clear" method="POST" id="clear-form">
513
+ <button type="submit" class="flex items-center bg-red-500 hover:bg-red-600 text-white text-xs font-semibold py-1.5 px-3 rounded-full transition duration-200 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-opacity-75 tooltip">
514
+ <i class="fa-solid fa-trash-can mr-1.5"></i>
515
+ <span class="hidden sm:inline">Effacer</span>
516
+ <span class="tooltip-text">Effacer la conversation</span>
517
+ </button>
518
+ </form>
519
+ </div>
520
+ </div>
521
  </header>
522
 
523
+ <!-- Main Container -->
524
+ <main class="max-w-4xl mx-auto chat-layout">
525
+ <!-- Chat Messages Container -->
526
+ <section id="chat-messages" class="flex flex-col space-y-6 p-4 overflow-y-auto">
527
+ <!-- Message de chargement de l'historique -->
528
+ <div id="history-loading" class="text-center py-10">
529
+ <div class="inline-flex items-center px-4 py-2 bg-primary-50 text-primary-700 rounded-lg dark:bg-primary-900/30 dark:text-primary-300">
530
+ <svg class="animate-spin h-5 w-5 mr-3" viewBox="0 0 24 24">
531
  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
532
  <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
533
  </svg>
534
+ <span>Chargement de la conversation...</span>
535
  </div>
536
  </div>
537
+
538
+ <!-- Indicateur de chargement pour les réponses -->
539
+ <div id="loading-indicator" class="flex items-start space-x-2 hidden">
540
+ <div class="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center dark:bg-primary-900/50">
541
+ <span class="text-lg">✨</span>
542
+ </div>
543
+ <div class="message-bubble assistant-message bg-secondary-50 text-secondary-900 border border-secondary-200 flex items-center">
544
+ <span>Mariam réfléchit</span>
545
+ <div class="typing-indicator">
546
+ <span class="typing-dot"></span>
547
+ <span class="typing-dot"></span>
548
+ <span class="typing-dot"></span>
549
+ </div>
550
  </div>
551
  </div>
552
+ </section>
553
+
554
+ <!-- Bottom Container -->
555
+ <div class="border-t border-gray-200 dark:border-gray-700">
556
+ <!-- Error Message -->
557
+ <div id="error-message" class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 dark:bg-red-900/30 dark:text-red-300 dark:border-red-600 hidden" role="alert">
558
+ <div class="flex">
559
+ <div class="flex-shrink-0">
560
+ <i class="fa-solid fa-circle-exclamation"></i>
561
+ </div>
562
+ <div class="ml-3">
563
+ <p class="text-sm font-medium" id="error-text">Le message d'erreur détaillé ira ici.</p>
564
+ </div>
565
+ <button class="ml-auto" id="dismiss-error">
566
+ <i class="fa-solid fa-xmark"></i>
567
+ </button>
568
+ </div>
569
+ </div>
570
+
571
+ <!-- Preview Area -->
572
+ <div id="preview-area" class="px-4 py-2 bg-gray-50 dark:bg-gray-800/50 hidden">
573
+ <!-- File Preview -->
574
+ <div id="file-preview" class="hidden"></div>
575
+ </div>
576
+
577
+ <!-- Options Bar -->
578
+ <div class="flex items-center justify-between px-4 py-2 bg-gray-100 dark:bg-gray-800/80 text-sm text-gray-600 dark:text-gray-300">
579
+ <!-- Options du chat -->
580
+ <div class="flex items-center space-x-4">
581
+ <!-- Web Search Toggle -->
582
+ <label class="flex items-center cursor-pointer tooltip">
583
+ <span class="mr-2 text-xs sm:text-sm font-medium">
584
+ <i class="fa-solid fa-globe mr-1.5"></i>
585
+ <span class="hidden sm:inline">Recherche Web</span>
586
+ </span>
587
+ <span class="toggle-switch">
588
+ <input type="checkbox" id="web_search_toggle" name="web_search" value="true">
589
+ <span class="toggle-slider"></span>
590
+ </span>
591
+ <span class="tooltip-text">Activer la recherche web pour Mariam</span>
592
+ </label>
593
+
594
+ <!-- Advanced Reasoning Toggle -->
595
+ <label class="flex items-center cursor-pointer tooltip">
596
+ <span class="mr-2 text-xs sm:text-sm font-medium text-accent-700 dark:text-accent-300">
597
+ <i class="fa-solid fa-brain mr-1.5"></i>
598
+ <span class="hidden sm:inline">Avancé</span>
599
+ <span id="advanced-cooldown-timer" class="text-xs ml-1 hidden"></span>
600
+ </span>
601
+ <span class="toggle-switch">
602
+ <input type="checkbox" id="advanced_reasoning_toggle" name="advanced_reasoning" value="true">
603
+ <span class="toggle-slider"></span>
604
+ </span>
605
+ <span class="tooltip-text">Activer le raisonnement avancé (1 fois/min)</span>
606
+ </label>
607
+ </div>
608
+
609
+ <!-- Upload Button -->
610
+ <div>
611
+ <label for="file_upload" class="cursor-pointer flex items-center text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 tooltip">
612
+ <i class="fa-solid fa-paperclip"></i>
613
+ <span class="ml-1.5 hidden sm:inline">Fichier</span>
614
  <input type="file" id="file_upload" name="file" class="hidden" accept=".txt,.pdf,.png,.jpg,.jpeg">
615
+ <span class="tooltip-text">Joindre un fichier (txt, pdf, image)</span>
616
  </label>
617
+
618
+ <!-- File Chip (apparaît quand un fichier est sélectionné) -->
619
+ <div id="file-chip" class="chip mt-2 hidden">
620
+ <i class="fa-solid fa-file chip-icon"></i>
621
+ <span id="file-name" class="truncate max-w-[120px]"></span>
622
+ <i id="clear-file" class="fa-solid fa-xmark chip-close"></i>
623
+ </div>
624
  </div>
625
  </div>
626
+
627
+ <!-- Chat Input Form -->
628
+ <form id="chat-form" class="p-3 sm:p-4 bg-white dark:bg-gray-900">
629
+ <div class="relative">
630
+ <input
631
+ type="text"
632
+ id="prompt"
633
+ name="prompt"
634
+ class="chat-input w-full pl-4 pr-12 py-3 rounded-full border focus:outline-none text-sm sm:text-base"
635
+ placeholder="Posez votre question à Mariam..."
636
+ autocomplete="off"
637
+ >
638
+ <button
639
+ type="submit"
640
+ id="send-button"
641
+ class="absolute right-2 top-1/2 transform -translate-y-1/2 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-300 text-white rounded-full p-2 transition focus:outline-none focus:ring-2 focus:ring-primary-400"
642
+ title="Envoyer le message"
643
+ >
644
+ <i class="fa-solid fa-paper-plane"></i>
645
+ </button>
646
+ </div>
647
+ <div class="text-xs text-center mt-2 text-gray-400 dark:text-gray-500">
648
+ Appuyez sur <kbd class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 rounded border border-gray-300 dark:border-gray-700">Entrée</kbd> pour envoyer • <kbd class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 rounded border border-gray-300 dark:border-gray-700">Shift+Entrée</kbd> pour une nouvelle ligne
649
+ </div>
650
+ </form>
651
  </div>
652
+ </main>
653
 
654
+ <!-- Scripts -->
 
 
 
 
 
 
 
 
 
 
 
 
 
655
  <script>
656
  document.addEventListener('DOMContentLoaded', () => {
657
+ // Elements
658
  const chatForm = document.getElementById('chat-form');
659
  const promptInput = document.getElementById('prompt');
660
  const chatMessages = document.getElementById('chat-messages');
 
662
  const historyLoadingIndicator = document.getElementById('history-loading');
663
  const errorMessageDiv = document.getElementById('error-message');
664
  const errorTextP = document.getElementById('error-text');
665
+ const dismissErrorBtn = document.getElementById('dismiss-error');
666
  const webSearchToggle = document.getElementById('web_search_toggle');
667
  const fileUpload = document.getElementById('file_upload');
668
+ const fileChip = document.getElementById('file-chip');
669
  const fileNameSpan = document.getElementById('file-name');
670
  const clearFileButton = document.getElementById('clear-file');
671
  const filePreview = document.getElementById('file-preview');
672
+ const previewArea = document.getElementById('preview-area');
673
  const sendButton = document.getElementById('send-button');
674
  const clearForm = document.getElementById('clear-form');
675
  const advancedToggle = document.getElementById('advanced_reasoning_toggle');
676
  const advancedCooldownTimerSpan = document.getElementById('advanced-cooldown-timer');
677
+ const themeToggleBtn = document.getElementById('theme-toggle');
678
 
679
+ // API endpoints
680
  const API_CHAT_ENDPOINT = '/api/chat';
681
  const API_HISTORY_ENDPOINT = '/api/history';
682
  const CLEAR_ENDPOINT = '/clear';
683
 
684
+ // Constantes
685
+ const COOLDOWN_DURATION = 60 * 1000; // 60 secondes
686
+
687
+ // Variables
688
+ let advancedToggleCooldownEndTime = 0;
689
+ let isComposing = false; // Pour la composition (pour les langues qui utilisent IME)
690
+
691
+ // Thème
692
+ function initializeTheme() {
693
+ // Vérifier les préférences locales ou systèmes
694
+ if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
695
+ document.documentElement.classList.add('dark');
696
+ } else {
697
+ document.documentElement.classList.remove('dark');
698
+ }
699
+ }
700
+
701
+ function toggleTheme() {
702
+ if (document.documentElement.classList.contains('dark')) {
703
+ document.documentElement.classList.remove('dark');
704
+ localStorage.theme = 'light';
705
+ } else {
706
+ document.documentElement.classList.add('dark');
707
+ localStorage.theme = 'dark';
708
+ }
709
+ }
710
+
711
+ themeToggleBtn.addEventListener('click', toggleTheme);
712
+ initializeTheme();
713
+
714
+ // Fonction de défilement vers le bas
715
+ function scrollToBottom(smooth = true) {
716
  setTimeout(() => {
717
+ chatMessages.scrollTo({
718
+ top: chatMessages.scrollHeight,
719
+ behavior: smooth ? 'smooth' : 'auto'
720
+ });
721
  }, 50);
722
  }
723
 
724
+ // Gestion du chargement
 
 
725
  function showLoading(show) {
726
+ if (show) {
727
+ loadingIndicator.classList.remove('hidden');
 
728
  chatMessages.appendChild(loadingIndicator);
729
  scrollToBottom();
730
+ } else {
731
+ loadingIndicator.classList.add('hidden');
732
  }
733
+
734
  sendButton.disabled = show;
735
  promptInput.disabled = show;
736
  fileUpload.disabled = show;
737
  clearFileButton.disabled = show;
 
738
  }
739
 
740
+ // Gestion des erreurs
741
  function displayError(message) {
742
  errorTextP.textContent = message || "Une erreur inconnue est survenue.";
743
+ errorMessageDiv.classList.remove('hidden');
744
  errorMessageDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
745
  }
746
+
747
+ // Fermer le message d'erreur
748
+ dismissErrorBtn.addEventListener('click', () => {
749
+ errorMessageDiv.classList.add('hidden');
750
+ });
751
 
752
+ // Ajouter un message au chat
753
+ function addMessageToChat(role, content, isHtml = false) {
754
+ errorMessageDiv.classList.add('hidden');
755
+
756
  const messageWrapper = document.createElement('div');
757
+ messageWrapper.classList.add('flex', role === 'user' ? 'justify-end' : 'justify-start');
758
+
759
+ let html = '';
760
+
 
761
  if (role === 'user') {
762
+ // Message utilisateur
763
+ html = `
764
+ <div class="flex items-start space-x-2 max-w-[85%]">
765
+ <div class="message-bubble user-message">
766
+ <p class="text-sm sm:text-base whitespace-pre-wrap break-words">${escapeHtml(content)}</p>
767
+ </div>
768
+ </div>
769
+ `;
770
  } else {
771
+ // Message assistant
772
+ html = `
773
+ <div class="flex items-start space-x-2 max-w-[85%]">
774
+ <div class="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0 dark:bg-primary-900/50">
775
+ <span class="text-lg">✨</span>
776
+ </div>
777
+ <div class="message-bubble assistant-message bg-secondary-50 text-secondary-900 border border-secondary-200 relative">
778
+ <div class="prose prose-sm sm:prose-base max-w-none dark:prose-invert">
779
+ ${isHtml ? content : escapeHtml(content)}
780
+ </div>
781
+ <button class="copy-btn text-xs bg-white/90 dark:bg-gray-800/90 hover:bg-gray-100 dark:hover:bg-gray-700 py-1 px-2 rounded text-gray-600 dark:text-gray-300 flex items-center">
782
+ <i class="fa-regular fa-copy mr-1"></i> Copier
783
+ </button>
784
+ </div>
785
+ </div>
786
+ `;
787
+ }
788
+
789
+ messageWrapper.innerHTML = html;
790
+ chatMessages.insertBefore(messageWrapper, loadingIndicator);
791
+
792
+ // Activer les boutons de copie pour les blocs de code
793
+ const preTags = messageWrapper.querySelectorAll('pre');
794
+ preTags.forEach(pre => {
795
+ if (!pre.querySelector('.code-copy-btn')) {
796
+ const codeBtn = document.createElement('button');
797
+ codeBtn.className = 'code-copy-btn bg-white/90 dark:bg-gray-800/90 text-xs p-1 rounded text-gray-600 dark:text-gray-300';
798
+ codeBtn.innerHTML = '<i class="fa-regular fa-copy"></i>';
799
+ codeBtn.title = 'Copier le code';
800
+ codeBtn.addEventListener('click', () => {
801
+ const code = pre.querySelector('code')?.innerText || pre.innerText;
802
+ navigator.clipboard.writeText(code)
803
+ .then(() => {
804
+ codeBtn.innerHTML = '<i class="fa-solid fa-check"></i>';
805
+ setTimeout(() => {
806
+ codeBtn.innerHTML = '<i class="fa-regular fa-copy"></i>';
807
+ }, 2000);
808
+ });
809
+ });
810
+ pre.appendChild(codeBtn);
811
  }
812
+ });
813
+
814
+ // Activer les boutons de copie pour les messages
815
+ const copyBtn = messageWrapper.querySelector('.copy-btn');
816
+ if (copyBtn) {
 
 
817
  copyBtn.addEventListener('click', (e) => {
818
  e.stopPropagation();
819
  e.preventDefault();
820
+
821
+ const textContent = messageWrapper.querySelector('.prose').innerText;
822
+ navigator.clipboard.writeText(textContent)
 
 
 
 
 
823
  .then(() => {
824
+ copyBtn.innerHTML = '<i class="fa-solid fa-check mr-1"></i> Copié';
825
  setTimeout(() => {
826
+ copyBtn.innerHTML = '<i class="fa-regular fa-copy mr-1"></i> Copier';
827
  }, 2000);
 
 
 
828
  });
829
  });
 
830
  }
831
+
 
 
832
  if (historyLoadingIndicator.parentNode !== chatMessages) {
833
  scrollToBottom();
834
  }
835
  }
836
 
837
+ // Échappement HTML pour éviter les injections XSS
838
+ function escapeHtml(unsafe) {
839
+ return unsafe
840
+ .replace(/&/g, "&amp;")
841
+ .replace(/</g, "&lt;")
842
+ .replace(/>/g, "&gt;")
843
+ .replace(/"/g, "&quot;")
844
+ .replace(/'/g, "&#039;");
845
+ }
846
+
847
+ // Gestion du cooldown du mode avancé
848
  function startAdvancedCooldownTimer() {
849
+ advancedToggle.disabled = true;
850
  advancedToggleCooldownEndTime = Date.now() + COOLDOWN_DURATION;
851
 
852
  const updateTimer = () => {
853
+ const now = Date.now();
854
+ if (now >= advancedToggleCooldownEndTime) {
855
+ clearInterval(intervalId);
856
+ advancedCooldownTimerSpan.classList.add('hidden');
857
+ advancedToggle.disabled = false;
858
+ advancedToggleCooldownEndTime = 0;
859
+ } else {
860
+ const remainingSeconds = Math.ceil((advancedToggleCooldownEndTime - now) / 1000);
861
+ advancedCooldownTimerSpan.textContent = `(${remainingSeconds}s)`;
862
+ advancedCooldownTimerSpan.classList.remove('hidden');
863
+ }
864
  };
865
 
866
  const intervalId = setInterval(updateTimer, 1000);
867
+ updateTimer();
868
  }
869
 
870
+ // Chargement de l'historique de chat
871
  async function loadChatHistory() {
872
  historyLoadingIndicator.style.display = 'block';
873
  try {
 
877
  try {
878
  const errData = await response.json();
879
  errorMsg = errData.error || errorMsg;
880
+ } catch (e) {}
881
  throw new Error(errorMsg);
882
  }
883
+
884
  const data = await response.json();
885
  if (data.success && Array.isArray(data.history)) {
886
+ // Vider le conteneur et préparer les nouvelles entrées
887
  chatMessages.innerHTML = '';
888
  chatMessages.appendChild(loadingIndicator);
889
+ loadingIndicator.classList.add('hidden');
890
+
891
  if (data.history.length === 0) {
892
+ // Message d'accueil
893
+ addMessageToChat('assistant', "Bonjour ! Je suis Mariam, votre assistant IA. Comment puis-je vous aider aujourd'hui ?", true);
894
  } else {
895
+ // Afficher l'historique
896
  data.history.forEach(message => {
897
  const isAssistantHtml = message.role === 'assistant';
898
  addMessageToChat(message.role, message.text, isAssistantHtml);
899
  });
900
  }
901
+
902
+ scrollToBottom(false);
903
  } else {
904
  throw new Error(data.error || "Format de réponse de l'historique invalide.");
905
  }
906
  } catch (error) {
907
  chatMessages.innerHTML = '';
908
  chatMessages.appendChild(loadingIndicator);
909
+ loadingIndicator.classList.add('hidden');
910
  displayError(`Impossible de charger l'historique: ${error.message}`);
911
  } finally {
912
+ historyLoadingIndicator.remove();
 
 
913
  promptInput.focus();
914
  }
915
  }
916
 
917
+ // Gestion des fichiers
918
  function clearFileInput() {
919
  fileUpload.value = '';
920
+ fileChip.classList.add('hidden');
921
+ filePreview.classList.add('hidden');
922
+ previewArea.classList.add('hidden');
923
  filePreview.innerHTML = '';
924
  }
925
 
 
927
  if (fileUpload.files.length > 0) {
928
  const file = fileUpload.files[0];
929
  const name = file.name;
930
+
931
+ // Mettre à jour le chip de fichier
932
+ fileNameSpan.textContent = name;
933
  fileNameSpan.title = name;
934
+ fileChip.classList.remove('hidden');
935
+
936
+ // Si c'est une image, créer une prévisualisation
937
  if (file.type.startsWith('image/')) {
938
  const reader = new FileReader();
939
  reader.onload = (e) => {
940
+ filePreview.innerHTML = `
941
+ <div class="file-preview">
942
+ <img src="${e.target.result}" alt="Prévisualisation de l'image">
943
+ </div>
944
+ `;
945
+ filePreview.classList.remove('hidden');
946
+ previewArea.classList.remove('hidden');
947
  };
948
  reader.readAsDataURL(file);
949
  } else {
950
+ // Afficher une icône pour les autres types de fichiers
951
+ filePreview.innerHTML = `
952
+ <div class="flex items-center justify-center py-4">
953
+ <div class="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg text-center">
954
+ <i class="fa-solid ${getFileIcon(file.type)} text-3xl text-gray-600 dark:text-gray-300 mb-2"></i>
955
+ <p class="text-sm text-gray-500 dark:text-gray-400">${formatFileSize(file.size)}</p>
956
+ </div>
957
+ </div>
958
+ `;
959
+ filePreview.classList.remove('hidden');
960
+ previewArea.classList.remove('hidden');
961
  }
962
  } else {
963
  clearFileInput();
964
  }
965
  });
966
 
967
+ // Obtenir l'icône en fonction du type de fichier
968
+ function getFileIcon(fileType) {
969
+ if (fileType.includes('pdf')) return 'fa-file-pdf';
970
+ if (fileType.includes('text')) return 'fa-file-lines';
971
+ return 'fa-file';
972
+ }
973
+
974
+ // Formater la taille du fichier
975
+ function formatFileSize(bytes) {
976
+ if (bytes < 1024) return bytes + ' octets';
977
+ if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' Ko';
978
+ return (bytes / 1048576).toFixed(1) + ' Mo';
979
+ }
980
+
981
  clearFileButton.addEventListener('click', () => {
982
  clearFileInput();
983
  });
984
 
985
+ // Soumission du formulaire
986
  chatForm.addEventListener('submit', async (e) => {
987
  e.preventDefault();
988
  const prompt = promptInput.value.trim();
 
995
  promptInput.focus();
996
  return;
997
  }
998
+
999
+ errorMessageDiv.classList.add('hidden');
1000
 
1001
+ // Vérification du cooldown pour le mode avancé
1002
  if (useAdvanced) {
1003
+ const now = Date.now();
1004
+ if (now < advancedToggleCooldownEndTime) {
1005
+ const remainingSeconds = Math.ceil((advancedToggleCooldownEndTime - now) / 1000);
1006
+ displayError(`Le raisonnement avancé est disponible dans ${remainingSeconds} seconde(s).`);
1007
+ return;
1008
+ }
 
1009
  }
 
1010
 
1011
+ // Construire le message utilisateur
1012
  let userMessageText = prompt;
1013
  if (file && file.name) {
1014
+ userMessageText = prompt ? `${prompt}` : `[Fichier joint: ${file.name}]`;
1015
  }
1016
+
1017
  addMessageToChat('user', userMessageText);
1018
+
1019
+ // Préparer les données
1020
  const formData = new FormData();
1021
  formData.append('prompt', prompt);
1022
  formData.append('web_search', useWebSearch);
1023
+
1024
  if (file) {
1025
+ formData.append('file', file);
1026
  }
1027
+
1028
  formData.append('advanced_reasoning', useAdvanced);
1029
 
1030
+ // Afficher le chargement et nettoyer le formulaire
1031
  showLoading(true);
1032
  promptInput.value = '';
1033
  clearFileInput();
1034
 
1035
+ // Réinitialiser les options
1036
+ advancedToggle.checked = false;
1037
+ if (useAdvanced) startAdvancedCooldownTimer();
 
1038
 
1039
  try {
1040
  const response = await fetch(API_CHAT_ENDPOINT, {
1041
  method: 'POST',
1042
  body: formData,
1043
  });
1044
+
1045
  const data = await response.json();
1046
+
1047
  if (!response.ok) {
1048
  throw new Error(data.error || `Erreur serveur: ${response.status}`);
1049
  }
1050
+
1051
  if (data.success && data.message) {
1052
  addMessageToChat('assistant', data.message, true);
1053
  } else {
 
1055
  }
1056
  } catch (error) {
1057
  displayError(error.message);
 
1058
  } finally {
1059
  showLoading(false);
1060
  promptInput.focus();
1061
  }
1062
  });
1063
 
1064
+ // Effacement de la conversation
1065
  clearForm.addEventListener('submit', async (e) => {
1066
  e.preventDefault();
1067
+
1068
+ // Demander confirmation
1069
+ const confirmDialog = document.createElement('div');
1070
+ confirmDialog.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
1071
+ confirmDialog.innerHTML = `
1072
+ <div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-xl max-w-sm mx-4 animate-[message-fade-in_0.2s_ease-out]">
1073
+ <h3 class="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100">Confirmer l'effacement</h3>
1074
+ <p class="text-gray-600 dark:text-gray-300 mb-4">Êtes-vous sûr de vouloir effacer toute la conversation ?</p>
1075
+ <div class="flex justify-end space-x-3">
1076
+ <button id="cancel-clear" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
1077
+ Annuler
1078
+ </button>
1079
+ <button id="confirm-clear" class="px-4 py-2 text-sm font-medium text-white bg-red-500 rounded hover:bg-red-600 transition-colors">
1080
+ Effacer
1081
+ </button>
1082
+ </div>
1083
+ </div>
1084
+ `;
1085
+
1086
+ document.body.appendChild(confirmDialog);
1087
+ document.body.classList.add('overflow-hidden');
1088
+
1089
+ // Gérer les boutons
1090
+ document.getElementById('cancel-clear').addEventListener('click', () => {
1091
+ document.body.removeChild(confirmDialog);
1092
+ document.body.classList.remove('overflow-hidden');
1093
+ });
1094
+
1095
+ document.getElementById('confirm-clear').addEventListener('click', async () => {
1096
+ document.body.removeChild(confirmDialog);
1097
+ document.body.classList.remove('overflow-hidden');
1098
+
1099
+ const originalButtonText = e.target.querySelector('button').innerHTML;
1100
+ e.target.querySelector('button').innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
1101
  e.target.querySelector('button').disabled = true;
1102
+
1103
  try {
1104
  const response = await fetch(CLEAR_ENDPOINT, {
1105
  method: 'POST',
 
1107
  'X-Requested-With': 'XMLHttpRequest'
1108
  }
1109
  });
1110
+
1111
  const data = await response.json();
1112
+
1113
  if (response.ok && data.success) {
1114
  chatMessages.innerHTML = '';
1115
  chatMessages.appendChild(loadingIndicator);
1116
+ loadingIndicator.classList.add('hidden');
1117
+
1118
+ addMessageToChat('assistant', "Conversation effacée. Comment puis-je vous aider ?", true);
1119
+ errorMessageDiv.classList.add('hidden');
1120
  } else {
1121
  throw new Error(data.error || "Impossible d'effacer côté serveur.");
1122
  }
1123
  } catch (error) {
1124
  displayError(`Erreur lors de l'effacement du chat: ${error.message}`);
1125
  } finally {
1126
+ e.target.querySelector('button').innerHTML = originalButtonText;
1127
  e.target.querySelector('button').disabled = false;
1128
  promptInput.focus();
1129
  }
1130
+ });
1131
+ });
1132
+
1133
+ // Raccourcis clavier
1134
+ promptInput.addEventListener('keydown', (e) => {
1135
+ // Ignorer pendant la composition IME
1136
+ if (isComposing) return;
1137
+
1138
+ // Shift+Enter pour nouvelle ligne, Enter pour envoyer
1139
+ if (e.key === 'Enter' && !e.shiftKey) {
1140
+ e.preventDefault();
1141
+ if (!sendButton.disabled) {
1142
+ chatForm.dispatchEvent(new Event('submit'));
1143
+ }
1144
  }
1145
  });
1146
+
1147
+ // Support IME pour les langues asiatiques
1148
+ promptInput.addEventListener('compositionstart', () => {
1149
+ isComposing = true;
1150
+ });
1151
+
1152
+ promptInput.addEventListener('compositionend', () => {
1153
+ isComposing = false;
1154
+ });
1155
 
1156
+ // Démarrer le chargement
1157
  loadChatHistory();
1158
+
1159
+ // Activer l'ajustement automatique de la hauteur du textarea
1160
+ const resizeInput = () => {
1161
+ // Pour une implémentation future si on change l'input en textarea
1162
+ };
1163
+
1164
+ // Vérifier les préréglages du navigateur pour le thème sombre
1165
+ const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
1166
+ prefersDarkScheme.addEventListener('change', (e) => {
1167
+ if (!localStorage.theme) { // Seulement si l'utilisateur n'a pas explicitement choisi
1168
+ if (e.matches) {
1169
+ document.documentElement.classList.add('dark');
1170
+ } else {
1171
+ document.documentElement.classList.remove('dark');
1172
+ }
1173
+ }
1174
+ });
1175
  });
1176
  </script>
1177
  </body>