django-lucy-assist 1.2.7__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-lucy-assist
3
- Version: 1.2.7
3
+ Version: 1.2.8
4
4
  Summary: Assistant IA intelligent Revolucy pour outil métier
5
5
  Author-email: Revolucy <hello@revolucy.fr>
6
6
  Maintainer-email: Maxence <hello@revolucy.fr>
@@ -1,4 +1,4 @@
1
- lucy_assist/__init__.py,sha256=EkRnUX4L8-zS6xa8n69SBMnNBFN_e2WIfefZ1Kj7YBE,325
1
+ lucy_assist/__init__.py,sha256=wTA9Xnf5XEPL_0lrQlF26nBIrmbTFMCazE4-6A1hPYA,325
2
2
  lucy_assist/admin.py,sha256=LnIXlYOIanAUoCPYpShAoYNzOr1XQukK2RSwlsWWXis,3086
3
3
  lucy_assist/apps.py,sha256=zHZtlBXs5ML4CKtGg7xDyptSWzLfB1ks2VvbXF50hdo,264
4
4
  lucy_assist/conf.py,sha256=Yyrhlyjj7RFf2jNYj9C__azQWR0tgJpU09ExfTpkiLM,4189
@@ -26,8 +26,8 @@ lucy_assist/services/tools_definition.py,sha256=cLRDx49eIGvElqT4avMtdsPzJO1mkcHj
26
26
  lucy_assist/services/view_discovery_service.py,sha256=5FybECCoHUkDhUQhdZ_tJ4Lei6Muyo4L-zISL5ELRfs,13096
27
27
  lucy_assist/static/lucy_assist/css/lucy-assist.css,sha256=tLCFhIg744wE7aED1wRC31CId-uLhL5QnscdNoOtQOA,24305
28
28
  lucy_assist/static/lucy_assist/image/icon-lucy.png,sha256=FOYlwXAt40Gr9jsWFmhgPivYOBFWKeYW0lxJI5Up-GM,6710
29
- lucy_assist/static/lucy_assist/js/lucy-assist.js,sha256=44ScbPrvyW1I1tc27gRHOAjrTjf-aWLMwaOwj-43Wpk,29474
30
- lucy_assist/templates/lucy_assist/chatbot_sidebar.html,sha256=D1XC_roCJ1Djs6N5YoTWwI1Ip8SXSphaVsuNSEbB32Y,11437
29
+ lucy_assist/static/lucy_assist/js/lucy-assist.js,sha256=eYipYnKZ9_qDfBxRkjVvKDFb0Vzuvroj7bDdRYcG5Ds,31412
30
+ lucy_assist/templates/lucy_assist/chatbot_sidebar.html,sha256=67dlLfQj_Sc73hQxBYDcqUHVtOB_gQm9OtGp-_rMi9A,11428
31
31
  lucy_assist/templates/lucy_assist/partials/documentation_content.html,sha256=8O1KIsr269s5BnQKhlxK5SIG7NKkF4fUCp3tB66xhfs,3784
32
32
  lucy_assist/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  lucy_assist/tests/test_lucy_assist.py,sha256=KBvuED10_gOJWdaRS2zMigM3W5Kp00KhMEwaR8tvhUc,6949
@@ -39,7 +39,7 @@ lucy_assist/utils/message_utils.py,sha256=YzcLHnl1ig4d5_utHCJwgxS7tKmd49Q-tuo78e
39
39
  lucy_assist/utils/token_utils.py,sha256=aBzyKVqpU67rAetO_Ee3QIfPtbNyjyWSe6qPfzIRJF8,2608
40
40
  lucy_assist/views/__init__.py,sha256=uUPYpuHlBC8j7zKS_DDoWjwpCpRnOIXETY-S2-Ss0cY,288
41
41
  lucy_assist/views/api_views.py,sha256=sJwKNC9-mi1zMbgyKZTVvT3pz4hMmw4jR0rrS7iJR-g,23619
