Docfile commited on
Commit
0b3c9ac
·
verified ·
1 Parent(s): a9ea92e

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +187 -538
templates/index.html CHANGED
@@ -1,32 +1,20 @@
1
-
2
  <!DOCTYPE html>
3
-
4
  <html lang="fr">
5
  <head>
6
  <meta charset="UTF-8">
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
  <title>Mariam AI</title>
9
-
10
  <!-- Tailwind CSS via CDN -->
11
-
12
  <script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
13
-
14
  <!-- Font Awesome pour les icônes -->
15
-
16
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
17
-
18
  <!-- Google Fonts -->
19
-
20
  <link rel="preconnect" href="https://fonts.googleapis.com">
21
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
22
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
23
-
24
  <!-- Favicon (Emoji amélioré) -->
25
-
26
  <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>">
27
-
28
  <script>
29
- // Configuration avancée de Tailwind
30
  tailwind.config = {
31
  darkMode: 'class',
32
  theme: {
@@ -89,53 +77,28 @@
89
  }
90
  }
91
  </script>
92
-
93
  <style>
94
- /* Base styles */
95
  html {
96
  scroll-behavior: smooth;
97
  }
98
-
99
  body {
100
  font-family: 'Inter', sans-serif;
101
  transition: background-color 0.3s ease, color 0.3s ease;
102
  }
103
-
104
- /* Layout */
105
  .chat-layout {
106
  min-height: calc(100vh - 64px);
107
  display: grid;
108
  grid-template-rows: 1fr auto;
109
  }
110
-
111
- /* Scrollbar styling */
112
- ::-webkit-scrollbar {
113
- width: 5px;
114
- height: 5px;
115
- }
116
-
117
- ::-webkit-scrollbar-track {
118
- background: transparent;
119
- }
120
-
121
  ::-webkit-scrollbar-thumb {
122
  background: #cbd5e1;
123
  border-radius: 5px;
124
  }
125
-
126
- .dark ::-webkit-scrollbar-thumb {
127
- background: #475569;
128
- }
129
-
130
- ::-webkit-scrollbar-thumb:hover {
131
- background: #94a3b8;
132
- }
133
-
134
- .dark ::-webkit-scrollbar-thumb:hover {
135
- background: #64748b;
136
- }
137
-
138
- /* Message bubbles */
139
  .message-bubble {
140
  position: relative;
141
  max-width: 85%;
@@ -146,38 +109,30 @@
146
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
147
  transition: transform 0.2s ease, box-shadow 0.2s ease;
148
  }
149
-
150
  .message-bubble:hover {
151
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05), 0 1px 3px rgba(0, 0, 0, 0.1);
152
  }
153
-
154
  .dark .message-bubble {
155
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
156
  }
157
-
158
  .dark .message-bubble:hover {
159
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2), 0 1px 3px rgba(0, 0, 0, 0.3);
160
  }
161
-
162
  .user-message {
163
  border-bottom-right-radius: 0.125rem;
164
  align-self: flex-end;
165
  background: linear-gradient(to bottom right, #3b82f6, #2563eb);
166
  color: white;
167
  }
168
-
169
  .assistant-message {
170
  border-bottom-left-radius: 0.125rem;
171
  align-self: flex-start;
172
  }
173
-
174
  .dark .assistant-message {
175
  background-color: #1e293b;
176
  color: #e2e8f0;
177
  border-color: #334155;
178
  }
179
-
180
- /* Message bubble arrow */
181
  .user-message::after {
182
  content: '';
183
  position: absolute;
@@ -188,7 +143,6 @@
188
  background: #2563eb;
189
  clip-path: polygon(0 0, 100% 0, 100% 100%);
190
  }
191
-
192
  .assistant-message::after {
193
  content: '';
194
  position: absolute;
@@ -199,38 +153,20 @@
199
  background: #f8fafc;
200
  clip-path: polygon(0 0, 100% 0, 0 100%);
201
  }
202
-
203
- .dark .assistant-message::after {
204
- background: #1e293b;
205
- }
206
-
207
- /* Animations */
208
  @keyframes message-fade-in {
209
- from {
210
- opacity: 0;
211
- transform: translateY(10px);
212
- }
213
- to {
214
- opacity: 1;
215
- transform: translateY(0);
216
- }
217
  }
218
-
219
  @keyframes pulse-fade {
220
- 0%, 100% {
221
- opacity: 0.5;
222
- }
223
- 50% {
224
- opacity: 1;
225
- }
226
  }
227
-
228
  .typing-indicator {
229
  display: inline-flex;
230
  align-items: center;
231
  margin-left: 0.5rem;
232
  }
233
-
234
  .typing-dot {
235
  width: 0.5rem;
236
  height: 0.5rem;
@@ -239,30 +175,14 @@
239
  opacity: 0.7;
240
  margin: 0 0.1rem;
241
  }
242
-
243
- .typing-dot:nth-child(1) {
244
- animation: pulse-fade 1.2s 0s infinite;
245
- }
246
-
247
- .typing-dot:nth-child(2) {
248
- animation: pulse-fade 1.2s 0.2s infinite;
249
- }
250
-
251
- .typing-dot:nth-child(3) {
252
- animation: pulse-fade 1.2s 0.4s infinite;
253
- }
254
-
255
- /* Style spécifique pour le mode sombre */
256
  .dark body {
257
  background-color: #0f172a;
258
  color: #e2e8f0;
259
  }
260
-
261
- /* Tooltip custom */
262
- .tooltip {
263
- position: relative;
264
- }
265
-
266
  .tooltip .tooltip-text {
267
  visibility: hidden;
268
  width: max-content;
@@ -283,12 +203,7 @@
283
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
284
  pointer-events: none;
285
  }
286
-
287
- .dark .tooltip .tooltip-text {
288
- background-color: #475569;
289
- color: #f1f5f9;
290
- }
291
-
292
  .tooltip .tooltip-text::after {
293
  content: "";
294
  position: absolute;
@@ -299,17 +214,8 @@
299
  border-style: solid;
300
  border-color: #1e293b transparent transparent transparent;
301
  }
302
-
303
- .dark .tooltip .tooltip-text::after {
304
- border-color: #475569 transparent transparent transparent;
305
- }
306
-
307
- .tooltip:hover .tooltip-text {
308
- visibility: visible;
309
- opacity: 1;
310
- }
311
-
312
- /* Bouton de copie */
313
  .copy-btn {
314
  position: absolute;
315
  top: 0.5rem;
@@ -318,12 +224,7 @@
318
  transition: opacity 0.2s ease, background-color 0.2s ease;
319
  z-index: 2;
320
  }
321
-
322
- .message-bubble:hover .copy-btn {
323
- opacity: 1;
324
- }
325
-
326
- /* File preview */
327
  .file-preview {
328
  max-width: 300px;
329
  margin: 0.5rem auto;
@@ -332,19 +233,13 @@
332
  border-radius: 0.5rem;
333
  transition: transform 0.2s ease;
334
  }
335
-
336
- .file-preview:hover {
337
- transform: scale(1.02);
338
- }
339
-
340
  .file-preview img {
341
  width: 100%;
342
  height: auto;
343
  display: block;
344
  object-fit: cover;
345
  }
346
-
347
- /* Chip style */
348
  .chip {
349
  display: inline-flex;
350
  align-items: center;
@@ -356,41 +251,22 @@
356
  color: #0369a1;
357
  transition: all 0.2s ease;
358
  }
359
-
360
- .chip .chip-icon {
361
- margin-right: 0.25rem;
362
- }
363
-
364
  .chip .chip-close {
365
  margin-left: 0.25rem;
366
  cursor: pointer;
367
  opacity: 0.7;
368
  transition: opacity 0.2s ease;
369
  }
370
-
371
- .chip .chip-close:hover {
372
- opacity: 1;
373
- }
374
-
375
- .dark .chip {
376
- background: #0c4a6e;
377
- color: #7dd3fc;
378
- }
379
-
380
- /* Switch toggle style */
381
  .toggle-switch {
382
  position: relative;
383
  display: inline-block;
384
  width: 2.5rem;
385
  height: 1.25rem;
386
  }
387
-
388
- .toggle-switch input {
389
- opacity: 0;
390
- width: 0;
391
- height: 0;
392
- }
393
-
394
  .toggle-slider {
395
  position: absolute;
396
  cursor: pointer;
@@ -402,7 +278,6 @@
402
  transition: .4s;
403
  border-radius: 1.25rem;
404
  }
405
-
406
  .toggle-slider:before {
407
  position: absolute;
408
  content: "";
@@ -414,50 +289,28 @@
414
  transition: .4s;
415
  border-radius: 50%;
416
  }
417
-
418
- input:checked + .toggle-slider {
419
- background-color: #0ea5e9;
420
- }
421
-
422
- input:focus + .toggle-slider {
423
- box-shadow: 0 0 1px #0ea5e9;
424
- }
425
-
426
- input:checked + .toggle-slider:before {
427
- transform: translateX(1.125rem);
428
- }
429
-
430
- .dark .toggle-slider {
431
- background-color: #475569;
432
- }
433
-
434
- .dark input:checked + .toggle-slider {
435
- background-color: #38bdf8;
436
- }
437
-
438
- /* Input styling */
439
  .chat-input {
440
  transition: all 0.3s ease;
441
  border-color: #e2e8f0;
442
  }
443
-
444
  .chat-input:focus {
445
  border-color: #38bdf8;
446
  box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2);
447
  }
