@developpement/tp-chatbot-widget 0.0.9 → 0.0.11

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 (3) hide show
  1. package/package.json +1 -1
  2. package/src/chatbot.css +254 -39
  3. package/src/chatbot.js +416 -297
package/src/chatbot.js CHANGED
@@ -1,9 +1,42 @@
1
1
  (function () {
2
2
  'use strict';
3
3
 
4
+ // ─── SVG Icons ──────────────────────────────────────────────────────────────
5
+ // viewBox 0 0 20 20 | stroke-width 1.6 | round caps & joins | fill none | stroke currentColor
6
+
7
+ const ICONS = {
8
+ bell: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M10 2a6 6 0 0 0-6 6c0 3.5-1.5 5-1.5 5h15s-1.5-1.5-1.5-5a6 6 0 0 0-6-6Z"/><path d="M11.73 17a2 2 0 0 1-3.46 0"/></svg>`,
9
+ bell_off: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4 16 16M8.27 3.05A6 6 0 0 1 16 8c0 2.2.5 3.7 1 4.7M6.15 6.17A5.97 5.97 0 0 0 4 8c0 3.5-1.5 5-1.5 5H14M11.73 17a2 2 0 0 1-3.46 0"/></svg>`,
10
+ minimize: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="10" x2="16" y2="10"/></svg>`,
11
+ restore: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="3" width="10" height="10" rx="1.5"/><path d="M3 7v10h10"/></svg>`,
12
+ back: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M13 16 7 10l6-6"/></svg>`,
13
+ close: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5l10 10M15 5 5 15"/></svg>`,
14
+ user: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="10" cy="7" r="3.5"/><path d="M3 17c0-3.3 3.1-6 7-6s7 2.7 7 6"/></svg>`,
15
+ agent: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10a7 7 0 1 1 14 0"/><path d="M2 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1Z"/><path d="M16 10h1a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1Z"/><path d="M16 13v1a3 3 0 0 1-3 3h-2"/></svg>`,
16
+ bot: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="7" width="14" height="10" rx="2"/><path d="M7 11h.01M13 11h.01M7 14.5h6"/><circle cx="10" cy="4" r="1.5"/><line x1="10" y1="5.5" x2="10" y2="7"/></svg>`,
17
+ send: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3 3 9l5 2 2 5 7-13Z"/><path d="m8 11 4-4"/></svg>`,
18
+ attach: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M16.5 10.5 9 18a5 5 0 0 1-7-7l8.5-8.5a3 3 0 0 1 4.24 4.24L6 15.5a1 1 0 0 1-1.42-1.42L12 7"/></svg>`,
19
+ helpful: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10V17M7 10l3-7a3 3 0 0 1 3 3v2h3.5a1 1 0 0 1 1 1.15l-.9 5A1 1 0 0 1 15.6 17H7"/><rect x="3" y="10" width="4" height="7" rx="1"/></svg>`,
20
+ not_helpful: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M13 10V3M13 10l-3 7a3 3 0 0 1-3-3v-2H3.5a1 1 0 0 1-1-1.15l.9-5A1 1 0 0 1 4.4 3H13"/><rect x="13" y="3" width="4" height="7" rx="1"/></svg>`,
21
+ check: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 10l5 5 7-8"/></svg>`,
22
+ msg_plus: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3H6a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h2l2 3 2-3h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2Z"/><path d="M10 7v5M7.5 9.5h5"/></svg>`,
23
+ msg: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3H6a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h2l2 3 2-3h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2Z"/></svg>`,
24
+ };
25
+
26
+ function icon(name, size = 18, extra_style = '') {
27
+ const svg = ICONS[name];
28
+ if (!svg) return '';
29
+ return svg.replace(
30
+ '<svg ',
31
+ `<svg width="${size}" height="${size}" style="display:inline-block;vertical-align:middle;flex-shrink:0;${extra_style}" `
32
+ );
33
+ }
34
+
35
+ // ─── Clients ─────────────────────────────────────────────────────────────────
36
+
4
37
  const CLIENT_THEMES = {
5
38
  flix: {
6
- primary: '#73d700',
39
+ primary: '#97d700',
7
40
  name: 'Flix Corporate',
8
41
  position: 'left',
9
42
  suggestions: [
@@ -83,7 +116,6 @@
83
116
  };
84
117
 
85
118
  const DEFAULT_THEME = { primary: '#7b1fa2', name: 'Support', position: 'right', suggestions: [] };
86
-
87
119
  const WINDOW_WIDTH = '380px';
88
120
  const WINDOW_HEIGHT = '580px';
89
121
 
@@ -111,19 +143,21 @@
111
143
  function playSound() {
112
144
  try {
113
145
  const ctx = new (window.AudioContext || window.webkitAudioContext)();
114
- const oscillator = ctx.createOscillator();
146
+ const osc = ctx.createOscillator();
115
147
  const gain = ctx.createGain();
116
- oscillator.connect(gain);
148
+ osc.connect(gain);
117
149
  gain.connect(ctx.destination);
118
- oscillator.frequency.value = 880;
119
- oscillator.type = 'sine';
150
+ osc.frequency.value = 880;
151
+ osc.type = 'sine';
120
152
  gain.gain.setValueAtTime(0.2, ctx.currentTime);
121
153
  gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4);
122
- oscillator.start(ctx.currentTime);
123
- oscillator.stop(ctx.currentTime + 0.4);
154
+ osc.start(ctx.currentTime);
155
+ osc.stop(ctx.currentTime + 0.4);
124
156
  } catch {}
125
157
  }
126
158
 
159
+ // ─── Component ───────────────────────────────────────────────────────────────
160
+
127
161
  class TpChatbot extends HTMLElement {
128
162
  constructor() {
129
163
  super();
@@ -169,6 +203,32 @@
169
203
  }
170
204
  }
171
205
 
206
+ async fetchTheme() {
207
+ try {
208
+ const response = await fetch(`${this.api_url}/chat/config/theme/${this.client_id}`);
209
+ if (!response.ok) return;
210
+ const data = await response.json();
211
+ const t = data.result?.result;
212
+ console.log('[fetchTheme] open_by_default:', t.open_by_default, typeof t.open_by_default);
213
+ if (!t) return;
214
+ if (CLIENT_THEMES[this.client_id]) {
215
+ if (t.primary) CLIENT_THEMES[this.client_id].primary = t.primary;
216
+ if (t.position) CLIENT_THEMES[this.client_id].position = t.position;
217
+ if (t.name) CLIENT_THEMES[this.client_id].name = t.name;
218
+ if (t.open_by_default !== undefined) CLIENT_THEMES[this.client_id].open_by_default = t.open_by_default;
219
+ } else {
220
+ CLIENT_THEMES[this.client_id] = {
221
+ primary: t.primary || DEFAULT_THEME.primary,
222
+ position: t.position || DEFAULT_THEME.position,
223
+ name: t.name || DEFAULT_THEME.name,
224
+ suggestions: [],
225
+ };
226
+ }
227
+ } catch (e) {
228
+ console.warn('[tp-chatbot] fetchTheme failed, using defaults:', e.message);
229
+ }
230
+ }
231
+
172
232
  connectedCallback() {
173
233
  this.client_id = this.getAttribute('client-id') || 'flix';
174
234
  this.access_token = this.getAttribute('access-token') || '';
@@ -183,224 +243,269 @@
183
243
  this.is_fullscreen = false;
184
244
  this.is_minimized = false;
185
245
 
186
- this.render();
187
- this.applyTheme();
188
- this.attachEvents();
189
- this._initialized = true;
246
+ this.fetchTheme().then(() => {
247
+ this.render();
248
+ this.applyTheme();
249
+ this.attachEvents();
250
+ this._initialized = true;
190
251
 
191
- if (this.access_token) {
192
- this.user_info = extractUserInfo(this.access_token);
193
- this.user_id = this.user_info?.user_id;
194
- this.showConversationList();
195
- } else {
196
- setTimeout(() => {
197
- const token = this.getAttribute('access-token');
198
- if (token) {
199
- this.access_token = token;
200
- this.user_info = extractUserInfo(token);
201
- this.user_id = this.user_info?.user_id;
202
- this.showConversationList();
203
- } else {
204
- this.showGuestForm();
205
- }
206
- }, 500);
207
- }
252
+ const theme = getClientTheme(this.client_id);
253
+ console.log('[widget] theme after fetch:', theme);
254
+
255
+ if (theme.open_by_default) {
256
+ setTimeout(() => this.toggleChat(), 300);
257
+ }
258
+
259
+ if (this.access_token) {
260
+ this.user_info = extractUserInfo(this.access_token);
261
+ this.user_id = this.user_info?.user_id;
262
+ this.showConversationList();
263
+ } else {
264
+ setTimeout(() => {
265
+ const token = this.getAttribute('access-token');
266
+ if (token) {
267
+ this.access_token = token;
268
+ this.user_info = extractUserInfo(token);
269
+ this.user_id = this.user_info?.user_id;
270
+ this.showConversationList();
271
+ } else {
272
+ this.showGuestForm();
273
+ }
274
+ }, 500);
275
+ }
276
+ });
208
277
  }
209
278
 
210
279
  disconnectedCallback() {
211
280
  if (this.polling_interval) clearInterval(this.polling_interval);
212
281
  }
213
282
 
214
- // ─── i18n ─────────────────────────────────────────────────────────────────
283
+ // ─── i18n ──────────────────────────────────────────────────────────────────
215
284
 
216
285
  getMessages() {
217
286
  const lang = (this.user_info?.language || 'EN').toUpperCase();
218
287
  const msgs = {
219
288
  FR: {
220
- closed: 'Conversation clôturée',
289
+ closed: 'Conversation clôturée',
221
290
  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',
291
+ csat_positive: 'Merci pour votre retour !',
292
+ csat_negative: 'Merci, nous allons nous améliorer.',
293
+ new_conv: 'Nouvelle conversation',
294
+ back_to_list: 'Voir toutes les conversations',
226
295
  agent_taken: 'Un agent a pris en charge votre conversation. Vous pouvez lui écrire directement.',
227
296
  agent_released: 'Notre assistant virtuel reprend la conversation. Comment puis-je vous aider ?',
228
297
  close_confirm: 'Voulez-vous terminer cette conversation ?',
229
- terminated: 'Conversation terminée',
230
- virtual: '🤖 Assistant virtuel',
231
- human_agent: '👤 Agent humain',
298
+ terminated: 'Conversation terminée',
299
+ virtual: 'Assistant virtuel',
300
+ human_agent: 'Agent humain',
232
301
  loading: 'Chargement...',
233
302
  no_conv: 'Aucune conversation pour le moment.',
234
- new_conv_btn: '💬 Nouvelle conversation',
303
+ new_conv_btn: 'Nouvelle conversation',
235
304
  error_load: 'Erreur de chargement.',
236
305
  recent_conv: 'Vos conversations récentes',
237
306
  hello: 'Bonjour',
238
- close_btn: 'Terminer la conversation',
239
- speak_agent: '👤 Parler à un agent',
307
+ close_btn: 'Terminer la conversation',
308
+ speak_agent: 'Parler à un agent',
240
309
  write_msg: 'Écrivez votre message...',
241
310
  limit_reached: 'Vous avez atteint la limite de 30 messages. Veuillez contacter un agent ou écrire à support@travelplanet.com.',
242
311
  too_many: 'Trop de messages. Veuillez patienter avant de réessayer.',
243
312
  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.',
313
+ outside_hours: next => `Notre support est actuellement fermé. Prochain créneau : ${next}.`,
314
+ no_agents: 'Aucun agent disponible pour le moment. Veuillez réessayer plus tard.',
315
+ waiting_agent: 'Votre demande a été transmise. Un agent va prendre en charge votre conversation.',
316
+ waiting_agent_already: "Vous êtes déjà en file d'attente. Merci de patienter.",
317
+ agent_already: 'Un agent gère déjà votre conversation.',
318
+ suggest_agent_escalation: "Je n'arrive pas à trouver une réponse adaptée. Souhaitez-vous parler à un agent ?",
319
+ status_closed: 'Fermé',
320
+ status_agent: 'Agent',
321
+ status_waiting: 'En attente',
322
+ status_bot: 'Bot',
323
+ messages: 'message',
249
324
  },
250
325
  EN: {
251
- closed: 'Conversation closed',
326
+ closed: 'Conversation closed',
252
327
  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',
328
+ csat_positive: 'Thank you for your feedback!',
329
+ csat_negative: 'Thank you, we will improve.',
330
+ new_conv: 'New conversation',
331
+ back_to_list: 'Back to conversations',
257
332
  agent_taken: 'An agent has taken over your conversation. You can now write to them directly.',
258
333
  agent_released: 'Our virtual assistant is back. How can I help you?',
259
334
  close_confirm: 'Do you want to end this conversation?',
260
- terminated: 'Conversation ended',
261
- virtual: '🤖 Virtual assistant',
262
- human_agent: '👤 Human agent',
335
+ terminated: 'Conversation ended',
336
+ virtual: 'Virtual assistant',
337
+ human_agent: 'Human agent',
263
338
  loading: 'Loading...',
264
339
  no_conv: 'No conversations yet.',
265
- new_conv_btn: '💬 New conversation',
340
+ new_conv_btn: 'New conversation',
266
341
  error_load: 'Loading error.',
267
342
  recent_conv: 'Your recent conversations',
268
343
  hello: 'Hello',
269
- close_btn: 'End conversation',
270
- speak_agent: '👤 Talk to an agent',
344
+ close_btn: 'End conversation',
345
+ speak_agent: 'Talk to an agent',
271
346
  write_msg: 'Write your message...',
272
347
  limit_reached: 'You have reached the 30 message limit. Please contact an agent or write to support@travelplanet.com.',
273
348
  too_many: 'Too many messages. Please wait before retrying.',
274
349
  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.',
350
+ outside_hours: next => `Our support is currently closed. Next available slot: ${next}.`,
351
+ no_agents: 'No agent available at the moment. Please try again later.',
352
+ waiting_agent: 'Your request has been sent. An agent will take over shortly.',
353
+ waiting_agent_already: 'You are already in the queue. Please wait.',
354
+ agent_already: 'An agent is already handling your conversation.',
355
+ suggest_agent_escalation: "I'm having trouble finding a suitable answer. Would you like to speak to an agent?",
356
+ status_closed: 'Closed',
357
+ status_agent: 'Agent',
358
+ status_waiting: 'Waiting',
359
+ status_bot: 'Bot',
360
+ messages: 'message',
280
361
  },
281
362
  DE: {
282
- closed: 'Gespräch beendet',
363
+ closed: 'Gespräch beendet',
283
364
  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',
365
+ csat_positive: 'Vielen Dank für Ihr Feedback!',
366
+ csat_negative: 'Danke, wir werden uns verbessern.',
367
+ new_conv: 'Neues Gespräch',
368
+ back_to_list: 'Zurück zur Liste',
288
369
  agent_taken: 'Ein Agent hat Ihr Gespräch übernommen. Sie können ihm jetzt direkt schreiben.',
289
370
  agent_released: 'Unser virtueller Assistent ist zurück. Wie kann ich Ihnen helfen?',
290
371
  close_confirm: 'Möchten Sie das Gespräch beenden?',
291
- terminated: 'Gespräch beendet',
292
- virtual: '🤖 Virtueller Assistent',
293
- human_agent: '👤 Menschlicher Agent',
372
+ terminated: 'Gespräch beendet',
373
+ virtual: 'Virtueller Assistent',
374
+ human_agent: 'Menschlicher Agent',
294
375
  loading: 'Laden...',
295
376
  no_conv: 'Noch keine Gespräche.',
296
- new_conv_btn: '💬 Neues Gespräch',
377
+ new_conv_btn: 'Neues Gespräch',
297
378
  error_load: 'Ladefehler.',
298
379
  recent_conv: 'Ihre letzten Gespräche',
299
380
  hello: 'Hallo',
300
- close_btn: 'Gespräch beenden',
301
- speak_agent: '👤 Mit einem Agenten sprechen',
381
+ close_btn: 'Gespräch beenden',
382
+ speak_agent: 'Mit einem Agenten sprechen',
302
383
  write_msg: 'Schreiben Sie Ihre Nachricht...',
303
384
  limit_reached: 'Sie haben das Limit von 30 Nachrichten erreicht.',
304
385
  too_many: 'Zu viele Nachrichten. Bitte warten Sie.',
305
386
  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.',
387
+ outside_hours: next => `Unser Support ist derzeit geschlossen. Nächster Termin: ${next}.`,
388
+ no_agents: 'Derzeit kein Agent verfügbar. Bitte versuchen Sie es später.',
389
+ waiting_agent: 'Ihre Anfrage wurde übermittelt. Ein Agent übernimmt gleich.',
390
+ waiting_agent_already: 'Sie sind bereits in der Warteschlange. Bitte warten.',
391
+ agent_already: 'Ein Agent betreut bereits Ihr Gespräch.',
392
+ suggest_agent_escalation: 'Ich kann keine passende Antwort finden. Möchten Sie mit einem Agenten sprechen?',
393
+ status_closed: 'Geschlossen',
394
+ status_agent: 'Agent',
395
+ status_waiting: 'Wartend',
396
+ status_bot: 'Bot',
397
+ messages: 'Nachricht',
311
398
  },
312
399
  ES: {
313
- closed: 'Conversación cerrada',
400
+ closed: 'Conversación cerrada',
314
401
  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',
402
+ csat_positive: '¡Gracias por su opinión!',
403
+ csat_negative: 'Gracias, mejoraremos.',
404
+ new_conv: 'Nueva conversación',
405
+ back_to_list: 'Volver a las conversaciones',
319
406
  agent_taken: 'Un agente ha tomado su conversación. Ahora puede escribirle directamente.',
320
407
  agent_released: 'Nuestro asistente virtual ha vuelto. ¿En qué puedo ayudarle?',
321
408
  close_confirm: '¿Desea finalizar esta conversación?',
322
- terminated: 'Conversación terminada',
323
- virtual: '🤖 Asistente virtual',
324
- human_agent: '👤 Agente humano',
409
+ terminated: 'Conversación terminada',
410
+ virtual: 'Asistente virtual',
411
+ human_agent: 'Agente humano',
325
412
  loading: 'Cargando...',
326
413
  no_conv: 'No hay conversaciones por el momento.',
327
- new_conv_btn: '💬 Nueva conversación',
414
+ new_conv_btn: 'Nueva conversación',
328
415
  error_load: 'Error de carga.',
329
416
  recent_conv: 'Sus conversaciones recientes',
330
417
  hello: 'Hola',
331
- close_btn: 'Finalizar conversación',
332
- speak_agent: '👤 Hablar con un agente',
418
+ close_btn: 'Finalizar conversación',
419
+ speak_agent: 'Hablar con un agente',
333
420
  write_msg: 'Escriba su mensaje...',
334
421
  limit_reached: 'Ha alcanzado el límite de 30 mensajes.',
335
422
  too_many: 'Demasiados mensajes. Por favor espere.',
336
423
  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.',
424
+ outside_hours: next => `Nuestro soporte está cerrado. Próximo horario: ${next}.`,
425
+ no_agents: 'No hay agentes disponibles. Inténtelo más tarde.',
426
+ waiting_agent: 'Su solicitud ha sido enviada. Un agente le atenderá pronto.',
427
+ waiting_agent_already: 'Ya está en la cola de espera. Por favor, espere.',
428
+ agent_already: 'Un agente ya está gestionando su conversación.',
429
+ suggest_agent_escalation: 'No encuentro una respuesta adecuada. ¿Desea hablar con un agente?',
430
+ status_closed: 'Cerrado',
431
+ status_agent: 'Agente',
432
+ status_waiting: 'Esperando',
433
+ status_bot: 'Bot',
434
+ messages: 'mensaje',
342
435
  },
343
436
  IT: {
344
- closed: 'Conversazione chiusa',
437
+ closed: 'Conversazione chiusa',
345
438
  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',
439
+ csat_positive: 'Grazie per il suo feedback!',
440
+ csat_negative: 'Grazie, miglioreremo.',
441
+ new_conv: 'Nuova conversazione',
442
+ back_to_list: 'Torna alle conversazioni',
350
443
  agent_taken: 'Un agente ha preso in carico la sua conversazione.',
351
444
  agent_released: 'Il nostro assistente virtuale è tornato. Come posso aiutarla?',
352
445
  close_confirm: 'Vuole terminare questa conversazione?',
353
- terminated: 'Conversazione terminata',
354
- virtual: '🤖 Assistente virtuale',
355
- human_agent: '👤 Agente umano',
446
+ terminated: 'Conversazione terminata',
447
+ virtual: 'Assistente virtuale',
448
+ human_agent: 'Agente umano',
356
449
  loading: 'Caricamento...',
357
450
  no_conv: 'Nessuna conversazione per il momento.',
358
- new_conv_btn: '💬 Nuova conversazione',
451
+ new_conv_btn: 'Nuova conversazione',
359
452
  error_load: 'Errore di caricamento.',
360
453
  recent_conv: 'Le sue conversazioni recenti',
361
454
  hello: 'Ciao',
362
- close_btn: 'Termina conversazione',
363
- speak_agent: '👤 Parla con un agente',
455
+ close_btn: 'Termina conversazione',
456
+ speak_agent: 'Parla con un agente',
364
457
  write_msg: 'Scrivi il tuo messaggio...',
365
458
  limit_reached: 'Hai raggiunto il limite di 30 messaggi.',
366
459
  too_many: 'Troppi messaggi. Attendere prego.',
367
460
  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.',
461
+ outside_hours: next => `Il supporto è chiuso. Prossima disponibilità: ${next}.`,
462
+ no_agents: 'Nessun agente disponibile. Riprova più tardi.',
463
+ waiting_agent: 'Richiesta inviata. Un agente prenderà in carico la conversazione.',
464
+ waiting_agent_already: 'È già in coda. Attenda.',
465
+ agent_already: 'Un agente sta già gestendo la conversazione.',
466
+ suggest_agent_escalation: 'Non riesco a trovare una risposta adeguata. Desidera parlare con un agente?',
467
+ status_closed: 'Chiuso',
468
+ status_agent: 'Agente',
469
+ status_waiting: 'Attesa',
470
+ status_bot: 'Bot',
471
+ messages: 'messaggio',
373
472
  },
374
473
  NL: {
375
- closed: 'Gesprek gesloten',
474
+ closed: 'Gesprek gesloten',
376
475
  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',
476
+ csat_positive: 'Bedankt voor uw feedback!',
477
+ csat_negative: 'Bedankt, we zullen verbeteren.',
478
+ new_conv: 'Nieuw gesprek',
479
+ back_to_list: 'Terug naar gesprekken',
381
480
  agent_taken: 'Een agent heeft uw gesprek overgenomen.',
382
481
  agent_released: 'Onze virtuele assistent is terug. Hoe kan ik u helpen?',
383
482
  close_confirm: 'Wilt u dit gesprek beëindigen?',
384
- terminated: 'Gesprek beëindigd',
385
- virtual: '🤖 Virtuele assistent',
386
- human_agent: '👤 Menselijke agent',
483
+ terminated: 'Gesprek beëindigd',
484
+ virtual: 'Virtuele assistent',
485
+ human_agent: 'Menselijke agent',
387
486
  loading: 'Laden...',
388
487
  no_conv: 'Geen gesprekken op dit moment.',
389
- new_conv_btn: '💬 Nieuw gesprek',
390
- error_load: 'Laadфout.',
488
+ new_conv_btn: 'Nieuw gesprek',
489
+ error_load: 'Laadout.',
391
490
  recent_conv: 'Uw recente gesprekken',
392
491
  hello: 'Hallo',
393
- close_btn: 'Gesprek beëindigen',
394
- speak_agent: '👤 Praat met een agent',
492
+ close_btn: 'Gesprek beëindigen',
493
+ speak_agent: 'Praat met een agent',
395
494
  write_msg: 'Schrijf uw bericht...',
396
495
  limit_reached: 'U heeft de limiet van 30 berichten bereikt.',
397
496
  too_many: 'Te veel berichten. Wacht even.',
398
497
  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.',
498
+ outside_hours: next => `Onze support is gesloten. Volgende beschikbare tijd: ${next}.`,
499
+ no_agents: 'Geen agent beschikbaar. Probeer het later opnieuw.',
500
+ waiting_agent: 'Uw verzoek is verzonden. Een agent neemt het gesprek over.',
501
+ waiting_agent_already: 'U staat al in de wachtrij. Even geduld.',
502
+ agent_already: 'Een agent beheert uw gesprek al.',
503
+ suggest_agent_escalation: 'Ik kan geen passend antwoord vinden. Wilt u met een agent spreken?',
504
+ status_closed: 'Gesloten',
505
+ status_agent: 'Agent',
506
+ status_waiting: 'Wachten',
507
+ status_bot: 'Bot',
508
+ messages: 'bericht',
404
509
  },
405
510
  };
406
511
  return msgs[lang] || msgs['EN'];
@@ -410,17 +515,17 @@
410
515
  const lang = (this.user_info?.language || 'EN').toUpperCase();
411
516
  const client_name = getClientTheme(this.client_id).name;
412
517
  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?`,
518
+ FR: `Bonjour ${first_name} ! Je m'appelle Maria, votre assistante virtuelle ${client_name}. Comment puis-je vous aider aujourd'hui ?`,
519
+ EN: `Hello ${first_name}! My name is Maria, your ${client_name} virtual assistant. How can I help you today?`,
520
+ DE: `Hallo ${first_name}! Ich bin Maria, Ihre virtuelle Assistentin von ${client_name}. Wie kann ich Ihnen helfen?`,
521
+ ES: `¡Hola ${first_name}! Me llamo Maria, tu asistente virtual de ${client_name}. ¿En qué puedo ayudarte?`,
522
+ IT: `Ciao ${first_name}! Mi chiamo Maria, la tua assistente virtuale di ${client_name}. Come posso aiutarti?`,
523
+ NL: `Hallo ${first_name}! Ik ben Maria, uw virtuele assistent van ${client_name}. Hoe kan ik u helpen?`,
419
524
  };
420
525
  return msgs[lang] || msgs['EN'];
421
526
  }
422
527
 
423
- // ─── Theme ────────────────────────────────────────────────────────────────
528
+ // ─── Theme ─────────────────────────────────────────────────────────────────
424
529
 
425
530
  getThemeColor() {
426
531
  return getClientTheme(this.client_id).primary;
@@ -456,6 +561,9 @@
456
561
  #tp-new-conversation { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
457
562
  .tp-conv-item:hover { background: ${color}11 !important; }
458
563
  .tp-conv-new-btn { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
564
+ .tp-badge-closed { background: ${color}22 !important; color: ${dark} !important; }
565
+ .tp-attach-btn { color: ${color} !important; }
566
+ .tp-attach-btn:hover { background: ${color}18 !important; }
459
567
  `;
460
568
  document.head.appendChild(style);
461
569
 
@@ -466,13 +574,11 @@
466
574
  host.style.left = position === 'left' ? '24px' : 'auto';
467
575
  host.style.right = position === 'left' ? 'auto' : '24px';
468
576
  }
469
-
470
577
  const bubble = this.querySelector('.tp-chatbot-bubble');
471
578
  if (bubble) {
472
579
  bubble.style.marginLeft = position === 'left' ? '0' : 'auto';
473
580
  bubble.style.marginRight = position === 'left' ? 'auto' : '0';
474
581
  }
475
-
476
582
  const win = this.querySelector('.tp-chatbot-window');
477
583
  if (win) {
478
584
  win.style.left = '0';
@@ -481,7 +587,7 @@
481
587
  }
482
588
  }
483
589
 
484
- // ─── Render ───────────────────────────────────────────────────────────────
590
+ // ─── Render ────────────────────────────────────────────────────────────────
485
591
 
486
592
  render() {
487
593
  const theme = getClientTheme(this.client_id);
@@ -490,39 +596,40 @@
490
596
  <div class="tp-chatbot-window" id="tp-window">
491
597
  <div class="tp-chatbot-header" id="tp-header">
492
598
  <div class="tp-chatbot-avatar">M</div>
493
- <div style="flex:1">
599
+ <div style="flex:1;min-width:0;">
494
600
  <div class="tp-chatbot-title">${theme.name} Support</div>
495
- <div class="tp-chatbot-subtitle" id="tp-subtitle">🤖 Assistant</div>
601
+ <div class="tp-chatbot-subtitle" id="tp-subtitle">${icon('bot', 12, 'margin-right:4px')} Assistant</div>
496
602
  </div>
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>
603
+ <button id="tp-sound-btn" class="tp-header-btn" title="Sound">${icon('bell', 16)}</button>
604
+ <button id="tp-minimize-btn" class="tp-header-btn" title="Minimize">${icon('minimize', 16)}</button>
605
+ <button id="tp-maximize-btn" class="tp-header-btn" title="Fullscreen">${icon('restore', 16)}</button>
606
+ <button id="tp-back-btn" class="tp-header-btn" style="display:none;" title="Back">${icon('back', 16)}</button>
501
607
  </div>
502
608
  <div class="tp-chatbot-messages" id="tp-messages"></div>
503
609
  <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>
610
+ <button class="tp-chatbot-agent-btn" id="tp-agent-btn">
611
+ <span style="display:inline-flex;align-items:center;gap:6px;">${icon('agent', 16)} Talk to an agent</span>
612
+ </button>
505
613
  </div>
506
614
  <div class="tp-chatbot-input-bar" id="tp-chatbot-input-bar" style="display:none;">
507
615
  <textarea class="tp-chatbot-input" id="tp-input" placeholder="Write your message..." rows="1"></textarea>
508
- <button class="tp-chatbot-send" id="tp-send">➤</button>
616
+ <button class="tp-chatbot-send" id="tp-send">${icon('send', 16, 'color:white')}</button>
509
617
  </div>
510
618
  <div id="tp-close-bar" style="padding:8px 12px;border-top:1px solid #ede8f5;text-align:center;display:none;">
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;">
512
- End conversation
619
+ <button id="tp-close-btn" class="tp-close-btn">
620
+ <span style="display:inline-flex;align-items:center;gap:6px;">${icon('check', 14)} End conversation</span>
513
621
  </button>
514
622
  </div>
515
623
  </div>
516
- <div class="tp-chatbot-bubble" id="tp-bubble">💬</div>
624
+ <div class="tp-chatbot-bubble" id="tp-bubble">${icon('msg', 24, 'color:white')}</div>
517
625
  </div>
518
626
  `;
519
627
  }
520
628
 
521
- // ─── Window controls ──────────────────────────────────────────────────────
629
+ // ─── Window controls ───────────────────────────────────────────────────────
522
630
 
523
631
  minimize() {
524
632
  const win = this.querySelector('#tp-window');
525
- // Exit fullscreen first if needed
526
633
  if (this.is_fullscreen) this.exitFullscreen(false);
527
634
  this.is_minimized = true;
528
635
  win.style.height = '56px';
@@ -542,7 +649,8 @@
542
649
  win.style.borderRadius = '0';
543
650
  win.style.zIndex = '99999';
544
651
  host.style.width = '100vw';
545
- this.querySelector('#tp-maximize-btn').textContent = '⤡';
652
+ const btn = this.querySelector('#tp-maximize-btn');
653
+ if (btn) btn.innerHTML = icon('restore', 16);
546
654
  }
547
655
 
548
656
  exitFullscreen(restore_height = true) {
@@ -561,10 +669,11 @@
561
669
  host.style.right = position === 'left' ? 'auto' : '24px';
562
670
  if (restore_height) win.style.height = WINDOW_HEIGHT;
563
671
  win.style.overflow = 'hidden';
564
- this.querySelector('#tp-maximize-btn').textContent = '□';
672
+ const btn = this.querySelector('#tp-maximize-btn');
673
+ if (btn) btn.innerHTML = icon('restore', 16);
565
674
  }
566
675
 
567
- // ─── Events ───────────────────────────────────────────────────────────────
676
+ // ─── Events ────────────────────────────────────────────────────────────────
568
677
 
569
678
  attachEvents() {
570
679
  this.querySelector('#tp-bubble').addEventListener('click', () => this.toggleChat());
@@ -581,18 +690,15 @@
581
690
 
582
691
  this.querySelector('#tp-sound-btn').addEventListener('click', () => {
583
692
  this.sound_enabled = !this.sound_enabled;
584
- this.querySelector('#tp-sound-btn').textContent = this.sound_enabled ? '🔔' : '🔕';
693
+ this.querySelector('#tp-sound-btn').innerHTML = icon(this.sound_enabled ? 'bell' : 'bell_off', 16);
585
694
  });
586
695
 
587
696
  this.querySelector('#tp-close-btn').addEventListener('click', () => {
588
- if (window.confirm(this.getMessages().close_confirm)) {
589
- this.closeByUser();
590
- }
697
+ if (window.confirm(this.getMessages().close_confirm)) this.closeByUser();
591
698
  });
592
699
 
593
700
  this.querySelector('#tp-minimize-btn').addEventListener('click', () => {
594
701
  if (this.is_minimized) {
595
- // Restore
596
702
  this.is_minimized = false;
597
703
  const win = this.querySelector('#tp-window');
598
704
  win.style.height = WINDOW_HEIGHT;
@@ -620,13 +726,15 @@
620
726
  updateUILanguage() {
621
727
  const m = this.getMessages();
622
728
  const agent_btn = this.querySelector('#tp-agent-btn');
623
- if (agent_btn) agent_btn.textContent = m.speak_agent;
729
+ if (agent_btn)
730
+ agent_btn.innerHTML = `<span style="display:inline-flex;align-items:center;gap:6px;">${icon('agent', 16)} ${m.speak_agent}</span>`;
624
731
  const close_btn = this.querySelector('#tp-close-btn');
625
- if (close_btn) close_btn.textContent = m.close_btn;
732
+ if (close_btn)
733
+ close_btn.innerHTML = `<span style="display:inline-flex;align-items:center;gap:6px;">${icon('check', 14)} ${m.close_btn}</span>`;
626
734
  const input = this.querySelector('#tp-input');
627
735
  if (input) input.placeholder = m.write_msg;
628
736
  const subtitle = this.querySelector('#tp-subtitle');
629
- if (subtitle) subtitle.textContent = m.virtual;
737
+ if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
630
738
  }
631
739
 
632
740
  toggleChat() {
@@ -635,8 +743,7 @@
635
743
  const bubble = this.querySelector('#tp-bubble');
636
744
  if (this.is_open) {
637
745
  window_el.classList.add('open');
638
- bubble.textContent = '';
639
- // Restore from minimized if needed
746
+ bubble.innerHTML = icon('close', 20, 'color:white');
640
747
  if (this.is_minimized) {
641
748
  this.is_minimized = false;
642
749
  window_el.style.height = WINDOW_HEIGHT;
@@ -644,7 +751,7 @@
644
751
  }
645
752
  } else {
646
753
  window_el.classList.remove('open');
647
- bubble.textContent = '💬';
754
+ bubble.innerHTML = icon('msg', 24, 'color:white');
648
755
  if (this.is_fullscreen) this.exitFullscreen(true);
649
756
  }
650
757
  }
@@ -660,11 +767,7 @@
660
767
  const response = await fetch(`${this.api_url}/chat/conversations`, {
661
768
  method: 'POST',
662
769
  headers: this.getHeaders(true),
663
- body: JSON.stringify({
664
- user_id: this.user_id,
665
- user_info: this.user_info,
666
- client_id: this.client_id,
667
- }),
770
+ body: JSON.stringify({ user_id: this.user_id, user_info: this.user_info, client_id: this.client_id }),
668
771
  });
669
772
  const data = await response.json();
670
773
  return data.result?.conversation_id;
@@ -676,15 +779,12 @@
676
779
  const existing = this.querySelector('#tp-suggestions');
677
780
  if (existing) existing.remove();
678
781
  if (!suggestions || suggestions.length === 0) return;
679
-
680
782
  const lang = (this.user_info?.language || 'EN').toUpperCase();
681
783
  const color = this.getThemeColor();
682
784
  const container = this.querySelector('#tp-messages');
683
-
684
785
  const wrap = document.createElement('div');
685
786
  wrap.id = 'tp-suggestions';
686
787
  wrap.style.cssText = 'padding:8px 12px 12px;display:flex;flex-direction:column;gap:6px;';
687
-
688
788
  suggestions.forEach(s => {
689
789
  const label = s[lang] || s['EN'];
690
790
  const btn = document.createElement('button');
@@ -709,38 +809,29 @@
709
809
  });
710
810
  wrap.appendChild(btn);
711
811
  });
712
-
713
812
  container.appendChild(wrap);
714
813
  container.scrollTop = container.scrollHeight;
715
814
  }
716
815
 
717
- // ─── Conversation List ────────────────────────────────────────────────────
816
+ // ─── Conversation List ─────────────────────────────────────────────────────
718
817
 
719
818
  async showConversationList() {
720
819
  this.view = 'list';
721
820
  const m = this.getMessages();
722
-
723
821
  if (this.polling_interval) {
724
822
  clearInterval(this.polling_interval);
725
823
  this.polling_interval = null;
726
824
  }
727
825
 
728
826
  const subtitle = this.querySelector('#tp-subtitle');
729
- if (subtitle) subtitle.textContent = m.virtual;
730
-
731
- const back_btn = this.querySelector('#tp-back-btn');
732
- if (back_btn) back_btn.style.display = 'none';
733
-
734
- const input_bar = this.querySelector('#tp-chatbot-input-bar');
735
- if (input_bar) input_bar.style.display = 'none';
736
-
737
- const agent_bar = this.querySelector('#tp-agent-bar');
738
- if (agent_bar) agent_bar.style.display = 'none';
739
-
740
- const close_bar = this.querySelector('#tp-close-bar');
741
- if (close_bar) close_bar.style.display = 'none';
827
+ if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
828
+ this.querySelector('#tp-back-btn').style.display = 'none';
829
+ this.querySelector('#tp-chatbot-input-bar').style.display = 'none';
830
+ this.querySelector('#tp-agent-bar').style.display = 'none';
831
+ this.querySelector('#tp-close-bar').style.display = 'none';
742
832
 
743
833
  const container = this.querySelector('#tp-messages');
834
+ container.style.background = '#f5f5f7';
744
835
  container.innerHTML = `<div style="padding:16px;text-align:center;color:#aaa;font-size:12px;">${m.loading}</div>`;
745
836
 
746
837
  try {
@@ -751,35 +842,40 @@
751
842
  const color = this.getThemeColor();
752
843
 
753
844
  container.innerHTML = '';
845
+ container.style.background = 'white';
846
+ container.style.padding = '0';
754
847
 
848
+ // Header greeting
755
849
  const header_el = document.createElement('div');
756
- header_el.style.cssText = 'padding:16px;border-bottom:1px solid #ede8f5;';
850
+ header_el.style.cssText = 'padding:16px;border-bottom:1px solid #f0f0f0;';
757
851
  header_el.innerHTML = `
758
- <div style="font-size:13px;font-weight:700;color:#1a1a2e;margin-bottom:4px;">${m.hello} ${this.user_info?.first_name || ''} 👋</div>
852
+ <div style="font-size:14px;font-weight:700;color:#1a1a2e;margin-bottom:2px;">${m.hello} ${this.user_info?.first_name || ''} 👋</div>
759
853
  <div style="font-size:12px;color:#aaa;">${m.recent_conv}</div>
760
854
  `;
761
855
  container.appendChild(header_el);
762
856
 
763
857
  if (conversations.length === 0) {
764
858
  const empty = document.createElement('div');
765
- empty.style.cssText = 'padding:24px 16px;text-align:center;color:#aaa;font-size:13px;';
766
- empty.textContent = m.no_conv;
859
+ empty.style.cssText = 'padding:32px 16px;text-align:center;color:#bbb;font-size:13px;';
860
+ empty.innerHTML = `<div style="margin-bottom:8px;opacity:0.4;">${icon('msg', 32)}</div>${m.no_conv}`;
767
861
  container.appendChild(empty);
768
862
  } else {
769
863
  conversations.forEach(conv => {
770
864
  const item = document.createElement('div');
771
865
  item.className = 'tp-conv-item';
772
- item.style.cssText = 'padding:14px 16px;border-bottom:1px solid #f5f0fa;cursor:pointer;transition:background 0.15s;';
866
+ item.style.cssText =
867
+ 'padding:14px 16px;border-bottom:1px solid #f5f5f5;cursor:pointer;transition:background 0.15s;display:flex;justify-content:space-between;align-items:center;';
773
868
 
774
869
  const is_closed = conv.status === 'closed';
775
870
  const status_label = is_closed
776
- ? '✅ Closed'
871
+ ? m.status_closed
777
872
  : conv.status === 'agent'
778
- ? '👤 Agent'
873
+ ? m.status_agent
779
874
  : conv.status === 'waiting_agent'
780
- ? '⏳ Waiting'
781
- : '🤖 Bot';
782
- const status_color = is_closed ? '#9ca3af' : conv.status === 'waiting_agent' ? '#f59e0b' : color;
875
+ ? m.status_waiting
876
+ : m.status_bot;
877
+
878
+ const badge_color = is_closed ? color : conv.status === 'waiting_agent' ? '#f59e0b' : color;
783
879
  const date = new Date(conv.updated_at).toLocaleDateString('fr-FR', {
784
880
  day: '2-digit',
785
881
  month: '2-digit',
@@ -788,20 +884,27 @@
788
884
  });
789
885
 
790
886
  item.innerHTML = `
791
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
792
- <span style="font-size:13px;font-weight:600;color:${is_closed ? '#aaa' : '#1a1a2e'};">${date}</span>
793
- <span style="font-size:10px;font-weight:700;color:${status_color};padding:2px 8px;border-radius:10px;background:${status_color}18;">${status_label}</span>
887
+ <div>
888
+ <div style="font-size:13px;font-weight:600;color:${is_closed ? '#888' : '#1a1a2e'};margin-bottom:3px;">${date}</div>
889
+ <div style="font-size:11px;color:#bbb;">${conv.message_count} ${m.messages}${conv.message_count > 1 ? 's' : ''}</div>
794
890
  </div>
795
- <div style="font-size:11px;color:#aaa;">${conv.message_count} message${conv.message_count > 1 ? 's' : ''}</div>
891
+ <span class="tp-badge-closed" style="display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;background:${badge_color}18;color:${badge_color};">
892
+ ${is_closed ? icon('check', 11) : ''} ${status_label}
893
+ </span>
796
894
  `;
797
895
  item.addEventListener('click', () => this.openConversation(conv.conversation_id, is_closed));
798
896
  container.appendChild(item);
799
897
  });
800
898
  }
801
899
 
900
+ // New conversation button
802
901
  const new_btn_wrap = document.createElement('div');
803
902
  new_btn_wrap.style.cssText = 'padding:16px;';
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>`;
903
+ new_btn_wrap.innerHTML = `
904
+ <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;display:flex;align-items:center;justify-content:center;gap:8px;">
905
+ ${icon('msg_plus', 16, 'color:white')} ${m.new_conv_btn}
906
+ </button>
907
+ `;
805
908
  new_btn_wrap.querySelector('button').addEventListener('click', () => this.startNewConversation());
806
909
  container.appendChild(new_btn_wrap);
807
910
  } catch (e) {
@@ -820,10 +923,10 @@
820
923
  this.agent_requested = false;
821
924
 
822
925
  const m = this.getMessages();
823
- const back_btn = this.querySelector('#tp-back-btn');
824
- if (back_btn) back_btn.style.display = 'block';
825
-
926
+ this.querySelector('#tp-back-btn').style.display = 'block';
826
927
  const container = this.querySelector('#tp-messages');
928
+ container.style.background = '#f5f5f7';
929
+ container.style.padding = '16px';
827
930
  container.innerHTML = '';
828
931
 
829
932
  const input_bar = this.querySelector('#tp-chatbot-input-bar');
@@ -839,10 +942,9 @@
839
942
 
840
943
  conv.messages.forEach(msg => {
841
944
  if (['user', 'assistant', 'agent', 'system'].includes(msg.role)) {
842
- this.addMessage(msg.role, msg.content);
945
+ this.addMessage(msg.role, msg.content, msg.message_id || null);
843
946
  }
844
947
  });
845
-
846
948
  this.last_message_count = conv.messages.length;
847
949
 
848
950
  if (conv.status === 'closed') {
@@ -859,14 +961,14 @@
859
961
  if (conv.status === 'agent') {
860
962
  this.agent_mode = true;
861
963
  const subtitle = this.querySelector('#tp-subtitle');
862
- if (subtitle) subtitle.textContent = m.human_agent;
964
+ if (subtitle) subtitle.innerHTML = `${icon('user', 12, 'margin-right:4px')} ${m.human_agent}`;
863
965
  if (agent_bar) agent_bar.style.display = 'none';
864
966
  if (input_bar) input_bar.style.display = 'flex';
865
967
  } else {
866
968
  if (input_bar) input_bar.style.display = 'flex';
867
969
  if (agent_bar) agent_bar.style.display = 'none';
868
970
  const subtitle = this.querySelector('#tp-subtitle');
869
- if (subtitle) subtitle.textContent = m.virtual;
971
+ if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
870
972
  }
871
973
 
872
974
  this.updateUILanguage();
@@ -886,52 +988,40 @@
886
988
  this.conversation_id = null;
887
989
 
888
990
  const m = this.getMessages();
889
-
890
- const back_btn = this.querySelector('#tp-back-btn');
891
- if (back_btn) back_btn.style.display = 'block';
892
-
893
- const close_bar = this.querySelector('#tp-close-bar');
894
- if (close_bar) close_bar.style.display = 'block';
991
+ this.querySelector('#tp-back-btn').style.display = 'block';
992
+ this.querySelector('#tp-close-bar').style.display = 'block';
895
993
 
896
994
  const container = this.querySelector('#tp-messages');
995
+ container.style.background = '#f5f5f7';
996
+ container.style.padding = '16px';
897
997
  container.innerHTML = '';
898
998
 
899
999
  const subtitle = this.querySelector('#tp-subtitle');
900
- if (subtitle) subtitle.textContent = m.virtual;
901
-
902
- const input_bar = this.querySelector('#tp-chatbot-input-bar');
903
- if (input_bar) input_bar.style.display = 'flex';
904
-
905
- const agent_bar = this.querySelector('#tp-agent-bar');
906
- if (agent_bar) agent_bar.style.display = 'none';
1000
+ if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
907
1001
 
1002
+ this.querySelector('#tp-chatbot-input-bar').style.display = 'flex';
1003
+ this.querySelector('#tp-agent-bar').style.display = 'none';
908
1004
  this.updateUILanguage();
1005
+
909
1006
  this.conversation_id = await this.createConversation();
910
1007
  this.addMessage('assistant', this.getWelcomeMessage(this.user_info?.first_name || ''));
911
1008
  this.last_message_count = 1;
912
1009
 
913
1010
  const theme = getClientTheme(this.client_id);
914
- if (theme.suggestions && theme.suggestions.length > 0) {
915
- this.showSuggestions(theme.suggestions);
916
- }
1011
+ if (theme.suggestions && theme.suggestions.length > 0) this.showSuggestions(theme.suggestions);
917
1012
 
918
1013
  this.polling_interval = setInterval(() => this.pollMessages(), 3000);
919
1014
  }
920
1015
 
921
- // ─── Guest form ───────────────────────────────────────────────────────────
1016
+ // ─── Guest form ────────────────────────────────────────────────────────────
922
1017
 
923
1018
  showGuestForm() {
924
1019
  const container = this.querySelector('#tp-messages');
925
1020
  if (!container) return;
926
-
927
- const input_bar = this.querySelector('#tp-chatbot-input-bar');
928
- if (input_bar) input_bar.style.display = 'none';
929
- const agent_bar = this.querySelector('#tp-agent-bar');
930
- if (agent_bar) agent_bar.style.display = 'none';
931
-
1021
+ this.querySelector('#tp-chatbot-input-bar').style.display = 'none';
1022
+ this.querySelector('#tp-agent-bar').style.display = 'none';
932
1023
  const color = this.getThemeColor();
933
1024
  const dark = this.shadeColor(color, -20);
934
-
935
1025
  const form_el = document.createElement('div');
936
1026
  form_el.id = 'tp-guest-form';
937
1027
  form_el.innerHTML = `
@@ -940,9 +1030,7 @@
940
1030
  <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
1031
  <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
1032
  <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 →
945
- </button>
1033
+ <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;">Start conversation →</button>
946
1034
  <p id="tp-guest-error" style="margin:0;font-size:12px;color:#c0392b;display:none;"></p>
947
1035
  </div>
948
1036
  `;
@@ -955,7 +1043,6 @@
955
1043
  const company_name = this.querySelector('#tp-guest-company')?.value.trim();
956
1044
  const email = this.querySelector('#tp-guest-email')?.value.trim();
957
1045
  const error_el = this.querySelector('#tp-guest-error');
958
-
959
1046
  if (!first_name || !company_name || !email) {
960
1047
  error_el.textContent = 'Please fill in all fields.';
961
1048
  error_el.style.display = 'block';
@@ -976,57 +1063,86 @@
976
1063
  this.view = 'chat';
977
1064
  this.conversation_id = await this.createConversation();
978
1065
 
979
- const input_bar = this.querySelector('#tp-chatbot-input-bar');
980
- if (input_bar) input_bar.style.display = 'flex';
981
- const agent_bar = this.querySelector('#tp-agent-bar');
982
- if (agent_bar) agent_bar.style.display = 'none';
983
- const close_bar = this.querySelector('#tp-close-bar');
984
- if (close_bar) close_bar.style.display = 'block';
1066
+ this.querySelector('#tp-chatbot-input-bar').style.display = 'flex';
1067
+ this.querySelector('#tp-agent-bar').style.display = 'none';
1068
+ this.querySelector('#tp-close-bar').style.display = 'block';
1069
+
1070
+ const container = this.querySelector('#tp-messages');
1071
+ container.style.background = '#f5f5f7';
1072
+ container.style.padding = '16px';
985
1073
 
986
1074
  this.updateUILanguage();
987
1075
  this.addMessage('assistant', this.getWelcomeMessage(first_name));
988
1076
  this.last_message_count = 1;
989
1077
 
990
1078
  const theme = getClientTheme(this.client_id);
991
- if (theme.suggestions && theme.suggestions.length > 0) {
992
- this.showSuggestions(theme.suggestions);
993
- }
1079
+ if (theme.suggestions && theme.suggestions.length > 0) this.showSuggestions(theme.suggestions);
994
1080
 
995
1081
  this.polling_interval = setInterval(() => this.pollMessages(), 3000);
996
1082
  }
997
1083
 
998
- // ─── Messages ─────────────────────────────────────────────────────────────
1084
+ // ─── Messages ──────────────────────────────────────────────────────────────
999
1085
 
1000
- addMessage(role, content) {
1001
- this.messages.push({ role, content, created_at: new Date().toISOString() });
1086
+ addMessage(role, content, message_id = null) {
1087
+ this.messages.push({ role, content, message_id, created_at: new Date().toISOString() });
1002
1088
  const container = this.querySelector('#tp-messages');
1003
1089
 
1004
1090
  const role_label =
1005
1091
  role === 'user'
1006
- ? `👤 ${this.user_info?.first_name || 'You'}`
1092
+ ? `${icon('user', 11, 'margin-right:3px')} ${this.user_info?.first_name || 'You'}`
1007
1093
  : role === 'agent'
1008
- ? '👨‍💼 Agent'
1094
+ ? `${icon('agent', 11, 'margin-right:3px')} Agent`
1009
1095
  : role === 'system'
1010
- ? 'ℹ️ Info'
1011
- : '🤖 Maria';
1096
+ ? 'Info'
1097
+ : `${icon('bot', 11, 'margin-right:3px')} Maria`;
1012
1098
 
1013
1099
  const msg_el = document.createElement('div');
1014
1100
  msg_el.className = `tp-chatbot-message ${role}`;
1015
1101
  const rendered_content =
1016
1102
  role === 'assistant' || role === 'agent' ? (typeof marked !== 'undefined' ? marked.parse(content) : content) : content;
1017
1103
 
1104
+ const feedback_html =
1105
+ role === 'assistant' && message_id
1106
+ ? `<div class="tp-feedback-bar" data-message-id="${message_id}">
1107
+ <button class="tp-feedback-btn" data-rating="positive" title="Helpful">${icon('helpful', 14)}</button>
1108
+ <button class="tp-feedback-btn" data-rating="negative" title="Not helpful">${icon('not_helpful', 14)}</button>
1109
+ </div>`
1110
+ : '';
1111
+
1018
1112
  msg_el.innerHTML = `
1019
1113
  <div class="tp-chatbot-bubble-msg">
1020
1114
  <div class="tp-chatbot-role">${role_label}</div>
1021
1115
  <div class="tp-chatbot-content tp-chatbot-markdown">${rendered_content}</div>
1116
+ ${feedback_html}
1022
1117
  </div>
1023
1118
  `;
1119
+
1120
+ if (role === 'assistant' && message_id) {
1121
+ const bar = msg_el.querySelector('.tp-feedback-bar');
1122
+ bar.querySelectorAll('.tp-feedback-btn').forEach(btn => {
1123
+ btn.addEventListener('click', async () => {
1124
+ if (bar.dataset.voted) return;
1125
+ bar.dataset.voted = '1';
1126
+ const rating = btn.dataset.rating;
1127
+ bar.querySelectorAll('.tp-feedback-btn').forEach(b => {
1128
+ b.style.opacity = b === btn ? '1' : '0.3';
1129
+ b.style.cursor = 'default';
1130
+ });
1131
+ try {
1132
+ await fetch(`${this.api_url}/chat/conversations/${this.conversation_id}/feedback`, {
1133
+ method: 'POST',
1134
+ headers: this.getHeaders(true),
1135
+ body: JSON.stringify({ message_id, rating, user_id: this.user_id }),
1136
+ });
1137
+ } catch {}
1138
+ });
1139
+ });
1140
+ }
1141
+
1024
1142
  container.appendChild(msg_el);
1025
1143
  container.scrollTop = container.scrollHeight;
1026
1144
 
1027
- if ((role === 'assistant' || role === 'agent') && this.sound_enabled) {
1028
- playSound();
1029
- }
1145
+ if ((role === 'assistant' || role === 'agent') && this.sound_enabled) playSound();
1030
1146
  }
1031
1147
 
1032
1148
  showTyping() {
@@ -1040,8 +1156,8 @@
1040
1156
  }
1041
1157
 
1042
1158
  hideTyping() {
1043
- const typing = this.querySelector('#tp-typing');
1044
- if (typing) typing.remove();
1159
+ const t = this.querySelector('#tp-typing');
1160
+ if (t) t.remove();
1045
1161
  }
1046
1162
 
1047
1163
  showClosed(existing_csat = null) {
@@ -1053,14 +1169,11 @@
1053
1169
 
1054
1170
  const m = this.getMessages();
1055
1171
  const subtitle = this.querySelector('#tp-subtitle');
1056
- if (subtitle) subtitle.textContent = m.terminated;
1172
+ if (subtitle) subtitle.innerHTML = `${icon('check', 12, 'margin-right:4px')} ${m.terminated}`;
1057
1173
 
1058
- const input_bar = this.querySelector('#tp-chatbot-input-bar');
1059
- if (input_bar) input_bar.style.display = 'none';
1060
- const agent_bar = this.querySelector('#tp-agent-bar');
1061
- if (agent_bar) agent_bar.style.display = 'none';
1062
- const close_bar = this.querySelector('#tp-close-bar');
1063
- if (close_bar) close_bar.style.display = 'none';
1174
+ this.querySelector('#tp-chatbot-input-bar').style.display = 'none';
1175
+ this.querySelector('#tp-agent-bar').style.display = 'none';
1176
+ this.querySelector('#tp-close-bar').style.display = 'none';
1064
1177
  const sugg_el = this.querySelector('#tp-suggestions');
1065
1178
  if (sugg_el) sugg_el.remove();
1066
1179
 
@@ -1078,17 +1191,21 @@
1078
1191
  ? `<div style="font-size:13px;color:#888;margin-bottom:14px;">${existing_csat === 'positive' ? m.csat_positive : m.csat_negative}</div>`
1079
1192
  : `<div style="font-size:12px;color:#888;margin-bottom:10px;">${m.csat_question}</div>
1080
1193
  <div style="display:flex;gap:10px;justify-content:center;margin-bottom:14px;">
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>
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>
1194
+ <button id="tp-csat-positive" class="tp-csat-btn" style="border-color:#22c55e;color:#22c55e;">${icon('helpful', 20)}</button>
1195
+ <button id="tp-csat-negative" class="tp-csat-btn" style="border-color:#ef4444;color:#ef4444;">${icon('not_helpful', 20)}</button>
1083
1196
  </div>`;
1084
1197
 
1085
1198
  const banner = document.createElement('div');
1086
1199
  banner.id = 'tp-closed-banner';
1087
1200
  banner.innerHTML = `
1088
- <div style="margin:16px;padding:16px;background:#f9f5ff;border:1px solid #ede8f5;border-radius:12px;text-align:center;">
1089
- <div style="font-size:13px;color:#1a1a2e;font-weight:600;margin-bottom:8px;">${m.closed}</div>
1201
+ <div style="margin:16px;padding:16px;background:#f9f9f9;border:1px solid #ede8f5;border-radius:12px;text-align:center;">
1202
+ <div style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:#1a1a2e;font-weight:600;margin-bottom:10px;">
1203
+ ${icon('check', 14)} ${m.closed}
1204
+ </div>
1090
1205
  <div id="tp-csat-block">${csat_html}</div>
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>
1206
+ <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:700;cursor:pointer;margin-bottom:8px;width:100%;display:flex;align-items:center;justify-content:center;gap:8px;">
1207
+ ${icon('msg_plus', 15, 'color:white')} ${m.new_conv}
1208
+ </button>
1092
1209
  <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>
1093
1210
  </div>
1094
1211
  `;
@@ -1118,7 +1235,7 @@
1118
1235
  this.querySelector('#tp-back-to-list').addEventListener('click', () => this.showConversationList());
1119
1236
  }
1120
1237
 
1121
- // ─── Send / Poll ──────────────────────────────────────────────────────────
1238
+ // ─── Send / Poll ───────────────────────────────────────────────────────────
1122
1239
 
1123
1240
  async sendMessage() {
1124
1241
  if (this.is_closed || !this.conversation_id) return;
@@ -1133,8 +1250,7 @@
1133
1250
  const user_messages = this.messages.filter(msg => msg.role === 'user').length;
1134
1251
  if (user_messages >= 30) {
1135
1252
  this.addMessage('system', m.limit_reached);
1136
- const input_bar = this.querySelector('#tp-chatbot-input-bar');
1137
- if (input_bar) input_bar.style.display = 'none';
1253
+ this.querySelector('#tp-chatbot-input-bar').style.display = 'none';
1138
1254
  return;
1139
1255
  }
1140
1256
 
@@ -1149,7 +1265,6 @@
1149
1265
  headers: this.getHeaders(true),
1150
1266
  body: JSON.stringify({ query, user_id: this.user_id, user_info: this.user_info }),
1151
1267
  });
1152
-
1153
1268
  const data = await response.json();
1154
1269
  this.hideTyping();
1155
1270
 
@@ -1161,19 +1276,24 @@
1161
1276
 
1162
1277
  this.agent_mode = data.result.agent_mode;
1163
1278
  const source = data.result.source || null;
1164
-
1165
1279
  const subtitle = this.querySelector('#tp-subtitle');
1166
- if (subtitle) subtitle.textContent = this.agent_mode ? m.human_agent : m.virtual;
1280
+ if (subtitle)
1281
+ subtitle.innerHTML = this.agent_mode
1282
+ ? `${icon('user', 12, 'margin-right:4px')} ${m.human_agent}`
1283
+ : `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
1167
1284
 
1168
1285
  if (data.result.reply) {
1169
- this.addMessage(this.agent_mode ? 'agent' : 'assistant', data.result.reply);
1170
- this.last_message_count += 2; // fix double message
1286
+ this.addMessage(this.agent_mode ? 'agent' : 'assistant', data.result.reply, data.result.message_id || null);
1287
+ this.last_message_count += 2;
1171
1288
  }
1172
1289
 
1173
1290
  const user_msg_count = this.messages.filter(msg => msg.role === 'user').length;
1174
1291
  const agent_bar = this.querySelector('#tp-agent-bar');
1292
+ const suggest_agent = data.result.suggest_agent || false;
1293
+
1175
1294
  if (agent_bar && !this.agent_requested && !this.agent_mode) {
1176
1295
  if (
1296
+ suggest_agent ||
1177
1297
  source === 'fallback' ||
1178
1298
  source === 'clarification' ||
1179
1299
  source === 'no_match' ||
@@ -1183,9 +1303,14 @@
1183
1303
  agent_bar.style.display = 'block';
1184
1304
  }
1185
1305
  }
1306
+
1307
+ if (suggest_agent && !this.agent_requested) {
1308
+ this.addMessage('system', m.suggest_agent_escalation);
1309
+ this.last_message_count += 1;
1310
+ }
1186
1311
  } catch {
1187
1312
  this.hideTyping();
1188
- this.addMessage('assistant', m.error_occurred);
1313
+ this.addMessage('assistant', this.getMessages().error_occurred);
1189
1314
  }
1190
1315
 
1191
1316
  this.is_loading = false;
@@ -1208,17 +1333,11 @@
1208
1333
  const status = data.result?.status;
1209
1334
 
1210
1335
  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
- }
1336
+ if (status === 'outside_hours') msg = m.outside_hours(data.result.next_opening);
1337
+ else if (status === 'no_agents') msg = m.no_agents;
1338
+ else if (status === 'waiting_agent') msg = m.waiting_agent;
1339
+ else if (status === 'waiting_agent_already') msg = m.waiting_agent_already;
1340
+ else if (status === 'agent') msg = m.agent_already;
1222
1341
 
1223
1342
  if (msg) {
1224
1343
  this.addMessage('system', msg);
@@ -1231,6 +1350,7 @@
1231
1350
  }
1232
1351
  } catch {
1233
1352
  this.agent_requested = false;
1353
+ const agent_bar = this.querySelector('#tp-agent-bar');
1234
1354
  if (agent_bar) agent_bar.style.display = 'block';
1235
1355
  }
1236
1356
  }
@@ -1255,7 +1375,6 @@
1255
1375
  const url = `${this.api_url}/chat/conversations/${this.conversation_id}?user_id=${encodeURIComponent(this.user_id)}`;
1256
1376
  const response = await fetch(url, { headers: this.getHeaders() });
1257
1377
  if (!response.ok) return;
1258
-
1259
1378
  const data = await response.json();
1260
1379
  const conv = data.result?.conversation;
1261
1380
  if (!conv) return;
@@ -1276,7 +1395,7 @@
1276
1395
  const typing = document.createElement('div');
1277
1396
  typing.className = 'tp-chatbot-message agent';
1278
1397
  typing.id = 'tp-agent-typing';
1279
- typing.innerHTML = `<div class="tp-chatbot-bubble-msg"><div class="tp-chatbot-role">👨‍💼 Agent</div><div class="tp-chatbot-typing"><span></span><span></span><span></span></div></div>`;
1398
+ typing.innerHTML = `<div class="tp-chatbot-bubble-msg"><div class="tp-chatbot-role">${icon('agent', 11, 'margin-right:3px')} Agent</div><div class="tp-chatbot-typing"><span></span><span></span><span></span></div></div>`;
1280
1399
  container.appendChild(typing);
1281
1400
  container.scrollTop = container.scrollHeight;
1282
1401
  }
@@ -1291,9 +1410,9 @@
1291
1410
  if (msg.role === 'agent') {
1292
1411
  const typing_el = this.querySelector('#tp-agent-typing');
1293
1412
  if (typing_el) typing_el.remove();
1294
- this.addMessage('agent', msg.content);
1413
+ this.addMessage('agent', msg.content, msg.message_id || null);
1295
1414
  } else if (msg.role === 'assistant') {
1296
- this.addMessage('assistant', msg.content);
1415
+ this.addMessage('assistant', msg.content, msg.message_id || null);
1297
1416
  } else if (msg.role === 'system') {
1298
1417
  this.addMessage('system', msg.content);
1299
1418
  }
@@ -1304,7 +1423,7 @@
1304
1423
  if (server_status === 'agent' && !this.agent_mode) {
1305
1424
  this.agent_mode = true;
1306
1425
  const subtitle = this.querySelector('#tp-subtitle');
1307
- if (subtitle) subtitle.textContent = m.human_agent;
1426
+ if (subtitle) subtitle.innerHTML = `${icon('user', 12, 'margin-right:4px')} ${m.human_agent}`;
1308
1427
  const agent_bar = this.querySelector('#tp-agent-bar');
1309
1428
  if (agent_bar) agent_bar.style.display = 'none';
1310
1429
  this.addMessage('system', m.agent_taken);
@@ -1313,7 +1432,7 @@
1313
1432
  this.agent_mode = false;
1314
1433
  this.agent_requested = false;
1315
1434
  const subtitle = this.querySelector('#tp-subtitle');
1316
- if (subtitle) subtitle.textContent = m.virtual;
1435
+ if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
1317
1436
  const agent_bar = this.querySelector('#tp-agent-bar');
1318
1437
  if (agent_bar) agent_bar.style.display = 'block';
1319
1438
  this.addMessage('system', m.agent_released);