Docfile commited on
Commit
22ae27d
·
verified ·
1 Parent(s): b81fd4a

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +418 -328
templates/index.html CHANGED
@@ -21,58 +21,15 @@
21
  extend: {
22
  fontFamily: {
23
  sans: ['Inter', 'system-ui', 'sans-serif'],
24
- mono: ['"JetBrains Mono"', 'monospace'],
25
  },
26
  colors: {
27
- primary: {
28
- 50: '#f0f9ff',
29
- 100: '#e0f2fe',
30
- 200: '#bae6fd',
31
- 300: '#7dd3fc',
32
- 400: '#38bdf8',
33
- 500: '#0ea5e9',
34
- 600: '#0284c7',
35
- 700: '#0369a1',
36
- 800: '#075985',
37
- 900: '#0c4a6e',
38
- },
39
- secondary: {
40
- 50: '#f8fafc',
41
- 100: '#f1f5f9',
42
- 200: '#e2e8f0',
43
- 300: '#cbd5e1',
44
- 400: '#94a3b8',
45
- 500: '#64748b',
46
- 600: '#475569',
47
- 700: '#334155',
48
- 800: '#1e293b',
49
- 900: '#0f172a',
50
- },
51
- accent: {
52
- 50: '#fdf4ff',
53
- 100: '#fae8ff',
54
- 200: '#f5d0fe',
55
- 300: '#f0abfc',
56
- 400: '#e879f9',
57
- 500: '#d946ef',
58
- 600: '#c026d3',
59
- 700: '#a21caf',
60
- 800: '#86198f',
61
- 900: '#701a75',
62
- }
63
- },
64
- animation: {
65
- 'bounce-slow': 'bounce 2s infinite',
66
- 'pulse-slow': 'pulse 3s infinite',
67
- 'typing': 'typing 1.2s steps(3) infinite',
68
  },
69
- keyframes: {
70
- typing: {
71
- '0%': { width: '0.15em' },
72
- '50%': { width: '0.7em' },
73
- '100%': { width: '0.15em' },
74
- }
75
- }
76
  }
77
  }
78
  }
@@ -80,25 +37,38 @@
80
  <style>
81
  html {
82
  scroll-behavior: smooth;
 
83
  }
84
  body {
85
  font-family: 'Inter', sans-serif;
86
  transition: background-color 0.3s ease, color 0.3s ease;
 
87
  }
88
  .chat-layout {
89
  min-height: calc(100vh - 64px);
90
  display: grid;
91
  grid-template-rows: 1fr auto;
92
  }
93
- ::-webkit-scrollbar { width: 5px; height: 5px; }
94
- ::-webkit-scrollbar-track { background: transparent; }
 
 
 
 
 
95
  ::-webkit-scrollbar-thumb {
96
  background: #cbd5e1;
97
  border-radius: 5px;
98
  }
