@developpement/tp-chatbot-widget 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/chatbot.js CHANGED
@@ -1,9 +1,6 @@
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
4
  const ICONS = {
8
5
  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
6
  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>`,
@@ -15,12 +12,13 @@
15
12
  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
13
  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
14
  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
15
  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
16
  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
17
  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
18
  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
19
  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>`,
20
+ history: `<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 0 7-7 7 7 0 0 0-5 2.1L3 7"/><path d="M3 3v4h4"/><path d="M10 7v4l2.5 2.5"/></svg>`,
21
+ lock: `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="9" width="12" height="9" rx="2"/><path d="M7 9V6a3 3 0 0 1 6 0v3"/></svg>`,
24
22
  };
25
23
 
26
24
  function icon(name, size = 18, extra_style = '') {
@@ -32,7 +30,114 @@
32
30
  );
33
31
  }
34
32
 
35
- // ─── Clients ─────────────────────────────────────────────────────────────────
33
+ function injectCSS() {
34
+ if (document.getElementById('tp-chatbot-base-css')) return;
35
+ const style = document.createElement('style');
36
+ style.id = 'tp-chatbot-base-css';
37
+ style.textContent = `
38
+ tp-chatbot * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
39
+ .tp-chatbot-host { position: fixed; bottom: 24px; z-index: 9999; display: flex; flex-direction: column; align-items: flex-end; width: 380px; }
40
+ .tp-chatbot-window { position: absolute; bottom: 72px; left: 0; right: auto; width: 380px; height: 580px; background: #fff; border-radius: 20px; box-shadow: 0 8px 40px rgba(0,0,0,0.14); display: flex; flex-direction: column; overflow: hidden; opacity: 0; transform: scale(0.92) translateY(12px); pointer-events: none; transition: opacity 0.22s ease, transform 0.22s ease; }
41
+ .tp-chatbot-window.open { opacity: 1; transform: scale(1) translateY(0); pointer-events: all; }
42
+ .tp-chatbot-bubble { width: 60px; height: 60px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: transform 0.18s, box-shadow 0.18s; margin-left: auto; flex-shrink: 0; }
43
+ .tp-chatbot-bubble:hover { transform: scale(1.07); }
44
+ .tp-header { padding: 14px 16px 0; color: white; }
45
+ .tp-header-top { display: flex; align-items: center; gap: 10px; padding-bottom: 12px; }
46
+ .tp-header-avatar { width: 38px; height: 38px; border-radius: 50%; background: rgba(255,255,255,0.25); display: flex; align-items: center; justify-content: center; font-size: 15px; font-weight: 700; color: white; position: relative; flex-shrink: 0; }
47
+ .tp-header-dot { position: absolute; bottom: 1px; right: 1px; width: 9px; height: 9px; border-radius: 50%; background: #22c55e; border: 2px solid white; }
48
+ .tp-header-info { flex: 1; min-width: 0; }
49
+ .tp-header-title { font-size: 14px; font-weight: 700; color: white; line-height: 1.2; }
50
+ .tp-header-subtitle { font-size: 11px; color: rgba(255,255,255,0.8); display: flex; align-items: center; gap: 4px; margin-top: 2px; }
51
+ .tp-header-btn { background: rgba(255,255,255,0.18); border: none; border-radius: 8px; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: white; transition: background 0.15s; flex-shrink: 0; }
52
+ .tp-header-btn:hover { background: rgba(255,255,255,0.32); }
53
+ .tp-header-controls { display: flex; align-items: center; gap: 6px; }
54
+ .tp-tabs { display: flex; gap: 0; padding: 0 16px; border-bottom: 1px solid rgba(255,255,255,0.2); margin-top: 2px; justify-content: center; }
55
+ .tp-tab { padding: 8px 14px; font-size: 12px; font-weight: 600; color: rgba(255,255,255,0.65); border: none; background: none; cursor: pointer; border-bottom: 2px solid transparent; transition: color 0.15s, border-color 0.15s; display: flex; align-items: center; gap: 5px; margin-bottom: -1px; }
56
+ .tp-tab.active { color: white; border-bottom-color: white; }
57
+ .tp-body { flex: 1; overflow-y: auto; background: #f7f7f8; display: flex; flex-direction: column; }
58
+ .tp-body::-webkit-scrollbar { width: 4px; }
59
+ .tp-body::-webkit-scrollbar-thumb { background: #ddd; border-radius: 2px; }
60
+ .tp-messages-wrap { flex: 1; overflow-y: auto; padding: 16px 14px; display: flex; flex-direction: column; gap: 10px; background: #f7f7f8; }
61
+ .tp-messages-wrap::-webkit-scrollbar { width: 4px; }
62
+ .tp-messages-wrap::-webkit-scrollbar-thumb { background: #ddd; border-radius: 2px; }
63
+ .tp-msg { display: flex; flex-direction: column; max-width: 82%; }
64
+ .tp-msg.user { align-self: flex-end; align-items: flex-end; }
65
+ .tp-msg.assistant, .tp-msg.agent, .tp-msg.system { align-self: flex-start; align-items: flex-start; }
66
+ .tp-msg-role { font-size: 10px; color: #aaa; margin-bottom: 3px; display: flex; align-items: center; gap: 3px; }
67
+ .tp-msg.user .tp-msg-role { color: #aaa; }
68
+ .tp-bubble-msg { padding: 10px 13px; border-radius: 16px; font-size: 13px; line-height: 1.55; word-break: break-word; }
69
+ .tp-msg.user .tp-bubble-msg { color: white; border-bottom-right-radius: 4px; }
70
+ .tp-msg.assistant .tp-bubble-msg, .tp-msg.agent .tp-bubble-msg { background: white; color: #1a1a2e; border-bottom-left-radius: 4px; box-shadow: 0 1px 4px rgba(0,0,0,0.07); }
71
+ .tp-msg.system .tp-bubble-msg { background: #e8f5e9; color: #2e7d32; font-size: 12px; border-radius: 10px; padding: 8px 12px; }
72
+ .tp-msg-time { font-size: 10px; color: #bbb; margin-top: 3px; }
73
+ .tp-md p { margin-bottom: 6px; }
74
+ .tp-md p:last-child { margin-bottom: 0; }
75
+ .tp-md ul, .tp-md ol { padding-left: 18px; margin-bottom: 6px; }
76
+ .tp-md li { margin-bottom: 2px; }
77
+ .tp-md strong { font-weight: 600; }
78
+ .tp-md code { background: #f0f0f0; border-radius: 4px; padding: 1px 5px; font-size: 12px; }
79
+ .tp-md pre { background: #f0f0f0; border-radius: 8px; padding: 10px; overflow-x: auto; margin-bottom: 6px; }
80
+ .tp-feedback-bar { display: flex; gap: 6px; margin-top: 7px; }
81
+ .tp-feedback-btn { background: none; border: 1px solid #e5e5e5; border-radius: 8px; padding: 4px 8px; cursor: pointer; color: #aaa; display: flex; align-items: center; transition: all 0.15s; font-size: 11px; }
82
+ .tp-feedback-btn:hover { border-color: #bbb; color: #666; }
83
+ .tp-feedback-btn.voted-positive { border-color: #22c55e; color: #22c55e; background: #f0fdf4; }
84
+ .tp-feedback-btn.voted-negative { border-color: #ef4444; color: #ef4444; background: #fef2f2; }
85
+ .tp-typing { display: flex; align-items: center; gap: 4px; padding: 4px 2px; }
86
+ .tp-typing span { width: 7px; height: 7px; border-radius: 50%; background: #ccc; animation: tp-bounce 1.2s infinite ease-in-out; }
87
+ .tp-typing span:nth-child(2) { animation-delay: 0.18s; }
88
+ .tp-typing span:nth-child(3) { animation-delay: 0.36s; }
89
+ @keyframes tp-bounce { 0%,60%,100%{transform:translateY(0)} 30%{transform:translateY(-5px)} }
90
+ .tp-agent-bar { padding: 8px 14px; border-top: 1px solid #f0f0f0; background: white; }
91
+ .tp-agent-btn { width: 100%; padding: 9px 14px; border-radius: 10px; border: 1.5px solid; background: white; font-size: 12px; font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 7px; transition: all 0.15s; font-family: inherit; }
92
+ .tp-agent-btn:hover { color: white !important; }
93
+ .tp-close-bar { padding: 8px 14px; border-top: 1px solid #f0f0f0; background: white; text-align: center; }
94
+ .tp-close-btn { background: none; border: none; font-size: 12px; color: #aaa; cursor: pointer; display: inline-flex; align-items: center; gap: 5px; font-family: inherit; padding: 4px 8px; border-radius: 8px; transition: background 0.15s; }
95
+ .tp-close-btn:hover { background: #f5f5f5; color: #888; }
96
+ .tp-input-bar { display: flex; align-items: flex-end; gap: 8px; padding: 10px 12px; border-top: 1px solid #f0f0f0; background: white; }
97
+ .tp-input { flex: 1; padding: 9px 12px; border: 1.5px solid #e5e5e5; border-radius: 12px; font-size: 13px; font-family: inherit; resize: none; outline: none; max-height: 100px; line-height: 1.4; transition: border-color 0.15s; background: #fafafa; }
98
+ .tp-input:focus { border-color: inherit; background: white; }
99
+ .tp-send { width: 36px; height: 36px; border-radius: 10px; border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; transition: transform 0.15s; }
100
+ .tp-send:hover { transform: scale(1.06); }
101
+ .tp-conv-list { display: flex; flex-direction: column; flex: 1; }
102
+ .tp-conv-item { padding: 13px 16px; border-bottom: 1px solid #f2f2f2; cursor: pointer; transition: background 0.13s; display: flex; justify-content: space-between; align-items: center; background: white; }
103
+ .tp-conv-item:hover { background: #fafafa; }
104
+ .tp-conv-item-left { display: flex; flex-direction: column; gap: 3px; }
105
+ .tp-conv-item-date { font-size: 12px; font-weight: 600; color: #1a1a2e; }
106
+ .tp-conv-item-count { font-size: 11px; color: #bbb; }
107
+ .tp-badge { font-size: 10px; font-weight: 700; padding: 3px 9px; border-radius: 20px; display: inline-flex; align-items: center; gap: 3px; }
108
+ .tp-new-conv-btn { width: 100%; padding: 11px; color: white; border: none; border-radius: 12px; font-size: 13px; font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; font-family: inherit; transition: opacity 0.15s; }
109
+ .tp-new-conv-btn:hover { opacity: 0.9; }
110
+ .tp-guest-wrap { padding: 16px; display: flex; flex-direction: column; gap: 10px; flex: 1; }
111
+ .tp-guest-intro { background: white; border-radius: 14px; padding: 14px; box-shadow: 0 1px 6px rgba(0,0,0,0.07); }
112
+ .tp-guest-intro-name { font-size: 13px; font-weight: 700; color: #1a1a2e; margin-bottom: 4px; display: flex; align-items: center; gap: 6px; }
113
+ .tp-guest-intro-sub { font-size: 12px; color: #888; line-height: 1.5; }
114
+ .tp-guest-field-label { font-size: 11px; font-weight: 600; color: #666; margin-bottom: 4px; display: block; }
115
+ .tp-guest-input { width: 100%; padding: 10px 13px; border: 1.5px solid #e8e8e8; border-radius: 10px; font-size: 13px; font-family: inherit; outline: none; background: white; transition: border-color 0.15s; }
116
+ .tp-guest-input:focus { border-color: inherit; }
117
+ .tp-guest-submit { width: 100%; padding: 11px; color: white; border: none; border-radius: 12px; font-size: 13px; font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; font-family: inherit; margin-top: 2px; transition: opacity 0.15s; }
118
+ .tp-guest-submit:hover { opacity: 0.9; }
119
+ .tp-guest-footer { text-align: center; font-size: 10px; color: #ccc; display: flex; align-items: center; justify-content: center; gap: 4px; }
120
+ .tp-guest-error { font-size: 12px; color: #ef4444; display: none; text-align: center; }
121
+ .tp-closed-banner { margin: 12px; padding: 16px; background: white; border: 1px solid #f0f0f0; border-radius: 16px; text-align: center; box-shadow: 0 1px 6px rgba(0,0,0,0.06); }
122
+ .tp-closed-title { font-size: 13px; font-weight: 700; color: #1a1a2e; margin-bottom: 12px; display: flex; align-items: center; justify-content: center; gap: 6px; }
123
+ .tp-closed-date { font-size: 11px; color: #aaa; margin-top: -8px; margin-bottom: 12px; }
124
+ .tp-csat-q { font-size: 12px; color: #888; margin-bottom: 10px; }
125
+ .tp-csat-btns { display: flex; gap: 10px; justify-content: center; margin-bottom: 14px; }
126
+ .tp-csat-btn { flex: 1; padding: 10px; border-radius: 10px; border: 1.5px solid; background: white; cursor: pointer; font-size: 13px; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 6px; transition: all 0.15s; font-family: inherit; }
127
+ .tp-csat-btn.positive { border-color: #22c55e; color: #22c55e; }
128
+ .tp-csat-btn.positive:hover { background: #22c55e; color: white; }
129
+ .tp-csat-btn.negative { border-color: #ef4444; color: #ef4444; }
130
+ .tp-csat-btn.negative:hover { background: #ef4444; color: white; }
131
+ .tp-csat-thanks { font-size: 13px; color: #888; margin-bottom: 14px; }
132
+ .tp-closed-new-btn { width: 100%; padding: 11px; 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; font-family: inherit; margin-bottom: 8px; transition: opacity 0.15s; }
133
+ .tp-closed-new-btn:hover { opacity: 0.9; }
134
+ .tp-closed-hist-btn { width: 100%; padding: 9px; background: none; border: 1px solid #e8e8e8; border-radius: 10px; font-size: 12px; font-weight: 600; cursor: pointer; font-family: inherit; color: #888; transition: background 0.15s; }
135
+ .tp-closed-hist-btn:hover { background: #f5f5f5; }
136
+ .tp-suggestions { display: flex; flex-direction: column; gap: 6px; padding: 4px 0 6px; }
137
+ .tp-suggestion-btn { padding: 8px 14px; border-radius: 20px; border: 1.5px solid; background: white; font-size: 12px; font-weight: 600; cursor: pointer; text-align: left; font-family: inherit; transition: all 0.12s; }
138
+ `;
139
+ document.head.appendChild(style);
140
+ }
36
141
 