448
-
449
  .dark .chat-input {
450
  background-color: #1e293b;
451
  color: #f1f5f9;
452
  border-color: #334155;
453
  }
454
-
455
  .dark .chat-input:focus {
456
  border-color: #38bdf8;
457
  box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2);
458
  }
459
-
460
- /* Code block styling */
461
  pre {
462
  position: relative;
463
  background-color: #f8fafc;
@@ -466,18 +319,11 @@
466
  padding: 1.25rem 1rem;
467
  overflow-x: auto;
468
  }
469
-
470
- .dark pre {
471
- background-color: #1e293b;
472
- color: #e2e8f0;
473
- }
474
-
475
  code {
476
  font-family: 'JetBrains Mono', monospace;
477
  font-size: 0.875rem;
478
  }
479
-
480
- /* Code block copy button */
481
  .code-copy-btn {
482
  position: absolute;
483
  top: 0.5rem;
@@ -485,55 +331,41 @@
485
  opacity: 0;
486
  transition: opacity 0.2s ease;
487
  }
488
-
489
- pre:hover .code-copy-btn {
490
- opacity: 0.7;
491
- }
492
-
493
- pre:hover .code-copy-btn:hover {
494
- opacity: 1;
495
- }
496
  </style>
497
-
498
  </head>
499
  <body class="bg-gray-50 text-gray-900 antialiased">
500
  <!-- Header -->
501
  <header class="bg-gradient-to-r from-primary-600 to-primary-800 text-white py-3 px-4 shadow-md sticky top-0 z-10">
502
  <div class="max-w-4xl mx-auto flex justify-between items-center">
503
- <!-- Logo & Title -->
504
  <div class="flex items-center space-x-2">
505
- <span class="text-2xl">✨</span>
506
  <h1 class="text-xl font-bold">Mariam AI</h1>
507
  </div>
508
-
509
- <!-- Actions -->
510
- <div class="flex items-center space-x-2 sm:space-x-4">
511
- <!-- Thème sombre/clair -->
512
- <button id="theme-toggle" class="p-2 rounded-full hover:bg-primary-700/50 transition-colors duration-200 tooltip" aria-label="Changer de thème">
513
- <i class="fa-solid fa-moon dark:hidden"></i>
514
- <i class="fa-solid fa-sun hidden dark:inline"></i>
515
- <span class="tooltip-text">Mode clair/sombre</span>
516
- </button>
517
-
518
- <!-- Bouton d'effacement -->
519
- <form action="/clear" method="POST" id="clear-form">
520
- <button type="submit" class="flex items-center bg-red-500 hover:bg-red-600 text-white text-xs font-semibold py-1.5 px-3 rounded-full transition duration-200 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-opacity-75 tooltip">
521
- <i class="fa-solid fa-trash-can mr-1.5"></i>
522
- <span class="hidden sm:inline">Effacer</span>
523
- <span class="tooltip-text">Effacer la conversation</span>
524
- </button>
525
- </form>
526
- </div>
527
- </div>
528
-
529
  </header>
530
-
531
  <!-- Main Container -->
532
-
533
  <main class="max-w-4xl mx-auto chat-layout">
534
- <!-- Chat Messages Container -->
535
  <section id="chat-messages" class="flex flex-col space-y-6 p-4 overflow-y-auto">
536
- <!-- Message de chargement de l'historique -->
537
  <div id="history-loading" class="text-center py-10">
538
  <div class="inline-flex items-center px-4 py-2 bg-primary-50 text-primary-700 rounded-lg dark:bg-primary-900/30 dark:text-primary-300">
539
  <svg class="animate-spin h-5 w-5 mr-3" viewBox="0 0 24 24">
@@ -543,133 +375,110 @@
543
  <span>Chargement de la conversation...</span>
544
  </div>
545
  </div>
546
-
547
- <!-- Indicateur de chargement pour les réponses -->
548
- <div id="loading-indicator" class="flex items-start space-x-2 hidden">
549
- <div class="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center dark:bg-primary-900/50">
550
- <span class="text-lg">✨</span>
551
- </div>
552
- <div class="message-bubble assistant-message bg-secondary-50 text-secondary-900 border border-secondary-200 flex items-center">
553
- <span>Mariam réfléchit</span>
554
- <div class="typing-indicator">
555
- <span class="typing-dot"></span>
556
- <span class="typing-dot"></span>
557
- <span class="typing-dot"></span>
 
558
  </div>
559
- </div>
560
- </div>
561
- </section>
562
-
563
- <!-- Bottom Container -->
564
- <div class="border-t border-gray-200 dark:border-gray-700">
565
- <!-- Error Message -->
566
- <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">
567
- <div class="flex">
568
- <div class="flex-shrink-0">
569
- <i class="fa-solid fa-circle-exclamation"></i>
 
 
 
 
 
570
  </div>
571
- <div class="ml-3">
572
- <p class="text-sm font-medium" id="error-text">Le message d'erreur détaillé ira ici.</p>
 
573
  </div>
