Docfile commited on
Commit
26a9bf8
·
verified ·
1 Parent(s): b212844

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +234 -335
templates/index.html CHANGED
@@ -12,30 +12,27 @@
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
- /* Styles généraux */
16
  html, body {
17
- min-height: 100vh; /* Assure que le body prend toute la hauteur */
18
  margin: 0;
19
  padding: 0;
20
  }
21
  body {
22
  display: flex;
23
- flex-direction: column; /* Empile les enfants verticalement */
24
  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";
25
  }
26
  #chat-container {
27
  display: flex;
28
- flex-direction: column; /* Empile les zones de message et d'input */
29
- flex-grow: 1; /* Permet au conteneur de grandir pour remplir l'espace */
30
- min-height: 0; /* Nécessaire pour que flex-grow fonctionne correctement dans un conteneur flex */
31
  }
32
  #chat-messages {
33
- flex-grow: 1; /* Permet à la zone de message de grandir */
34
- overflow-y: auto; /* Ajoute une barre de défilement si nécessaire */
35
- min-height: 0; /* Nécessaire pour le défilement correct */
36
  }
37
-
38
- /* Barre de défilement personnalisée */
39
  ::-webkit-scrollbar {
40
  width: 8px;
41
  }
@@ -50,23 +47,22 @@
50
  ::-webkit-scrollbar-thumb:hover {
51
  background: #7a7a7a;
52
  }
53
-
54
- /* Styles pour le rendu Markdown (Prose) */
55
- #chat-messages .prose code:not(pre code) { /* Inline code */
56
- background-color: #e5e7eb; /* gray-200 */
57
  padding: 0.2em 0.4em;
58
  font-size: 85%;
59
  border-radius: 4px;
60
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
61
  }
62
- #chat-messages .prose pre { /* Code block */
63
- background-color: #f3f4f6; /* gray-100 */
64
  padding: 0.8em 1em;
65
  border-radius: 6px;
66
- overflow-x: auto; /* Horizontal scroll for long code lines */
67
- font-size: 0.875rem; /* text-sm */
68
  }
69
- #chat-messages .prose pre code { /* Code inside block */
70
  background-color: transparent;
71
  padding: 0;
72
  font-size: inherit;
@@ -74,43 +70,39 @@
74
  color: inherit;
75
  }
76
  #chat-messages .prose blockquote {
77
- border-left-color: #9ca3af; /* gray-400 */
78
- color: #4b5563; /* gray-600 */
79
  }
80
  #chat-messages .prose strong {
81
- color: #1f2937; /* gray-800 */
82
  }
83
  #chat-messages .prose a {
84
- color: #2563eb; /* blue-600 */
85
  text-decoration: underline;
86
- text-decoration-color: #93c5fd; /* blue-300 */
87
  transition: color 0.2s ease;
88
  }
89
  #chat-messages .prose a:hover {
90
- color: #1d4ed8; /* blue-700 */
91
- text-decoration-color: #60a5fa; /* blue-400 */
92
  }
93
-
94
- /* Indicateur de chargement */
95
  #history-loading {
96
  padding: 20px;
97
  text-align: center;
98
- color: #6b7280; /* gray-500 */
99
  font-style: italic;
100
  }
101
-
102
  /* Zone de prévisualisation des images */
103
  #file-preview {
104
  margin-top: 8px;
105
  }
106
  #file-preview img {
107
- max-width: 100%; /* Empêche l'image de dépasser */
108
- max-height: 150px; /* Limite la hauteur */
109
- border: 1px solid #ddd; /* gray-300 */
110
  border-radius: 4px;
111
  padding: 4px;
112
  }
113
-
114
  /* Bouton de copie (positionné en haut à droite de la bulle de réponse) */
115
  .copy-btn {
116
  position: absolute;
@@ -121,15 +113,10 @@
121
  border-radius: 4px;
122
  padding: 2px 4px;
123
  cursor: pointer;
124
- font-size: 0.75rem; /* text-xs */
125
- display: none; /* Caché par défaut */
126
- color: #374151; /* gray-700 */
127
- transition: background-color 0.2s ease;
128
- }
129
- .copy-btn:hover {
130
- background: rgba(230, 230, 230, 0.9);
131
  }
132
- /* Afficher le bouton au survol de la bulle parente */
133
  .message-wrapper:hover .copy-btn {
134
  display: block;
135
  }
@@ -148,23 +135,26 @@
148
 
