django-lucy-assist 1.2.5__py3-none-any.whl → 1.2.7__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.
- {django_lucy_assist-1.2.5.dist-info → django_lucy_assist-1.2.7.dist-info}/METADATA +1 -1
- {django_lucy_assist-1.2.5.dist-info → django_lucy_assist-1.2.7.dist-info}/RECORD +8 -8
- lucy_assist/__init__.py +1 -1
- lucy_assist/static/lucy_assist/js/lucy-assist.js +693 -761
- lucy_assist/templates/lucy_assist/chatbot_sidebar.html +120 -366
- lucy_assist/templates/lucy_assist/partials/documentation_content.html +8 -16
- {django_lucy_assist-1.2.5.dist-info → django_lucy_assist-1.2.7.dist-info}/WHEEL +0 -0
- {django_lucy_assist-1.2.5.dist-info → django_lucy_assist-1.2.7.dist-info}/top_level.txt +0 -0
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Lucy Assist -
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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,742 @@ function lucyAssist() {
|
|
|
31
26
|
buyAmount: 10,
|
|
32
27
|
feedbackDescription: '',
|
|
33
28
|
feedbackSending: false,
|
|
29
|
+
guideStep: 1,
|
|
30
|
+
};
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
38
|
+
// ========================================================================
|
|
39
|
+
// ELEMENTS DOM (caches apres init)
|
|
40
|
+
// ========================================================================
|
|
41
|
+
const $ = (id) => document.getElementById(id);
|
|
77
42
|
|
|
78
|
-
|
|
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
|
+
let els = {};
|
|
126
44
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
}
|
|
45
|
+
// ========================================================================
|
|
46
|
+
// HELPERS
|
|
47
|
+
// ========================================================================
|
|
296
48
|
|
|
297
|
-
|
|
298
|
-
|
|
49
|
+
function getCookie(name) {
|
|
50
|
+
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
|
51
|
+
return match ? decodeURIComponent(match[2]) : null;
|
|
52
|
+
}
|
|
299
53
|
|
|
300
|
-
|
|
301
|
-
|
|
54
|
+
function formatTokens(tokens) {
|
|
55
|
+
if (tokens >= 1000000) return (tokens / 1000000).toFixed(1) + 'M';
|
|
56
|
+
if (tokens >= 1000) return (tokens / 1000).toFixed(0) + 'K';
|
|
57
|
+
return tokens.toString();
|
|
58
|
+
}
|
|
302
59
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
this.messages = [];
|
|
307
|
-
}
|
|
60
|
+
function formatTokenCost(tokens) {
|
|
61
|
+
return ((tokens / 1000000) * 10).toFixed(4) + '€';
|
|
62
|
+
}
|
|
308
63
|
|
|
309
|
-
|
|
64
|
+
function formatDate(dateString) {
|
|
65
|
+
const date = new Date(dateString);
|
|
66
|
+
const now = new Date();
|
|
67
|
+
const diff = now - date;
|
|
68
|
+
if (diff < 60000) return "À l'instant";
|
|
69
|
+
if (diff < 3600000) return `Il y a ${Math.floor(diff / 60000)} min`;
|
|
70
|
+
if (date.toDateString() === now.toDateString())
|
|
71
|
+
return `Aujourd'hui à ${date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`;
|
|
72
|
+
if (diff < 7 * 86400000)
|
|
73
|
+
return date.toLocaleDateString('fr-FR', { weekday: 'long', hour: '2-digit', minute: '2-digit' });
|
|
74
|
+
return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' });
|
|
75
|
+
}
|
|
310
76
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
}
|
|
77
|
+
function formatTime(dateString) {
|
|
78
|
+
return new Date(dateString).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
|
|
79
|
+
}
|
|
340
80
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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();
|
|
355
|
-
});
|
|
81
|
+
function escapeHtml(s) {
|
|
82
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
83
|
+
}
|
|
356
84
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
85
|
+
function formatMessage(content) {
|
|
86
|
+
if (!content) return '';
|
|
87
|
+
let f = escapeHtml(content);
|
|
88
|
+
f = f.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
89
|
+
f = f.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
|
90
|
+
f = f.replace(/`(.*?)`/g, '<code style="background:rgba(0,0,0,.1);padding:0 4px;border-radius:3px;">$1</code>');
|
|
91
|
+
f = f.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" style="color:var(--lucy-primary,#6366f1);text-decoration:underline;">$1</a>');
|
|
92
|
+
f = f.replace(/\n/g, '<br>');
|
|
93
|
+
return f;
|
|
94
|
+
}
|
|
360
95
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
96
|
+
function checkIfLucyDidNotUnderstand(response) {
|
|
97
|
+
if (!response) return false;
|
|
98
|
+
const lower = response.toLowerCase();
|
|
99
|
+
const patterns = [
|
|
100
|
+
'je ne comprends pas', "je n'ai pas compris", 'pourriez-vous reformuler',
|
|
101
|
+
'pouvez-vous reformuler', 'pourriez-vous préciser', 'pouvez-vous préciser',
|
|
102
|
+
"je ne suis pas sûr de comprendre", "je n'ai pas accès", "je n'ai pas les outils",
|
|
103
|
+
'je ne peux pas', 'je ne suis pas en mesure', 'fonctionnalité non disponible',
|
|
104
|
+
'je ne dispose pas', 'mes outils actuels ne permettent pas', "je n'arrive pas à",
|
|
105
|
+
'impossible de réaliser', 'en dehors de mon périmètre', 'hors de mon champ',
|
|
106
|
+
'dépasse mes capacités', 'comment puis-je vous aider avec votre crm'
|
|
107
|
+
];
|
|
108
|
+
return patterns.some(p => lower.includes(p));
|
|
109
|
+
}
|
|
365
110
|
|
|
366
|
-
|
|
367
|
-
|
|
111
|
+
// ========================================================================
|
|
112
|
+
// API
|
|
113
|
+
// ========================================================================
|
|
368
114
|
|
|
369
|
-
|
|
370
|
-
|
|
115
|
+
async function apiGet(endpoint) {
|
|
116
|
+
const r = await fetch(API_BASE + endpoint, {
|
|
117
|
+
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }
|
|
118
|
+
});
|
|
119
|
+
if (!r.ok) throw { status: r.status, ...(await r.json()) };
|
|
120
|
+
return r.json();
|
|
121
|
+
}
|
|
371
122
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
123
|
+
async function apiPost(endpoint, data) {
|
|
124
|
+
const r = await fetch(API_BASE + endpoint, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
|
|
127
|
+
body: JSON.stringify(data)
|
|
128
|
+
});
|
|
129
|
+
if (!r.ok) throw { status: r.status, ...(await r.json()) };
|
|
130
|
+
return r.json();
|
|
131
|
+
}
|
|
380
132
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
});
|
|
133
|
+
async function apiDelete(endpoint) {
|
|
134
|
+
const r = await fetch(API_BASE + endpoint, {
|
|
135
|
+
method: 'DELETE',
|
|
136
|
+
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }
|
|
137
|
+
});
|
|
138
|
+
if (!r.ok) throw { status: r.status, ...(await r.json()) };
|
|
139
|
+
return r.json();
|
|
140
|
+
}
|
|
403
141
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
}
|
|
142
|
+
// ========================================================================
|
|
143
|
+
// RENDU
|
|
144
|
+
// ========================================================================
|
|
420
145
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
}
|
|
146
|
+
function scrollToBottom() {
|
|
147
|
+
const c = els.messages;
|
|
148
|
+
if (c) c.scrollTop = c.scrollHeight;
|
|
149
|
+
}
|
|
463
150
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
151
|
+
function updateTokensDisplay() {
|
|
152
|
+
els.tokens.textContent = formatTokens(state.tokensDisponibles);
|
|
153
|
+
els.btnBuyCredits.style.display = state.tokensDisponibles < 100000 ? '' : 'none';
|
|
154
|
+
}
|
|
468
155
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
156
|
+
function updateExpandIcons() {
|
|
157
|
+
const expand = els.sidebar.querySelector('.lucy-icon-expand');
|
|
158
|
+
}
|
|
472
159
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
160
|
+
/** Affiche la bonne vue : chat / history / doc */
|
|
161
|
+
function showView(view) {
|
|
162
|
+
state.showHistory = view === 'history';
|
|
163
|
+
state.showDoc = view === 'doc';
|
|
477
164
|
|
|
478
|
-
|
|
479
|
-
|
|
165
|
+
els.viewHistory.style.display = state.showHistory ? '' : 'none';
|
|
166
|
+
els.viewDoc.style.display = state.showDoc ? '' : 'none';
|
|
167
|
+
els.viewChat.style.display = (!state.showHistory && !state.showDoc) ? '' : 'none';
|
|
480
168
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
169
|
+
// boutons actifs
|
|
170
|
+
els.btnHistory.classList.toggle('active', state.showHistory);
|
|
171
|
+
els.btnDoc.classList.toggle('active', state.showDoc);
|
|
172
|
+
}
|
|
484
173
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
];
|
|
174
|
+
/** Rend la liste des conversations dans l'historique */
|
|
175
|
+
function renderHistoryList() {
|
|
176
|
+
const container = els.historyList;
|
|
177
|
+
if (!container) return;
|
|
557
178
|
|
|
558
|
-
|
|
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
|
-
}
|
|
179
|
+
if (state.conversations.length === 0) {
|
|
180
|
+
container.innerHTML = '<p class="lucy-history-empty">Aucune conversation</p>';
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
585
183
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
if (this.buyAmount < 10) {
|
|
613
|
-
this.showToast('Le montant minimum est de 10 EUR', 'warning');
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
184
|
+
container.innerHTML = state.conversations.map(conv => `
|
|
185
|
+
<div class="lucy-history-item ${state.currentConversationId === conv.id ? 'active' : ''}"
|
|
186
|
+
data-conv-id="${conv.id}">
|
|
187
|
+
<div class="lucy-history-item-title">${escapeHtml(conv.titre || 'Sans titre')}</div>
|
|
188
|
+
<div class="lucy-history-item-meta">
|
|
189
|
+
<span>${formatDate(conv.created_date)}</span>
|
|
190
|
+
<span style="margin-left:.5rem">${conv.total_tokens} tokens | ${formatTokenCost(conv.total_tokens)}</span>
|
|
191
|
+
</div>
|
|
192
|
+
<button class="lucy-btn-delete" data-delete-id="${conv.id}">🗑️ Supprimer</button>
|
|
193
|
+
</div>
|
|
194
|
+
`).join('');
|
|
195
|
+
|
|
196
|
+
// Event delegation
|
|
197
|
+
container.querySelectorAll('[data-conv-id]').forEach(el => {
|
|
198
|
+
el.addEventListener('click', (e) => {
|
|
199
|
+
if (e.target.closest('[data-delete-id]')) return;
|
|
200
|
+
loadConversation(parseInt(el.dataset.convId));
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
container.querySelectorAll('[data-delete-id]').forEach(btn => {
|
|
204
|
+
btn.addEventListener('click', (e) => {
|
|
205
|
+
e.stopPropagation();
|
|
206
|
+
deleteConversation(parseInt(btn.dataset.deleteId));
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
616
210
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
211
|
+
/** Rend la zone de messages */
|
|
212
|
+
function renderMessages() {
|
|
213
|
+
const container = els.messages;
|
|
214
|
+
if (!container) return;
|
|
215
|
+
|
|
216
|
+
if (state.messages.length === 0) {
|
|
217
|
+
els.welcome.style.display = '';
|
|
218
|
+
// Feedback bars cachees quand pas de messages
|
|
219
|
+
els.feedbackContact.style.display = 'none';
|
|
220
|
+
els.feedbackError.style.display = 'none';
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
621
223
|
|
|
622
|
-
|
|
623
|
-
window.open(response.url_souscription, '_blank');
|
|
224
|
+
els.welcome.style.display = 'none';
|
|
624
225
|
|
|
625
|
-
|
|
626
|
-
|
|
226
|
+
// Garder le welcome puis ajouter les messages
|
|
227
|
+
// On reconstruit uniquement les bulles
|
|
228
|
+
// Supprimer les anciens message-divs
|
|
229
|
+
container.querySelectorAll('.lucy-message').forEach(el => el.remove());
|
|
627
230
|
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
}
|
|
231
|
+
const avatarUrl = container.dataset.avatarUrl || '';
|
|
232
|
+
const lucyIcon = container.dataset.lucyIcon || '';
|
|
675
233
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
}
|
|
234
|
+
state.messages.forEach(msg => {
|
|
235
|
+
const isUser = msg.repondant === 'UTILISATEUR';
|
|
236
|
+
const div = document.createElement('div');
|
|
237
|
+
div.className = 'lucy-message ' + (isUser ? 'user' : 'bot');
|
|
681
238
|
|
|
682
|
-
|
|
683
|
-
if (
|
|
684
|
-
|
|
239
|
+
let avatarHtml;
|
|
240
|
+
if (isUser) {
|
|
241
|
+
avatarHtml = avatarUrl
|
|
242
|
+
? `<img src="${avatarUrl}" alt="Utilisateur">`
|
|
243
|
+
: `<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>`;
|
|
244
|
+
} else {
|
|
245
|
+
avatarHtml = lucyIcon
|
|
246
|
+
? `<img src="${lucyIcon}" alt="Lucy">`
|
|
247
|
+
: '🤖';
|
|
685
248
|
}
|
|
686
249
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
250
|
+
const bubbleContent = msg.contenu
|
|
251
|
+
? `<span>${formatMessage(msg.contenu)}</span>`
|
|
252
|
+
: `<span class="lucy-loading-dots"><span></span><span></span><span></span></span>`;
|
|
253
|
+
|
|
254
|
+
div.innerHTML = `
|
|
255
|
+
<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>
|
|
260
|
+
`;
|
|
261
|
+
container.appendChild(div);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Feedback bars
|
|
265
|
+
els.feedbackContact.style.display = (state.lucyDidNotUnderstand && state.messages.length > 0) ? '' : 'none';
|
|
266
|
+
els.feedbackError.style.display = (state.hasError && !state.lucyDidNotUnderstand && state.messages.length > 0) ? '' : 'none';
|
|
267
|
+
|
|
268
|
+
requestAnimationFrame(scrollToBottom);
|
|
269
|
+
}
|
|
691
270
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
271
|
+
/** Rend les suggestions */
|
|
272
|
+
function renderSuggestions() {
|
|
273
|
+
const container = els.suggestions;
|
|
274
|
+
if (!container) return;
|
|
275
|
+
container.innerHTML = state.suggestions.map(s => `
|
|
276
|
+
<button class="lucy-suggestion-btn" data-suggestion="${escapeHtml(s)}">
|
|
277
|
+
<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>
|
|
278
|
+
<span>${escapeHtml(s)}</span>
|
|
279
|
+
</button>
|
|
280
|
+
`).join('');
|
|
281
|
+
|
|
282
|
+
container.querySelectorAll('[data-suggestion]').forEach(btn => {
|
|
283
|
+
btn.addEventListener('click', () => sendMessage(btn.dataset.suggestion));
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function updateBuyModal() {
|
|
288
|
+
const tokens = Math.floor((state.buyAmount / 10) * 1000000);
|
|
289
|
+
els.buyTokens.textContent = formatTokens(tokens);
|
|
290
|
+
els.buyConvs.textContent = Math.floor(tokens / 2000).toString();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ========================================================================
|
|
294
|
+
// ACTIONS
|
|
295
|
+
// ========================================================================
|
|
296
|
+
|
|
297
|
+
function openSidebar() {
|
|
298
|
+
state.isOpen = true;
|
|
299
|
+
state.hasNewMessage = false;
|
|
300
|
+
els.floatBtn.style.display = 'none';
|
|
301
|
+
els.sidebar.style.display = '';
|
|
302
|
+
els.sidebar.classList.remove('hidden');
|
|
303
|
+
saveState();
|
|
304
|
+
setTimeout(() => els.input.focus(), 100);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function closeSidebar() {
|
|
308
|
+
state.isOpen = false;
|
|
309
|
+
state.showHistory = false;
|
|
310
|
+
state.showDoc = false;
|
|
311
|
+
els.sidebar.style.display = 'none';
|
|
312
|
+
els.sidebar.classList.add('hidden');
|
|
313
|
+
els.floatBtn.style.display = '';
|
|
314
|
+
showView('chat');
|
|
315
|
+
saveState();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function toggleSidebar() {
|
|
319
|
+
state.isOpen ? closeSidebar() : openSidebar();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function toggleExpanded() {
|
|
323
|
+
state.isExpanded = !state.isExpanded;
|
|
324
|
+
els.sidebar.classList.toggle('expanded', state.isExpanded);
|
|
325
|
+
updateExpandIcons();
|
|
326
|
+
saveState();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function loadTokenStatus() {
|
|
330
|
+
try {
|
|
331
|
+
const r = await apiGet('/tokens/status');
|
|
332
|
+
state.tokensDisponibles = r.tokens_disponibles || 0;
|
|
333
|
+
updateTokensDisplay();
|
|
334
|
+
} catch (e) { console.error('Erreur chargement tokens:', e); }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function loadConversations() {
|
|
338
|
+
try {
|
|
339
|
+
const r = await apiGet('/conversations');
|
|
340
|
+
state.conversations = r.conversations || [];
|
|
341
|
+
renderHistoryList();
|
|
342
|
+
} catch (e) { console.error('Erreur chargement conversations:', e); }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function loadSuggestions() {
|
|
346
|
+
try {
|
|
347
|
+
const r = await apiGet('/suggestions');
|
|
348
|
+
state.suggestions = r.suggestions || [];
|
|
349
|
+
} catch (e) {
|
|
350
|
+
state.suggestions = [
|
|
351
|
+
"Comment créer un nouveau membre ?",
|
|
352
|
+
"Comment effectuer un paiement ?",
|
|
353
|
+
"Où trouver la liste des adhésions ?"
|
|
354
|
+
];
|
|
355
|
+
}
|
|
356
|
+
renderSuggestions();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function newConversation() {
|
|
360
|
+
try {
|
|
361
|
+
const r = await apiPost('/conversations', { page_contexte: window.location.pathname });
|
|
362
|
+
state.currentConversationId = r.id;
|
|
363
|
+
state.messages = [];
|
|
364
|
+
showView('chat');
|
|
365
|
+
renderMessages();
|
|
366
|
+
saveState();
|
|
367
|
+
await loadConversations();
|
|
368
|
+
} catch (e) { console.error('Erreur création conversation:', e); }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function loadConversation(id) {
|
|
372
|
+
try {
|
|
373
|
+
const r = await apiGet(`/conversations/${id}`);
|
|
374
|
+
state.currentConversationId = id;
|
|
375
|
+
state.messages = r.messages || [];
|
|
376
|
+
showView('chat');
|
|
377
|
+
renderMessages();
|
|
378
|
+
saveState();
|
|
379
|
+
setTimeout(scrollToBottom, 100);
|
|
380
|
+
} catch (e) {
|
|
381
|
+
console.error('Erreur chargement conversation:', e);
|
|
382
|
+
state.currentConversationId = null;
|
|
383
|
+
state.messages = [];
|
|
384
|
+
renderMessages();
|
|
385
|
+
saveState();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function deleteConversation(id) {
|
|
390
|
+
if (!confirm('Êtes-vous sûr de vouloir supprimer cette conversation ?')) return;
|
|
391
|
+
try {
|
|
392
|
+
await apiDelete(`/conversations/${id}`);
|
|
393
|
+
await loadConversations();
|
|
394
|
+
if (state.currentConversationId === id) {
|
|
395
|
+
state.currentConversationId = null;
|
|
396
|
+
state.messages = [];
|
|
397
|
+
renderMessages();
|
|
738
398
|
}
|
|
739
|
-
},
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
399
|
+
} catch (e) { console.error('Erreur suppression:', e); }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function sendMessage(message) {
|
|
403
|
+
if (state.isLoading || !message.trim()) return;
|
|
404
|
+
|
|
405
|
+
state.lucyDidNotUnderstand = false;
|
|
406
|
+
|
|
407
|
+
if (!state.currentConversationId) await newConversation();
|
|
408
|
+
|
|
409
|
+
// Message utilisateur
|
|
410
|
+
state.messages.push({
|
|
411
|
+
id: Date.now(),
|
|
412
|
+
repondant: 'UTILISATEUR',
|
|
413
|
+
contenu: message,
|
|
414
|
+
created_date: new Date().toISOString(),
|
|
415
|
+
tokens_utilises: 0
|
|
416
|
+
});
|
|
417
|
+
state.currentMessage = '';
|
|
418
|
+
els.input.value = '';
|
|
419
|
+
els.input.style.height = 'auto';
|
|
420
|
+
updateSendBtn();
|
|
421
|
+
renderMessages();
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
state.isLoading = true;
|
|
425
|
+
els.input.disabled = true;
|
|
426
|
+
els.btnSend.disabled = true;
|
|
427
|
+
|
|
428
|
+
await apiPost(`/conversations/${state.currentConversationId}/messages`, { contenu: message });
|
|
429
|
+
await streamChatCompletion();
|
|
430
|
+
} catch (e) {
|
|
431
|
+
console.error('Erreur envoi message:', e);
|
|
432
|
+
if (e.status === 402) {
|
|
433
|
+
showModal('buy');
|
|
748
434
|
} else {
|
|
749
|
-
|
|
750
|
-
|
|
435
|
+
state.hasError = true;
|
|
436
|
+
renderMessages();
|
|
751
437
|
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
438
|
+
} finally {
|
|
439
|
+
state.isLoading = false;
|
|
440
|
+
els.input.disabled = false;
|
|
441
|
+
updateSendBtn();
|
|
442
|
+
els.input.focus();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function streamChatCompletion() {
|
|
447
|
+
const url = `${API_BASE}/conversations/${state.currentConversationId}/chat`;
|
|
448
|
+
const botIdx = state.messages.length;
|
|
449
|
+
|
|
450
|
+
state.messages.push({
|
|
451
|
+
id: Date.now() + 1,
|
|
452
|
+
repondant: 'CHATBOT',
|
|
453
|
+
contenu: '',
|
|
454
|
+
created_date: new Date().toISOString(),
|
|
455
|
+
tokens_utilises: 0
|
|
456
|
+
});
|
|
457
|
+
renderMessages();
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
const response = await fetch(url, {
|
|
461
|
+
method: 'POST',
|
|
462
|
+
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
|
|
463
|
+
body: JSON.stringify({ page_contexte: window.location.pathname })
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
if (!response.ok) throw { status: response.status, ...(await response.json()) };
|
|
467
|
+
|
|
468
|
+
const reader = response.body.getReader();
|
|
469
|
+
const decoder = new TextDecoder();
|
|
470
|
+
let fullContent = '';
|
|
471
|
+
|
|
472
|
+
while (true) {
|
|
473
|
+
const { done, value } = await reader.read();
|
|
474
|
+
if (done) break;
|
|
475
|
+
|
|
476
|
+
const text = decoder.decode(value);
|
|
477
|
+
for (const line of text.split('\n')) {
|
|
478
|
+
if (!line.startsWith('data: ')) continue;
|
|
479
|
+
try {
|
|
480
|
+
const data = JSON.parse(line.slice(6));
|
|
481
|
+
if (data.type === 'content') {
|
|
482
|
+
fullContent += data.content;
|
|
483
|
+
state.messages[botIdx].contenu = fullContent;
|
|
484
|
+
renderMessages();
|
|
485
|
+
} else if (data.type === 'done') {
|
|
486
|
+
state.messages[botIdx].id = data.message_id;
|
|
487
|
+
state.messages[botIdx].tokens_utilises = data.tokens_utilises;
|
|
488
|
+
await loadTokenStatus();
|
|
489
|
+
} else if (data.type === 'error') {
|
|
490
|
+
throw new Error(data.error);
|
|
491
|
+
}
|
|
492
|
+
} catch (e) { if (e.message) throw e; }
|
|
767
493
|
}
|
|
768
494
|
}
|
|
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
495
|
|
|
782
|
-
if (!
|
|
783
|
-
|
|
784
|
-
throw { status: response.status, ...error };
|
|
496
|
+
if (!state.messages[botIdx]?.contenu) {
|
|
497
|
+
state.messages.splice(botIdx, 1);
|
|
785
498
|
}
|
|
786
499
|
|
|
787
|
-
|
|
788
|
-
|
|
500
|
+
state.lucyDidNotUnderstand = checkIfLucyDidNotUnderstand(state.messages[botIdx]?.contenu || '');
|
|
501
|
+
if (!state.lucyDidNotUnderstand) state.hasError = false;
|
|
789
502
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
503
|
+
renderMessages();
|
|
504
|
+
await loadConversations();
|
|
505
|
+
} catch (e) {
|
|
506
|
+
state.hasError = true;
|
|
507
|
+
if (botIdx < state.messages.length && !state.messages[botIdx]?.contenu) {
|
|
508
|
+
state.messages.splice(botIdx, 1);
|
|
509
|
+
}
|
|
510
|
+
renderMessages();
|
|
511
|
+
throw e;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function sendFeedback() {
|
|
516
|
+
if (!state.currentConversationId) return;
|
|
517
|
+
state.feedbackSending = true;
|
|
518
|
+
try {
|
|
519
|
+
await apiPost('/feedback', {
|
|
520
|
+
conversation_id: state.currentConversationId,
|
|
521
|
+
description: state.feedbackDescription,
|
|
522
|
+
page_url: window.location.href
|
|
798
523
|
});
|
|
524
|
+
state.hasError = false;
|
|
525
|
+
state.lucyDidNotUnderstand = false;
|
|
526
|
+
hideModal('feedback');
|
|
527
|
+
renderMessages();
|
|
528
|
+
} catch (e) { console.error('Erreur feedback:', e); }
|
|
529
|
+
finally { state.feedbackSending = false; }
|
|
530
|
+
}
|
|
799
531
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
}
|
|
532
|
+
async function buyCredits() {
|
|
533
|
+
if (state.buyAmount < 10) return;
|
|
534
|
+
try {
|
|
535
|
+
const r = await apiPost('/tokens/buy', { montant_ht: state.buyAmount });
|
|
536
|
+
window.open(r.url_souscription, '_blank');
|
|
537
|
+
hideModal('buy');
|
|
538
|
+
} catch (e) { console.error('Erreur achat:', e); }
|
|
539
|
+
}
|
|
804
540
|
|
|
805
|
-
|
|
806
|
-
|
|
541
|
+
// ========================================================================
|
|
542
|
+
// MODALS
|
|
543
|
+
// ========================================================================
|
|
544
|
+
|
|
545
|
+
function showModal(name) {
|
|
546
|
+
if (name === 'buy') {
|
|
547
|
+
els.modalBuy.style.display = '';
|
|
548
|
+
updateBuyModal();
|
|
549
|
+
} else if (name === 'feedback') {
|
|
550
|
+
els.modalFeedback.style.display = '';
|
|
551
|
+
state.feedbackDescription = '';
|
|
552
|
+
els.feedbackDesc.value = '';
|
|
553
|
+
}
|
|
554
|
+
}
|
|
807
555
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
'Content-Type': 'application/json',
|
|
813
|
-
'X-CSRFToken': this.csrfToken
|
|
814
|
-
}
|
|
815
|
-
});
|
|
556
|
+
function hideModal(name) {
|
|
557
|
+
if (name === 'buy') els.modalBuy.style.display = 'none';
|
|
558
|
+
else if (name === 'feedback') els.modalFeedback.style.display = 'none';
|
|
559
|
+
}
|
|
816
560
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
}
|
|
561
|
+
// ========================================================================
|
|
562
|
+
// GUIDE
|
|
563
|
+
// ========================================================================
|
|
821
564
|
|
|
822
|
-
|
|
565
|
+
function checkFirstLaunch() {
|
|
566
|
+
if (!localStorage.getItem('lucy_assist_guide_seen') && state.conversations.length === 0) {
|
|
567
|
+
setTimeout(() => {
|
|
568
|
+
if (state.conversations.length === 0) {
|
|
569
|
+
els.guideOverlay.style.display = '';
|
|
570
|
+
openSidebar();
|
|
571
|
+
}
|
|
572
|
+
}, 2000);
|
|
823
573
|
}
|
|
824
|
-
}
|
|
825
|
-
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function guideNext() {
|
|
577
|
+
state.guideStep = 2;
|
|
578
|
+
els.guideStep1.style.display = 'none';
|
|
579
|
+
els.guideStep2.style.display = '';
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function guideFinish() {
|
|
583
|
+
els.guideOverlay.style.display = 'none';
|
|
584
|
+
localStorage.setItem('lucy_assist_guide_seen', 'true');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ========================================================================
|
|
588
|
+
// STATE PERSISTENCE
|
|
589
|
+
// ========================================================================
|
|
590
|
+
|
|
591
|
+
function saveState() {
|
|
592
|
+
sessionStorage.setItem('lucy_assist_state', JSON.stringify({
|
|
593
|
+
isOpen: state.isOpen,
|
|
594
|
+
isExpanded: state.isExpanded,
|
|
595
|
+
currentConversationId: state.currentConversationId,
|
|
596
|
+
showHistory: state.showHistory,
|
|
597
|
+
showDoc: state.showDoc
|
|
598
|
+
}));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function restoreState() {
|
|
602
|
+
try {
|
|
603
|
+
const s = JSON.parse(sessionStorage.getItem('lucy_assist_state'));
|
|
604
|
+
if (!s) return;
|
|
605
|
+
state.isOpen = s.isOpen || false;
|
|
606
|
+
state.isExpanded = s.isExpanded || false;
|
|
607
|
+
state.currentConversationId = s.currentConversationId || null;
|
|
608
|
+
state.showHistory = s.showHistory || false;
|
|
609
|
+
state.showDoc = s.showDoc || false;
|
|
610
|
+
} catch (e) {}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ========================================================================
|
|
614
|
+
// SEND BUTTON STATE
|
|
615
|
+
// ========================================================================
|
|
616
|
+
|
|
617
|
+
function updateSendBtn() {
|
|
618
|
+
els.btnSend.disabled = state.isLoading || !els.input.value.trim();
|
|
619
|
+
}
|
|
826
620
|
|
|
827
|
-
//
|
|
828
|
-
|
|
621
|
+
// ========================================================================
|
|
622
|
+
// INIT
|
|
623
|
+
// ========================================================================
|
|
624
|
+
|
|
625
|
+
function init() {
|
|
626
|
+
// Cache des elements
|
|
627
|
+
els = {
|
|
628
|
+
floatBtn: $('lucy-float-btn'),
|
|
629
|
+
sidebar: $('lucy-sidebar'),
|
|
630
|
+
tokens: $('lucy-tokens'),
|
|
631
|
+
btnBuyCredits: $('lucy-btn-buy-credits'),
|
|
632
|
+
btnExpand: $('lucy-btn-expand'),
|
|
633
|
+
btnHistory: $('lucy-btn-history'),
|
|
634
|
+
btnNew: $('lucy-btn-new'),
|
|
635
|
+
btnDoc: $('lucy-btn-doc'),
|
|
636
|
+
btnClose: $('lucy-btn-close'),
|
|
637
|
+
btnSend: $('lucy-btn-send'),
|
|
638
|
+
input: $('lucy-input'),
|
|
639
|
+
messages: $('lucy-messages'),
|
|
640
|
+
welcome: $('lucy-welcome'),
|
|
641
|
+
suggestions: $('lucy-suggestions'),
|
|
642
|
+
viewHistory: $('lucy-view-history'),
|
|
643
|
+
viewDoc: $('lucy-view-doc'),
|
|
644
|
+
viewChat: $('lucy-view-chat'),
|
|
645
|
+
historyList: $('lucy-history-list'),
|
|
646
|
+
feedbackContact: $('lucy-feedback-contact'),
|
|
647
|
+
feedbackError: $('lucy-feedback-error'),
|
|
648
|
+
btnFeedbackContact: $('lucy-btn-feedback-contact'),
|
|
649
|
+
btnFeedbackError: $('lucy-btn-feedback-error'),
|
|
650
|
+
modalBuy: $('lucy-modal-buy'),
|
|
651
|
+
modalFeedback: $('lucy-modal-feedback'),
|
|
652
|
+
buyAmount: $('lucy-buy-amount'),
|
|
653
|
+
buyTokens: $('lucy-buy-tokens'),
|
|
654
|
+
buyConvs: $('lucy-buy-convs'),
|
|
655
|
+
buyCancel: $('lucy-buy-cancel'),
|
|
656
|
+
buyConfirm: $('lucy-buy-confirm'),
|
|
657
|
+
feedbackDesc: $('lucy-feedback-desc'),
|
|
658
|
+
feedbackCancel: $('lucy-feedback-cancel'),
|
|
659
|
+
feedbackConfirm: $('lucy-feedback-confirm'),
|
|
660
|
+
guideOverlay: $('lucy-guide-overlay'),
|
|
661
|
+
guideStep1: $('lucy-guide-step1'),
|
|
662
|
+
guideStep2: $('lucy-guide-step2'),
|
|
663
|
+
guideSkip: $('lucy-guide-skip'),
|
|
664
|
+
guideNext: $('lucy-guide-next'),
|
|
665
|
+
guideFinish: $('lucy-guide-finish'),
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
// CSRF
|
|
669
|
+
csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value || getCookie('csrftoken') || '';
|
|
670
|
+
|
|
671
|
+
// Restaurer l'etat
|
|
672
|
+
restoreState();
|
|
673
|
+
|
|
674
|
+
// Appliquer l'etat initial
|
|
675
|
+
if (state.isOpen) {
|
|
676
|
+
els.floatBtn.style.display = 'none';
|
|
677
|
+
els.sidebar.style.display = '';
|
|
678
|
+
els.sidebar.classList.remove('hidden');
|
|
679
|
+
}
|
|
680
|
+
if (state.isExpanded) {
|
|
681
|
+
els.sidebar.classList.add('expanded');
|
|
682
|
+
}
|
|
683
|
+
updateExpandIcons();
|
|
684
|
+
|
|
685
|
+
if (state.showHistory) showView('history');
|
|
686
|
+
else if (state.showDoc) showView('doc');
|
|
687
|
+
else showView('chat');
|
|
688
|
+
|
|
689
|
+
// ---- EVENT LISTENERS ----
|
|
690
|
+
|
|
691
|
+
// Bouton flottant
|
|
692
|
+
els.floatBtn.addEventListener('click', toggleSidebar);
|
|
693
|
+
|
|
694
|
+
// Header buttons
|
|
695
|
+
els.btnExpand.addEventListener('click', toggleExpanded);
|
|
696
|
+
els.btnHistory.addEventListener('click', () => showView(state.showHistory ? 'chat' : 'history'));
|
|
697
|
+
els.btnNew.addEventListener('click', newConversation);
|
|
698
|
+
els.btnDoc.addEventListener('click', () => showView(state.showDoc ? 'chat' : 'doc'));
|
|
699
|
+
els.btnClose.addEventListener('click', closeSidebar);
|
|
700
|
+
|
|
701
|
+
// Input
|
|
702
|
+
els.input.addEventListener('input', () => {
|
|
703
|
+
state.currentMessage = els.input.value;
|
|
704
|
+
els.input.style.height = 'auto';
|
|
705
|
+
els.input.style.height = Math.min(els.input.scrollHeight, 128) + 'px';
|
|
706
|
+
updateSendBtn();
|
|
707
|
+
});
|
|
708
|
+
els.input.addEventListener('keydown', (e) => {
|
|
709
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
710
|
+
e.preventDefault();
|
|
711
|
+
sendMessage(els.input.value);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// Send
|
|
716
|
+
els.btnSend.addEventListener('click', () => sendMessage(els.input.value));
|
|
717
|
+
|
|
718
|
+
// Feedback
|
|
719
|
+
els.btnFeedbackContact.addEventListener('click', () => showModal('feedback'));
|
|
720
|
+
els.btnFeedbackError.addEventListener('click', () => showModal('feedback'));
|
|
721
|
+
|
|
722
|
+
// Buy credits
|
|
723
|
+
els.btnBuyCredits.addEventListener('click', () => showModal('buy'));
|
|
724
|
+
els.buyCancel.addEventListener('click', () => hideModal('buy'));
|
|
725
|
+
els.buyConfirm.addEventListener('click', buyCredits);
|
|
726
|
+
els.buyAmount.addEventListener('input', () => {
|
|
727
|
+
state.buyAmount = parseInt(els.buyAmount.value) || 10;
|
|
728
|
+
updateBuyModal();
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// Feedback modal
|
|
732
|
+
els.feedbackCancel.addEventListener('click', () => hideModal('feedback'));
|
|
733
|
+
els.feedbackConfirm.addEventListener('click', sendFeedback);
|
|
734
|
+
els.feedbackDesc.addEventListener('input', () => {
|
|
735
|
+
state.feedbackDescription = els.feedbackDesc.value;
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
// Guide
|
|
739
|
+
els.guideSkip.addEventListener('click', guideFinish);
|
|
740
|
+
els.guideNext.addEventListener('click', guideNext);
|
|
741
|
+
els.guideFinish.addEventListener('click', guideFinish);
|
|
742
|
+
|
|
743
|
+
// Raccourcis clavier globaux
|
|
744
|
+
document.addEventListener('keydown', (e) => {
|
|
745
|
+
if (e.ctrlKey && e.key === 'k') { e.preventDefault(); toggleSidebar(); }
|
|
746
|
+
if (e.key === 'Escape' && state.isOpen) closeSidebar();
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// Sauvegarder avant de quitter
|
|
750
|
+
window.addEventListener('beforeunload', saveState);
|
|
751
|
+
|
|
752
|
+
// Charger les donnees
|
|
753
|
+
loadTokenStatus();
|
|
754
|
+
loadConversations().then(() => {
|
|
755
|
+
if (state.currentConversationId) loadConversation(state.currentConversationId);
|
|
756
|
+
});
|
|
757
|
+
loadSuggestions();
|
|
758
|
+
checkFirstLaunch();
|
|
759
|
+
}
|
|
829
760
|
|
|
830
|
-
//
|
|
831
|
-
document.
|
|
832
|
-
|
|
833
|
-
|
|
761
|
+
// Lancer l'init quand le DOM est pret
|
|
762
|
+
if (document.readyState === 'loading') {
|
|
763
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
764
|
+
} else {
|
|
765
|
+
init();
|
|
834
766
|
}
|
|
835
|
-
});
|
|
767
|
+
})();
|