django-lucy-assist 0.1.0__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-0.1.0.dist-info/METADATA +206 -0
- django_lucy_assist-0.1.0.dist-info/RECORD +44 -0
- django_lucy_assist-0.1.0.dist-info/WHEEL +5 -0
- django_lucy_assist-0.1.0.dist-info/top_level.txt +1 -0
- lucy_assist/__init__.py +11 -0
- lucy_assist/admin.py +22 -0
- lucy_assist/apps.py +10 -0
- lucy_assist/conf.py +103 -0
- lucy_assist/constantes.py +120 -0
- lucy_assist/context_processors.py +65 -0
- lucy_assist/migrations/0001_initial.py +92 -0
- lucy_assist/migrations/__init__.py +0 -0
- lucy_assist/models/__init__.py +14 -0
- lucy_assist/models/base.py +54 -0
- lucy_assist/models/configuration.py +175 -0
- lucy_assist/models/conversation.py +54 -0
- lucy_assist/models/message.py +45 -0
- lucy_assist/models/project_context_cache.py +213 -0
- lucy_assist/services/__init__.py +21 -0
- lucy_assist/services/bug_notification_service.py +183 -0
- lucy_assist/services/claude_service.py +417 -0
- lucy_assist/services/context_service.py +350 -0
- lucy_assist/services/crud_service.py +364 -0
- lucy_assist/services/gitlab_service.py +248 -0
- lucy_assist/services/project_context_service.py +412 -0
- lucy_assist/services/tool_executor_service.py +343 -0
- lucy_assist/services/tools_definition.py +229 -0
- lucy_assist/signals.py +25 -0
- lucy_assist/static/lucy_assist/css/lucy-assist.css +160 -0
- lucy_assist/static/lucy_assist/image/icon-lucy.png +0 -0
- lucy_assist/static/lucy_assist/js/lucy-assist.js +824 -0
- lucy_assist/templates/lucy_assist/chatbot_sidebar.html +419 -0
- lucy_assist/templates/lucy_assist/partials/documentation_content.html +107 -0
- lucy_assist/tests/__init__.py +0 -0
- lucy_assist/tests/factories/__init__.py +15 -0
- lucy_assist/tests/factories/lucy_assist_factories.py +109 -0
- lucy_assist/tests/test_lucy_assist.py +186 -0
- lucy_assist/urls.py +36 -0
- lucy_assist/utils/__init__.py +7 -0
- lucy_assist/utils/log_utils.py +59 -0
- lucy_assist/utils/message_utils.py +130 -0
- lucy_assist/utils/token_utils.py +87 -0
- lucy_assist/views/__init__.py +13 -0
- lucy_assist/views/api_views.py +595 -0
|
@@ -0,0 +1,824 @@
|
|
|
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.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function lucyAssist() {
|
|
9
|
+
return {
|
|
10
|
+
// État de l'interface
|
|
11
|
+
isOpen: false,
|
|
12
|
+
isLoading: false,
|
|
13
|
+
showHistory: false,
|
|
14
|
+
showDoc: false,
|
|
15
|
+
showBuyCredits: false,
|
|
16
|
+
showFeedback: false,
|
|
17
|
+
showGuide: false,
|
|
18
|
+
guideStep: 1,
|
|
19
|
+
hasNewMessage: false,
|
|
20
|
+
hasError: false,
|
|
21
|
+
lucyDidNotUnderstand: false,
|
|
22
|
+
|
|
23
|
+
// Données
|
|
24
|
+
currentMessage: '',
|
|
25
|
+
messages: [],
|
|
26
|
+
conversations: [],
|
|
27
|
+
suggestions: [],
|
|
28
|
+
currentConversationId: null,
|
|
29
|
+
tokensDisponibles: 0,
|
|
30
|
+
buyAmount: 10,
|
|
31
|
+
feedbackDescription: '',
|
|
32
|
+
feedbackSending: false,
|
|
33
|
+
|
|
34
|
+
// Configuration
|
|
35
|
+
apiBaseUrl: '/lucy-assist/api',
|
|
36
|
+
csrfToken: '',
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Initialisation du composant
|
|
40
|
+
*/
|
|
41
|
+
init() {
|
|
42
|
+
// Récupérer le token CSRF
|
|
43
|
+
this.csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value ||
|
|
44
|
+
this.getCookie('csrftoken');
|
|
45
|
+
|
|
46
|
+
// Restaurer l'état depuis sessionStorage
|
|
47
|
+
this.restoreState();
|
|
48
|
+
|
|
49
|
+
// Charger les données initiales
|
|
50
|
+
this.loadTokenStatus();
|
|
51
|
+
this.loadConversations().then(() => {
|
|
52
|
+
// Recharger la conversation en cours si elle existe
|
|
53
|
+
if (this.currentConversationId) {
|
|
54
|
+
this.loadConversation(this.currentConversationId);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
this.loadSuggestions();
|
|
58
|
+
|
|
59
|
+
// Vérifier si c'est le premier lancement
|
|
60
|
+
this.checkFirstLaunch();
|
|
61
|
+
|
|
62
|
+
// Sauvegarder l'état quand isOpen change
|
|
63
|
+
this.$watch('isOpen', (value) => {
|
|
64
|
+
this.saveState();
|
|
65
|
+
if (value) {
|
|
66
|
+
this.$nextTick(() => {
|
|
67
|
+
this.$refs.messageInput?.focus();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Sauvegarder l'état quand la conversation change
|
|
73
|
+
this.$watch('currentConversationId', () => {
|
|
74
|
+
this.saveState();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Sauvegarder avant de quitter la page
|
|
78
|
+
window.addEventListener('beforeunload', () => {
|
|
79
|
+
this.saveState();
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Sauvegarde l'état dans sessionStorage
|
|
85
|
+
*/
|
|
86
|
+
saveState() {
|
|
87
|
+
const state = {
|
|
88
|
+
isOpen: this.isOpen,
|
|
89
|
+
currentConversationId: this.currentConversationId,
|
|
90
|
+
showHistory: this.showHistory,
|
|
91
|
+
showDoc: this.showDoc
|
|
92
|
+
};
|
|
93
|
+
sessionStorage.setItem('lucy_assist_state', JSON.stringify(state));
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Restaure l'état depuis sessionStorage
|
|
98
|
+
*/
|
|
99
|
+
restoreState() {
|
|
100
|
+
try {
|
|
101
|
+
const savedState = sessionStorage.getItem('lucy_assist_state');
|
|
102
|
+
if (savedState) {
|
|
103
|
+
const state = JSON.parse(savedState);
|
|
104
|
+
this.isOpen = state.isOpen || false;
|
|
105
|
+
this.currentConversationId = state.currentConversationId || null;
|
|
106
|
+
this.showHistory = state.showHistory || false;
|
|
107
|
+
this.showDoc = state.showDoc || false;
|
|
108
|
+
}
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.error('Erreur restauration état Lucy Assist:', e);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Gestion des raccourcis clavier
|
|
116
|
+
*/
|
|
117
|
+
handleKeydown(event) {
|
|
118
|
+
// Ctrl+K pour ouvrir
|
|
119
|
+
if (event.ctrlKey && event.key === 'k') {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
this.toggleSidebar();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Echap pour fermer
|
|
125
|
+
if (event.key === 'Escape' && this.isOpen) {
|
|
126
|
+
this.closeSidebar();
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Toggle du sidebar
|
|
132
|
+
*/
|
|
133
|
+
toggleSidebar() {
|
|
134
|
+
this.isOpen = !this.isOpen;
|
|
135
|
+
if (this.isOpen) {
|
|
136
|
+
this.hasNewMessage = false;
|
|
137
|
+
}
|
|
138
|
+
this.saveState();
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Fermeture du sidebar
|
|
143
|
+
*/
|
|
144
|
+
closeSidebar() {
|
|
145
|
+
this.isOpen = false;
|
|
146
|
+
this.saveState();
|
|
147
|
+
this.showHistory = false;
|
|
148
|
+
this.showDoc = false;
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Vérifie si c'est le premier lancement
|
|
153
|
+
*/
|
|
154
|
+
checkFirstLaunch() {
|
|
155
|
+
const hasSeenGuide = localStorage.getItem('lucy_assist_guide_seen');
|
|
156
|
+
if (!hasSeenGuide && this.conversations.length === 0) {
|
|
157
|
+
// Afficher le guide au premier chargement si pas de conversations
|
|
158
|
+
setTimeout(() => {
|
|
159
|
+
if (this.conversations.length === 0) {
|
|
160
|
+
this.showGuide = true;
|
|
161
|
+
this.isOpen = true;
|
|
162
|
+
}
|
|
163
|
+
}, 2000);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Navigation dans le guide
|
|
169
|
+
*/
|
|
170
|
+
nextGuideStep() {
|
|
171
|
+
this.guideStep++;
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
skipGuide() {
|
|
175
|
+
this.finishGuide();
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
finishGuide() {
|
|
179
|
+
this.showGuide = false;
|
|
180
|
+
localStorage.setItem('lucy_assist_guide_seen', 'true');
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Charge le statut des tokens
|
|
185
|
+
*/
|
|
186
|
+
async loadTokenStatus() {
|
|
187
|
+
try {
|
|
188
|
+
const response = await this.apiGet('/tokens/status');
|
|
189
|
+
this.tokensDisponibles = response.tokens_disponibles || 0;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error('Erreur chargement tokens:', error);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Charge la liste des conversations
|
|
197
|
+
*/
|
|
198
|
+
async loadConversations() {
|
|
199
|
+
try {
|
|
200
|
+
const response = await this.apiGet('/conversations');
|
|
201
|
+
this.conversations = response.conversations || [];
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error('Erreur chargement conversations:', error);
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Charge les suggestions
|
|
209
|
+
*/
|
|
210
|
+
async loadSuggestions() {
|
|
211
|
+
try {
|
|
212
|
+
const response = await this.apiGet('/suggestions');
|
|
213
|
+
this.suggestions = response.suggestions || [];
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error('Erreur chargement suggestions:', error);
|
|
216
|
+
// Suggestions par défaut
|
|
217
|
+
this.suggestions = [
|
|
218
|
+
"Comment créer un nouveau membre ?",
|
|
219
|
+
"Comment effectuer un paiement ?",
|
|
220
|
+
"Où trouver la liste des adhésions ?"
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Crée une nouvelle conversation
|
|
227
|
+
*/
|
|
228
|
+
async newConversation() {
|
|
229
|
+
try {
|
|
230
|
+
const response = await this.apiPost('/conversations', {
|
|
231
|
+
page_contexte: window.location.pathname
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
this.currentConversationId = response.id;
|
|
235
|
+
this.messages = [];
|
|
236
|
+
this.showHistory = false;
|
|
237
|
+
this.showDoc = false;
|
|
238
|
+
this.saveState();
|
|
239
|
+
|
|
240
|
+
// Recharger les conversations
|
|
241
|
+
await this.loadConversations();
|
|
242
|
+
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error('Erreur création conversation:', error);
|
|
245
|
+
this.showToast('Erreur lors de la création de la conversation', 'error');
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Charge une conversation existante
|
|
251
|
+
*/
|
|
252
|
+
async loadConversation(conversationId) {
|
|
253
|
+
try {
|
|
254
|
+
const response = await this.apiGet(`/conversations/${conversationId}`);
|
|
255
|
+
|
|
256
|
+
this.currentConversationId = conversationId;
|
|
257
|
+
this.messages = response.messages || [];
|
|
258
|
+
this.showHistory = false;
|
|
259
|
+
this.saveState();
|
|
260
|
+
|
|
261
|
+
// Scroll vers le bas avec un petit délai pour laisser le DOM se mettre à jour
|
|
262
|
+
this.$nextTick(() => {
|
|
263
|
+
setTimeout(() => {
|
|
264
|
+
this.scrollToBottom();
|
|
265
|
+
}, 100);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error('Erreur chargement conversation:', error);
|
|
270
|
+
// Si la conversation n'existe plus, réinitialiser
|
|
271
|
+
this.currentConversationId = null;
|
|
272
|
+
this.messages = [];
|
|
273
|
+
this.saveState();
|
|
274
|
+
this.showToast('Erreur lors du chargement de la conversation', 'error');
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Supprime une conversation
|
|
280
|
+
*/
|
|
281
|
+
async deleteConversation(conversationId) {
|
|
282
|
+
if (!confirm('Êtes-vous sûr de vouloir supprimer cette conversation ?')) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
await this.apiDelete(`/conversations/${conversationId}`);
|
|
288
|
+
|
|
289
|
+
// Recharger les conversations
|
|
290
|
+
await this.loadConversations();
|
|
291
|
+
|
|
292
|
+
// Si c'était la conversation courante, reset
|
|
293
|
+
if (this.currentConversationId === conversationId) {
|
|
294
|
+
this.currentConversationId = null;
|
|
295
|
+
this.messages = [];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.showToast('Conversation supprimée', 'success');
|
|
299
|
+
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.error('Erreur suppression conversation:', error);
|
|
302
|
+
this.showToast('Erreur lors de la suppression', 'error');
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Envoie le message courant
|
|
308
|
+
*/
|
|
309
|
+
async sendCurrentMessage() {
|
|
310
|
+
const message = this.currentMessage.trim();
|
|
311
|
+
if (!message) return;
|
|
312
|
+
|
|
313
|
+
await this.sendMessage(message);
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Envoie un message (depuis l'input ou une suggestion)
|
|
318
|
+
*/
|
|
319
|
+
async sendMessage(message) {
|
|
320
|
+
if (this.isLoading) return;
|
|
321
|
+
|
|
322
|
+
// Réinitialiser le flag "Lucy n'a pas compris" pour le nouveau message
|
|
323
|
+
this.lucyDidNotUnderstand = false;
|
|
324
|
+
|
|
325
|
+
// Créer une conversation si nécessaire
|
|
326
|
+
if (!this.currentConversationId) {
|
|
327
|
+
await this.newConversation();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Ajouter le message utilisateur à l'affichage
|
|
331
|
+
const userMessage = {
|
|
332
|
+
id: Date.now(),
|
|
333
|
+
repondant: 'UTILISATEUR',
|
|
334
|
+
contenu: message,
|
|
335
|
+
created_date: new Date().toISOString(),
|
|
336
|
+
tokens_utilises: 0
|
|
337
|
+
};
|
|
338
|
+
this.messages.push(userMessage);
|
|
339
|
+
this.currentMessage = '';
|
|
340
|
+
|
|
341
|
+
// Scroll vers le bas
|
|
342
|
+
this.$nextTick(() => {
|
|
343
|
+
this.scrollToBottom();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Envoyer au serveur
|
|
347
|
+
try {
|
|
348
|
+
this.isLoading = true;
|
|
349
|
+
|
|
350
|
+
// Envoyer le message utilisateur
|
|
351
|
+
await this.apiPost(`/conversations/${this.currentConversationId}/messages`, {
|
|
352
|
+
contenu: message
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Demander une réponse à Claude (avec streaming)
|
|
356
|
+
await this.streamChatCompletion();
|
|
357
|
+
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error('Erreur envoi message:', error);
|
|
360
|
+
|
|
361
|
+
if (error.status === 402) {
|
|
362
|
+
// Pas assez de crédits
|
|
363
|
+
this.showBuyCredits = true;
|
|
364
|
+
} else {
|
|
365
|
+
// Marquer qu'il y a eu une erreur pour proposer le feedback
|
|
366
|
+
this.hasError = true;
|
|
367
|
+
this.showToast('Erreur lors de l\'envoi du message', 'error');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
} finally {
|
|
371
|
+
this.isLoading = false;
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Stream la réponse de Claude
|
|
377
|
+
*/
|
|
378
|
+
async streamChatCompletion() {
|
|
379
|
+
const url = `${this.apiBaseUrl}/conversations/${this.currentConversationId}/chat`;
|
|
380
|
+
|
|
381
|
+
// Index du message bot dans le tableau
|
|
382
|
+
const botMessageIndex = this.messages.length;
|
|
383
|
+
|
|
384
|
+
// Créer un placeholder pour la réponse
|
|
385
|
+
this.messages.push({
|
|
386
|
+
id: Date.now() + 1,
|
|
387
|
+
repondant: 'CHATBOT',
|
|
388
|
+
contenu: '',
|
|
389
|
+
created_date: new Date().toISOString(),
|
|
390
|
+
tokens_utilises: 0
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const response = await fetch(url, {
|
|
395
|
+
method: 'POST',
|
|
396
|
+
headers: {
|
|
397
|
+
'Content-Type': 'application/json',
|
|
398
|
+
'X-CSRFToken': this.csrfToken
|
|
399
|
+
},
|
|
400
|
+
body: JSON.stringify({
|
|
401
|
+
page_contexte: window.location.pathname
|
|
402
|
+
})
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
const errorData = await response.json();
|
|
407
|
+
throw { status: response.status, ...errorData };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const reader = response.body.getReader();
|
|
411
|
+
const decoder = new TextDecoder();
|
|
412
|
+
let fullContent = '';
|
|
413
|
+
|
|
414
|
+
while (true) {
|
|
415
|
+
const { done, value } = await reader.read();
|
|
416
|
+
if (done) break;
|
|
417
|
+
|
|
418
|
+
const text = decoder.decode(value);
|
|
419
|
+
const lines = text.split('\n');
|
|
420
|
+
|
|
421
|
+
for (const line of lines) {
|
|
422
|
+
if (line.startsWith('data: ')) {
|
|
423
|
+
try {
|
|
424
|
+
const data = JSON.parse(line.slice(6));
|
|
425
|
+
|
|
426
|
+
if (data.type === 'content') {
|
|
427
|
+
fullContent += data.content;
|
|
428
|
+
// Forcer la réactivité Alpine en réassignant l'objet
|
|
429
|
+
this.messages[botMessageIndex] = {
|
|
430
|
+
...this.messages[botMessageIndex],
|
|
431
|
+
contenu: fullContent
|
|
432
|
+
};
|
|
433
|
+
this.$nextTick(() => this.scrollToBottom());
|
|
434
|
+
} else if (data.type === 'done') {
|
|
435
|
+
// Mise à jour finale avec l'ID et les tokens
|
|
436
|
+
this.messages[botMessageIndex] = {
|
|
437
|
+
...this.messages[botMessageIndex],
|
|
438
|
+
id: data.message_id,
|
|
439
|
+
tokens_utilises: data.tokens_utilises
|
|
440
|
+
};
|
|
441
|
+
// Mettre à jour les tokens
|
|
442
|
+
await this.loadTokenStatus();
|
|
443
|
+
} else if (data.type === 'error') {
|
|
444
|
+
throw new Error(data.error);
|
|
445
|
+
}
|
|
446
|
+
} catch (e) {
|
|
447
|
+
if (e.message) throw e;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Supprimer le message placeholder s'il est resté vide
|
|
454
|
+
if (!this.messages[botMessageIndex]?.contenu) {
|
|
455
|
+
this.messages.splice(botMessageIndex, 1);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Vérifier si Lucy n'a pas compris le message
|
|
459
|
+
const botResponse = this.messages[botMessageIndex]?.contenu || '';
|
|
460
|
+
this.lucyDidNotUnderstand = this.checkIfLucyDidNotUnderstand(botResponse);
|
|
461
|
+
|
|
462
|
+
// Réinitialiser le flag d'erreur après une réponse réussie (sauf si Lucy n'a pas compris)
|
|
463
|
+
if (!this.lucyDidNotUnderstand) {
|
|
464
|
+
this.hasError = false;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Recharger les conversations pour mettre à jour les titres
|
|
468
|
+
await this.loadConversations();
|
|
469
|
+
|
|
470
|
+
} catch (error) {
|
|
471
|
+
// Marquer qu'il y a eu une erreur
|
|
472
|
+
this.hasError = true;
|
|
473
|
+
|
|
474
|
+
// Supprimer le message placeholder vide en cas d'erreur
|
|
475
|
+
if (botMessageIndex < this.messages.length && !this.messages[botMessageIndex]?.contenu) {
|
|
476
|
+
this.messages.splice(botMessageIndex, 1);
|
|
477
|
+
}
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Vérifie si Lucy n'a pas compris ou ne peut pas aider l'utilisateur
|
|
484
|
+
*/
|
|
485
|
+
checkIfLucyDidNotUnderstand(response) {
|
|
486
|
+
if (!response) return false;
|
|
487
|
+
|
|
488
|
+
const lowerResponse = response.toLowerCase();
|
|
489
|
+
|
|
490
|
+
// Patterns indiquant que Lucy n'a pas compris ou ne peut pas aider
|
|
491
|
+
const notUnderstoodPatterns = [
|
|
492
|
+
// Ne comprend pas
|
|
493
|
+
'je ne comprends pas',
|
|
494
|
+
'je n\'ai pas compris',
|
|
495
|
+
'je n\'arrive pas à comprendre',
|
|
496
|
+
'pourriez-vous reformuler',
|
|
497
|
+
'pouvez-vous reformuler',
|
|
498
|
+
'pourriez-vous préciser',
|
|
499
|
+
'pouvez-vous préciser',
|
|
500
|
+
'je ne suis pas sûr de comprendre',
|
|
501
|
+
'je ne suis pas certain de comprendre',
|
|
502
|
+
'votre message n\'est pas clair',
|
|
503
|
+
'erreur de frappe',
|
|
504
|
+
'erreur de saisie',
|
|
505
|
+
'message incompréhensible',
|
|
506
|
+
'je n\'ai pas pu interpréter',
|
|
507
|
+
'que souhaitez-vous faire',
|
|
508
|
+
'que voulez-vous faire',
|
|
509
|
+
// Ne peut pas aider / pas accès
|
|
510
|
+
'je n\'ai pas accès',
|
|
511
|
+
'je n\'ai pas les outils',
|
|
512
|
+
'je n\'ai pas la possibilité',
|
|
513
|
+
'je ne peux pas',
|
|
514
|
+
'je ne suis pas en mesure',
|
|
515
|
+
'pas accès aux outils',
|
|
516
|
+
'outils nécessaires',
|
|
517
|
+
'fonctionnalité non disponible',
|
|
518
|
+
'cette fonctionnalité n\'est pas',
|
|
519
|
+
'je ne dispose pas',
|
|
520
|
+
'mes outils actuels ne permettent pas',
|
|
521
|
+
'mes outils ne permettent pas',
|
|
522
|
+
'je n\'arrive pas à',
|
|
523
|
+
'impossible de réaliser',
|
|
524
|
+
'je ne trouve pas',
|
|
525
|
+
'modèle non disponible',
|
|
526
|
+
'n\'existe pas dans',
|
|
527
|
+
// Questions de clarification multiples
|
|
528
|
+
'pouvez-vous me dire',
|
|
529
|
+
'pourriez-vous me préciser',
|
|
530
|
+
'solutions possibles',
|
|
531
|
+
// Hors du périmètre / redirection vers autres outils
|
|
532
|
+
'je suis spécialisée',
|
|
533
|
+
'je vous recommande de consulter',
|
|
534
|
+
'je vous conseille de',
|
|
535
|
+
'en dehors de mon périmètre',
|
|
536
|
+
'hors de mon champ',
|
|
537
|
+
'ne fait pas partie de mes compétences',
|
|
538
|
+
'dépasse mes capacités',
|
|
539
|
+
'assistants culinaires',
|
|
540
|
+
'chatgpt',
|
|
541
|
+
'google assistant',
|
|
542
|
+
'sites spécialisés',
|
|
543
|
+
'comment puis-je vous aider avec votre crm',
|
|
544
|
+
'y a-t-il quelque chose que vous souhaitez faire'
|
|
545
|
+
];
|
|
546
|
+
|
|
547
|
+
return notUnderstoodPatterns.some(pattern => lowerResponse.includes(pattern));
|
|
548
|
+
},
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Ouvre le modal de feedback
|
|
552
|
+
*/
|
|
553
|
+
openFeedback() {
|
|
554
|
+
this.showFeedback = true;
|
|
555
|
+
this.feedbackDescription = '';
|
|
556
|
+
},
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Ferme le modal de feedback
|
|
560
|
+
*/
|
|
561
|
+
closeFeedback() {
|
|
562
|
+
this.showFeedback = false;
|
|
563
|
+
this.feedbackDescription = '';
|
|
564
|
+
},
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Envoie le feedback à Revolucy
|
|
568
|
+
*/
|
|
569
|
+
async sendFeedback() {
|
|
570
|
+
if (!this.currentConversationId) {
|
|
571
|
+
this.showToast('Aucune conversation à signaler', 'warning');
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
this.feedbackSending = true;
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
const response = await this.apiPost('/feedback', {
|
|
579
|
+
conversation_id: this.currentConversationId,
|
|
580
|
+
description: this.feedbackDescription,
|
|
581
|
+
page_url: window.location.href
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
this.showToast(response.message || 'Feedback envoyé avec succès !', 'success');
|
|
585
|
+
this.closeFeedback();
|
|
586
|
+
this.hasError = false;
|
|
587
|
+
this.lucyDidNotUnderstand = false;
|
|
588
|
+
|
|
589
|
+
} catch (error) {
|
|
590
|
+
console.error('Erreur envoi feedback:', error);
|
|
591
|
+
this.showToast('Erreur lors de l\'envoi du feedback', 'error');
|
|
592
|
+
} finally {
|
|
593
|
+
this.feedbackSending = false;
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Achat de crédits
|
|
599
|
+
*/
|
|
600
|
+
async buyCredits() {
|
|
601
|
+
if (this.buyAmount < 10) {
|
|
602
|
+
this.showToast('Le montant minimum est de 10 EUR', 'warning');
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
const response = await this.apiPost('/tokens/buy', {
|
|
608
|
+
montant_ht: this.buyAmount
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// Ouvrir l'URL de souscription dans un nouvel onglet
|
|
612
|
+
window.open(response.url_souscription, '_blank');
|
|
613
|
+
|
|
614
|
+
this.showBuyCredits = false;
|
|
615
|
+
this.showToast('Redirection vers la page de paiement...', 'info');
|
|
616
|
+
|
|
617
|
+
} catch (error) {
|
|
618
|
+
console.error('Erreur achat crédits:', error);
|
|
619
|
+
this.showToast('Erreur lors de la génération du lien de paiement', 'error');
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Calcule le nombre de tokens pour un montant
|
|
625
|
+
*/
|
|
626
|
+
calculateTokens(amount) {
|
|
627
|
+
return Math.floor((amount / 10) * 1000000);
|
|
628
|
+
},
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Formate un nombre de tokens
|
|
632
|
+
*/
|
|
633
|
+
formatTokens(tokens) {
|
|
634
|
+
if (tokens >= 1000000) {
|
|
635
|
+
return (tokens / 1000000).toFixed(1) + 'M';
|
|
636
|
+
} else if (tokens >= 1000) {
|
|
637
|
+
return (tokens / 1000).toFixed(0) + 'K';
|
|
638
|
+
}
|
|
639
|
+
return tokens.toString();
|
|
640
|
+
},
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Calcule et formate le coût en euros pour un nombre de tokens
|
|
644
|
+
* Prix par défaut: 10€ par million de tokens
|
|
645
|
+
*/
|
|
646
|
+
formatTokenCost(tokens) {
|
|
647
|
+
const prixParMillion = 10; // 10€ par million de tokens
|
|
648
|
+
const cout = (tokens / 1000000) * prixParMillion;
|
|
649
|
+
return cout.toFixed(4) + '€';
|
|
650
|
+
},
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Formate une date
|
|
654
|
+
*/
|
|
655
|
+
formatDate(dateString) {
|
|
656
|
+
const date = new Date(dateString);
|
|
657
|
+
const now = new Date();
|
|
658
|
+
const diff = now - date;
|
|
659
|
+
|
|
660
|
+
// Moins d'une minute
|
|
661
|
+
if (diff < 60000) {
|
|
662
|
+
return 'À l\'instant';
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Moins d'une heure
|
|
666
|
+
if (diff < 3600000) {
|
|
667
|
+
const minutes = Math.floor(diff / 60000);
|
|
668
|
+
return `Il y a ${minutes} min`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Aujourd'hui
|
|
672
|
+
if (date.toDateString() === now.toDateString()) {
|
|
673
|
+
return `Aujourd'hui à ${date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Cette semaine
|
|
677
|
+
if (diff < 7 * 24 * 3600000) {
|
|
678
|
+
return date.toLocaleDateString('fr-FR', { weekday: 'long', hour: '2-digit', minute: '2-digit' });
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' });
|
|
682
|
+
},
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Formate une heure
|
|
686
|
+
*/
|
|
687
|
+
formatTime(dateString) {
|
|
688
|
+
const date = new Date(dateString);
|
|
689
|
+
return date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
|
|
690
|
+
},
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Formate le contenu d'un message (markdown basique)
|
|
694
|
+
*/
|
|
695
|
+
formatMessage(content) {
|
|
696
|
+
if (!content) return '';
|
|
697
|
+
|
|
698
|
+
// Échapper le HTML
|
|
699
|
+
let formatted = content
|
|
700
|
+
.replace(/&/g, '&')
|
|
701
|
+
.replace(/</g, '<')
|
|
702
|
+
.replace(/>/g, '>');
|
|
703
|
+
|
|
704
|
+
// Markdown basique
|
|
705
|
+
formatted = formatted
|
|
706
|
+
// Gras
|
|
707
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
708
|
+
// Italique
|
|
709
|
+
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
710
|
+
// Code inline
|
|
711
|
+
.replace(/`(.*?)`/g, '<code class="bg-base-300 px-1 rounded">$1</code>')
|
|
712
|
+
// Liens
|
|
713
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" class="link link-primary">$1</a>')
|
|
714
|
+
// Sauts de ligne
|
|
715
|
+
.replace(/\n/g, '<br>');
|
|
716
|
+
|
|
717
|
+
return formatted;
|
|
718
|
+
},
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Scroll vers le bas de la zone de messages
|
|
722
|
+
*/
|
|
723
|
+
scrollToBottom() {
|
|
724
|
+
const container = this.$refs.messagesContainer;
|
|
725
|
+
if (container) {
|
|
726
|
+
container.scrollTop = container.scrollHeight;
|
|
727
|
+
}
|
|
728
|
+
},
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Affiche un toast de notification
|
|
732
|
+
*/
|
|
733
|
+
showToast(message, type = 'info') {
|
|
734
|
+
// Utiliser le système de toast Alpine.js existant si disponible
|
|
735
|
+
if (window.Alpine && window.toastData) {
|
|
736
|
+
window.toastData.show(message, type);
|
|
737
|
+
} else {
|
|
738
|
+
// Fallback: alert simple
|
|
739
|
+
console.log(`[${type}] ${message}`);
|
|
740
|
+
}
|
|
741
|
+
},
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Récupère un cookie
|
|
745
|
+
*/
|
|
746
|
+
getCookie(name) {
|
|
747
|
+
let cookieValue = null;
|
|
748
|
+
if (document.cookie && document.cookie !== '') {
|
|
749
|
+
const cookies = document.cookie.split(';');
|
|
750
|
+
for (let i = 0; i < cookies.length; i++) {
|
|
751
|
+
const cookie = cookies[i].trim();
|
|
752
|
+
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
|
753
|
+
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return cookieValue;
|
|
759
|
+
},
|
|
760
|
+
|
|
761
|
+
// Méthodes utilitaires pour les appels API
|
|
762
|
+
async apiGet(endpoint) {
|
|
763
|
+
const response = await fetch(`${this.apiBaseUrl}${endpoint}`, {
|
|
764
|
+
method: 'GET',
|
|
765
|
+
headers: {
|
|
766
|
+
'Content-Type': 'application/json',
|
|
767
|
+
'X-CSRFToken': this.csrfToken
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
if (!response.ok) {
|
|
772
|
+
const error = await response.json();
|
|
773
|
+
throw { status: response.status, ...error };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return response.json();
|
|
777
|
+
},
|
|
778
|
+
|
|
779
|
+
async apiPost(endpoint, data) {
|
|
780
|
+
const response = await fetch(`${this.apiBaseUrl}${endpoint}`, {
|
|
781
|
+
method: 'POST',
|
|
782
|
+
headers: {
|
|
783
|
+
'Content-Type': 'application/json',
|
|
784
|
+
'X-CSRFToken': this.csrfToken
|
|
785
|
+
},
|
|
786
|
+
body: JSON.stringify(data)
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
if (!response.ok) {
|
|
790
|
+
const error = await response.json();
|
|
791
|
+
throw { status: response.status, ...error };
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return response.json();
|
|
795
|
+
},
|
|
796
|
+
|
|
797
|
+
async apiDelete(endpoint) {
|
|
798
|
+
const response = await fetch(`${this.apiBaseUrl}${endpoint}`, {
|
|
799
|
+
method: 'DELETE',
|
|
800
|
+
headers: {
|
|
801
|
+
'Content-Type': 'application/json',
|
|
802
|
+
'X-CSRFToken': this.csrfToken
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
if (!response.ok) {
|
|
807
|
+
const error = await response.json();
|
|
808
|
+
throw { status: response.status, ...error };
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return response.json();
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Exposer la fonction globalement pour que x-data puisse y accéder
|
|
817
|
+
window.lucyAssist = lucyAssist;
|
|
818
|
+
|
|
819
|
+
// Enregistrer le composant avec Alpine si disponible
|
|
820
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
821
|
+
if (window.Alpine && window.Alpine.data) {
|
|
822
|
+
window.Alpine.data('lucyAssist', lucyAssist);
|
|
823
|
+
}
|
|
824
|
+
});
|