@developpement/tp-chatbot-widget 0.0.7 → 0.0.9

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@developpement/tp-chatbot-widget",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Chatbot widget for TravelPlanet / Makitizy",
5
5
  "main": "src/chatbot.js",
6
6
  "files": [
@@ -10,8 +10,7 @@
10
10
  "keywords": [
11
11
  "chatbot",
12
12
  "widget",
13
- "makitizy",
14
- "travelplanet"
13
+ "makitizy"
15
14
  ],
16
15
  "license": "ISC"
17
16
  }
package/src/chatbot.css CHANGED
@@ -32,8 +32,8 @@
32
32
  .tp-chatbot-window {
33
33
  position: absolute;
34
34
  bottom: 70px;
35
- width: 360px;
36
- height: 500px;
35
+ width: 380px;
36
+ height: 580px;
37
37
  background: white;
38
38
  border-radius: 16px;
39
39
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
package/src/chatbot.js CHANGED
@@ -2,10 +2,90 @@
2
2
  'use strict';
3
3
 
4
4
  const CLIENT_THEMES = {
5
- flix: { primary: '#5cc500', name: 'Flix Corporate', position: 'left' },
6
- sncf: { primary: '#1a6b5a', name: 'SNCF', position: 'right' },
5
+ flix: {
6
+ primary: '#73d700',
7
+ name: 'Flix Corporate',
8
+ position: 'left',
9
+ suggestions: [
10
+ {
11
+ FR: 'Comment réserver un billet ?',
12
+ EN: 'How to book a ticket?',
13
+ DE: 'Wie buche ich ein Ticket?',
14
+ ES: '¿Cómo reservar un billete?',
15
+ IT: 'Come prenotare un biglietto?',
16
+ NL: 'Hoe boek ik een ticket?',
17
+ },
18
+ {
19
+ FR: 'Annuler ma réservation',
20
+ EN: 'Cancel my booking',
21
+ DE: 'Meine Buchung stornieren',
22
+ ES: 'Cancelar mi reserva',
23
+ IT: 'Cancellare la mia prenotazione',
24
+ NL: 'Mijn boeking annuleren',
25
+ },
26
+ {
27
+ FR: 'Politique bagages',
28
+ EN: 'Baggage policy',
29
+ DE: 'Gepäckrichtlinien',
30
+ ES: 'Política de equipaje',
31
+ IT: 'Politica bagagli',
32
+ NL: 'Bagagebeleid',
33
+ },
34
+ {
35
+ FR: 'Mon compte entreprise',
36
+ EN: 'My corporate account',
37
+ DE: 'Mein Unternehmenskonto',
38
+ ES: 'Mi cuenta empresarial',
39
+ IT: 'Il mio account aziendale',
40
+ NL: 'Mijn zakelijk account',
41
+ },
42
+ ],
43
+ },
44
+ sncf: {
45
+ primary: '#1a6b5a',
46
+ name: 'SNCF',
47
+ position: 'right',
48
+ suggestions: [
49
+ {
50
+ FR: 'Comment réserver ?',
51
+ EN: 'How to book?',
52
+ DE: 'Wie buche ich?',
53
+ ES: '¿Cómo reservar?',
54
+ IT: 'Come prenotare?',
55
+ NL: 'Hoe boek ik?',
56
+ },
57
+ {
58
+ FR: 'Annuler un billet',
59
+ EN: 'Cancel a ticket',
60
+ DE: 'Ticket stornieren',
61
+ ES: 'Cancelar un billete',
62
+ IT: 'Cancellare un biglietto',
63
+ NL: 'Ticket annuleren',
64
+ },
65
+ {
66
+ FR: 'Politique bagages',
67
+ EN: 'Baggage policy',
68
+ DE: 'Gepäckrichtlinien',
69
+ ES: 'Política de equipaje',
70
+ IT: 'Politica bagagli',
71
+ NL: 'Bagagebeleid',
72
+ },
73
+ {
74
+ FR: 'Retard ou incident',
75
+ EN: 'Delay or incident',
76
+ DE: 'Verspätung oder Vorfall',
77
+ ES: 'Retraso o incidente',
78
+ IT: 'Ritardo o incidente',
79
+ NL: 'Vertraging of incident',
80
+ },
81
+ ],
82
+ },
7
83
  };
8
- const DEFAULT_THEME = { primary: '#7b1fa2', name: 'Support', position: 'right' };
84
+
85
+ const DEFAULT_THEME = { primary: '#7b1fa2', name: 'Support', position: 'right', suggestions: [] };
86
+
87
+ const WINDOW_WIDTH = '380px';
88
+ const WINDOW_HEIGHT = '580px';
9
89
 
10
90
  function getClientTheme(client_id) {
11
91
  return CLIENT_THEMES[client_id] || DEFAULT_THEME;
@@ -20,7 +100,7 @@
20
100
  first_name: payload.firstName || '',
21
101
  last_name: payload.lastName || '',
22
102
  company_name: payload.companyName || '',
23
- language: payload.language || 'FR',
103
+ language: payload.language || 'EN',
24
104
  site_id: payload.siteId || '',
25
105
  };
26
106
  } catch {
@@ -63,6 +143,8 @@
63
143
  this.sound_enabled = true;
64
144
  this.client_id = 'flix';
65
145
  this.access_token = '';
146
+ this.is_fullscreen = false;
147
+ this.is_minimized = false;
66
148
  }
67
149
 
68
150
  static get observedAttributes() {
@@ -98,6 +180,8 @@
98
180
  this.polling_interval = null;
99
181
  this.is_closed = false;
100
182
  this.view = 'list';
183
+ this.is_fullscreen = false;
184
+ this.is_minimized = false;
101
185
 
102
186
  this.render();
103
187
  this.applyTheme();
@@ -127,6 +211,215 @@
127
211
  if (this.polling_interval) clearInterval(this.polling_interval);
128
212
  }
129
213
 