574
- <button class="ml-auto" id="dismiss-error">
575
- <i class="fa-solid fa-xmark"></i>
576
- </button>
577
- </div>
578
- </div>
579
-
580
- <!-- Preview Area -->
581
- <div id="preview-area" class="px-4 py-2 bg-gray-50 dark:bg-gray-800/50 hidden">
582
- <!-- File Preview -->
583
- <div id="file-preview" class="hidden"></div>
584
- </div>
585
-
586
- <!-- Options Bar -->
587
- <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">
588
- <!-- Options du chat -->
589
- <div class="flex items-center space-x-4">
590
- <!-- Web Search Toggle -->
591
- <label class="flex items-center cursor-pointer tooltip">
592
- <span class="mr-2 text-xs sm:text-sm font-medium">
593
- <i class="fa-solid fa-globe mr-1.5"></i>
594
- <span class="hidden sm:inline">Recherche Web</span>
595
- </span>
596
- <span class="toggle-switch">
597
- <input type="checkbox" id="web_search_toggle" name="web_search" value="true">
598
- <span class="toggle-slider"></span>
599
- </span>
600
- <span class="tooltip-text">Activer la recherche web pour Mariam</span>
601
- </label>
602
-
603
- <!-- Advanced Reasoning Toggle -->
604
- <label class="flex items-center cursor-pointer tooltip">
605
- <span class="mr-2 text-xs sm:text-sm font-medium text-accent-700 dark:text-accent-300">
606
- <i class="fa-solid fa-brain mr-1.5"></i>
607
- <span class="hidden sm:inline">Avancé</span>
608
- <span id="advanced-cooldown-timer" class="text-xs ml-1 hidden"></span>
609
- </span>
610
- <span class="toggle-switch">
611
- <input type="checkbox" id="advanced_reasoning_toggle" name="advanced_reasoning" value="true">
612
- <span class="toggle-slider"></span>
613
- </span>
614
- <span class="tooltip-text">Activer le raisonnement avancé (1 fois/min)</span>
615
- </label>
616
- </div>
617
-
618
- <!-- Upload Button -->
619
- <div>
620
- <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">
621
- <i class="fa-solid fa-paperclip"></i>
622
- <span class="ml-1.5 hidden sm:inline">Fichier</span>
623
- <input type="file" id="file_upload" name="file" class="hidden" accept=".txt,.pdf,.png,.jpg,.jpeg">
624
- <span class="tooltip-text">Joindre un fichier (txt, pdf, image)</span>
625
- </label>
626
-
627
- <!-- File Chip (apparaît quand un fichier est sélectionné) -->
628
- <div id="file-chip" class="chip mt-2 hidden">
629
- <i class="fa-solid fa-file chip-icon"></i>
630
- <span id="file-name" class="truncate max-w-[120px]"></span>
631
- <i id="clear-file" class="fa-solid fa-xmark chip-close"></i>
632
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
633
  </div>
634
- </div>
635
-
636
- <!-- Chat Input Form -->
637
- <form id="chat-form" class="p-3 sm:p-4 bg-white dark:bg-gray-900">
638
- <div class="relative">
639
- <input
640
- type="text"
641
- id="prompt"
642
- name="prompt"
643
- class="chat-input w-full pl-4 pr-12 py-3 rounded-full border focus:outline-none text-sm sm:text-base"
644
- placeholder="Posez votre question à Mariam..."
645
- autocomplete="off"
646
- >
647
- <button
648
- type="submit"
649
- id="send-button"
650
- class="absolute right-2 top-1/2 transform -translate-y-1/2 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-300 text-white rounded-full p-2 transition focus:outline-none focus:ring-2 focus:ring-primary-400"
651
- title="Envoyer le message"
652
- >
653
- <i class="fa-solid fa-paper-plane"></i>
654
- </button>
655
- </div>
656
- <div class="text-xs text-center mt-2 text-gray-400 dark:text-gray-500">
657
- 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
658
- </div>
659
- </form>
660
- </div>
661
- IGNORE_WHEN_COPYING_START
662
- content_copy
663
- download
664
- Use code with caution.
665
- IGNORE_WHEN_COPYING_END
666
  </main>
667
-
668
  <!-- Scripts -->
669
-
670
  <script>
