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

Update templates/index.html

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