214
+ // ─── i18n ─────────────────────────────────────────────────────────────────
215
+
216
+ getMessages() {
217
+ const lang = (this.user_info?.language || 'EN').toUpperCase();
218
+ const msgs = {
219
+ FR: {
220
+ closed: '✅ Conversation clôturée',
221
+ csat_question: 'Cette conversation vous a-t-elle été utile ?',
222
+ csat_positive: '👍 Merci pour votre retour !',
223
+ csat_negative: '👎 Merci, nous allons nous améliorer.',
224
+ new_conv: '💬 Nouvelle conversation',
225
+ back_to_list: '← Voir toutes les conversations',
226
+ agent_taken: 'Un agent a pris en charge votre conversation. Vous pouvez lui écrire directement.',
227
+ agent_released: 'Notre assistant virtuel reprend la conversation. Comment puis-je vous aider ?',
228
+ close_confirm: 'Voulez-vous terminer cette conversation ?',
229
+ terminated: '✅ Conversation terminée',
230
+ virtual: '🤖 Assistant virtuel',
231
+ human_agent: '👤 Agent humain',
232
+ loading: 'Chargement...',
233
+ no_conv: 'Aucune conversation pour le moment.',
234
+ new_conv_btn: '💬 Nouvelle conversation',
235
+ error_load: 'Erreur de chargement.',
236
+ recent_conv: 'Vos conversations récentes',
237
+ hello: 'Bonjour',
238
+ close_btn: '✅ Terminer la conversation',
239
+ speak_agent: '👤 Parler à un agent',
240
+ write_msg: 'Écrivez votre message...',
241
+ limit_reached: 'Vous avez atteint la limite de 30 messages. Veuillez contacter un agent ou écrire à support@travelplanet.com.',
242
+ too_many: 'Trop de messages. Veuillez patienter avant de réessayer.',
243
+ error_occurred: 'Une erreur est survenue. Veuillez réessayer.',
244
+ outside_hours: next => `ℹ️ Notre support est actuellement fermé. Prochain créneau : ${next}.`,
245
+ no_agents: 'ℹ️ Aucun agent disponible pour le moment. Veuillez réessayer plus tard.',
246
+ waiting_agent: '✅ Votre demande a été transmise. Un agent va prendre en charge votre conversation.',
247
+ waiting_agent_already: "ℹ️ Vous êtes déjà en file d'attente. Merci de patienter.",
248
+ agent_already: 'ℹ️ Un agent gère déjà votre conversation.',
249
+ },
250
+ EN: {
251
+ closed: '✅ Conversation closed',
252
+ csat_question: 'Was this conversation helpful?',
253
+ csat_positive: '👍 Thank you for your feedback!',
254
+ csat_negative: '👎 Thank you, we will improve.',
255
+ new_conv: '💬 New conversation',
256
+ back_to_list: '← Back to conversations',
257
+ agent_taken: 'An agent has taken over your conversation. You can now write to them directly.',
258
+ agent_released: 'Our virtual assistant is back. How can I help you?',
259
+ close_confirm: 'Do you want to end this conversation?',
260
+ terminated: '✅ Conversation ended',
261
+ virtual: '🤖 Virtual assistant',
262
+ human_agent: '👤 Human agent',
263
+ loading: 'Loading...',
264
+ no_conv: 'No conversations yet.',
265
+ new_conv_btn: '💬 New conversation',
266
+ error_load: 'Loading error.',
267
+ recent_conv: 'Your recent conversations',
268
+ hello: 'Hello',
269
+ close_btn: '✅ End conversation',
270
+ speak_agent: '👤 Talk to an agent',
271
+ write_msg: 'Write your message...',
272
+ limit_reached: 'You have reached the 30 message limit. Please contact an agent or write to support@travelplanet.com.',
273
+ too_many: 'Too many messages. Please wait before retrying.',
274
+ error_occurred: 'An error occurred. Please try again.',
275
+ outside_hours: next => `ℹ️ Our support is currently closed. Next available slot: ${next}.`,
276
+ no_agents: 'ℹ️ No agent available at the moment. Please try again later.',
277
+ waiting_agent: '✅ Your request has been sent. An agent will take over shortly.',
278
+ waiting_agent_already: 'ℹ️ You are already in the queue. Please wait.',
279
+ agent_already: 'ℹ️ An agent is already handling your conversation.',
280
+ },
281
+ DE: {
282
+ closed: '✅ Gespräch beendet',
283
+ csat_question: 'War dieses Gespräch hilfreich?',
284
+ csat_positive: '👍 Vielen Dank für Ihr Feedback!',
285
+ csat_negative: '👎 Danke, wir werden uns verbessern.',
286
+ new_conv: '💬 Neues Gespräch',
287
+ back_to_list: '← Zurück zur Liste',
288
+ agent_taken: 'Ein Agent hat Ihr Gespräch übernommen. Sie können ihm jetzt direkt schreiben.',
289
+ agent_released: 'Unser virtueller Assistent ist zurück. Wie kann ich Ihnen helfen?',
290
+ close_confirm: 'Möchten Sie das Gespräch beenden?',
291
+ terminated: '✅ Gespräch beendet',
292
+ virtual: '🤖 Virtueller Assistent',
293
+ human_agent: '👤 Menschlicher Agent',
294
+ loading: 'Laden...',
295
+ no_conv: 'Noch keine Gespräche.',
296
+ new_conv_btn: '💬 Neues Gespräch',
297
+ error_load: 'Ladefehler.',
298
+ recent_conv: 'Ihre letzten Gespräche',
299
+ hello: 'Hallo',
300
+ close_btn: '✅ Gespräch beenden',
301
+ speak_agent: '👤 Mit einem Agenten sprechen',
302
+ write_msg: 'Schreiben Sie Ihre Nachricht...',
303
+ limit_reached: 'Sie haben das Limit von 30 Nachrichten erreicht.',
304
+ too_many: 'Zu viele Nachrichten. Bitte warten Sie.',
305
+ error_occurred: 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.',
306
+ outside_hours: next => `ℹ️ Unser Support ist derzeit geschlossen. Nächster Termin: ${next}.`,
307
+ no_agents: 'ℹ️ Derzeit kein Agent verfügbar. Bitte versuchen Sie es später.',
308
+ waiting_agent: '✅ Ihre Anfrage wurde übermittelt. Ein Agent übernimmt gleich.',
309
+ waiting_agent_already: 'ℹ️ Sie sind bereits in der Warteschlange. Bitte warten.',
310
+ agent_already: 'ℹ️ Ein Agent betreut bereits Ihr Gespräch.',
311
+ },
312
+ ES: {
313
+ closed: '✅ Conversación cerrada',
314
+ csat_question: '¿Fue útil esta conversación?',
315
+ csat_positive: '👍 ¡Gracias por su opinión!',
316
+ csat_negative: '👎 Gracias, mejoraremos.',
317
+ new_conv: '💬 Nueva conversación',
318
+ back_to_list: '← Volver a las conversaciones',
319
+ agent_taken: 'Un agente ha tomado su conversación. Ahora puede escribirle directamente.',
320
+ agent_released: 'Nuestro asistente virtual ha vuelto. ¿En qué puedo ayudarle?',
321
+ close_confirm: '¿Desea finalizar esta conversación?',
322
+ terminated: '✅ Conversación terminada',
323
+ virtual: '🤖 Asistente virtual',
324
+ human_agent: '👤 Agente humano',
325
+ loading: 'Cargando...',
326
+ no_conv: 'No hay conversaciones por el momento.',
327
+ new_conv_btn: '💬 Nueva conversación',
328
+ error_load: 'Error de carga.',
329
+ recent_conv: 'Sus conversaciones recientes',
330
+ hello: 'Hola',
331
+ close_btn: '✅ Finalizar conversación',
332
+ speak_agent: '👤 Hablar con un agente',
333
+ write_msg: 'Escriba su mensaje...',
334
+ limit_reached: 'Ha alcanzado el límite de 30 mensajes.',
335
+ too_many: 'Demasiados mensajes. Por favor espere.',
336
+ error_occurred: 'Se produjo un error. Por favor, inténtelo de nuevo.',
337
+ outside_hours: next => `ℹ️ Nuestro soporte está cerrado. Próximo horario: ${next}.`,
338
+ no_agents: 'ℹ️ No hay agentes disponibles. Inténtelo más tarde.',
339
+ waiting_agent: '✅ Su solicitud ha sido enviada. Un agente le atenderá pronto.',
340
+ waiting_agent_already: 'ℹ️ Ya está en la cola de espera. Por favor, espere.',
341
+ agent_already: 'ℹ️ Un agente ya está gestionando su conversación.',
342
+ },
343
+ IT: {
344
+ closed: '✅ Conversazione chiusa',
345
+ csat_question: 'Questa conversazione è stata utile?',
346
+ csat_positive: '👍 Grazie per il suo feedback!',
347
+ csat_negative: '👎 Grazie, miglioreremo.',
348
+ new_conv: '💬 Nuova conversazione',
349
+ back_to_list: '← Torna alle conversazioni',
350
+ agent_taken: 'Un agente ha preso in carico la sua conversazione.',
351
+ agent_released: 'Il nostro assistente virtuale è tornato. Come posso aiutarla?',
352
+ close_confirm: 'Vuole terminare questa conversazione?',
353
+ terminated: '✅ Conversazione terminata',
354
+ virtual: '🤖 Assistente virtuale',
355
+ human_agent: '👤 Agente umano',
356
+ loading: 'Caricamento...',
357
+ no_conv: 'Nessuna conversazione per il momento.',
358
+ new_conv_btn: '💬 Nuova conversazione',
359
+ error_load: 'Errore di caricamento.',
360
+ recent_conv: 'Le sue conversazioni recenti',
361
+ hello: 'Ciao',
362
+ close_btn: '✅ Termina conversazione',
363
+ speak_agent: '👤 Parla con un agente',
364
+ write_msg: 'Scrivi il tuo messaggio...',
365
+ limit_reached: 'Hai raggiunto il limite di 30 messaggi.',
366
+ too_many: 'Troppi messaggi. Attendere prego.',
367
+ error_occurred: 'Si è verificato un errore. Riprova.',
368
+ outside_hours: next => `ℹ️ Il supporto è chiuso. Prossima disponibilità: ${next}.`,
369
+ no_agents: 'ℹ️ Nessun agente disponibile. Riprova più tardi.',
370
+ waiting_agent: '✅ Richiesta inviata. Un agente prenderà in carico la conversazione.',
371
+ waiting_agent_already: 'ℹ️ È già in coda. Attenda.',
372
+ agent_already: 'ℹ️ Un agente sta già gestendo la conversazione.',
373
+ },
374
+ NL: {
375
+ closed: '✅ Gesprek gesloten',
376
+ csat_question: 'Was dit gesprek nuttig?',
377
+ csat_positive: '👍 Bedankt voor uw feedback!',
378
+ csat_negative: '👎 Bedankt, we zullen verbeteren.',
379
+ new_conv: '💬 Nieuw gesprek',
380
+ back_to_list: '← Terug naar gesprekken',
381
+ agent_taken: 'Een agent heeft uw gesprek overgenomen.',
382
+ agent_released: 'Onze virtuele assistent is terug. Hoe kan ik u helpen?',
383
+ close_confirm: 'Wilt u dit gesprek beëindigen?',
384
+ terminated: '✅ Gesprek beëindigd',
385
+ virtual: '🤖 Virtuele assistent',
386
+ human_agent: '👤 Menselijke agent',
387
+ loading: 'Laden...',
388
+ no_conv: 'Geen gesprekken op dit moment.',
389
+ new_conv_btn: '💬 Nieuw gesprek',
390
+ error_load: 'Laadфout.',
391
+ recent_conv: 'Uw recente gesprekken',
392
+ hello: 'Hallo',
393
+ close_btn: '✅ Gesprek beëindigen',
394
+ speak_agent: '👤 Praat met een agent',
395
+ write_msg: 'Schrijf uw bericht...',
396
+ limit_reached: 'U heeft de limiet van 30 berichten bereikt.',
397
+ too_many: 'Te veel berichten. Wacht even.',
398
+ error_occurred: 'Er is een fout opgetreden. Probeer het opnieuw.',
399
+ outside_hours: next => `ℹ️ Onze support is gesloten. Volgende beschikbare tijd: ${next}.`,
400
+ no_agents: 'ℹ️ Geen agent beschikbaar. Probeer het later opnieuw.',
401
+ waiting_agent: '✅ Uw verzoek is verzonden. Een agent neemt het gesprek over.',
402
+ waiting_agent_already: 'ℹ️ U staat al in de wachtrij. Even geduld.',
403
+ agent_already: 'ℹ️ Een agent beheert uw gesprek al.',
404
+ },
405
+ };
406
+ return msgs[lang] || msgs['EN'];
407
+ }
408
+
409
+ getWelcomeMessage(first_name = '') {
410
+ const lang = (this.user_info?.language || 'EN').toUpperCase();
411
+ const client_name = getClientTheme(this.client_id).name;
412
+ const msgs = {
413
+ FR: `Bonjour ${first_name} ! 👋 Je m'appelle Maria, votre assistante virtuelle ${client_name}. Comment puis-je vous aider aujourd'hui ?`,
414
+ EN: `Hello ${first_name}! 👋 My name is Maria, your ${client_name} virtual assistant. How can I help you today?`,
415
+ DE: `Hallo ${first_name}! 👋 Ich bin Maria, Ihre virtuelle Assistentin von ${client_name}. Wie kann ich Ihnen helfen?`,
416
+ ES: `¡Hola ${first_name}! 👋 Me llamo Maria, tu asistente virtual de ${client_name}. ¿En qué puedo ayudarte?`,
417
+ IT: `Ciao ${first_name}! 👋 Mi chiamo Maria, la tua assistente virtuale di ${client_name}. Come posso aiutarti?`,
418
+ NL: `Hallo ${first_name}! 👋 Ik ben Maria, uw virtuele assistent van ${client_name}. Hoe kan ik u helpen?`,
419
+ };
420
+ return msgs[lang] || msgs['EN'];
421
+ }
422
+
130
423
  // ─── Theme ────────────────────────────────────────────────────────────────
