@developpement/tp-chatbot-widget 0.0.9 → 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 +1 -1
  2. package/src/chatbot.css +254 -39
  3. package/src/chatbot.js +410 -296
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,224 +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.',
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.',
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',
249
319
  },
250
320
  EN: {
251
- closed: 'Conversation closed',
321
+ closed: 'Conversation closed',
252
322
  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',
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',
257
327
  agent_taken: 'An agent has taken over your conversation. You can now write to them directly.',
258
328
  agent_released: 'Our virtual assistant is back. How can I help you?',
259
329
  close_confirm: 'Do you want to end this conversation?',
260
- terminated: 'Conversation ended',
261
- virtual: '🤖 Virtual assistant',
262
- human_agent: '👤 Human agent',
330
+ terminated: 'Conversation ended',
331
+ virtual: 'Virtual assistant',
332
+ human_agent: 'Human agent',
263
333
  loading: 'Loading...',
264
334
  no_conv: 'No conversations yet.',
265
- new_conv_btn: '💬 New conversation',
335
+ new_conv_btn: 'New conversation',
266
336
  error_load: 'Loading error.',
267
337
  recent_conv: 'Your recent conversations',
268
338
  hello: 'Hello',
269
- close_btn: 'End conversation',
270
- speak_agent: '👤 Talk to an agent',
339
+ close_btn: 'End conversation',
340
+ speak_agent: 'Talk to an agent',
271
341
  write_msg: 'Write your message...',
272
342
  limit_reached: 'You have reached the 30 message limit. Please contact an agent or write to support@travelplanet.com.',
273
343
  too_many: 'Too many messages. Please wait before retrying.',
274
344
  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.',
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',
280
356
  },
281
357
  DE: {
282
- closed: 'Gespräch beendet',
358
+ closed: 'Gespräch beendet',
283
359
  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',
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',
288
364
  agent_taken: 'Ein Agent hat Ihr Gespräch übernommen. Sie können ihm jetzt direkt schreiben.',
289
365
  agent_released: 'Unser virtueller Assistent ist zurück. Wie kann ich Ihnen helfen?',
290
366
  close_confirm: 'Möchten Sie das Gespräch beenden?',
291
- terminated: 'Gespräch beendet',
292
- virtual: '🤖 Virtueller Assistent',
293
- human_agent: '👤 Menschlicher Agent',
367
+ terminated: 'Gespräch beendet',
368
+ virtual: 'Virtueller Assistent',
369
+ human_agent: 'Menschlicher Agent',
294
370
  loading: 'Laden...',
295
371
  no_conv: 'Noch keine Gespräche.',
296
- new_conv_btn: '💬 Neues Gespräch',
372
+ new_conv_btn: 'Neues Gespräch',
297
373
  error_load: 'Ladefehler.',
298
374
  recent_conv: 'Ihre letzten Gespräche',
299
375
  hello: 'Hallo',
300
- close_btn: 'Gespräch beenden',
301
- speak_agent: '👤 Mit einem Agenten sprechen',
376
+ close_btn: 'Gespräch beenden',
377
+ speak_agent: 'Mit einem Agenten sprechen',
302
378
  write_msg: 'Schreiben Sie Ihre Nachricht...',
303
379
  limit_reached: 'Sie haben das Limit von 30 Nachrichten erreicht.',
304
380
  too_many: 'Zu viele Nachrichten. Bitte warten Sie.',
305
381
  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.',
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',
311
393
  },
312
394
  ES: {
313
- closed: 'Conversación cerrada',
395
+ closed: 'Conversación cerrada',
314
396
  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',
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',
319
401
  agent_taken: 'Un agente ha tomado su conversación. Ahora puede escribirle directamente.',
320
402
  agent_released: 'Nuestro asistente virtual ha vuelto. ¿En qué puedo ayudarle?',
321
403
  close_confirm: '¿Desea finalizar esta conversación?',
322
- terminated: 'Conversación terminada',
323
- virtual: '🤖 Asistente virtual',
324
- human_agent: '👤 Agente humano',
404
+ terminated: 'Conversación terminada',
405
+ virtual: 'Asistente virtual',
406
+ human_agent: 'Agente humano',
325
407
  loading: 'Cargando...',
326
408
  no_conv: 'No hay conversaciones por el momento.',
327
- new_conv_btn: '💬 Nueva conversación',
409
+ new_conv_btn: 'Nueva conversación',
328
410
  error_load: 'Error de carga.',
329
411
  recent_conv: 'Sus conversaciones recientes',
330
412
  hello: 'Hola',
331
- close_btn: 'Finalizar conversación',
332
- speak_agent: '👤 Hablar con un agente',
413
+ close_btn: 'Finalizar conversación',
414
+ speak_agent: 'Hablar con un agente',
333
415
  write_msg: 'Escriba su mensaje...',
334
416
  limit_reached: 'Ha alcanzado el límite de 30 mensajes.',
335
417
  too_many: 'Demasiados mensajes. Por favor espere.',
336
418
  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.',
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',
342
430
  },
