Docfile commited on
Commit
e2542f2
·
verified ·
1 Parent(s): 984f26b

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +335 -234
templates/index.html CHANGED
@@ -12,27 +12,30 @@
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,22 +50,23 @@
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,39 +74,43 @@
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,10 +121,15 @@
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,26 +148,23 @@
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,50 +173,58 @@
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,10 +233,12 @@
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,307 +251,388 @@
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>
 
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
  ::-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
  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
  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
 
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
  <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
  <!-- 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
  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>