131
424
 
132
425
  getThemeColor() {
@@ -167,10 +460,9 @@
167
460
  document.head.appendChild(style);
168
461
 
169
462
  const position = getClientTheme(this.client_id).position || 'right';
170
-
171
463
  const host = this.querySelector('.tp-chatbot-host');
172
464
  if (host) {
173
- host.style.width = '360px';
465
+ host.style.width = WINDOW_WIDTH;
174
466
  host.style.left = position === 'left' ? '24px' : 'auto';
175
467
  host.style.right = position === 'left' ? 'auto' : '24px';
176
468
  }
@@ -200,22 +492,24 @@
200
492
  <div class="tp-chatbot-avatar">M</div>
201
493
  <div style="flex:1">
202
494
  <div class="tp-chatbot-title">${theme.name} Support</div>
203
- <div class="tp-chatbot-subtitle" id="tp-subtitle">🤖 Assistant virtuel</div>
495
+ <div class="tp-chatbot-subtitle" id="tp-subtitle">🤖 Assistant</div>
204
496
  </div>
205
- <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>
206
- <button id="tp-back-btn" style="display:none;background:none;border:none;color:white;font-size:18px;cursor:pointer;padding:4px 8px;">←</button>
497
+ <button id="tp-sound-btn" style="background:none;border:none;color:white;font-size:15px;cursor:pointer;padding:4px 6px;" title="Sound">🔔</button>
498
+ <button id="tp-minimize-btn" style="background:none;border:none;color:white;font-size:15px;cursor:pointer;padding:4px 6px;" title="Minimize">─</button>
499
+ <button id="tp-maximize-btn" style="background:none;border:none;color:white;font-size:15px;cursor:pointer;padding:4px 6px;" title="Fullscreen">□</button>
500
+ <button id="tp-back-btn" style="display:none;background:none;border:none;color:white;font-size:18px;cursor:pointer;padding:4px 6px;">←</button>
207
501
  </div>
208
502
  <div class="tp-chatbot-messages" id="tp-messages"></div>
209
- <div class="tp-chatbot-agent-bar" id="tp-agent-bar">
210
- <button class="tp-chatbot-agent-btn" id="tp-agent-btn">👤 Parler à un agent</button>
503
+ <div class="tp-chatbot-agent-bar" id="tp-agent-bar" style="display:none;">
504
+ <button class="tp-chatbot-agent-btn" id="tp-agent-btn">👤 Talk to an agent</button>
211
505
  </div>
212
- <div class="tp-chatbot-input-bar" id="tp-chatbot-input-bar">
213
- <textarea class="tp-chatbot-input" id="tp-input" placeholder="Écrivez votre message..." rows="1"></textarea>
506
+ <div class="tp-chatbot-input-bar" id="tp-chatbot-input-bar" style="display:none;">
507
+ <textarea class="tp-chatbot-input" id="tp-input" placeholder="Write your message..." rows="1"></textarea>
214
508
  <button class="tp-chatbot-send" id="tp-send">➤</button>
215
509
  </div>
216
510
  <div id="tp-close-bar" style="padding:8px 12px;border-top:1px solid #ede8f5;text-align:center;display:none;">
217
511
  <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;">
218
- Terminer la conversation
512
+ End conversation
219
513
  </button>
220
514
  </div>
221
515
  </div>
@@ -224,28 +518,115 @@
224
518
  `;
225
519
  }
226
520
 
521
+ // ─── Window controls ──────────────────────────────────────────────────────
522
+
523
+ minimize() {
524
+ const win = this.querySelector('#tp-window');
525
+ // Exit fullscreen first if needed
526
+ if (this.is_fullscreen) this.exitFullscreen(false);
527
+ this.is_minimized = true;
528
+ win.style.height = '56px';
529
+ win.style.overflow = 'hidden';
530
+ }
531
+
532
+ enterFullscreen() {
533
+ const win = this.querySelector('#tp-window');
534
+ const host = this.querySelector('.tp-chatbot-host');
535
+ this.is_fullscreen = true;
536
+ this.is_minimized = false;
537
+ win.style.position = 'fixed';
538
+ win.style.top = '0';
539
+ win.style.left = '0';
540
+ win.style.width = '100vw';
541
+ win.style.height = '100vh';
542
+ win.style.borderRadius = '0';
543
+ win.style.zIndex = '99999';
544
+ host.style.width = '100vw';
545
+ this.querySelector('#tp-maximize-btn').textContent = '⤡';
546
+ }
547
+
548
+ exitFullscreen(restore_height = true) {
549
+ const win = this.querySelector('#tp-window');
550
+ const position = getClientTheme(this.client_id).position || 'right';
551
+ const host = this.querySelector('.tp-chatbot-host');
552
+ this.is_fullscreen = false;
553
+ win.style.position = 'absolute';
554
+ win.style.top = '';
555
+ win.style.left = '0';
556
+ win.style.width = WINDOW_WIDTH;
557
+ win.style.borderRadius = '16px';
558
+ win.style.zIndex = '';
559
+ host.style.width = WINDOW_WIDTH;
560
+ host.style.left = position === 'left' ? '24px' : 'auto';
561
+ host.style.right = position === 'left' ? 'auto' : '24px';
562
+ if (restore_height) win.style.height = WINDOW_HEIGHT;
563
+ win.style.overflow = 'hidden';
564
+ this.querySelector('#tp-maximize-btn').textContent = '□';
565
+ }
566
+
567
+ // ─── Events ───────────────────────────────────────────────────────────────
568
+
227
569
  attachEvents() {
228
570
  this.querySelector('#tp-bubble').addEventListener('click', () => this.toggleChat());
229
571
  this.querySelector('#tp-send').addEventListener('click', () => this.sendMessage());
230
572
  this.querySelector('#tp-agent-btn').addEventListener('click', () => this.requestAgent());
231
573
  this.querySelector('#tp-back-btn').addEventListener('click', () => this.showConversationList());
574
+
232
575
  this.querySelector('#tp-input').addEventListener('keydown', e => {
233
576
  if (e.key === 'Enter' && !e.shiftKey) {
234
577
  e.preventDefault();
235
578
  this.sendMessage();
236
579
  }
237
580
  });
581
+
238
582
  this.querySelector('#tp-sound-btn').addEventListener('click', () => {
239
583
  this.sound_enabled = !this.sound_enabled;
240
- const btn = this.querySelector('#tp-sound-btn');
241
- btn.textContent = this.sound_enabled ? '🔔' : '🔕';
242
- btn.title = this.sound_enabled ? 'Désactiver le son' : 'Activer le son';
584
+ this.querySelector('#tp-sound-btn').textContent = this.sound_enabled ? '🔔' : '🔕';
243
585
  });
586
+
244
587
  this.querySelector('#tp-close-btn').addEventListener('click', () => {
245
- if (window.confirm('Voulez-vous terminer cette conversation ?')) {
588
+ if (window.confirm(this.getMessages().close_confirm)) {
246
589
  this.closeByUser();
247
590
  }
248
591
  });
592
+
593
+ this.querySelector('#tp-minimize-btn').addEventListener('click', () => {
594
+ if (this.is_minimized) {
595
+ // Restore
596
+ this.is_minimized = false;
597
+ const win = this.querySelector('#tp-window');
598
+ win.style.height = WINDOW_HEIGHT;
599
+ win.style.overflow = 'hidden';
600
+ } else {
601
+ this.minimize();
602
+ }
603
+ });
604
+
605
+ this.querySelector('#tp-maximize-btn').addEventListener('click', () => {
606
+ if (this.is_fullscreen) {
607
+ this.exitFullscreen(true);
608
+ } else {
609
+ if (this.is_minimized) {
610
+ this.is_minimized = false;
611
+ const win = this.querySelector('#tp-window');
612
+ win.style.height = WINDOW_HEIGHT;
613
+ win.style.overflow = 'hidden';
614
+ }
615
+ this.enterFullscreen();
616
+ }
617
+ });
618
+ }
619
+
620
+ updateUILanguage() {
621
+ const m = this.getMessages();
622
+ const agent_btn = this.querySelector('#tp-agent-btn');
623
+ if (agent_btn) agent_btn.textContent = m.speak_agent;
624
+ const close_btn = this.querySelector('#tp-close-btn');
625
+ if (close_btn) close_btn.textContent = m.close_btn;
626
+ const input = this.querySelector('#tp-input');
627
+ if (input) input.placeholder = m.write_msg;
628
+ const subtitle = this.querySelector('#tp-subtitle');
629
+ if (subtitle) subtitle.textContent = m.virtual;
249
630
  }
250
631
 
251
632
  toggleChat() {
@@ -255,9 +636,16 @@
255
636
  if (this.is_open) {
256
637
  window_el.classList.add('open');
257
638
  bubble.textContent = '✕';
639
+ // Restore from minimized if needed
640
+ if (this.is_minimized) {
641
+ this.is_minimized = false;
642
+ window_el.style.height = WINDOW_HEIGHT;
643
+ window_el.style.overflow = 'hidden';
644
+ }
258
645
  } else {
259
646
  window_el.classList.remove('open');
260
647
  bubble.textContent = '💬';
648
+ if (this.is_fullscreen) this.exitFullscreen(true);
261
649
  }
262
650
  }
263
651
 
@@ -282,10 +670,55 @@
282
670
  return data.result?.conversation_id;
283
671
  }
284
672
 
673
+ // ─── Suggestions ──────────────────────────────────────────────────────────
674
+
675
+ showSuggestions(suggestions) {
676
+ const existing = this.querySelector('#tp-suggestions');
677
+ if (existing) existing.remove();
678
+ if (!suggestions || suggestions.length === 0) return;
679
+
680
+ const lang = (this.user_info?.language || 'EN').toUpperCase();
681
+ const color = this.getThemeColor();
682
+ const container = this.querySelector('#tp-messages');
683
+
684
+ const wrap = document.createElement('div');
685
+ wrap.id = 'tp-suggestions';
686
+ wrap.style.cssText = 'padding:8px 12px 12px;display:flex;flex-direction:column;gap:6px;';
687
+
688
+ suggestions.forEach(s => {
689
+ const label = s[lang] || s['EN'];
690
+ const btn = document.createElement('button');
691
+ btn.style.cssText = `padding:8px 14px;border-radius:20px;border:1.5px solid ${color};background:white;color:${color};font-size:12px;font-weight:600;cursor:pointer;text-align:left;font-family:inherit;transition:all 0.12s;`;
692
+ btn.textContent = `→ ${label}`;
693
+ btn.addEventListener('mouseenter', () => {
694
+ btn.style.background = color;
695
+ btn.style.color = 'white';
696
+ });
697
+ btn.addEventListener('mouseleave', () => {
698
+ btn.style.background = 'white';
699
+ btn.style.color = color;
700
+ });
701
+ btn.addEventListener('click', () => {
702
+ const sugg_el = this.querySelector('#tp-suggestions');
703
+ if (sugg_el) sugg_el.remove();
704
+ const input = this.querySelector('#tp-input');
705
+ if (input) {
706
+ input.value = label;
707
+ this.sendMessage();
708
+ }
709
+ });
710
+ wrap.appendChild(btn);
711
+ });
712
+
713
+ container.appendChild(wrap);
714
+ container.scrollTop = container.scrollHeight;
715
+ }
716
+
285
717
  // ─── Conversation List ────────────────────────────────────────────────────
286
718
 
287
719
  async showConversationList() {
288
720
  this.view = 'list';
721
+ const m = this.getMessages();
289
722
 
290
723
  if (this.polling_interval) {
291
724
  clearInterval(this.polling_interval);
@@ -293,7 +726,7 @@
293
726
  }
294
727
 
295
728
  const subtitle = this.querySelector('#tp-subtitle');
296
- if (subtitle) subtitle.textContent = '🤖 Assistant virtuel';
729
+ if (subtitle) subtitle.textContent = m.virtual;
297
730
 
298
731
  const back_btn = this.querySelector('#tp-back-btn');
299
732
  if (back_btn) back_btn.style.display = 'none';
@@ -308,31 +741,29 @@
308
741
  if (close_bar) close_bar.style.display = 'none';
309
742
 
310
743
  const container = this.querySelector('#tp-messages');
311
- container.innerHTML = '<div style="padding:16px;text-align:center;color:#aaa;font-size:12px;">Chargement...</div>';
744
+ container.innerHTML = `<div style="padding:16px;text-align:center;color:#aaa;font-size:12px;">${m.loading}</div>`;
312
745
 
313
746
  try {
314
747
  const url = `${this.api_url}/chat/conversations?user_id=${encodeURIComponent(this.user_id)}&limit=5`;
315
748
  const response = await fetch(url, { headers: this.getHeaders() });
316
749
  const data = await response.json();
317
750
  const conversations = data.result?.conversations || [];
318
-
319
751
  const color = this.getThemeColor();
320
- const dark = this.shadeColor(color, -20);
321
752
 
322
753
  container.innerHTML = '';
323
754
 
324
755
  const header_el = document.createElement('div');
325
756
  header_el.style.cssText = 'padding:16px;border-bottom:1px solid #ede8f5;';
326
757
  header_el.innerHTML = `
327
- <div style="font-size:13px;font-weight:700;color:#1a1a2e;margin-bottom:4px;">Bonjour ${this.user_info?.first_name || ''} 👋</div>
328
- <div style="font-size:12px;color:#aaa;">Vos conversations récentes</div>
758
+ <div style="font-size:13px;font-weight:700;color:#1a1a2e;margin-bottom:4px;">${m.hello} ${this.user_info?.first_name || ''} 👋</div>
759
+ <div style="font-size:12px;color:#aaa;">${m.recent_conv}</div>
329
760
  `;
330
761
  container.appendChild(header_el);
331
762
 
332
763
  if (conversations.length === 0) {
333
764
  const empty = document.createElement('div');
334
765
  empty.style.cssText = 'padding:24px 16px;text-align:center;color:#aaa;font-size:13px;';
335
- empty.textContent = 'Aucune conversation pour le moment.';
766
+ empty.textContent = m.no_conv;
336
767
  container.appendChild(empty);
337
768
  } else {
338
769
  conversations.forEach(conv => {
@@ -342,14 +773,13 @@
342
773
 
343
774
  const is_closed = conv.status === 'closed';
344
775
  const status_label = is_closed
345
- ? '✅ Fermée'
776
+ ? '✅ Closed'
346
777
  : conv.status === 'agent'
347
778
  ? '👤 Agent'
348
779
  : conv.status === 'waiting_agent'
349
- ? '⏳ En attente'
780
+ ? '⏳ Waiting'
350
781
  : '🤖 Bot';
351
782
  const status_color = is_closed ? '#9ca3af' : conv.status === 'waiting_agent' ? '#f59e0b' : color;
352
-
353
783
  const date = new Date(conv.updated_at).toLocaleDateString('fr-FR', {
354
784
  day: '2-digit',
355
785
  month: '2-digit',
@@ -364,7 +794,6 @@
364
794
  </div>
365
795
  <div style="font-size:11px;color:#aaa;">${conv.message_count} message${conv.message_count > 1 ? 's' : ''}</div>
366
796
  `;
367
-
368
797
  item.addEventListener('click', () => this.openConversation(conv.conversation_id, is_closed));
369
798
  container.appendChild(item);
370
799
  });