671
  document.addEventListener('DOMContentLoaded', () => {
672
- // Elements
673
  const chatForm = document.getElementById('chat-form');
674
  const promptInput = document.getElementById('prompt');
675
  const chatMessages = document.getElementById('chat-messages');
@@ -690,29 +499,21 @@ IGNORE_WHEN_COPYING_END
690
  const advancedToggle = document.getElementById('advanced_reasoning_toggle');
691
  const advancedCooldownTimerSpan = document.getElementById('advanced-cooldown-timer');
692
  const themeToggleBtn = document.getElementById('theme-toggle');
693
-
694
- // API endpoints
695
  const API_CHAT_ENDPOINT = '/api/chat';
696
  const API_HISTORY_ENDPOINT = '/api/history';
697
  const CLEAR_ENDPOINT = '/clear';
698
-
699
- // Constantes
700
- const COOLDOWN_DURATION = 60 * 1000; // 60 secondes
701
-
702
- // Variables
703
  let advancedToggleCooldownEndTime = 0;
704
- let isComposing = false; // Pour la composition (pour les langues qui utilisent IME)
705
 
706
- // Thème
707
  function initializeTheme() {
708
- // Vérifier les préférences locales ou systèmes
709
  if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
710
  document.documentElement.classList.add('dark');
711
  } else {
712
  document.documentElement.classList.remove('dark');
713
  }
714
  }
715
-
716
  function toggleTheme() {
717
  if (document.documentElement.classList.contains('dark')) {
718
  document.documentElement.classList.remove('dark');
@@ -722,21 +523,17 @@ IGNORE_WHEN_COPYING_END
722
  localStorage.theme = 'dark';
723
  }
724
  }
725
-
726
  themeToggleBtn.addEventListener('click', toggleTheme);
727
  initializeTheme();
728
 
729
- // Fonction de défilement vers le bas
730
  function scrollToBottom(smooth = true) {
731
  setTimeout(() => {
732
- chatMessages.scrollTo({
733
- top: chatMessages.scrollHeight,
734
- behavior: smooth ? 'smooth' : 'auto'
735
- });
736
  }, 50);
737
  }
738
-
739
- // Gestion du chargement
740
  function showLoading(show) {
741
  if (show) {
742
  loadingIndicator.classList.remove('hidden');
@@ -745,36 +542,27 @@ IGNORE_WHEN_COPYING_END
745
  } else {
746
  loadingIndicator.classList.add('hidden');
747
  }
748
-
749
  sendButton.disabled = show;
750
  promptInput.disabled = show;
751
  fileUpload.disabled = show;
752
  clearFileButton.disabled = show;
753
  }
754
-
755
- // Gestion des erreurs
756
  function displayError(message) {
757
  errorTextP.textContent = message || "Une erreur inconnue est survenue.";
758
  errorMessageDiv.classList.remove('hidden');
759
  errorMessageDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
760
  }
 
761
 
762
- // Fermer le message d'erreur
763
- dismissErrorBtn.addEventListener('click', () => {
764
- errorMessageDiv.classList.add('hidden');
765
- });
766
-
767
- // Ajouter un message au chat
768
  function addMessageToChat(role, content, isHtml = false) {
769
  errorMessageDiv.classList.add('hidden');
770
-
771
  const messageWrapper = document.createElement('div');
772
  messageWrapper.classList.add('flex', role === 'user' ? 'justify-end' : 'justify-start');
773
-
774
  let html = '';
775
-
776
  if (role === 'user') {
777
- // Message utilisateur
778
  html = `
779
  <div class="flex items-start space-x-2 max-w-[85%]">
780
  <div class="message-bubble user-message">
@@ -783,7 +571,6 @@ IGNORE_WHEN_COPYING_END
783
  </div>
784
  `;
785
  } else {
786
- // Message assistant
787
  html = `
788
  <div class="flex items-start space-x-2 max-w-[85%]">
789
  <div class="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0 dark:bg-primary-900/50">
@@ -800,70 +587,22 @@ IGNORE_WHEN_COPYING_END
800
  </div>
801
  `;
802
  }
803
-
804
  messageWrapper.innerHTML = html;
805
  chatMessages.insertBefore(messageWrapper, loadingIndicator);
806
-
807
- // Activer les boutons de copie pour les blocs de code
808
- const preTags = messageWrapper.querySelectorAll('pre');
809
- preTags.forEach(pre => {
810
- if (!pre.querySelector('.code-copy-btn')) {
811
- const codeBtn = document.createElement('button');
812
- codeBtn.className = 'code-copy-btn bg-white/90 dark:bg-gray-800/90 text-xs p-1 rounded text-gray-600 dark:text-gray-300';
813
- codeBtn.innerHTML = '<i class="fa-regular fa-copy"></i>';
814
- codeBtn.title = 'Copier le code';
815
- codeBtn.addEventListener('click', () => {
816
- const code = pre.querySelector('code')?.innerText || pre.innerText;
817
- navigator.clipboard.writeText(code)
818
- .then(() => {
819
- codeBtn.innerHTML = '<i class="fa-solid fa-check"></i>';
820
- setTimeout(() => {
821
- codeBtn.innerHTML = '<i class="fa-regular fa-copy"></i>';
822
- }, 2000);
823
- });
824
- });
825
- pre.appendChild(codeBtn);
826
- }
827
- });
828
-
829
- // Activer les boutons de copie pour les messages
830
- const copyBtn = messageWrapper.querySelector('.copy-btn');
831
- if (copyBtn) {
832
- copyBtn.addEventListener('click', (e) => {
833
- e.stopPropagation();
834
- e.preventDefault();
835
-
836
- const textContent = messageWrapper.querySelector('.prose').innerText;
837
- navigator.clipboard.writeText(textContent)
838
- .then(() => {
839
- copyBtn.innerHTML = '<i class="fa-solid fa-check mr-1"></i> Copié';
840
- setTimeout(() => {
841
- copyBtn.innerHTML = '<i class="fa-regular fa-copy mr-1"></i> Copier';
842
- }, 2000);
843
- });
844
- });
845
- }
846
-
847
- if (historyLoadingIndicator.parentNode !== chatMessages) {
848
- scrollToBottom();
849
- }
850
  }
851
-
852
- // Échappement HTML pour éviter les injections XSS
853
  function escapeHtml(unsafe) {
854
- return unsafe
855
- .replace(/&/g, "&amp;")
856
- .replace(/</g, "&lt;")
857
- .replace(/>/g, "&gt;")
858
- .replace(/"/g, "&quot;")
859
- .replace(/'/g, "&#039;");
860
  }
861
-
862
  // Gestion du cooldown du mode avancé
863
  function startAdvancedCooldownTimer() {
864
  advancedToggle.disabled = true;
865
  advancedToggleCooldownEndTime = Date.now() + COOLDOWN_DURATION;
866
-
867
  const updateTimer = () => {
868
  const now = Date.now();
869
  if (now >= advancedToggleCooldownEndTime) {
@@ -877,46 +616,36 @@ IGNORE_WHEN_COPYING_END
877
  advancedCooldownTimerSpan.classList.remove('hidden');
878
  }
879
  };
880
-
881
  const intervalId = setInterval(updateTimer, 1000);
882
  updateTimer();
883
  }
884
-
885
- // Chargement de l'historique de chat
886
  async function loadChatHistory() {
887
  historyLoadingIndicator.style.display = 'block';
888
  try {
889
  const response = await fetch(API_HISTORY_ENDPOINT);
890
  if (!response.ok) {
891
  let errorMsg = `Erreur serveur (${response.status})`;
892
- try {
893
- const errData = await response.json();
894
- errorMsg = errData.error || errorMsg;
895
- } catch (e) {}
896
  throw new Error(errorMsg);
897
  }
898
-
899
  const data = await response.json();
900
  if (data.success && Array.isArray(data.history)) {
901
- // Vider le conteneur et préparer les nouvelles entrées
902
  chatMessages.innerHTML = '';
903
  chatMessages.appendChild(loadingIndicator);
904
  loadingIndicator.classList.add('hidden');
905
-
906
  if (data.history.length === 0) {
907
- // Message d'accueil
908
  addMessageToChat('assistant', "Bonjour ! Je suis Mariam, votre assistant IA. Comment puis-je vous aider aujourd'hui ?", true);
909
  } else {
910
- // Afficher l'historique
911
  data.history.forEach(message => {
912
  const isAssistantHtml = message.role === 'assistant';
913
  addMessageToChat(message.role, message.text, isAssistantHtml);
914
  });
915
  }
916
-
917
  scrollToBottom(false);
918
  } else {
919
- throw new Error(data.error || "Format de réponse de l'historique invalide.");
920
  }
921
  } catch (error) {
922
  chatMessages.innerHTML = '';
@@ -928,7 +657,7 @@ IGNORE_WHEN_COPYING_END
928
  promptInput.focus();
929
  }
930
  }
931
-
932
  // Gestion des fichiers
933
  function clearFileInput() {
934
  fileUpload.value = '';
@@ -937,18 +666,13 @@ IGNORE_WHEN_COPYING_END
937
  previewArea.classList.add('hidden');
938
  filePreview.innerHTML = '';
939
  }
940
-
941
  fileUpload.addEventListener('change', () => {
942
  if (fileUpload.files.length > 0) {
943
  const file = fileUpload.files[0];
944
  const name = file.name;
945
-
946
- // Mettre à jour le chip de fichier
947
  fileNameSpan.textContent = name;
948
  fileNameSpan.title = name;
949
  fileChip.classList.remove('hidden');
950
-
951
- // Si c'est une image, créer une prévisualisation
952
  if (file.type.startsWith('image/')) {
953
  const reader = new FileReader();
954
  reader.onload = (e) => {
@@ -962,7 +686,6 @@ IGNORE_WHEN_COPYING_END
962
  };
963
  reader.readAsDataURL(file);
964
  } else {
965
- // Afficher une icône pour les autres types de fichiers
966
  filePreview.innerHTML = `
967
  <div class="flex items-center justify-center py-4">
968
  <div class="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg text-center">
@@ -978,25 +701,18 @@ IGNORE_WHEN_COPYING_END
978
  clearFileInput();
979
  }
980
  });
981
-
982
- // Obtenir l'icône en fonction du type de fichier
983
  function getFileIcon(fileType) {
984
  if (fileType.includes('pdf')) return 'fa-file-pdf';
985
  if (fileType.includes('text')) return 'fa-file-lines';
986
  return 'fa-file';
987
  }
988
-
989
- // Formater la taille du fichier
990
  function formatFileSize(bytes) {
991
  if (bytes < 1024) return bytes + ' octets';
992
  if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' Ko';
993
  return (bytes / 1048576).toFixed(1) + ' Mo';
994
  }
995
-
996
- clearFileButton.addEventListener('click', () => {
997
- clearFileInput();
998
- });
999
-
1000
  // Soumission du formulaire
1001
  chatForm.addEventListener('submit', async (e) => {
1002
  e.preventDefault();
@@ -1004,16 +720,12 @@ IGNORE_WHEN_COPYING_END
1004
  const file = fileUpload.files[0];
1005
  const useWebSearch = webSearchToggle.checked;
1006
  const useAdvanced = advancedToggle.checked;
1007
-
1008
  if (!prompt && !file) {
1009
  displayError("Veuillez entrer un message ou sélectionner un fichier.");
1010
  promptInput.focus();
1011
  return;
1012
  }
1013
-
1014
  errorMessageDiv.classList.add('hidden');
1015
-
1016
- // Vérification du cooldown pour le mode avancé
1017
  if (useAdvanced) {
1018
  const now = Date.now();
1019
  if (now < advancedToggleCooldownEndTime) {
@@ -1022,51 +734,29 @@ IGNORE_WHEN_COPYING_END
1022
  return;
1023
  }
1024
  }
1025
-
1026
- // Construire le message utilisateur
1027
  let userMessageText = prompt;
1028
  if (file && file.name) {
1029
  userMessageText = prompt ? `${prompt}` : `[Fichier joint: ${file.name}]`;
1030
  }
1031
-
1032
  addMessageToChat('user', userMessageText);
1033
-
1034
- // Préparer les données
1035
  const formData = new FormData();
1036
  formData.append('prompt', prompt);
1037
  formData.append('web_search', useWebSearch);
1038
-
1039
- if (file) {
1040
- formData.append('file', file);
1041
- }
1042
-
1043
  formData.append('advanced_reasoning', useAdvanced);
1044
-
1045
- // Afficher le chargement et nettoyer le formulaire
1046
  showLoading(true);
1047
  promptInput.value = '';
1048
  clearFileInput();
1049
-
1050
- // Réinitialiser les options
1051
  advancedToggle.checked = false;
1052
  if (useAdvanced) startAdvancedCooldownTimer();
1053
-
1054
  try {
1055
- const response = await fetch(API_CHAT_ENDPOINT, {
1056
- method: 'POST',
1057
- body: formData,
1058
- });
1059
-
1060
  const data = await response.json();
1061
-
1062
- if (!response.ok) {
1063
- throw new Error(data.error || `Erreur serveur: ${response.status}`);
1064
- }
1065
-
1066
  if (data.success && data.message) {
1067
  addMessageToChat('assistant', data.message, true);
1068
  } else {
1069
- throw new Error(data.error || "Réponse invalide ou vide du serveur.");
1070
  }
1071
  } catch (error) {
1072
  displayError(error.message);
@@ -1075,12 +765,10 @@ IGNORE_WHEN_COPYING_END
1075
  promptInput.focus();
1076
  }
1077
  });
1078
-
1079
  // Effacement de la conversation
1080
  clearForm.addEventListener('submit', async (e) => {
1081
  e.preventDefault();
1082
-
1083
- // Demander confirmation
1084
  const confirmDialog = document.createElement('div');
1085
  confirmDialog.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
1086
  confirmDialog.innerHTML = `
@@ -1097,46 +785,32 @@ IGNORE_WHEN_COPYING_END
1097
  </div>
1098
  </div>
1099
  `;
1100
-
1101
  document.body.appendChild(confirmDialog);
1102
  document.body.classList.add('overflow-hidden');
1103
-
1104
- // Gérer les boutons
1105
  document.getElementById('cancel-clear').addEventListener('click', () => {
1106
  document.body.removeChild(confirmDialog);
1107
  document.body.classList.remove('overflow-hidden');
1108
  });
1109
-
1110
  document.getElementById('confirm-clear').addEventListener('click', async () => {
1111
  document.body.removeChild(confirmDialog);
1112
  document.body.classList.remove('overflow-hidden');
1113
-
1114
  const originalButtonText = e.target.querySelector('button').innerHTML;
1115
  e.target.querySelector('button').innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
1116
  e.target.querySelector('button').disabled = true;
1117
-
1118
  try {
1119
- const response = await fetch(CLEAR_ENDPOINT, {
1120
- method: 'POST',
1121
- headers: {
1122
- 'X-Requested-With': 'XMLHttpRequest'
1123
- }
1124
- });
1125
-
1126
  const data = await response.json();
1127
-
1128
  if (response.ok && data.success) {
1129
  chatMessages.innerHTML = '';
1130
  chatMessages.appendChild(loadingIndicator);
1131
  loadingIndicator.classList.add('hidden');
1132
-
1133
  addMessageToChat('assistant', "Conversation effacée. Comment puis-je vous aider ?", true);
1134
  errorMessageDiv.classList.add('hidden');
1135
  } else {
1136
- throw new Error(data.error || "Impossible d'effacer côté serveur.");
1137
  }
1138
  } catch (error) {
1139
- displayError(`Erreur lors de l'effacement du chat: ${error.message}`);
1140
  } finally {
1141
  e.target.querySelector('button').innerHTML = originalButtonText;
1142
  e.target.querySelector('button').disabled = false;
@@ -1144,51 +818,26 @@ IGNORE_WHEN_COPYING_END
1144
  }
1145
  });
1146
  });
1147
-
1148
- // Raccourcis clavier
1149
  promptInput.addEventListener('keydown', (e) => {
1150
- // Ignorer pendant la composition IME
1151
  if (isComposing) return;
1152
-
1153
- // Shift+Enter pour nouvelle ligne, Enter pour envoyer
1154
  if (e.key === 'Enter' && !e.shiftKey) {
1155
  e.preventDefault();
1156
- if (!sendButton.disabled) {
1157
- chatForm.dispatchEvent(new Event('submit'));
1158
- }
1159
  }
1160
  });
1161
 
1162
- // Support IME pour les langues asiatiques
1163
- promptInput.addEventListener('compositionstart', () => {
1164
- isComposing = true;
1165
- });
1166
-
1167
- promptInput.addEventListener('compositionend', () => {
1168
- isComposing = false;
1169
- });
1170
-
1171
- // Démarrer le chargement
1172
  loadChatHistory();
1173
-
1174
- // Activer l'ajustement automatique de la hauteur du textarea
1175
- const resizeInput = () => {
1176
- // Pour une implémentation future si on change l'input en textarea
1177
- };
1178
-
1179
- // Vérifier les préréglages du navigateur pour le thème sombre
1180
  const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
1181
  prefersDarkScheme.addEventListener('change', (e) => {
1182
- if (!localStorage.theme) { // Seulement si l'utilisateur n'a pas explicitement choisi
1183
- if (e.matches) {
1184
- document.documentElement.classList.add('dark');
1185
- } else {
1186
- document.documentElement.classList.remove('dark');
1187
- }
1188
  }
1189
  });
1190
  });