149
  <!-- Conteneur Principal du Chat -->
150
  <div id="chat-container" class="max-w-4xl w-full mx-auto bg-white shadow-xl rounded-b-lg flex flex-col flex-grow">
151
-
152
  <!-- Zone d'affichage des messages -->
153
  <div id="chat-messages" class="flex-grow overflow-y-auto p-4 sm:p-6 space-y-4 scroll-smooth">
154
- <!-- Indicateur de chargement initial de l'historique -->
155
  <div id="history-loading">
156
  <div class="flex justify-center items-center space-x-2">
157
- <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"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <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> </svg>
 
 
 
158
  <span>Chargement de l'historique...</span>
159
  </div>
160
  </div>
161
- <!-- Indicateur de chargement pendant la réponse de l'IA -->
162
- <div id="loading-indicator" class="text-center text-gray-500 italic py-4" style="display: none;">
163
  <div class="flex justify-center items-center space-x-2">
164
- <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"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <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> </svg>
165
- <span id="loading-text">Mariam réfléchit...</span> <!-- Texte dynamique -->
 
 
 
166
  </div>
167
- </div>
168
  </div>
169
 
170
  <!-- Zone d'erreur -->
@@ -173,58 +163,50 @@
173
  <p id="error-text">Le message d'erreur détaillé ira ici.</p>
174
  </div>
175
 
176
- <!-- Barre d'options, d'upload et YouTube -->
177
- <div class="bg-gray-50 border-t border-gray-200 px-4 py-2 flex-shrink-0 space-y-2">
178
- <!-- Ligne 1 : Toggles & Upload Fichier -->
179
  <div class="flex items-center justify-between text-sm">
180
- <!-- Groupe de Toggles (Gauche) -->
181
- <div class="flex items-center space-x-4">
182
- <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">
183
- <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">
184
- <span class="hidden sm:inline">Recherche Web</span>
185
- <span class="sm:hidden">Web</span> <!-- Version courte pour mobile -->
186
- </label>
187
- <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é (Gemini Pro - recommandé pour vidéo, peut avoir un cooldown)">
188
- <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">
189
- <span class="hidden sm:inline">Raisonnement Avancé</span>
190
- <span class="sm:hidden">Avancé</span> <!-- Version courte pour mobile -->
191
- <span id="advanced-cooldown-timer" class="text-xs text-gray-500 ml-1" style="display: none;"></span> <!-- Indicateur cooldown -->
192
- </label>
193
- </div>
194
- <!-- Upload Fichier (Droite) -->
195
  <div class="flex items-center space-x-2">
196
- <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, img, vidéo)">
197
- <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"> <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" /> </svg>
198
- <span class="hidden sm:inline">Fichier/Vidéo</span>
199
- <input type="file" id="file_upload" name="file" class="hidden" accept=".txt,.pdf,.png,.jpg,.jpeg,.mp4,.mov,.avi,.mkv,.webm,image/*,video/*"> <!-- Input caché -->
 
 
200
  </label>
201
- <span id="file-name" class="text-gray-500 text-xs truncate max-w-[150px]" title=""></span> <!-- Nom du fichier -->
202
- <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;"> <!-- Bouton pour retirer -->
203
- <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"> <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> </svg>
 
 
204
  </button>
205
  </div>
206
  </div>
207
-
208
- <!-- Ligne 2 : Input URL YouTube -->
209
- <div class="flex items-center space-x-2">
210
- <label for="youtube_url_input" class="text-sm font-medium text-gray-700 whitespace-nowrap">Lien YouTube:</label>
211
- <input type="url" id="youtube_url_input" name="youtube_url" class="flex-grow form-input px-3 py-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent text-sm" placeholder="Collez l'URL de la vidéo YouTube ici...">
212
- </div>
213
-
214
- <!-- Notes & Prévisualisation -->
215
- <p id="video-processing-note" class="text-xs text-gray-500 text-center mt-1" style="display: none;">
216
- Note: Le traitement des vidéos (upload ou YouTube) peut prendre plusieurs minutes.
217
- </p>
218
- <div id="file-preview"></div> <!-- Zone pour prévisualiser les images -->
219
  </div>
220
 
221
- <!-- Formulaire d'entrée du message principal -->
222
  <form id="chat-form" class="bg-gray-100 p-3 sm:p-4 border-t border-gray-200 rounded-b-lg flex-shrink-0">