@@ -372,16 +801,12 @@
372
801
 
373
802
  const new_btn_wrap = document.createElement('div');
374
803
  new_btn_wrap.style.cssText = 'padding:16px;';
375
- new_btn_wrap.innerHTML = `
376
- <button class="tp-conv-new-btn" style="width:100%;padding:12px;color:white;border:none;border-radius:10px;font-size:13px;font-weight:700;cursor:pointer;">
377
- 💬 Nouvelle conversation
378
- </button>
379
- `;
804
+ new_btn_wrap.innerHTML = `<button class="tp-conv-new-btn" style="width:100%;padding:12px;color:white;border:none;border-radius:10px;font-size:13px;font-weight:700;cursor:pointer;">${m.new_conv_btn}</button>`;
380
805
  new_btn_wrap.querySelector('button').addEventListener('click', () => this.startNewConversation());
381
806
  container.appendChild(new_btn_wrap);
382
807
  } catch (e) {
383
808
  console.error('showConversationList error:', e);
384
- container.innerHTML = '<div style="padding:16px;text-align:center;color:#ef4444;font-size:13px;">Erreur de chargement.</div>';
809
+ container.innerHTML = `<div style="padding:16px;text-align:center;color:#ef4444;font-size:13px;">${m.error_load}</div>`;
385
810
  }