1191
  </script>
1192
-
1193
  </body>
1194
- </html>
 
 
1
  <!DOCTYPE html>
 
2
  <html lang="fr">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Mariam AI</title>
 
7
  <!-- Tailwind CSS via CDN -->
 
8
  <script src="https://cdn.tailwindcss.com?plugins=forms,typography"></script>
 
9
  <!-- Font Awesome pour les icônes -->
 
10
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
 
11
  <!-- Google Fonts -->
 
12
  <link rel="preconnect" href="https://fonts.googleapis.com">
13
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
 
15
  <!-- Favicon (Emoji amélioré) -->
 
16
  <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>">
 
17
  <script>
 
18
  tailwind.config = {
19
  darkMode: 'class',
20
  theme: {
 
77
  }
78
  }
79
  </script>
 
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%;
 
109
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
110
  transition: transform 0.2s ease, box-shadow 0.2s ease;
111
  }
 
112
  .message-bubble:hover {
113
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05), 0 1px 3px rgba(0, 0, 0, 0.1);
114
  }
 
115
  .dark .message-bubble {
116
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
117
  }
 
118
  .dark .message-bubble:hover {
119
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2), 0 1px 3px rgba(0, 0, 0, 0.3);
120
  }
 
121
  .user-message {
122
  border-bottom-right-radius: 0.125rem;
123
  align-self: flex-end;
124
  background: linear-gradient(to bottom right, #3b82f6, #2563eb);
125
  color: white;
126
  }
 
127
  .assistant-message {
128
  border-bottom-left-radius: 0.125rem;
129
  align-self: flex-start;
130
  }
 
131
  .dark .assistant-message {
132
  background-color: #1e293b;
133
  color: #e2e8f0;
134
  border-color: #334155;
135
  }
 
 
136
  .user-message::after {
137
  content: '';
138
  position: absolute;
 
143
  background: #2563eb;
144
  clip-path: polygon(0 0, 100% 0, 100% 100%);
145
  }
 
146
  .assistant-message::after {
147
  content: '';
148
  position: absolute;
 
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); }
 
 
 
 
 
 
160
  }
 
