@developpement/tp-chatbot-widget 0.0.8 → 0.0.10

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