223
  <div class="flex items-center space-x-2 sm:space-x-3">
224
- <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 une question (optionnel si fichier/lien)..." autocomplete="off">
225
  <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">
226
- <!-- Icône d'envoi -->
227
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <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" /> </svg>
 
228
  </button>
229
  </div>
230
  </form>
@@ -233,12 +215,10 @@
233
  <!-- Script JavaScript pour l'interaction -->
234
  <script>
235
  document.addEventListener('DOMContentLoaded', () => {
236
- // --- Références aux éléments DOM ---
237
  const chatForm = document.getElementById('chat-form');
238
  const promptInput = document.getElementById('prompt');
239
  const chatMessages = document.getElementById('chat-messages');
240
  const loadingIndicator = document.getElementById('loading-indicator');
241
- const loadingText = document.getElementById('loading-text');
242
  const historyLoadingIndicator = document.getElementById('history-loading');
243
  const errorMessageDiv = document.getElementById('error-message');
244
  const errorTextP = document.getElementById('error-text');
@@ -251,388 +231,307 @@
251
  const clearForm = document.getElementById('clear-form');
252
  const advancedToggle = document.getElementById('advanced_reasoning_toggle');
253
  const advancedCooldownTimerSpan = document.getElementById('advanced-cooldown-timer');
254
- const videoProcessingNote = document.getElementById('video-processing-note');
255
- const youtubeUrlInput = document.getElementById('youtube_url_input'); // Input YouTube
256
 
257
- // --- Constantes & État ---
258
  const API_CHAT_ENDPOINT = '/api/chat';
259
  const API_HISTORY_ENDPOINT = '/api/history';
260
  const CLEAR_ENDPOINT = '/clear';
261
- let advancedToggleCooldownEndTime = 0; // Timestamp de fin du cooldown
262
- const COOLDOWN_DURATION = 60 * 1000; // 60 secondes en millisecondes
263
-
264
- // --- Fonctions Utilitaires ---
265
 
266
- // Fait défiler la zone de chat vers le bas
267
  function scrollToBottom() {
268
- setTimeout(() => { // Léger délai pour laisser le temps au DOM de se mettre à jour
269
  chatMessages.scrollTop = chatMessages.scrollHeight;
270
  }, 50);
271
  }
272
 
273
- // Affiche ou cache l'indicateur de chargement principal
274
- function showLoading(show, isMediaOrYoutube = false) {
 
 
275
  const currentlyLoading = loadingIndicator.style.display !== 'none';
276
  if (show && !currentlyLoading) {
277
- loadingText.textContent = isMediaOrYoutube
278
- ? "Traitement média en cours..." // Texte si fichier ou URL YT
279
- : "Mariam réfléchit..."; // Texte par défaut
280
  loadingIndicator.style.display = 'block';
281
- chatMessages.appendChild(loadingIndicator); // Déplace l'indicateur à la fin
282
  scrollToBottom();
283
  } else if (!show && currentlyLoading) {
284
  loadingIndicator.style.display = 'none';
285
- loadingText.textContent = "Mariam réfléchit..."; // Réinitialise le texte
286
  }
287
- // Désactive les éléments pendant le chargement
288
  sendButton.disabled = show;
289
  promptInput.disabled = show;
290
  fileUpload.disabled = show;
291
  clearFileButton.disabled = show;
292
- youtubeUrlInput.disabled = show; // Désactive aussi l'input YouTube
293
- // Ne pas désactiver les toggles pendant le chargement normal
294
  }
295
 
296
- // Affiche un message d'erreur
297
  function displayError(message) {
298
  errorTextP.textContent = message || "Une erreur inconnue est survenue.";
299
- errorMessageDiv.style.display = 'flex'; // Utilise flex pour aligner l'icône et le texte si nécessaire
300
- // Fait défiler pour voir l'erreur
301
  errorMessageDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
302
  }
303
 
304
- // Ajoute un message (utilisateur ou assistant) à la zone de chat
305
  function addMessageToChat(role, text, isHtml = false) {
306
- errorMessageDiv.style.display = 'none'; // Cache l'erreur précédente
307
- const messageWrapper = document.createElement('div');
308
- messageWrapper.classList.add('message-wrapper', 'flex', role === 'user' ? 'justify-end' : 'justify-start', 'mb-4');
309
-
310
- const bubbleDiv = document.createElement('div');
311
- bubbleDiv.classList.add('p-3', 'rounded-lg', 'max-w-[85%]', 'sm:max-w-[75%]', 'shadow-md', 'relative'); // `relative` pour le bouton copier
312
-
313
- if (role === 'user') {
314
- // Style pour les messages utilisateur
315
- bubbleDiv.classList.add('bg-blue-500', 'text-white', 'rounded-br-none');
316
- const paragraph = document.createElement('p');
317
- // `break-words` pour couper les mots longs, `whitespace-pre-wrap` pour conserver les sauts de ligne de l'utilisateur
318
- paragraph.classList.add('text-sm', 'sm:text-base', 'break-words', 'whitespace-pre-wrap');
319
- paragraph.textContent = text;
320
- bubbleDiv.appendChild(paragraph);
321
- } else {
322
- // Style pour les messages de l'assistant
323
- bubbleDiv.classList.add('bg-gray-100', 'text-gray-800', 'rounded-bl-none', 'border', 'border-gray-200');
324
- // Conteneur pour le contenu formaté en Markdown/HTML
325
- const proseDiv = document.createElement('div');
326
- 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');
327
- if (isHtml) {
328
- proseDiv.innerHTML = text; // Injecte le HTML si fourni
329
- } else {
330
- proseDiv.textContent = text; // Sinon, texte brut
331
- }
332
- bubbleDiv.appendChild(proseDiv);
333
-
334
- // Ajout du bouton de copie
335
- const copyBtn = document.createElement('button');
336
- copyBtn.textContent = 'Copier';
337
- copyBtn.classList.add('copy-btn');
338
- copyBtn.title = "Copier la réponse";
339
- copyBtn.addEventListener('click', (e) => {
340
- e.stopPropagation(); // Empêche le clic de se propager
341
- e.preventDefault();
342
- // Copie le texte brut (innerText) même si c'est du HTML
343
- let textToCopy = proseDiv.innerText || text; // Fallback au texte original
344
- navigator.clipboard.writeText(textToCopy)
345
- .then(() => {
346
- copyBtn.textContent = 'Copié'; // Confirmation visuelle
347
- setTimeout(() => { copyBtn.textContent = 'Copier'; }, 2000); // Revient à l'état initial après 2s
348
- })
349
- .catch(err => {
350
- console.error('Erreur lors de la copie :', err);
351
- // Optionnel : Afficher une petite erreur près du bouton
352
- });
353
- });
354
- bubbleDiv.appendChild(copyBtn);
355
- }
356
-
357
- messageWrapper.appendChild(bubbleDiv);
358
- // Insère le nouveau message AVANT l'indicateur de chargement s'il est visible
359
- if (loadingIndicator.parentNode === chatMessages) {
360
- chatMessages.insertBefore(messageWrapper, loadingIndicator);
361
- } else {
362
- chatMessages.appendChild(messageWrapper); // Sinon, ajoute à la fin
363
- }
364
-
365
- // Ne fait défiler que si l'historique n'est pas en cours de chargement (pour éviter les sauts)
366
- if (historyLoadingIndicator.style.display === 'none') {
367
- scrollToBottom();
368
- }
369
  }
370
 
371
- // Démarre le minuteur pour le cooldown du raisonnement avancé
372
  function startAdvancedCooldownTimer() {
373
- advancedToggle.disabled = true; // Désactive le toggle
374
  advancedToggleCooldownEndTime = Date.now() + COOLDOWN_DURATION;
375
 
376
  const updateTimer = () => {
377
  const now = Date.now();
378
  if (now >= advancedToggleCooldownEndTime) {
379
- // Fin du cooldown
380
  clearInterval(intervalId);
381
  advancedCooldownTimerSpan.style.display = 'none';
382
- advancedToggle.disabled = false; // Réactive le toggle
383
- advancedToggleCooldownEndTime = 0;
384
  } else {
385
- // Met à jour le temps restant
386
  const remainingSeconds = Math.ceil((advancedToggleCooldownEndTime - now) / 1000);
387
  advancedCooldownTimerSpan.textContent = `(${remainingSeconds}s)`;
388
  advancedCooldownTimerSpan.style.display = 'inline';
389
  }
390
  };
391
 
392
- const intervalId = setInterval(updateTimer, 1000); // Met à jour toutes les secondes
393
- updateTimer(); // Appel initial pour afficher immédiatement
394
  }
395
 
396
- // Charge l'historique de chat depuis le serveur
397
  async function loadChatHistory() {
398
- historyLoadingIndicator.style.display = 'block'; // Affiche le chargement de l'historique
399
  try {
400
  const response = await fetch(API_HISTORY_ENDPOINT);
401
  if (!response.ok) {
402
- let errorMsg = `Erreur serveur (${response.status})`;
403
- try { errorMsg = (await response.json()).error || errorMsg; } catch(e){}
404
- throw new Error(errorMsg);
 
 
 
405
  }
406
  const data = await response.json();
407
  if (data.success && Array.isArray(data.history)) {
408
- chatMessages.innerHTML = ''; // Vide la zone de chat avant d'ajouter l'historique
409
- chatMessages.appendChild(loadingIndicator); // Ré-ajoute le template de chargement (caché)
410
- loadingIndicator.style.display = 'none'; // S'assure qu'il est caché
411
-
412
  if (data.history.length === 0) {
413
- // Message d'accueil si l'historique est vide
414
- addMessageToChat('assistant', "Bonjour ! Posez une question, joignez un fichier/vidéo ou collez un lien YouTube.");
415
  } else {
416
- // Ajoute chaque message de l'historique
417
  data.history.forEach(message => {
418
- const isAssistantHtml = message.role === 'assistant'; // Les messages de l'assistant sont en HTML
419
- addMessageToChat(message.role, message.text, isAssistantHtml);
420
- });
421
  }
422
- scrollToBottom(); // Fait défiler vers le bas après avoir chargé l'historique
423
  } else {
424
  throw new Error(data.error || "Format de réponse de l'historique invalide.");
425
  }
426
  } catch (error) {
427
- // En cas d'erreur de chargement, vide quand même et affiche l'erreur
428
  chatMessages.innerHTML = '';
429
  chatMessages.appendChild(loadingIndicator);
430
  loadingIndicator.style.display = 'none';
431
  displayError(`Impossible de charger l'historique: ${error.message}`);
432
  } finally {
433
- // Supprime définitivement l'indicateur de chargement de l'historique
434
- const histLoadingElement = document.getElementById('history-loading');
435
- if (histLoadingElement) { histLoadingElement.remove(); }
436
- promptInput.focus(); // Met le focus sur l'input principal
437
  }
438
  }
439
 
440
- // Réinitialise l'input de fichier et la prévisualisation
441
  function clearFileInput() {
442
- fileUpload.value = ''; // Vide la valeur de l'input file
443
- fileNameSpan.textContent = ''; // Vide le nom affiché
444
- fileNameSpan.title = ''; // Vide le title (tooltip)
445
- clearFileButton.style.display = 'none'; // Cache le bouton de suppression
446
- filePreview.innerHTML = ''; // Vide la zone de prévisualisation
447
- videoProcessingNote.style.display = 'none'; // Cache la note sur le temps de traitement
448
  }
449
 
450
- // --- Écouteurs d'Événements ---
451
-
452
- // Changement dans l'input de fichier
453
  fileUpload.addEventListener('change', () => {
454
  if (fileUpload.files.length > 0) {
455
  const file = fileUpload.files[0];
456
  const name = file.name;
457
- // Affiche le nom du fichier (tronqué si trop long)
458
  fileNameSpan.textContent = name.length > 20 ? name.substring(0, 17) + '...' : name;
459
- fileNameSpan.title = name; // Nom complet en tooltip
460
- clearFileButton.style.display = 'inline-block'; // Affiche le bouton de suppression
461
- filePreview.innerHTML = ''; // Vide la prévisualisation précédente
462
- videoProcessingNote.style.display = 'none'; // Cache la note
463
-
464
- // Efface l'input YouTube si un fichier est sélectionné (priorité au fichier)
465
- youtubeUrlInput.value = '';
466
-
467
- // Affiche une prévisualisation si c'est une image
468
  if (file.type.startsWith('image/')) {
469
  const reader = new FileReader();
470
- reader.onload = (e) => { filePreview.innerHTML = `<img src="${e.target.result}" alt="Prévisualisation">`; };
 
 
471
  reader.readAsDataURL(file);
472
- } else if (file.type.startsWith('video/')) {
473
- // Affiche un texte simple pour les vidéos et la note sur le temps
474
- filePreview.innerHTML = `<p class="text-xs text-gray-500 italic">Fichier vidéo sélectionné.</p>`;
475
- videoProcessingNote.style.display = 'block';
476
  }
477
  } else {
478
- // Si aucun fichier n'est sélectionné (ou annulé), réinitialise
479
  clearFileInput();
480
  }
481
  });
482
 
483
- // Saisie dans l'input YouTube
484
- youtubeUrlInput.addEventListener('input', () => {
485
- if (youtubeUrlInput.value.trim() !== '') {
486
- // Si l'utilisateur saisit une URL, efface l'input fichier (priorité à l'URL)
487
- clearFileInput();
488
- videoProcessingNote.style.display = 'block'; // Affiche la note pour les URL aussi
489
- } else {
490
- // Si l'input URL est vidé, cache la note
491
- videoProcessingNote.style.display = 'none';
492
- }
493
- });
494
-
495
- // Clic sur le bouton pour retirer le fichier
496
  clearFileButton.addEventListener('click', () => {
497
  clearFileInput();
498
  });
499
 
500
- // Soumission du formulaire de chat principal
501
  chatForm.addEventListener('submit', async (e) => {
502
- e.preventDefault(); // Empêche la soumission classique du formulaire
503
-
504
- // Récupère les valeurs des inputs et toggles
505
  const prompt = promptInput.value.trim();
506
  const file = fileUpload.files[0];
507
- const youtubeUrl = youtubeUrlInput.value.trim();
508
  const useWebSearch = webSearchToggle.checked;
509
  const useAdvanced = advancedToggle.checked;
510
 
511
- // Validation : il faut au moins un fichier, une URL YouTube ou un prompt texte
512
- if (!file && !youtubeUrl && !prompt) {
513
- displayError("Veuillez fournir un message, un fichier/vidéo ou un lien YouTube.");
514
  promptInput.focus();
515
- return; // Arrête l'exécution si rien n'est fourni
516
  }
517
- errorMessageDiv.style.display = 'none'; // Cache les erreurs précédentes
518
 
519
- // Vérifie le cooldown pour le raisonnement avancé
520
  if (useAdvanced) {
521
  const now = Date.now();
522
  if (now < advancedToggleCooldownEndTime) {
523
- const remainingSeconds = Math.ceil((advancedToggleCooldownEndTime - now) / 1000);
524
- displayError(`Le raisonnement avancé est disponible dans ${remainingSeconds} seconde(s).`);
525
- return; // Arrête si en cooldown
526
  }
527
- // Si la vérification passe, le cooldown sera démarré APRÈS la soumission réussie
528
  }
 
529
 
530
- // Détermine si la requête concerne un média (fichier ou URL)
531
- let isMediaInput = !!file || !!youtubeUrl;
532
-
533
- // Construit le message à afficher pour l'utilisateur
534
  let userMessageText = prompt;
535
- if (file) { // Priorité au fichier pour l'affichage
536
- userMessageText = prompt ? `[${file.name}] ${prompt}` : `[${file.name}]`;
537
- } else if (youtubeUrl) { // Sinon, URL YouTube
538
- // Affiche "[YouTube]" suivi du prompt (si existe) et de l'URL sur une nouvelle ligne
539
- userMessageText = `[YouTube]${prompt ? ` ${prompt}` : ''}\n${youtubeUrl}`;
540
  }
541
- addMessageToChat('user', userMessageText); // Ajoute le message utilisateur au chat
542
-
543
- // Prépare les données à envoyer au serveur
544
  const formData = new FormData();
545
  formData.append('prompt', prompt);
546
  formData.append('web_search', useWebSearch);
547
  if (file) {
548
- formData.append('file', file); // Ajoute le fichier s'il existe
549
  }
550
- formData.append('youtube_url', youtubeUrl); // Ajoute l'URL YouTube (même si vide)
551
  formData.append('advanced_reasoning', useAdvanced);
552
 
553
- // Affiche l'indicateur de chargement (avec texte spécifique si média)
554
- showLoading(true, isMediaInput);
555
- // Réinitialise les inputs après avoir récupéré leurs valeurs
556
  promptInput.value = '';
557
- clearFileInput(); // Réinitialise aussi l'upload fichier
558
- youtubeUrlInput.value = ''; // Réinitialise l'input YouTube
559
- videoProcessingNote.style.display = 'none'; // Cache la note
560
 
561
- // Réinitialise le toggle avancé et démarre le cooldown s'il a été utilisé
562
- advancedToggle.checked = false;
563
- if (useAdvanced) startAdvancedCooldownTimer();
 
564
 
565
- // --- Appel API au backend ---
566
  try {
567
  const response = await fetch(API_CHAT_ENDPOINT, {
568
  method: 'POST',
569
- body: formData, // Envoie les données du formulaire
570
  });
571
- const data = await response.json(); // Attend la réponse JSON
572
-
573
- // Gère les erreurs HTTP ou les erreurs retournées par l'API
574
  if (!response.ok) {
575
  throw new Error(data.error || `Erreur serveur: ${response.status}`);
576
  }
577
- // Si la réponse est OK et contient un message
578
  if (data.success && data.message) {
579
- addMessageToChat('assistant', data.message, true); // Ajoute la réponse de l'assistant (HTML)
580
  } else {
581
- // Si la réponse est OK mais le format est incorrect
582
  throw new Error(data.error || "Réponse invalide ou vide du serveur.");
583
  }
584
  } catch (error) {
585
- // Affiche l'erreur à l'utilisateur
586
  displayError(error.message);
587
- // Optionnel : Gérer l'annulation du cooldown en cas d'erreur API ?
588
  } finally {
589
- // Cache l'indicateur de chargement dans tous les cas (succès ou erreur)
590
  showLoading(false);
591
- promptInput.focus(); // Remet le focus sur l'input principal
592
  }
593
  });
594
 
595
- // Soumission du formulaire pour effacer le chat
596
  clearForm.addEventListener('submit', async (e) => {
597
- e.preventDefault();
598
- if (confirm("Êtes-vous sûr de vouloir effacer toute la conversation ?")) {
599
- const btn = e.target.querySelector('button');
600
- const originalText = btn.textContent;
601
- btn.textContent = '...'; // Indique le traitement
602
- btn.disabled = true;
603
- try {
604
- // Envoie une requête POST pour effacer côté serveur
605
- const response = await fetch(CLEAR_ENDPOINT, {
606
- method: 'POST',
607
- headers: { 'X-Requested-With': 'XMLHttpRequest' } // Pour identifier comme requête AJAX côté serveur si nécessaire
608
- });
609
- const data = await response.json();
610
- if (response.ok && data.success) {
611
- // Si succès, vide le chat localement et affiche un message
612
- chatMessages.innerHTML = '';
613
- chatMessages.appendChild(loadingIndicator); // Ré-ajoute le template
614
- loadingIndicator.style.display = 'none';
615
- addMessageToChat('assistant', "Conversation effacée. Comment puis-je vous aider ?");
616
- errorMessageDiv.style.display = 'none'; // Cache les erreurs précédentes
617
- } else {
618
- // Si erreur côté serveur
619
- throw new Error(data.error || "Impossible d'effacer côté serveur.");
620
- }
621
- } catch (error) {
622
- // Affiche l'erreur si l'effacement échoue
623
- displayError(`Erreur lors de l'effacement du chat: ${error.message}`);
624
- } finally {
625
- // Réactive le bouton dans tous les cas
626
- btn.textContent = originalText;
627
- btn.disabled = false;
628
- promptInput.focus();
629
  }
630
- }
 
 
 
 
 
 
 
631
  });
632
 
633
- // --- Chargement Initial ---
634
- loadChatHistory(); // Charge l'historique au démarrage de la page
635
-
636
  });
637
  </script>
638
  </body>
 
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
  }
 
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;
 
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;
 
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
  }
 
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 -->
 
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é (Gemini 1.5 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>
 
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');
221
  const loadingIndicator = document.getElementById('loading-indicator');
 
222
  const historyLoadingIndicator = document.getElementById('history-loading');
223
  const errorMessageDiv = document.getElementById('error-message');
224
  const errorTextP = document.getElementById('error-text');
 
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 {
356
  const response = await fetch(API_HISTORY_ENDPOINT);
357
  if (!response.ok) {
358
+ let errorMsg = `Erreur serveur (${response.status})`;
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
 
 
 
 
403
  fileUpload.addEventListener('change', () => {
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();
432
  const file = fileUpload.files[0];
 
433
  const useWebSearch = webSearchToggle.checked;
434
  const useAdvanced = advancedToggle.checked;
435
 
436
+ if (!prompt && !file) {
437
+ displayError("Veuillez entrer un message ou sélectionner un fichier.");
 
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 {
 
490
  throw new Error(data.error || "Réponse invalide ou vide du serveur.");
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',
510
+ headers: {
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>