@developpement/tp-chatbot-widget 0.0.1 → 0.0.4

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/chatbot.js +135 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@developpement/tp-chatbot-widget",
3
- "version": "0.0.1",
3
+ "version": "0.0.4",
4
4
  "description": "Chatbot widget for TravelPlanet / Makitizy",
5
5
  "main": "src/chatbot.js",
6
6
  "files": [
package/src/chatbot.js CHANGED
@@ -1,6 +1,16 @@
1
1
  (function () {
2
2
  'use strict';
3
3
 
4
+ const CLIENT_THEMES = {
5
+ flix: { primary: '#7b1fa2', name: 'Flix Corporate' },
6
+ sncf: { primary: '#1a6b5a', name: 'SNCF' },
7
+ };
8
+ const DEFAULT_THEME = { primary: '#7b1fa2', name: 'Support' };
9
+
10
+ function getClientTheme(client_id) {
11
+ return CLIENT_THEMES[client_id] || DEFAULT_THEME;
12
+ }
13
+
4
14
  function extractUserInfo(access_token) {
5
15
  if (!access_token) return null;
6
16
  try {
@@ -18,6 +28,22 @@
18
28
  }
19
29
  }
20
30
 
31
+ function playSound() {
32
+ try {
33
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
34
+ const oscillator = ctx.createOscillator();
35
+ const gain = ctx.createGain();
36
+ oscillator.connect(gain);
37
+ gain.connect(ctx.destination);
38
+ oscillator.frequency.value = 880;
39
+ oscillator.type = 'sine';
40
+ gain.gain.setValueAtTime(0.2, ctx.currentTime);
41
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4);
42
+ oscillator.start(ctx.currentTime);
43
+ oscillator.stop(ctx.currentTime + 0.4);
44
+ } catch {}
45
+ }
46
+
21
47
  class TpChatbot extends HTMLElement {
22
48
  constructor() {
23
49
  super();
@@ -32,10 +58,37 @@
32
58
  this.polling_interval = null;
33
59
  this.user_info = null;
34
60
  this.is_closed = false;
35
- this.view = 'list'; // 'list' | 'chat'
61
+ this.view = 'list';
62
+ this._initialized = false;
63
+ this.sound_enabled = true;
64
+ this.client_id = 'flix';
65
+ this.access_token = '';
66
+ }
67
+
68
+ static get observedAttributes() {
69
+ return ['access-token', 'client-id'];
70
+ }
71
+
72
+ attributeChangedCallback(name, old_val, new_val) {
73
+ if (name === 'client-id' && new_val && new_val !== old_val) {
74
+ this.client_id = new_val;
75
+ if (this._initialized) this.applyTheme();
76
+ }
77
+ if (name === 'access-token' && new_val && new_val !== old_val && this._initialized) {
78
+ this.access_token = new_val;
79
+ this.user_info = extractUserInfo(new_val);
80
+ this.user_id = this.user_info?.user_id;
81
+ this.applyTheme();
82
+ const guest_form = this.querySelector('#tp-guest-form');
83
+ if (guest_form) {
84
+ guest_form.remove();
85
+ this.showConversationList();
86
+ }
87
+ }
36
88
  }
37
89
 
38
90
  connectedCallback() {
91
+ this.client_id = this.getAttribute('client-id') || 'flix';
39
92
  this.access_token = this.getAttribute('access-token') || '';
40
93
  this.api_url = this.getAttribute('api-url') || 'https://api.tst.travelplanet.click/chatbot/v1';
41
94
  this.user_info = null;
@@ -47,15 +100,27 @@
47
100
  this.view = 'list';
48
101
 
49
102
  this.render();
103
+ this.applyTheme(); // ← couleur appliquée immédiatement, avant le token
50
104
  this.attachEvents();
105
+ this._initialized = true;
51
106
 
52
107
  if (this.access_token) {
53
108
  this.user_info = extractUserInfo(this.access_token);
54
- this.user_id = this.user_info.user_id;
55
- this.applyTheme();
109
+ this.user_id = this.user_info?.user_id;
56
110
  this.showConversationList();
57
111
  } else {
58
- this.showGuestForm();
112
+ // Attend 500ms que le framework passe le token
113
+ setTimeout(() => {
114
+ const token = this.getAttribute('access-token');
115
+ if (token) {
116
+ this.access_token = token;
117
+ this.user_info = extractUserInfo(token);
118
+ this.user_id = this.user_info?.user_id;
119
+ this.showConversationList();
120
+ } else {
121
+ this.showGuestForm();
122
+ }
123
+ }, 500);
59
124
  }
60
125
  }
61
126
 
@@ -66,8 +131,7 @@
66
131
  // ─── Theme ────────────────────────────────────────────────────────────────
67
132
 
68
133
  getThemeColor() {
69
- const site_id = this.user_info?.site_id || '';
70
- return site_id.startsWith('I') ? '#97d700' : '#7b1fa2';
134
+ return getClientTheme(this.client_id).primary;
71
135
  }
72
136
 
73
137
  shadeColor(hex, percent) {
@@ -98,7 +162,7 @@
98
162
  .tp-chatbot-agent-btn:hover { background: ${color} !important; color: white !important; }
99
163
  .tp-chatbot-typing span { background: ${color} !important; }
100
164
  #tp-new-conversation { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
101
- .tp-conv-item:hover { background: #f9f0ff !important; }
165
+ .tp-conv-item:hover { background: ${color}11 !important; }
102
166
  .tp-conv-new-btn { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
103
167
  `;
104
168
  document.head.appendChild(style);
@@ -107,15 +171,17 @@
107
171
  // ─── Render ───────────────────────────────────────────────────────────────
108
172
 
109
173
  render() {
174
+ const theme = getClientTheme(this.client_id);
110
175
  this.innerHTML = `
111
176
  <div class="tp-chatbot-host">
112
177
  <div class="tp-chatbot-window" id="tp-window">
113
178
  <div class="tp-chatbot-header" id="tp-header">
114
179
  <div class="tp-chatbot-avatar">M</div>
115
180
  <div style="flex:1">
116
- <div class="tp-chatbot-title">Makitizy Support</div>
181
+ <div class="tp-chatbot-title">${theme.name} Support</div>
117
182
  <div class="tp-chatbot-subtitle" id="tp-subtitle">🤖 Assistant virtuel</div>
118
183
  </div>
184
+ <button id="tp-sound-btn" style="background:none;border:none;color:white;font-size:16px;cursor:pointer;padding:4px 8px;" title="Activer/désactiver le son">🔔</button>
119
185
  <button id="tp-back-btn" style="display:none;background:none;border:none;color:white;font-size:18px;cursor:pointer;padding:4px 8px;">←</button>
120
186
  </div>
121
187
  <div class="tp-chatbot-messages" id="tp-messages"></div>
@@ -126,6 +192,11 @@
126
192
  <textarea class="tp-chatbot-input" id="tp-input" placeholder="Écrivez votre message..." rows="1"></textarea>
127
193
  <button class="tp-chatbot-send" id="tp-send">➤</button>
128
194
  </div>
195
+ <div id="tp-close-bar" style="padding:8px 12px;border-top:1px solid #ede8f5;text-align:center;display:none;">
196
+ <button id="tp-close-btn" style="background:none;border:1px solid #e5e7eb;border-radius:8px;padding:6px 16px;font-size:12px;color:#9ca3af;cursor:pointer;font-weight:600;">
197
+ ✅ Terminer la conversation
198
+ </button>
199
+ </div>
129
200
  </div>
130
201
  <div class="tp-chatbot-bubble" id="tp-bubble">💬</div>
131
202
  </div>
@@ -143,6 +214,17 @@
143
214
  this.sendMessage();
144
215
  }
145
216
  });
217
+ this.querySelector('#tp-sound-btn').addEventListener('click', () => {
218
+ this.sound_enabled = !this.sound_enabled;
219
+ const btn = this.querySelector('#tp-sound-btn');
220
+ btn.textContent = this.sound_enabled ? '🔔' : '🔕';
221
+ btn.title = this.sound_enabled ? 'Désactiver le son' : 'Activer le son';
222
+ });
223
+ this.querySelector('#tp-close-btn').addEventListener('click', () => {
224
+ if (window.confirm('Voulez-vous terminer cette conversation ?')) {
225
+ this.closeByUser();
226
+ }
227
+ });
146
228
  }
147
229
 
148
230
  toggleChat() {
@@ -169,7 +251,11 @@
169
251
  const response = await fetch(`${this.api_url}/chat/conversations`, {
170
252
  method: 'POST',
171
253
  headers: this.getHeaders(true),
172
- body: JSON.stringify({ user_id: this.user_id, user_info: this.user_info }),
254
+ body: JSON.stringify({
255
+ user_id: this.user_id,
256
+ user_info: this.user_info,
257
+ client_id: this.client_id,
258
+ }),
173
259
  });
174
260
  const data = await response.json();
175
261
  return data.result?.conversation_id;
@@ -197,6 +283,9 @@
197
283
  const agent_bar = this.querySelector('#tp-agent-bar');
198
284
  if (agent_bar) agent_bar.style.display = 'none';
199
285
 
286
+ const close_bar = this.querySelector('#tp-close-bar');
287
+ if (close_bar) close_bar.style.display = 'none';
288
+
200
289
  const container = this.querySelector('#tp-messages');
201
290
  container.innerHTML = '<div style="padding:16px;text-align:center;color:#aaa;font-size:12px;">Chargement...</div>';
202
291
 
@@ -211,18 +300,14 @@
211
300
 
212
301
  container.innerHTML = '';
213
302
 
214
- // Header list
215
303
  const header_el = document.createElement('div');
216
304
  header_el.style.cssText = 'padding:16px;border-bottom:1px solid #ede8f5;';
217
305
  header_el.innerHTML = `
218
- <div style="font-size:13px;font-weight:700;color:#1a1a2e;margin-bottom:4px;">
219
- Bonjour ${this.user_info?.first_name || ''} 👋
220
- </div>
306
+ <div style="font-size:13px;font-weight:700;color:#1a1a2e;margin-bottom:4px;">Bonjour ${this.user_info?.first_name || ''} 👋</div>
221
307
  <div style="font-size:12px;color:#aaa;">Vos conversations récentes</div>
222
308
  `;
223
309
  container.appendChild(header_el);
224
310
 
225
- // Conversation items
226
311
  if (conversations.length === 0) {
227
312
  const empty = document.createElement('div');
228
313
  empty.style.cssText = 'padding:24px 16px;text-align:center;color:#aaa;font-size:13px;';
@@ -264,7 +349,6 @@
264
349
  });
265
350
  }
266
351
 
267
- // New conversation button
268
352
  const new_btn_wrap = document.createElement('div');
269
353
  new_btn_wrap.style.cssText = 'padding:16px;';
270
354
  new_btn_wrap.innerHTML = `
@@ -297,6 +381,7 @@
297
381
 
298
382
  const input_bar = this.querySelector('#tp-chatbot-input-bar');
299
383
  const agent_bar = this.querySelector('#tp-agent-bar');
384
+ const close_bar = this.querySelector('#tp-close-bar');
300
385
 
301
386
  try {
302
387
  const url = `${this.api_url}/chat/conversations/${conversation_id}?user_id=${encodeURIComponent(this.user_id)}`;
@@ -318,6 +403,12 @@
318
403
  return;
319
404
  }
320
405
 
406
+ if (conv.status === 'bot' || conv.status === 'waiting_agent') {
407
+ if (close_bar) close_bar.style.display = 'block';
408
+ } else {
409
+ if (close_bar) close_bar.style.display = 'none';
410
+ }
411
+
321
412
  if (conv.status === 'agent') {
322
413
  this.agent_mode = true;
323
414
  const subtitle = this.querySelector('#tp-subtitle');
@@ -349,6 +440,9 @@
349
440
  const back_btn = this.querySelector('#tp-back-btn');
350
441
  if (back_btn) back_btn.style.display = 'block';
351
442
 
443
+ const close_bar = this.querySelector('#tp-close-bar');
444
+ if (close_bar) close_bar.style.display = 'block';
445
+
352
446
  const container = this.querySelector('#tp-messages');
353
447
  container.innerHTML = '';
354
448
 
@@ -421,7 +515,6 @@
421
515
 
422
516
  this.user_info = { user_id: `guest-${email}`, first_name, last_name: '', company_name, email, language: 'FR', site_id: '' };
423
517
  this.user_id = this.user_info.user_id;
424
- this.applyTheme();
425
518
 
426
519
  const form_el = this.querySelector('#tp-guest-form');
427
520
  if (form_el) form_el.remove();
@@ -434,6 +527,9 @@
434
527
  const agent_bar = this.querySelector('#tp-agent-bar');
435
528
  if (agent_bar) agent_bar.style.display = 'block';
436
529
 
530
+ const close_bar = this.querySelector('#tp-close-bar');
531
+ if (close_bar) close_bar.style.display = 'block';
532
+
437
533
  this.addMessage('assistant', `Bonjour ${first_name} ! Comment puis-je vous aider aujourd'hui ?`);
438
534
  this.polling_interval = setInterval(() => this.pollMessages(), 3000);
439
535
  }