343
431
  IT: {
344
- closed: 'Conversazione chiusa',
432
+ closed: 'Conversazione chiusa',
345
433
  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',
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',
350
438
  agent_taken: 'Un agente ha preso in carico la sua conversazione.',
351
439
  agent_released: 'Il nostro assistente virtuale è tornato. Come posso aiutarla?',
352
440
  close_confirm: 'Vuole terminare questa conversazione?',
353
- terminated: 'Conversazione terminata',
354
- virtual: '🤖 Assistente virtuale',
355
- human_agent: '👤 Agente umano',
441
+ terminated: 'Conversazione terminata',
442
+ virtual: 'Assistente virtuale',
443
+ human_agent: 'Agente umano',
356
444
  loading: 'Caricamento...',
357
445
  no_conv: 'Nessuna conversazione per il momento.',
358
- new_conv_btn: '💬 Nuova conversazione',
446
+ new_conv_btn: 'Nuova conversazione',
359
447
  error_load: 'Errore di caricamento.',
360
448
  recent_conv: 'Le sue conversazioni recenti',
361
449
  hello: 'Ciao',
362
- close_btn: 'Termina conversazione',
363
- speak_agent: '👤 Parla con un agente',
450
+ close_btn: 'Termina conversazione',
451
+ speak_agent: 'Parla con un agente',
364
452
  write_msg: 'Scrivi il tuo messaggio...',
365
453
  limit_reached: 'Hai raggiunto il limite di 30 messaggi.',
366
454
  too_many: 'Troppi messaggi. Attendere prego.',
367
455
  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.',
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',
373
467
  },
374
468
  NL: {
375
- closed: 'Gesprek gesloten',
469
+ closed: 'Gesprek gesloten',
376
470
  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',
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',
381
475
  agent_taken: 'Een agent heeft uw gesprek overgenomen.',
382
476
  agent_released: 'Onze virtuele assistent is terug. Hoe kan ik u helpen?',
383
477
  close_confirm: 'Wilt u dit gesprek beëindigen?',
384
- terminated: 'Gesprek beëindigd',
385
- virtual: '🤖 Virtuele assistent',
386
- human_agent: '👤 Menselijke agent',
478
+ terminated: 'Gesprek beëindigd',
479
+ virtual: 'Virtuele assistent',
480
+ human_agent: 'Menselijke agent',
387
481
  loading: 'Laden...',
388
482
  no_conv: 'Geen gesprekken op dit moment.',
389
- new_conv_btn: '💬 Nieuw gesprek',
390
- error_load: 'Laadфout.',
483
+ new_conv_btn: 'Nieuw gesprek',
484
+ error_load: 'Laadout.',
391
485
  recent_conv: 'Uw recente gesprekken',
392
486
  hello: 'Hallo',
393
- close_btn: 'Gesprek beëindigen',
394
- speak_agent: '👤 Praat met een agent',
487
+ close_btn: 'Gesprek beëindigen',
488
+ speak_agent: 'Praat met een agent',
395
489
  write_msg: 'Schrijf uw bericht...',
396
490
  limit_reached: 'U heeft de limiet van 30 berichten bereikt.',
397
491
  too_many: 'Te veel berichten. Wacht even.',
398
492
  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.',
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',
404
504
  },
405
505
  };
406
506
  return msgs[lang] || msgs['EN'];
@@ -410,17 +510,17 @@
410
510
  const lang = (this.user_info?.language || 'EN').toUpperCase();
411
511
  const client_name = getClientTheme(this.client_id).name;
412
512
  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?`,
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?`,
419
519
  };
420
520
  return msgs[lang] || msgs['EN'];
421
521
  }
422
522
 
423
- // ─── Theme ────────────────────────────────────────────────────────────────
523
+ // ─── Theme ─────────────────────────────────────────────────────────────────
424
524
 