37
142
  const CLIENT_THEMES = {
38
143
  flix: {
@@ -116,8 +221,8 @@
116
221
  };
117
222
 
118
223
  const DEFAULT_THEME = { primary: '#7b1fa2', name: 'Support', position: 'right', suggestions: [] };
119
- const WINDOW_WIDTH = '380px';
120
- const WINDOW_HEIGHT = '580px';
224
+ const WINDOW_WIDTH = '340px';
225
+ const WINDOW_HEIGHT = '480px';
121
226
 
122
227
  function getClientTheme(client_id) {
123
228
  return CLIENT_THEMES[client_id] || DEFAULT_THEME;
@@ -143,8 +248,8 @@
143
248
  function playSound() {
144
249
  try {
145
250
  const ctx = new (window.AudioContext || window.webkitAudioContext)();
146
- const osc = ctx.createOscillator();
147
- const gain = ctx.createGain();
251
+ const osc = ctx.createOscillator(),
252
+ gain = ctx.createGain();
148
253
  osc.connect(gain);
149
254
  gain.connect(ctx.destination);
150
255
  osc.frequency.value = 880;
@@ -156,7 +261,9 @@
156
261
  } catch {}
157
262
  }
158
263
 
159
- // ─── Component ───────────────────────────────────────────────────────────────
264
+ function formatTime(iso) {
265
+ return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
266
+ }
160
267
 
161
268
  class TpChatbot extends HTMLElement {
162
269
  constructor() {
@@ -179,6 +286,7 @@
179
286
  this.access_token = '';
180
287
  this.is_fullscreen = false;
181
288
  this.is_minimized = false;
289
+ this.active_tab = 'new';
182
290
  }
183
291
 
184
292
  static get observedAttributes() {
@@ -195,9 +303,9 @@
195
303
  this.user_info = extractUserInfo(new_val);
196
304
  this.user_id = this.user_info?.user_id;
197
305
  this.applyTheme();
198
- const guest_form = this.querySelector('#tp-guest-form');
199
- if (guest_form) {
200
- guest_form.remove();
306
+ const guest_wrap = this.querySelector('#tp-guest-wrap');
307
+ if (guest_wrap) {
308
+ guest_wrap.remove();
201
309
  this.showConversationList();
202
310
  }
203
311
  }
@@ -209,7 +317,6 @@
209
317
  if (!response.ok) return;
210
318
  const data = await response.json();
211
319
  const t = data.result?.result;
212
- console.log('[fetchTheme] open_by_default:', t.open_by_default, typeof t.open_by_default);
213
320
  if (!t) return;
214
321
  if (CLIENT_THEMES[this.client_id]) {
215
322
  if (t.primary) CLIENT_THEMES[this.client_id].primary = t.primary;
@@ -225,7 +332,7 @@
225
332
  };
226
333
  }
227
334
  } catch (e) {
228
- console.warn('[tp-chatbot] fetchTheme failed, using defaults:', e.message);
335
+ console.warn('[tp-chatbot] fetchTheme failed:', e.message);
229
336
  }
230
337
  }
231
338
 
@@ -242,6 +349,9 @@
242
349
  this.view = 'list';
243
350
  this.is_fullscreen = false;
244
351
  this.is_minimized = false;
352
+ this.active_tab = 'new';
353
+
354
+ injectCSS();
245
355
 
246
356
  this.fetchTheme().then(() => {
247
357
  this.render();
@@ -250,11 +360,7 @@
250
360
  this._initialized = true;
251
361
 
252
362
  const theme = getClientTheme(this.client_id);
253
- console.log('[widget] theme after fetch:', theme);
254
-
255
- if (theme.open_by_default) {
256
- setTimeout(() => this.toggleChat(), 300);
257
- }
363
+ if (theme.open_by_default) setTimeout(() => this.toggleChat(), 300);
258
364
 
259
365
  if (this.access_token) {
260
366
  this.user_info = extractUserInfo(this.access_token);
@@ -280,8 +386,6 @@
280
386
  if (this.polling_interval) clearInterval(this.polling_interval);
281
387
  }
282
388
 
283
- // ─── i18n ──────────────────────────────────────────────────────────────────
284
-
285
389
  getMessages() {
286
390
  const lang = (this.user_info?.language || 'EN').toUpperCase();
287
391
  const msgs = {
@@ -307,6 +411,8 @@
307
411
  close_btn: 'Terminer la conversation',
308
412
  speak_agent: 'Parler à un agent',
309
413
  write_msg: 'Écrivez votre message...',
414
+ tab_new: 'Nouveau',
415
+ tab_history: 'Historique',
310
416
  limit_reached: 'Vous avez atteint la limite de 30 messages. Veuillez contacter un agent ou écrire à support@travelplanet.com.',
311
417
  too_many: 'Trop de messages. Veuillez patienter avant de réessayer.',
312
418
  error_occurred: 'Une erreur est survenue. Veuillez réessayer.',
@@ -321,6 +427,17 @@
321
427
  status_waiting: 'En attente',
322
428
  status_bot: 'Bot',
323
429
  messages: 'message',
430
+ guest_title: 'Bonjour ! Je suis Maria 👋',
431
+ guest_sub: "Identifiez-vous pour démarrer. Réponse en moins d'une minute.",
432
+ guest_firstname: 'Prénom *',
433
+ guest_company: 'Entreprise *',
434
+ guest_email: 'Email *',
435
+ guest_submit: 'Démarrer la conversation',
436
+ guest_secured: 'Données sécurisées · Propulsé par Makitizy',
437
+ guest_fill: 'Veuillez remplir tous les champs.',
438
+ guest_email_invalid: 'Veuillez entrer un email valide.',
439
+ helpful: 'Utile',
440
+ not_helpful: 'Pas utile',
324
441
  },
325
442
  EN: {
326
443
  closed: 'Conversation closed',
@@ -344,6 +461,8 @@
344
461
  close_btn: 'End conversation',
345
462
  speak_agent: 'Talk to an agent',
346
463
  write_msg: 'Write your message...',
464
+ tab_new: 'New',
465
+ tab_history: 'History',
347
466
  limit_reached: 'You have reached the 30 message limit. Please contact an agent or write to support@travelplanet.com.',
348
467
  too_many: 'Too many messages. Please wait before retrying.',
349
468
  error_occurred: 'An error occurred. Please try again.',
@@ -358,6 +477,17 @@
358
477
  status_waiting: 'Waiting',
359
478
  status_bot: 'Bot',
360
479
  messages: 'message',
480
+ guest_title: "Hello! I'm Maria 👋",
481
+ guest_sub: 'Identify yourself to start. Answer in less than a minute.',
482
+ guest_firstname: 'First name *',
483
+ guest_company: 'Company *',
484
+ guest_email: 'Email *',
485
+ guest_submit: 'Start conversation',
486
+ guest_secured: 'Secured data · Powered by Makitizy',
487
+ guest_fill: 'Please fill in all fields.',
488
+ guest_email_invalid: 'Please enter a valid email.',
489
+ helpful: 'Helpful',
490
+ not_helpful: 'Not helpful',
361
491
  },
362
492
  DE: {
363
493
  closed: 'Gespräch beendet',
@@ -366,8 +496,8 @@
366
496
  csat_negative: 'Danke, wir werden uns verbessern.',
367
497
  new_conv: 'Neues Gespräch',
368
498
  back_to_list: 'Zurück zur Liste',
369
- agent_taken: 'Ein Agent hat Ihr Gespräch übernommen. Sie können ihm jetzt direkt schreiben.',
370
- agent_released: 'Unser virtueller Assistent ist zurück. Wie kann ich Ihnen helfen?',
499
+ agent_taken: 'Ein Agent hat Ihr Gespräch übernommen.',
500
+ agent_released: 'Unser virtueller Assistent ist zurück.',
371
501
  close_confirm: 'Möchten Sie das Gespräch beenden?',
372
502
  terminated: 'Gespräch beendet',
373
503
  virtual: 'Virtueller Assistent',
@@ -381,13 +511,15 @@
381
511
  close_btn: 'Gespräch beenden',
382
512
  speak_agent: 'Mit einem Agenten sprechen',
383
513
  write_msg: 'Schreiben Sie Ihre Nachricht...',
514
+ tab_new: 'Neu',
515
+ tab_history: 'Verlauf',
384
516
  limit_reached: 'Sie haben das Limit von 30 Nachrichten erreicht.',
385
517
  too_many: 'Zu viele Nachrichten. Bitte warten Sie.',
386
- error_occurred: 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.',
518
+ error_occurred: 'Ein Fehler ist aufgetreten.',
387
519
  outside_hours: next => `Unser Support ist derzeit geschlossen. Nächster Termin: ${next}.`,
388
- no_agents: 'Derzeit kein Agent verfügbar. Bitte versuchen Sie es später.',
389
- waiting_agent: 'Ihre Anfrage wurde übermittelt. Ein Agent übernimmt gleich.',
390
- waiting_agent_already: 'Sie sind bereits in der Warteschlange. Bitte warten.',
520
+ no_agents: 'Derzeit kein Agent verfügbar.',
521
+ waiting_agent: 'Ihre Anfrage wurde übermittelt.',
522
+ waiting_agent_already: 'Sie sind bereits in der Warteschlange.',
391
523
  agent_already: 'Ein Agent betreut bereits Ihr Gespräch.',
392
524
  suggest_agent_escalation: 'Ich kann keine passende Antwort finden. Möchten Sie mit einem Agenten sprechen?',
393
525
  status_closed: 'Geschlossen',
@@ -395,6 +527,17 @@
395
527
  status_waiting: 'Wartend',
396
528
  status_bot: 'Bot',
397
529
  messages: 'Nachricht',
530
+ guest_title: 'Hallo! Ich bin Maria 👋',
531
+ guest_sub: 'Identifizieren Sie sich, um zu starten.',
532
+ guest_firstname: 'Vorname *',
533
+ guest_company: 'Unternehmen *',
534
+ guest_email: 'E-Mail *',
535
+ guest_submit: 'Gespräch starten',
536
+ guest_secured: 'Gesicherte Daten · Powered by Makitizy',
537
+ guest_fill: 'Bitte füllen Sie alle Felder aus.',
538
+ guest_email_invalid: 'Bitte geben Sie eine gültige E-Mail ein.',
539
+ helpful: 'Hilfreich',
540
+ not_helpful: 'Nicht hilfreich',
398
541
  },
399
542
  ES: {
400
543
  closed: 'Conversación cerrada',
@@ -403,14 +546,14 @@
403
546
  csat_negative: 'Gracias, mejoraremos.',
404
547
  new_conv: 'Nueva conversación',
405
548
  back_to_list: 'Volver a las conversaciones',
406
- agent_taken: 'Un agente ha tomado su conversación. Ahora puede escribirle directamente.',
407
- agent_released: 'Nuestro asistente virtual ha vuelto. ¿En qué puedo ayudarle?',
549
+ agent_taken: 'Un agente ha tomado su conversación.',
550
+ agent_released: 'Nuestro asistente virtual ha vuelto.',
408
551
  close_confirm: '¿Desea finalizar esta conversación?',
409
552
  terminated: 'Conversación terminada',
410
553
  virtual: 'Asistente virtual',
411
554
  human_agent: 'Agente humano',
412
555
  loading: 'Cargando...',
413
- no_conv: 'No hay conversaciones por el momento.',
556
+ no_conv: 'No hay conversaciones.',
414
557
  new_conv_btn: 'Nueva conversación',
415
558
  error_load: 'Error de carga.',
416
559
  recent_conv: 'Sus conversaciones recientes',
@@ -418,20 +561,33 @@
418
561
  close_btn: 'Finalizar conversación',
419
562
  speak_agent: 'Hablar con un agente',
420
563
  write_msg: 'Escriba su mensaje...',
564
+ tab_new: 'Nuevo',
565
+ tab_history: 'Historial',
421
566
  limit_reached: 'Ha alcanzado el límite de 30 mensajes.',
422
- too_many: 'Demasiados mensajes. Por favor espere.',
423
- error_occurred: 'Se produjo un error. Por favor, inténtelo de nuevo.',
567
+ too_many: 'Demasiados mensajes.',
568
+ error_occurred: 'Se produjo un error.',
424
569
  outside_hours: next => `Nuestro soporte está cerrado. Próximo horario: ${next}.`,
425
- no_agents: 'No hay agentes disponibles. Inténtelo más tarde.',
426
- waiting_agent: 'Su solicitud ha sido enviada. Un agente le atenderá pronto.',
427
- waiting_agent_already: 'Ya está en la cola de espera. Por favor, espere.',
570
+ no_agents: 'No hay agentes disponibles.',
571
+ waiting_agent: 'Su solicitud ha sido enviada.',
572
+ waiting_agent_already: 'Ya está en la cola.',
428
573
  agent_already: 'Un agente ya está gestionando su conversación.',
429
- suggest_agent_escalation: 'No encuentro una respuesta adecuada. ¿Desea hablar con un agente?',
574
+ suggest_agent_escalation: '¿Desea hablar con un agente?',
430
575
  status_closed: 'Cerrado',
431
576
  status_agent: 'Agente',
432
577
  status_waiting: 'Esperando',
433
578
  status_bot: 'Bot',
434
579
  messages: 'mensaje',
580
+ guest_title: '¡Hola! Soy Maria 👋',
581
+ guest_sub: 'Identifíquese para comenzar.',
582
+ guest_firstname: 'Nombre *',
583
+ guest_company: 'Empresa *',
584
+ guest_email: 'Email *',
585
+ guest_submit: 'Iniciar conversación',
586
+ guest_secured: 'Datos seguros · Powered by Makitizy',
587
+ guest_fill: 'Complete todos los campos.',
588
+ guest_email_invalid: 'Email inválido.',
589
+ helpful: 'Útil',
590
+ not_helpful: 'No útil',
435
591
  },
436
592
  IT: {
437
593
  closed: 'Conversazione chiusa',
@@ -441,13 +597,13 @@
441
597
  new_conv: 'Nuova conversazione',
442
598
  back_to_list: 'Torna alle conversazioni',
443
599
  agent_taken: 'Un agente ha preso in carico la sua conversazione.',
444
- agent_released: 'Il nostro assistente virtuale è tornato. Come posso aiutarla?',
600
+ agent_released: 'Il nostro assistente virtuale è tornato.',
445
601
  close_confirm: 'Vuole terminare questa conversazione?',
446
602
  terminated: 'Conversazione terminata',
447
603
  virtual: 'Assistente virtuale',
448
604
  human_agent: 'Agente umano',
449
605
  loading: 'Caricamento...',
450
- no_conv: 'Nessuna conversazione per il momento.',
606
+ no_conv: 'Nessuna conversazione.',
451
607
  new_conv_btn: 'Nuova conversazione',
452
608
  error_load: 'Errore di caricamento.',
453
609
  recent_conv: 'Le sue conversazioni recenti',
@@ -455,20 +611,33 @@
455
611
  close_btn: 'Termina conversazione',
456
612
  speak_agent: 'Parla con un agente',
457
613
  write_msg: 'Scrivi il tuo messaggio...',
614
+ tab_new: 'Nuovo',
615
+ tab_history: 'Cronologia',
458
616
  limit_reached: 'Hai raggiunto il limite di 30 messaggi.',
459
- too_many: 'Troppi messaggi. Attendere prego.',
460
- error_occurred: 'Si è verificato un errore. Riprova.',
617
+ too_many: 'Troppi messaggi.',
618
+ error_occurred: 'Si è verificato un errore.',
461
619
  outside_hours: next => `Il supporto è chiuso. Prossima disponibilità: ${next}.`,
462
- no_agents: 'Nessun agente disponibile. Riprova più tardi.',
463
- waiting_agent: 'Richiesta inviata. Un agente prenderà in carico la conversazione.',
464
- waiting_agent_already: 'È già in coda. Attenda.',
620
+ no_agents: 'Nessun agente disponibile.',
621
+ waiting_agent: 'Richiesta inviata.',
622
+ waiting_agent_already: 'È già in coda.',
465
623
  agent_already: 'Un agente sta già gestendo la conversazione.',
466
- suggest_agent_escalation: 'Non riesco a trovare una risposta adeguata. Desidera parlare con un agente?',
624
+ suggest_agent_escalation: 'Desidera parlare con un agente?',
467
625
  status_closed: 'Chiuso',
468
626
  status_agent: 'Agente',
469
627
  status_waiting: 'Attesa',
470
628
  status_bot: 'Bot',
471
629
  messages: 'messaggio',
630
+ guest_title: 'Ciao! Sono Maria 👋',
631
+ guest_sub: 'Si identifichi per iniziare.',
632
+ guest_firstname: 'Nome *',
633
+ guest_company: 'Azienda *',
634
+ guest_email: 'Email *',
635
+ guest_submit: 'Inizia conversazione',
636
+ guest_secured: 'Dati sicuri · Powered by Makitizy',
637
+ guest_fill: 'Compila tutti i campi.',
638
+ guest_email_invalid: 'Email non valida.',
639
+ helpful: 'Utile',
640
+ not_helpful: 'Non utile',
472
641
  },
473
642
  NL: {
474
643
  closed: 'Gesprek gesloten',
@@ -478,13 +647,13 @@
478
647
  new_conv: 'Nieuw gesprek',
479
648
  back_to_list: 'Terug naar gesprekken',
480
649
  agent_taken: 'Een agent heeft uw gesprek overgenomen.',
481
- agent_released: 'Onze virtuele assistent is terug. Hoe kan ik u helpen?',
650
+ agent_released: 'Onze virtuele assistent is terug.',
482
651
  close_confirm: 'Wilt u dit gesprek beëindigen?',
483
652
  terminated: 'Gesprek beëindigd',
484
653
  virtual: 'Virtuele assistent',
485
654
  human_agent: 'Menselijke agent',
486
655
  loading: 'Laden...',
487
- no_conv: 'Geen gesprekken op dit moment.',
656
+ no_conv: 'Geen gesprekken.',
488
657
  new_conv_btn: 'Nieuw gesprek',
489
658
  error_load: 'Laadout.',
490
659
  recent_conv: 'Uw recente gesprekken',
@@ -492,20 +661,33 @@
492
661
  close_btn: 'Gesprek beëindigen',
493
662
  speak_agent: 'Praat met een agent',
494
663
  write_msg: 'Schrijf uw bericht...',
664
+ tab_new: 'Nieuw',
665
+ tab_history: 'Geschiedenis',
495
666
  limit_reached: 'U heeft de limiet van 30 berichten bereikt.',
496
- too_many: 'Te veel berichten. Wacht even.',
497
- error_occurred: 'Er is een fout opgetreden. Probeer het opnieuw.',
498
- outside_hours: next => `Onze support is gesloten. Volgende beschikbare tijd: ${next}.`,
499
- no_agents: 'Geen agent beschikbaar. Probeer het later opnieuw.',
500
- waiting_agent: 'Uw verzoek is verzonden. Een agent neemt het gesprek over.',
501
- waiting_agent_already: 'U staat al in de wachtrij. Even geduld.',
667
+ too_many: 'Te veel berichten.',
668
+ error_occurred: 'Er is een fout opgetreden.',
669
+ outside_hours: next => `Support is gesloten. Volgende tijd: ${next}.`,
670
+ no_agents: 'Geen agent beschikbaar.',
671
+ waiting_agent: 'Uw verzoek is verzonden.',
672
+ waiting_agent_already: 'U staat al in de wachtrij.',
502
673
  agent_already: 'Een agent beheert uw gesprek al.',
503
- suggest_agent_escalation: 'Ik kan geen passend antwoord vinden. Wilt u met een agent spreken?',
674
+ suggest_agent_escalation: 'Wilt u met een agent spreken?',
504
675
  status_closed: 'Gesloten',
505
676
  status_agent: 'Agent',
506
677
  status_waiting: 'Wachten',
507
678
  status_bot: 'Bot',
508
679
  messages: 'bericht',
680
+ guest_title: 'Hallo! Ik ben Maria 👋',
681
+ guest_sub: 'Identificeer uzelf om te starten.',
682
+ guest_firstname: 'Voornaam *',
683
+ guest_company: 'Bedrijf *',
684
+ guest_email: 'E-mail *',
685
+ guest_submit: 'Gesprek starten',
686
+ guest_secured: 'Beveiligde gegevens · Powered by Makitizy',
687
+ guest_fill: 'Vul alle velden in.',
688
+ guest_email_invalid: 'Ongeldig e-mailadres.',
689
+ helpful: 'Nuttig',
690
+ not_helpful: 'Niet nuttig',
509
691
  },
510
692
  };
511
693
  return msgs[lang] || msgs['EN'];
@@ -525,8 +707,6 @@
525
707
  return msgs[lang] || msgs['EN'];
526
708
  }
527
709
 
528
- // ─── Theme ─────────────────────────────────────────────────────────────────
529
-
530
710
  getThemeColor() {
531
711
  return getClientTheme(this.client_id).primary;
532
712
  }
@@ -547,30 +727,28 @@
547
727
  const style = document.createElement('style');
548
728
  style.id = 'tp-chatbot-theme';
549
729
  style.textContent = `
550
- .tp-chatbot-bubble { background: linear-gradient(135deg, ${color}, ${dark}) !important; box-shadow: 0 4px 16px ${color}66 !important; }
551
- .tp-chatbot-header { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
552
- .tp-chatbot-message.user .tp-chatbot-bubble-msg { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
553
- .tp-chatbot-avatar { color: ${color} !important; }
554
- .tp-chatbot-role { color: ${color} !important; }
555
- .tp-chatbot-message.user .tp-chatbot-role { color: rgba(255,255,255,0.8) !important; }
556
- .tp-chatbot-input:focus { border-color: ${color} !important; }
557
- .tp-chatbot-send { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
558
- .tp-chatbot-agent-btn { border-color: ${color} !important; color: ${color} !important; }
559
- .tp-chatbot-agent-btn:hover { background: ${color} !important; color: white !important; }
560
- .tp-chatbot-typing span { background: ${color} !important; }
561
- #tp-new-conversation { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
562
- .tp-conv-item:hover { background: ${color}11 !important; }
730
+ .tp-chatbot-bubble { background: linear-gradient(135deg, ${color}, ${dark}) !important; box-shadow: 0 4px 20px ${color}55 !important; }
731
+ .tp-header { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
732
+ .tp-msg.user .tp-bubble-msg { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
733
+ .tp-input:focus { border-color: ${color} !important; }
734
+ .tp-send { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
735
+ .tp-agent-btn { border-color: ${color} !important; color: ${color} !important; }
736
+ .tp-agent-btn:hover { background: ${color} !important; color: white !important; }
737
+ .tp-typing span { background: ${color} !important; }
738
+ .tp-new-conv-btn { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
563
739
  .tp-conv-new-btn { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
564
- .tp-badge-closed { background: ${color}22 !important; color: ${dark} !important; }
565
- .tp-attach-btn { color: ${color} !important; }
566
- .tp-attach-btn:hover { background: ${color}18 !important; }
740
+ .tp-conv-item:hover { background: ${color}0d !important; }
741
+ .tp-guest-input:focus { border-color: ${color} !important; }
742
+ .tp-guest-submit { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
743
+ .tp-tab.active { border-bottom-color: white !important; color: white !important; }
744
+ .tp-closed-new-btn { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
745
+ .tp-suggestion-btn { border-color: ${color} !important; color: ${color} !important; }
746
+ .tp-suggestion-btn:hover { background: ${color} !important; color: white !important; }
567
747
  `;
568
748
  document.head.appendChild(style);
569
-
570
749
  const position = getClientTheme(this.client_id).position || 'right';
571
750
  const host = this.querySelector('.tp-chatbot-host');
572
751
  if (host) {
573
- host.style.width = WINDOW_WIDTH;
574
752
  host.style.left = position === 'left' ? '24px' : 'auto';
575
753
  host.style.right = position === 'left' ? 'auto' : '24px';
576
754
  }
@@ -579,7 +757,7 @@
579
757
  bubble.style.marginLeft = position === 'left' ? '0' : 'auto';
580
758
  bubble.style.marginRight = position === 'left' ? 'auto' : '0';
581
759
  }
582
- const win = this.querySelector('.tp-chatbot-window');
760
+ const win = this.querySelector('#tp-window');
583
761
  if (win) {
584
762
  win.style.left = '0';
585
763
  win.style.right = 'auto';
@@ -587,47 +765,55 @@
587
765
  }
588
766
  }