386
811
  }
387
812
 
@@ -394,6 +819,7 @@
394
819
  this.agent_mode = false;
395
820
  this.agent_requested = false;
396
821
 
822
+ const m = this.getMessages();
397
823
  const back_btn = this.querySelector('#tp-back-btn');
398
824
  if (back_btn) back_btn.style.display = 'block';
399
825
 
@@ -433,16 +859,17 @@
433
859
  if (conv.status === 'agent') {
434
860
  this.agent_mode = true;
435
861
  const subtitle = this.querySelector('#tp-subtitle');
436
- if (subtitle) subtitle.textContent = '👤 Agent humain';
862
+ if (subtitle) subtitle.textContent = m.human_agent;
437
863
  if (agent_bar) agent_bar.style.display = 'none';
438
864
  if (input_bar) input_bar.style.display = 'flex';
439
865
  } else {
440
866
  if (input_bar) input_bar.style.display = 'flex';
441
- if (agent_bar) agent_bar.style.display = 'block';
867
+ if (agent_bar) agent_bar.style.display = 'none';
442
868
  const subtitle = this.querySelector('#tp-subtitle');
443
- if (subtitle) subtitle.textContent = '🤖 Assistant virtuel';
869
+ if (subtitle) subtitle.textContent = m.virtual;
444
870
  }
