Docfile commited on
Commit
92666fa
·
verified ·
1 Parent(s): 525bd03

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +568 -385
templates/index.html CHANGED
@@ -3,442 +3,625 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Admin - Conversations</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <script>
9
- tailwind.config = {
10
- darkMode: 'class',
11
- theme: {
12
- extend: {
13
- colors: {
14
- dark: {
15
- 50: '#f8fafc',
16
- 100: '#f1f5f9',
17
- 200: '#e2e8f0',
18
- 300: '#cbd5e1',
19
- 400: '#94a3b8',
20
- 500: '#64748b',
21
- 600: '#475569',
22
- 700: '#334155',
23
- 800: '#1e293b',
24
- 900: '#0f172a',
25
- }
26
- }
27
- }
28
- }
29
- }
30
- </script>
31
  <style>
32
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
33
- body { font-family: 'Inter', sans-serif; }
34
- .conversation-card {
35
- transition: all 0.3s ease;
36
- }
37
- .conversation-card:hover {
38
- transform: translateY(-2px);
39
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
40
- }
41
- .message-bubble {
42
- animation: fadeInUp 0.3s ease-out;
43
- }
44
- @keyframes fadeInUp {
45
- from {
46
- opacity: 0;
47
- transform: translateY(10px);
48
- }
49
- to {
50
- opacity: 1;
51
- transform: translateY(0);
52
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  }
54
- .loading-dots {
 
 
55
  display: inline-block;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  }
57
- .loading-dots::after {
58
- content: '';
59
- animation: dots 2s infinite;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
- @keyframes dots {
62
- 0%, 20% { content: '.'; }
63
- 40% { content: '..'; }
64
- 60%, 100% { content: '...'; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  }
66
  </style>
67
  </head>
68
- <body class="bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 min-h-screen text-white">
69
- <!-- Header -->
70
- <header class="bg-slate-800/50 backdrop-blur-sm border-b border-slate-700/50 sticky top-0 z-50">
71
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
72
- <div class="flex items-center justify-between h-16">
73
- <div class="flex items-center space-x-3">
74
- <div class="w-8 h-8 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
75
- <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
76
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.031 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
77
- </svg>
78
- </div>
79
- <h1 class="text-xl font-bold text-white">Admin Dashboard</h1>
80
- </div>
81
-
82
- <div class="flex items-center space-x-4">
83
- <button id="refreshBtn" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors duration-200 flex items-center space-x-2">
84
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
85
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
86
- </svg>
87
- <span>Actualiser</span>
88
- </button>
89
- </div>
90
- </div>
91
- </div>
92
- </header>
93
-
94
- <!-- Stats Bar -->
95
- <div class="bg-slate-800/30 border-b border-slate-700/30">
96
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
97
- <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
98
- <div class="bg-gradient-to-r from-blue-500/20 to-blue-600/20 rounded-lg p-4 border border-blue-500/30">
99
- <div class="flex items-center">
100
- <div class="p-2 bg-blue-500/30 rounded-lg">
101
- <svg class="w-6 h-6 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
102
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
103
- </svg>
104
- </div>
105
- <div class="ml-4">
106
- <p class="text-sm text-slate-400">Conversations</p>
107
- <p class="text-2xl font-bold text-white" id="totalConversations">0</p>
108
- </div>
109
- </div>
110
- </div>
111
-
112
- <div class="bg-gradient-to-r from-green-500/20 to-green-600/20 rounded-lg p-4 border border-green-500/30">
113
- <div class="flex items-center">
114
- <div class="p-2 bg-green-500/30 rounded-lg">
115
- <svg class="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
116
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"/>
117
- </svg>
118
- </div>
119
- <div class="ml-4">
120
- <p class="text-sm text-slate-400">Messages</p>
121
- <p class="text-2xl font-bold text-white" id="totalMessages">0</p>
122
- </div>
123
- </div>
124
- </div>
125
-
126
- <div class="bg-gradient-to-r from-purple-500/20 to-purple-600/20 rounded-lg p-4 border border-purple-500/30">
127
- <div class="flex items-center">
128
- <div class="p-2 bg-purple-500/30 rounded-lg">
129
- <svg class="w-6 h-6 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
130
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
131
- </svg>
132
- </div>
133
- <div class="ml-4">
134
- <p class="text-sm text-slate-400">Actives</p>
135
- <p class="text-2xl font-bold text-white" id="activeConversations">0</p>
136
- </div>
137
- </div>
138
- </div>
139
-
140
- <div class="bg-gradient-to-r from-orange-500/20 to-orange-600/20 rounded-lg p-4 border border-orange-500/30">
141
- <div class="flex items-center">
142
- <div class="p-2 bg-orange-500/30 rounded-lg">
143
- <svg class="w-6 h-6 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
144
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2a2 2 0 00-2-2z"/>
145
- </svg>
146
- </div>
147
- <div class="ml-4">
148
- <p class="text-sm text-slate-400">Avec fichiers</p>
149
- <p class="text-2xl font-bold text-white" id="fileConversations">0</p>
150
- </div>
151
- </div>
152
  </div>
 
 
 
153
  </div>
154
  </div>
155
- </div>
156
 
157
- <!-- Main Content -->
158
- <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
159
- <!-- Search & Filter -->
160
- <div class="mb-6">
161
- <div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
162
- <div class="relative flex-1 max-w-md">
163
- <svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
164
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
165
- </svg>
166
- <input
167
- type="text"
168
- id="searchInput"
169
- placeholder="Rechercher dans les conversations..."
170
- class="w-full pl-10 pr-4 py-2 bg-slate-800/50 border border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-slate-400"
171
- >
172
- </div>
173
 
174
- <div class="flex gap-2">
175
- <select id="sortSelect" class="px-4 py-2 bg-slate-800/50 border border-slate-600 rounded-lg text-white focus:ring-2 focus:ring-blue-500">
176
- <option value="newest">Plus récentes</option>
177
- <option value="oldest">Plus anciennes</option>
178
- <option value="messages">Plus de messages</option>
179
- </select>
180
- </div>
181
- </div>
182
- </div>
183
-
184
- <!-- Loading State -->
185
- <div id="loadingState" class="text-center py-12">
186
- <div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
187
- <p class="mt-4 text-slate-400">Chargement des conversations<span class="loading-dots"></span></p>
188
  </div>
189
 
190
- <!-- Empty State -->
191
- <div id="emptyState" class="text-center py-12 hidden">
192
- <div class="w-16 h-16 bg-slate-700 rounded-full flex items-center justify-center mx-auto mb-4">
193
- <svg class="w-8 h-8 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
194
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
195
- </svg>
196
  </div>
197
- <h3 class="text-lg font-medium text-white mb-2">Aucune conversation</h3>
198
- <p class="text-slate-400">Aucune conversation n'a été trouvée.</p>
199
  </div>
200
 
201
- <!-- Conversations Grid -->
202
- <div id="conversationsGrid" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6 hidden">
203
- <!-- Les conversations seront ajoutées ici dynamiquement -->
204
- </div>
205
-
206
- <!-- Modal pour voir une conversation -->
207
- <div id="conversationModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden">
208
- <div class="flex items-center justify-center min-h-screen p-4">
209
- <div class="bg-slate-800 rounded-2xl max-w-4xl w-full max-h-[80vh] overflow-hidden">
210
- <div class="flex items-center justify-between p-6 border-b border-slate-700">
211
- <h2 class="text-xl font-bold text-white" id="modalTitle">Conversation</h2>
212
- <button id="closeModal" class="text-slate-400 hover:text-white transition-colors">
213
- <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
214
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
215
- </svg>
216
- </button>
217
- </div>
218
- <div class="p-6 overflow-y-auto max-h-96" id="modalContent">
219
- <!-- Contenu de la conversation -->
220
- </div>
221
  </div>
222
  </div>
223
  </div>
224
- </main>
225
 
 
226
  <script>
227
- let conversations = [];
228
- let filteredConversations = [];
229
-
230
- // Initialisation
231
- document.addEventListener('DOMContentLoaded', function() {
232
- loadConversations();
233
- setupEventListeners();
234
- });
235
 
236
- function setupEventListeners() {
237
- document.getElementById('refreshBtn').addEventListener('click', loadConversations);
238
- document.getElementById('searchInput').addEventListener('input', filterConversations);
239
- document.getElementById('sortSelect').addEventListener('change', sortConversations);
240
- document.getElementById('closeModal').addEventListener('click', closeModal);
241
-
242
- // Fermer modal avec Escape
243
- document.addEventListener('keydown', function(e) {
244
- if (e.key === 'Escape') closeModal();
245
- });
246
  }
247
 
248
- async function loadConversations() {
249
- showLoading();
250
-
251
- try {
252
- const response = await fetch('/admin/conversations');
253
- const data = await response.json();
254
-
255
- conversations = data.conversations || [];
256
- filteredConversations = [...conversations];
257
-
258
- updateStats(data.stats || {});
259
- displayConversations();
260
-
261
- } catch (error) {
262
- console.error('Erreur lors du chargement:', error);
263
- showError('Erreur lors du chargement des conversations');
264
  }
265
  }
266
 
267
- function updateStats(stats) {
268
- document.getElementById('totalConversations').textContent = stats.total || 0;
269
- document.getElementById('totalMessages').textContent = stats.totalMessages || 0;
270
- document.getElementById('activeConversations').textContent = stats.active || 0;
271
- document.getElementById('fileConversations').textContent = stats.withFiles || 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  }
273
 
274
- function displayConversations() {
275
- const grid = document.getElementById('conversationsGrid');
276
- const loading = document.getElementById('loadingState');
277
- const empty = document.getElementById('emptyState');
 
 
 
 
 
 
 
 
 
 
278
 
279
- loading.classList.add('hidden');
 
280
 
281
- if (filteredConversations.length === 0) {
282
- grid.classList.add('hidden');
283
- empty.classList.remove('hidden');
284
- return;
285
  }
286
 
287
- empty.classList.add('hidden');
288
- grid.classList.remove('hidden');
289
 
290
- grid.innerHTML = filteredConversations.map(conv => createConversationCard(conv)).join('');
291
- }
292
 
293
- function createConversationCard(conv) {
294
- const lastMessage = conv.messages[conv.messages.length - 1] || {};
295
- const messageCount = conv.messages.length;
296
- const hasFiles = conv.messages.some(m => m.hasFile);
297
-
298
- const lastActivity = conv.lastActivity ?
299
- new Date(conv.lastActivity).toLocaleString('fr-FR', {
300
- day: '2-digit',
301
- month: '2-digit',
302
- year: 'numeric',
303
- hour: '2-digit',
304
- minute: '2-digit'
305
- }) : 'Inconnue';
306
-
307
- return `
308
- <div class="conversation-card bg-slate-800/50 rounded-xl border border-slate-700/50 p-6 cursor-pointer hover:border-slate-600"
309
- onclick="openConversation('${conv.id}')">
310
- <div class="flex items-start justify-between mb-4">
311
- <div class="flex items-center space-x-3">
312
- <div class="w-10 h-10 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
313
- <svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
314
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
315
- </svg>
316
- </div>
317
- <div>
318
- <h3 class="font-semibold text-white">${conv.id}</h3>
319
- <p class="text-sm text-slate-400">${messageCount} message${messageCount > 1 ? 's' : ''}</p>
320
- </div>
321
- </div>
322
-
323
- <div class="flex flex-col items-end space-y-2">
324
- ${hasFiles ? `
325
- <span class="px-2 py-1 bg-orange-500/20 text-orange-400 text-xs rounded-full border border-orange-500/30">
326
- 📎 Fichiers
327
- </span>
328
- ` : ''}
329
- <span class="px-2 py-1 bg-blue-500/20 text-blue-400 text-xs rounded-full border border-blue-500/30">
330
- ${conv.status || 'Active'}
331
- </span>
332
- </div>
333
- </div>
334
-
335
- <div class="mb-4">
336
- <p class="text-sm text-slate-300 line-clamp-2">
337
- ${lastMessage.content ?
338
- (lastMessage.content.length > 100 ?
339
- lastMessage.content.substring(0, 100) + '...' :
340
- lastMessage.content
341
- ) : 'Conversation vide'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  }
343
- </p>
344
- </div>
345
-
346
- <div class="flex items-center justify-between text-xs text-slate-500">
347
- <span>Dernière activité</span>
348
- <span>${lastActivity}</span>
349
- </div>
350
- </div>
351
- `;
 
 
 
 
352
  }
353
 
354
- function openConversation(conversationId) {
355
- const conv = conversations.find(c => c.id === conversationId);
356
- if (!conv) return;
357
-
358
- document.getElementById('modalTitle').textContent = `Conversation: ${conversationId}`;
359
-
360
- const modalContent = document.getElementById('modalContent');
361
- modalContent.innerHTML = conv.messages.map(msg => `
362
- <div class="message-bubble mb-4 ${msg.role === 'user' ? 'ml-8' : 'mr-8'}">
363
- <div class="flex items-start space-x-3">
364
- <div class="flex-shrink-0">
365
- <div class="w-8 h-8 rounded-full flex items-center justify-center ${
366
- msg.role === 'user'
367
- ? 'bg-blue-500/20 text-blue-400'
368
- : 'bg-green-500/20 text-green-400'
369
- }">
370
- ${msg.role === 'user' ? '👤' : '🤖'}
371
- </div>
372
- </div>
373
- <div class="flex-1">
374
- <div class="flex items-center space-x-2 mb-1">
375
- <span class="text-sm font-medium ${msg.role === 'user' ? 'text-blue-400' : 'text-green-400'}">
376
- ${msg.role === 'user' ? 'Utilisateur' : 'Assistant'}
377
- </span>
378
- ${msg.timestamp ? `
379
- <span class="text-xs text-slate-500">
380
- ${new Date(msg.timestamp).toLocaleString('fr-FR')}
381
- </span>
382
- ` : ''}
383
- ${msg.hasFile ? '<span class="text-xs bg-orange-500/20 text-orange-400 px-2 py-1 rounded">📎 Fichier</span>' : ''}
384
- </div>
385
- <div class="bg-slate-700/50 rounded-lg p-3 text-sm text-slate-200">
386
- ${msg.content || 'Message vide'}
387
- </div>
388
- </div>
389
- </div>
390
- </div>
391
- `).join('');
392
-
393
- document.getElementById('conversationModal').classList.remove('hidden');
394
  }
395
 
396
- function closeModal() {
397
- document.getElementById('conversationModal').classList.add('hidden');
 
 
 
 
 
 
398
  }
399
-
400
- function filterConversations() {
401
- const searchTerm = document.getElementById('searchInput').value.toLowerCase();
402
-
403
- filteredConversations = conversations.filter(conv => {
404
- return conv.id.toLowerCase().includes(searchTerm) ||
405
- conv.messages.some(msg =>
406
- msg.content && msg.content.toLowerCase().includes(searchTerm)
407
- );
408
- });
409
-
410
- displayConversations();
411
  }
412
 
413
- function sortConversations() {
414
- const sortBy = document.getElementById('sortSelect').value;
415
-
416
- filteredConversations.sort((a, b) => {
417
- switch(sortBy) {
418
- case 'newest':
419
- return new Date(b.lastActivity || 0) - new Date(a.lastActivity || 0);
420
- case 'oldest':
421
- return new Date(a.lastActivity || 0) - new Date(b.lastActivity || 0);
422
- case 'messages':
423
- return b.messages.length - a.messages.length;
424
- default:
425
- return 0;
426
- }
427
- });
428
-
429
- displayConversations();
430
  }
431
 
432
- function showLoading() {
433
- document.getElementById('loadingState').classList.remove('hidden');
434
- document.getElementById('conversationsGrid').classList.add('hidden');
435
- document.getElementById('emptyState').classList.add('hidden');
 
 
 
 
436
  }
437
 
 
438
  function showError(message) {
439
- // Vous pouvez implémenter une notification d'erreur ici
440
- console.error(message);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
  }
 
 
 
 
442
  </script>
443
  </body>
444
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title> AI Assistant - Flat Black & White</title>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  <style>
8
+ /* --- RESET ET CONFIGURATION GLOBALE --- */
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ html {
16
+ scroll-behavior: smooth;
17
+ }
18
+
19
+ body {
20
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
21
+ background-color: #121212; /* Fond sombre principal */
22
+ color: #f1f1f1; /* Couleur de texte par défaut */
23
+ min-height: 100vh;
24
+ display: flex;
25
+ justify-content: center;
26
+ align-items: center;
27
+ padding: 0; /* Pas de padding sur le body pour le mobile */
28
+ }
29
+
30
+ /* --- CONTENEUR PRINCIPAL DU CHAT --- */
31
+ .chat-container {
32
+ width: 100%;
33
+ max-width: 800px;
34
+ height: 100vh; /* Prend toute la hauteur par défaut (mobile-first) */
35
+ background-color: #1e1e1e; /* Fond du conteneur légèrement plus clair */
36
+ display: flex;
37
+ flex-direction: column;
38
+ overflow: hidden;
39
+ }
40
+
41
+ /* --- EN-TÊTE --- */
42
+ .header {
43
+ background-color: #1e1e1e;
44
+ color: #ffffff;
45
+ padding: 20px;
46
+ text-align: center;
47
+ border-bottom: 1px solid #333; /* Séparateur plat */
48
+ flex-shrink: 0;
49
+ }
50
+
51
+ .header h1 {
52
+ font-size: 22px;
53
+ font-weight: 600;
54
+ margin-bottom: 5px;
55
+ }
56
+
57
+ .header p {
58
+ font-size: 14px;
59
+ color: #aaa;
60
+ }
61
+
62
+ .controls {
63
+ display: flex;
64
+ justify-content: center;
65
+ align-items: center;
66
+ gap: 20px;
67
+ margin-top: 15px;
68
+ }
69
+
70
+ .thinking-toggle {
71
+ display: flex;
72
+ align-items: center;
73
+ gap: 10px;
74
+ font-size: 14px;
75
+ color: #ccc;
76
  }
77
+
78
+ .switch {
79
+ position: relative;
80
  display: inline-block;
81
+ width: 50px;
82
+ height: 28px;
83
+ }
84
+
85
+ .switch input { opacity: 0; width: 0; height: 0; }
86
+
87
+ .slider {
88
+ position: absolute;
89
+ cursor: pointer;
90
+ top: 0;
91
+ left: 0;
92
+ right: 0;
93
+ bottom: 0;
94
+ background-color: #444;
95
+ transition: .2s;
96
+ border-radius: 28px;
97
+ }
98
+
99
+ .slider:before {
100
+ position: absolute;
101
+ content: "";
102
+ height: 20px;
103
+ width: 20px;
104
+ left: 4px;
105
+ bottom: 4px;
106
+ background-color: #f1f1f1;
107
+ transition: .2s;
108
+ border-radius: 50%;
109
+ }
110
+
111
+ input:checked + .slider { background-color: #777; }
112
+ input:checked + .slider:before { transform: translateX(22px); }
113
+
114
+ .btn-reset {
115
+ background: transparent;
116
+ border: 1px solid #555;
117
+ color: #ccc;
118
+ padding: 8px 16px;
119
+ border-radius: 20px;
120
+ cursor: pointer;
121
+ font-size: 14px;
122
+ transition: background-color 0.2s ease;
123
+ }
124
+
125
+ .btn-reset:hover { background-color: #333; }
126
+
127
+ /* --- ZONE DES MESSAGES --- */
128
+ .messages {
129
+ flex: 1;
130
+ overflow-y: auto;
131
+ padding: 20px 15px;
132
+ display: flex;
133
+ flex-direction: column;
134
+ gap: 15px;
135
+ }
136
+
137
+ .message {
138
+ max-width: 85%;
139
+ padding: 12px 18px;
140
+ border-radius: 18px;
141
+ font-size: 16px;
142
+ line-height: 1.5;
143
+ animation: fadeIn 0.3s ease;
144
+ }
145
+
146
+ .message.user {
147
+ background-color: #3a3a3a; /* Couleur message utilisateur */
148
+ color: #f1f1f1;
149
+ align-self: flex-end;
150
+ border-bottom-right-radius: 4px;
151
+ }
152
+
153
+ .message.assistant {
154
+ background-color: #2c2c2c; /* Couleur message assistant */
155
+ color: #e0e0e0;
156
+ align-self: flex-start;
157
+ border-bottom-left-radius: 4px;
158
  }
159
+
160
+ .message ul { margin-top: 10px; padding-left: 20px; }
161
+
162
+ .thoughts {
163
+ background: #252525;
164
+ color: #aaa;
165
+ border-left: 3px solid #666; /* Indicateur visuel plat */
166
+ margin: 10px 0;
167
+ padding: 15px;
168
+ border-radius: 8px;
169
+ font-style: italic;
170
+ font-size: 14px;
171
+ }
172
+
173
+ .thoughts-header {
174
+ font-weight: 600;
175
+ margin-bottom: 8px;
176
+ display: flex;
177
+ align-items: center;
178
+ gap: 8px;
179
+ color: #ccc;
180
+ }
181
+
182
+ /* --- ZONE DE SAISIE --- */
183
+ .input-container {
184
+ padding: 15px;
185
+ background-color: #1e1e1e;
186
+ border-top: 1px solid #333;
187
+ flex-shrink: 0;
188
+ }
189
+
190
+ .input-row { display: flex; gap: 10px; align-items: flex-end; }
191
+ .input-wrapper { flex: 1; position: relative; }
192
+
193
+ .message-input {
194
+ width: 100%;
195
+ min-height: 48px;
196
+ padding: 12px 55px 12px 20px;
197
+ border: 1px solid #444;
198
+ border-radius: 24px;
199
+ font-size: 16px;
200
+ resize: none;
201
+ outline: none;
202
+ transition: border-color 0.2s ease;
203
+ font-family: inherit;
204
+ background-color: #2c2c2c;
205
+ color: #f1f1f1;
206
+ }
207
+
208
+ .message-input::placeholder { color: #777; }
209
+ .message-input:focus { border-color: #888; }
210
+
211
+ .send-button {
212
+ position: absolute;
213
+ right: 4px;
214
+ top: 50%;
215
+ transform: translateY(-50%);
216
+ background-color: #333;
217
+ border: none;
218
+ border-radius: 50%;
219
+ width: 40px;
220
+ height: 40px;
221
+ color: #f1f1f1;
222
+ cursor: pointer;
223
+ transition: background-color 0.2s ease;
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ font-size: 20px; /* Taille de l'icône */
228
+ }
229
+
230
+ .send-button:hover:not(:disabled) { background-color: #444; }
231
+ .send-button:disabled { background: #252525; color: #555; cursor: not-allowed; }
232
+
233
+ .file-input { display: none; }
234
+
235
+ .file-button {
236
+ background: #333;
237
+ border: none;
238
+ color: #f1f1f1;
239
+ height: 48px;
240
+ width: 48px;
241
+ border-radius: 50%;
242
+ cursor: pointer;
243
+ font-size: 20px; /* Taille de l'icône */
244
+ transition: background-color 0.2s ease;
245
+ flex-shrink: 0;
246
+ }
247
+
248
+ .file-button:hover { background-color: #444; }
249
+
250
+ .file-preview {
251
+ margin-bottom: 10px;
252
+ padding: 10px;
253
+ background: #2c2c2c;
254
+ border-radius: 10px;
255
+ font-size: 14px;
256
+ display: none;
257
+ }
258
+
259
+ /* --- INDICATEURS ET NOTIFICATIONS --- */
260
+ .typing-indicator {
261
+ display: none;
262
+ align-items: center;
263
+ gap: 8px;
264
+ color: #999;
265
+ font-style: italic;
266
+ padding: 0 15px 10px;
267
+ }
268
+
269
+ .typing-dots span {
270
+ width: 8px; height: 8px; border-radius: 50%;
271
+ background: #666;
272
+ animation: typing 1.4s infinite ease-in-out;
273
  }
274
+ .typing-dots span:nth-child(1) { animation-delay: 0s; }
275
+ .typing-dots span:nth-child(2) { animation-delay: 0.2s; }
276
+ .typing-dots span:nth-child(3) { animation-delay: 0.4s; }
277
+
278
+ .error {
279
+ background: #4d1c20; color: #ffacb6;
280
+ border-left: 3px solid #e53935;
281
+ padding: 15px; border-radius: 8px; margin: 10px 0;
282
+ }
283
+ .success {
284
+ background: #1a4331; color: #a6f1c4;
285
+ border-left: 3px solid #2e7d32;
286
+ padding: 15px; border-radius: 8px; margin: 10px 0;
287
+ }
288
+
289
+ /* --- STYLE DE LA SCROLLBAR (THÈME SOMBRE) --- */
290
+ .messages::-webkit-scrollbar { width: 8px; }
291
+ .messages::-webkit-scrollbar-track { background: #1e1e1e; }
292
+ .messages::-webkit-scrollbar-thumb { background: #555; border-radius: 4px; }
293
+ .messages::-webkit-scrollbar-thumb:hover { background: #777; }
294
+
295
+ /* --- ANIMATIONS --- */
296
+ @keyframes typing {
297
+ 0%, 60%, 100% { transform: scale(0.8); opacity: 0.5; }
298
+ 30% { transform: scale(1.2); opacity: 1; }
299
+ }
300
+
301
+ @keyframes fadeIn {
302
+ from { opacity: 0; transform: translateY(10px); }
303
+ to { opacity: 1; transform: translateY(0); }
304
+ }
305
+
306
+ /* --- RESPONSIVE DESIGN (pour les écrans plus larges) --- */
307
+ @media (min-width: 768px) {
308
+ body {
309
+ padding: 20px; /* Ajoute de l'espace autour sur grand écran */
310
+ }
311
+ .chat-container {
312
+ height: 90vh; /* Hauteur limitée sur grand écran */
313
+ border-radius: 12px;
314
+ border: 1px solid #333;
315
+ }
316
  }
317
  </style>
318
  </head>
319
+ <body>
320
+ <div class="chat-container">
321
+ <div class="header">
322
+ <h1>Mariam AI Assistant</h1>
323
+ <p>❤️</p>
324
+ <div class="controls">
325
+ <div class="thinking-toggle">
326
+ <span>réflexion Mode:</span>
327
+ <label class="switch">
328
+ <input type="checkbox" id="thinkingToggle" checked>
329
+ <span class="slider"></span>
330
+ </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  </div>
332
+ <button class="btn-reset" onclick="resetConversation()">
333
+ 🔄 Reset
334
+ </button>
335
  </div>
336
  </div>
 
337
 
338
+ <div class="messages" id="messages">
339
+ <div class="message assistant">
340
+ <div>👋 bonjour, sur quoi allons nous travailler aujourdhui ?</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
341
 
342
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  </div>
344
 
345
+ <div class="typing-indicator" id="typingIndicator">
346
+ <span>L'assistant réfléchit</span>
347
+ <div class="typing-dots">
348
+ <span></span>
349
+ <span></span>
350
+ <span></span>
351
  </div>
 
 
352
  </div>
353
 
354
+ <div class="input-container">
355
+ <div class="file-preview" id="filePreview"></div>
356
+ <div class="input-row">
357
+ <input type="file" id="fileInput" class="file-input" accept="image/*,video/*,.pdf,.txt,.csv,.json" onchange="handleFileSelect(event)">
358
+ <button class="file-button" onclick="document.getElementById('fileInput').click()" title="Joindre un fichier">
359
+ 📎
360
+ </button>
361
+ <div class="input-wrapper">
362
+ <textarea
363
+ id="messageInput"
364
+ class="message-input"
365
+ placeholder="Tapez votre message ici..."
366
+ rows="1"
367
+ onkeydown="handleKeyDown(event)"
368
+ oninput="autoResize(this)"
369
+ ></textarea>
370
+ <button class="send-button" id="sendButton" onclick="sendMessage()" title="Envoyer">
371
+
372
+ </button>
 
373
  </div>
374
  </div>
375
  </div>
376
+ </div>
377
 
378
+ <!-- SCRIPT ORIGINAL ET FONCTIONNEL -->
379
  <script>
380
+ let currentFile = null;
381
+ let conversationId = 'session_' + Date.now();
 
 
 
 
 
 
382
 
383
+ function autoResize(textarea) {
384
+ textarea.style.height = 'auto';
385
+ textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px';
 
 
 
 
 
 
 
386
  }
387
 
388
+ function handleKeyDown(event) {
389
+ if (event.key === 'Enter' && !event.shiftKey) {
390
+ event.preventDefault();
391
+ sendMessage();
 
 
 
 
 
 
 
 
 
 
 
 
392
  }
393
  }
394
 
395
+ function handleFileSelect(event) {
396
+ const file = event.target.files[0];
397
+ if (!file) return;
398
+
399
+ const filePreview = document.getElementById('filePreview');
400
+ const formData = new FormData();
401
+ formData.append('file', file);
402
+
403
+ fetch('/upload', {
404
+ method: 'POST',
405
+ body: formData
406
+ })
407
+ .then(response => response.json())
408
+ .then(data => {
409
+ if (data.success) {
410
+ currentFile = data;
411
+ filePreview.innerHTML = `
412
+ <div style="display: flex; justify-content: space-between; align-items: center;">
413
+ <span>📎 ${data.filename}</span>
414
+ <button onclick="removeFile()" style="background: #555; color: white; border: none; padding: 5px 10px; border-radius: 5px; cursor: pointer;">✕</button>
415
+ </div>
416
+ `;
417
+ filePreview.style.display = 'block';
418
+ } else {
419
+ showError('Erreur lors du téléchargement: ' + data.error);
420
+ }
421
+ })
422
+ .catch(error => {
423
+ showError('Erreur lors du téléchargement: ' + error.message);
424
+ });
425
  }
426
 
427
+ function removeFile() {
428
+ currentFile = null;
429
+ document.getElementById('filePreview').style.display = 'none';
430
+ document.getElementById('fileInput').value = '';
431
+ }
432
+
433
+ function sendMessage() {
434
+ const messageInput = document.getElementById('messageInput');
435
+ const message = messageInput.value.trim();
436
+
437
+ if (!message && !currentFile) return;
438
+
439
+ const sendButton = document.getElementById('sendButton');
440
+ const typingIndicator = document.getElementById('typingIndicator');
441
 
442
+ messageInput.disabled = true;
443
+ sendButton.disabled = true;
444
 
445
+ if (message) {
446
+ addMessage('user', message);
 
 
447
  }
448
 
449
+ typingIndicator.style.display = 'flex';
 
450
 
451
+ messageInput.value = '';
452
+ autoResize(messageInput);
453
 
454
+ const thinkingEnabled = document.getElementById('thinkingToggle').checked;
455
+ const endpoint = currentFile ? '/chat_with_file' : '/chat';
456
+ const payload = {
457
+ message: message || 'Analyse ce fichier',
458
+ thinking_enabled: thinkingEnabled,
459
+ conversation_id: conversationId
460
+ };
461
+
462
+ if (currentFile) {
463
+ payload.file_data = currentFile;
464
+ }
465
+
466
+ fetch(endpoint, {
467
+ method: 'POST',
468
+ headers: { 'Content-Type': 'application/json' },
469
+ body: JSON.stringify(payload)
470
+ })
471
+ .then(response => {
472
+ if (!response.ok) {
473
+ throw new Error(`Erreur réseau: ${response.status} ${response.statusText}`);
474
+ }
475
+
476
+ const reader = response.body.getReader();
477
+ const decoder = new TextDecoder();
478
+ let buffer = '';
479
+ let messageElement = null;
480
+ let thoughtsElement = null;
481
+
482
+ function processStream() {
483
+ return reader.read().then(({ done, value }) => {
484
+ if (done) {
485
+ // La fin du stream est gérée par le message 'end'
486
+ return;
487
+ }
488
+
489
+ buffer += decoder.decode(value, { stream: true });
490
+ const lines = buffer.split('\n');
491
+ buffer = lines.pop() || '';
492
+
493
+ for (const line of lines) {
494
+ if (line.startsWith('data: ')) {
495
+ try {
496
+ const data = JSON.parse(line.substring(6));
497
+
498
+ if (data.type === 'text') {
499
+ if (!messageElement) {
500
+ messageElement = addMessage('assistant', '');
501
+ }
502
+ messageElement.innerHTML += formatChunk(data.content);
503
+ } else if (data.type === 'thought' && thinkingEnabled) {
504
+ if (!thoughtsElement) {
505
+ thoughtsElement = addThoughts('');
506
+ thoughtsElement.innerHTML = `<div class="thoughts-header">🧠 Réflexion de l'assistant:</div><div class="thought-content"></div>`;
507
+ }
508
+ thoughtsElement.querySelector('.thought-content').innerHTML += formatChunk(data.content);
509
+ } else if (data.type === 'error') {
510
+ showError(data.content);
511
+ } else if (data.type === 'end') {
512
+ typingIndicator.style.display = 'none';
513
+ messageInput.disabled = false;
514
+ sendButton.disabled = false;
515
+ messageInput.focus();
516
+ removeFile();
517
+ return; // Fin du traitement du stream
518
+ }
519
+ // Scroll vers le bas à chaque nouveau chunk
520
+ const messagesContainer = document.getElementById('messages');
521
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
522
+
523
+ } catch (e) {
524
+ console.error('Erreur parsing JSON:', e, "Ligne reçue:", line);
525
+ }
526
  }
527
+ }
528
+ return processStream();
529
+ });
530
+ }
531
+ return processStream();
532
+ })
533
+ .catch(error => {
534
+ typingIndicator.style.display = 'none';
535
+ messageInput.disabled = false;
536
+ sendButton.disabled = false;
537
+ showError('Erreur: ' + error.message);
538
+ messageInput.focus();
539
+ });
540
  }
541
 
542
+ function addMessage(role, content) {
543
+ const messagesContainer = document.getElementById('messages');
544
+ const messageDiv = document.createElement('div');
545
+ messageDiv.className = `message ${role}`;
546
+ messageDiv.innerHTML = formatMessage(content);
547
+ messagesContainer.appendChild(messageDiv);
548
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
549
+ return messageDiv;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  }
551
 
552
+ function addThoughts(content) {
553
+ const messagesContainer = document.getElementById('messages');
554
+ const thoughtsDiv = document.createElement('div');
555
+ thoughtsDiv.className = 'thoughts';
556
+ thoughtsDiv.innerHTML = content;
557
+ messagesContainer.appendChild(thoughtsDiv);
558
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
559
+ return thoughtsDiv;
560
  }
561
+
562
+ function escapeHtml(unsafe) {
563
+ return unsafe
564
+ .replace(/&/g, "&amp;")
565
+ .replace(/</g, "&lt;")
566
+ .replace(/>/g, "&gt;")
567
+ .replace(/"/g, "&quot;")
568
+ .replace(/'/g, "&#039;");
 
 
 
 
569
  }
570
 
571
+ function formatChunk(chunk) {
572
+ // Pour le streaming, on échappe le HTML et on remplace juste les sauts de ligne
573
+ return escapeHtml(chunk).replace(/\n/g, '<br>');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
574
  }
575
 
576
+ function formatMessage(content) {
577
+ // Cette fonction est utilisée pour le message initial de l'utilisateur
578
+ // Elle peut gérer plus de markdown car elle traite le message en une seule fois
579
+ return escapeHtml(content)
580
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
581
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
582
+ .replace(/`(.*?)`/g, '<code style="background: #444; padding: 2px 5px; border-radius: 4px; font-family: monospace;">$1</code>')
583
+ .replace(/\n/g, '<br>');
584
  }
585
 
586
+
587
  function showError(message) {
588
+ const messagesContainer = document.getElementById('messages');
589
+ const errorDiv = document.createElement('div');
590
+ errorDiv.className = 'error';
591
+ errorDiv.textContent = message;
592
+ messagesContainer.appendChild(errorDiv);
593
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
594
+ }
595
+
596
+ function resetConversation() {
597
+ if (confirm('Êtes-vous sûr de vouloir réinitialiser la conversation ?')) {
598
+ fetch('/reset_conversation', {
599
+ method: 'POST',
600
+ headers: { 'Content-Type': 'application/json' },
601
+ body: JSON.stringify({ conversation_id: conversationId })
602
+ })
603
+ .then(response => {
604
+ if (response.ok) {
605
+ document.getElementById('messages').innerHTML = `
606
+ <div class="message assistant">
607
+ <div>👋 Conversation réinitialisée ! Comment puis-je vous aider ?</div>
608
+ </div>
609
+ `;
610
+ conversationId = 'session_' + Date.now();
611
+ removeFile();
612
+ } else {
613
+ showError('La réinitialisation a échoué.');
614
+ }
615
+ })
616
+ .catch(error => {
617
+ showError('Erreur lors de la réinitialisation: ' + error.message);
618
+ });
619
+ }
620
  }
621
+
622
+ document.addEventListener('DOMContentLoaded', function() {
623
+ document.getElementById('messageInput').focus();
624
+ });
625
  </script>
626
  </body>
627
  </html>