589
767
 
590
- // ─── Render ────────────────────────────────────────────────────────────────
591
-
592
768
  render() {
593
769
  const theme = getClientTheme(this.client_id);
770
+ const m = this.getMessages();
594
771
  this.innerHTML = `
595
772
  <div class="tp-chatbot-host">
596
773
  <div class="tp-chatbot-window" id="tp-window">
597
- <div class="tp-chatbot-header" id="tp-header">
598
- <div class="tp-chatbot-avatar">M</div>
599
- <div style="flex:1;min-width:0;">
600
- <div class="tp-chatbot-title">${theme.name} Support</div>
601
- <div class="tp-chatbot-subtitle" id="tp-subtitle">${icon('bot', 12, 'margin-right:4px')} Assistant</div>
774
+ <div class="tp-header" id="tp-header">
775
+ <div class="tp-header-top">
776
+ <div class="tp-header-avatar">M<div class="tp-header-dot"></div></div>
777
+ <div class="tp-header-info">
778
+ <div class="tp-header-title">${theme.name} Support</div>
779
+ <div class="tp-header-subtitle" id="tp-subtitle">${icon('bot', 11)} ${m.virtual}</div>
780
+ </div>
781
+ <div class="tp-header-controls">
782
+ <button id="tp-sound-btn" class="tp-header-btn" title="Sound">${icon('bell', 14)}</button>
783
+ <button id="tp-minimize-btn" class="tp-header-btn" title="Minimize">${icon('minimize', 14)}</button>
784
+ <button id="tp-maximize-btn" class="tp-header-btn" title="Fullscreen">${icon('restore', 14)}</button>
785
+ <button id="tp-back-btn" class="tp-header-btn" style="display:none;" title="Back">${icon('back', 14)}</button>
786
+ <button id="tp-close-win-btn" class="tp-header-btn" title="Close">${icon('close', 14)}</button>
787
+ </div>
788
+ </div>
789
+ <div class="tp-tabs" id="tp-tabs">
790
+ <button class="tp-tab active" data-tab="new" id="tp-tab-new">${icon('msg_plus', 12)} ${m.tab_new}</button>
791
+ <button class="tp-tab" data-tab="history" id="tp-tab-history">${icon('history', 12)} ${m.tab_history}</button>
602
792
  </div>
603
- <button id="tp-sound-btn" class="tp-header-btn" title="Sound">${icon('bell', 16)}</button>
604
- <button id="tp-minimize-btn" class="tp-header-btn" title="Minimize">${icon('minimize', 16)}</button>
605
- <button id="tp-maximize-btn" class="tp-header-btn" title="Fullscreen">${icon('restore', 16)}</button>
606
- <button id="tp-back-btn" class="tp-header-btn" style="display:none;" title="Back">${icon('back', 16)}</button>
607
793
  </div>
608
- <div class="tp-chatbot-messages" id="tp-messages"></div>
609
- <div class="tp-chatbot-agent-bar" id="tp-agent-bar" style="display:none;">
610
- <button class="tp-chatbot-agent-btn" id="tp-agent-btn">
611
- <span style="display:inline-flex;align-items:center;gap:6px;">${icon('agent', 16)} Talk to an agent</span>
794
+ <div class="tp-body" id="tp-body">
795
+ <div class="tp-messages-wrap" id="tp-messages"></div>
796
+ </div>
797
+ <div class="tp-agent-bar" id="tp-agent-bar" style="display:none;">
798
+ <button class="tp-agent-btn" id="tp-agent-btn">
799
+ <span style="display:inline-flex;align-items:center;gap:6px;">${icon('agent', 15)} ${m.speak_agent}</span>
612
800
  </button>
613
801
  </div>
614
- <div class="tp-chatbot-input-bar" id="tp-chatbot-input-bar" style="display:none;">
615
- <textarea class="tp-chatbot-input" id="tp-input" placeholder="Write your message..." rows="1"></textarea>
616
- <button class="tp-chatbot-send" id="tp-send">${icon('send', 16, 'color:white')}</button>
802
+ <div class="tp-input-bar" id="tp-input-bar" style="display:none;">
803
+ <textarea class="tp-input" id="tp-input" placeholder="${m.write_msg}" rows="1"></textarea>
804
+ <button class="tp-send" id="tp-send">${icon('send', 15, 'color:white')}</button>
617
805
  </div>
618
- <div id="tp-close-bar" style="padding:8px 12px;border-top:1px solid #ede8f5;text-align:center;display:none;">
619
- <button id="tp-close-btn" class="tp-close-btn">
620
- <span style="display:inline-flex;align-items:center;gap:6px;">${icon('check', 14)} End conversation</span>
806
+ <div class="tp-close-bar" id="tp-close-bar" style="display:none;">
807
+ <button class="tp-close-btn" id="tp-close-btn">
808
+ <span style="display:inline-flex;align-items:center;gap:5px;">${icon('check', 12)} ${m.close_btn}</span>
621
809
  </button>
622
810
  </div>
623
811
  </div>
624
- <div class="tp-chatbot-bubble" id="tp-bubble">${icon('msg', 24, 'color:white')}</div>
812
+ <div class="tp-chatbot-bubble" id="tp-bubble">${icon('msg', 26, 'color:white')}</div>
625
813
  </div>
626
814
  `;
627
815
  }
