@developpement/tp-chatbot-widget 0.0.2 → 0.0.5

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 +156 -33
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@developpement/tp-chatbot-widget",
3
- "version": "0.0.2",
3
+ "version": "0.0.5",
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();
@@ -34,19 +60,25 @@
34
60
  this.is_closed = false;
35
61
  this.view = 'list';
36
62
  this._initialized = false;
63
+ this.sound_enabled = true;
64
+ this.client_id = 'flix';
65
+ this.access_token = '';
37
66
  }
38
67
 
39
68
  static get observedAttributes() {
40
- return ['access-token'];
69
+ return ['access-token', 'client-id'];
41
70
  }
42
71
 
43
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
+ }
44
77
  if (name === 'access-token' && new_val && new_val !== old_val && this._initialized) {
45
78
  this.access_token = new_val;
46
79
  this.user_info = extractUserInfo(new_val);
47
80
  this.user_id = this.user_info?.user_id;
48
81
  this.applyTheme();
49
-
50
82
  const guest_form = this.querySelector('#tp-guest-form');
51
83
  if (guest_form) {
52
84
  guest_form.remove();
@@ -56,6 +88,7 @@
56
88
  }
57
89
 
58
90
  connectedCallback() {
91
+ this.client_id = this.getAttribute('client-id') || 'flix';
59
92
  this.access_token = this.getAttribute('access-token') || '';
60
93
  this.api_url = this.getAttribute('api-url') || 'https://api.tst.travelplanet.click/chatbot/v1';
61
94
  this.user_info = null;
@@ -67,13 +100,13 @@
67
100
  this.view = 'list';
68
101
 
69
102
  this.render();
103
+ this.applyTheme(); // ← couleur appliquée immédiatement, avant le token
70
104
  this.attachEvents();
71
105
  this._initialized = true;
72
106
 
73
107
  if (this.access_token) {
74
108
  this.user_info = extractUserInfo(this.access_token);
75
109
  this.user_id = this.user_info?.user_id;
76
- this.applyTheme();
77
110
  this.showConversationList();
78
111
  } else {
79
112
  // Attend 500ms que le framework passe le token
@@ -83,7 +116,6 @@
83
116
  this.access_token = token;
84
117
  this.user_info = extractUserInfo(token);
85
118
  this.user_id = this.user_info?.user_id;
86
- this.applyTheme();
87
119
  this.showConversationList();
88
120
  } else {
89
121
  this.showGuestForm();
@@ -99,8 +131,7 @@
99
131
  // ─── Theme ────────────────────────────────────────────────────────────────
100
132
 
101
133
  getThemeColor() {
102
- const site_id = this.user_info?.site_id || '';
103
- return site_id.startsWith('I') ? '#97d700' : '#7b1fa2';
134
+ return getClientTheme(this.client_id).primary;
104
135
  }
105
136
 
106
137
  shadeColor(hex, percent) {
@@ -131,7 +162,7 @@
131
162
  .tp-chatbot-agent-btn:hover { background: ${color} !important; color: white !important; }
132
163
  .tp-chatbot-typing span { background: ${color} !important; }
133
164
  #tp-new-conversation { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
134
- .tp-conv-item:hover { background: #f9f0ff !important; }
165
+ .tp-conv-item:hover { background: ${color}11 !important; }
135
166
  .tp-conv-new-btn { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
136
167
  `;
137
168
  document.head.appendChild(style);
@@ -140,15 +171,17 @@
140
171
  // ─── Render ───────────────────────────────────────────────────────────────
141
172
 
142
173
  render() {
174
+ const theme = getClientTheme(this.client_id);
143
175
  this.innerHTML = `
144
176
  <div class="tp-chatbot-host">
145
177
  <div class="tp-chatbot-window" id="tp-window">
146
178
  <div class="tp-chatbot-header" id="tp-header">
147
179
  <div class="tp-chatbot-avatar">M</div>
148
180
  <div style="flex:1">
149
- <div class="tp-chatbot-title">Makitizy Support</div>
181
+ <div class="tp-chatbot-title">${theme.name} Support</div>
150
182
  <div class="tp-chatbot-subtitle" id="tp-subtitle">🤖 Assistant virtuel</div>
151
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>
152
185
  <button id="tp-back-btn" style="display:none;background:none;border:none;color:white;font-size:18px;cursor:pointer;padding:4px 8px;">←</button>
153
186
  </div>
154
187
  <div class="tp-chatbot-messages" id="tp-messages"></div>
@@ -159,6 +192,11 @@
159
192
  <textarea class="tp-chatbot-input" id="tp-input" placeholder="Écrivez votre message..." rows="1"></textarea>
160
193
  <button class="tp-chatbot-send" id="tp-send">➤</button>
161
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>
162
200
  </div>
163
201
  <div class="tp-chatbot-bubble" id="tp-bubble">💬</div>
164
202
  </div>
@@ -171,7 +209,21 @@
171
209
  this.querySelector('#tp-agent-btn').addEventListener('click', () => this.requestAgent());
172
210
  this.querySelector('#tp-back-btn').addEventListener('click', () => this.showConversationList());
173
211
  this.querySelector('#tp-input').addEventListener('keydown', e => {
174
- if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendMessage(); }
212
+ if (e.key === 'Enter' && !e.shiftKey) {
213
+ e.preventDefault();
214
+ this.sendMessage();
215
+ }
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
+ }
175
227
  });
176
228
  }
177
229
 
@@ -179,8 +231,13 @@
179
231
  this.is_open = !this.is_open;
180
232
  const window_el = this.querySelector('#tp-window');
181
233
  const bubble = this.querySelector('#tp-bubble');
182
- if (this.is_open) { window_el.classList.add('open'); bubble.textContent = '✕'; }
183
- else { window_el.classList.remove('open'); bubble.textContent = '💬'; }
234
+ if (this.is_open) {
235
+ window_el.classList.add('open');
236
+ bubble.textContent = '✕';
237
+ } else {
238
+ window_el.classList.remove('open');
239
+ bubble.textContent = '💬';
240
+ }
184
241
  }
185
242
 
186
243
  getHeaders(content_type = false) {
@@ -194,7 +251,11 @@
194
251
  const response = await fetch(`${this.api_url}/chat/conversations`, {
195
252
  method: 'POST',
196
253
  headers: this.getHeaders(true),
197
- 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
+ }),
198
259
  });
199
260
  const data = await response.json();
200
261
  return data.result?.conversation_id;
@@ -205,7 +266,10 @@
205
266
  async showConversationList() {
206
267
  this.view = 'list';
207
268
 
208
- if (this.polling_interval) { clearInterval(this.polling_interval); this.polling_interval = null; }
269
+ if (this.polling_interval) {
270
+ clearInterval(this.polling_interval);
271
+ this.polling_interval = null;
272
+ }
209
273
 
210
274
  const subtitle = this.querySelector('#tp-subtitle');
211
275
  if (subtitle) subtitle.textContent = '🤖 Assistant virtuel';
@@ -219,6 +283,9 @@
219
283
  const agent_bar = this.querySelector('#tp-agent-bar');
220
284
  if (agent_bar) agent_bar.style.display = 'none';
221
285
 
286
+ const close_bar = this.querySelector('#tp-close-bar');
287
+ if (close_bar) close_bar.style.display = 'none';
288
+
222
289
  const container = this.querySelector('#tp-messages');
223
290
  container.innerHTML = '<div style="padding:16px;text-align:center;color:#aaa;font-size:12px;">Chargement...</div>';
224
291
 
@@ -253,10 +320,21 @@
253
320
  item.style.cssText = 'padding:14px 16px;border-bottom:1px solid #f5f0fa;cursor:pointer;transition:background 0.15s;';
254
321
 
255
322
  const is_closed = conv.status === 'closed';
256
- const status_label = is_closed ? '✅ Fermée' : conv.status === 'agent' ? '👤 Agent' : conv.status === 'waiting_agent' ? '⏳ En attente' : '🤖 Bot';
323
+ const status_label = is_closed
324
+ ? '✅ Fermée'
325
+ : conv.status === 'agent'
326
+ ? '👤 Agent'
327
+ : conv.status === 'waiting_agent'
328
+ ? '⏳ En attente'
329
+ : '🤖 Bot';
257
330
  const status_color = is_closed ? '#9ca3af' : conv.status === 'waiting_agent' ? '#f59e0b' : color;
258
331
 
259
- const date = new Date(conv.updated_at).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
332
+ const date = new Date(conv.updated_at).toLocaleDateString('fr-FR', {
333
+ day: '2-digit',
334
+ month: '2-digit',
335
+ hour: '2-digit',
336
+ minute: '2-digit',
337
+ });
260
338
 
261
339
  item.innerHTML = `
262
340
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
@@ -280,7 +358,6 @@
280
358
  `;
281
359
  new_btn_wrap.querySelector('button').addEventListener('click', () => this.startNewConversation());
282
360
  container.appendChild(new_btn_wrap);
283
-
284
361
  } catch (e) {
285
362
  console.error('showConversationList error:', e);
286
363
  container.innerHTML = '<div style="padding:16px;text-align:center;color:#ef4444;font-size:13px;">Erreur de chargement.</div>';
@@ -304,6 +381,7 @@
304
381
 
305
382
  const input_bar = this.querySelector('#tp-chatbot-input-bar');
306
383
  const agent_bar = this.querySelector('#tp-agent-bar');
384
+ const close_bar = this.querySelector('#tp-close-bar');
307
385
 
308
386
  try {
309
387
  const url = `${this.api_url}/chat/conversations/${conversation_id}?user_id=${encodeURIComponent(this.user_id)}`;
@@ -320,7 +398,16 @@
320
398
 
321
399
  this.last_message_count = conv.messages.length;
322
400
 
323
- if (conv.status === 'closed') { this.showClosed(); return; }
401
+ if (conv.status === 'closed') {
402
+ this.showClosed();
403
+ return;
404
+ }
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
+ }
324
411
 
325
412
  if (conv.status === 'agent') {
326
413
  this.agent_mode = true;
@@ -353,6 +440,9 @@
353
440
  const back_btn = this.querySelector('#tp-back-btn');
354
441
  if (back_btn) back_btn.style.display = 'block';
355
442
 
443
+ const close_bar = this.querySelector('#tp-close-bar');
444
+ if (close_bar) close_bar.style.display = 'block';
445
+
356
446
  const container = this.querySelector('#tp-messages');
357
447
  container.innerHTML = '';
358
448
 
@@ -425,11 +515,10 @@
425
515
 
426
516
  this.user_info = { user_id: `guest-${email}`, first_name, last_name: '', company_name, email, language: 'FR', site_id: '' };
427
517
  this.user_id = this.user_info.user_id;
428
- this.applyTheme();
429
518
 
430
519
  const form_el = this.querySelector('#tp-guest-form');
431
520
  if (form_el) form_el.remove();
432
-
521
+ this.view = 'chat';
433
522
  this.conversation_id = await this.createConversation();
434
523
 
435
524
  const input_bar = this.querySelector('#tp-chatbot-input-bar');
@@ -438,6 +527,9 @@
438
527
  const agent_bar = this.querySelector('#tp-agent-bar');
439
528
  if (agent_bar) agent_bar.style.display = 'block';
440
529
 
530
+ const close_bar = this.querySelector('#tp-close-bar');
531
+ if (close_bar) close_bar.style.display = 'block';
532
+
441
533
  this.addMessage('assistant', `Bonjour ${first_name} ! Comment puis-je vous aider aujourd'hui ?`);
442
534
  this.polling_interval = setInterval(() => this.pollMessages(), 3000);
443
535
  }
@@ -449,16 +541,18 @@
449
541
  const container = this.querySelector('#tp-messages');
450
542
 
451
543
  const role_label =
452
- role === 'user' ? `👤 ${this.user_info?.first_name || 'Vous'}`
453
- : role === 'agent' ? '👨‍💼 Agent'
454
- : role === 'system' ? 'ℹ️ Info'
455
- : '🤖 Assistant';
544
+ role === 'user'
545
+ ? `👤 ${this.user_info?.first_name || 'Vous'}`
546
+ : role === 'agent'
547
+ ? '👨‍💼 Agent'
548
+ : role === 'system'
549
+ ? 'ℹ️ Info'
550
+ : '🤖 Assistant';
456
551
 
457
552
  const msg_el = document.createElement('div');
458
553
  msg_el.className = `tp-chatbot-message ${role}`;
459
- const rendered_content = (role === 'assistant' || role === 'agent')
460
- ? (typeof marked !== 'undefined' ? marked.parse(content) : content)
461
- : content;
554
+ const rendered_content =
555
+ role === 'assistant' || role === 'agent' ? (typeof marked !== 'undefined' ? marked.parse(content) : content) : content;
462
556
 
463
557
  msg_el.innerHTML = `
464
558
  <div class="tp-chatbot-bubble-msg">
@@ -468,6 +562,10 @@
468
562
  `;
469
563
  container.appendChild(msg_el);
470
564
  container.scrollTop = container.scrollHeight;
565
+
566
+ if ((role === 'assistant' || role === 'agent') && this.sound_enabled) {
567
+ playSound();
568
+ }
471
569
  }
472
570
 
473
571
  showTyping() {
@@ -487,8 +585,10 @@
487
585
 
488
586
  showClosed() {
489
587
  this.is_closed = true;
490
-
491
- if (this.polling_interval) { clearInterval(this.polling_interval); this.polling_interval = null; }
588
+ if (this.polling_interval) {
589
+ clearInterval(this.polling_interval);
590
+ this.polling_interval = null;
591
+ }
492
592
 
493
593
  const subtitle = this.querySelector('#tp-subtitle');
494
594
  if (subtitle) subtitle.textContent = '✅ Conversation terminée';
@@ -499,6 +599,9 @@
499
599
  const agent_bar = this.querySelector('#tp-agent-bar');
500
600
  if (agent_bar) agent_bar.style.display = 'none';
501
601
 
602
+ const close_bar = this.querySelector('#tp-close-bar');
603
+ if (close_bar) close_bar.style.display = 'none';
604
+
502
605
  const container = this.querySelector('#tp-messages');
503
606
  if (this.querySelector('#tp-closed-banner')) return;
504
607
 
@@ -509,11 +612,11 @@
509
612
  banner.id = 'tp-closed-banner';
510
613
  banner.innerHTML = `
511
614
  <div style="margin:16px;padding:16px;background:#fef2f2;border:1px solid #fecaca;border-radius:10px;text-align:center;">
512
- <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>
513
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%;">
514
617
  💬 Nouvelle conversation
515
618
  </button>
516
- <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%;">
517
620
  ← Voir toutes les conversations
518
621
  </button>
519
622
  </div>
@@ -535,7 +638,10 @@
535
638
 
536
639
  const user_messages = this.messages.filter(m => m.role === 'user').length;
537
640
  if (user_messages >= 30) {
538
- this.addMessage('system', 'Vous avez atteint la limite de 30 messages. Veuillez contacter un agent ou écrire à support@travelplanet.com.');
641
+ this.addMessage(
642
+ 'system',
643
+ 'Vous avez atteint la limite de 30 messages. Veuillez contacter un agent ou écrire à support@travelplanet.com.'
644
+ );
539
645
  const input_bar = this.querySelector('#tp-chatbot-input-bar');
540
646
  if (input_bar) input_bar.style.display = 'none';
541
647
  return;
@@ -604,6 +710,20 @@
604
710
  }
605
711
  }
606
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
+
607
727
  async pollMessages() {
608
728
  if (!this.conversation_id || this.is_closed || this.view !== 'chat') return;
609
729
  try {
@@ -619,7 +739,10 @@
619
739
  const server_status = conv.status;
620
740
  const is_typing = conv.is_typing || false;
621
741
 
622
- if (server_status === 'closed' && !this.is_closed) { this.showClosed(); return; }
742
+ if (server_status === 'closed' && !this.is_closed) {
743
+ this.showClosed();
744
+ return;
745
+ }
623
746
 
624
747
  if (is_typing && server_status === 'agent') {
625
748
  if (!this.querySelector('#tp-agent-typing')) {
@@ -673,4 +796,4 @@
673
796
  }
674
797
 
675
798
  customElements.define('tp-chatbot', TpChatbot);
676
- })();
799
+ })();