django-lucy-assist 1.2.6__py3-none-any.whl → 1.2.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,27 +1,22 @@
1
1
  /**
2
- * Lucy Assist - Composant Alpine.js pour le chatbot
3
- *
4
- * Ce fichier contient toute la logique client du chatbot Lucy Assist.
5
- * Il utilise Alpine.js pour la réactivité et communique avec le backend Django via des API REST.
2
+ * Lucy Assist - Chatbot en vanilla JavaScript
3
+ * Zero dependance externe - pas d'Alpine.js, pas de Font Awesome
6
4
  */
5
+ (function () {
6
+ 'use strict';
7
7
 
8
- function lucyAssist() {
9
- return {
10
- // État de l'interface
8
+ // ========================================================================
9
+ // ETAT
10
+ // ========================================================================
11
+ const state = {
11
12
  isOpen: false,
12
13
  isExpanded: false,
13
14
  isLoading: false,
14
15
  showHistory: false,
15
16
  showDoc: false,
16
- showBuyCredits: false,
17
- showFeedback: false,
18
- showGuide: false,
19
- guideStep: 1,
20
17
  hasNewMessage: false,
21
18
  hasError: false,
22
19
  lucyDidNotUnderstand: false,
23
-
24
- // Données
25
20
  currentMessage: '',
26
21
  messages: [],
27
22
  conversations: [],
@@ -31,805 +26,759 @@ function lucyAssist() {
31
26
  buyAmount: 10,
32
27
  feedbackDescription: '',
33
28
  feedbackSending: false,
29
+ guideStep: 1,
30
+ };
34
31
 
35
- // Configuration
36
- apiBaseUrl: '/lucy-assist/api',
37
- csrfToken: '',
38
-
39
- /**
40
- * Initialisation du composant
41
- */
42
- init() {
43
- // Récupérer le token CSRF
44
- this.csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value ||
45
- this.getCookie('csrftoken');
46
-
47
- // Restaurer l'état depuis sessionStorage
48
- this.restoreState();
49
-
50
- // Charger les données initiales
51
- this.loadTokenStatus();
52
- this.loadConversations().then(() => {
53
- // Recharger la conversation en cours si elle existe
54
- if (this.currentConversationId) {
55
- this.loadConversation(this.currentConversationId);
56
- }
57
- });
58
- this.loadSuggestions();
59
-
60
- // Vérifier si c'est le premier lancement
61
- this.checkFirstLaunch();
62
-
63
- // Sauvegarder l'état quand isOpen change
64
- this.$watch('isOpen', (value) => {
65
- this.saveState();
66
- if (value) {
67
- this.$nextTick(() => {
68
- this.$refs.messageInput?.focus();
69
- });
70
- }
71
- });
32
+ // ========================================================================
33
+ // CONFIG
34
+ // ========================================================================
35
+ const API_BASE = '/lucy-assist/api';
36
+ let csrfToken = '';
72
37
 
73
- // Sauvegarder l'état quand la conversation change
74
- this.$watch('currentConversationId', () => {
75
- this.saveState();
76
- });
38
+ // ========================================================================
39
+ // ELEMENTS DOM
40
+ // ========================================================================
41
+ const el = {};
77
42
 
78
- // Sauvegarder avant de quitter la page
79
- window.addEventListener('beforeunload', () => {
80
- this.saveState();
81
- });
82
- },
83
-
84
- /**
85
- * Sauvegarde l'état dans sessionStorage
86
- */
87
- saveState() {
88
- const state = {
89
- isOpen: this.isOpen,
90
- isExpanded: this.isExpanded,
91
- currentConversationId: this.currentConversationId,
92
- showHistory: this.showHistory,
93
- showDoc: this.showDoc
94
- };
95
- sessionStorage.setItem('lucy_assist_state', JSON.stringify(state));
96
- },
97
-
98
- /**
99
- * Restaure l'état depuis sessionStorage
100
- */
101
- restoreState() {
102
- try {
103
- const savedState = sessionStorage.getItem('lucy_assist_state');
104
- if (savedState) {
105
- const state = JSON.parse(savedState);
106
- this.isOpen = state.isOpen || false;
107
- this.isExpanded = state.isExpanded || false;
108
- this.currentConversationId = state.currentConversationId || null;
109
- this.showHistory = state.showHistory || false;
110
- this.showDoc = state.showDoc || false;
111
- }
112
- } catch (e) {
113
- console.error('Erreur restauration état Lucy Assist:', e);
114
- }
115
- },
116
-
117
- /**
118
- * Gestion des raccourcis clavier
119
- */
120
- handleKeydown(event) {
121
- // Ctrl+K pour ouvrir
122
- if (event.ctrlKey && event.key === 'k') {
123
- event.preventDefault();
124
- this.toggleSidebar();
125
- }
43
+ function $(id) {
44
+ return document.getElementById(id);
45
+ }
126
46
 
127
- // Echap pour fermer
128
- if (event.key === 'Escape' && this.isOpen) {
129
- this.closeSidebar();
130
- }
131
- },
132
-
133
- /**
134
- * Toggle du sidebar
135
- */
136
- toggleSidebar() {
137
- this.isOpen = !this.isOpen;
138
- if (this.isOpen) {
139
- this.hasNewMessage = false;
140
- }
141
- this.saveState();
142
- },
143
-
144
- /**
145
- * Fermeture du sidebar
146
- */
147
- closeSidebar() {
148
- this.isOpen = false;
149
- this.saveState();
150
- this.showHistory = false;
151
- this.showDoc = false;
152
- },
153
-
154
- /**
155
- * Toggle du mode agrandi (80% de la fenetre)
156
- */
157
- toggleExpanded() {
158
- this.isExpanded = !this.isExpanded;
159
- this.saveState();
160
- },
161
-
162
- /**
163
- * Vérifie si c'est le premier lancement
164
- */
165
- checkFirstLaunch() {
166
- const hasSeenGuide = localStorage.getItem('lucy_assist_guide_seen');
167
- if (!hasSeenGuide && this.conversations.length === 0) {
168
- // Afficher le guide au premier chargement si pas de conversations
169
- setTimeout(() => {
170
- if (this.conversations.length === 0) {
171
- this.showGuide = true;
172
- this.isOpen = true;
173
- }
174
- }, 2000);
175
- }
176
- },
177
-
178
- /**
179
- * Navigation dans le guide
180
- */
181
- nextGuideStep() {
182
- this.guideStep++;
183
- },
184
-
185
- skipGuide() {
186
- this.finishGuide();
187
- },
188
-
189
- finishGuide() {
190
- this.showGuide = false;
191
- localStorage.setItem('lucy_assist_guide_seen', 'true');
192
- },
193
-
194
- /**
195
- * Charge le statut des tokens
196
- */
197
- async loadTokenStatus() {
198
- try {
199
- const response = await this.apiGet('/tokens/status');
200
- this.tokensDisponibles = response.tokens_disponibles || 0;
201
- } catch (error) {
202
- console.error('Erreur chargement tokens:', error);
203
- }
204
- },
205
-
206
- /**
207
- * Charge la liste des conversations
208
- */
209
- async loadConversations() {
210
- try {
211
- const response = await this.apiGet('/conversations');
212
- this.conversations = response.conversations || [];
213
- } catch (error) {
214
- console.error('Erreur chargement conversations:', error);
215
- }
216
- },
217
-
218
- /**
219
- * Charge les suggestions
220
- */
221
- async loadSuggestions() {
222
- try {
223
- const response = await this.apiGet('/suggestions');
224
- this.suggestions = response.suggestions || [];
225
- } catch (error) {
226
- console.error('Erreur chargement suggestions:', error);
227
- // Suggestions par défaut
228
- this.suggestions = [
229
- "Comment créer un nouveau membre ?",
230
- "Comment effectuer un paiement ?",
231
- "Où trouver la liste des adhésions ?"
232
- ];
233
- }
234
- },
235
-
236
- /**
237
- * Crée une nouvelle conversation
238
- */
239
- async newConversation() {
240
- try {
241
- const response = await this.apiPost('/conversations', {
242
- page_contexte: window.location.pathname
243
- });
244
-
245
- this.currentConversationId = response.id;
246
- this.messages = [];
247
- this.showHistory = false;
248
- this.showDoc = false;
249
- this.saveState();
250
-
251
- // Recharger les conversations
252
- await this.loadConversations();
253
-
254
- } catch (error) {
255
- console.error('Erreur création conversation:', error);
256
- this.showToast('Erreur lors de la création de la conversation', 'error');
257
- }
258
- },
259
-
260
- /**
261
- * Charge une conversation existante
262
- */
263
- async loadConversation(conversationId) {
264
- try {
265
- const response = await this.apiGet(`/conversations/${conversationId}`);
266
-
267
- this.currentConversationId = conversationId;
268
- this.messages = response.messages || [];
269
- this.showHistory = false;
270
- this.saveState();
271
-
272
- // Scroll vers le bas avec un petit délai pour laisser le DOM se mettre à jour
273
- this.$nextTick(() => {
274
- setTimeout(() => {
275
- this.scrollToBottom();
276
- }, 100);
277
- });
278
-
279
- } catch (error) {
280
- console.error('Erreur chargement conversation:', error);
281
- // Si la conversation n'existe plus, réinitialiser
282
- this.currentConversationId = null;
283
- this.messages = [];
284
- this.saveState();
285
- this.showToast('Erreur lors du chargement de la conversation', 'error');
286
- }
287
- },
288
-
289
- /**
290
- * Supprime une conversation
291
- */
292
- async deleteConversation(conversationId) {
293
- if (!confirm('Êtes-vous sûr de vouloir supprimer cette conversation ?')) {
294
- return;
295
- }
47
+ /** Attache un event listener si l'element existe */
48
+ function on(element, event, handler) {
49
+ if (element) element.addEventListener(event, handler);
50
+ }
296
51
 
297
- try {
298
- await this.apiDelete(`/conversations/${conversationId}`);
52
+ // ========================================================================
53
+ // HELPERS
54
+ // ========================================================================
299
55
 
300
- // Recharger les conversations
301
- await this.loadConversations();
56
+ function getCookie(name) {
57
+ const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
58
+ return match ? decodeURIComponent(match[2]) : null;
59
+ }
302
60
 
303
- // Si c'était la conversation courante, reset
304
- if (this.currentConversationId === conversationId) {
305
- this.currentConversationId = null;
306
- this.messages = [];
307
- }
61
+ function formatTokens(tokens) {
62
+ if (tokens >= 1000000) return (tokens / 1000000).toFixed(1) + 'M';
63
+ if (tokens >= 1000) return (tokens / 1000).toFixed(0) + 'K';
64
+ return tokens.toString();
65
+ }
66
+
67
+ function formatTokenCost(tokens) {
68
+ return ((tokens / 1000000) * 10).toFixed(4) + '€';
69
+ }
308
70
 
309
- this.showToast('Conversation supprimée', 'success');
71
+ function formatDate(dateString) {
72
+ const date = new Date(dateString);
73
+ const now = new Date();
74
+ const diff = now - date;
75
+ if (diff < 60000) return "À l'instant";
76
+ if (diff < 3600000) return 'Il y a ' + Math.floor(diff / 60000) + ' min';
77
+ if (date.toDateString() === now.toDateString())
78
+ return "Aujourd'hui à " + date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
79
+ if (diff < 7 * 86400000)
80
+ return date.toLocaleDateString('fr-FR', { weekday: 'long', hour: '2-digit', minute: '2-digit' });
81
+ return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' });
82
+ }
310
83
 
311
- } catch (error) {
312
- console.error('Erreur suppression conversation:', error);
313
- this.showToast('Erreur lors de la suppression', 'error');
314
- }
315
- },
316
-
317
- /**
318
- * Envoie le message courant
319
- */
320
- async sendCurrentMessage() {
321
- const message = this.currentMessage.trim();
322
- if (!message) return;
323
-
324
- await this.sendMessage(message);
325
- },
326
-
327
- /**
328
- * Envoie un message (depuis l'input ou une suggestion)
329
- */
330
- async sendMessage(message) {
331
- if (this.isLoading) return;
332
-
333
- // Réinitialiser le flag "Lucy n'a pas compris" pour le nouveau message
334
- this.lucyDidNotUnderstand = false;
335
-
336
- // Créer une conversation si nécessaire
337
- if (!this.currentConversationId) {
338
- await this.newConversation();
339
- }
84
+ function formatTime(dateString) {
85
+ return new Date(dateString).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
86
+ }
87
+
88
+ function escapeHtml(s) {
89
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
90
+ }
91
+
92
+ function formatMessage(content) {
93
+ if (!content) return '';
94
+ var f = escapeHtml(content);
95
+ f = f.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
96
+ f = f.replace(/\*(.*?)\*/g, '<em>$1</em>');
97
+ f = f.replace(/`(.*?)`/g, '<code style="background:rgba(0,0,0,.1);padding:0 4px;border-radius:3px;">$1</code>');
98
+ f = f.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" style="color:var(--lucy-primary,#6366f1);text-decoration:underline;">$1</a>');
99
+ f = f.replace(/\n/g, '<br>');
100
+ return f;
101
+ }
340
102
 
341
- // Ajouter le message utilisateur à l'affichage
342
- const userMessage = {
343
- id: Date.now(),
344
- repondant: 'UTILISATEUR',
345
- contenu: message,
346
- created_date: new Date().toISOString(),
347
- tokens_utilises: 0
348
- };
349
- this.messages.push(userMessage);
350
- this.currentMessage = '';
351
-
352
- // Scroll vers le bas
353
- this.$nextTick(() => {
354
- this.scrollToBottom();
103
+ function checkIfLucyDidNotUnderstand(response) {
104
+ if (!response) return false;
105
+ var lower = response.toLowerCase();
106
+ var patterns = [
107
+ 'je ne comprends pas', "je n'ai pas compris", 'pourriez-vous reformuler',
108
+ 'pouvez-vous reformuler', 'pourriez-vous préciser', 'pouvez-vous préciser',
109
+ "je ne suis pas sûr de comprendre", "je n'ai pas accès", "je n'ai pas les outils",
110
+ 'je ne peux pas', 'je ne suis pas en mesure', 'fonctionnalité non disponible',
111
+ 'je ne dispose pas', 'mes outils actuels ne permettent pas', "je n'arrive pas à",
112
+ 'impossible de réaliser', 'en dehors de mon périmètre', 'hors de mon champ',
113
+ 'dépasse mes capacités', 'comment puis-je vous aider avec votre crm'
114
+ ];
115
+ return patterns.some(function (p) { return lower.includes(p); });
116
+ }
117
+
118
+ // ========================================================================
119
+ // API
120
+ // ========================================================================
121
+
122
+ async function apiGet(endpoint) {
123
+ var r = await fetch(API_BASE + endpoint, {
124
+ headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }
125
+ });
126
+ if (!r.ok) { var e = await r.json(); e.status = r.status; throw e; }
127
+ return r.json();
128
+ }
129
+
130
+ async function apiPost(endpoint, data) {
131
+ var r = await fetch(API_BASE + endpoint, {
132
+ method: 'POST',
133
+ headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
134
+ body: JSON.stringify(data)
135
+ });
136
+ if (!r.ok) { var e = await r.json(); e.status = r.status; throw e; }
137
+ return r.json();
138
+ }
139
+
140
+ async function apiDelete(endpoint) {
141
+ var r = await fetch(API_BASE + endpoint, {
142
+ method: 'DELETE',
143
+ headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }
144
+ });
145
+ if (!r.ok) { var e = await r.json(); e.status = r.status; throw e; }
146
+ return r.json();
147
+ }
148
+
149
+ // ========================================================================
150
+ // RENDU
151
+ // ========================================================================
152
+
153
+ function scrollToBottom() {
154
+ if (el.messages) el.messages.scrollTop = el.messages.scrollHeight;
155
+ }
156
+
157
+ function updateTokensDisplay() {
158
+ if (el.tokens) el.tokens.textContent = formatTokens(state.tokensDisponibles);
159
+ if (el.btnBuyCredits) el.btnBuyCredits.style.display = state.tokensDisponibles < 100000 ? '' : 'none';
160
+ }
161
+
162
+ function updateExpandIcons() {
163
+ var expand = el.sidebar ? el.sidebar.querySelector('.lucy-icon-expand') : null;
164
+ var compress = el.sidebar ? el.sidebar.querySelector('.lucy-icon-compress') : null;
165
+ if (expand) expand.style.display = state.isExpanded ? 'none' : '';
166
+ if (compress) compress.style.display = state.isExpanded ? '' : 'none';
167
+ }
168
+
169
+ /** Affiche la bonne vue : chat / history / doc */
170
+ function showView(view) {
171
+ state.showHistory = view === 'history';
172
+ state.showDoc = view === 'doc';
173
+
174
+ if (el.viewHistory) el.viewHistory.style.display = state.showHistory ? '' : 'none';
175
+ if (el.viewDoc) el.viewDoc.style.display = state.showDoc ? '' : 'none';
176
+ if (el.viewChat) el.viewChat.style.display = (!state.showHistory && !state.showDoc) ? '' : 'none';
177
+
178
+ if (el.btnHistory) el.btnHistory.classList.toggle('active', state.showHistory);
179
+ if (el.btnDoc) el.btnDoc.classList.toggle('active', state.showDoc);
180
+ }
181
+
182
+ /** Rend la liste des conversations dans l'historique */
183
+ function renderHistoryList() {
184
+ if (!el.historyList) return;
185
+
186
+ if (state.conversations.length === 0) {
187
+ el.historyList.innerHTML = '<p class="lucy-history-empty">Aucune conversation</p>';
188
+ return;
189
+ }
190
+
191
+ el.historyList.innerHTML = state.conversations.map(function (conv) {
192
+ return '<div class="lucy-history-item ' + (state.currentConversationId === conv.id ? 'active' : '') + '" data-conv-id="' + conv.id + '">'
193
+ + '<div class="lucy-history-item-title">' + escapeHtml(conv.titre || 'Sans titre') + '</div>'
194
+ + '<div class="lucy-history-item-meta"><span>' + formatDate(conv.created_date) + '</span>'
195
+ + '<span style="margin-left:.5rem">🪙 ' + conv.total_tokens + ' tokens | ' + formatTokenCost(conv.total_tokens) + '</span></div>'
196
+ + '<button class="lucy-btn-delete" data-delete-id="' + conv.id + '">🗑️ Supprimer</button>'
197
+ + '</div>';
198
+ }).join('');
199
+
200
+ el.historyList.querySelectorAll('[data-conv-id]').forEach(function (item) {
201
+ item.addEventListener('click', function (e) {
202
+ if (e.target.closest('[data-delete-id]')) return;
203
+ loadConversation(parseInt(item.dataset.convId));
355
204
  });
205
+ });
206
+ el.historyList.querySelectorAll('[data-delete-id]').forEach(function (btn) {
207
+ btn.addEventListener('click', function (e) {
208
+ e.stopPropagation();
209
+ deleteConversation(parseInt(btn.dataset.deleteId));
210
+ });
211
+ });
212
+ }
356
213
 
357
- // Envoyer au serveur
358
- try {
359
- this.isLoading = true;
214
+ /** Rend la zone de messages */
215
+ function renderMessages() {
216
+ if (!el.messages) return;
217
+
218
+ if (state.messages.length === 0) {
219
+ if (el.welcome) el.welcome.style.display = '';
220
+ if (el.feedbackContact) el.feedbackContact.style.display = 'none';
221
+ if (el.feedbackError) el.feedbackError.style.display = 'none';
222
+ // Supprimer les anciens messages
223
+ el.messages.querySelectorAll('.lucy-message').forEach(function (m) { m.remove(); });
224
+ return;
225
+ }
360
226
 
361
- // Envoyer le message utilisateur
362
- await this.apiPost(`/conversations/${this.currentConversationId}/messages`, {
363
- contenu: message
364
- });
227
+ if (el.welcome) el.welcome.style.display = 'none';
365
228
 
366
- // Demander une réponse à Claude (avec streaming)
367
- await this.streamChatCompletion();
229
+ // Supprimer les anciens messages
230
+ el.messages.querySelectorAll('.lucy-message').forEach(function (m) { m.remove(); });
368
231
 
369
- } catch (error) {
370
- console.error('Erreur envoi message:', error);
232
+ var avatarUrl = el.messages.dataset.avatarUrl || '';
233
+ var lucyIcon = el.messages.dataset.lucyIcon || '';
371
234
 
372
- if (error.status === 402) {
373
- // Pas assez de crédits
374
- this.showBuyCredits = true;
375
- } else {
376
- // Marquer qu'il y a eu une erreur pour proposer le feedback
377
- this.hasError = true;
378
- this.showToast('Erreur lors de l\'envoi du message', 'error');
379
- }
235
+ state.messages.forEach(function (msg) {
236
+ var isUser = msg.repondant === 'UTILISATEUR';
237
+ var div = document.createElement('div');
238
+ div.className = 'lucy-message ' + (isUser ? 'user' : 'bot');
380
239
 
381
- } finally {
382
- this.isLoading = false;
240
+ var avatarHtml;
241
+ if (isUser) {
242
+ avatarHtml = avatarUrl
243
+ ? '<img src="' + avatarUrl + '" alt="Utilisateur">'
244
+ : '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>';
245
+ } else {
246
+ avatarHtml = lucyIcon
247
+ ? '<img src="' + lucyIcon + '" alt="Lucy">'
248
+ : '🤖';
383
249
  }
384
- },
385
-
386
- /**
387
- * Stream la réponse de Claude
388
- */
389
- async streamChatCompletion() {
390
- const url = `${this.apiBaseUrl}/conversations/${this.currentConversationId}/chat`;
391
-
392
- // Index du message bot dans le tableau
393
- const botMessageIndex = this.messages.length;
394
-
395
- // Créer un placeholder pour la réponse
396
- this.messages.push({
397
- id: Date.now() + 1,
398
- repondant: 'CHATBOT',
399
- contenu: '',
400
- created_date: new Date().toISOString(),
401
- tokens_utilises: 0
402
- });
403
250
 
404
- try {
405
- const response = await fetch(url, {
406
- method: 'POST',
407
- headers: {
408
- 'Content-Type': 'application/json',
409
- 'X-CSRFToken': this.csrfToken
410
- },
411
- body: JSON.stringify({
412
- page_contexte: window.location.pathname
413
- })
414
- });
415
-
416
- if (!response.ok) {
417
- const errorData = await response.json();
418
- throw { status: response.status, ...errorData };
419
- }
251
+ var bubbleContent = msg.contenu
252
+ ? '<span>' + formatMessage(msg.contenu) + '</span>'
253
+ : '<span class="lucy-loading-dots"><span></span><span></span><span></span></span>';
420
254
 
421
- const reader = response.body.getReader();
422
- const decoder = new TextDecoder();
423
- let fullContent = '';
424
-
425
- while (true) {
426
- const { done, value } = await reader.read();
427
- if (done) break;
428
-
429
- const text = decoder.decode(value);
430
- const lines = text.split('\n');
431
-
432
- for (const line of lines) {
433
- if (line.startsWith('data: ')) {
434
- try {
435
- const data = JSON.parse(line.slice(6));
436
-
437
- if (data.type === 'content') {
438
- fullContent += data.content;
439
- // Forcer la réactivité Alpine en réassignant l'objet
440
- this.messages[botMessageIndex] = {
441
- ...this.messages[botMessageIndex],
442
- contenu: fullContent
443
- };
444
- this.$nextTick(() => this.scrollToBottom());
445
- } else if (data.type === 'done') {
446
- // Mise à jour finale avec l'ID et les tokens
447
- this.messages[botMessageIndex] = {
448
- ...this.messages[botMessageIndex],
449
- id: data.message_id,
450
- tokens_utilises: data.tokens_utilises
451
- };
452
- // Mettre à jour les tokens
453
- await this.loadTokenStatus();
454
- } else if (data.type === 'error') {
455
- throw new Error(data.error);
456
- }
457
- } catch (e) {
458
- if (e.message) throw e;
459
- }
460
- }
461
- }
462
- }
255
+ div.innerHTML = '<div class="lucy-message-avatar">' + avatarHtml + '</div>'
256
+ + '<div class="lucy-message-content">'
257
+ + '<div class="lucy-message-bubble">' + bubbleContent + '</div>'
258
+ + '<div class="lucy-message-time">' + formatTime(msg.created_date) + '</div>'
259
+ + '</div>';
463
260
 
464
- // Supprimer le message placeholder s'il est resté vide
465
- if (!this.messages[botMessageIndex]?.contenu) {
466
- this.messages.splice(botMessageIndex, 1);
467
- }
261
+ el.messages.appendChild(div);
262
+ });
468
263
 
469
- // Vérifier si Lucy n'a pas compris le message
470
- const botResponse = this.messages[botMessageIndex]?.contenu || '';
471
- this.lucyDidNotUnderstand = this.checkIfLucyDidNotUnderstand(botResponse);
264
+ // Feedback bars
265
+ if (el.feedbackContact) el.feedbackContact.style.display = (state.lucyDidNotUnderstand && state.messages.length > 0) ? '' : 'none';
266
+ if (el.feedbackError) el.feedbackError.style.display = (state.hasError && !state.lucyDidNotUnderstand && state.messages.length > 0) ? '' : 'none';
472
267
 
473
- // Réinitialiser le flag d'erreur après une réponse réussie (sauf si Lucy n'a pas compris)
474
- if (!this.lucyDidNotUnderstand) {
475
- this.hasError = false;
476
- }
268
+ requestAnimationFrame(scrollToBottom);
269
+ }
270
+
271
+ /** Rend les suggestions */
272
+ function renderSuggestions() {
273
+ if (!el.suggestions) return;
274
+ el.suggestions.innerHTML = state.suggestions.map(function (s) {
275
+ return '<button class="lucy-suggestion-btn" data-suggestion="' + escapeHtml(s) + '">'
276
+ + '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" style="flex-shrink:0;color:#f59e0b"><path stroke-linecap="round" stroke-linejoin="round" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>'
277
+ + '<span>' + escapeHtml(s) + '</span></button>';
278
+ }).join('');
279
+
280
+ el.suggestions.querySelectorAll('[data-suggestion]').forEach(function (btn) {
281
+ btn.addEventListener('click', function () { sendMessage(btn.dataset.suggestion); });
282
+ });
283
+ }
477
284
 
478
- // Recharger les conversations pour mettre à jour les titres
479
- await this.loadConversations();
285
+ function updateBuyModal() {
286
+ var tokens = Math.floor((state.buyAmount / 10) * 1000000);
287
+ if (el.buyTokens) el.buyTokens.textContent = formatTokens(tokens);
288
+ if (el.buyConvs) el.buyConvs.textContent = Math.floor(tokens / 2000).toString();
289
+ }
480
290
 
481
- } catch (error) {
482
- // Marquer qu'il y a eu une erreur
483
- this.hasError = true;
291
+ function updateSendBtn() {
292
+ if (el.btnSend) el.btnSend.disabled = state.isLoading || !(el.input && el.input.value.trim());
293
+ }
484
294
 
485
- // Supprimer le message placeholder vide en cas d'erreur
486
- if (botMessageIndex < this.messages.length && !this.messages[botMessageIndex]?.contenu) {
487
- this.messages.splice(botMessageIndex, 1);
488
- }
489
- throw error;
490
- }
491
- },
492
-
493
- /**
494
- * Vérifie si Lucy n'a pas compris ou ne peut pas aider l'utilisateur
495
- */
496
- checkIfLucyDidNotUnderstand(response) {
497
- if (!response) return false;
498
-
499
- const lowerResponse = response.toLowerCase();
500
-
501
- // Patterns indiquant que Lucy n'a pas compris ou ne peut pas aider
502
- const notUnderstoodPatterns = [
503
- // Ne comprend pas
504
- 'je ne comprends pas',
505
- 'je n\'ai pas compris',
506
- 'je n\'arrive pas à comprendre',
507
- 'pourriez-vous reformuler',
508
- 'pouvez-vous reformuler',
509
- 'pourriez-vous préciser',
510
- 'pouvez-vous préciser',
511
- 'je ne suis pas sûr de comprendre',
512
- 'je ne suis pas certain de comprendre',
513
- 'votre message n\'est pas clair',
514
- 'erreur de frappe',
515
- 'erreur de saisie',
516
- 'message incompréhensible',
517
- 'je n\'ai pas pu interpréter',
518
- 'que souhaitez-vous faire',
519
- 'que voulez-vous faire',
520
- // Ne peut pas aider / pas accès
521
- 'je n\'ai pas accès',
522
- 'je n\'ai pas les outils',
523
- 'je n\'ai pas la possibilité',
524
- 'je ne peux pas',
525
- 'je ne suis pas en mesure',
526
- 'pas accès aux outils',
527
- 'outils nécessaires',
528
- 'fonctionnalité non disponible',
529
- 'cette fonctionnalité n\'est pas',
530
- 'je ne dispose pas',
531
- 'mes outils actuels ne permettent pas',
532
- 'mes outils ne permettent pas',
533
- 'je n\'arrive pas à',
534
- 'impossible de réaliser',
535
- 'je ne trouve pas',
536
- 'modèle non disponible',
537
- 'n\'existe pas dans',
538
- // Questions de clarification multiples
539
- 'pouvez-vous me dire',
540
- 'pourriez-vous me préciser',
541
- 'solutions possibles',
542
- // Hors du périmètre / redirection vers autres outils
543
- 'je suis spécialisée',
544
- 'je vous recommande de consulter',
545
- 'je vous conseille de',
546
- 'en dehors de mon périmètre',
547
- 'hors de mon champ',
548
- 'ne fait pas partie de mes compétences',
549
- 'dépasse mes capacités',
550
- 'assistants culinaires',
551
- 'chatgpt',
552
- 'google assistant',
553
- 'sites spécialisés',
554
- 'comment puis-je vous aider avec votre crm',
555
- 'y a-t-il quelque chose que vous souhaitez faire'
556
- ];
295
+ // ========================================================================
296
+ // ACTIONS
297
+ // ========================================================================
298
+
299
+ function openSidebar() {
300
+ state.isOpen = true;
301
+ state.hasNewMessage = false;
302
+ if (el.floatBtn) el.floatBtn.style.display = 'none';
303
+ if (el.sidebar) {
304
+ // 1) Rendre visible mais hors-ecran (classe hidden = translateX(100%))
305
+ el.sidebar.style.display = '';
306
+ // 2) Forcer un reflow pour que le navigateur prenne en compte le display
307
+ el.sidebar.offsetHeight; // eslint-disable-line no-unused-expressions
308
+ // 3) Retirer hidden pour declencher la transition CSS
309
+ el.sidebar.classList.remove('hidden');
310
+ }
311
+ saveState();
312
+ setTimeout(function () { if (el.input) el.input.focus(); }, 300);
313
+ }
557
314
 
558
- return notUnderstoodPatterns.some(pattern => lowerResponse.includes(pattern));
559
- },
560
-
561
- /**
562
- * Ouvre le modal de feedback
563
- */
564
- openFeedback() {
565
- this.showFeedback = true;
566
- this.feedbackDescription = '';
567
- },
568
-
569
- /**
570
- * Ferme le modal de feedback
571
- */
572
- closeFeedback() {
573
- this.showFeedback = false;
574
- this.feedbackDescription = '';
575
- },
576
-
577
- /**
578
- * Envoie le feedback à Revolucy
579
- */
580
- async sendFeedback() {
581
- if (!this.currentConversationId) {
582
- this.showToast('Aucune conversation à signaler', 'warning');
583
- return;
584
- }
315
+ function closeSidebar() {
316
+ state.isOpen = false;
317
+ state.showHistory = false;
318
+ state.showDoc = false;
319
+ if (el.sidebar) {
320
+ el.sidebar.classList.add('hidden');
321
+ // Attendre la fin de la transition avant de masquer
322
+ setTimeout(function () {
323
+ if (!state.isOpen && el.sidebar) el.sidebar.style.display = 'none';
324
+ }, 350);
325
+ }
326
+ if (el.floatBtn) el.floatBtn.style.display = '';
327
+ showView('chat');
328
+ saveState();
329
+ }
585
330
 
586
- this.feedbackSending = true;
587
-
588
- try {
589
- const response = await this.apiPost('/feedback', {
590
- conversation_id: this.currentConversationId,
591
- description: this.feedbackDescription,
592
- page_url: window.location.href
593
- });
594
-
595
- this.showToast(response.message || 'Feedback envoyé avec succès !', 'success');
596
- this.closeFeedback();
597
- this.hasError = false;
598
- this.lucyDidNotUnderstand = false;
599
-
600
- } catch (error) {
601
- console.error('Erreur envoi feedback:', error);
602
- this.showToast('Erreur lors de l\'envoi du feedback', 'error');
603
- } finally {
604
- this.feedbackSending = false;
605
- }
606
- },
607
-
608
- /**
609
- * Achat de crédits
610
- */
611
- async buyCredits() {
612
- if (this.buyAmount < 10) {
613
- this.showToast('Le montant minimum est de 10 EUR', 'warning');
614
- return;
615
- }
331
+ function toggleSidebar() {
332
+ state.isOpen ? closeSidebar() : openSidebar();
333
+ }
616
334
 
617
- try {
618
- const response = await this.apiPost('/tokens/buy', {
619
- montant_ht: this.buyAmount
620
- });
335
+ function toggleExpanded() {
336
+ state.isExpanded = !state.isExpanded;
337
+ if (el.sidebar) el.sidebar.classList.toggle('expanded', state.isExpanded);
338
+ updateExpandIcons();
339
+ saveState();
340
+ }
621
341
 
622
- // Ouvrir l'URL de souscription dans un nouvel onglet
623
- window.open(response.url_souscription, '_blank');
342
+ async function loadTokenStatus() {
343
+ try {
344
+ var r = await apiGet('/tokens/status');
345
+ state.tokensDisponibles = r.tokens_disponibles || 0;
346
+ updateTokensDisplay();
347
+ } catch (e) { console.error('Lucy: erreur tokens', e); }
348
+ }
624
349
 
625
- this.showBuyCredits = false;
626
- this.showToast('Redirection vers la page de paiement...', 'info');
350
+ async function loadConversations() {
351
+ try {
352
+ var r = await apiGet('/conversations');
353
+ state.conversations = r.conversations || [];
354
+ renderHistoryList();
355
+ } catch (e) { console.error('Lucy: erreur conversations', e); }
356
+ }
627
357
 
628
- } catch (error) {
629
- console.error('Erreur achat crédits:', error);
630
- this.showToast('Erreur lors de la génération du lien de paiement', 'error');
631
- }
632
- },
633
-
634
- /**
635
- * Calcule le nombre de tokens pour un montant
636
- */
637
- calculateTokens(amount) {
638
- return Math.floor((amount / 10) * 1000000);
639
- },
640
-
641
- /**
642
- * Formate un nombre de tokens
643
- */
644
- formatTokens(tokens) {
645
- if (tokens >= 1000000) {
646
- return (tokens / 1000000).toFixed(1) + 'M';
647
- } else if (tokens >= 1000) {
648
- return (tokens / 1000).toFixed(0) + 'K';
649
- }
650
- return tokens.toString();
651
- },
652
-
653
- /**
654
- * Calcule et formate le coût en euros pour un nombre de tokens
655
- * Prix par défaut: 10€ par million de tokens
656
- */
657
- formatTokenCost(tokens) {
658
- const prixParMillion = 10; // 10€ par million de tokens
659
- const cout = (tokens / 1000000) * prixParMillion;
660
- return cout.toFixed(4) + '€';
661
- },
662
-
663
- /**
664
- * Formate une date
665
- */
666
- formatDate(dateString) {
667
- const date = new Date(dateString);
668
- const now = new Date();
669
- const diff = now - date;
670
-
671
- // Moins d'une minute
672
- if (diff < 60000) {
673
- return 'À l\'instant';
674
- }
358
+ async function loadSuggestions() {
359
+ try {
360
+ var r = await apiGet('/suggestions');
361
+ state.suggestions = r.suggestions || [];
362
+ } catch (e) {
363
+ state.suggestions = [
364
+ 'Comment créer un nouveau membre ?',
365
+ 'Comment effectuer un paiement ?',
366
+ 'Où trouver la liste des adhésions ?'
367
+ ];
368
+ }
369
+ renderSuggestions();
370
+ }
675
371
 
676
- // Moins d'une heure
677
- if (diff < 3600000) {
678
- const minutes = Math.floor(diff / 60000);
679
- return `Il y a ${minutes} min`;
680
- }
372
+ async function newConversation() {
373
+ try {
374
+ var r = await apiPost('/conversations', { page_contexte: window.location.pathname });
375
+ state.currentConversationId = r.id;
376
+ state.messages = [];
377
+ showView('chat');
378
+ renderMessages();
379
+ saveState();
380
+ await loadConversations();
381
+ } catch (e) { console.error('Lucy: erreur new conversation', e); }
382
+ }
681
383
 
682
- // Aujourd'hui
683
- if (date.toDateString() === now.toDateString()) {
684
- return `Aujourd'hui à ${date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`;
685
- }
384
+ async function loadConversation(id) {
385
+ try {
386
+ var r = await apiGet('/conversations/' + id);
387
+ state.currentConversationId = id;
388
+ state.messages = r.messages || [];
389
+ showView('chat');
390
+ renderMessages();
391
+ saveState();
392
+ setTimeout(scrollToBottom, 100);
393
+ } catch (e) {
394
+ console.error('Lucy: erreur load conversation', e);
395
+ state.currentConversationId = null;
396
+ state.messages = [];
397
+ renderMessages();
398
+ saveState();
399
+ }
400
+ }
686
401
 
687
- // Cette semaine
688
- if (diff < 7 * 24 * 3600000) {
689
- return date.toLocaleDateString('fr-FR', { weekday: 'long', hour: '2-digit', minute: '2-digit' });
402
+ async function deleteConversation(id) {
403
+ if (!confirm('Êtes-vous sûr de vouloir supprimer cette conversation ?')) return;
404
+ try {
405
+ await apiDelete('/conversations/' + id);
406
+ await loadConversations();
407
+ if (state.currentConversationId === id) {
408
+ state.currentConversationId = null;
409
+ state.messages = [];
410
+ renderMessages();
690
411
  }
412
+ } catch (e) { console.error('Lucy: erreur suppression', e); }
413
+ }
691
414
 
692
- return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' });
693
- },
694
-
695
- /**
696
- * Formate une heure
697
- */
698
- formatTime(dateString) {
699
- const date = new Date(dateString);
700
- return date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
701
- },
702
-
703
- /**
704
- * Formate le contenu d'un message (markdown basique)
705
- */
706
- formatMessage(content) {
707
- if (!content) return '';
708
-
709
- // Échapper le HTML
710
- let formatted = content
711
- .replace(/&/g, '&amp;')
712
- .replace(/</g, '&lt;')
713
- .replace(/>/g, '&gt;');
714
-
715
- // Markdown basique
716
- formatted = formatted
717
- // Gras
718
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
719
- // Italique
720
- .replace(/\*(.*?)\*/g, '<em>$1</em>')
721
- // Code inline
722
- .replace(/`(.*?)`/g, '<code class="bg-base-300 px-1 rounded">$1</code>')
723
- // Liens
724
- .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" class="link link-primary">$1</a>')
725
- // Sauts de ligne
726
- .replace(/\n/g, '<br>');
727
-
728
- return formatted;
729
- },
730
-
731
- /**
732
- * Scroll vers le bas de la zone de messages
733
- */
734
- scrollToBottom() {
735
- const container = this.$refs.messagesContainer;
736
- if (container) {
737
- container.scrollTop = container.scrollHeight;
738
- }
739
- },
740
-
741
- /**
742
- * Affiche un toast de notification
743
- */
744
- showToast(message, type = 'info') {
745
- // Utiliser le système de toast Alpine.js existant si disponible
746
- if (window.Alpine && window.toastData) {
747
- window.toastData.show(message, type);
415
+ async function sendMessage(message) {
416
+ if (state.isLoading || !message || !message.trim()) return;
417
+
418
+ state.lucyDidNotUnderstand = false;
419
+
420
+ if (!state.currentConversationId) await newConversation();
421
+
422
+ // Message utilisateur
423
+ state.messages.push({
424
+ id: Date.now(),
425
+ repondant: 'UTILISATEUR',
426
+ contenu: message,
427
+ created_date: new Date().toISOString(),
428
+ tokens_utilises: 0
429
+ });
430
+ state.currentMessage = '';
431
+ if (el.input) {
432
+ el.input.value = '';
433
+ el.input.style.height = 'auto';
434
+ }
435
+ updateSendBtn();
436
+ renderMessages();
437
+
438
+ try {
439
+ state.isLoading = true;
440
+ if (el.input) el.input.disabled = true;
441
+ if (el.btnSend) el.btnSend.disabled = true;
442
+
443
+ await apiPost('/conversations/' + state.currentConversationId + '/messages', { contenu: message });
444
+ await streamChatCompletion();
445
+ } catch (e) {
446
+ console.error('Lucy: erreur envoi', e);
447
+ if (e.status === 402) {
448
+ showModal('buy');
748
449
  } else {
749
- // Fallback: alert simple
750
- console.log(`[${type}] ${message}`);
450
+ state.hasError = true;
451
+ renderMessages();
751
452
  }
752
- },
753
-
754
- /**
755
- * Récupère un cookie
756
- */
757
- getCookie(name) {
758
- let cookieValue = null;
759
- if (document.cookie && document.cookie !== '') {
760
- const cookies = document.cookie.split(';');
761
- for (let i = 0; i < cookies.length; i++) {
762
- const cookie = cookies[i].trim();
763
- if (cookie.substring(0, name.length + 1) === (name + '=')) {
764
- cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
765
- break;
766
- }
453
+ } finally {
454
+ state.isLoading = false;
455
+ if (el.input) el.input.disabled = false;
456
+ updateSendBtn();
457
+ if (el.input) el.input.focus();
458
+ }
459
+ }
460
+
461
+ async function streamChatCompletion() {
462
+ var url = API_BASE + '/conversations/' + state.currentConversationId + '/chat';
463
+ var botIdx = state.messages.length;
464
+
465
+ state.messages.push({
466
+ id: Date.now() + 1,
467
+ repondant: 'CHATBOT',
468
+ contenu: '',
469
+ created_date: new Date().toISOString(),
470
+ tokens_utilises: 0
471
+ });
472
+ renderMessages();
473
+
474
+ try {
475
+ var response = await fetch(url, {
476
+ method: 'POST',
477
+ headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
478
+ body: JSON.stringify({ page_contexte: window.location.pathname })
479
+ });
480
+
481
+ if (!response.ok) { var err = await response.json(); err.status = response.status; throw err; }
482
+
483
+ var reader = response.body.getReader();
484
+ var decoder = new TextDecoder();
485
+ var fullContent = '';
486
+
487
+ while (true) {
488
+ var result = await reader.read();
489
+ if (result.done) break;
490
+
491
+ var text = decoder.decode(result.value);
492
+ var lines = text.split('\n');
493
+
494
+ for (var i = 0; i < lines.length; i++) {
495
+ var line = lines[i];
496
+ if (!line.startsWith('data: ')) continue;
497
+ try {
498
+ var data = JSON.parse(line.slice(6));
499
+ if (data.type === 'content') {
500
+ fullContent += data.content;
501
+ state.messages[botIdx].contenu = fullContent;
502
+ renderMessages();
503
+ } else if (data.type === 'done') {
504
+ state.messages[botIdx].id = data.message_id;
505
+ state.messages[botIdx].tokens_utilises = data.tokens_utilises;
506
+ await loadTokenStatus();
507
+ } else if (data.type === 'error') {
508
+ throw new Error(data.error);
509
+ }
510
+ } catch (parseErr) { if (parseErr.message) throw parseErr; }
767
511
  }
768
512
  }
769
- return cookieValue;
770
- },
771
-
772
- // Méthodes utilitaires pour les appels API
773
- async apiGet(endpoint) {
774
- const response = await fetch(`${this.apiBaseUrl}${endpoint}`, {
775
- method: 'GET',
776
- headers: {
777
- 'Content-Type': 'application/json',
778
- 'X-CSRFToken': this.csrfToken
779
- }
780
- });
781
513
 
782
- if (!response.ok) {
783
- const error = await response.json();
784
- throw { status: response.status, ...error };
514
+ if (!state.messages[botIdx] || !state.messages[botIdx].contenu) {
515
+ state.messages.splice(botIdx, 1);
785
516
  }
786
517
 
787
- return response.json();
788
- },
518
+ state.lucyDidNotUnderstand = checkIfLucyDidNotUnderstand(
519
+ (state.messages[botIdx] && state.messages[botIdx].contenu) || ''
520
+ );
521
+ if (!state.lucyDidNotUnderstand) state.hasError = false;
522
+
523
+ renderMessages();
524
+ await loadConversations();
525
+ } catch (e) {
526
+ state.hasError = true;
527
+ if (botIdx < state.messages.length && state.messages[botIdx] && !state.messages[botIdx].contenu) {
528
+ state.messages.splice(botIdx, 1);
529
+ }
530
+ renderMessages();
531
+ throw e;
532
+ }
533
+ }
789
534
 
790
- async apiPost(endpoint, data) {
791
- const response = await fetch(`${this.apiBaseUrl}${endpoint}`, {
792
- method: 'POST',
793
- headers: {
794
- 'Content-Type': 'application/json',
795
- 'X-CSRFToken': this.csrfToken
796
- },
797
- body: JSON.stringify(data)
535
+ async function sendFeedback() {
536
+ if (!state.currentConversationId) return;
537
+ state.feedbackSending = true;
538
+ try {
539
+ await apiPost('/feedback', {
540
+ conversation_id: state.currentConversationId,
541
+ description: state.feedbackDescription,
542
+ page_url: window.location.href
798
543
  });
544
+ state.hasError = false;
545
+ state.lucyDidNotUnderstand = false;
546
+ hideModal('feedback');
547
+ renderMessages();
548
+ } catch (e) { console.error('Lucy: erreur feedback', e); }
549
+ finally { state.feedbackSending = false; }
550
+ }
799
551
 
800
- if (!response.ok) {
801
- const error = await response.json();
802
- throw { status: response.status, ...error };
803
- }
552
+ async function buyCredits() {
553
+ if (state.buyAmount < 10) return;
554
+ try {
555
+ var r = await apiPost('/tokens/buy', { montant_ht: state.buyAmount });
556
+ window.open(r.url_souscription, '_blank');
557
+ hideModal('buy');
558
+ } catch (e) { console.error('Lucy: erreur achat', e); }
559
+ }
804
560
 
805
- return response.json();
806
- },
561
+ // ========================================================================
562
+ // MODALS
563
+ // ========================================================================
564
+
565
+ function showModal(name) {
566
+ if (name === 'buy' && el.modalBuy) {
567
+ el.modalBuy.style.display = '';
568
+ updateBuyModal();
569
+ } else if (name === 'feedback' && el.modalFeedback) {
570
+ el.modalFeedback.style.display = '';
571
+ state.feedbackDescription = '';
572
+ if (el.feedbackDesc) el.feedbackDesc.value = '';
573
+ }
574
+ }
575
+
576
+ function hideModal(name) {
577
+ if (name === 'buy' && el.modalBuy) el.modalBuy.style.display = 'none';
578
+ else if (name === 'feedback' && el.modalFeedback) el.modalFeedback.style.display = 'none';
579
+ }
807
580
 
808
- async apiDelete(endpoint) {
809
- const response = await fetch(`${this.apiBaseUrl}${endpoint}`, {
810
- method: 'DELETE',
811
- headers: {
812
- 'Content-Type': 'application/json',
813
- 'X-CSRFToken': this.csrfToken
581
+ // ========================================================================
582
+ // GUIDE
583
+ // ========================================================================
584
+
585
+ function checkFirstLaunch() {
586
+ if (!localStorage.getItem('lucy_assist_guide_seen') && state.conversations.length === 0) {
587
+ setTimeout(function () {
588
+ if (state.conversations.length === 0 && el.guideOverlay) {
589
+ el.guideOverlay.style.display = '';
590
+ openSidebar();
814
591
  }
815
- });
592
+ }, 2000);
593
+ }
594
+ }
816
595
 
817
- if (!response.ok) {
818
- const error = await response.json();
819
- throw { status: response.status, ...error };
820
- }
596
+ function guideNext() {
597
+ state.guideStep = 2;
598
+ if (el.guideStep1) el.guideStep1.style.display = 'none';
599
+ if (el.guideStep2) el.guideStep2.style.display = '';
600
+ }
821
601
 
822
- return response.json();
602
+ function guideFinish() {
603
+ if (el.guideOverlay) el.guideOverlay.style.display = 'none';
604
+ localStorage.setItem('lucy_assist_guide_seen', 'true');
605
+ }
606
+
607
+ // ========================================================================
608
+ // STATE PERSISTENCE
609
+ // ========================================================================
610
+
611
+ function saveState() {
612
+ try {
613
+ sessionStorage.setItem('lucy_assist_state', JSON.stringify({
614
+ isOpen: state.isOpen,
615
+ isExpanded: state.isExpanded,
616
+ currentConversationId: state.currentConversationId,
617
+ showHistory: state.showHistory,
618
+ showDoc: state.showDoc
619
+ }));
620
+ } catch (e) {}
621
+ }
622
+
623
+ function restoreState() {
624
+ try {
625
+ var s = JSON.parse(sessionStorage.getItem('lucy_assist_state'));
626
+ if (!s) return;
627
+ state.isOpen = s.isOpen || false;
628
+ state.isExpanded = s.isExpanded || false;
629
+ state.currentConversationId = s.currentConversationId || null;
630
+ state.showHistory = s.showHistory || false;
631
+ state.showDoc = s.showDoc || false;
632
+ } catch (e) {}
633
+ }
634
+
635
+ // ========================================================================
636
+ // INIT
637
+ // ========================================================================
638
+
639
+ function init() {
640
+ console.log('Lucy Assist: init');
641
+
642
+ // Cache des elements - tous optionnels
643
+ el.floatBtn = $('lucy-float-btn');
644
+ el.sidebar = $('lucy-sidebar');
645
+ el.tokens = $('lucy-tokens');
646
+ el.btnBuyCredits = $('lucy-btn-buy-credits');
647
+ el.btnExpand = $('lucy-btn-expand');
648
+ el.btnHistory = $('lucy-btn-history');
649
+ el.btnNew = $('lucy-btn-new');
650
+ el.btnDoc = $('lucy-btn-doc');
651
+ el.btnClose = $('lucy-btn-close');
652
+ el.btnSend = $('lucy-btn-send');
653
+ el.input = $('lucy-input');
654
+ el.messages = $('lucy-messages');
655
+ el.welcome = $('lucy-welcome');
656
+ el.suggestions = $('lucy-suggestions');
657
+ el.viewHistory = $('lucy-view-history');
658
+ el.viewDoc = $('lucy-view-doc');
659
+ el.viewChat = $('lucy-view-chat');
660
+ el.historyList = $('lucy-history-list');
661
+ el.feedbackContact = $('lucy-feedback-contact');
662
+ el.feedbackError = $('lucy-feedback-error');
663
+ el.modalBuy = $('lucy-modal-buy');
664
+ el.modalFeedback = $('lucy-modal-feedback');
665
+ el.buyAmount = $('lucy-buy-amount');
666
+ el.buyTokens = $('lucy-buy-tokens');
667
+ el.buyConvs = $('lucy-buy-convs');
668
+ el.buyCancel = $('lucy-buy-cancel');
669
+ el.buyConfirm = $('lucy-buy-confirm');
670
+ el.feedbackDesc = $('lucy-feedback-desc');
671
+ el.feedbackCancel = $('lucy-feedback-cancel');
672
+ el.feedbackConfirm = $('lucy-feedback-confirm');
673
+ el.guideOverlay = $('lucy-guide-overlay');
674
+ el.guideStep1 = $('lucy-guide-step1');
675
+ el.guideStep2 = $('lucy-guide-step2');
676
+
677
+ // Verifier que le root existe
678
+ if (!el.floatBtn && !el.sidebar) {
679
+ console.warn('Lucy Assist: elements DOM introuvables, abort.');
680
+ return;
823
681
  }
824
- };
825
- }
826
682
 
827
- // Exposer la fonction globalement pour que x-data puisse y accéder
828
- window.lucyAssist = lucyAssist;
683
+ console.log('Lucy Assist: elements trouves, floatBtn=' + !!el.floatBtn + ', sidebar=' + !!el.sidebar);
684
+
685
+ // CSRF
686
+ var csrfEl = document.querySelector('[name=csrfmiddlewaretoken]');
687
+ csrfToken = (csrfEl && csrfEl.value) || getCookie('csrftoken') || '';
688
+
689
+ // Restaurer l'etat
690
+ restoreState();
691
+
692
+ // Appliquer l'etat initial
693
+ if (state.isOpen) {
694
+ if (el.floatBtn) el.floatBtn.style.display = 'none';
695
+ if (el.sidebar) {
696
+ el.sidebar.style.display = '';
697
+ el.sidebar.classList.remove('hidden');
698
+ }
699
+ }
700
+ if (state.isExpanded && el.sidebar) {
701
+ el.sidebar.classList.add('expanded');
702
+ }
703
+ updateExpandIcons();
704
+
705
+ if (state.showHistory) showView('history');
706
+ else if (state.showDoc) showView('doc');
707
+ else showView('chat');
708
+
709
+ // ---- EVENT LISTENERS (tous avec null-safety via on()) ----
710
+ on(el.floatBtn, 'click', toggleSidebar);
711
+ on(el.btnExpand, 'click', toggleExpanded);
712
+ on(el.btnHistory, 'click', function () { showView(state.showHistory ? 'chat' : 'history'); });
713
+ on(el.btnNew, 'click', newConversation);
714
+ on(el.btnDoc, 'click', function () { showView(state.showDoc ? 'chat' : 'doc'); });
715
+ on(el.btnClose, 'click', closeSidebar);
716
+
717
+ function onInputChange() {
718
+ if (!el.input) return;
719
+ state.currentMessage = el.input.value;
720
+ el.input.style.height = 'auto';
721
+ el.input.style.height = Math.min(el.input.scrollHeight, 128) + 'px';
722
+ updateSendBtn();
723
+ }
724
+ on(el.input, 'input', onInputChange);
725
+ on(el.input, 'keyup', onInputChange);
726
+ on(el.input, 'change', onInputChange);
727
+ on(el.input, 'keydown', function (e) {
728
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
729
+ e.preventDefault();
730
+ sendMessage(el.input.value);
731
+ }
732
+ // Mettre a jour le bouton sur la prochaine frame
733
+ setTimeout(updateSendBtn, 0);
734
+ });
735
+
736
+ on(el.btnSend, 'click', function () { sendMessage(el.input ? el.input.value : ''); });
737
+
738
+ on($('lucy-btn-feedback-contact'), 'click', function () { showModal('feedback'); });
739
+ on($('lucy-btn-feedback-error'), 'click', function () { showModal('feedback'); });
740
+
741
+ on(el.btnBuyCredits, 'click', function () { showModal('buy'); });
742
+ on(el.buyCancel, 'click', function () { hideModal('buy'); });
743
+ on(el.buyConfirm, 'click', buyCredits);
744
+ on(el.buyAmount, 'input', function () {
745
+ state.buyAmount = parseInt(el.buyAmount.value) || 10;
746
+ updateBuyModal();
747
+ });
748
+
749
+ on(el.feedbackCancel, 'click', function () { hideModal('feedback'); });
750
+ on(el.feedbackConfirm, 'click', sendFeedback);
751
+ on(el.feedbackDesc, 'input', function () {
752
+ state.feedbackDescription = el.feedbackDesc.value;
753
+ });
754
+
755
+ on($('lucy-guide-skip'), 'click', guideFinish);
756
+ on($('lucy-guide-next'), 'click', guideNext);
757
+ on($('lucy-guide-finish'), 'click', guideFinish);
758
+
759
+ // Raccourcis clavier globaux
760
+ document.addEventListener('keydown', function (e) {
761
+ if (e.ctrlKey && e.key === 'k') { e.preventDefault(); toggleSidebar(); }
762
+ if (e.key === 'Escape' && state.isOpen) closeSidebar();
763
+ });
764
+
765
+ window.addEventListener('beforeunload', saveState);
766
+
767
+ // Charger les donnees
768
+ loadTokenStatus();
769
+ loadConversations().then(function () {
770
+ if (state.currentConversationId) loadConversation(state.currentConversationId);
771
+ });
772
+ loadSuggestions();
773
+ checkFirstLaunch();
774
+
775
+ console.log('Lucy Assist: init complete');
776
+ }
829
777
 
830
- // Enregistrer le composant avec Alpine si disponible
831
- document.addEventListener('DOMContentLoaded', () => {
832
- if (window.Alpine && window.Alpine.data) {
833
- window.Alpine.data('lucyAssist', lucyAssist);
778
+ // Lancer quand le DOM est pret
779
+ if (document.readyState === 'loading') {
780
+ document.addEventListener('DOMContentLoaded', init);
781
+ } else {
782
+ init();
834
783
  }
835
- });
784
+ })();