425
525
  getThemeColor() {
426
526
  return getClientTheme(this.client_id).primary;
@@ -456,6 +556,9 @@
456
556
  #tp-new-conversation { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
457
557
  .tp-conv-item:hover { background: ${color}11 !important; }
458
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; }
459
562
  `;
460
563
  document.head.appendChild(style);
461
564
 
@@ -466,13 +569,11 @@
466
569
  host.style.left = position === 'left' ? '24px' : 'auto';
467
570
  host.style.right = position === 'left' ? 'auto' : '24px';
468
571
  }
469
-
470
572
  const bubble = this.querySelector('.tp-chatbot-bubble');
471
573
  if (bubble) {
472
574
  bubble.style.marginLeft = position === 'left' ? '0' : 'auto';
473
575
  bubble.style.marginRight = position === 'left' ? 'auto' : '0';
474
576
  }
475
-
476
577
  const win = this.querySelector('.tp-chatbot-window');
477
578
  if (win) {
478
579
  win.style.left = '0';
@@ -481,7 +582,7 @@
481
582
  }
482
583
  }
483
584
 
484
- // ─── Render ───────────────────────────────────────────────────────────────
585
+ // ─── Render ────────────────────────────────────────────────────────────────
485
586
 
486
587
  render() {
487
588
  const theme = getClientTheme(this.client_id);
@@ -490,39 +591,40 @@
490
591
  <div class="tp-chatbot-window" id="tp-window">
491
592
  <div class="tp-chatbot-header" id="tp-header">
492
593
  <div class="tp-chatbot-avatar">M</div>
493
- <div style="flex:1">
594
+ <div style="flex:1;min-width:0;">
494
595
  <div class="tp-chatbot-title">${theme.name} Support</div>
495
- <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>
496
597
  </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>
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>
501
602
  </div>
502
603
  <div class="tp-chatbot-messages" id="tp-messages"></div>
503
604
  <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>
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>
505
608
  </div>
506
609
  <div class="tp-chatbot-input-bar" id="tp-chatbot-input-bar" style="display:none;">
507
610
  <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>
611
+ <button class="tp-chatbot-send" id="tp-send">${icon('send', 16, 'color:white')}</button>
509
612
  </div>
510
613
  <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
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>
513
616
  </button>
514
617
  </div>
515
618
  </div>
516
- <div class="tp-chatbot-bubble" id="tp-bubble">💬</div>
619
+ <div class="tp-chatbot-bubble" id="tp-bubble">${icon('msg', 24, 'color:white')}</div>
517
620
  </div>
518
621
  `;
519
622
  }
520
623
 
521
- // ─── Window controls ──────────────────────────────────────────────────────
624
+ // ─── Window controls ───────────────────────────────────────────────────────
522
625
 
523
626
  minimize() {
524
627
  const win = this.querySelector('#tp-window');
525
- // Exit fullscreen first if needed
526
628
  if (this.is_fullscreen) this.exitFullscreen(false);
527
629
  this.is_minimized = true;
528
630
  win.style.height = '56px';
@@ -542,7 +644,8 @@
542
644
  win.style.borderRadius = '0';
543
645
  win.style.zIndex = '99999';
544
646
  host.style.width = '100vw';
545
- this.querySelector('#tp-maximize-btn').textContent = '⤡';
647
+ const btn = this.querySelector('#tp-maximize-btn');
648
+ if (btn) btn.innerHTML = icon('restore', 16);
546
649
  }
547
650
 
548
651
  exitFullscreen(restore_height = true) {
@@ -561,10 +664,11 @@
561
664
  host.style.right = position === 'left' ? 'auto' : '24px';
562
665
  if (restore_height) win.style.height = WINDOW_HEIGHT;
563
666
  win.style.overflow = 'hidden';
564
- this.querySelector('#tp-maximize-btn').textContent = '□';
667
+ const btn = this.querySelector('#tp-maximize-btn');
668
+ if (btn) btn.innerHTML = icon('restore', 16);
565
669
  }
566
670
 
567
- // ─── Events ───────────────────────────────────────────────────────────────
671
+ // ─── Events ────────────────────────────────────────────────────────────────
568
672
 