99
- .dark ::-webkit-scrollbar-thumb { background: #475569; }
100
- ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
101
- .dark ::-webkit-scrollbar-thumb:hover { background: #64748b; }
 
 
 
 
 
 
102
  .message-bubble {
103
  position: relative;
104
  max-width: 85%;
@@ -133,27 +103,6 @@
133
  color: #e2e8f0;
134
  border-color: #334155;
135
  }
136
- .user-message::after {
137
- content: '';
138
- position: absolute;
139
- bottom: -0.5rem;
140
- right: 0.125rem;
141
- width: 0.75rem;
142
- height: 0.75rem;
143
- background: #2563eb;
144
- clip-path: polygon(0 0, 100% 0, 100% 100%);
145
- }
146
- .assistant-message::after {
147
- content: '';
148
- position: absolute;
149
- bottom: -0.5rem;
150
- left: 0.125rem;
151
- width: 0.75rem;
152
- height: 0.75rem;
153
- background: #f8fafc;
154
- clip-path: polygon(0 0, 100% 0, 0 100%);
155
- }
156
- .dark .assistant-message::after { background: #1e293b; }
157
  @keyframes message-fade-in {
158
  from { opacity: 0; transform: translateY(10px); }
159
  to { opacity: 1; transform: translateY(0); }
@@ -182,7 +131,9 @@
182
  background-color: #0f172a;
183
  color: #e2e8f0;
184
  }
185
- .tooltip { position: relative; }
 
 
186
  .tooltip .tooltip-text {
187
  visibility: hidden;
188
  width: max-content;
@@ -203,7 +154,10 @@
203
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
204
  pointer-events: none;
205
  }
206
- .dark .tooltip .tooltip-text { background-color: #475569; color: #f1f5f9; }
 
 
 
207
  .tooltip .tooltip-text::after {
208
  content: "";
209
  position: absolute;
@@ -214,8 +168,13 @@
214
  border-style: solid;
215
  border-color: #1e293b transparent transparent transparent;
216
  }
217
- .dark .tooltip .tooltip-text::after { border-color: #475569 transparent transparent transparent; }
218
- .tooltip:hover .tooltip-text { visibility: visible; opacity: 1; }
 
 
 
 
 
219
  .copy-btn {
220
  position: absolute;
221
  top: 0.5rem;
@@ -224,7 +183,9 @@
224
  transition: opacity 0.2s ease, background-color 0.2s ease;
225
  z-index: 2;
226
  }
227
- .message-bubble:hover .copy-btn { opacity: 1; }
 
 
228
  .file-preview {
229
  max-width: 300px;
230
  margin: 0.5rem auto;
@@ -233,7 +194,9 @@
233
  border-radius: 0.5rem;
234
  transition: transform 0.2s ease;
235
  }
236
- .file-preview:hover { transform: scale(1.02); }
 
 
237
  .file-preview img {
238
  width: 100%;
239
  height: auto;
@@ -259,14 +222,21 @@
259
  transition: opacity 0.2s ease;
260
  }
261
  .chip .chip-close:hover { opacity: 1; }
262
- .dark .chip { background: #0c4a6e; color: #7dd3fc; }
 
 
 
263
  .toggle-switch {
264
  position: relative;
265
  display: inline-block;
266
  width: 2.5rem;
267
  height: 1.25rem;
268
  }
269
- .toggle-switch input { opacity: 0; width: 0; height: 0; }
 
 
 
 
270
  .toggle-slider {
271
  position: absolute;
272
  cursor: pointer;
@@ -289,11 +259,21 @@
289
  transition: .4s;
290
  border-radius: 50%;
291
  }
292
- input:checked + .toggle-slider { background-color: #0ea5e9; }
293
- input:focus + .toggle-slider { box-shadow: 0 0 1px #0ea5e9; }
294
- input:checked + .toggle-slider:before { transform: translateX(1.125rem); }
295
- .dark .toggle-slider { background-color: #475569; }
296
- .dark input:checked + .toggle-slider { background-color: #38bdf8; }
 
 
 
 
 
 
 
 
 
 
297
  .chat-input {
298
  transition: all 0.3s ease;
299
  border-color: #e2e8f0;
@@ -319,7 +299,10 @@
319
  padding: 1.25rem 1rem;
320
  overflow-x: auto;
321
  }
322
- .dark pre { background-color: #1e293b; color: #e2e8f0; }
 
 
 
323
  code {
324
  font-family: 'JetBrains Mono', monospace;
325
  font-size: 0.875rem;
@@ -331,91 +314,122 @@
331
  opacity: 0;
332
  transition: opacity 0.2s ease;
333
  }
334
- pre:hover .code-copy-btn { opacity: 0.7; }
335
- pre:hover .code-copy-btn:hover { opacity: 1; }
336
-
337
  /* Styles pour améliorer les tableaux Markdown */
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  .prose table {
339
  width: 100%;
 
340
  border-collapse: collapse;
341
- margin: 1rem 0;
342
- overflow-x: auto;
343
- display: block;
344
  }
345
-
346
  .prose table th,
347
  .prose table td {
348
  border: 1px solid #e2e8f0;
349
  padding: 0.5rem 0.75rem;
350
  text-align: left;
 
 
 
 
 
 
351
  }
352
-
353
  .dark .prose table th,
354
  .dark .prose table td {
355
  border-color: #334155;
356
  }
357
-
358
  .prose table thead {
359
  background-color: #f8fafc;
360
  font-weight: 600;
361
  }
362
-
363
  .dark .prose table thead {
364
  background-color: #1e293b;
365
  }
366
-
367
  .prose table tbody tr:nth-child(even) {
368
  background-color: #f8fafc;
369
  }
370
-
371
  .dark .prose table tbody tr:nth-child(even) {
372
  background-color: #1e293b;
373
  }
374
-
375
  /* Pour le textarea qui s'adapte au contenu */
376
  .chat-textarea {
377
  resize: none;
378
- min-height: 40px;
379
  max-height: 200px;
380
  overflow-y: auto;
381
  line-height: 1.5;
382
  width: 100%;
383
  border-radius: 9999px;
384
- padding-top: 0.75rem;
385
- padding-bottom: 0.75rem;
 
 
 
386
  }
387
-
388
  /* Ajustements pour le responsive */
389
  @media (max-width: 640px) {
390
  .message-bubble {
391
  max-width: 90%;
392
  padding: 0.75rem 0.875rem;
393
  }
394
-
395
  .chat-textarea {
396
  max-height: 120px;
397
  font-size: 0.95rem;
398
- padding-top: 0.625rem;
399
- padding-bottom: 0.625rem;
 
400
  }
401
-
402
  .prose table {
 
403
  font-size: 0.85rem;
404
  }
405
-
406
  .prose table th,
407
  .prose table td {
408
  padding: 0.375rem 0.5rem;
409
  }
410
-
411
  .send-button-wrapper {
412
- top: 0;
413
- right: 0;
414
- height: 100%;
415
- display: flex;
 
 
 
 
 
 
416
  align-items: center;
417
- padding-right: 0.5rem;
418
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  }
420
  </style>
421
  </head>
@@ -461,7 +475,7 @@
461
  </div>
462
  <!-- Indicateur de chargement pour les réponses -->
463
  <div id="loading-indicator" class="flex items-start space-x-2 hidden">
464
- <div class="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center dark:bg-primary-900/50">
465
  <span class="text-lg">✨</span>
466
  </div>
467
  <div class="message-bubble assistant-message bg-secondary-50 text-secondary-900 border border-secondary-200 flex items-center">
@@ -479,15 +493,9 @@
479
  <!-- Zone d'erreur -->
480
  <div id="error-message" class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 dark:bg-red-900/30 dark:text-red-300 dark:border-red-600 hidden" role="alert">
481
  <div class="flex">
482
- <div class="flex-shrink-0">
483
- <i class="fa-solid fa-circle-exclamation"></i>
484
- </div>
485
- <div class="ml-3">
486
- <p class="text-sm font-medium" id="error-text">Le message d'erreur détaillé ira ici.</p>
487
- </div>
488
- <button class="ml-auto" id="dismiss-error">
489
- <i class="fa-solid fa-xmark"></i>
490
- </button>
491
  </div>
492
  </div>
493
  <!-- Zone de prévisualisation -->
@@ -495,68 +503,73 @@
495
  <div id="file-preview" class="hidden"></div>
496
  </div>
497
  <!-- Barre d'options -->
498
- <div class="flex items-center justify-between px-4 py-2 bg-gray-100 dark:bg-gray-800/80 text-sm text-gray-600 dark:text-gray-300">
499
- <div class="flex items-center space-x-4">
500
- <label class="flex items-center cursor-pointer tooltip">
501
- <span class="mr-2 text-xs sm:text-sm font-medium">
502
- <i class="fa-solid fa-globe mr-1.5"></i>
503
- <span class="hidden sm:inline">Recherche Web</span>
504
- </span>
505
- <span class="toggle-switch">
506
- <input type="checkbox" id="web_search_toggle" name="web_search" value="true">
507
- <span class="toggle-slider"></span>
508
- </span>
509
- <span class="tooltip-text">Activer la recherche web pour Mariam</span>
510
- </label>
511
- <label class="flex items-center cursor-pointer tooltip">
512
- <span class="mr-2 text-xs sm:text-sm font-medium text-accent-700 dark:text-accent-300">
513
- <i class="fa-solid fa-brain mr-1.5"></i>
514
- <span class="hidden sm:inline">Avancé</span>
515
- <span id="advanced-cooldown-timer" class="text-xs ml-1 hidden"></span>
516
- </span>
517
- <span class="toggle-switch">
518
- <input type="checkbox" id="advanced_reasoning_toggle" name="advanced_reasoning" value="true">
519
- <span class="toggle-slider"></span>
520
- </span>
521
- <span class="tooltip-text">Activer le raisonnement avancé (1 fois/min)</span>
522
- </label>
523
- </div>
524
- <div>
525
- <label for="file_upload" class="cursor-pointer flex items-center text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 tooltip">
526
- <i class="fa-solid fa-paperclip"></i>
527
- <span class="ml-1.5 hidden sm:inline">Fichier</span>
528
- <input type="file" id="file_upload" name="file" class="hidden" accept=".txt,.pdf,.png,.jpg,.jpeg">
529
- <span class="tooltip-text">Joindre un fichier (txt, pdf, image)</span>
530
- </label>
531
- <div id="file-chip" class="chip mt-2 hidden">
532
- <i class="fa-solid fa-file chip-icon"></i>
533
- <span id="file-name" class="truncate max-w-[120px]"></span>
534
- <i id="clear-file" class="fa-solid fa-xmark chip-close"></i>
535
- </div>
536
- </div>
537
  </div>
538
- <!-- Formulaire de chat avec textarea au lieu d'input -->
539
  <form id="chat-form" class="p-3 sm:p-4 bg-white dark:bg-gray-900">
540
- <div class="relative">
541
- <textarea
542
- id="prompt"
543
- name="prompt"
544
- class="chat-input chat-textarea w-full pl-4 pr-12 py-3 rounded-full border focus:outline-none text-sm sm:text-base"
545
- placeholder="Posez votre question à Mariam..."
546
  autocomplete="off"
547
  rows="1"></textarea>
548
- <div class="send-button-wrapper absolute right-2 top-1/2 transform -translate-y-1/2">
549
- <button
550
- type="submit"
551
- id="send-button"
552
- class="bg-primary-500 hover:bg-primary-600 disabled:bg-primary-300 text-white rounded-full p-2 transition focus:outline-none focus:ring-2 focus:ring-primary-400"
553
  title="Envoyer le message">
554
- <i class="fa-solid fa-paper-plane"></i>
555
  </button>
556
  </div>
557
  </div>
558
  <div class="text-xs text-center mt-2 text-gray-400 dark:text-gray-500">
559
- Appuyez sur <kbd class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 rounded border border-gray-300 dark:border-gray-700">Entrée</kbd> pour envoyer • <kbd class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 rounded border border-gray-300 dark:border-gray-700">Shift+Entrée</kbd> pour une nouvelle ligne
 
 
 
 
 
 
560
  </div>
561
  </form>
562
  </div>
@@ -564,7 +577,7 @@
564
  <!-- Scripts -->
565
  <script>
566
  document.addEventListener('DOMContentLoaded', () => {
567
- // Sélection des éléments et initialisation des variables
568
  const chatForm = document.getElementById('chat-form');
569
  const promptInput = document.getElementById('prompt');
570
  const chatMessages = document.getElementById('chat-messages');
@@ -589,9 +602,10 @@
589
  const API_HISTORY_ENDPOINT = '/api/history';
590
  const CLEAR_ENDPOINT = '/clear';
591
  const COOLDOWN_DURATION = 60 * 1000;
 
592
  let advancedToggleCooldownEndTime = 0;
593
  let isComposing = false;
594
-
595
  // Gestion du thème
596
  function initializeTheme() {
597
  if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
@@ -611,32 +625,24 @@
611
  }
612
  themeToggleBtn.addEventListener('click', toggleTheme);
613
  initializeTheme();
614
-
615
- // Fonction pour ajuster la hauteur du textarea
616
  function adjustTextareaHeight(textarea) {
617
  textarea.style.height = 'auto';
618
- const newHeight = Math.min(200, textarea.scrollHeight);
 
 
619
  textarea.style.height = newHeight + 'px';
620
-
621
- // Ajuste la position du bouton d'envoi
622
- const sendButtonWrapper = document.querySelector('.send-button-wrapper');
623
- if (sendButtonWrapper) {
624
- if (window.innerWidth <= 640) {
625
- sendButtonWrapper.style.top = '';
626
- } else {
627
- sendButtonWrapper.style.top = (newHeight / 2) + 'px';
628
- }
629
- }
630
  }
631
-
632
  // Défilement vers le bas
633
  function scrollToBottom(smooth = true) {
634
  setTimeout(() => {
635
  chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: smooth ? 'smooth' : 'auto' });
636
  }, 50);
637
  }
638
-
639
- // Affichage du chargement
640
  function showLoading(show) {
641
  if (show) {
642
  loadingIndicator.classList.remove('hidden');
@@ -648,40 +654,51 @@
648
  sendButton.disabled = show;
649
  promptInput.disabled = show;
650
  fileUpload.disabled = show;
651
- clearFileButton.disabled = show;
 
 
 
 
 
 
652
  }
653
-
654
  // Affichage des erreurs
655
  function displayError(message) {
656
  errorTextP.textContent = message || "Une erreur inconnue est survenue.";
657
  errorMessageDiv.classList.remove('hidden');
658
- errorMessageDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
659
  }
660
  dismissErrorBtn.addEventListener('click', () => { errorMessageDiv.classList.add('hidden'); });
661
-
662
  // Ajout d'un message dans le chat
663
  function addMessageToChat(role, content, isHtml = false) {
664
  errorMessageDiv.classList.add('hidden');
665
  const messageWrapper = document.createElement('div');
666
- messageWrapper.classList.add('flex', role === 'user' ? 'justify-end' : 'justify-start');
667
- let html = '';
 
 
 
 
 
 
 
 
668
  if (role === 'user') {
669
- html = `
670
- <div class="flex items-start space-x-2 max-w-[85%]">
671
- <div class="message-bubble user-message">
672
- <p class="text-sm sm:text-base whitespace-pre-wrap break-words">${escapeHtml(content)}</p>
673
- </div>
674
  </div>
675
  `;
676
  } else {
677
- html = `
678
  <div class="flex items-start space-x-2 max-w-[85%]">
679
  <div class="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0 dark:bg-primary-900/50">
680
  <span class="text-lg">✨</span>
681
  </div>
682
  <div class="message-bubble assistant-message bg-secondary-50 text-secondary-900 border border-secondary-200 relative">
683
  <div class="prose prose-sm sm:prose-base max-w-none dark:prose-invert">
684
- ${isHtml ? content : escapeHtml(content)}
685
  </div>
686
  <button class="copy-btn text-xs bg-white/90 dark:bg-gray-800/90 hover:bg-gray-100 dark:hover:bg-gray-700 py-1 px-2 rounded text-gray-600 dark:text-gray-300 flex items-center">
687
  <i class="fa-regular fa-copy mr-1"></i> Copier
@@ -690,39 +707,70 @@
690
  </div>
691
  `;
692
  }
693
- messageWrapper.innerHTML = html;
694
-
695
- // Activer les boutons de copie sur les nouveaux messages
696
  const copyBtns = messageWrapper.querySelectorAll('.copy-btn');
697
  copyBtns.forEach(btn => {
698
  btn.addEventListener('click', function() {
699
- const textToCopy = this.closest('.message-bubble').querySelector('.prose').innerText;
700
  navigator.clipboard.writeText(textToCopy).then(() => {
701
  const originalText = this.innerHTML;
702
  this.innerHTML = '<i class="fa-solid fa-check mr-1"></i> Copié';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
703
  setTimeout(() => {
704
- this.innerHTML = originalText;
 
 
705
  }, 2000);
 
 
 
 
706
  });
707
  });
 
708
  });
709
-
710
  chatMessages.insertBefore(messageWrapper, loadingIndicator);
711
  scrollToBottom();
712
  }
713
  function escapeHtml(unsafe) {
714
- return unsafe.replace(/&/g, "&amp;")
715
- .replace(/</g, "&lt;")
716
- .replace(/>/g, "&gt;")
717
- .replace(/"/g, "&quot;")
718
- .replace(/'/g, "&#039;");
 
719
  }
720
-
721
- // Gestion du cooldown du mode avancé
722
  function startAdvancedCooldownTimer() {
723
  advancedToggle.disabled = true;
724
  advancedToggleCooldownEndTime = Date.now() + COOLDOWN_DURATION;
725
- const updateTimer = () => {
726
  const now = Date.now();
727
  if (now >= advancedToggleCooldownEndTime) {
728
  clearInterval(intervalId);
@@ -734,26 +782,30 @@
734
  advancedCooldownTimerSpan.textContent = `(${remainingSeconds}s)`;
735
  advancedCooldownTimerSpan.classList.remove('hidden');
736
  }
737
- };
738
- const intervalId = setInterval(updateTimer, 1000);
739
- updateTimer();
 
740
  }
741
-
742
  // Chargement de l'historique du chat
743
  async function loadChatHistory() {
744
- historyLoadingIndicator.style.display = 'block';
745
  try {
746
  const response = await fetch(API_HISTORY_ENDPOINT);
747
  if (!response.ok) {
748
  let errorMsg = `Erreur serveur (${response.status})`;
749
- try { const errData = await response.json(); errorMsg = errData.error || errorMsg; } catch (e) {}
 
 
 
750
  throw new Error(errorMsg);
751
  }
752
  const data = await response.json();
 
 
 
753
  if (data.success && Array.isArray(data.history)) {
754
- chatMessages.innerHTML = '';
755
- chatMessages.appendChild(loadingIndicator);
756
- loadingIndicator.classList.add('hidden');
757
  if (data.history.length === 0) {
758
  addMessageToChat('assistant', "Bonjour ! Je suis Mariam, votre assistant IA. Comment puis-je vous aider aujourd'hui ?", true);
759
  } else {
@@ -764,26 +816,29 @@
764
  }
765
  scrollToBottom(false);
766
  } else {
767
- throw new Error(data.error || "Format de réponse invalide.");
768
  }
769
  } catch (error) {
770
- chatMessages.innerHTML = '';
771
- chatMessages.appendChild(loadingIndicator);
772
- loadingIndicator.classList.add('hidden');
773
  displayError(`Impossible de charger l'historique: ${error.message}`);
 
 
 
774
  } finally {
775
  historyLoadingIndicator.remove();
776
  promptInput.focus();
 
777
  }
778
  }
779
-
780
  // Gestion des fichiers
781
  function clearFileInput() {
782
  fileUpload.value = '';
783
  fileChip.classList.add('hidden');
 
 
 
784
  filePreview.classList.add('hidden');
785
  previewArea.classList.add('hidden');
786
- filePreview.innerHTML = '';
787
  }
788
  fileUpload.addEventListener('change', () => {
789
  if (fileUpload.files.length > 0) {
@@ -792,27 +847,31 @@
792
  fileNameSpan.textContent = name;
793
  fileNameSpan.title = name;
794
  fileChip.classList.remove('hidden');
 
795
  if (file.type.startsWith('image/')) {
796
  const reader = new FileReader();
797
  reader.onload = (e) => {
798
  filePreview.innerHTML = `
799
  <div class="file-preview">
800
- <img src="${e.target.result}" alt="Prévisualisation de l'image">
801
- </div>
802
- `;
 
 
 
 
803
  filePreview.classList.remove('hidden');
804
  previewArea.classList.remove('hidden');
805
  };
806
  reader.readAsDataURL(file);
807
  } else {
808
  filePreview.innerHTML = `
809
- <div class="flex items-center justify-center py-4">
810
- <div class="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg text-center">
811
- <i class="fa-solid ${getFileIcon(file.type)} text-3xl text-gray-600 dark:text-gray-300 mb-2"></i>
812
- <p class="text-sm text-gray-500 dark:text-gray-400">${formatFileSize(file.size)}</p>
813
  </div>
814
- </div>
815
- `;
816
  filePreview.classList.remove('hidden');
817
  previewArea.classList.remove('hidden');
818
  }
@@ -821,36 +880,53 @@
821
  }
822
  });
823
  function getFileIcon(fileType) {
 
824
  if (fileType.includes('pdf')) return 'fa-file-pdf';
825
  if (fileType.includes('text')) return 'fa-file-lines';
826
  return 'fa-file';
827
  }
828
  function formatFileSize(bytes) {
829
- if (bytes < 1024) return bytes + ' octets';
830
- if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' Ko';
831
- return (bytes / 1048576).toFixed(1) + ' Mo';
 
 
832
  }
833
  clearFileButton.addEventListener('click', clearFileInput);
834
-
835
  // Événements pour le textarea
836
- promptInput.addEventListener('input', function() {
837
- adjustTextareaHeight(this);
838
  });
839
-
840
  promptInput.addEventListener('keydown', (e) => {
841
  if (isComposing) return;
842
- if (e.key === 'Enter' && !e.shiftKey) {
843
- e.preventDefault();
844
- if (!sendButton.disabled) chatForm.dispatchEvent(new Event('submit'));
845
- } else if (e.key === 'Enter' && e.shiftKey) {
846
- // Laisser le comportement par défaut (nouvelle ligne)
847
- setTimeout(() => adjustTextareaHeight(promptInput), 0);
 
 
 
 
 
 
 
 
 
 
 
 
848
  }
849
  });
850
-
851
  promptInput.addEventListener('compositionstart', () => { isComposing = true; });
852
- promptInput.addEventListener('compositionend', () => { isComposing = false; });
853
-
 
 
 
 
854
  // Soumission du formulaire
855
  chatForm.addEventListener('submit', async (e) => {
856
  e.preventDefault();
@@ -858,15 +934,15 @@
858
  const file = fileUpload.files[0];
859
  const useWebSearch = webSearchToggle.checked;
860
  const useAdvanced = advancedToggle.checked;
 
861
  if (!prompt && !file) {
862
- displayError("Veuillez entrer un message ou sélectionner un fichier.");
863
  promptInput.focus();
864
  return;
865
  }
866
  errorMessageDiv.classList.add('hidden');
867
  if (useAdvanced) {
868
  const now = Date.now();
869
- if (now < advancedToggleCooldownEndTime) {
870
  const remainingSeconds = Math.ceil((advancedToggleCooldownEndTime - now) / 1000);
871
  displayError(`Le raisonnement avancé est disponible dans ${remainingSeconds} seconde(s).`);
872
  return;
@@ -874,129 +950,143 @@
874
  }
875
  let userMessageText = prompt;
876
  if (file && file.name) {
877
- userMessageText = prompt ? `${prompt}` : `[Fichier joint: ${file.name}]`;
878
  }
879
  addMessageToChat('user', userMessageText);
880
  const formData = new FormData();
881
  formData.append('prompt', prompt);
882
  formData.append('web_search', useWebSearch);
883
- if (file) { formData.append('file', file); }
884
  formData.append('advanced_reasoning', useAdvanced);
885
- showLoading(true);
 
 
886
  promptInput.value = '';
887
- promptInput.style.height = 'auto'; // Réinitialiser la hauteur du textarea
888
  clearFileInput();
889
- advancedToggle.checked = false;
890
- if (useAdvanced) startAdvancedCooldownTimer();
 
 
 
 
891
  try {
892
  const response = await fetch(API_CHAT_ENDPOINT, { method: 'POST', body: formData });
893
  const data = await response.json();
894
- if (!response.ok) throw new Error(data.error || `Erreur serveur: ${response.status}`);
 
 
895
  if (data.success && data.message) {
896
  addMessageToChat('assistant', data.message, true);
897
  } else {
898
- throw new Error(data.error || "Réponse invalide du serveur.");
899
  }
900
  } catch (error) {
901
- displayError(error.message);
 
902
  } finally {
903
  showLoading(false);
904
  promptInput.focus();
905
  }
906
  });
907
-
908
  // Effacement de la conversation
909
  clearForm.addEventListener('submit', async (e) => {
910
  e.preventDefault();
911
  const confirmDialog = document.createElement('div');
912
- confirmDialog.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
913
  confirmDialog.innerHTML = `
914
- <div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-xl max-w-sm mx-4 animate-[message-fade-in_0.2s_ease-out]">
915
  <h3 class="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100">Confirmer l'effacement</h3>
916
- <p class="text-gray-600 dark:text-gray-300 mb-4">Êtes-vous sûr de vouloir effacer toute la conversation ?</p>
917
  <div class="flex justify-end space-x-3">
918
- <button id="cancel-clear" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
919
  Annuler
920
  </button>
921
- <button id="confirm-clear" class="px-4 py-2 text-sm font-medium text-white bg-red-500 rounded hover:bg-red-600 transition-colors">
922
  Effacer
923
  </button>
924
  </div>
925
  </div>
926
  `;
927
- document.body.appendChild(confirmDialog);
928
- document.body.classList.add('overflow-hidden');
929
- document.getElementById('cancel-clear').addEventListener('click', () => {
930
- document.body.removeChild(confirmDialog);
931
- document.body.classList.remove('overflow-hidden');
932
- });
933
- document.getElementById('confirm-clear').addEventListener('click', async () => {
934
- document.body.removeChild(confirmDialog);
935
- document.body.classList.remove('overflow-hidden');
936
- const originalButtonText = e.target.querySelector('button').innerHTML;
937
- e.target.querySelector('button').innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
938
- e.target.querySelector('button').disabled = true;
 
939
  try {
940
- const response = await fetch(CLEAR_ENDPOINT, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } });
941
  const data = await response.json();
942
  if (response.ok && data.success) {
943
- chatMessages.innerHTML = '';
944
- chatMessages.appendChild(loadingIndicator);
945
- loadingIndicator.classList.add('hidden');
946
- addMessageToChat('assistant', "Conversation effacée. Comment puis-je vous aider ?", true);
947
  errorMessageDiv.classList.add('hidden');
948
  } else {
949
  throw new Error(data.error || "Impossible d'effacer la conversation.");
950
  }
951
  } catch (error) {
 
952
  displayError(`Erreur lors de l'effacement: ${error.message}`);
953
  } finally {
954
- e.target.querySelector('button').innerHTML = originalButtonText;
955
- e.target.querySelector('button').disabled = false;
956
  promptInput.focus();
957
  }
958
  });
 
 
 
959
  });
960
-
961
- // Init
962
  loadChatHistory();
963
- adjustTextareaHeight(promptInput);
964
-
965
- // Gérer les changements de taille de l'écran
966
- window.addEventListener('resize', () => {
967
- adjustTextareaHeight(promptInput);
968
- });
969
-
970
- // Observer pour les tableaux Markdown et ajouter une classe spéciale si nécessaire
971
  const observeMarkdownTables = () => {
972
  const observer = new MutationObserver(mutations => {
973
  mutations.forEach(mutation => {
974
- if (mutation.type === 'childList') {
975
- const tables = document.querySelectorAll('.prose table:not(.markdown-enhanced)');
976
- tables.forEach(table => {
977
- table.classList.add('markdown-enhanced');
978
- const wrapper = document.createElement('div');
979
- wrapper.className = 'overflow-x-auto';
980
- table.parentNode.insertBefore(wrapper, table);
981
- wrapper.appendChild(table);
 
 
 
 
 
982
  });
983
  }
984
  });
985
  });
986
-
987
  observer.observe(chatMessages, { childList: true, subtree: true });
988
  };
989
-
990
  observeMarkdownTables();
991
-
 
992
  const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
993
- prefersDarkScheme.addEventListener('change', (e) => {
994
- if (!localStorage.theme) {
995
- if (e.matches) { document.documentElement.classList.add('dark'); }
996
- else { document.documentElement.classList.remove('dark'); }
 
 
 
997
  }
998
- });
 
999
  });
1000
  </script>
1001
  </body>
1002
- </html>
 
21
  extend: {
22
  fontFamily: {
23
  sans: ['Inter', 'system-ui', 'sans-serif'],
24
+ mono: ['"JetBrains Mono"', 'monospace']
25
  },
26
  colors: {
27
+ primary: { 50: '#f0f9ff', 100: '#e0f2fe', 200: '#bae6fd', 300: '#7dd3fc', 400: '#38bdf8', 500: '#0ea5e9', 600: '#0284c7', 700: '#0369a1', 800: '#075985', 900: '#0c4a6e' },
28
+ secondary: { 50: '#f8fafc', 100: '#f1f5f9', 200: '#e2e8f0', 300: '#cbd5e1', 400: '#94a3b8', 500: '#64748b', 600: '#475569', 700: '#334155', 800: '#1e293b', 900: '#0f172a' },
29
+ accent: { 50: '#fdf4ff', 100: '#fae8ff', 200: '#f5d0fe', 300: '#f0abfc', 400: '#e879f9', 500: '#d946ef', 600: '#c026d3', 700: '#a21caf', 800: '#86198f', 900: '#701a75' }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  },
31
+ animation: { 'bounce-slow': 'bounce 2s infinite', 'pulse-slow': 'pulse 3s infinite', 'typing': 'typing 1.2s steps(3) infinite' },
32
+ keyframes: { typing: { '0%': { width: '0.15em' }, '50%': { width: '0.7em' }, '100%': { width: '0.15em' } } }
 
 
 
 
 
33
  }
34
  }
35
  }
 
37
  <style>
38
  html {
39
  scroll-behavior: smooth;
40
+ overflow-x: hidden; /* Empêche le scroll horizontal global */
41
  }
42
  body {
43
  font-family: 'Inter', sans-serif;
44
  transition: background-color 0.3s ease, color 0.3s ease;
45
+ overflow-x: hidden; /* Empêche le scroll horizontal du body */
46
  }
47
  .chat-layout {
48
  min-height: calc(100vh - 64px);
49
  display: grid;
50
  grid-template-rows: 1fr auto;
51
  }
52
+ ::-webkit-scrollbar {
53
+ width: 5px;
54
+ height: 5px;
55
+ }
56
+ ::-webkit-scrollbar-track {
57
+ background: transparent;
58
+ }
59
  ::-webkit-scrollbar-thumb {
60
  background: #cbd5e1;
61
  border-radius: 5px;
62
  }
63
+ .dark ::-webkit-scrollbar-thumb {
64
+ background: #475569;
65
+ }
66
+ ::-webkit-scrollbar-thumb:hover {
67
+ background: #94a3b8;
68
+ }
69
+ .dark ::-webkit-scrollbar-thumb:hover {
70
+ background: #64748b;
71
+ }
72
  .message-bubble {
73
  position: relative;
74
  max-width: 85%;
 
103
  color: #e2e8f0;
104
  border-color: #334155;
105
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  @keyframes message-fade-in {
107
  from { opacity: 0; transform: translateY(10px); }
108
  to { opacity: 1; transform: translateY(0); }
 
131
  background-color: #0f172a;
132
  color: #e2e8f0;
133
  }
134
+ .tooltip {
135
+ position: relative;
136
+ }
137
  .tooltip .tooltip-text {
138
  visibility: hidden;
139
  width: max-content;
 
154
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
155
  pointer-events: none;
156
  }
157
+ .dark .tooltip .tooltip-text {
158
+ background-color: #475569;
159
+ color: #f1f5f9;
160
+ }
161
  .tooltip .tooltip-text::after {
162
  content: "";
163
  position: absolute;
 
168
  border-style: solid;
169
  border-color: #1e293b transparent transparent transparent;
170
  }
171
+ .dark .tooltip .tooltip-text::after {
172
+ border-color: #475569 transparent transparent transparent;
173
+ }
174
+ .tooltip:hover .tooltip-text {
175
+ visibility: visible;
176
+ opacity: 1;
177
+ }
178
  .copy-btn {
179
  position: absolute;
180
  top: 0.5rem;
 
183
  transition: opacity 0.2s ease, background-color 0.2s ease;
184
  z-index: 2;
185
  }
186
+ .message-bubble:hover .copy-btn {
187
+ opacity: 1;
188
+ }
189
  .file-preview {
190
  max-width: 300px;
191
  margin: 0.5rem auto;
 
194
  border-radius: 0.5rem;
195
  transition: transform 0.2s ease;
196
  }
197
+ .file-preview:hover {
198
+ transform: scale(1.02);
199
+ }
200
  .file-preview img {
201
  width: 100%;
202
  height: auto;
 
222
  transition: opacity 0.2s ease;
223
  }
224
  .chip .chip-close:hover { opacity: 1; }
225
+ .dark .chip {
226
+ background: #0c4a6e;
227
+ color: #7dd3fc;
228
+ }
229
  .toggle-switch {
230
  position: relative;
231
  display: inline-block;
232
  width: 2.5rem;
233
  height: 1.25rem;
234
  }
235
+ .toggle-switch input {
236
+ opacity: 0;
237
+ width: 0;
238
+ height: 0;
239
+ }
240
  .toggle-slider {
241
  position: absolute;
242
  cursor: pointer;
 
259
  transition: .4s;
260
  border-radius: 50%;
261
  }
262
+ input:checked + .toggle-slider {
263
+ background-color: #0ea5e9;
264
+ }
265
+ input:focus + .toggle-slider {
266
+ box-shadow: 0 0 1px #0ea5e9;
267
+ }
268
+ input:checked + .toggle-slider:before {
269
+ transform: translateX(1.125rem);
270
+ }
271
+ .dark .toggle-slider {
272
+ background-color: #475569;
273
+ }
274
+ .dark input:checked + .toggle-slider {
275
+ background-color: #38bdf8;
276
+ }
277
  .chat-input {
278
  transition: all 0.3s ease;
279
  border-color: #e2e8f0;
 
299
  padding: 1.25rem 1rem;
300
  overflow-x: auto;
301
  }
302
+ .dark pre {
303
+ background-color: #1e293b;
304
+ color: #e2e8f0;
305
+ }
306
  code {
307
  font-family: 'JetBrains Mono', monospace;
308
  font-size: 0.875rem;
 
314
  opacity: 0;
315
  transition: opacity 0.2s ease;
316
  }
317
+ pre:hover .code-copy-btn {
318
+ opacity: 0.7;
319
+ }
320
  /* Styles pour améliorer les tableaux Markdown */
321
+ .prose {
322
+ overflow-wrap: break-word;
323
+ word-break: break-word;
324
+ }
325
+ .table-wrapper {
326
+ overflow-x: auto;
327
+ margin: 1rem 0;
328
+ border: 1px solid #e2e8f0;
329
+ border-radius: 0.5rem;
330
+ }
331
+ .dark .table-wrapper {
332
+ border-color: #334155;
333
+ }
334
  .prose table {
335
  width: 100%;
336
+ min-width: 500px;
337
  border-collapse: collapse;
338
+ margin: 0;
 
 
339
  }
 
340
  .prose table th,
341
  .prose table td {
342
  border: 1px solid #e2e8f0;
343
  padding: 0.5rem 0.75rem;
344
  text-align: left;
345
+ border-top: none;
346
+ border-left: none;
347
+ border-right: none;
348
+ }
349
+ .prose table th {
350
+ border-bottom-width: 2px;
351
  }
 
352
  .dark .prose table th,
353
  .dark .prose table td {
354
  border-color: #334155;
355
  }
 
356
  .prose table thead {
357
  background-color: #f8fafc;
358
  font-weight: 600;
359
  }
 
360
  .dark .prose table thead {
361
  background-color: #1e293b;
362
  }
 
363
  .prose table tbody tr:nth-child(even) {
364
  background-color: #f8fafc;
365
  }
 
366
  .dark .prose table tbody tr:nth-child(even) {
367
  background-color: #1e293b;
368
  }
 
369
  /* Pour le textarea qui s'adapte au contenu */
370
  .chat-textarea {
371
  resize: none;
372
+ min-height: 44px;
373
  max-height: 200px;
374
  overflow-y: auto;
375
  line-height: 1.5;
376
  width: 100%;
377
  border-radius: 9999px;
378
+ padding-top: 0.625rem;
379
+ padding-bottom: 0.625rem;
380
+ padding-left: 1rem;
381
+ padding-right: 3rem;
382
+ box-sizing: border-box;
383
  }
 
384
  /* Ajustements pour le responsive */
385
  @media (max-width: 640px) {
386
  .message-bubble {
387
  max-width: 90%;
388
  padding: 0.75rem 0.875rem;
389
  }
 
390
  .chat-textarea {
391
  max-height: 120px;
392
  font-size: 0.95rem;
393
+ min-height: 40px;
394
+ padding-top: 0.5rem;
395
+ padding-bottom: 0.5rem;
396
  }
 
397
  .prose table {
398
+ min-width: 0;
399
  font-size: 0.85rem;
400
  }
 
401
  .prose table th,
402
  .prose table td {
403
  padding: 0.375rem 0.5rem;
404
  }
 
405
  .send-button-wrapper {
406
+ padding-right: 0.25rem;
407
+ }
408
+ .chat-textarea {
409
+ padding-right: 2.75rem;
410
+ }
411
+ #send-button {
412
+ width: 32px;
413
+ height: 32px;
414
+ padding: 0;
415
+ display: inline-flex;
416
  align-items: center;
417
+ justify-content: center;
418
  }
419
+ #send-button i {
420
+ font-size: 0.9rem;
421
+ }
422
+ }
423
+ /* Container pour le textarea et le bouton pour un meilleur contrôle */
424
+ .input-wrapper {
425
+ position: relative;
426
+ display: flex;
427
+ align-items: flex-end;
428
+ }
429
+ .send-button-wrapper {
430
+ position: absolute;
431
+ right: 0.5rem;
432
+ bottom: 0.5rem;
433
  }
434
  </style>
435
  </head>
 
475
  </div>
476
  <!-- Indicateur de chargement pour les réponses -->
477
  <div id="loading-indicator" class="flex items-start space-x-2 hidden">
478
+ <div class="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0 dark:bg-primary-900/50">
479
  <span class="text-lg">✨</span>
480
  </div>
481
  <div class="message-bubble assistant-message bg-secondary-50 text-secondary-900 border border-secondary-200 flex items-center">
 
493
  <!-- Zone d'erreur -->
494
  <div id="error-message" class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 dark:bg-red-900/30 dark:text-red-300 dark:border-red-600 hidden" role="alert">
495
  <div class="flex">
496
+ <div class="flex-shrink-0"><i class="fa-solid fa-circle-exclamation"></i></div>
497
+ <div class="ml-3"><p class="text-sm font-medium" id="error-text">Le message d'erreur détaillé ira ici.</p></div>
498
+ <button class="ml-auto" id="dismiss-error"><i class="fa-solid fa-xmark"></i></button>
 
 
 
 
 
 
499
  </div>
500
  </div>
501
  <!-- Zone de prévisualisation -->
 
503
  <div id="file-preview" class="hidden"></div>
504
  </div>
505
  <!-- Barre d'options -->
506
+ <div class="flex items-center justify-between flex-wrap gap-y-2 px-4 py-2 bg-gray-100 dark:bg-gray-800/80 text-sm text-gray-600 dark:text-gray-300">
507
+ <!-- Ajout du flex-wrap -->
508
+ <div class="flex items-center space-x-4 flex-wrap gap-y-2">
509
+ <label class="flex items-center cursor-pointer tooltip">
510
+ <span class="mr-2 text-xs sm:text-sm font-medium"><i class="fa-solid fa-globe mr-1.5"></i><span class="hidden sm:inline">Recherche Web</span></span>
511
+ <span class="toggle-switch">
512
+ <input type="checkbox" id="web_search_toggle" name="web_search" value="true">
513
+ <span class="toggle-slider"></span>
514
+ </span>
515
+ <span class="tooltip-text">Activer la recherche web</span>
516
+ </label>
517
+ <label class="flex items-center cursor-pointer tooltip">
518
+ <span class="mr-2 text-xs sm:text-sm font-medium text-accent-700 dark:text-accent-300">
519
+ <i class="fa-solid fa-brain mr-1.5"></i>
520
+ <span class="hidden sm:inline">Avancé</span>
521
+ <span id="advanced-cooldown-timer" class="text-xs ml-1 hidden"></span>
522
+ </span>
523
+ <span class="toggle-switch">
524
+ <input type="checkbox" id="advanced_reasoning_toggle" name="advanced_reasoning" value="true">
525
+ <span class="toggle-slider"></span>
526
+ </span>
527
+ <span class="tooltip-text">Raisonnement avancé (1 fois/min)</span>
528
+ </label>
529
+ </div>
530
+ <!-- Conteneur pour le fichier -->
531
+ <div class="flex items-center space-x-2">
532
+ <label for="file_upload" class="cursor-pointer flex items-center text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 tooltip">
533
+ <i class="fa-solid fa-paperclip"></i>
534
+ <span class="ml-1.5 hidden sm:inline">Fichier</span>
535
+ <input type="file" id="file_upload" name="file" class="hidden" accept=".txt,.pdf,.png,.jpg,.jpeg">
536
+ <span class="tooltip-text">Joindre (txt, pdf, image)</span>
537
+ </label>
538
+ <div id="file-chip" class="chip hidden">
539
+ <i class="fa-solid fa-file chip-icon"></i>
540
+ <span id="file-name" class="truncate max-w-[100px] sm:max-w-[120px]"></span>
541
+ <i id="clear-file" class="fa-solid fa-xmark chip-close"></i>
542
+ </div>
543
+ </div>
 
544
  </div>
545
+ <!-- Formulaire de chat -->
546
  <form id="chat-form" class="p-3 sm:p-4 bg-white dark:bg-gray-900">
547
+ <div class="input-wrapper">
548
+ <textarea
549
+ id="prompt"
550
+ name="prompt"
551
+ class="chat-input chat-textarea w-full border focus:outline-none text-sm sm:text-base"
552
+ placeholder="Posez votre question à Mariam..."
553
  autocomplete="off"
554
  rows="1"></textarea>
555
+ <div class="send-button-wrapper">
556
+ <button
557
+ type="submit"
558
+ id="send-button"
559
+ class="bg-primary-500 hover:bg-primary-600 disabled:bg-primary-300 text-white rounded-full p-2 transition focus:outline-none focus:ring-2 focus:ring-primary-400 flex items-center justify-center"
560
  title="Envoyer le message">
561
+ <i class="fa-solid fa-paper-plane text-sm"></i>
562
  </button>
563
  </div>
564
  </div>
565
  <div class="text-xs text-center mt-2 text-gray-400 dark:text-gray-500">
566
+ Appuyez sur <kbd class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 rounded border border-gray-300 dark:border-gray-700">Entrée</kbd>
567
+ <span class="hidden sm:inline"> pour envoyer</span>
568
+ <span class="sm:hidden"> pour une nouvelle ligne</span>
569
+
570
+ <kbd class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 rounded border border-gray-300 dark:border-gray-700">Shift+Entrée</kbd>
571
+ <span class="hidden sm:inline"> pour une nouvelle ligne</span>
572
+ <span class="sm:hidden"> pour envoyer</span>
573
  </div>
574
  </form>
575
  </div>
 
577
  <!-- Scripts -->
578
  <script>
579
  document.addEventListener('DOMContentLoaded', () => {
580
+ // Sélection des éléments
581
  const chatForm = document.getElementById('chat-form');
582
  const promptInput = document.getElementById('prompt');
583
  const chatMessages = document.getElementById('chat-messages');
 
602
  const API_HISTORY_ENDPOINT = '/api/history';
603
  const CLEAR_ENDPOINT = '/clear';
604
  const COOLDOWN_DURATION = 60 * 1000;
605
+ const MOBILE_BREAKPOINT = 640;
606
  let advancedToggleCooldownEndTime = 0;
607
  let isComposing = false;
608
+
609
  // Gestion du thème
610
  function initializeTheme() {
611
  if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
 
625
  }
626
  themeToggleBtn.addEventListener('click', toggleTheme);
627
  initializeTheme();
628
+
629
+ // Ajuster la hauteur du textarea
630
  function adjustTextareaHeight(textarea) {
631
  textarea.style.height = 'auto';
632
+ const style = window.getComputedStyle(textarea);
633
+ const maxHeight = parseInt(style.maxHeight, 10);
634
+ const newHeight = Math.min(maxHeight || 200, textarea.scrollHeight);
635
  textarea.style.height = newHeight + 'px';
 
 
 
 
 
 
 
 
 
 
636
  }
637
+
638
  // Défilement vers le bas
639
  function scrollToBottom(smooth = true) {
640
  setTimeout(() => {
641
  chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: smooth ? 'smooth' : 'auto' });
642
  }, 50);
643
  }
644
+
645
+ // Affichage de l'indicateur de chargement
646
  function showLoading(show) {
647
  if (show) {
648
  loadingIndicator.classList.remove('hidden');
 
654
  sendButton.disabled = show;
655
  promptInput.disabled = show;
656
  fileUpload.disabled = show;
657
+ if (show) {
658
+ clearFileButton.style.pointerEvents = 'none';
659
+ clearFileButton.style.opacity = '0.5';
660
+ } else {
661
+ clearFileButton.style.pointerEvents = 'auto';
662
+ clearFileButton.style.opacity = '0.7';
663
+ }
664
  }
665
+
666
  // Affichage des erreurs
667
  function displayError(message) {
668
  errorTextP.textContent = message || "Une erreur inconnue est survenue.";
669
  errorMessageDiv.classList.remove('hidden');
 
670
  }
671
  dismissErrorBtn.addEventListener('click', () => { errorMessageDiv.classList.add('hidden'); });
672
+
673
  // Ajout d'un message dans le chat
674
  function addMessageToChat(role, content, isHtml = false) {
675
  errorMessageDiv.classList.add('hidden');
676
  const messageWrapper = document.createElement('div');
677
+ messageWrapper.classList.add('flex', 'w-full', role === 'user' ? 'justify-end' : 'justify-start');
678
+
679
+ let messageContentHtml = '';
680
+ if (isHtml) {
681
+ messageContentHtml = content;
682
+ } else {
683
+ messageContentHtml = `<p class="text-sm sm:text-base whitespace-pre-wrap">${escapeHtml(content)}</p>`;
684
+ }
685
+
686
+ let bubbleHtml = '';
687
  if (role === 'user') {
688
+ bubbleHtml = `
689
+ <div class="message-bubble user-message">
690
+ ${messageContentHtml}
 
 
691
  </div>
692
  `;
693
  } else {
694
+ bubbleHtml = `
695
  <div class="flex items-start space-x-2 max-w-[85%]">
696
  <div class="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0 dark:bg-primary-900/50">
697
  <span class="text-lg">✨</span>
698
  </div>
699
  <div class="message-bubble assistant-message bg-secondary-50 text-secondary-900 border border-secondary-200 relative">
700
  <div class="prose prose-sm sm:prose-base max-w-none dark:prose-invert">
701
+ ${messageContentHtml}
702
  </div>
703
  <button class="copy-btn text-xs bg-white/90 dark:bg-gray-800/90 hover:bg-gray-100 dark:hover:bg-gray-700 py-1 px-2 rounded text-gray-600 dark:text-gray-300 flex items-center">
704
  <i class="fa-regular fa-copy mr-1"></i> Copier
 
707
  </div>
708
  `;
709
  }
710
+ messageWrapper.innerHTML = bubbleHtml;
711
+
712
+ // Activation du bouton de copie
713
  const copyBtns = messageWrapper.querySelectorAll('.copy-btn');
714
  copyBtns.forEach(btn => {
715
  btn.addEventListener('click', function() {
716
+ const textToCopy = this.closest('.message-bubble').querySelector('.prose').innerText || this.closest('.message-bubble').innerText;
717
  navigator.clipboard.writeText(textToCopy).then(() => {
718
  const originalText = this.innerHTML;
719
  this.innerHTML = '<i class="fa-solid fa-check mr-1"></i> Copié';
720
+ setTimeout(() => { this.innerHTML = originalText; }, 2000);
721
+ }).catch(err => console.error('Copy failed:', err));
722
+ });
723
+ });
724
+
725
+ // Activation de la copie pour les blocs de code
726
+ const codeBlocks = messageWrapper.querySelectorAll('pre');
727
+ codeBlocks.forEach(pre => {
728
+ const code = pre.querySelector('code');
729
+ if (!code) return;
730
+ if (pre.querySelector('.code-copy-btn')) return;
731
+ const copyButton = document.createElement('button');
732
+ copyButton.innerHTML = '<i class="fa-regular fa-copy"></i>';
733
+ copyButton.className = 'code-copy-btn p-1.5 bg-gray-200/50 dark:bg-gray-700/50 rounded text-gray-600 dark:text-gray-300 hover:bg-gray-300/70 dark:hover:bg-gray-600/70 tooltip';
734
+ copyButton.setAttribute('aria-label', 'Copier le code');
735
+ const tooltipText = document.createElement('span');
736
+ tooltipText.className = 'tooltip-text !text-xs';
737
+ tooltipText.textContent = 'Copier le code';
738
+ copyButton.appendChild(tooltipText);
739
+ copyButton.addEventListener('click', () => {
740
+ navigator.clipboard.writeText(code.innerText).then(() => {
741
+ tooltipText.textContent = 'Copié!';
742
+ copyButton.innerHTML = '<i class="fa-solid fa-check"></i>';
743
  setTimeout(() => {
744
+ copyButton.innerHTML = '<i class="fa-regular fa-copy"></i>';
745
+ copyButton.appendChild(tooltipText);
746
+ tooltipText.textContent = 'Copier le code';
747
  }, 2000);
748
+ }).catch(err => {
749
+ tooltipText.textContent = 'Erreur copie';
750
+ console.error('Failed to copy code: ', err);
751
+ setTimeout(() => { tooltipText.textContent = 'Copier le code'; }, 2000);
752
  });
753
  });
754
+ pre.appendChild(copyButton);
755
  });
756
+
757
  chatMessages.insertBefore(messageWrapper, loadingIndicator);
758
  scrollToBottom();
759
  }
760
  function escapeHtml(unsafe) {
761
+ return unsafe
762
+ .replace(/&/g, "&amp;")
763
+ .replace(/</g, "&lt;")
764
+ .replace(/>/g, "&gt;")
765
+ .replace(/"/g, "&quot;")
766
+ .replace(/'/g, "&#039;");
767
  }
768
+
769
+ // Gestion du cooldown pour le raisonnement avancé
770
  function startAdvancedCooldownTimer() {
771
  advancedToggle.disabled = true;
772
  advancedToggleCooldownEndTime = Date.now() + COOLDOWN_DURATION;
773
+ const intervalId = setInterval(() => {
774
  const now = Date.now();
775
  if (now >= advancedToggleCooldownEndTime) {
776
  clearInterval(intervalId);
 
782
  advancedCooldownTimerSpan.textContent = `(${remainingSeconds}s)`;
783
  advancedCooldownTimerSpan.classList.remove('hidden');
784
  }
785
+ }, 1000);
786
+ const remainingSeconds = Math.ceil((advancedToggleCooldownEndTime - Date.now()) / 1000);
787
+ advancedCooldownTimerSpan.textContent = `(${remainingSeconds}s)`;
788
+ advancedCooldownTimerSpan.classList.remove('hidden');
789
  }
790
+
791
  // Chargement de l'historique du chat
792
  async function loadChatHistory() {
793
+ historyLoadingIndicator.style.display = 'flex';
794
  try {
795
  const response = await fetch(API_HISTORY_ENDPOINT);
796
  if (!response.ok) {
797
  let errorMsg = `Erreur serveur (${response.status})`;
798
+ try {
799
+ const errData = await response.json();
800
+ errorMsg = errData.error || errorMsg;
801
+ } catch (e) {}
802
  throw new Error(errorMsg);
803
  }
804
  const data = await response.json();
805
+ const messagesToRemove = chatMessages.querySelectorAll(':scope > *:not(#loading-indicator)');
806
+ messagesToRemove.forEach(el => el.remove());
807
+ loadingIndicator.classList.add('hidden');
808
  if (data.success && Array.isArray(data.history)) {
 
 
 
809
  if (data.history.length === 0) {
810
  addMessageToChat('assistant', "Bonjour ! Je suis Mariam, votre assistant IA. Comment puis-je vous aider aujourd'hui ?", true);
811
  } else {
 
816
  }
817
  scrollToBottom(false);
818
  } else {
819
+ throw new Error(data.error || "Format de réponse invalide pour l'historique.");
820
  }
821
  } catch (error) {
 
 
 
822
  displayError(`Impossible de charger l'historique: ${error.message}`);
823
+ if (chatMessages.querySelectorAll(':scope > *:not(#loading-indicator):not(#history-loading)').length === 0) {
824
+ addMessageToChat('assistant', "Bonjour ! Je suis Mariam. Je n'ai pas pu charger notre conversation précédente. Comment puis-je vous aider ?", true);
825
+ }
826
  } finally {
827
  historyLoadingIndicator.remove();
828
  promptInput.focus();
829
+ adjustTextareaHeight(promptInput);
830
  }
831
  }
832
+
833
  // Gestion des fichiers
834
  function clearFileInput() {
835
  fileUpload.value = '';
836
  fileChip.classList.add('hidden');
837
+ fileNameSpan.textContent = '';
838
+ fileNameSpan.title = '';
839
+ filePreview.innerHTML = '';
840
  filePreview.classList.add('hidden');
841
  previewArea.classList.add('hidden');
 
842
  }
843
  fileUpload.addEventListener('change', () => {
844
  if (fileUpload.files.length > 0) {
 
847
  fileNameSpan.textContent = name;
848
  fileNameSpan.title = name;
849
  fileChip.classList.remove('hidden');
850
+ filePreview.innerHTML = '';
851
  if (file.type.startsWith('image/')) {
852
  const reader = new FileReader();
853
  reader.onload = (e) => {
854
  filePreview.innerHTML = `
855
  <div class="file-preview">
856
+ <img src="${e.target.result}" alt="Prévisualisation: ${escapeHtml(name)}">
857
+ </div>`;
858
+ filePreview.classList.remove('hidden');
859
+ previewArea.classList.remove('hidden');
860
+ };
861
+ reader.onerror = () => {
862
+ filePreview.innerHTML = `<p class="text-red-500 text-xs text-center p-2">Erreur lecture image</p>`;
863
  filePreview.classList.remove('hidden');
864
  previewArea.classList.remove('hidden');
865
  };
866
  reader.readAsDataURL(file);
867
  } else {
868
  filePreview.innerHTML = `
869
+ <div class="flex items-center justify-center p-3">
870
+ <div class="bg-gray-100 dark:bg-gray-800 p-3 rounded-lg text-center">
871
+ <i class="fa-solid ${getFileIcon(file.type)} text-3xl text-gray-500 dark:text-gray-400 mb-2"></i>
872
+ <p class="text-xs text-gray-500 dark:text-gray-400">${formatFileSize(file.size)}</p>
873
  </div>
874
+ </div>`;
 
875
  filePreview.classList.remove('hidden');
876
  previewArea.classList.remove('hidden');
877
  }
 
880
  }
881
  });
882
  function getFileIcon(fileType) {
883
+ if (!fileType) return 'fa-file';
884
  if (fileType.includes('pdf')) return 'fa-file-pdf';
885
  if (fileType.includes('text')) return 'fa-file-lines';
886
  return 'fa-file';
887
  }
888
  function formatFileSize(bytes) {
889
+ if (bytes === 0) return '0 octets';
890
+ const k = 1024;
891
+ const sizes = ['octets', 'Ko', 'Mo', 'Go', 'To'];
892
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
893
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
894
  }
895
  clearFileButton.addEventListener('click', clearFileInput);
896
+
897
  // Événements pour le textarea
898
+ promptInput.addEventListener('input', () => {
899
+ adjustTextareaHeight(promptInput);
900
  });
 
901
  promptInput.addEventListener('keydown', (e) => {
902
  if (isComposing) return;
903
+ const isMobile = window.innerWidth < MOBILE_BREAKPOINT;
904
+ if (e.key === 'Enter') {
905
+ if (isMobile && !e.shiftKey) {
906
+ setTimeout(() => adjustTextareaHeight(promptInput), 0);
907
+ } else if (!isMobile && !e.shiftKey) {
908
+ e.preventDefault();
909
+ if (!sendButton.disabled && (promptInput.value.trim() || fileUpload.files.length > 0)) {
910
+ chatForm.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
911
+ }
912
+ } else if (e.shiftKey) {
913
+ setTimeout(() => adjustTextareaHeight(promptInput), 0);
914
+ if (isMobile) {
915
+ e.preventDefault();
916
+ if (!sendButton.disabled && (promptInput.value.trim() || fileUpload.files.length > 0)) {
917
+ chatForm.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
918
+ }
919
+ }
920
+ }
921
  }
922
  });
 
923
  promptInput.addEventListener('compositionstart', () => { isComposing = true; });
924
+ promptInput.addEventListener('compositionend', () => {
925
+ isComposing = false;
926
+ setTimeout(() => adjustTextareaHeight(promptInput), 0);
927
+ });
928
+ adjustTextareaHeight(promptInput);
929
+
930
  // Soumission du formulaire
931
  chatForm.addEventListener('submit', async (e) => {
932
  e.preventDefault();
 
934
  const file = fileUpload.files[0];
935
  const useWebSearch = webSearchToggle.checked;
936
  const useAdvanced = advancedToggle.checked;
937
+ if (sendButton.disabled) return;
938
  if (!prompt && !file) {
 
939
  promptInput.focus();
940
  return;
941
  }
942
  errorMessageDiv.classList.add('hidden');
943
  if (useAdvanced) {
944
  const now = Date.now();
945
+ if (advancedToggleCooldownEndTime > 0 && now < advancedToggleCooldownEndTime) {
946
  const remainingSeconds = Math.ceil((advancedToggleCooldownEndTime - now) / 1000);
947
  displayError(`Le raisonnement avancé est disponible dans ${remainingSeconds} seconde(s).`);
948
  return;
 
950
  }
951
  let userMessageText = prompt;
952
  if (file && file.name) {
953
+ userMessageText = prompt ? `${prompt}\n[Fichier: ${file.name}]` : `[Fichier joint: ${file.name}]`;
954
  }
955
  addMessageToChat('user', userMessageText);
956
  const formData = new FormData();
957
  formData.append('prompt', prompt);
958
  formData.append('web_search', useWebSearch);
 
959
  formData.append('advanced_reasoning', useAdvanced);
960
+ if (file) {
961
+ formData.append('file', file);
962
+ }
963
  promptInput.value = '';
964
+ adjustTextareaHeight(promptInput);
965
  clearFileInput();
966
+ if (useAdvanced) {
967
+ startAdvancedCooldownTimer();
968
+ advancedToggle.checked = false;
969
+ }
970
+ webSearchToggle.checked = false;
971
+ showLoading(true);
972
  try {
973
  const response = await fetch(API_CHAT_ENDPOINT, { method: 'POST', body: formData });
974
  const data = await response.json();
975
+ if (!response.ok) {
976
+ throw new Error(data.error || `Erreur serveur: ${response.status} ${response.statusText}`);
977
+ }
978
  if (data.success && data.message) {
979
  addMessageToChat('assistant', data.message, true);
980
  } else {
981
+ throw new Error(data.error || "Réponse invalide ou vide du serveur.");
982
  }
983
  } catch (error) {
984
+ console.error("Chat Error:", error);
985
+ addMessageToChat('assistant', `<p class="text-red-600 dark:text-red-400">Désolé, une erreur est survenue :<br>${escapeHtml(error.message)}</p>`, true);
986
  } finally {
987
  showLoading(false);
988
  promptInput.focus();
989
  }
990
  });
991
+
992
  // Effacement de la conversation
993
  clearForm.addEventListener('submit', async (e) => {
994
  e.preventDefault();
995
  const confirmDialog = document.createElement('div');
996
+ confirmDialog.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4';
997
  confirmDialog.innerHTML = `
998
+ <div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-xl max-w-sm w-full animate-[message-fade-in_0.2s_ease-out]">
999
  <h3 class="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100">Confirmer l'effacement</h3>
1000
+ <p class="text-sm text-gray-600 dark:text-gray-300 mb-5">Êtes-vous sûr de vouloir effacer toute la conversation ? Cette action est irréversible.</p>
1001
  <div class="flex justify-end space-x-3">
1002
+ <button type="button" id="cancel-clear" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-400">
1003
  Annuler
1004
  </button>
1005
+ <button type="button" id="confirm-clear" class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded hover:bg-red-700 transition-colors focus:outline-none focus:ring-2 focus:ring-red-500">
1006
  Effacer
1007
  </button>
1008
  </div>
1009
  </div>
1010
  `;
1011
+ const closeModal = () => {
1012
+ if (document.body.contains(confirmDialog)) {
1013
+ document.body.removeChild(confirmDialog);
1014
+ document.body.style.overflow = '';
1015
+ }
1016
+ };
1017
+ confirmDialog.querySelector('#cancel-clear').addEventListener('click', closeModal);
1018
+ confirmDialog.querySelector('#confirm-clear').addEventListener('click', async () => {
1019
+ closeModal();
1020
+ const clearButton = e.target.querySelector('button[type="submit"]');
1021
+ const originalButtonContent = clearButton.innerHTML;
1022
+ clearButton.disabled = true;
1023
+ clearButton.innerHTML = '<i class="fa-solid fa-spinner fa-spin mr-1.5"></i><span class="hidden sm:inline">Effacement...</span>';
1024
  try {
1025
+ const response = await fetch(CLEAR_ENDPOINT, { method: 'POST' });
1026
  const data = await response.json();
1027
  if (response.ok && data.success) {
1028
+ const messagesToRemove = chatMessages.querySelectorAll(':scope > *:not(#loading-indicator)');
1029
+ messagesToRemove.forEach(el => el.remove());
1030
+ addMessageToChat('assistant', "Conversation effacée. Comment puis-je vous aider maintenant ?", true);
 
1031
  errorMessageDiv.classList.add('hidden');
1032
  } else {
1033
  throw new Error(data.error || "Impossible d'effacer la conversation.");
1034
  }
1035
  } catch (error) {
1036
+ console.error("Clear Error:", error);
1037
  displayError(`Erreur lors de l'effacement: ${error.message}`);
1038
  } finally {
1039
+ clearButton.innerHTML = originalButtonContent;
1040
+ clearButton.disabled = false;
1041
  promptInput.focus();
1042
  }
1043
  });
1044
+ document.body.appendChild(confirmDialog);
1045
+ document.body.style.overflow = 'hidden';
1046
+ confirmDialog.querySelector('#cancel-clear').focus();
1047
  });
1048
+
1049
+ // Initialisation
1050
  loadChatHistory();
1051
+
1052
+ // Observer pour les tableaux Markdown
 
 
 
 
 
 
1053
  const observeMarkdownTables = () => {
1054
  const observer = new MutationObserver(mutations => {
1055
  mutations.forEach(mutation => {
1056
+ if (mutation.addedNodes.length) {
1057
+ mutation.addedNodes.forEach(node => {
1058
+ if (node.nodeType === 1) {
1059
+ const tables = node.matches('.prose table') ? [node] : node.querySelectorAll('.prose table');
1060
+ tables.forEach(table => {
1061
+ if (!table.closest('.table-wrapper')) {
1062
+ const wrapper = document.createElement('div');
1063
+ wrapper.className = 'table-wrapper';
1064
+ table.parentNode.insertBefore(wrapper, table);
1065
+ wrapper.appendChild(table);
1066
+ }
1067
+ });
1068
+ }
1069
  });
1070
  }
1071
  });
1072
  });
 
1073
  observer.observe(chatMessages, { childList: true, subtree: true });
1074
  };
 
1075
  observeMarkdownTables();
1076
+
1077
+ // Gestion du changement de thème selon le système
1078
  const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
1079
+ const handleSchemeChange = (e) => {
1080
+ if (!localStorage.getItem('theme')) {
1081
+ if (e.matches) {
1082
+ document.documentElement.classList.add('dark');
1083
+ } else {
1084
+ document.documentElement.classList.remove('dark');
1085
+ }
1086
  }
1087
+ };
1088
+ prefersDarkScheme.addEventListener('change', handleSchemeChange);
1089
  });
1090
  </script>
1091
  </body>
1092
+ </html>