445
871
 
872
+ this.updateUILanguage();
446
873
  this.polling_interval = setInterval(() => this.pollMessages(), 3000);
447
874
  } catch (e) {
448
875
  console.error('openConversation error:', e);
@@ -458,6 +885,8 @@
458
885
  this.last_message_count = 0;
459
886
  this.conversation_id = null;
460
887
 
888
+ const m = this.getMessages();
889
+
461
890
  const back_btn = this.querySelector('#tp-back-btn');
462
891
  if (back_btn) back_btn.style.display = 'block';
463
892
 
@@ -468,7 +897,7 @@
468
897
  container.innerHTML = '';
469
898
 
470
899
  const subtitle = this.querySelector('#tp-subtitle');
471
- if (subtitle) subtitle.textContent = '🤖 Assistant virtuel';
900
+ if (subtitle) subtitle.textContent = m.virtual;
472
901
 
473
902
  const input_bar = this.querySelector('#tp-chatbot-input-bar');
474
903
  if (input_bar) input_bar.style.display = 'flex';
@@ -476,12 +905,16 @@
476
905
  const agent_bar = this.querySelector('#tp-agent-bar');
477
906
  if (agent_bar) agent_bar.style.display = 'none';
478
907
 
908
+ this.updateUILanguage();
479
909
  this.conversation_id = await this.createConversation();
480
- const client_name = this.client_id === 'flix' ? 'Flix Corporate' : this.client_id === 'sncf' ? 'SNCF' : this.client_id;
481
- this.addMessage(
482
- 'assistant',
483
- `Bonjour ${this.user_info?.first_name || ''} ! 👋 Je m'appelle Maria, votre assistante virtuelle ${client_name}. Comment puis-je vous aider aujourd'hui ?`
484
- );
910
+ this.addMessage('assistant', this.getWelcomeMessage(this.user_info?.first_name || ''));
911
+ this.last_message_count = 1;
912
+
913
+ const theme = getClientTheme(this.client_id);
914
+ if (theme.suggestions && theme.suggestions.length > 0) {
915
+ this.showSuggestions(theme.suggestions);
916
+ }
917
+
485
918
  this.polling_interval = setInterval(() => this.pollMessages(), 3000);
486
919
  }
487
920
 
@@ -493,7 +926,6 @@
493
926
 
494
927
  const input_bar = this.querySelector('#tp-chatbot-input-bar');
495
928
  if (input_bar) input_bar.style.display = 'none';
496
-
497
929
  const agent_bar = this.querySelector('#tp-agent-bar');
498
930
  if (agent_bar) agent_bar.style.display = 'none';
499
931
 
@@ -503,18 +935,17 @@
503
935
  const form_el = document.createElement('div');
504
936
  form_el.id = 'tp-guest-form';
505
937
  form_el.innerHTML = `
506
- <div style="padding: 16px; display: flex; flex-direction: column; gap: 12px;">
507
- <p style="margin: 0; font-size: 13px; color: #1a1a2e; font-weight: 600;">Avant de commencer, merci de vous identifier :</p>
508
- <input id="tp-guest-firstname" type="text" placeholder="Prénom *" style="padding: 10px 14px; border-radius: 8px; border: 1.5px solid #ede8f5; font-size: 13px; font-family: inherit; outline: none;" />
509
- <input id="tp-guest-company" type="text" placeholder="Nom d'entreprise *" style="padding: 10px 14px; border-radius: 8px; border: 1.5px solid #ede8f5; font-size: 13px; font-family: inherit; outline: none;" />
510
- <input id="tp-guest-email" type="email" placeholder="Email *" style="padding: 10px 14px; border-radius: 8px; border: 1.5px solid #ede8f5; font-size: 13px; font-family: inherit; outline: none;" />
511
- <button id="tp-guest-submit" style="padding: 10px; background: linear-gradient(135deg, ${color}, ${dark}); color: white; border: none; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer;">
512
- Commencer la discussion
938
+ <div style="padding:16px;display:flex;flex-direction:column;gap:12px;">
939
+ <p style="margin:0;font-size:13px;color:#1a1a2e;font-weight:600;">Before we start, please identify yourself:</p>
940
+ <input id="tp-guest-firstname" type="text" placeholder="First name *" style="padding:10px 14px;border-radius:8px;border:1.5px solid #ede8f5;font-size:13px;font-family:inherit;outline:none;" />
941
+ <input id="tp-guest-company" type="text" placeholder="Company name *" style="padding:10px 14px;border-radius:8px;border:1.5px solid #ede8f5;font-size:13px;font-family:inherit;outline:none;" />
942
+ <input id="tp-guest-email" type="email" placeholder="Email *" style="padding:10px 14px;border-radius:8px;border:1.5px solid #ede8f5;font-size:13px;font-family:inherit;outline:none;" />
943
+ <button id="tp-guest-submit" style="padding:10px;background:linear-gradient(135deg,${color},${dark});color:white;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;">
944
+ Start conversation
513
945
  </button>
514
- <p id="tp-guest-error" style="margin: 0; font-size: 12px; color: #c0392b; display: none;"></p>
946
+ <p id="tp-guest-error" style="margin:0;font-size:12px;color:#c0392b;display:none;"></p>
515
947
  </div>
516
948
  `;
517
-
518
949
  container.appendChild(form_el);
519
950
  this.querySelector('#tp-guest-submit').addEventListener('click', () => this.submitGuestForm());
520
951
  }
@@ -526,40 +957,41 @@
526
957
  const error_el = this.querySelector('#tp-guest-error');
527
958
 