42
- django_lucy_assist-1.2.7.dist-info/METADATA,sha256=2pMJnzX9gPbgh7w6cNh0vD-8fp4CwdLIHQOVKgMLCaQ,5899
43
- django_lucy_assist-1.2.7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
44
- django_lucy_assist-1.2.7.dist-info/top_level.txt,sha256=T-UCiwpn5yF3Oem3234TUpSVnEgbkrM2rGz9Tz5N-QA,12
45
- django_lucy_assist-1.2.7.dist-info/RECORD,,
42
+ django_lucy_assist-1.2.8.dist-info/METADATA,sha256=TKK8fLdPuQlp_U8ZQyPrMMGgj8kCr1yTBcNDKBL-Aac,5899
43
+ django_lucy_assist-1.2.8.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
44
+ django_lucy_assist-1.2.8.dist-info/top_level.txt,sha256=T-UCiwpn5yF3Oem3234TUpSVnEgbkrM2rGz9Tz5N-QA,12
45
+ django_lucy_assist-1.2.8.dist-info/RECORD,,
lucy_assist/__init__.py CHANGED
@@ -5,7 +5,7 @@ Un chatbot IA base sur Mistral AI, integrable dans n'importe quelle
5
5
  application Django pour fournir une assistance contextuelle aux utilisateurs.