569
673
  attachEvents() {
570
674
  this.querySelector('#tp-bubble').addEventListener('click', () => this.toggleChat());
@@ -581,18 +685,15 @@
581
685
 
582
686
  this.querySelector('#tp-sound-btn').addEventListener('click', () => {
583
687
  this.sound_enabled = !this.sound_enabled;
584
- this.querySelector('#tp-sound-btn').textContent = this.sound_enabled ? '🔔' : '🔕';
688
+ this.querySelector('#tp-sound-btn').innerHTML = icon(this.sound_enabled ? 'bell' : 'bell_off', 16);
585
689
  });
586
690
 
587
691
  this.querySelector('#tp-close-btn').addEventListener('click', () => {
588
- if (window.confirm(this.getMessages().close_confirm)) {
589
- this.closeByUser();
590
- }
692
+ if (window.confirm(this.getMessages().close_confirm)) this.closeByUser();
591
693
  });
592
694
 
593
695
  this.querySelector('#tp-minimize-btn').addEventListener('click', () => {
594
696
  if (this.is_minimized) {
595
- // Restore
596
697
  this.is_minimized = false;
597
698
  const win = this.querySelector('#tp-window');
598
699
  win.style.height = WINDOW_HEIGHT;
@@ -620,13 +721,15 @@
620
721
  updateUILanguage() {
621
722
  const m = this.getMessages();
622
723
  const agent_btn = this.querySelector('#tp-agent-btn');
623
- 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>`;
624
726
  const close_btn = this.querySelector('#tp-close-btn');
625
- 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>`;
626
729
  const input = this.querySelector('#tp-input');
627
730
  if (input) input.placeholder = m.write_msg;
628
731
  const subtitle = this.querySelector('#tp-subtitle');
629
- if (subtitle) subtitle.textContent = m.virtual;
732
+ if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
630
733
  }
631
734
 
632
735
  toggleChat() {
@@ -635,8 +738,7 @@
635
738
  const bubble = this.querySelector('#tp-bubble');
636
739
  if (this.is_open) {
637
740
  window_el.classList.add('open');
638
- bubble.textContent = '';
639
- // Restore from minimized if needed
741
+ bubble.innerHTML = icon('close', 20, 'color:white');
640
742
  if (this.is_minimized) {
641
743
  this.is_minimized = false;
642
744
  window_el.style.height = WINDOW_HEIGHT;
@@ -644,7 +746,7 @@
644
746
  }
645
747
  } else {
646
748
  window_el.classList.remove('open');
647
- bubble.textContent = '💬';
749
+ bubble.innerHTML = icon('msg', 24, 'color:white');
648
750
  if (this.is_fullscreen) this.exitFullscreen(true);
649
751
  }
650
752
  }
@@ -660,11 +762,7 @@
660
762
  const response = await fetch(`${this.api_url}/chat/conversations`, {
661
763
  method: 'POST',
662
764
  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
- }),
765
+ body: JSON.stringify({ user_id: this.user_id, user_info: this.user_info, client_id: this.client_id }),
668
766
  });
669
767
  const data = await response.json();
670
768
  return data.result?.conversation_id;
@@ -676,15 +774,12 @@
676
774
  const existing = this.querySelector('#tp-suggestions');
677
775
  if (existing) existing.remove();
678
776
  if (!suggestions || suggestions.length === 0) return;
679
-
680
777
  const lang = (this.user_info?.language || 'EN').toUpperCase();
681
778
  const color = this.getThemeColor();
682
779
  const container = this.querySelector('#tp-messages');
683
-
684
780
  const wrap = document.createElement('div');
685
781
  wrap.id = 'tp-suggestions';
686
782
  wrap.style.cssText = 'padding:8px 12px 12px;display:flex;flex-direction:column;gap:6px;';
687
-
688
783
  suggestions.forEach(s => {
689
784
  const label = s[lang] || s['EN'];
690
785
  const btn = document.createElement('button');
@@ -709,38 +804,29 @@
709
804
  });
710
805
  wrap.appendChild(btn);
711
806
  });
712
-
713
807
  container.appendChild(wrap);
714
808
  container.scrollTop = container.scrollHeight;
715
809
  }
716
810
 
717
- // ─── Conversation List ────────────────────────────────────────────────────
811
+ // ─── Conversation List ─────────────────────────────────────────────────────
718
812
 