628
816
 
629
- // ─── Window controls ───────────────────────────────────────────────────────
630
-
631
817
  minimize() {
632
818
  const win = this.querySelector('#tp-window');
633
819
  if (this.is_fullscreen) this.exitFullscreen(false);
@@ -637,8 +823,8 @@
637
823
  }
638
824
 
639
825
  enterFullscreen() {
640
- const win = this.querySelector('#tp-window');
641
- const host = this.querySelector('.tp-chatbot-host');
826
+ const win = this.querySelector('#tp-window'),
827
+ host = this.querySelector('.tp-chatbot-host');
642
828
  this.is_fullscreen = true;
643
829
  this.is_minimized = false;
644
830
  win.style.position = 'fixed';
@@ -650,19 +836,19 @@
650
836
  win.style.zIndex = '99999';
651
837
  host.style.width = '100vw';
652
838
  const btn = this.querySelector('#tp-maximize-btn');
653
- if (btn) btn.innerHTML = icon('restore', 16);
839
+ if (btn) btn.innerHTML = icon('restore', 14);
654
840
  }
655
841
 
656
842
  exitFullscreen(restore_height = true) {
657
- const win = this.querySelector('#tp-window');
658
- const position = getClientTheme(this.client_id).position || 'right';
659
- const host = this.querySelector('.tp-chatbot-host');
843
+ const win = this.querySelector('#tp-window'),
844
+ position = getClientTheme(this.client_id).position || 'right',
845
+ host = this.querySelector('.tp-chatbot-host');
660
846
  this.is_fullscreen = false;
661
847
  win.style.position = 'absolute';
662
848
  win.style.top = '';
663
849
  win.style.left = '0';
664
850
  win.style.width = WINDOW_WIDTH;
665
- win.style.borderRadius = '16px';
851
+ win.style.borderRadius = '20px';
666
852
  win.style.zIndex = '';
667
853
  host.style.width = WINDOW_WIDTH;
668
854
  host.style.left = position === 'left' ? '24px' : 'auto';
@@ -670,33 +856,33 @@
670
856
  if (restore_height) win.style.height = WINDOW_HEIGHT;
671
857
  win.style.overflow = 'hidden';
672
858
  const btn = this.querySelector('#tp-maximize-btn');
673
- if (btn) btn.innerHTML = icon('restore', 16);
859
+ if (btn) btn.innerHTML = icon('restore', 14);
674
860
  }
675
861
 
676
- // ─── Events ────────────────────────────────────────────────────────────────
677
-
678
862
  attachEvents() {
679
863
  this.querySelector('#tp-bubble').addEventListener('click', () => this.toggleChat());
864
+ this.querySelector('#tp-close-win-btn').addEventListener('click', () => {
865
+ if (this.is_open) this.toggleChat();
866
+ });
680
867
  this.querySelector('#tp-send').addEventListener('click', () => this.sendMessage());
681
868
  this.querySelector('#tp-agent-btn').addEventListener('click', () => this.requestAgent());
682
- this.querySelector('#tp-back-btn').addEventListener('click', () => this.showConversationList());
683
-
869
+ this.querySelector('#tp-back-btn').addEventListener('click', () => {
870
+ this.setTab('new');
871
+ this.showConversationList();
872
+ });
684
873
  this.querySelector('#tp-input').addEventListener('keydown', e => {
685
874
  if (e.key === 'Enter' && !e.shiftKey) {
686
875
  e.preventDefault();
687
876
  this.sendMessage();
688
877
  }
689
878
  });
690
-
691
879
  this.querySelector('#tp-sound-btn').addEventListener('click', () => {
692
880
  this.sound_enabled = !this.sound_enabled;
693
- this.querySelector('#tp-sound-btn').innerHTML = icon(this.sound_enabled ? 'bell' : 'bell_off', 16);
881
+ this.querySelector('#tp-sound-btn').innerHTML = icon(this.sound_enabled ? 'bell' : 'bell_off', 14);
694
882
  });
695
-
696
883
  this.querySelector('#tp-close-btn').addEventListener('click', () => {
697
884
  if (window.confirm(this.getMessages().close_confirm)) this.closeByUser();
698
885
  });
699
-
700
886
  this.querySelector('#tp-minimize-btn').addEventListener('click', () => {
701
887
  if (this.is_minimized) {
702
888
  this.is_minimized = false;
@@ -707,7 +893,6 @@
707
893
  this.minimize();
708
894
  }
709
895
  });
710
-
711
896
  this.querySelector('#tp-maximize-btn').addEventListener('click', () => {
712
897
  if (this.is_fullscreen) {
713
898
  this.exitFullscreen(true);
@@ -721,29 +906,45 @@
721
906
  this.enterFullscreen();
722
907
  }
723
908
  });
909
+ this.querySelector('#tp-tab-new').addEventListener('click', () => {
910
+ this.setTab('new');
911
+ this.showConversationList();
912
+ });
913
+ this.querySelector('#tp-tab-history').addEventListener('click', () => {
914
+ this.setTab('history');
915
+ this.showConversationList();
916
+ });
917
+ }
918
+
919
+ setTab(tab) {
920
+ this.active_tab = tab;
921
+ this.querySelectorAll('.tp-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
922
+ }
923
+
924
+ showTabs(visible) {
925
+ const tabs = this.querySelector('#tp-tabs');
926
+ if (tabs) tabs.style.display = visible ? 'flex' : 'none';
724
927
  }
725
928
 
726
929
  updateUILanguage() {
727
930
  const m = this.getMessages();
728
931
  const agent_btn = this.querySelector('#tp-agent-btn');
729
932
  if (agent_btn)
730
- agent_btn.innerHTML = `<span style="display:inline-flex;align-items:center;gap:6px;">${icon('agent', 16)} ${m.speak_agent}</span>`;
933
+ agent_btn.innerHTML = `<span style="display:inline-flex;align-items:center;gap:6px;">${icon('agent', 15)} ${m.speak_agent}</span>`;
731
934
  const close_btn = this.querySelector('#tp-close-btn');
732
935
  if (close_btn)
733
- close_btn.innerHTML = `<span style="display:inline-flex;align-items:center;gap:6px;">${icon('check', 14)} ${m.close_btn}</span>`;
936
+ close_btn.innerHTML = `<span style="display:inline-flex;align-items:center;gap:5px;">${icon('check', 12)} ${m.close_btn}</span>`;
734
937
  const input = this.querySelector('#tp-input');
735
938
  if (input) input.placeholder = m.write_msg;
736
- const subtitle = this.querySelector('#tp-subtitle');
737
- if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
738
939
  }
739
940
 
740
941
  toggleChat() {
741
942
  this.is_open = !this.is_open;
742
- const window_el = this.querySelector('#tp-window');
743
- const bubble = this.querySelector('#tp-bubble');
943
+ const window_el = this.querySelector('#tp-window'),
944
+ bubble = this.querySelector('#tp-bubble');
744
945
  if (this.is_open) {
745
946
  window_el.classList.add('open');
746
- bubble.innerHTML = icon('close', 20, 'color:white');
947
+ bubble.innerHTML = icon('close', 22, 'color:white');
747
948
  if (this.is_minimized) {
748
949
  this.is_minimized = false;
749
950
  window_el.style.height = WINDOW_HEIGHT;
@@ -751,7 +952,7 @@
751
952
  }
752
953
  } else {
753
954
  window_el.classList.remove('open');
754
- bubble.innerHTML = icon('msg', 24, 'color:white');
955
+ bubble.innerHTML = icon('msg', 22, 'color:white');
755
956
  if (this.is_fullscreen) this.exitFullscreen(true);
756
957
  }
757
958
  }
@@ -773,31 +974,20 @@
773
974
  return data.result?.conversation_id;
774
975
  }