161
  @keyframes pulse-fade {
162
+ 0%, 100% { opacity: 0.5; }
163
+ 50% { opacity: 1; }
 
 
 
 
164
  }
 
165
  .typing-indicator {
166
  display: inline-flex;
167
  align-items: center;
168
  margin-left: 0.5rem;
169
  }
 
170
  .typing-dot {
171
  width: 0.5rem;
172
  height: 0.5rem;
 
175
  opacity: 0.7;
176
  margin: 0 0.1rem;
177
  }
178
+ .typing-dot:nth-child(1) { animation: pulse-fade 1.2s 0s infinite; }
179
+ .typing-dot:nth-child(2) { animation: pulse-fade 1.2s 0.2s infinite; }
180
+ .typing-dot:nth-child(3) { animation: pulse-fade 1.2s 0.4s infinite; }
 
 
 
 
 
 
 
 
 
 
 
181
  .dark body {
182
  background-color: #0f172a;
183
  color: #e2e8f0;
184
  }
185
+ .tooltip { position: relative; }
 
 
 
 
 
186
  .tooltip .tooltip-text {
187
  visibility: hidden;
188
  width: max-content;
 
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
  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
  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
  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;
240
  display: block;
241
  object-fit: cover;
242
  }
 
 
243
  .chip {
244
  display: inline-flex;
245
  align-items: center;
 
251
  color: #0369a1;
252
  transition: all 0.2s ease;
253
  }
254
+ .chip .chip-icon { margin-right: 0.25rem; }
 
 
 
 
255
  .chip .chip-close {
256
  margin-left: 0.25rem;
257
  cursor: pointer;
258
  opacity: 0.7;
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;
 
278
  transition: .4s;
279
  border-radius: 1.25rem;
280
  }
 
281
  .toggle-slider:before {
282
  position: absolute;
283
  content: "";
 
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;
300
  }
 
301
  .chat-input:focus {
302
  border-color: #38bdf8;
303
  box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2);
304
  }
 
305
  .dark .chat-input {
306
  background-color: #1e293b;
307
  color: #f1f5f9;
308
  border-color: #334155;
309
  }
 
310
  .dark .chat-input:focus {
311
  border-color: #38bdf8;
312
  box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2);
313
  }
 
 
314
  pre {
315
  position: relative;
316
  background-color: #f8fafc;
 
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;
326
  }
 
 
327
  .code-copy-btn {
328
  position: absolute;
329
  top: 0.5rem;
 
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
  </style>
 
337
  </head>
338
  <body class="bg-gray-50 text-gray-900 antialiased">
339
  <!-- Header -->
340
  <header class="bg-gradient-to-r from-primary-600 to-primary-800 text-white py-3 px-4 shadow-md sticky top-0 z-10">
341
  <div class="max-w-4xl mx-auto flex justify-between items-center">
342
+ <!-- Logo & Titre -->
343
  <div class="flex items-center space-x-2">
344
+ <img src="https://mariam-241.vercel.app/static/image/logoboma.png" alt="Logo Mariam AI" class="h-8 sm:h-10 object-contain">
345
  <h1 class="text-xl font-bold">Mariam AI</h1>
346
  </div>
347
+ <!-- Actions -->
348
+ <div class="flex items-center space-x-2 sm:space-x-4">
349
+ <button id="theme-toggle" class="p-2 rounded-full hover:bg-primary-700/50 transition-colors duration-200 tooltip" aria-label="Changer de thème">
350
+ <i class="fa-solid fa-moon dark:hidden"></i>
351
+ <i class="fa-solid fa-sun hidden dark:inline"></i>
352
+ <span class="tooltip-text">Mode clair/sombre</span>
353
+ </button>
354
+ <form action="/clear" method="POST" id="clear-form">
355
+ <button type="submit" class="flex items-center bg-red-500 hover:bg-red-600 text-white text-xs font-semibold py-1.5 px-3 rounded-full transition duration-200 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-opacity-75 tooltip">
356
+ <i class="fa-solid fa-trash-can mr-1.5"></i>
357
+ <span class="hidden sm:inline">Effacer</span>
358
+ <span class="tooltip-text">Effacer la conversation</span>
359
+ </button>
360
+ </form>
361
+ </div>
362
+ </div>
 
 
 
 
 
363
  </header>
 
364
  <!-- Main Container -->
 
365
  <main class="max-w-4xl mx-auto chat-layout">
366
+ <!-- Conteneur des messages -->
367
  <section id="chat-messages" class="flex flex-col space-y-6 p-4 overflow-y-auto">
368
+ <!-- Chargement de l'historique -->
369
  <div id="history-loading" class="text-center py-10">
370
  <div class="inline-flex items-center px-4 py-2 bg-primary-50 text-primary-700 rounded-lg dark:bg-primary-900/30 dark:text-primary-300">
371
  <svg class="animate-spin h-5 w-5 mr-3" viewBox="0 0 24 24">
 
375
  <span>Chargement de la conversation...</span>
376
  </div>
377
  </div>
378
+ <!-- Indicateur de chargement pour les réponses -->
379
+ <div id="loading-indicator" class="flex items-start space-x-2 hidden">
380
+ <div class="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center dark:bg-primary-900/50">
381
+ <span class="text-lg">✨</span>
382
+ </div>
383
+ <div class="message-bubble assistant-message bg-secondary-50 text-secondary-900 border border-secondary-200 flex items-center">
384
+ <span>Mariam réfléchit</span>
385
+ <div class="typing-indicator">
386
+ <span class="typing-dot"></span>
387
+ <span class="typing-dot"></span>
388
+ <span class="typing-dot"></span>
389
+ </div>
390
+ </div>
391
  </div>
392
+ </section>
393
+ <!-- Conteneur du bas -->
394
+ <div class="border-t border-gray-200 dark:border-gray-700">
395
+ <!-- Zone d'erreur -->
396
+ <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">
397
+ <div class="flex">
398
+ <div class="flex-shrink-0">
399
+ <i class="fa-solid fa-circle-exclamation"></i>
400
+ </div>
401
+ <div class="ml-3">
402
+ <p class="text-sm font-medium" id="error-text">Le message d'erreur détaillé ira ici.</p>
403
+ </div>
404
+ <button class="ml-auto" id="dismiss-error">
405
+ <i class="fa-solid fa-xmark"></i>
406
+ </button>
407
+ </div>
408
  </div>
409
+ <!-- Zone de prévisualisation -->
410
+ <div id="preview-area" class="px-4 py-2 bg-gray-50 dark:bg-gray-800/50 hidden">
411
+ <div id="file-preview" class="hidden"></div>
412
  </div>
413
+ <!-- Barre d'options -->
414
+ <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">
415
+ <div class="flex items-center space-x-4">
416
+ <label class="flex items-center cursor-pointer tooltip">
417
+ <span class="mr-2 text-xs sm:text-sm font-medium">
418
+ <i class="fa-solid fa-globe mr-1.5"></i>
419
+ <span class="hidden sm:inline">Recherche Web</span>
420
+ </span>
421
+ <span class="toggle-switch">
422
+ <input type="checkbox" id="web_search_toggle" name="web_search" value="true">
423
+ <span class="toggle-slider"></span>
424
+ </span>
425
+ <span class="tooltip-text">Activer la recherche web pour Mariam</span>
426
+ </label>
427
+ <label class="flex items-center cursor-pointer tooltip">
428
+ <span class="mr-2 text-xs sm:text-sm font-medium text-accent-700 dark:text-accent-300">
429
+ <i class="fa-solid fa-brain mr-1.5"></i>
430
+ <span class="hidden sm:inline">Avancé</span>
431
+ <span id="advanced-cooldown-timer" class="text-xs ml-1 hidden"></span>
432
+ </span>
433
+ <span class="toggle-switch">
434
+ <input type="checkbox" id="advanced_reasoning_toggle" name="advanced_reasoning" value="true">
435
+ <span class="toggle-slider"></span>
436
+ </span>
437
+ <span class="tooltip-text">Activer le raisonnement avancé (1 fois/min)</span>
438
+ </label>
439
+ </div>
440
+ <div>
441
+ <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">
442
+ <i class="fa-solid fa-paperclip"></i>
443
+ <span class="ml-1.5 hidden sm:inline">Fichier</span>
444
+ <input type="file" id="file_upload" name="file" class="hidden" accept=".txt,.pdf,.png,.jpg,.jpeg">
445
+ <span class="tooltip-text">Joindre un fichier (txt, pdf, image)</span>
446
+ </label>
447
+ <div id="file-chip" class="chip mt-2 hidden">
448
+ <i class="fa-solid fa-file chip-icon"></i>
449
+ <span id="file-name" class="truncate max-w-[120px]"></span>
450
+ <i id="clear-file" class="fa-solid fa-xmark chip-close"></i>
451
+ </div>
452
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  </div>
454
+ <!-- Formulaire de chat -->
455
+ <form id="chat-form" class="p-3 sm:p-4 bg-white dark:bg-gray-900">
456
+ <div class="relative">
457
+ <input
458
+ type="text"
459
+ id="prompt"
460
+ name="prompt"
461
+ class="chat-input w-full pl-4 pr-12 py-3 rounded-full border focus:outline-none text-sm sm:text-base"
462
+ placeholder="Posez votre question à Mariam..."
463
+ autocomplete="off">
464
+ <button
465
+ type="submit"
466
+ id="send-button"
467
+ class="absolute right-2 top-1/2 transform -translate-y-1/2 bg-primary-500 hover:bg-primary-600 disabled:bg-primary-300 text-white rounded-full p-2 transition focus:outline-none focus:ring-2 focus:ring-primary-400"
468
+ title="Envoyer le message">
469
+ <i class="fa-solid fa-paper-plane"></i>
470
+ </button>
471
+ </div>
472
+ <div class="text-xs text-center mt-2 text-gray-400 dark:text-gray-500">
473
+ 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
474
+ </div>
475
+ </form>
476
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
  </main>
 
478
  <!-- Scripts -->
 
479
  <script>
480
  document.addEventListener('DOMContentLoaded', () => {
481
+ // Sélection des éléments et initialisation des variables
482
  const chatForm = document.getElementById('chat-form');
483
  const promptInput = document.getElementById('prompt');
484
  const chatMessages = document.getElementById('chat-messages');
 
499
  const advancedToggle = document.getElementById('advanced_reasoning_toggle');
500
  const advancedCooldownTimerSpan = document.getElementById('advanced-cooldown-timer');
501
  const themeToggleBtn = document.getElementById('theme-toggle');
 
 
502
  const API_CHAT_ENDPOINT = '/api/chat';
503
  const API_HISTORY_ENDPOINT = '/api/history';
504
  const CLEAR_ENDPOINT = '/clear';
505
+ const COOLDOWN_DURATION = 60 * 1000;
 
 
 
 
506
  let advancedToggleCooldownEndTime = 0;
507
+ let isComposing = false;
508
 
509
+ // Gestion du thème
510
  function initializeTheme() {
 
511
  if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
512
  document.documentElement.classList.add('dark');
513
  } else {
514
  document.documentElement.classList.remove('dark');
515
  }
516
  }
 
517
  function toggleTheme() {
518
  if (document.documentElement.classList.contains('dark')) {
519
  document.documentElement.classList.remove('dark');
 
523
  localStorage.theme = 'dark';
524
  }
525
  }
 
526
  themeToggleBtn.addEventListener('click', toggleTheme);
527
  initializeTheme();
528
 
529
+ // Défilement vers le bas
530
  function scrollToBottom(smooth = true) {
531
  setTimeout(() => {
532
+ chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: smooth ? 'smooth' : 'auto' });
 
 
 
533
  }, 50);
534
  }