6
6
  """
7
7
 
8
- __version__ = '1.2.7'
8
+ __version__ = '1.2.8'
9
9
  __author__ = 'Revolucy'
10
10
 
11
11
  default_app_config = 'lucy_assist.apps.LucyAssistConfig'
@@ -36,11 +36,18 @@
36
36
  let csrfToken = '';
37
37
 
38
38
  // ========================================================================
39
- // ELEMENTS DOM (caches apres init)
39
+ // ELEMENTS DOM
40
40
  // ========================================================================
41
- const $ = (id) => document.getElementById(id);
41
+ const el = {};
42
42
 
43
- let els = {};
43
+ function $(id) {
44
+ return document.getElementById(id);
45
+ }
46
+
47
+ /** Attache un event listener si l'element existe */
48
+ function on(element, event, handler) {
49
+ if (element) element.addEventListener(event, handler);
50
+ }
44
51
 
45
52
  // ========================================================================
46
53
  // HELPERS
@@ -66,9 +73,9 @@
66
73
  const now = new Date();
67
74
  const diff = now - date;
68
75
  if (diff < 60000) return "À l'instant";
69
- if (diff < 3600000) return `Il y a ${Math.floor(diff / 60000)} min`;
76
+ if (diff < 3600000) return 'Il y a ' + Math.floor(diff / 60000) + ' min';
70
77
  if (date.toDateString() === now.toDateString())
71
- return `Aujourd'hui à ${date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`;
78
+ return "Aujourd'hui à " + date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
72
79
  if (diff < 7 * 86400000)
73
80
  return date.toLocaleDateString('fr-FR', { weekday: 'long', hour: '2-digit', minute: '2-digit' });
74
81
  return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' });
@@ -84,7 +91,7 @@
84
91
 
85
92
  function formatMessage(content) {
86
93
  if (!content) return '';
87
- let f = escapeHtml(content);
94
+ var f = escapeHtml(content);
88
95
  f = f.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
89
96
  f = f.replace(/\*(.*?)\*/g, '<em>$1</em>');
90
97
  f = f.replace(/`(.*?)`/g, '<code style="background:rgba(0,0,0,.1);padding:0 4px;border-radius:3px;">$1</code>');
@@ -95,8 +102,8 @@
95
102
 
96
103
  function checkIfLucyDidNotUnderstand(response) {
97
104
  if (!response) return false;
98
- const lower = response.toLowerCase();
99
- const patterns = [
105
+ var lower = response.toLowerCase();
106
+ var patterns = [
100
107
  'je ne comprends pas', "je n'ai pas compris", 'pourriez-vous reformuler',
101
108
  'pouvez-vous reformuler', 'pourriez-vous préciser', 'pouvez-vous préciser',
102
109
  "je ne suis pas sûr de comprendre", "je n'ai pas accès", "je n'ai pas les outils",
@@ -105,7 +112,7 @@
105
112
  'impossible de réaliser', 'en dehors de mon périmètre', 'hors de mon champ',
106
113
  'dépasse mes capacités', 'comment puis-je vous aider avec votre crm'
107
114
  ];
108
- return patterns.some(p => lower.includes(p));
115
+ return patterns.some(function (p) { return lower.includes(p); });
109
116
  }
110
117
 
111
118
  // ========================================================================
@@ -113,29 +120,29 @@
113
120
  // ========================================================================
114
121
 
115
122
  async function apiGet(endpoint) {
116
- const r = await fetch(API_BASE + endpoint, {
123
+ var r = await fetch(API_BASE + endpoint, {
117
124
  headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }
118
125
  });
119
- if (!r.ok) throw { status: r.status, ...(await r.json()) };
126
+ if (!r.ok) { var e = await r.json(); e.status = r.status; throw e; }
120
127
  return r.json();
121
128
  }
122
129
 
123
130
  async function apiPost(endpoint, data) {
124
- const r = await fetch(API_BASE + endpoint, {
131
+ var r = await fetch(API_BASE + endpoint, {
125
132
  method: 'POST',
126
133
  headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
127
134
  body: JSON.stringify(data)
128
135
  });
129
- if (!r.ok) throw { status: r.status, ...(await r.json()) };
136
+ if (!r.ok) { var e = await r.json(); e.status = r.status; throw e; }
130
137
  return r.json();
131
138
  }
132
139
 
133
140
  async function apiDelete(endpoint) {
134
- const r = await fetch(API_BASE + endpoint, {
141
+ var r = await fetch(API_BASE + endpoint, {
135
142
  method: 'DELETE',
136
143
  headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken }
137
144
  });
138
- if (!r.ok) throw { status: r.status, ...(await r.json()) };
145
+ if (!r.ok) { var e = await r.json(); e.status = r.status; throw e; }
139
146
  return r.json();
140
147
  }
141
148
 
@@ -144,17 +151,19 @@
144
151
  // ========================================================================
145
152
 
146
153
  function scrollToBottom() {
147
- const c = els.messages;
148
- if (c) c.scrollTop = c.scrollHeight;
154
+ if (el.messages) el.messages.scrollTop = el.messages.scrollHeight;
149
155
  }
150
156
 
151
157
  function updateTokensDisplay() {
152
- els.tokens.textContent = formatTokens(state.tokensDisponibles);
153
- els.btnBuyCredits.style.display = state.tokensDisponibles < 100000 ? '' : 'none';
158
+ if (el.tokens) el.tokens.textContent = formatTokens(state.tokensDisponibles);
159
+ if (el.btnBuyCredits) el.btnBuyCredits.style.display = state.tokensDisponibles < 100000 ? '' : 'none';
154
160
  }
155
161
 
156
162
  function updateExpandIcons() {
157
- const expand = els.sidebar.querySelector('.lucy-icon-expand');
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';
158
167
  }
159
168
 
160
169
  /** Affiche la bonne vue : chat / history / doc */
@@ -162,46 +171,40 @@
162
171
  state.showHistory = view === 'history';
163
172
  state.showDoc = view === 'doc';
164
173
 
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';
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';
168
177
 
169
- // boutons actifs
170
- els.btnHistory.classList.toggle('active', state.showHistory);
171
- els.btnDoc.classList.toggle('active', state.showDoc);
178
+ if (el.btnHistory) el.btnHistory.classList.toggle('active', state.showHistory);
179
+ if (el.btnDoc) el.btnDoc.classList.toggle('active', state.showDoc);
172
180
  }
173
181
 
174
182
  /** Rend la liste des conversations dans l'historique */
175
183
  function renderHistoryList() {
176
- const container = els.historyList;
177
- if (!container) return;
184
+ if (!el.historyList) return;
178
185
 
179
186
  if (state.conversations.length === 0) {
180
- container.innerHTML = '<p class="lucy-history-empty">Aucune conversation</p>';
187
+ el.historyList.innerHTML = '<p class="lucy-history-empty">Aucune conversation</p>';
181
188
  return;
182
189
  }
183
190
 
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) => {
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) {
199
202
  if (e.target.closest('[data-delete-id]')) return;
200
- loadConversation(parseInt(el.dataset.convId));
203
+ loadConversation(parseInt(item.dataset.convId));
201
204
  });
202
205
  });
203
- container.querySelectorAll('[data-delete-id]').forEach(btn => {
204
- btn.addEventListener('click', (e) => {
206
+ el.historyList.querySelectorAll('[data-delete-id]').forEach(function (btn) {
207
+ btn.addEventListener('click', function (e) {
205
208
  e.stopPropagation();
206
209
  deleteConversation(parseInt(btn.dataset.deleteId));
207
210
  });
@@ -210,84 +213,83 @@
210
213
 
211
214
  /** Rend la zone de messages */
212
215
  function renderMessages() {
213
- const container = els.messages;
214
- if (!container) return;
216
+ if (!el.messages) return;
215
217
 
216
218
  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';
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(); });
221
224
  return;
222
225
  }
223
226
 
224
- els.welcome.style.display = 'none';
227
+ if (el.welcome) el.welcome.style.display = 'none';
225
228
 
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());
229
+ // Supprimer les anciens messages
230
+ el.messages.querySelectorAll('.lucy-message').forEach(function (m) { m.remove(); });
230
231
 
231
- const avatarUrl = container.dataset.avatarUrl || '';
232
- const lucyIcon = container.dataset.lucyIcon || '';
232
+ var avatarUrl = el.messages.dataset.avatarUrl || '';
233
+ var lucyIcon = el.messages.dataset.lucyIcon || '';
233
234
 
234
- state.messages.forEach(msg => {
235
- const isUser = msg.repondant === 'UTILISATEUR';
236
- const div = document.createElement('div');
235
+ state.messages.forEach(function (msg) {
236
+ var isUser = msg.repondant === 'UTILISATEUR';
237
+ var div = document.createElement('div');
237
238
  div.className = 'lucy-message ' + (isUser ? 'user' : 'bot');
238
239
 
239
- let avatarHtml;
240
+ var avatarHtml;
240
241
  if (isUser) {
241
242
  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>`;
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>';
244
245
  } else {
245
246
  avatarHtml = lucyIcon
246
- ? `<img src="${lucyIcon}" alt="Lucy">`
247
+ ? '<img src="' + lucyIcon + '" alt="Lucy">'
247
248
  : '🤖';
248
249
  }
249
250
 
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);
251
+ var bubbleContent = msg.contenu
252
+ ? '<span>' + formatMessage(msg.contenu) + '</span>'
253
+ : '<span class="lucy-loading-dots"><span></span><span></span><span></span></span>';
254
+
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>';
260
+
261
+ el.messages.appendChild(div);
262
262
  });
263
263
 
264
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';
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';
267
267
 
268
268
  requestAnimationFrame(scrollToBottom);
269
269
  }
270
270
 
271
271
  /** Rend les suggestions */
272
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));
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); });
284
282
  });
285
283
  }
286
284
 
287
285
  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();
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
+ }
290
+
291
+ function updateSendBtn() {
292
+ if (el.btnSend) el.btnSend.disabled = state.isLoading || !(el.input && el.input.value.trim());
291
293
  }
292
294
 
293
295
  // ========================================================================
@@ -297,20 +299,31 @@
297
299
  function openSidebar() {
298
300
  state.isOpen = true;
299
301
  state.hasNewMessage = false;
300
- els.floatBtn.style.display = 'none';
301
- els.sidebar.style.display = '';
302
- els.sidebar.classList.remove('hidden');
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
+ }
303
311
  saveState();
304
- setTimeout(() => els.input.focus(), 100);
312
+ setTimeout(function () { if (el.input) el.input.focus(); }, 300);
305
313
  }
306
314
 
307
315
  function closeSidebar() {
308
316
  state.isOpen = false;
309
317
  state.showHistory = false;
310
318
  state.showDoc = false;
311
- els.sidebar.style.display = 'none';
312
- els.sidebar.classList.add('hidden');
313
- els.floatBtn.style.display = '';
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 = '';
314
327
  showView('chat');
315
328
  saveState();
316
329
  }
@@ -321,36 +334,36 @@
321
334
 
322
335
  function toggleExpanded() {
323
336
  state.isExpanded = !state.isExpanded;
324
- els.sidebar.classList.toggle('expanded', state.isExpanded);
337
+ if (el.sidebar) el.sidebar.classList.toggle('expanded', state.isExpanded);
325
338
  updateExpandIcons();
326
339
  saveState();
327
340
  }
328
341
 
329
342
  async function loadTokenStatus() {
330
343
  try {
331
- const r = await apiGet('/tokens/status');
344
+ var r = await apiGet('/tokens/status');
332
345
  state.tokensDisponibles = r.tokens_disponibles || 0;
333
346
  updateTokensDisplay();
334
- } catch (e) { console.error('Erreur chargement tokens:', e); }
347
+ } catch (e) { console.error('Lucy: erreur tokens', e); }
335
348
  }
336
349
 
337
350
  async function loadConversations() {
338
351
  try {
339
- const r = await apiGet('/conversations');
352
+ var r = await apiGet('/conversations');
340
353
  state.conversations = r.conversations || [];
341
354
  renderHistoryList();
342
- } catch (e) { console.error('Erreur chargement conversations:', e); }
355
+ } catch (e) { console.error('Lucy: erreur conversations', e); }
343
356
  }
344
357
 
345
358
  async function loadSuggestions() {
346
359
  try {
347
- const r = await apiGet('/suggestions');
360
+ var r = await apiGet('/suggestions');
348
361
  state.suggestions = r.suggestions || [];
349
362
  } catch (e) {
350
363
  state.suggestions = [
351
- "Comment créer un nouveau membre ?",
352
- "Comment effectuer un paiement ?",
353
- "Où trouver la liste des adhésions ?"
364
+ 'Comment créer un nouveau membre ?',
365
+ 'Comment effectuer un paiement ?',
366
+ 'Où trouver la liste des adhésions ?'
354
367
  ];
355
368
  }
356
369
  renderSuggestions();
@@ -358,19 +371,19 @@
358
371
 
359
372
  async function newConversation() {
360
373
  try {
361
- const r = await apiPost('/conversations', { page_contexte: window.location.pathname });
374
+ var r = await apiPost('/conversations', { page_contexte: window.location.pathname });
362
375
  state.currentConversationId = r.id;
363
376
  state.messages = [];
364
377
  showView('chat');
365
378
  renderMessages();
366
379
  saveState();
367
380
  await loadConversations();
368
- } catch (e) { console.error('Erreur création conversation:', e); }
381
+ } catch (e) { console.error('Lucy: erreur new conversation', e); }
369
382
  }
370
383
 
371
384
  async function loadConversation(id) {
372
385
  try {
373
- const r = await apiGet(`/conversations/${id}`);
386
+ var r = await apiGet('/conversations/' + id);
374
387
  state.currentConversationId = id;
375
388
  state.messages = r.messages || [];
376
389
  showView('chat');
@@ -378,7 +391,7 @@
378
391
  saveState();
379
392
  setTimeout(scrollToBottom, 100);
380
393
  } catch (e) {
381
- console.error('Erreur chargement conversation:', e);
394
+ console.error('Lucy: erreur load conversation', e);
382
395
  state.currentConversationId = null;
383
396
  state.messages = [];
384
397
  renderMessages();
@@ -389,18 +402,18 @@
389
402
  async function deleteConversation(id) {
390
403
  if (!confirm('Êtes-vous sûr de vouloir supprimer cette conversation ?')) return;
391
404
  try {
392
- await apiDelete(`/conversations/${id}`);
405
+ await apiDelete('/conversations/' + id);
393
406
  await loadConversations();
394
407
  if (state.currentConversationId === id) {
395
408
  state.currentConversationId = null;
396
409
  state.messages = [];
397
410
  renderMessages();
398
411
  }
399
- } catch (e) { console.error('Erreur suppression:', e); }
412
+ } catch (e) { console.error('Lucy: erreur suppression', e); }
400
413
  }
401
414
 
402
415
  async function sendMessage(message) {
403
- if (state.isLoading || !message.trim()) return;
416
+ if (state.isLoading || !message || !message.trim()) return;
404
417
 
405
418
  state.lucyDidNotUnderstand = false;
406
419
 
@@ -415,20 +428,22 @@
415
428
  tokens_utilises: 0
416
429
  });
417
430
  state.currentMessage = '';
418
- els.input.value = '';
419
- els.input.style.height = 'auto';
431
+ if (el.input) {
432
+ el.input.value = '';
433
+ el.input.style.height = 'auto';
434
+ }
420
435
  updateSendBtn();
421
436
  renderMessages();
422
437
 
423
438
  try {
424
439
  state.isLoading = true;
425
- els.input.disabled = true;
426
- els.btnSend.disabled = true;
440
+ if (el.input) el.input.disabled = true;
441
+ if (el.btnSend) el.btnSend.disabled = true;
427
442
 
428
- await apiPost(`/conversations/${state.currentConversationId}/messages`, { contenu: message });
443
+ await apiPost('/conversations/' + state.currentConversationId + '/messages', { contenu: message });
429
444
  await streamChatCompletion();
430
445
  } catch (e) {
431
- console.error('Erreur envoi message:', e);
446
+ console.error('Lucy: erreur envoi', e);
432
447
  if (e.status === 402) {
433
448
  showModal('buy');
434
449
  } else {
@@ -437,15 +452,15 @@
437
452
  }
438
453
  } finally {
439
454
  state.isLoading = false;
440
- els.input.disabled = false;
455
+ if (el.input) el.input.disabled = false;
441
456
  updateSendBtn();
442
- els.input.focus();
457
+ if (el.input) el.input.focus();
443
458
  }
444
459
  }
445
460
 
446
461
  async function streamChatCompletion() {
447
- const url = `${API_BASE}/conversations/${state.currentConversationId}/chat`;
448
- const botIdx = state.messages.length;
462
+ var url = API_BASE + '/conversations/' + state.currentConversationId + '/chat';
463
+ var botIdx = state.messages.length;
449
464
 
450
465
  state.messages.push({
451
466
  id: Date.now() + 1,
@@ -457,27 +472,30 @@
457
472
  renderMessages();
458
473
 
459
474
  try {
460
- const response = await fetch(url, {
475
+ var response = await fetch(url, {
461
476
  method: 'POST',
462
477
  headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
463
478
  body: JSON.stringify({ page_contexte: window.location.pathname })
464
479
  });
465
480
 
466
- if (!response.ok) throw { status: response.status, ...(await response.json()) };
481
+ if (!response.ok) { var err = await response.json(); err.status = response.status; throw err; }
467
482
 
468
- const reader = response.body.getReader();
469
- const decoder = new TextDecoder();
470
- let fullContent = '';
483
+ var reader = response.body.getReader();
484
+ var decoder = new TextDecoder();
485
+ var fullContent = '';
471
486
 
472
487
  while (true) {
473
- const { done, value } = await reader.read();
474
- if (done) break;
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');
475
493
 
476
- const text = decoder.decode(value);
477
- for (const line of text.split('\n')) {
494
+ for (var i = 0; i < lines.length; i++) {
495
+ var line = lines[i];
478
496
  if (!line.startsWith('data: ')) continue;
479
497
  try {
480
- const data = JSON.parse(line.slice(6));
498
+ var data = JSON.parse(line.slice(6));
481
499
  if (data.type === 'content') {
482
500
  fullContent += data.content;
483
501
  state.messages[botIdx].contenu = fullContent;
@@ -489,22 +507,24 @@
489
507
  } else if (data.type === 'error') {
490
508
  throw new Error(data.error);
491
509
  }
492
- } catch (e) { if (e.message) throw e; }
510
+ } catch (parseErr) { if (parseErr.message) throw parseErr; }
493
511
  }
494
512
  }
495
513
 
496
- if (!state.messages[botIdx]?.contenu) {
514
+ if (!state.messages[botIdx] || !state.messages[botIdx].contenu) {
497
515
  state.messages.splice(botIdx, 1);
498
516
  }
499
517
 
500
- state.lucyDidNotUnderstand = checkIfLucyDidNotUnderstand(state.messages[botIdx]?.contenu || '');
518
+ state.lucyDidNotUnderstand = checkIfLucyDidNotUnderstand(
519
+ (state.messages[botIdx] && state.messages[botIdx].contenu) || ''
520
+ );
501
521
  if (!state.lucyDidNotUnderstand) state.hasError = false;
502
522
 
503
523
  renderMessages();
504
524
  await loadConversations();
505
525
  } catch (e) {
506
526
  state.hasError = true;
507
- if (botIdx < state.messages.length && !state.messages[botIdx]?.contenu) {
527
+ if (botIdx < state.messages.length && state.messages[botIdx] && !state.messages[botIdx].contenu) {
508
528
  state.messages.splice(botIdx, 1);
509
529
  }
510
530
  renderMessages();
@@ -525,17 +545,17 @@
525
545
  state.lucyDidNotUnderstand = false;
526
546
  hideModal('feedback');
527
547
  renderMessages();
528
- } catch (e) { console.error('Erreur feedback:', e); }
548
+ } catch (e) { console.error('Lucy: erreur feedback', e); }
529
549
  finally { state.feedbackSending = false; }
530
550
  }
531
551
 
532
552
  async function buyCredits() {
533
553
  if (state.buyAmount < 10) return;
534
554
  try {
535
- const r = await apiPost('/tokens/buy', { montant_ht: state.buyAmount });
555
+ var r = await apiPost('/tokens/buy', { montant_ht: state.buyAmount });
536
556
  window.open(r.url_souscription, '_blank');
537
557
  hideModal('buy');
538
- } catch (e) { console.error('Erreur achat:', e); }
558
+ } catch (e) { console.error('Lucy: erreur achat', e); }
539
559
  }
540
560
 
541
561
  // ========================================================================
@@ -543,19 +563,19 @@
543
563
  // ========================================================================
544
564
 
545
565
  function showModal(name) {
546
- if (name === 'buy') {
547
- els.modalBuy.style.display = '';
566
+ if (name === 'buy' && el.modalBuy) {
567
+ el.modalBuy.style.display = '';
548
568
  updateBuyModal();
549
- } else if (name === 'feedback') {
550
- els.modalFeedback.style.display = '';
569
+ } else if (name === 'feedback' && el.modalFeedback) {
570
+ el.modalFeedback.style.display = '';
551
571
  state.feedbackDescription = '';
552
- els.feedbackDesc.value = '';
572
+ if (el.feedbackDesc) el.feedbackDesc.value = '';
553
573
  }
554
574
  }
555
575
 
556
576
  function hideModal(name) {
557
- if (name === 'buy') els.modalBuy.style.display = 'none';
558
- else if (name === 'feedback') els.modalFeedback.style.display = 'none';
577
+ if (name === 'buy' && el.modalBuy) el.modalBuy.style.display = 'none';
578
+ else if (name === 'feedback' && el.modalFeedback) el.modalFeedback.style.display = 'none';
559
579
  }
560
580
 
561
581
  // ========================================================================
@@ -564,9 +584,9 @@
564
584
 
565
585
  function checkFirstLaunch() {
566
586
  if (!localStorage.getItem('lucy_assist_guide_seen') && state.conversations.length === 0) {
567
- setTimeout(() => {
568
- if (state.conversations.length === 0) {
569
- els.guideOverlay.style.display = '';
587
+ setTimeout(function () {
588
+ if (state.conversations.length === 0 && el.guideOverlay) {
589
+ el.guideOverlay.style.display = '';
570
590
  openSidebar();
571
591
  }
572
592
  }, 2000);
@@ -575,12 +595,12 @@
575
595
 
576
596
  function guideNext() {
577
597
  state.guideStep = 2;
578
- els.guideStep1.style.display = 'none';
579
- els.guideStep2.style.display = '';
598
+ if (el.guideStep1) el.guideStep1.style.display = 'none';
599
+ if (el.guideStep2) el.guideStep2.style.display = '';
580
600
  }
581
601
 
582
602
  function guideFinish() {
583
- els.guideOverlay.style.display = 'none';
603
+ if (el.guideOverlay) el.guideOverlay.style.display = 'none';
584
604
  localStorage.setItem('lucy_assist_guide_seen', 'true');
585
605
  }
586
606
 
@@ -589,18 +609,20 @@
589
609
  // ========================================================================
590
610
 
591
611
  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
- }));
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) {}
599
621
  }
600
622
 
601
623
  function restoreState() {
602
624
  try {
603
- const s = JSON.parse(sessionStorage.getItem('lucy_assist_state'));
625
+ var s = JSON.parse(sessionStorage.getItem('lucy_assist_state'));
604
626
  if (!s) return;
605
627
  state.isOpen = s.isOpen || false;
606
628
  state.isExpanded = s.isExpanded || false;
@@ -610,75 +632,73 @@
610
632
  } catch (e) {}
611
633
  }
612
634
 
613
- // ========================================================================
614
- // SEND BUTTON STATE
615
- // ========================================================================
616
-
617
- function updateSendBtn() {
618
- els.btnSend.disabled = state.isLoading || !els.input.value.trim();
619
- }
620
-
621
635
  // ========================================================================
622
636
  // INIT
623
637
  // ========================================================================
624
638
 
625
639
  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
- };
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;
681
+ }
682
+
683
+ console.log('Lucy Assist: elements trouves, floatBtn=' + !!el.floatBtn + ', sidebar=' + !!el.sidebar);
667
684
 
668
685
  // CSRF
669
- csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value || getCookie('csrftoken') || '';
686
+ var csrfEl = document.querySelector('[name=csrfmiddlewaretoken]');
687
+ csrfToken = (csrfEl && csrfEl.value) || getCookie('csrftoken') || '';
670
688
 
671
689
  // Restaurer l'etat
672
690
  restoreState();
673
691
 
674
692
  // Appliquer l'etat initial
675
693
  if (state.isOpen) {
676
- els.floatBtn.style.display = 'none';
677
- els.sidebar.style.display = '';
678
- els.sidebar.classList.remove('hidden');
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
+ }
679
699
  }
680
- if (state.isExpanded) {
681
- els.sidebar.classList.add('expanded');
700
+ if (state.isExpanded && el.sidebar) {
701
+ el.sidebar.classList.add('expanded');
682
702
  }
683
703
  updateExpandIcons();
684
704
 
@@ -686,79 +706,76 @@
686
706
  else if (state.showDoc) showView('doc');
687
707
  else showView('chat');
688
708
 
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';
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';
706
722
  updateSendBtn();
707
- });
708
- els.input.addEventListener('keydown', (e) => {
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) {
709
728
  if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
710
729
  e.preventDefault();
711
- sendMessage(els.input.value);
730
+ sendMessage(el.input.value);
712
731
  }
732
+ // Mettre a jour le bouton sur la prochaine frame
733
+ setTimeout(updateSendBtn, 0);
713
734
  });
714
735
 
715
- // Send
716
- els.btnSend.addEventListener('click', () => sendMessage(els.input.value));
736
+ on(el.btnSend, 'click', function () { sendMessage(el.input ? el.input.value : ''); });
717
737
 
718
- // Feedback
719
- els.btnFeedbackContact.addEventListener('click', () => showModal('feedback'));
720
- els.btnFeedbackError.addEventListener('click', () => showModal('feedback'));
738
+ on($('lucy-btn-feedback-contact'), 'click', function () { showModal('feedback'); });
739
+ on($('lucy-btn-feedback-error'), 'click', function () { showModal('feedback'); });
721
740
 
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;
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;
728
746
  updateBuyModal();
729
747
  });
730
748
 
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;
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;
736
753
  });
737
754
 
738
- // Guide
739
- els.guideSkip.addEventListener('click', guideFinish);
740
- els.guideNext.addEventListener('click', guideNext);
741
- els.guideFinish.addEventListener('click', guideFinish);
755
+ on($('lucy-guide-skip'), 'click', guideFinish);
756
+ on($('lucy-guide-next'), 'click', guideNext);
757
+ on($('lucy-guide-finish'), 'click', guideFinish);
742
758
 
743
759
  // Raccourcis clavier globaux
744
- document.addEventListener('keydown', (e) => {
760
+ document.addEventListener('keydown', function (e) {
745
761
  if (e.ctrlKey && e.key === 'k') { e.preventDefault(); toggleSidebar(); }
746
762
  if (e.key === 'Escape' && state.isOpen) closeSidebar();
747
763
  });
748
764
 
749
- // Sauvegarder avant de quitter
750
765
  window.addEventListener('beforeunload', saveState);
751
766
 
752
767
  // Charger les donnees
753
768
  loadTokenStatus();
754
- loadConversations().then(() => {
769
+ loadConversations().then(function () {
755
770
  if (state.currentConversationId) loadConversation(state.currentConversationId);
756
771
  });
757
772
  loadSuggestions();
758
773
  checkFirstLaunch();
774
+
775
+ console.log('Lucy Assist: init complete');
759
776
  }
760
777
 
761
- // Lancer l'init quand le DOM est pret
778
+ // Lancer quand le DOM est pret
762
779
  if (document.readyState === 'loading') {
763
780
  document.addEventListener('DOMContentLoaded', init);
764
781
  } else {
@@ -91,7 +91,7 @@
91
91
  <div class="lucy-input-area">
92
92
  <div class="lucy-input-wrapper">
93
93
  <textarea id="lucy-input" placeholder="Tapez votre message..." class="lucy-input" rows="1"></textarea>
94
- <button id="lucy-btn-send" class="lucy-send-btn" disabled>
94
+ <button id="lucy-btn-send" class="lucy-send-btn">
95
95
  <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="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/></svg>
96
96
  </button>
97
97
  </div>