528
959
  if (!first_name || !company_name || !email) {
529
- error_el.textContent = 'Veuillez remplir tous les champs.';
960
+ error_el.textContent = 'Please fill in all fields.';
530
961
  error_el.style.display = 'block';
531
962
  return;
532
963
  }
533
-
534
- const email_valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
535
- if (!email_valid) {
536
- error_el.textContent = 'Veuillez entrer un email valide.';
964
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
965
+ error_el.textContent = 'Please enter a valid email.';
537
966
  error_el.style.display = 'block';
538
967
  return;
539
968
  }
540
969
 
541
- this.user_info = { user_id: `guest-${email}`, first_name, last_name: '', company_name, email, language: 'FR', site_id: '' };
970
+ this.user_info = { user_id: `guest-${email}`, first_name, last_name: '', company_name, email, language: 'EN', site_id: '' };
542
971
  this.user_id = this.user_info.user_id;
543
972
 
544
973
  const form_el = this.querySelector('#tp-guest-form');
545
974
  if (form_el) form_el.remove();
975
+
546
976
  this.view = 'chat';
547
977
  this.conversation_id = await this.createConversation();
548
978
 
549
979
  const input_bar = this.querySelector('#tp-chatbot-input-bar');
550
980
  if (input_bar) input_bar.style.display = 'flex';
551
-
552
981
  const agent_bar = this.querySelector('#tp-agent-bar');
553
- if (agent_bar) agent_bar.style.display = 'block';
554
-
982
+ if (agent_bar) agent_bar.style.display = 'none';
555
983
  const close_bar = this.querySelector('#tp-close-bar');
556
984
  if (close_bar) close_bar.style.display = 'block';
557
985
 
558
- const client_name = this.client_id === 'flix' ? 'Flix Corporate' : this.client_id === 'sncf' ? 'SNCF' : this.client_id;
559
- this.addMessage(
560
- 'assistant',
561
- `Bonjour ${first_name} ! 👋 Je m'appelle Maria, votre assistante virtuelle ${client_name}. Comment puis-je vous aider aujourd'hui ?`
562
- );
986
+ this.updateUILanguage();
987
+ this.addMessage('assistant', this.getWelcomeMessage(first_name));
988
+ this.last_message_count = 1;
989
+
990
+ const theme = getClientTheme(this.client_id);
991
+ if (theme.suggestions && theme.suggestions.length > 0) {
992
+ this.showSuggestions(theme.suggestions);
993
+ }
994
+
563
995
  this.polling_interval = setInterval(() => this.pollMessages(), 3000);
564
996
  }
565
997
 
@@ -571,12 +1003,12 @@
571
1003
 
572
1004
  const role_label =
573
1005
  role === 'user'
574
- ? `👤 ${this.user_info?.first_name || 'Vous'}`
1006
+ ? `👤 ${this.user_info?.first_name || 'You'}`
575
1007
  : role === 'agent'
576
1008
  ? '👨‍💼 Agent'
577
1009
  : role === 'system'
578
1010
  ? 'ℹ️ Info'
579
- : '🤖 Assistant';
1011
+ : '🤖 Maria';
580
1012
 
581
1013
  const msg_el = document.createElement('div');
582
1014
  msg_el.className = `tp-chatbot-message ${role}`;
@@ -619,32 +1051,32 @@
619
1051
  this.polling_interval = null;
620
1052
  }
621
1053
 
1054
+ const m = this.getMessages();
622
1055
  const subtitle = this.querySelector('#tp-subtitle');
623
- if (subtitle) subtitle.textContent = '✅ Conversation terminée';
1056
+ if (subtitle) subtitle.textContent = m.terminated;
624
1057
 
625
1058
  const input_bar = this.querySelector('#tp-chatbot-input-bar');
626
1059
  if (input_bar) input_bar.style.display = 'none';
627
-
628
1060
  const agent_bar = this.querySelector('#tp-agent-bar');
629
1061
  if (agent_bar) agent_bar.style.display = 'none';
630
-
631
1062
  const close_bar = this.querySelector('#tp-close-bar');
632
1063
  if (close_bar) close_bar.style.display = 'none';
1064
+ const sugg_el = this.querySelector('#tp-suggestions');
1065
+ if (sugg_el) sugg_el.remove();
633
1066
 
634
1067
  const container = this.querySelector('#tp-messages');
635
-
636
1068
  const existing_banner = this.querySelector('#tp-closed-banner');
637
1069
  if (existing_banner) {
638
- if (existing_banner.querySelector('#tp-csat-block')) return; // déjà complet
639
- existing_banner.remove(); // incomplet, on recrée
1070
+ if (existing_banner.querySelector('#tp-csat-block')) return;
1071
+ existing_banner.remove();
640
1072
  }
641
1073
 
642
1074
  const color = this.getThemeColor();
643
1075
  const dark = this.shadeColor(color, -20);
644
1076
 
645
1077
  const csat_html = existing_csat
646
- ? `<div style="font-size:13px;color:#888;margin-bottom:14px;">${existing_csat === 'positive' ? '👍 Merci pour votre retour !' : '👎 Merci, nous allons nous améliorer.'}</div>`
647
- : `<div style="font-size:12px;color:#888;margin-bottom:10px;">Cette conversation vous a-t-elle été utile ?</div>
1078
+ ? `<div style="font-size:13px;color:#888;margin-bottom:14px;">${existing_csat === 'positive' ? m.csat_positive : m.csat_negative}</div>`
1079
+ : `<div style="font-size:12px;color:#888;margin-bottom:10px;">${m.csat_question}</div>
648
1080
  <div style="display:flex;gap:10px;justify-content:center;margin-bottom:14px;">
649
1081
  <button id="tp-csat-positive" style="padding:8px 20px;border-radius:8px;border:1.5px solid #22c55e;background:white;color:#22c55e;font-size:18px;cursor:pointer;font-weight:700;">👍</button>
650
1082
  <button id="tp-csat-negative" style="padding:8px 20px;border-radius:8px;border:1.5px solid #ef4444;background:white;color:#ef4444;font-size:18px;cursor:pointer;font-weight:700;">👎</button>
@@ -654,14 +1086,10 @@
654
1086
  banner.id = 'tp-closed-banner';
655
1087
  banner.innerHTML = `
656
1088
  <div style="margin:16px;padding:16px;background:#f9f5ff;border:1px solid #ede8f5;border-radius:12px;text-align:center;">
657
- <div style="font-size:13px;color:#1a1a2e;font-weight:600;margin-bottom:8px;">✅ Conversation clôturée</div>
1089
+ <div style="font-size:13px;color:#1a1a2e;font-weight:600;margin-bottom:8px;">${m.closed}</div>
658
1090
  <div id="tp-csat-block">${csat_html}</div>
659
- <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%;">
660
- 💬 Nouvelle conversation
661
- </button>
662
- <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%;">
663
- ← Voir toutes les conversations
664
- </button>
1091
+ <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%;">${m.new_conv}</button>
1092
+ <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%;">${m.back_to_list}</button>
665
1093
  </div>
666
1094
  `;
667
1095
  container.appendChild(banner);
@@ -680,9 +1108,8 @@
680
1108
  } catch (e) {
681
1109
  console.error('CSAT error:', e);
682
1110
  }