719
813
  async showConversationList() {
720
814
  this.view = 'list';
721
815
  const m = this.getMessages();
722
-
723
816
  if (this.polling_interval) {
724
817
  clearInterval(this.polling_interval);
725
818
  this.polling_interval = null;
726
819
  }
727
820
 
728
821
  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';
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';
742
827
 
743
828
  const container = this.querySelector('#tp-messages');
829
+ container.style.background = '#f5f5f7';
744
830
  container.innerHTML = `<div style="padding:16px;text-align:center;color:#aaa;font-size:12px;">${m.loading}</div>`;
745
831
 
746
832
  try {
@@ -751,35 +837,40 @@
751
837
  const color = this.getThemeColor();
752
838
 
753
839
  container.innerHTML = '';
840
+ container.style.background = 'white';
841
+ container.style.padding = '0';
754
842
 
843
+ // Header greeting
755
844
  const header_el = document.createElement('div');
756
- header_el.style.cssText = 'padding:16px;border-bottom:1px solid #ede8f5;';
845
+ header_el.style.cssText = 'padding:16px;border-bottom:1px solid #f0f0f0;';
757
846
  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>
847
+ <div style="font-size:14px;font-weight:700;color:#1a1a2e;margin-bottom:2px;">${m.hello} ${this.user_info?.first_name || ''} 👋</div>
759
848
  <div style="font-size:12px;color:#aaa;">${m.recent_conv}</div>
760
849
  `;
761
850
  container.appendChild(header_el);
762
851
 
763
852
  if (conversations.length === 0) {
764
853
  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;
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}`;
767
856
  container.appendChild(empty);
768
857
  } else {
769
858
  conversations.forEach(conv => {
770
859
  const item = document.createElement('div');
771
860
  item.className = 'tp-conv-item';
772
- 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;';
773
863
 
774
864
  const is_closed = conv.status === 'closed';
775
865
  const status_label = is_closed
776
- ? '✅ Closed'
866
+ ? m.status_closed
777
867
  : conv.status === 'agent'
778
- ? '👤 Agent'
868
+ ? m.status_agent
779
869
  : conv.status === 'waiting_agent'
780
- ? '⏳ Waiting'
781
- : '🤖 Bot';
782
- 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;
783
874
  const date = new Date(conv.updated_at).toLocaleDateString('fr-FR', {
784
875
  day: '2-digit',
785
876
  month: '2-digit',
@@ -788,20 +879,27 @@
788
879
  });
789
880
 
790
881
  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>
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>
794
885
  </div>
795
- <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>
796
889
  `;
797
890
  item.addEventListener('click', () => this.openConversation(conv.conversation_id, is_closed));
798
891
  container.appendChild(item);
799
892
  });
800
893
  }
801
894
 
895
+ // New conversation button
802
896
  const new_btn_wrap = document.createElement('div');
803
897
  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>`;
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
+ `;
805
903
  new_btn_wrap.querySelector('button').addEventListener('click', () => this.startNewConversation());
806
904
  container.appendChild(new_btn_wrap);
807
905
  } catch (e) {
@@ -820,10 +918,10 @@
820
918
  this.agent_requested = false;
821
919
 
822
920
  const m = this.getMessages();
823
- const back_btn = this.querySelector('#tp-back-btn');
824
- if (back_btn) back_btn.style.display = 'block';
825
-
921
+ this.querySelector('#tp-back-btn').style.display = 'block';
826
922
  const container = this.querySelector('#tp-messages');
923
+ container.style.background = '#f5f5f7';
924
+ container.style.padding = '16px';
827
925
  container.innerHTML = '';
828
926
 
829
927
  const input_bar = this.querySelector('#tp-chatbot-input-bar');
@@ -839,10 +937,9 @@
839
937
 
840
938
  conv.messages.forEach(msg => {
841
939
  if (['user', 'assistant', 'agent', 'system'].includes(msg.role)) {
842
- this.addMessage(msg.role, msg.content);
940
+ this.addMessage(msg.role, msg.content, msg.message_id || null);
843
941
  }
844
942
  });
845
-
846
943
  this.last_message_count = conv.messages.length;
847
944
 
848
945
  if (conv.status === 'closed') {
@@ -859,14 +956,14 @@
859
956
  if (conv.status === 'agent') {
860
957
  this.agent_mode = true;
861
958
  const subtitle = this.querySelector('#tp-subtitle');
862
- if (subtitle) subtitle.textContent = m.human_agent;
959
+ if (subtitle) subtitle.innerHTML = `${icon('user', 12, 'margin-right:4px')} ${m.human_agent}`;
863
960
  if (agent_bar) agent_bar.style.display = 'none';
864
961
  if (input_bar) input_bar.style.display = 'flex';
865
962
  } else {
866
963
  if (input_bar) input_bar.style.display = 'flex';
867
964
  if (agent_bar) agent_bar.style.display = 'none';
868
965
  const subtitle = this.querySelector('#tp-subtitle');
869
- if (subtitle) subtitle.textContent = m.virtual;
966
+ if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
870
967
  }
871
968
 
872
969
  this.updateUILanguage();
@@ -886,52 +983,40 @@
886
983
  this.conversation_id = null;
887
984
 
888
985
  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';
986
+ this.querySelector('#tp-back-btn').style.display = 'block';
987
+ this.querySelector('#tp-close-bar').style.display = 'block';
895
988
 
896
989
  const container = this.querySelector('#tp-messages');
990
+ container.style.background = '#f5f5f7';
991
+ container.style.padding = '16px';
897
992
  container.innerHTML = '';
898
993
 
899
994
  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';
995
+ if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
907
996
 
997
+ this.querySelector('#tp-chatbot-input-bar').style.display = 'flex';
998
+ this.querySelector('#tp-agent-bar').style.display = 'none';
908
999
  this.updateUILanguage();
1000
+
909
1001
  this.conversation_id = await this.createConversation();
910
1002
  this.addMessage('assistant', this.getWelcomeMessage(this.user_info?.first_name || ''));
911
1003
  this.last_message_count = 1;
912
1004
 
913
1005
  const theme = getClientTheme(this.client_id);
914
- if (theme.suggestions && theme.suggestions.length > 0) {
915
- this.showSuggestions(theme.suggestions);
916
- }
1006
+ if (theme.suggestions && theme.suggestions.length > 0) this.showSuggestions(theme.suggestions);
917
1007
 
918
1008
  this.polling_interval = setInterval(() => this.pollMessages(), 3000);
919
1009
  }
920
1010
 
921
- // ─── Guest form ───────────────────────────────────────────────────────────
1011
+ // ─── Guest form ────────────────────────────────────────────────────────────
922
1012
 
923
1013
  showGuestForm() {
924
1014
  const container = this.querySelector('#tp-messages');
925
1015
  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
-
1016
+ this.querySelector('#tp-chatbot-input-bar').style.display = 'none';
1017
+ this.querySelector('#tp-agent-bar').style.display = 'none';
932
1018
  const color = this.getThemeColor();
933
1019
  const dark = this.shadeColor(color, -20);
934
-
935
1020
  const form_el = document.createElement('div');
936
1021
  form_el.id = 'tp-guest-form';
937
1022
  form_el.innerHTML = `
@@ -940,9 +1025,7 @@
940
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;" />
941
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;" />
942
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;" />
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>
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>
946
1029
  <p id="tp-guest-error" style="margin:0;font-size:12px;color:#c0392b;display:none;"></p>
947
1030
  </div>
948
1031
  `;
@@ -955,7 +1038,6 @@
955
1038
  const company_name = this.querySelector('#tp-guest-company')?.value.trim();
956
1039
  const email = this.querySelector('#tp-guest-email')?.value.trim();
957
1040
  const error_el = this.querySelector('#tp-guest-error');
958
-
959
1041
  if (!first_name || !company_name || !email) {
960
1042
  error_el.textContent = 'Please fill in all fields.';
961
1043
  error_el.style.display = 'block';
@@ -976,57 +1058,86 @@
976
1058
  this.view = 'chat';
977
1059
  this.conversation_id = await this.createConversation();
978
1060
 
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';
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';
985
1068
 
986
1069
  this.updateUILanguage();
987
1070
  this.addMessage('assistant', this.getWelcomeMessage(first_name));
988
1071
  this.last_message_count = 1;
989
1072
 
990
1073
  const theme = getClientTheme(this.client_id);
991
- if (theme.suggestions && theme.suggestions.length > 0) {
992
- this.showSuggestions(theme.suggestions);
993
- }
1074
+ if (theme.suggestions && theme.suggestions.length > 0) this.showSuggestions(theme.suggestions);
994
1075
 
995
1076
  this.polling_interval = setInterval(() => this.pollMessages(), 3000);
996
1077
  }
997
1078
 
998
- // ─── Messages ─────────────────────────────────────────────────────────────
1079
+ // ─── Messages ──────────────────────────────────────────────────────────────
999
1080
 
1000
- addMessage(role, content) {
1001
- 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() });
1002
1083
  const container = this.querySelector('#tp-messages');
1003
1084
 
1004
1085
  const role_label =
1005
1086
  role === 'user'
1006
- ? `👤 ${this.user_info?.first_name || 'You'}`
1087
+ ? `${icon('user', 11, 'margin-right:3px')} ${this.user_info?.first_name || 'You'}`
1007
1088
  : role === 'agent'
1008
- ? '👨‍💼 Agent'
1089
+ ? `${icon('agent', 11, 'margin-right:3px')} Agent`
1009
1090
  : role === 'system'
1010
- ? 'ℹ️ Info'
1011
- : '🤖 Maria';
1091
+ ? 'Info'
1092
+ : `${icon('bot', 11, 'margin-right:3px')} Maria`;
1012
1093
 
1013
1094
  const msg_el = document.createElement('div');
1014
1095
  msg_el.className = `tp-chatbot-message ${role}`;
1015
1096
  const rendered_content =
1016
1097
  role === 'assistant' || role === 'agent' ? (typeof marked !== 'undefined' ? marked.parse(content) : content) : content;
1017
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
+
1018
1107
  msg_el.innerHTML = `
1019
1108
  <div class="tp-chatbot-bubble-msg">
1020
1109
  <div class="tp-chatbot-role">${role_label}</div>
1021
1110
  <div class="tp-chatbot-content tp-chatbot-markdown">${rendered_content}</div>
1111
+ ${feedback_html}
1022
1112
  </div>
1023
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
+
1024
1137
  container.appendChild(msg_el);
1025
1138
  container.scrollTop = container.scrollHeight;
1026
1139
 
1027
- if ((role === 'assistant' || role === 'agent') && this.sound_enabled) {
1028
- playSound();
1029
- }
1140
+ if ((role === 'assistant' || role === 'agent') && this.sound_enabled) playSound();
1030
1141
  }
1031
1142
 
1032
1143
  showTyping() {
@@ -1040,8 +1151,8 @@
1040
1151
  }
1041
1152
 
1042
1153
  hideTyping() {
1043
- const typing = this.querySelector('#tp-typing');
1044
- if (typing) typing.remove();
1154
+ const t = this.querySelector('#tp-typing');
1155
+ if (t) t.remove();
1045
1156
  }
1046
1157
 
1047
1158
  showClosed(existing_csat = null) {
@@ -1053,14 +1164,11 @@
1053
1164
 
1054
1165
  const m = this.getMessages();
1055
1166
  const subtitle = this.querySelector('#tp-subtitle');
1056
- if (subtitle) subtitle.textContent = m.terminated;
1167
+ if (subtitle) subtitle.innerHTML = `${icon('check', 12, 'margin-right:4px')} ${m.terminated}`;
1057
1168
 
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';
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';
1064
1172
  const sugg_el = this.querySelector('#tp-suggestions');
1065
1173
  if (sugg_el) sugg_el.remove();
1066
1174
 
@@ -1078,17 +1186,21 @@
1078
1186
  ? `<div style="font-size:13px;color:#888;margin-bottom:14px;">${existing_csat === 'positive' ? m.csat_positive : m.csat_negative}</div>`
1079
1187
  : `<div style="font-size:12px;color:#888;margin-bottom:10px;">${m.csat_question}</div>
1080
1188
  <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>
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>
1083
1191
  </div>`;
1084
1192
 
1085
1193
  const banner = document.createElement('div');
1086
1194
  banner.id = 'tp-closed-banner';
1087
1195
  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>
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>
1090
1200
  <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>
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>
1092
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>
1093
1205
  </div>
1094
1206
  `;
@@ -1118,7 +1230,7 @@
1118
1230
  this.querySelector('#tp-back-to-list').addEventListener('click', () => this.showConversationList());
1119
1231
  }
1120
1232
 
1121
- // ─── Send / Poll ──────────────────────────────────────────────────────────
1233
+ // ─── Send / Poll ───────────────────────────────────────────────────────────
1122
1234
 
1123
1235
  async sendMessage() {
1124
1236
  if (this.is_closed || !this.conversation_id) return;
@@ -1133,8 +1245,7 @@
1133
1245
  const user_messages = this.messages.filter(msg => msg.role === 'user').length;
1134
1246
  if (user_messages >= 30) {
1135
1247
  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';
1248
+ this.querySelector('#tp-chatbot-input-bar').style.display = 'none';
1138
1249
  return;
1139
1250
  }
1140
1251
 
@@ -1149,7 +1260,6 @@
1149
1260
  headers: this.getHeaders(true),
1150
1261
  body: JSON.stringify({ query, user_id: this.user_id, user_info: this.user_info }),
1151
1262
  });
1152
-
1153
1263
  const data = await response.json();
1154
1264
  this.hideTyping();
1155
1265
 
@@ -1161,19 +1271,24 @@
1161
1271
 
1162
1272
  this.agent_mode = data.result.agent_mode;
1163
1273
  const source = data.result.source || null;
1164
-
1165
1274
  const subtitle = this.querySelector('#tp-subtitle');
1166
- 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}`;
1167
1279
 
1168
1280
  if (data.result.reply) {
1169
- this.addMessage(this.agent_mode ? 'agent' : 'assistant', data.result.reply);
1170
- 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;
1171
1283
  }
1172
1284
 
1173
1285
  const user_msg_count = this.messages.filter(msg => msg.role === 'user').length;
1174
1286
  const agent_bar = this.querySelector('#tp-agent-bar');
1287
+ const suggest_agent = data.result.suggest_agent || false;
1288
+
1175
1289
  if (agent_bar && !this.agent_requested && !this.agent_mode) {
1176
1290
  if (
1291
+ suggest_agent ||
1177
1292
  source === 'fallback' ||
1178
1293
  source === 'clarification' ||
1179
1294
  source === 'no_match' ||
@@ -1183,9 +1298,14 @@
1183
1298
  agent_bar.style.display = 'block';
1184
1299
  }
1185
1300
  }
1301
+
1302
+ if (suggest_agent && !this.agent_requested) {
1303
+ this.addMessage('system', m.suggest_agent_escalation);
1304
+ this.last_message_count += 1;
1305
+ }
1186
1306
  } catch {
1187
1307
  this.hideTyping();
1188
- this.addMessage('assistant', m.error_occurred);
1308
+ this.addMessage('assistant', this.getMessages().error_occurred);
1189
1309
  }
1190
1310
 
1191
1311
  this.is_loading = false;
@@ -1208,17 +1328,11 @@
1208
1328
  const status = data.result?.status;
1209
1329
 
1210
1330
  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
- }
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;
1222
1336
 
1223
1337
  if (msg) {
1224
1338
  this.addMessage('system', msg);
@@ -1231,6 +1345,7 @@
1231
1345
  }
1232
1346
  } catch {
1233
1347
  this.agent_requested = false;
1348
+ const agent_bar = this.querySelector('#tp-agent-bar');
1234
1349
  if (agent_bar) agent_bar.style.display = 'block';
1235
1350
  }
1236
1351
  }