@@ -466,6 +562,10 @@
466
562
  `;
467
563
  container.appendChild(msg_el);
468
564
  container.scrollTop = container.scrollHeight;
565
+
566
+ if ((role === 'assistant' || role === 'agent') && this.sound_enabled) {
567
+ playSound();
568
+ }
469
569
  }
470
570
 
471
571
  showTyping() {
@@ -485,7 +585,6 @@
485
585
 
486
586
  showClosed() {
487
587
  this.is_closed = true;
488
-
489
588
  if (this.polling_interval) {
490
589
  clearInterval(this.polling_interval);
491
590
  this.polling_interval = null;
@@ -500,6 +599,9 @@
500
599
  const agent_bar = this.querySelector('#tp-agent-bar');
501
600
  if (agent_bar) agent_bar.style.display = 'none';
502
601
 
602
+ const close_bar = this.querySelector('#tp-close-bar');
603
+ if (close_bar) close_bar.style.display = 'none';
604
+
503
605
  const container = this.querySelector('#tp-messages');
504
606
  if (this.querySelector('#tp-closed-banner')) return;
505
607
 
@@ -510,11 +612,11 @@
510
612
  banner.id = 'tp-closed-banner';
511
613
  banner.innerHTML = `
512
614
  <div style="margin:16px;padding:16px;background:#fef2f2;border:1px solid #fecaca;border-radius:10px;text-align:center;">
