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

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +862 -510
templates/index.html CHANGED
@@ -1,4 +1,6 @@
 
1
  <!DOCTYPE html>
 
2
  <html lang="fr">
3
  <head>
4
  <meta charset="UTF-8">
@@ -6,17 +8,21 @@
6
  <title>Mariam AI</title>
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>
17
  <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">
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>
@@ -30,31 +36,53 @@
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,175 +92,413 @@
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">
@@ -240,31 +506,33 @@
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>
259
- </form>
260
- </div>
261
- </div>
 
262
  </header>
263
 
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,126 +540,133 @@
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>
288
- <span class="typing-dot"></span>
289
- <span class="typing-dot"></span>
290
- </div>
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>
342
- <span class="toggle-switch">
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>
393
 
394
  <!-- Scripts -->
 
395
  <script>
396
  document.addEventListener('DOMContentLoaded', () => {
397
  // Elements
@@ -423,157 +698,171 @@
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,7 +870,6 @@
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,117 +877,127 @@
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,135 +1006,189 @@
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
  });
840
  </script>
 
841
  </body>
842
- </html>
 
1
+
2
  <!DOCTYPE html>
3
+
4
  <html lang="fr">
5
  <head>
6
  <meta charset="UTF-8">
 
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>
 
36
  mono: ['"JetBrains Mono"', 'monospace'],
37
  },
38
  colors: {
39
+ primary: {
40
+ 50: '#f0f9ff',
41
+ 100: '#e0f2fe',
42
+ 200: '#bae6fd',
43
+ 300: '#7dd3fc',
44
+ 400: '#38bdf8',
45
+ 500: '#0ea5e9',
46
+ 600: '#0284c7',
47
+ 700: '#0369a1',
48
+ 800: '#075985',
49
+ 900: '#0c4a6e',
50
+ },
51
+ secondary: {
52
+ 50: '#f8fafc',
53
+ 100: '#f1f5f9',
54
+ 200: '#e2e8f0',
55
+ 300: '#cbd5e1',
56
+ 400: '#94a3b8',
57
+ 500: '#64748b',
58
+ 600: '#475569',
59
+ 700: '#334155',
60
+ 800: '#1e293b',
61
+ 900: '#0f172a',
62
+ },
63
+ accent: {
64
+ 50: '#fdf4ff',
65
+ 100: '#fae8ff',
66
+ 200: '#f5d0fe',
67
+ 300: '#f0abfc',
68
+ 400: '#e879f9',
69
+ 500: '#d946ef',
70
+ 600: '#c026d3',
71
+ 700: '#a21caf',
72
+ 800: '#86198f',
73
+ 900: '#701a75',
74
+ }
75
  },
76
  animation: {
77
  'bounce-slow': 'bounce 2s infinite',
78
  'pulse-slow': 'pulse 3s infinite',
79
  'typing': 'typing 1.2s steps(3) infinite',
 
 
80
  },
81
  keyframes: {
82
  typing: {
83
  '0%': { width: '0.15em' },
84
  '50%': { width: '0.7em' },
85
  '100%': { width: '0.15em' },
 
 
 
 
 
 
 
 
 
86
  }
87
  }
88
  }
 
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%;
142
  border-radius: 1rem;
143
+ padding: 0.875rem 1rem;
144
  line-height: 1.5;
145
  animation: message-fade-in 0.3s ease-out;
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;
184
+ bottom: -0.5rem;
185
+ right: 0.125rem;
186
+ width: 0.75rem;
187
+ height: 0.75rem;
188
+ background: #2563eb;
189
+ clip-path: polygon(0 0, 100% 0, 100% 100%);
190
+ }
191
+
192
+ .assistant-message::after {
193
+ content: '';
194
+ position: absolute;
195
+ bottom: -0.5rem;
196
+ left: 0.125rem;
197
+ width: 0.75rem;
198
+ height: 0.75rem;
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;
237
+ border-radius: 50%;
238
+ background-color: currentColor;
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;
269
+ max-width: 200px;
270
+ background-color: #1e293b;
271
+ color: #f8fafc;
272
+ text-align: center;
273
+ border-radius: 6px;
274
+ padding: 0.375rem 0.625rem;
275
+ position: absolute;
276
+ z-index: 1;
277
+ bottom: 125%;
278
+ left: 50%;
279
+ transform: translateX(-50%);
280
+ opacity: 0;
281
+ transition: opacity 0.3s, visibility 0.3s;
282
+ font-size: 0.75rem;
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;
295
+ top: 100%;
296
+ left: 50%;
297
+ margin-left: -5px;
298
+ border-width: 5px;
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;
316
+ right: 0.5rem;
317
+ opacity: 0;
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;
330
+ position: relative;
331
+ overflow: hidden;
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;
351
+ background: #e0f2fe;
352
+ border-radius: 9999px;
353
+ padding: 0.25rem 0.75rem;
354
+ font-size: 0.75rem;
355
+ font-weight: 500;
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;
397
+ top: 0;
398
+ left: 0;
399
+ right: 0;
400
+ bottom: 0;
401
+ background-color: #cbd5e1;
402
+ transition: .4s;
403
+ border-radius: 1.25rem;
404
  }