775
976
 
776
- // ─── Suggestions ──────────────────────────────────────────────────────────
777
-
778
977
  showSuggestions(suggestions) {
779
978
  const existing = this.querySelector('#tp-suggestions');
780
979
  if (existing) existing.remove();
781
980
  if (!suggestions || suggestions.length === 0) return;
782
981
  const lang = (this.user_info?.language || 'EN').toUpperCase();
783
- const color = this.getThemeColor();
784
982
  const container = this.querySelector('#tp-messages');
785
983
  const wrap = document.createElement('div');
786
984
  wrap.id = 'tp-suggestions';
787
- wrap.style.cssText = 'padding:8px 12px 12px;display:flex;flex-direction:column;gap:6px;';
985
+ wrap.className = 'tp-suggestions';
788
986
  suggestions.forEach(s => {
789
987
  const label = s[lang] || s['EN'];
790
988
  const btn = document.createElement('button');
791
- btn.style.cssText = `padding:8px 14px;border-radius:20px;border:1.5px solid ${color};background:white;color:${color};font-size:12px;font-weight:600;cursor:pointer;text-align:left;font-family:inherit;transition:all 0.12s;`;
989
+ btn.className = 'tp-suggestion-btn';
792
990
  btn.textContent = `→ ${label}`;
793
- btn.addEventListener('mouseenter', () => {
794
- btn.style.background = color;
795
- btn.style.color = 'white';
796
- });
797
- btn.addEventListener('mouseleave', () => {
798
- btn.style.background = 'white';
799
- btn.style.color = color;
800
- });
801
991
  btn.addEventListener('click', () => {
802
992
  const sugg_el = this.querySelector('#tp-suggestions');
803
993
  if (sugg_el) sugg_el.remove();
@@ -813,8 +1003,6 @@
813
1003
  container.scrollTop = container.scrollHeight;
814
1004
  }
815
1005
 
816
- // ─── Conversation List ─────────────────────────────────────────────────────
817
-
818
1006
  async showConversationList() {
819
1007
  this.view = 'list';
820
1008
  const m = this.getMessages();
@@ -822,94 +1010,107 @@
822
1010
  clearInterval(this.polling_interval);
823
1011
  this.polling_interval = null;
824
1012
  }
825
-
826
1013
  const subtitle = this.querySelector('#tp-subtitle');
827
- if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
1014
+ if (subtitle) subtitle.innerHTML = `${icon('bot', 11)} ${m.virtual}`;
828
1015
  this.querySelector('#tp-back-btn').style.display = 'none';
829
- this.querySelector('#tp-chatbot-input-bar').style.display = 'none';
1016
+ this.querySelector('#tp-input-bar').style.display = 'none';
830
1017
  this.querySelector('#tp-agent-bar').style.display = 'none';
831
1018
  this.querySelector('#tp-close-bar').style.display = 'none';
832
-
1019
+ this.showTabs(true);
833
1020
  const container = this.querySelector('#tp-messages');
834
- container.style.background = '#f5f5f7';
835
- container.innerHTML = `<div style="padding:16px;text-align:center;color:#aaa;font-size:12px;">${m.loading}</div>`;
836
-
1021
+ container.innerHTML = `<div style="padding:24px 16px;text-align:center;color:#ccc;font-size:12px;">${m.loading}</div>`;
837
1022
  try {
838
1023
  const url = `${this.api_url}/chat/conversations?user_id=${encodeURIComponent(this.user_id)}&limit=5`;
839
1024
  const response = await fetch(url, { headers: this.getHeaders() });
840
1025
  const data = await response.json();
841
1026
  const conversations = data.result?.conversations || [];
842
1027
  const color = this.getThemeColor();
843
-
844
1028
  container.innerHTML = '';
845
- container.style.background = 'white';
846
- container.style.padding = '0';
847
-
848
- // Header greeting
849
- const header_el = document.createElement('div');
850
- header_el.style.cssText = 'padding:16px;border-bottom:1px solid #f0f0f0;';
851
- header_el.innerHTML = `
852
- <div style="font-size:14px;font-weight:700;color:#1a1a2e;margin-bottom:2px;">${m.hello} ${this.user_info?.first_name || ''} 👋</div>
853
- <div style="font-size:12px;color:#aaa;">${m.recent_conv}</div>
854
- `;
855
- container.appendChild(header_el);
856
-
857
- if (conversations.length === 0) {
858
- const empty = document.createElement('div');
859
- empty.style.cssText = 'padding:32px 16px;text-align:center;color:#bbb;font-size:13px;';
860
- empty.innerHTML = `<div style="margin-bottom:8px;opacity:0.4;">${icon('msg', 32)}</div>${m.no_conv}`;
861
- container.appendChild(empty);
862
- } else {
863
- conversations.forEach(conv => {
864
- const item = document.createElement('div');
865
- item.className = 'tp-conv-item';
866
- item.style.cssText =
867
- 'padding:14px 16px;border-bottom:1px solid #f5f5f5;cursor:pointer;transition:background 0.15s;display:flex;justify-content:space-between;align-items:center;';
868
-
869
- const is_closed = conv.status === 'closed';
1029
+ if (this.active_tab === 'new') {
1030
+ const greeting = document.createElement('div');
1031
+ greeting.style.cssText = 'padding:20px 16px 8px;';
1032
+ greeting.innerHTML = `<div style="background:white;border-radius:16px;padding:16px;box-shadow:0 1px 8px rgba(0,0,0,0.07);margin-bottom:14px;"><div style="font-size:14px;font-weight:700;color:#1a1a2e;margin-bottom:4px;">${m.hello} ${this.user_info?.first_name || ''} 👋</div><div style="font-size:12px;color:#aaa;line-height:1.5;">${m.recent_conv}</div></div>`;
1033
+ container.appendChild(greeting);
1034
+ if (conversations.length === 0) {
1035
+ const empty = document.createElement('div');
1036
+ empty.style.cssText =
1037
+ 'padding:20px 16px;text-align:center;color:#ccc;font-size:13px;display:flex;flex-direction:column;align-items:center;gap:10px;';
1038
+ empty.innerHTML = `<div style="opacity:0.35;">${icon('msg', 36)}</div>${m.no_conv}`;
1039
+ container.appendChild(empty);
1040
+ } else {
1041
+ const last_conv = conversations[0];
1042
+ const is_closed = last_conv.status === 'closed';
1043
+ const badge_color = last_conv.status === 'waiting_agent' ? '#f59e0b' : color;
870
1044
  const status_label = is_closed
871
1045
  ? m.status_closed
872
- : conv.status === 'agent'
1046
+ : last_conv.status === 'agent'
873
1047
  ? m.status_agent
874
- : conv.status === 'waiting_agent'
1048
+ : last_conv.status === 'waiting_agent'
875
1049
  ? m.status_waiting
876
1050
  : m.status_bot;
877
-
878
- const badge_color = is_closed ? color : conv.status === 'waiting_agent' ? '#f59e0b' : color;
879
- const date = new Date(conv.updated_at).toLocaleDateString('fr-FR', {
1051
+ const date = new Date(last_conv.updated_at).toLocaleDateString('fr-FR', {
880
1052
  day: '2-digit',
881
1053
  month: '2-digit',
882
1054
  hour: '2-digit',
883
1055
  minute: '2-digit',
884
1056
  });
885
-
886
- item.innerHTML = `
887
- <div>
888
- <div style="font-size:13px;font-weight:600;color:${is_closed ? '#888' : '#1a1a2e'};margin-bottom:3px;">${date}</div>
889
- <div style="font-size:11px;color:#bbb;">${conv.message_count} ${m.messages}${conv.message_count > 1 ? 's' : ''}</div>
890
- </div>
891
- <span class="tp-badge-closed" style="display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:700;padding:3px 10px;border-radius:20px;background:${badge_color}18;color:${badge_color};">
892
- ${is_closed ? icon('check', 11) : ''} ${status_label}
893
- </span>
894
- `;
895
- item.addEventListener('click', () => this.openConversation(conv.conversation_id, is_closed));
1057
+ const item = document.createElement('div');
1058
+ item.style.cssText =
1059
+ 'margin:0 16px 14px;background:white;border-radius:14px;padding:14px 16px;box-shadow:0 1px 6px rgba(0,0,0,0.07);cursor:pointer;transition:box-shadow 0.15s;display:flex;justify-content:space-between;align-items:center;';
1060
+ item.innerHTML = `<div><div style="font-size:12px;font-weight:600;color:${is_closed ? '#aaa' : '#1a1a2e'};margin-bottom:3px;">${date}</div><div style="font-size:11px;color:#ccc;">${last_conv.message_count} ${m.messages}${last_conv.message_count > 1 ? 's' : ''}</div></div><span style="font-size:10px;font-weight:700;padding:3px 10px;border-radius:20px;background:${badge_color}18;color:${badge_color};">${status_label}</span>`;
1061
+ item.addEventListener('mouseenter', () => {
1062
+ item.style.boxShadow = '0 3px 12px rgba(0,0,0,0.12)';
1063
+ });
1064
+ item.addEventListener('mouseleave', () => {
1065
+ item.style.boxShadow = '0 1px 6px rgba(0,0,0,0.07)';
1066
+ });
1067
+ item.addEventListener('click', () => this.openConversation(last_conv.conversation_id, is_closed));
896
1068
  container.appendChild(item);
897
- });
1069
+ }
1070
+ const btn_wrap = document.createElement('div');
1071
+ btn_wrap.style.cssText = 'padding:4px 16px 16px;';
1072
+ btn_wrap.innerHTML = `<button class="tp-new-conv-btn">${icon('msg_plus', 15, 'color:white')} ${m.new_conv_btn}</button>`;
1073
+ btn_wrap.querySelector('button').addEventListener('click', () => this.startNewConversation());
1074
+ container.appendChild(btn_wrap);
1075
+ } else {
1076
+ const greeting = document.createElement('div');
1077
+ greeting.style.cssText = 'padding:16px 16px 8px;';
1078
+ greeting.innerHTML = `<div style="font-size:12px;font-weight:600;color:#aaa;text-transform:uppercase;letter-spacing:0.5px;">${m.recent_conv}</div>`;
1079
+ container.appendChild(greeting);
1080
+ if (conversations.length === 0) {
1081
+ const empty = document.createElement('div');
1082
+ empty.style.cssText =
1083
+ 'padding:32px 16px;text-align:center;color:#ccc;font-size:13px;display:flex;flex-direction:column;align-items:center;gap:10px;';
1084
+ empty.innerHTML = `<div style="opacity:0.35;">${icon('history', 36)}</div>${m.no_conv}`;
1085
+ container.appendChild(empty);
1086
+ } else {
1087
+ conversations.forEach(conv => {
1088
+ const is_closed = conv.status === 'closed';
1089
+ const badge_color = conv.status === 'waiting_agent' ? '#f59e0b' : color;
1090
+ const status_label = is_closed
1091
+ ? m.status_closed
1092
+ : conv.status === 'agent'
1093
+ ? m.status_agent
1094
+ : conv.status === 'waiting_agent'
1095
+ ? m.status_waiting
1096
+ : m.status_bot;
1097
+ const date = new Date(conv.updated_at).toLocaleDateString('fr-FR', {
1098
+ day: '2-digit',
1099
+ month: '2-digit',
1100
+ hour: '2-digit',
1101
+ minute: '2-digit',
1102
+ });
1103
+ const item = document.createElement('div');
1104
+ item.className = 'tp-conv-item';
1105
+ item.innerHTML = `<div class="tp-conv-item-left"><div class="tp-conv-item-date" style="color:${is_closed ? '#aaa' : '#1a1a2e'}">${date}</div><div class="tp-conv-item-count">${conv.message_count} ${m.messages}${conv.message_count > 1 ? 's' : ''}</div></div><span class="tp-badge" style="background:${badge_color}18;color:${badge_color};">${is_closed ? icon('check', 10) : ''} ${status_label}</span>`;
1106
+ item.addEventListener('click', () => this.openConversation(conv.conversation_id, is_closed));
1107
+ container.appendChild(item);
1108
+ });
1109
+ }
898
1110
  }
899
-
900
- // New conversation button
901
- const new_btn_wrap = document.createElement('div');
902
- new_btn_wrap.style.cssText = 'padding:16px;';
903
- new_btn_wrap.innerHTML = `
904
- <button class="tp-conv-new-btn" style="width:100%;padding:12px;color:white;border:none;border-radius:10px;font-size:13px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;">
905
- ${icon('msg_plus', 16, 'color:white')} ${m.new_conv_btn}
906
- </button>
907
- `;
908
- new_btn_wrap.querySelector('button').addEventListener('click', () => this.startNewConversation());
909
- container.appendChild(new_btn_wrap);
910
1111
  } catch (e) {
911
1112
  console.error('showConversationList error:', e);
912
- container.innerHTML = `<div style="padding:16px;text-align:center;color:#ef4444;font-size:13px;">${m.error_load}</div>`;
1113
+ container.innerHTML = `<div style="padding:24px 16px;text-align:center;color:#ef4444;font-size:13px;">${this.getMessages().error_load}</div>`;
913
1114
  }
914
1115
  }
915
1116
 
@@ -921,56 +1122,42 @@
921
1122
  this.is_closed = is_closed;
922
1123
  this.agent_mode = false;
923
1124
  this.agent_requested = false;
924
-
925
- const m = this.getMessages();
926
- this.querySelector('#tp-back-btn').style.display = 'block';
1125
+ this.querySelector('#tp-back-btn').style.display = 'flex';
1126
+ this.showTabs(false);
927
1127
  const container = this.querySelector('#tp-messages');
928
- container.style.background = '#f5f5f7';
929
- container.style.padding = '16px';
930
1128
  container.innerHTML = '';
931
-
932
- const input_bar = this.querySelector('#tp-chatbot-input-bar');
933
- const agent_bar = this.querySelector('#tp-agent-bar');
934
- const close_bar = this.querySelector('#tp-close-bar');
935
-
936
1129
  try {
937
1130
  const url = `${this.api_url}/chat/conversations/${conversation_id}?user_id=${encodeURIComponent(this.user_id)}`;
938
1131
  const response = await fetch(url, { headers: this.getHeaders() });
939
1132
  const data = await response.json();
940
1133
  const conv = data.result?.conversation;
941
1134
  if (!conv) return;
942
-
943
1135
  conv.messages.forEach(msg => {
944
- if (['user', 'assistant', 'agent', 'system'].includes(msg.role)) {
945
- this.addMessage(msg.role, msg.content, msg.message_id || null);
946
- }
1136
+ if (['user', 'assistant', 'agent', 'system'].includes(msg.role)) this.addMessage(msg.role, msg.content, msg.message_id || null);
947
1137
  });
948
1138
  this.last_message_count = conv.messages.length;
949
-
950
1139
  if (conv.status === 'closed') {
951
1140
  this.showClosed(conv.csat || null);
952
1141
  return;
953
1142
  }
954
-
1143
+ const input_bar = this.querySelector('#tp-input-bar'),
1144
+ agent_bar = this.querySelector('#tp-agent-bar'),
1145
+ close_bar = this.querySelector('#tp-close-bar');
955
1146
  if (conv.status === 'bot' || conv.status === 'waiting_agent') {
956
1147
  if (close_bar) close_bar.style.display = 'block';
957
1148
  } else {
958
1149
  if (close_bar) close_bar.style.display = 'none';
959
1150
  }
960
-
961
1151
  if (conv.status === 'agent') {
962
1152
  this.agent_mode = true;
963
- const subtitle = this.querySelector('#tp-subtitle');
964
- if (subtitle) subtitle.innerHTML = `${icon('user', 12, 'margin-right:4px')} ${m.human_agent}`;
1153
+ this._updateSubtitle('agent');
965
1154
  if (agent_bar) agent_bar.style.display = 'none';
966
1155
  if (input_bar) input_bar.style.display = 'flex';
967
1156
  } else {
968
1157
  if (input_bar) input_bar.style.display = 'flex';
969
1158
  if (agent_bar) agent_bar.style.display = 'none';
970
- const subtitle = this.querySelector('#tp-subtitle');
971
- if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
1159
+ this._updateSubtitle('bot');
972
1160
  }
973
-
974
1161
  this.updateUILanguage();
975
1162
  this.polling_interval = setInterval(() => this.pollMessages(), 3000);
976
1163
  } catch (e) {
@@ -978,6 +1165,7 @@
978
1165
  }
979
1166
  }
980
1167
 
1168
+ // ─── FIXED: conv créée au premier sendMessage, pas à l'ouverture ──────────
981
1169
  async startNewConversation() {
982
1170
  this.view = 'chat';
983
1171
  this.is_closed = false;
@@ -985,71 +1173,78 @@
985
1173
  this.agent_requested = false;
986
1174
  this.messages = [];
987
1175
  this.last_message_count = 0;
988
- this.conversation_id = null;
989
-
990
- const m = this.getMessages();
991
- this.querySelector('#tp-back-btn').style.display = 'block';
992
- this.querySelector('#tp-close-bar').style.display = 'block';
1176
+ this.conversation_id = null; // pas encore créée
993
1177
 
1178
+ this.querySelector('#tp-back-btn').style.display = 'flex';
1179
+ this.querySelector('#tp-close-bar').style.display = 'none'; // cachée tant que pas de conv
1180
+ this.showTabs(false);
994
1181
  const container = this.querySelector('#tp-messages');
995
- container.style.background = '#f5f5f7';
996
- container.style.padding = '16px';
997
1182
  container.innerHTML = '';
998
-
999
- const subtitle = this.querySelector('#tp-subtitle');
1000
- if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
1001
-
1002
- this.querySelector('#tp-chatbot-input-bar').style.display = 'flex';
1183
+ this._updateSubtitle('bot');
1184
+ this.querySelector('#tp-input-bar').style.display = 'flex';
1003
1185
  this.querySelector('#tp-agent-bar').style.display = 'none';
1004
1186
  this.updateUILanguage();
1005
-
1006
- this.conversation_id = await this.createConversation();
1007
1187
  this.addMessage('assistant', this.getWelcomeMessage(this.user_info?.first_name || ''));
1008
- this.last_message_count = 1;
1009
-
1010
1188
  const theme = getClientTheme(this.client_id);
1011
1189
  if (theme.suggestions && theme.suggestions.length > 0) this.showSuggestions(theme.suggestions);
1012
-
1013
- this.polling_interval = setInterval(() => this.pollMessages(), 3000);
1190
+ // pas de polling — rien à poller tant que la conv n'est pas créée
1014
1191
  }
1015
1192
 
1016
- // ─── Guest form ────────────────────────────────────────────────────────────
1193
+ _updateSubtitle(mode) {
1194
+ const m = this.getMessages(),
1195
+ subtitle = this.querySelector('#tp-subtitle');
1196
+ if (!subtitle) return;
1197
+ if (mode === 'agent') subtitle.innerHTML = `${icon('user', 11)} ${m.human_agent}`;
1198
+ else if (mode === 'closed') subtitle.innerHTML = `${icon('check', 11)} ${m.terminated}`;
1199
+ else subtitle.innerHTML = `${icon('bot', 11)} ${m.virtual}`;
1200
+ }
1017
1201
 
1018
1202
  showGuestForm() {
1019
1203
  const container = this.querySelector('#tp-messages');
1020
1204
  if (!container) return;
1021
- this.querySelector('#tp-chatbot-input-bar').style.display = 'none';
1205
+ this.querySelector('#tp-input-bar').style.display = 'none';
1022
1206
  this.querySelector('#tp-agent-bar').style.display = 'none';
1023
- const color = this.getThemeColor();
1024
- const dark = this.shadeColor(color, -20);
1025
- const form_el = document.createElement('div');
1026
- form_el.id = 'tp-guest-form';
1027
- form_el.innerHTML = `
1028
- <div style="padding:16px;display:flex;flex-direction:column;gap:12px;">
1029
- <p style="margin:0;font-size:13px;color:#1a1a2e;font-weight:600;">Before we start, please identify yourself:</p>
1030
- <input id="tp-guest-firstname" type="text" placeholder="First name *" style="padding:10px 14px;border-radius:8px;border:1.5px solid #ede8f5;font-size:13px;font-family:inherit;outline:none;" />
1031
- <input id="tp-guest-company" type="text" placeholder="Company name *" style="padding:10px 14px;border-radius:8px;border:1.5px solid #ede8f5;font-size:13px;font-family:inherit;outline:none;" />
1032
- <input id="tp-guest-email" type="email" placeholder="Email *" style="padding:10px 14px;border-radius:8px;border:1.5px solid #ede8f5;font-size:13px;font-family:inherit;outline:none;" />
1033
- <button id="tp-guest-submit" style="padding:10px;background:linear-gradient(135deg,${color},${dark});color:white;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;">Start conversation →</button>
1034
- <p id="tp-guest-error" style="margin:0;font-size:12px;color:#c0392b;display:none;"></p>
1207
+ this.showTabs(false);
1208
+ const m = this.getMessages();
1209
+ container.innerHTML = '';
1210
+ const wrap = document.createElement('div');
1211
+ wrap.id = 'tp-guest-wrap';
1212
+ wrap.className = 'tp-guest-wrap';
1213
+ wrap.innerHTML = `
1214
+ <div class="tp-guest-intro">
1215
+ <div class="tp-guest-intro-name">${m.guest_title}</div>
1216
+ <div class="tp-guest-intro-sub">${m.guest_sub}</div>
1035
1217
  </div>
1218
+ <div><label class="tp-guest-field-label">${m.guest_firstname}</label><input id="tp-guest-firstname" type="text" class="tp-guest-input" placeholder="Jean" /></div>
1219
+ <div><label class="tp-guest-field-label">${m.guest_company}</label><input id="tp-guest-company" type="text" class="tp-guest-input" placeholder="Acme Corp" /></div>
1220
+ <div><label class="tp-guest-field-label">${m.guest_email}</label><input id="tp-guest-email" type="email" class="tp-guest-input" placeholder="jean@acme.com" /></div>
1221
+ <p id="tp-guest-error" class="tp-guest-error"></p>
1222
+ <button id="tp-guest-submit" class="tp-guest-submit">${icon('msg_plus', 15, 'color:white')} ${m.guest_submit}</button>
1223
+ <div class="tp-guest-footer">${icon('lock', 11)} ${m.guest_secured}</div>
1036
1224
  `;
1037
- container.appendChild(form_el);
1225
+ container.appendChild(wrap);
1038
1226
  this.querySelector('#tp-guest-submit').addEventListener('click', () => this.submitGuestForm());
1227
+ ['#tp-guest-firstname', '#tp-guest-company', '#tp-guest-email'].forEach(sel => {
1228
+ this.querySelector(sel)?.addEventListener('keydown', e => {
1229
+ if (e.key === 'Enter') this.submitGuestForm();
1230
+ });
1231
+ });
1039
1232
  }
1040
1233
 
1234
+ // ─── FIXED: conv créée au premier sendMessage, pas après le formulaire ────
1041
1235
  async submitGuestForm() {
1042
1236
  const first_name = this.querySelector('#tp-guest-firstname')?.value.trim();
1043
1237
  const company_name = this.querySelector('#tp-guest-company')?.value.trim();
1044
1238
  const email = this.querySelector('#tp-guest-email')?.value.trim();
1045
1239
  const error_el = this.querySelector('#tp-guest-error');
1240
+ const m = this.getMessages();
1046
1241
  if (!first_name || !company_name || !email) {
1047
- error_el.textContent = 'Please fill in all fields.';
1242
+ error_el.textContent = m.guest_fill;
1048
1243
  error_el.style.display = 'block';
1049
1244
  return;
1050
1245
  }
1051
1246
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
1052
- error_el.textContent = 'Please enter a valid email.';
1247
+ error_el.textContent = m.guest_email_invalid;
1053
1248
  error_el.style.display = 'block';
1054
1249
  return;
1055
1250
  }
@@ -1057,66 +1252,49 @@
1057
1252
  this.user_info = { user_id: `guest-${email}`, first_name, last_name: '', company_name, email, language: 'EN', site_id: '' };
1058
1253
  this.user_id = this.user_info.user_id;
1059
1254
 
1060
- const form_el = this.querySelector('#tp-guest-form');
1061
- if (form_el) form_el.remove();
1255
+ const wrap = this.querySelector('#tp-guest-wrap');
1256
+ if (wrap) wrap.remove();
1062
1257
 
1063
1258
  this.view = 'chat';
1064
- this.conversation_id = await this.createConversation();
1065
-
1066
- this.querySelector('#tp-chatbot-input-bar').style.display = 'flex';
1259
+ this.conversation_id = null; // pas encore créée — sera créée au premier sendMessage
1260
+ this.querySelector('#tp-input-bar').style.display = 'flex';
1067
1261
  this.querySelector('#tp-agent-bar').style.display = 'none';
1068
- this.querySelector('#tp-close-bar').style.display = 'block';
1069
-
1070
- const container = this.querySelector('#tp-messages');
1071
- container.style.background = '#f5f5f7';
1072
- container.style.padding = '16px';
1073
-
1262
+ this.querySelector('#tp-close-bar').style.display = 'none'; // cachée tant que pas de conv
1263
+ this.querySelector('#tp-back-btn').style.display = 'flex';
1264
+ this.showTabs(false);
1074
1265
  this.updateUILanguage();
1075
1266
  this.addMessage('assistant', this.getWelcomeMessage(first_name));
1076
- this.last_message_count = 1;
1077
-
1078
1267
  const theme = getClientTheme(this.client_id);
1079
1268
  if (theme.suggestions && theme.suggestions.length > 0) this.showSuggestions(theme.suggestions);
1080
-
1081
- this.polling_interval = setInterval(() => this.pollMessages(), 3000);
1269
+ // pas de polling — rien à poller tant que la conv n'est pas créée
1082
1270
  }
1083
1271
 
1084
- // ─── Messages ──────────────────────────────────────────────────────────────
1085
-
1086
- addMessage(role, content, message_id = null) {
1087
- this.messages.push({ role, content, message_id, created_at: new Date().toISOString() });
1272
+ addMessage(role, content, message_id = null, source = null) {
1273
+ this.messages.push({ role, content, message_id, source, created_at: new Date().toISOString() });
1088
1274
  const container = this.querySelector('#tp-messages');
1089
-
1275
+ const m = this.getMessages();
1090
1276
  const role_label =
1091
1277
  role === 'user'
1092
- ? `${icon('user', 11, 'margin-right:3px')} ${this.user_info?.first_name || 'You'}`
1278
+ ? `${icon('user', 11)} ${this.user_info?.first_name || 'You'}`
1093
1279
  : role === 'agent'
1094
- ? `${icon('agent', 11, 'margin-right:3px')} Agent`
1280
+ ? `${icon('agent', 11)} Agent`
1095
1281
  : role === 'system'
1096
1282
  ? 'Info'
1097
- : `${icon('bot', 11, 'margin-right:3px')} Maria`;
1098
-
1283
+ : `${icon('bot', 11)} Maria`;
1099
1284
  const msg_el = document.createElement('div');
1100
- msg_el.className = `tp-chatbot-message ${role}`;
1285
+ msg_el.className = `tp-msg ${role}`;
1101
1286
  const rendered_content =
1102
1287
  role === 'assistant' || role === 'agent' ? (typeof marked !== 'undefined' ? marked.parse(content) : content) : content;
1103
-
1288
+ const now = formatTime(new Date().toISOString());
1104
1289
  const feedback_html =
1105
1290
  role === 'assistant' && message_id
1106
- ? `<div class="tp-feedback-bar" data-message-id="${message_id}">
1107
- <button class="tp-feedback-btn" data-rating="positive" title="Helpful">${icon('helpful', 14)}</button>
1108
- <button class="tp-feedback-btn" data-rating="negative" title="Not helpful">${icon('not_helpful', 14)}</button>
1109
- </div>`
1291
+ ? `<div class="tp-feedback-bar" data-message-id="${message_id}"><button class="tp-feedback-btn" data-rating="positive" title="${m.helpful}">${icon('helpful', 13)} <span style="font-size:11px;">${m.helpful}</span></button><button class="tp-feedback-btn" data-rating="negative" title="${m.not_helpful}">${icon('not_helpful', 13)} <span style="font-size:11px;">${m.not_helpful}</span></button></div>`
1110
1292
  : '';
1111
-
1112
- msg_el.innerHTML = `
1113
- <div class="tp-chatbot-bubble-msg">
1114
- <div class="tp-chatbot-role">${role_label}</div>
1115
- <div class="tp-chatbot-content tp-chatbot-markdown">${rendered_content}</div>
1116
- ${feedback_html}
1117
- </div>
1118
- `;
1119
-
1293
+ if (role === 'system') {
1294
+ msg_el.innerHTML = `<div class="tp-bubble-msg">${rendered_content}</div>`;
1295
+ } else {
1296
+ msg_el.innerHTML = `<div class="tp-msg-role">${role_label}</div><div class="tp-bubble-msg${role === 'assistant' || role === 'agent' ? ' tp-md' : ''}">${rendered_content}${feedback_html}</div><div class="tp-msg-time">${role === 'user' ? `${this.user_info?.first_name || 'You'} · ${now}` : `Maria · ${now}`}</div>`;
1297
+ }
1120
1298
  if (role === 'assistant' && message_id) {
1121
1299
  const bar = msg_el.querySelector('.tp-feedback-bar');
1122
1300
  bar.querySelectorAll('.tp-feedback-btn').forEach(btn => {
@@ -1125,9 +1303,11 @@
1125
1303
  bar.dataset.voted = '1';
1126
1304
  const rating = btn.dataset.rating;
1127
1305
  bar.querySelectorAll('.tp-feedback-btn').forEach(b => {
1128
- b.style.opacity = b === btn ? '1' : '0.3';
1306
+ b.classList.remove('voted-positive', 'voted-negative');
1307
+ b.style.opacity = b === btn ? '1' : '0.35';
1129
1308
  b.style.cursor = 'default';
1130
1309
  });
1310
+ btn.classList.add(rating === 'positive' ? 'voted-positive' : 'voted-negative');
1131
1311
  try {
1132
1312
  await fetch(`${this.api_url}/chat/conversations/${this.conversation_id}/feedback`, {
1133
1313
  method: 'POST',
@@ -1138,19 +1318,17 @@
1138
1318
  });
1139
1319
  });
1140
1320
  }
1141
-
1142
1321
  container.appendChild(msg_el);
1143
1322
  container.scrollTop = container.scrollHeight;
1144
-
1145
1323
  if ((role === 'assistant' || role === 'agent') && this.sound_enabled) playSound();
1146
1324
  }
1147
1325
 
1148
1326
  showTyping() {
1149
1327
  const container = this.querySelector('#tp-messages');
1150
1328
  const typing = document.createElement('div');
1151
- typing.className = 'tp-chatbot-message assistant';
1329
+ typing.className = 'tp-msg assistant';
1152
1330
  typing.id = 'tp-typing';
1153
- typing.innerHTML = `<div class="tp-chatbot-bubble-msg"><div class="tp-chatbot-typing"><span></span><span></span><span></span></div></div>`;
1331
+ typing.innerHTML = `<div class="tp-msg-role">${icon('bot', 11)} Maria</div><div class="tp-bubble-msg"><div class="tp-typing"><span></span><span></span><span></span></div></div>`;
1154
1332
  container.appendChild(typing);
1155
1333
  container.scrollTop = container.scrollHeight;
1156
1334
  }
@@ -1166,56 +1344,33 @@
1166
1344
  clearInterval(this.polling_interval);
1167
1345
  this.polling_interval = null;
1168
1346
  }
1169
-
1170
1347
  const m = this.getMessages();
1171
- const subtitle = this.querySelector('#tp-subtitle');
1172
- if (subtitle) subtitle.innerHTML = `${icon('check', 12, 'margin-right:4px')} ${m.terminated}`;
1173
-
1174
- this.querySelector('#tp-chatbot-input-bar').style.display = 'none';
1348
+ this._updateSubtitle('closed');
1349
+ this.querySelector('#tp-input-bar').style.display = 'none';
1175
1350
  this.querySelector('#tp-agent-bar').style.display = 'none';
1176
1351
  this.querySelector('#tp-close-bar').style.display = 'none';
1177
1352
  const sugg_el = this.querySelector('#tp-suggestions');
1178
1353
  if (sugg_el) sugg_el.remove();
1179
-
1180
1354
  const container = this.querySelector('#tp-messages');
1181
- const existing_banner = this.querySelector('#tp-closed-banner');
1355
+ const existing_banner = container.querySelector('#tp-closed-banner');
1182
1356
  if (existing_banner) {
1183
1357
  if (existing_banner.querySelector('#tp-csat-block')) return;
1184
1358
  existing_banner.remove();
1185
1359
  }
1186
-
1187
- const color = this.getThemeColor();
1188
- const dark = this.shadeColor(color, -20);
1189
-
1360
+ const now = new Date().toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
1190
1361
  const csat_html = existing_csat
1191
- ? `<div style="font-size:13px;color:#888;margin-bottom:14px;">${existing_csat === 'positive' ? m.csat_positive : m.csat_negative}</div>`
1192
- : `<div style="font-size:12px;color:#888;margin-bottom:10px;">${m.csat_question}</div>
1193
- <div style="display:flex;gap:10px;justify-content:center;margin-bottom:14px;">
1194
- <button id="tp-csat-positive" class="tp-csat-btn" style="border-color:#22c55e;color:#22c55e;">${icon('helpful', 20)}</button>
1195
- <button id="tp-csat-negative" class="tp-csat-btn" style="border-color:#ef4444;color:#ef4444;">${icon('not_helpful', 20)}</button>
1196
- </div>`;
1197
-
1362
+ ? `<div class="tp-csat-thanks">${existing_csat === 'positive' ? m.csat_positive : m.csat_negative}</div>`
1363
+ : `<div class="tp-csat-q">${m.csat_question}</div><div class="tp-csat-btns"><button id="tp-csat-positive" class="tp-csat-btn positive">${icon('helpful', 16)} ${m.helpful}</button><button id="tp-csat-negative" class="tp-csat-btn negative">${icon('not_helpful', 16)} ${m.not_helpful}</button></div>`;
1198
1364
  const banner = document.createElement('div');
1199
1365
  banner.id = 'tp-closed-banner';
1200
- banner.innerHTML = `
1201
- <div style="margin:16px;padding:16px;background:#f9f9f9;border:1px solid #ede8f5;border-radius:12px;text-align:center;">
1202
- <div style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:#1a1a2e;font-weight:600;margin-bottom:10px;">
1203
- ${icon('check', 14)} ${m.closed}
1204
- </div>
1205
- <div id="tp-csat-block">${csat_html}</div>
1206
- <button id="tp-new-conversation" style="padding:10px 20px;background:linear-gradient(135deg,${color},${dark});color:white;border:none;border-radius:8px;font-size:13px;font-weight:700;cursor:pointer;margin-bottom:8px;width:100%;display:flex;align-items:center;justify-content:center;gap:8px;">
1207
- ${icon('msg_plus', 15, 'color:white')} ${m.new_conv}
1208
- </button>
1209
- <button id="tp-back-to-list" style="padding:8px 20px;background:none;color:${color};border:1px solid #ede8f5;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;width:100%;">${m.back_to_list}</button>
1210
- </div>
1211
- `;
1366
+ banner.className = 'tp-closed-banner';
1367
+ banner.innerHTML = `<div class="tp-closed-title">${icon('check', 14)} ${m.closed}</div><div class="tp-closed-date">${now}</div><div id="tp-csat-block">${csat_html}</div><button class="tp-closed-new-btn" id="tp-closed-new-btn">${icon('msg_plus', 14, 'color:white')} ${m.new_conv}</button><button class="tp-closed-hist-btn" id="tp-closed-hist-btn">${m.back_to_list}</button>`;
1212
1368
  container.appendChild(banner);
1213
1369
  container.scrollTop = container.scrollHeight;
1214
-
1215
1370
  if (!existing_csat) {
1216
1371
  const submit_csat = async rating => {
1217
- const csat_block = this.querySelector('#tp-csat-block');
1218
- if (!csat_block) return;
1372
+ const block = this.querySelector('#tp-csat-block');
1373
+ if (!block) return;
1219
1374
  try {
1220
1375
  await fetch(`${this.api_url}/chat/conversations/${this.conversation_id}/csat`, {
1221
1376
  method: 'POST',
@@ -1225,20 +1380,21 @@
1225
1380
  } catch (e) {
1226
1381
  console.error('CSAT error:', e);
1227
1382
  }
1228
- csat_block.innerHTML = `<div style="font-size:13px;color:#888;margin-bottom:14px;">${rating === 'positive' ? m.csat_positive : m.csat_negative}</div>`;
1383
+ block.innerHTML = `<div class="tp-csat-thanks">${rating === 'positive' ? m.csat_positive : m.csat_negative}</div>`;
1229
1384
  };
1230
- this.querySelector('#tp-csat-positive').addEventListener('click', () => submit_csat('positive'));
1231
- this.querySelector('#tp-csat-negative').addEventListener('click', () => submit_csat('negative'));
1385
+ this.querySelector('#tp-csat-positive')?.addEventListener('click', () => submit_csat('positive'));
1386
+ this.querySelector('#tp-csat-negative')?.addEventListener('click', () => submit_csat('negative'));
1232
1387
  }
1233
-
1234
- this.querySelector('#tp-new-conversation').addEventListener('click', () => this.startNewConversation());
1235
- this.querySelector('#tp-back-to-list').addEventListener('click', () => this.showConversationList());
1388
+ this.querySelector('#tp-closed-new-btn').addEventListener('click', () => this.startNewConversation());
1389
+ this.querySelector('#tp-closed-hist-btn').addEventListener('click', () => {
1390
+ this.setTab('history');
1391
+ this.showConversationList();
1392
+ });
1236
1393
  }
1237
1394
 
1238
- // ─── Send / Poll ───────────────────────────────────────────────────────────
1239
-
1395
+ // ─── FIXED: createConversation lazy au premier sendMessage ────────────────
1240
1396
  async sendMessage() {
1241
- if (this.is_closed || !this.conversation_id) return;
1397
+ if (this.is_closed) return;
1242
1398
  const m = this.getMessages();
1243
1399
  const input = this.querySelector('#tp-input');
1244
1400
  const query = input.value.trim();
@@ -1250,10 +1406,22 @@
1250
1406
  const user_messages = this.messages.filter(msg => msg.role === 'user').length;
1251
1407
  if (user_messages >= 30) {
1252
1408
  this.addMessage('system', m.limit_reached);
1253
- this.querySelector('#tp-chatbot-input-bar').style.display = 'none';
1409
+ this.querySelector('#tp-input-bar').style.display = 'none';
1254
1410
  return;
1255
1411
  }
1256
1412
 
1413
+ // Créer la conv au premier message si pas encore créée
1414
+ if (!this.conversation_id) {
1415
+ try {
1416
+ this.conversation_id = await this.createConversation();
1417
+ this.querySelector('#tp-close-bar').style.display = 'block';
1418
+ this.polling_interval = setInterval(() => this.pollMessages(), 3000);
1419
+ } catch (e) {
1420
+ this.addMessage('system', m.error_occurred);
1421
+ return;
1422
+ }
1423
+ }
1424
+
1257
1425
  input.value = '';
1258
1426
  this.is_loading = true;
1259
1427
  this.addMessage('user', query);
@@ -1276,14 +1444,10 @@
1276
1444
 
1277
1445
  this.agent_mode = data.result.agent_mode;
1278
1446
  const source = data.result.source || null;
1279
- const subtitle = this.querySelector('#tp-subtitle');
1280
- if (subtitle)
1281
- subtitle.innerHTML = this.agent_mode
1282
- ? `${icon('user', 12, 'margin-right:4px')} ${m.human_agent}`
1283
- : `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
1447
+ this._updateSubtitle(this.agent_mode ? 'agent' : 'bot');
1284
1448
 
1285
1449
  if (data.result.reply) {
1286
- this.addMessage(this.agent_mode ? 'agent' : 'assistant', data.result.reply, data.result.message_id || null);
1450
+ this.addMessage(this.agent_mode ? 'agent' : 'assistant', data.result.reply, data.result.message_id || null, source);
1287
1451
  this.last_message_count += 2;
1288
1452
  }
1289
1453
 
@@ -1292,18 +1456,11 @@
1292
1456
  const suggest_agent = data.result.suggest_agent || false;
1293
1457
 
1294
1458
  if (agent_bar && !this.agent_requested && !this.agent_mode) {
1295
- if (
1296
- suggest_agent ||
1297
- source === 'fallback' ||
1298
- source === 'clarification' ||
1299
- source === 'no_match' ||
1300
- source === 'out_of_scope' ||
1301
- user_msg_count >= 5
1302
- ) {
1303
- agent_bar.style.display = 'block';
1304
- }
1459
+ const semantic_trigger = source === 'no_match' || source === 'out_of_scope';
1460
+ const clarif_count = this.messages.filter(msg => msg.role === 'assistant' && msg.source === 'clarification').length;
1461
+ const repeated_clarif = source === 'clarification' && clarif_count >= 3;
1462
+ if (semantic_trigger || repeated_clarif || (suggest_agent && user_msg_count >= 5)) agent_bar.style.display = 'block';
1305
1463
  }
1306
-
1307
1464
  if (suggest_agent && !this.agent_requested) {
1308
1465
  this.addMessage('system', m.suggest_agent_escalation);
1309
1466
  this.last_message_count += 1;
@@ -1312,7 +1469,6 @@
1312
1469
  this.hideTyping();
1313
1470
  this.addMessage('assistant', this.getMessages().error_occurred);
1314
1471
  }
1315
-
1316
1472
  this.is_loading = false;
1317
1473
  }
1318
1474
 
@@ -1321,7 +1477,6 @@
1321
1477
  this.agent_requested = true;
1322
1478
  const agent_bar = this.querySelector('#tp-agent-bar');
1323
1479
  if (agent_bar) agent_bar.style.display = 'none';
1324
-
1325
1480
  try {
1326
1481
  const response = await fetch(`${this.api_url}/chat/conversations/${this.conversation_id}/request-agent`, {
1327
1482
  method: 'POST',
@@ -1329,21 +1484,18 @@
1329
1484
  body: JSON.stringify({ user_id: this.user_id }),
1330
1485
  });
1331
1486
  const data = await response.json();
1332
- const m = this.getMessages();
1333
- const status = data.result?.status;
1334
-
1487
+ const m = this.getMessages(),
1488
+ status = data.result?.status;
1335
1489
  let msg = '';
1336
1490
  if (status === 'outside_hours') msg = m.outside_hours(data.result.next_opening);
1337
1491
  else if (status === 'no_agents') msg = m.no_agents;
1338
1492
  else if (status === 'waiting_agent') msg = m.waiting_agent;
1339
1493
  else if (status === 'waiting_agent_already') msg = m.waiting_agent_already;
1340
1494
  else if (status === 'agent') msg = m.agent_already;
1341
-
1342
1495
  if (msg) {
1343
1496
  this.addMessage('system', msg);
1344
1497
  this.last_message_count += 1;
1345
1498
  }
1346
-
1347
1499
  if (status === 'no_agents' || status === 'outside_hours' || status === 'waiting_agent_already') {
1348
1500
  this.agent_requested = false;
1349
1501
  if (agent_bar) agent_bar.style.display = 'block';
@@ -1378,24 +1530,21 @@
1378
1530
  const data = await response.json();
1379
1531
  const conv = data.result?.conversation;
1380
1532
  if (!conv) return;
1381
-
1382
- const server_messages = conv.messages || [];
1383
- const server_status = conv.status;
1384
- const is_typing = conv.is_typing || false;
1533
+ const server_messages = conv.messages || [],
1534
+ server_status = conv.status,
1535
+ is_typing = conv.is_typing || false;
1385
1536
  const m = this.getMessages();
1386
-
1387
1537
  if (server_status === 'closed' && !this.is_closed) {
1388
1538
  this.showClosed();
1389
1539
  return;
1390
1540
  }
1391
-
1392
1541
  if (is_typing && server_status === 'agent') {
1393
1542
  if (!this.querySelector('#tp-agent-typing')) {
1394
1543
  const container = this.querySelector('#tp-messages');
1395
1544
  const typing = document.createElement('div');
1396
- typing.className = 'tp-chatbot-message agent';
1545
+ typing.className = 'tp-msg agent';
1397
1546
  typing.id = 'tp-agent-typing';
1398
- typing.innerHTML = `<div class="tp-chatbot-bubble-msg"><div class="tp-chatbot-role">${icon('agent', 11, 'margin-right:3px')} Agent</div><div class="tp-chatbot-typing"><span></span><span></span><span></span></div></div>`;
1547
+ typing.innerHTML = `<div class="tp-msg-role">${icon('agent', 11)} Agent</div><div class="tp-bubble-msg"><div class="tp-typing"><span></span><span></span><span></span></div></div>`;
1399
1548
  container.appendChild(typing);
1400
1549
  container.scrollTop = container.scrollHeight;
1401
1550
  }
@@ -1403,13 +1552,12 @@
1403
1552
  const typing_el = this.querySelector('#tp-agent-typing');
1404
1553
  if (typing_el) typing_el.remove();
1405
1554
  }
1406
-
1407
1555
  if (server_messages.length > this.last_message_count) {
1408
1556
  const new_messages = server_messages.slice(this.last_message_count);
1409
1557
  new_messages.forEach(msg => {
1410
1558
  if (msg.role === 'agent') {
1411
- const typing_el = this.querySelector('#tp-agent-typing');
1412
- if (typing_el) typing_el.remove();
1559
+ const t = this.querySelector('#tp-agent-typing');
1560
+ if (t) t.remove();
1413
1561
  this.addMessage('agent', msg.content, msg.message_id || null);
1414
1562
  } else if (msg.role === 'assistant') {
1415
1563
  this.addMessage('assistant', msg.content, msg.message_id || null);
@@ -1419,22 +1567,23 @@
1419
1567
  });
1420
1568
  this.last_message_count = server_messages.length;
1421
1569
  }
1422
-
1423
1570
  if (server_status === 'agent' && !this.agent_mode) {
1424
1571
  this.agent_mode = true;
1425
- const subtitle = this.querySelector('#tp-subtitle');
1426
- if (subtitle) subtitle.innerHTML = `${icon('user', 12, 'margin-right:4px')} ${m.human_agent}`;
1572
+ this._updateSubtitle('agent');
1427
1573
  const agent_bar = this.querySelector('#tp-agent-bar');
1428
1574
  if (agent_bar) agent_bar.style.display = 'none';
1575
+ const close_bar = this.querySelector('#tp-close-bar');
1576
+ if (close_bar) close_bar.style.display = 'none';
1429
1577
  this.addMessage('system', m.agent_taken);
1430
1578
  this.last_message_count = server_messages.length;
1431
1579
  } else if (server_status === 'bot' && this.agent_mode) {
1432
1580
  this.agent_mode = false;
1433
1581
  this.agent_requested = false;
1434
- const subtitle = this.querySelector('#tp-subtitle');
1435
- if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
1582
+ this._updateSubtitle('bot');
1436
1583
  const agent_bar = this.querySelector('#tp-agent-bar');
1437
1584
  if (agent_bar) agent_bar.style.display = 'block';
1585
+ const close_bar = this.querySelector('#tp-close-bar');
1586
+ if (close_bar) close_bar.style.display = 'block';
1438
1587
  this.addMessage('system', m.agent_released);
1439
1588
  this.last_message_count = server_messages.length;
1440
1589
  }