683
- csat_block.innerHTML = `<div style="font-size:13px;color:#888;margin-bottom:14px;">${rating === 'positive' ? '👍 Merci pour votre retour !' : '👎 Merci, nous allons nous améliorer.'}</div>`;
1111
+ csat_block.innerHTML = `<div style="font-size:13px;color:#888;margin-bottom:14px;">${rating === 'positive' ? m.csat_positive : m.csat_negative}</div>`;
684
1112
  };
685
-
686
1113
  this.querySelector('#tp-csat-positive').addEventListener('click', () => submit_csat('positive'));
687
1114
  this.querySelector('#tp-csat-negative').addEventListener('click', () => submit_csat('negative'));
688
1115
  }
@@ -695,16 +1122,17 @@
695
1122
 
696
1123
  async sendMessage() {
697
1124
  if (this.is_closed || !this.conversation_id) return;
1125
+ const m = this.getMessages();
698
1126
  const input = this.querySelector('#tp-input');
699
1127
  const query = input.value.trim();
700
1128
  if (!query || this.is_loading) return;
701
1129
 
702
- const user_messages = this.messages.filter(m => m.role === 'user').length;
1130
+ const sugg_el = this.querySelector('#tp-suggestions');
1131
+ if (sugg_el) sugg_el.remove();
1132
+
1133
+ const user_messages = this.messages.filter(msg => msg.role === 'user').length;
703
1134
  if (user_messages >= 30) {
704
- this.addMessage(
705
- 'system',
706
- 'Vous avez atteint la limite de 30 messages. Veuillez contacter un agent ou écrire à support@travelplanet.com.'
707
- );
1135
+ this.addMessage('system', m.limit_reached);
708
1136
  const input_bar = this.querySelector('#tp-chatbot-input-bar');
709
1137
  if (input_bar) input_bar.style.display = 'none';
710
1138
  return;
@@ -726,7 +1154,7 @@
726
1154
  this.hideTyping();
727
1155
 
728
1156
  if (response.status === 429) {
729
- this.addMessage('system', data.error?.message || 'Trop de messages. Veuillez patienter avant de réessayer.');
1157
+ this.addMessage('system', data.error?.message || m.too_many);
730
1158
  this.is_loading = false;
731
1159
  return;
732
1160
  }
@@ -735,23 +1163,29 @@
735
1163
  const source = data.result.source || null;
736
1164
 
737
1165
  const subtitle = this.querySelector('#tp-subtitle');
738
- if (subtitle) subtitle.textContent = this.agent_mode ? '👤 Agent humain' : '🤖 Assistant virtuel';
1166
+ if (subtitle) subtitle.textContent = this.agent_mode ? m.human_agent : m.virtual;
739
1167
 
740
1168
  if (data.result.reply) {
741
1169
  this.addMessage(this.agent_mode ? 'agent' : 'assistant', data.result.reply);
1170
+ this.last_message_count += 2; // fix double message
742
1171
  }
743
1172
 
744
- // Afficher bouton agent si fallback ou ≥ 3 messages user
745
- const user_msg_count = this.messages.filter(m => m.role === 'user').length;
1173
+ const user_msg_count = this.messages.filter(msg => msg.role === 'user').length;
746
1174
  const agent_bar = this.querySelector('#tp-agent-bar');
747
1175
  if (agent_bar && !this.agent_requested && !this.agent_mode) {
748
- if (source === 'fallback' || source === 'clarification' || source === 'no_match' || user_msg_count >= 5) {
1176
+ if (
1177
+ source === 'fallback' ||
1178
+ source === 'clarification' ||
1179
+ source === 'no_match' ||
1180
+ source === 'out_of_scope' ||
1181
+ user_msg_count >= 5
1182
+ ) {
749
1183
  agent_bar.style.display = 'block';
750
1184
  }
751
1185
  }
752
1186
  } catch {
753
1187
  this.hideTyping();
754
- this.addMessage('assistant', 'Une erreur est survenue. Veuillez réessayer.');
1188
+ this.addMessage('assistant', m.error_occurred);
755
1189
  }
756
1190
 
757
1191
  this.is_loading = false;
@@ -770,9 +1204,28 @@
770
1204
  body: JSON.stringify({ user_id: this.user_id }),
771
1205
  });
772
1206
  const data = await response.json();
773
- this.addMessage('system', data.result.message);
1207
+ const m = this.getMessages();
1208
+ const status = data.result?.status;
1209
+
1210
+ let msg = '';
1211
+ if (status === 'outside_hours') {
1212
+ msg = m.outside_hours(data.result.next_opening);
1213
+ } else if (status === 'no_agents') {
1214
+ msg = m.no_agents;
1215
+ } else if (status === 'waiting_agent') {
1216
+ msg = m.waiting_agent;
1217
+ } else if (status === 'waiting_agent_already') {
1218
+ msg = m.waiting_agent_already;
1219
+ } else if (status === 'agent') {
1220
+ msg = m.agent_already;
1221
+ }
1222
+
1223
+ if (msg) {
1224
+ this.addMessage('system', msg);
1225
+ this.last_message_count += 1;
1226
+ }
774
1227
 
775
- if (data.result.status === 'no_agents' || data.result.status === 'outside_hours') {
1228
+ if (status === 'no_agents' || status === 'outside_hours' || status === 'waiting_agent_already') {
776
1229
  this.agent_requested = false;
777
1230
  if (agent_bar) agent_bar.style.display = 'block';
778
1231
  }
@@ -810,6 +1263,7 @@
810
1263
  const server_messages = conv.messages || [];
811
1264
  const server_status = conv.status;
812
1265
  const is_typing = conv.is_typing || false;
1266
+ const m = this.getMessages();
813
1267
 
814
1268
  if (server_status === 'closed' && !this.is_closed) {
815
1269
  this.showClosed();
@@ -850,19 +1304,19 @@
850
1304
  if (server_status === 'agent' && !this.agent_mode) {
851
1305
  this.agent_mode = true;
852
1306
  const subtitle = this.querySelector('#tp-subtitle');
853
- if (subtitle) subtitle.textContent = '👤 Agent humain';
1307
+ if (subtitle) subtitle.textContent = m.human_agent;
854
1308
  const agent_bar = this.querySelector('#tp-agent-bar');
855
1309
  if (agent_bar) agent_bar.style.display = 'none';
856
- this.addMessage('system', 'Un agent a pris en charge votre conversation. Vous pouvez lui écrire directement.');
1310
+ this.addMessage('system', m.agent_taken);
857
1311
  this.last_message_count = server_messages.length;
858
1312
  } else if (server_status === 'bot' && this.agent_mode) {
859
1313
  this.agent_mode = false;
860
1314
  this.agent_requested = false;
861
1315
  const subtitle = this.querySelector('#tp-subtitle');
862
- if (subtitle) subtitle.textContent = '🤖 Assistant virtuel';
1316
+ if (subtitle) subtitle.textContent = m.virtual;
863
1317
  const agent_bar = this.querySelector('#tp-agent-bar');
864
1318
  if (agent_bar) agent_bar.style.display = 'block';
865
- this.addMessage('system', 'Notre assistant virtuel reprend la conversation. Comment puis-je vous aider ?');
1319
+ this.addMessage('system', m.agent_released);
866
1320
  this.last_message_count = server_messages.length;
867
1321
  }
868
1322
  } catch (e) {