405
+
406
  .toggle-slider:before {
407
+ position: absolute;
408
+ content: "";
409
+ height: 0.875rem;
410
+ width: 0.875rem;
411
+ left: 0.25rem;
412
+ bottom: 0.1875rem;
413
+ background-color: white;
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;
464
+ border-radius: 0.5rem;
465
+ margin: 1rem 0;
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;
484
+ right: 0.5rem;
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">
 
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">
 
540
  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
541
  <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>
542
  </svg>
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
 
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');
719
+ localStorage.theme = 'light';
720
+ } else {
721
+ document.documentElement.classList.add('dark');
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');
743
+ chatMessages.appendChild(loadingIndicator);
744
+ scrollToBottom();
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">
781
+ <p class="text-sm sm:text-base whitespace-pre-wrap break-words">${escapeHtml(content)}</p>
782
+ </div>
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">
790
+ <span class="text-lg">✨</span>
791
+ </div>
792
+ <div class="message-bubble assistant-message bg-secondary-50 text-secondary-900 border border-secondary-200 relative">
793
+ <div class="prose prose-sm sm:prose-base max-w-none dark:prose-invert">
794
+ ${isHtml ? content : escapeHtml(content)}
795
+ </div>
796
+ <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">
797
+ <i class="fa-regular fa-copy mr-1"></i> Copier
798
+ </button>
799
  </div>
 
 
 
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();
 
870
  clearInterval(intervalId);
871
  advancedCooldownTimerSpan.classList.add('hidden');
872
  advancedToggle.disabled = false;
 
873
  advancedToggleCooldownEndTime = 0;
874
  } else {
875
  const remainingSeconds = Math.ceil((advancedToggleCooldownEndTime - now) / 1000);
 
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 = '';
923
+ chatMessages.appendChild(loadingIndicator);
924
+ loadingIndicator.classList.add('hidden');
925
+ displayError(`Impossible de charger l'historique: ${error.message}`);
926
  } finally {
927
+ historyLoadingIndicator.remove();
928
+ promptInput.focus();
 
 
929
  }
930
  }
931
 
932
+ // Gestion des fichiers
933
  function clearFileInput() {
934
+ fileUpload.value = '';
935
  fileChip.classList.add('hidden');
936
+ filePreview.classList.add('hidden');
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) => {
955
  filePreview.innerHTML = `
956
+ <div class="file-preview">
957
+ <img src="${e.target.result}" alt="Prévisualisation de l'image">
958
+ </div>
959
+ `;
960
+ filePreview.classList.remove('hidden');
961
+ previewArea.classList.remove('hidden');
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">
969
+ <i class="fa-solid ${getFileIcon(file.type)} text-3xl text-gray-600 dark:text-gray-300 mb-2"></i>
970
+ <p class="text-sm text-gray-500 dark:text-gray-400">${formatFileSize(file.size)}</p>
971
+ </div>
972
+ </div>
973
+ `;
974
+ filePreview.classList.remove('hidden');
975
+ previewArea.classList.remove('hidden');
976
  }
 
977
  } else {
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();
1003
  const prompt = promptInput.value.trim();
 
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) {
1020
  const remainingSeconds = Math.ceil((advancedToggleCooldownEndTime - now) / 1000);
1021
+ displayError(`Le raisonnement avancé est disponible dans ${remainingSeconds} seconde(s).`);
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);
 
1073
  } finally {
1074
  showLoading(false);
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 = `
1087
+ <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]">
1088
+ <h3 class="text-lg font-semibold mb-3 text-gray-900 dark:text-gray-100">Confirmer l'effacement</h3>
1089
+ <p class="text-gray-600 dark:text-gray-300 mb-4">Êtes-vous sûr de vouloir effacer toute la conversation ?</p>
1090
+ <div class="flex justify-end space-x-3">
1091
+ <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">
1092
+ Annuler
1093
+ </button>
1094
+ <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">
1095
+ Effacer
1096
+ </button>
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;
1143
+ promptInput.focus();
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>