@@ -1255,7 +1370,6 @@
1255
1370
  const url = `${this.api_url}/chat/conversations/${this.conversation_id}?user_id=${encodeURIComponent(this.user_id)}`;
1256
1371
  const response = await fetch(url, { headers: this.getHeaders() });
1257
1372
  if (!response.ok) return;
1258
-
1259
1373
  const data = await response.json();
1260
1374
  const conv = data.result?.conversation;
1261
1375
  if (!conv) return;
@@ -1276,7 +1390,7 @@
1276
1390
  const typing = document.createElement('div');
1277
1391
  typing.className = 'tp-chatbot-message agent';
1278
1392
  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>`;
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>`;
1280
1394
  container.appendChild(typing);
1281
1395
  container.scrollTop = container.scrollHeight;
1282
1396
  }
@@ -1291,9 +1405,9 @@
1291
1405
  if (msg.role === 'agent') {
1292
1406
  const typing_el = this.querySelector('#tp-agent-typing');
1293
1407
  if (typing_el) typing_el.remove();
1294
- this.addMessage('agent', msg.content);
1408
+ this.addMessage('agent', msg.content, msg.message_id || null);
1295
1409
  } else if (msg.role === 'assistant') {
1296
- this.addMessage('assistant', msg.content);
1410
+ this.addMessage('assistant', msg.content, msg.message_id || null);
1297
1411
  } else if (msg.role === 'system') {
1298
1412
  this.addMessage('system', msg.content);
1299
1413
  }
@@ -1304,7 +1418,7 @@
1304
1418
  if (server_status === 'agent' && !this.agent_mode) {
1305
1419
  this.agent_mode = true;
1306
1420
  const subtitle = this.querySelector('#tp-subtitle');
1307
- if (subtitle) subtitle.textContent = m.human_agent;
1421
+ if (subtitle) subtitle.innerHTML = `${icon('user', 12, 'margin-right:4px')} ${m.human_agent}`;
1308
1422
  const agent_bar = this.querySelector('#tp-agent-bar');
1309
1423
  if (agent_bar) agent_bar.style.display = 'none';
1310
1424
  this.addMessage('system', m.agent_taken);
@@ -1313,7 +1427,7 @@
1313
1427
  this.agent_mode = false;
1314
1428
  this.agent_requested = false;
1315
1429
  const subtitle = this.querySelector('#tp-subtitle');
1316
- if (subtitle) subtitle.textContent = m.virtual;
1430
+ if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
1317
1431
  const agent_bar = this.querySelector('#tp-agent-bar');
1318
1432
  if (agent_bar) agent_bar.style.display = 'block';
1319
1433
  this.addMessage('system', m.agent_released);