535
+
536
+ // Affichage du chargement
537
  function showLoading(show) {
538
  if (show) {
539
  loadingIndicator.classList.remove('hidden');
 
542
  } else {
543
  loadingIndicator.classList.add('hidden');
544
  }
 
545
  sendButton.disabled = show;
546
  promptInput.disabled = show;
547
  fileUpload.disabled = show;
548
  clearFileButton.disabled = show;
549
  }
550
+
551
+ // Affichage des erreurs
552
  function displayError(message) {
553
  errorTextP.textContent = message || "Une erreur inconnue est survenue.";
554
  errorMessageDiv.classList.remove('hidden');
555
  errorMessageDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
556
  }
557
+ dismissErrorBtn.addEventListener('click', () => { errorMessageDiv.classList.add('hidden'); });
558
 
559
+ // Ajout d'un message dans le chat
 
 
 
 
 
560
  function addMessageToChat(role, content, isHtml = false) {
561
  errorMessageDiv.classList.add('hidden');
 
562
  const messageWrapper = document.createElement('div');
563
  messageWrapper.classList.add('flex', role === 'user' ? 'justify-end' : 'justify-start');
 
564
  let html = '';
 
565
  if (role === 'user') {
 
566
  html = `
567
  <div class="flex items-start space-x-2 max-w-[85%]">
568
  <div class="message-bubble user-message">
 
571
  </div>
572
  `;
573
  } else {
 
574
  html = `
575
  <div class="flex items-start space-x-2 max-w-[85%]">
576
  <div class="w-8 h-8 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0 dark:bg-primary-900/50">
 
587
  </div>
588
  `;
589
  }
 
590
  messageWrapper.innerHTML = html;
591
  chatMessages.insertBefore(messageWrapper, loadingIndicator);
592
+ scrollToBottom();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
  }
 
 