513
- <div style="font-size:13px;color:#ef4444;font-weight:600;margin-bottom:12px;">✅ Cette conversation a été clôturée par un agent.</div>
615
+ <div style="font-size:13px;color:#ef4444;font-weight:600;margin-bottom:12px;">✅ Cette conversation a été clôturée.</div>
514
616
  <button id="tp-new-conversation" style="padding:10px 20px;background:linear-gradient(135deg,${color},${dark});color:white;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;margin-bottom:8px;width:100%;">
515
617
  💬 Nouvelle conversation
516
618
  </button>
517
- <button id="tp-back-to-list" style="padding:8px 20px;background:none;color:#7B1FA2;border:1px solid #ede8f5;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;width:100%;">
619
+ <button id="tp-back-to-list" style="padding:8px 20px;background:none;color:${color};border:1px solid #ede8f5;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;width:100%;">
518
620
  ← Voir toutes les conversations
519
621
  </button>
520
622
  </div>
@@ -608,6 +710,20 @@
608
710
  }
609
711
  }
610
712
 
713
+ async closeByUser() {
714
+ if (!this.conversation_id || this.is_closed) return;
715
+ try {
716
+ const response = await fetch(`${this.api_url}/chat/conversations/${this.conversation_id}/close`, {
717
+ method: 'POST',
718
+ headers: this.getHeaders(true),
719
+ body: JSON.stringify({ user_id: this.user_id }),
720
+ });
721
+ if (response.ok) this.showClosed();
722
+ } catch (e) {
723
+ console.error('closeByUser error:', e);
724
+ }
725
+ }
726
+
611
727
  async pollMessages() {
612
728
  if (!this.conversation_id || this.is_closed || this.view !== 'chat') return;
613
729
  try {