594
  function escapeHtml(unsafe) {
595
+ return unsafe.replace(/&/g, "&amp;")
596
+ .replace(/</g, "&lt;")
597
+ .replace(/>/g, "&gt;")
598
+ .replace(/"/g, "&quot;")
599
+ .replace(/'/g, "&#039;");
 
600
  }
601
+
602
  // Gestion du cooldown du mode avancé
603
  function startAdvancedCooldownTimer() {
604
  advancedToggle.disabled = true;
605
  advancedToggleCooldownEndTime = Date.now() + COOLDOWN_DURATION;
 
606
  const updateTimer = () => {
607
  const now = Date.now();
608
  if (now >= advancedToggleCooldownEndTime) {
 
616
  advancedCooldownTimerSpan.classList.remove('hidden');
617
  }
618
  };
 
619
  const intervalId = setInterval(updateTimer, 1000);
620
  updateTimer();
621
  }
622
+
623
+ // Chargement de l'historique du chat
624
  async function loadChatHistory() {
625
  historyLoadingIndicator.style.display = 'block';
626
  try {
627
  const response = await fetch(API_HISTORY_ENDPOINT);
628
  if (!response.ok) {
629
  let errorMsg = `Erreur serveur (${response.status})`;
630
+ try { const errData = await response.json(); errorMsg = errData.error || errorMsg; } catch (e) {}
 
 
 
631
  throw new Error(errorMsg);
632
  }
 
633
  const data = await response.json();
634
  if (data.success && Array.isArray(data.history)) {
 
635
  chatMessages.innerHTML = '';
636
  chatMessages.appendChild(loadingIndicator);
637
  loadingIndicator.classList.add('hidden');
 
638
  if (data.history.length === 0) {
 
639
  addMessageToChat('assistant', "Bonjour ! Je suis Mariam, votre assistant IA. Comment puis-je vous aider aujourd'hui ?", true);
640
  } else {
 
641
  data.history.forEach(message => {
642
  const isAssistantHtml = message.role === 'assistant';
643
  addMessageToChat(message.role, message.text, isAssistantHtml);
644
  });
645
  }
 
646
  scrollToBottom(false);
647
  } else {
648
+ throw new Error(data.error || "Format de réponse invalide.");
649
  }
650
  } catch (error) {
651
  chatMessages.innerHTML = '';
 
657
  promptInput.focus();
658
  }
659
  }
660
+
661
  // Gestion des fichiers
662
  function clearFileInput() {
663
  fileUpload.value = '';
 
666
  previewArea.classList.add('hidden');
667
  filePreview.innerHTML = '';
668
  }
 
669
  fileUpload.addEventListener('change', () => {
670
  if (fileUpload.files.length > 0) {
671
  const file = fileUpload.files[0];
672
  const name = file.name;
 
 
673
  fileNameSpan.textContent = name;
674
  fileNameSpan.title = name;
675
  fileChip.classList.remove('hidden');
 
 
676
  if (file.type.startsWith('image/')) {
677
  const reader = new FileReader();
678
  reader.onload = (e) => {
 
686
  };
687
  reader.readAsDataURL(file);
688
  } else {
 
689
  filePreview.innerHTML = `
690
  <div class="flex items-center justify-center py-4">
691
  <div class="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg text-center">
 
701
  clearFileInput();
702
  }
703
  });
 
 
704
  function getFileIcon(fileType) {
705
  if (fileType.includes('pdf')) return 'fa-file-pdf';
706
  if (fileType.includes('text')) return 'fa-file-lines';
707
  return 'fa-file';
708
  }
 
 
709
  function formatFileSize(bytes) {
710
  if (bytes < 1024) return bytes + ' octets';
711
  if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' Ko';
712
  return (bytes / 1048576).toFixed(1) + ' Mo';
713
  }
714
+ clearFileButton.addEventListener('click', clearFileInput);
715
+
 
 
 
716
  // Soumission du formulaire
717
  chatForm.addEventListener('submit', async (e) => {
718
  e.preventDefault();
 
720
  const file = fileUpload.files[0];
721
  const useWebSearch = webSearchToggle.checked;
722
  const useAdvanced = advancedToggle.checked;
 
723
  if (!prompt && !file) {
724
  displayError("Veuillez entrer un message ou sélectionner un fichier.");
725
  promptInput.focus();
726
  return;
727
  }
 
728
  errorMessageDiv.classList.add('hidden');
 
 
729
  if (useAdvanced) {
730
  const now = Date.now();
731
  if (now < advancedToggleCooldownEndTime) {
 
734
  return;
735
  }
736
  }
 
 
737
  let userMessageText = prompt;
738
  if (file && file.name) {
739
  userMessageText = prompt ? `${prompt}` : `[Fichier joint: ${file.name}]`;
740
  }
 
741
  addMessageToChat('user', userMessageText);
 
 
742
  const formData = new FormData();
743
  formData.append('prompt', prompt);
744
  formData.append('web_search', useWebSearch);
745
+ if (file) { formData.append('file', file); }
 
 
 
 
746
  formData.append('advanced_reasoning', useAdvanced);
 
 
747
  showLoading(true);
748
  promptInput.value = '';
749
  clearFileInput();
 
 
750
  advancedToggle.checked = false;
751
  if (useAdvanced) startAdvancedCooldownTimer();
 
752
  try {
753
+ const response = await fetch(API_CHAT_ENDPOINT, { method: 'POST', body: formData });
 
 
 
 
754
  const data = await response.json();
755
+ if (!response.ok) throw new Error(data.error || `Erreur serveur: ${response.status}`);
 
 
 
 
756
  if (data.success && data.message) {
757
  addMessageToChat('assistant', data.message, true);
758
  } else {
759
+ throw new Error(data.error || "Réponse invalide du serveur.");
760
  }
761
  } catch (error) {
762
  displayError(error.message);
 
765
  promptInput.focus();
766
  }
767
  });
768
+
769
  // Effacement de la conversation
770
  clearForm.addEventListener('submit', async (e) => {
771
  e.preventDefault();
 
 
772
  const confirmDialog = document.createElement('div');
773
  confirmDialog.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
774
  confirmDialog.innerHTML = `
 
785
  </div>
786
  </div>
787
  `;
 
788
  document.body.appendChild(confirmDialog);
789
  document.body.classList.add('overflow-hidden');
 
 
790
  document.getElementById('cancel-clear').addEventListener('click', () => {
791
  document.body.removeChild(confirmDialog);
792
  document.body.classList.remove('overflow-hidden');
793
  });
 
794
  document.getElementById('confirm-clear').addEventListener('click', async () => {
795
  document.body.removeChild(confirmDialog);
796
  document.body.classList.remove('overflow-hidden');
 
797
  const originalButtonText = e.target.querySelector('button').innerHTML;
798
  e.target.querySelector('button').innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
799
  e.target.querySelector('button').disabled = true;
 
800
  try {
801
+ const response = await fetch(CLEAR_ENDPOINT, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } });
 
 
 
 
 
 
802
  const data = await response.json();
 
803
  if (response.ok && data.success) {
804
  chatMessages.innerHTML = '';
805
  chatMessages.appendChild(loadingIndicator);
806
  loadingIndicator.classList.add('hidden');
 
807
  addMessageToChat('assistant', "Conversation effacée. Comment puis-je vous aider ?", true);
808
  errorMessageDiv.classList.add('hidden');
809
  } else {
810
+ throw new Error(data.error || "Impossible d'effacer la conversation.");
811
  }
812
  } catch (error) {
813
+ displayError(`Erreur lors de l'effacement: ${error.message}`);
814
  } finally {
815
  e.target.querySelector('button').innerHTML = originalButtonText;
816
  e.target.querySelector('button').disabled = false;
 
818
  }
819
  });
820
  });
821
+
 
822
  promptInput.addEventListener('keydown', (e) => {
 
823
  if (isComposing) return;
 
 
824
  if (e.key === 'Enter' && !e.shiftKey) {
825
  e.preventDefault();
826
+ if (!sendButton.disabled) chatForm.dispatchEvent(new Event('submit'));
 
 
827
  }
828
  });
829
 
830
+ promptInput.addEventListener('compositionstart', () => { isComposing = true; });
831
+ promptInput.addEventListener('compositionend', () => { isComposing = false; });
 
 
 
 
 
 
 
 
832
  loadChatHistory();
 
 
 
 
 
 
 
833
  const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
834
  prefersDarkScheme.addEventListener('change', (e) => {
835
+ if (!localStorage.theme) {
836
+ if (e.matches) { document.documentElement.classList.add('dark'); }
837
+ else { document.documentElement.classList.remove('dark'); }
 
 
 
838
  }
839
  });
840
  });
841
  </script>
 
842
  </body>
843
+ </html>