@developpement/tp-chatbot-widget 0.0.10 → 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
  }
@@ -214,6 +322,7 @@
214
322
  if (t.primary) CLIENT_THEMES[this.client_id].primary = t.primary;
215
323
  if (t.position) CLIENT_THEMES[this.client_id].position = t.position;
216
324
  if (t.name) CLIENT_THEMES[this.client_id].name = t.name;
325
+ if (t.open_by_default !== undefined) CLIENT_THEMES[this.client_id].open_by_default = t.open_by_default;
217
326
  } else {
218
327
  CLIENT_THEMES[this.client_id] = {
219
328
  primary: t.primary || DEFAULT_THEME.primary,
@@ -223,7 +332,7 @@
223
332
  };
224
333
  }
225
334
  } catch (e) {
226
- console.warn('[tp-chatbot] fetchTheme failed, using defaults:', e.message);
335
+ console.warn('[tp-chatbot] fetchTheme failed:', e.message);
227
336
  }
228
337
  }
229
338
 
@@ -240,6 +349,9 @@
240
349
  this.view = 'list';
241
350
  this.is_fullscreen = false;
242
351
  this.is_minimized = false;
352
+ this.active_tab = 'new';
353
+
354
+ injectCSS();
243
355
 
244
356
  this.fetchTheme().then(() => {
245
357
  this.render();
@@ -247,9 +359,8 @@
247
359
  this.attachEvents();
248
360
  this._initialized = true;
249
361
 
250
- setTimeout(() => {
251
- this.toggleChat();
252
- }, 300);
362
+ const theme = getClientTheme(this.client_id);
363
+ if (theme.open_by_default) setTimeout(() => this.toggleChat(), 300);
253
364
 
254
365
  if (this.access_token) {
255
366
  this.user_info = extractUserInfo(this.access_token);
@@ -275,8 +386,6 @@
275
386
  if (this.polling_interval) clearInterval(this.polling_interval);
276
387
  }
277
388
 
278
- // ─── i18n ──────────────────────────────────────────────────────────────────
279
-
280
389
  getMessages() {
281
390
  const lang = (this.user_info?.language || 'EN').toUpperCase();
282
391
  const msgs = {
@@ -302,6 +411,8 @@
302
411
  close_btn: 'Terminer la conversation',
303
412
  speak_agent: 'Parler à un agent',
304
413
  write_msg: 'Écrivez votre message...',
414
+ tab_new: 'Nouveau',
415
+ tab_history: 'Historique',
305
416
  limit_reached: 'Vous avez atteint la limite de 30 messages. Veuillez contacter un agent ou écrire à support@travelplanet.com.',
306
417
  too_many: 'Trop de messages. Veuillez patienter avant de réessayer.',
307
418
  error_occurred: 'Une erreur est survenue. Veuillez réessayer.',
@@ -316,6 +427,17 @@
316
427
  status_waiting: 'En attente',
317
428
  status_bot: 'Bot',
318
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',
319
441
  },
320
442
  EN: {
321
443
  closed: 'Conversation closed',
@@ -339,6 +461,8 @@
339
461
  close_btn: 'End conversation',
340
462
  speak_agent: 'Talk to an agent',
341
463
  write_msg: 'Write your message...',
464
+ tab_new: 'New',
465
+ tab_history: 'History',
342
466
  limit_reached: 'You have reached the 30 message limit. Please contact an agent or write to support@travelplanet.com.',
343
467
  too_many: 'Too many messages. Please wait before retrying.',
344
468
  error_occurred: 'An error occurred. Please try again.',
@@ -353,6 +477,17 @@
353
477
  status_waiting: 'Waiting',
354
478
  status_bot: 'Bot',
355
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',
356
491
  },
357
492
  DE: {
358
493
  closed: 'Gespräch beendet',
@@ -361,8 +496,8 @@
361
496
  csat_negative: 'Danke, wir werden uns verbessern.',
362
497
  new_conv: 'Neues Gespräch',
363
498
  back_to_list: 'Zurück zur Liste',
364
- agent_taken: 'Ein Agent hat Ihr Gespräch übernommen. Sie können ihm jetzt direkt schreiben.',
365
- 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.',
366
501
  close_confirm: 'Möchten Sie das Gespräch beenden?',
367
502
  terminated: 'Gespräch beendet',
368
503
  virtual: 'Virtueller Assistent',
@@ -376,13 +511,15 @@
376
511
  close_btn: 'Gespräch beenden',
377
512
  speak_agent: 'Mit einem Agenten sprechen',
378
513
  write_msg: 'Schreiben Sie Ihre Nachricht...',
514
+ tab_new: 'Neu',
515
+ tab_history: 'Verlauf',
379
516
  limit_reached: 'Sie haben das Limit von 30 Nachrichten erreicht.',
380
517
  too_many: 'Zu viele Nachrichten. Bitte warten Sie.',
381
- error_occurred: 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.',
518
+ error_occurred: 'Ein Fehler ist aufgetreten.',
382
519
  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.',
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.',
386
523
  agent_already: 'Ein Agent betreut bereits Ihr Gespräch.',
387
524
  suggest_agent_escalation: 'Ich kann keine passende Antwort finden. Möchten Sie mit einem Agenten sprechen?',
388
525
  status_closed: 'Geschlossen',
@@ -390,6 +527,17 @@
390
527
  status_waiting: 'Wartend',
391
528
  status_bot: 'Bot',
392
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',
393
541
  },
394
542
  ES: {
395
543
  closed: 'Conversación cerrada',
@@ -398,14 +546,14 @@
398
546
  csat_negative: 'Gracias, mejoraremos.',
399
547
  new_conv: 'Nueva conversación',
400
548
  back_to_list: 'Volver a las conversaciones',
401
- agent_taken: 'Un agente ha tomado su conversación. Ahora puede escribirle directamente.',
402
- 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.',
403
551
  close_confirm: '¿Desea finalizar esta conversación?',
404
552
  terminated: 'Conversación terminada',
405
553
  virtual: 'Asistente virtual',
406
554
  human_agent: 'Agente humano',
407
555
  loading: 'Cargando...',
408
- no_conv: 'No hay conversaciones por el momento.',
556
+ no_conv: 'No hay conversaciones.',
409
557
  new_conv_btn: 'Nueva conversación',
410
558
  error_load: 'Error de carga.',
411
559
  recent_conv: 'Sus conversaciones recientes',
@@ -413,20 +561,33 @@
413
561
  close_btn: 'Finalizar conversación',
414
562
  speak_agent: 'Hablar con un agente',
415
563
  write_msg: 'Escriba su mensaje...',
564
+ tab_new: 'Nuevo',
565
+ tab_history: 'Historial',
416
566
  limit_reached: 'Ha alcanzado el límite de 30 mensajes.',
417
- too_many: 'Demasiados mensajes. Por favor espere.',
418
- error_occurred: 'Se produjo un error. Por favor, inténtelo de nuevo.',
567
+ too_many: 'Demasiados mensajes.',
568
+ error_occurred: 'Se produjo un error.',
419
569
  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.',
570
+ no_agents: 'No hay agentes disponibles.',
571
+ waiting_agent: 'Su solicitud ha sido enviada.',
572
+ waiting_agent_already: 'Ya está en la cola.',
423
573
  agent_already: 'Un agente ya está gestionando su conversación.',
424
- suggest_agent_escalation: 'No encuentro una respuesta adecuada. ¿Desea hablar con un agente?',
574
+ suggest_agent_escalation: '¿Desea hablar con un agente?',
425
575
  status_closed: 'Cerrado',
426
576
  status_agent: 'Agente',
427
577
  status_waiting: 'Esperando',
428
578
  status_bot: 'Bot',
429
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',
430
591
  },
431
592
  IT: {
432
593
  closed: 'Conversazione chiusa',
@@ -436,13 +597,13 @@
436
597
  new_conv: 'Nuova conversazione',
437
598
  back_to_list: 'Torna alle conversazioni',
438
599
  agent_taken: 'Un agente ha preso in carico la sua conversazione.',
439
- agent_released: 'Il nostro assistente virtuale è tornato. Come posso aiutarla?',
600
+ agent_released: 'Il nostro assistente virtuale è tornato.',
440
601
  close_confirm: 'Vuole terminare questa conversazione?',
441
602
  terminated: 'Conversazione terminata',
442
603
  virtual: 'Assistente virtuale',
443
604
  human_agent: 'Agente umano',
444
605
  loading: 'Caricamento...',
445
- no_conv: 'Nessuna conversazione per il momento.',
606
+ no_conv: 'Nessuna conversazione.',
446
607
  new_conv_btn: 'Nuova conversazione',
447
608
  error_load: 'Errore di caricamento.',
448
609
  recent_conv: 'Le sue conversazioni recenti',
@@ -450,20 +611,33 @@
450
611
  close_btn: 'Termina conversazione',
451
612
  speak_agent: 'Parla con un agente',
452
613
  write_msg: 'Scrivi il tuo messaggio...',
614
+ tab_new: 'Nuovo',
615
+ tab_history: 'Cronologia',
453
616
  limit_reached: 'Hai raggiunto il limite di 30 messaggi.',
454
- too_many: 'Troppi messaggi. Attendere prego.',
455
- error_occurred: 'Si è verificato un errore. Riprova.',
617
+ too_many: 'Troppi messaggi.',
618
+ error_occurred: 'Si è verificato un errore.',
456
619
  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.',
620
+ no_agents: 'Nessun agente disponibile.',
621
+ waiting_agent: 'Richiesta inviata.',
622
+ waiting_agent_already: 'È già in coda.',
460
623
  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?',
624
+ suggest_agent_escalation: 'Desidera parlare con un agente?',
462
625
  status_closed: 'Chiuso',
463
626
  status_agent: 'Agente',
464
627
  status_waiting: 'Attesa',
465
628
  status_bot: 'Bot',
466
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',
467
641
  },
468
642
  NL: {
469
643
  closed: 'Gesprek gesloten',
@@ -473,13 +647,13 @@
473
647
  new_conv: 'Nieuw gesprek',
474
648
  back_to_list: 'Terug naar gesprekken',
475
649
  agent_taken: 'Een agent heeft uw gesprek overgenomen.',
476
- agent_released: 'Onze virtuele assistent is terug. Hoe kan ik u helpen?',
650
+ agent_released: 'Onze virtuele assistent is terug.',
477
651
  close_confirm: 'Wilt u dit gesprek beëindigen?',
478
652
  terminated: 'Gesprek beëindigd',
479
653
  virtual: 'Virtuele assistent',
480
654
  human_agent: 'Menselijke agent',
481
655
  loading: 'Laden...',
482
- no_conv: 'Geen gesprekken op dit moment.',
656
+ no_conv: 'Geen gesprekken.',
483
657
  new_conv_btn: 'Nieuw gesprek',
484
658
  error_load: 'Laadout.',
485
659
  recent_conv: 'Uw recente gesprekken',
@@ -487,20 +661,33 @@
487
661
  close_btn: 'Gesprek beëindigen',
488
662
  speak_agent: 'Praat met een agent',
489
663
  write_msg: 'Schrijf uw bericht...',
664
+ tab_new: 'Nieuw',
665
+ tab_history: 'Geschiedenis',
490
666
  limit_reached: 'U heeft de limiet van 30 berichten bereikt.',
491
- too_many: 'Te veel berichten. Wacht even.',
492
- error_occurred: 'Er is een fout opgetreden. Probeer het opnieuw.',
493
- outside_hours: next => `Onze support is gesloten. Volgende beschikbare tijd: ${next}.`,
494
- no_agents: 'Geen agent beschikbaar. Probeer het later opnieuw.',
495
- waiting_agent: 'Uw verzoek is verzonden. Een agent neemt het gesprek over.',
496
- waiting_agent_already: 'U staat al in de wachtrij. Even geduld.',
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.',
497
673
  agent_already: 'Een agent beheert uw gesprek al.',
498
- 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?',
499
675
  status_closed: 'Gesloten',
500
676
  status_agent: 'Agent',
501
677
  status_waiting: 'Wachten',
502
678
  status_bot: 'Bot',
503
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',
504
691
  },
505
692
  };
506
693
  return msgs[lang] || msgs['EN'];
@@ -520,8 +707,6 @@
520
707
  return msgs[lang] || msgs['EN'];
521
708
  }
522
709
 
523
- // ─── Theme ─────────────────────────────────────────────────────────────────
524
-
525
710
  getThemeColor() {
526
711
  return getClientTheme(this.client_id).primary;
527
712
  }
@@ -542,30 +727,28 @@
542
727
  const style = document.createElement('style');
543
728
  style.id = 'tp-chatbot-theme';
544
729
  style.textContent = `
545
- .tp-chatbot-bubble { background: linear-gradient(135deg, ${color}, ${dark}) !important; box-shadow: 0 4px 16px ${color}66 !important; }
546
- .tp-chatbot-header { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
547
- .tp-chatbot-message.user .tp-chatbot-bubble-msg { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
548
- .tp-chatbot-avatar { color: ${color} !important; }
549
- .tp-chatbot-role { color: ${color} !important; }
550
- .tp-chatbot-message.user .tp-chatbot-role { color: rgba(255,255,255,0.8) !important; }
551
- .tp-chatbot-input:focus { border-color: ${color} !important; }
552
- .tp-chatbot-send { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
553
- .tp-chatbot-agent-btn { border-color: ${color} !important; color: ${color} !important; }
554
- .tp-chatbot-agent-btn:hover { background: ${color} !important; color: white !important; }
555
- .tp-chatbot-typing span { background: ${color} !important; }
556
- #tp-new-conversation { background: linear-gradient(135deg, ${color}, ${dark}) !important; }
557
- .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; }
558
739
  .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; }
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; }
562
747
  `;
563
748
  document.head.appendChild(style);
564
-
565
749
  const position = getClientTheme(this.client_id).position || 'right';
566
750
  const host = this.querySelector('.tp-chatbot-host');
567
751
  if (host) {
568
- host.style.width = WINDOW_WIDTH;
569
752
  host.style.left = position === 'left' ? '24px' : 'auto';
570
753
  host.style.right = position === 'left' ? 'auto' : '24px';
571
754
  }
@@ -574,7 +757,7 @@
574
757
  bubble.style.marginLeft = position === 'left' ? '0' : 'auto';
575
758
  bubble.style.marginRight = position === 'left' ? 'auto' : '0';
576
759
  }
577
- const win = this.querySelector('.tp-chatbot-window');
760
+ const win = this.querySelector('#tp-window');
578
761
  if (win) {
579
762
  win.style.left = '0';
580
763
  win.style.right = 'auto';
@@ -582,47 +765,55 @@
582
765
  }
583
766
  }
584
767
 
585
- // ─── Render ────────────────────────────────────────────────────────────────
586
-
587
768
  render() {
588
769
  const theme = getClientTheme(this.client_id);
770
+ const m = this.getMessages();
589
771
  this.innerHTML = `
590
772
  <div class="tp-chatbot-host">
591
773
  <div class="tp-chatbot-window" id="tp-window">
592
- <div class="tp-chatbot-header" id="tp-header">
593
- <div class="tp-chatbot-avatar">M</div>
594
- <div style="flex:1;min-width:0;">
595
- <div class="tp-chatbot-title">${theme.name} Support</div>
596
- <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>
597
792
  </div>
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>
602
793
  </div>
603
- <div class="tp-chatbot-messages" id="tp-messages"></div>
604
- <div class="tp-chatbot-agent-bar" id="tp-agent-bar" style="display:none;">
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>
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>
607
800
  </button>
608
801
  </div>
609
- <div class="tp-chatbot-input-bar" id="tp-chatbot-input-bar" style="display:none;">
610
- <textarea class="tp-chatbot-input" id="tp-input" placeholder="Write your message..." rows="1"></textarea>
611
- <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>
612
805
  </div>
613
- <div id="tp-close-bar" style="padding:8px 12px;border-top:1px solid #ede8f5;text-align:center;display:none;">
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>
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>
616
809
  </button>
617
810
  </div>
618
811
  </div>
619
- <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>
620
813
  </div>
621
814
  `;
622
815
  }
623
816
 
624
- // ─── Window controls ───────────────────────────────────────────────────────
625
-
626
817
  minimize() {
627
818
  const win = this.querySelector('#tp-window');
628
819
  if (this.is_fullscreen) this.exitFullscreen(false);
@@ -632,8 +823,8 @@
632
823
  }
633
824
 
634
825
  enterFullscreen() {
635
- const win = this.querySelector('#tp-window');
636
- const host = this.querySelector('.tp-chatbot-host');
826
+ const win = this.querySelector('#tp-window'),
827
+ host = this.querySelector('.tp-chatbot-host');
637
828
  this.is_fullscreen = true;
638
829
  this.is_minimized = false;
639
830
  win.style.position = 'fixed';
@@ -645,19 +836,19 @@
645
836
  win.style.zIndex = '99999';
646
837
  host.style.width = '100vw';
647
838
  const btn = this.querySelector('#tp-maximize-btn');
648
- if (btn) btn.innerHTML = icon('restore', 16);
839
+ if (btn) btn.innerHTML = icon('restore', 14);
649
840
  }
650
841
 
651
842
  exitFullscreen(restore_height = true) {
652
- const win = this.querySelector('#tp-window');
653
- const position = getClientTheme(this.client_id).position || 'right';
654
- 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');
655
846
  this.is_fullscreen = false;
656
847
  win.style.position = 'absolute';
657
848
  win.style.top = '';
658
849
  win.style.left = '0';
659
850
  win.style.width = WINDOW_WIDTH;
660
- win.style.borderRadius = '16px';
851
+ win.style.borderRadius = '20px';
661
852
  win.style.zIndex = '';
662
853
  host.style.width = WINDOW_WIDTH;
663
854
  host.style.left = position === 'left' ? '24px' : 'auto';
@@ -665,33 +856,33 @@
665
856
  if (restore_height) win.style.height = WINDOW_HEIGHT;
666
857
  win.style.overflow = 'hidden';
667
858
  const btn = this.querySelector('#tp-maximize-btn');
668
- if (btn) btn.innerHTML = icon('restore', 16);
859
+ if (btn) btn.innerHTML = icon('restore', 14);
669
860
  }
670
861
 
671
- // ─── Events ────────────────────────────────────────────────────────────────
672
-
673
862
  attachEvents() {
674
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
+ });
675
867
  this.querySelector('#tp-send').addEventListener('click', () => this.sendMessage());
676
868
  this.querySelector('#tp-agent-btn').addEventListener('click', () => this.requestAgent());
677
- this.querySelector('#tp-back-btn').addEventListener('click', () => this.showConversationList());
678
-
869
+ this.querySelector('#tp-back-btn').addEventListener('click', () => {
870
+ this.setTab('new');
871
+ this.showConversationList();
872
+ });
679
873
  this.querySelector('#tp-input').addEventListener('keydown', e => {
680
874
  if (e.key === 'Enter' && !e.shiftKey) {
681
875
  e.preventDefault();
682
876
  this.sendMessage();
683
877
  }
684
878
  });
685
-
686
879
  this.querySelector('#tp-sound-btn').addEventListener('click', () => {
687
880
  this.sound_enabled = !this.sound_enabled;
688
- 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);
689
882
  });
690
-
691
883
  this.querySelector('#tp-close-btn').addEventListener('click', () => {
692
884
  if (window.confirm(this.getMessages().close_confirm)) this.closeByUser();
693
885
  });
694
-
695
886
  this.querySelector('#tp-minimize-btn').addEventListener('click', () => {
696
887
  if (this.is_minimized) {
697
888
  this.is_minimized = false;
@@ -702,7 +893,6 @@
702
893
  this.minimize();
703
894
  }
704
895
  });
705
-
706
896
  this.querySelector('#tp-maximize-btn').addEventListener('click', () => {
707
897
  if (this.is_fullscreen) {
708
898
  this.exitFullscreen(true);
@@ -716,29 +906,45 @@
716
906
  this.enterFullscreen();
717
907
  }
718
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';
719
927
  }
720
928
 
721
929
  updateUILanguage() {
722
930
  const m = this.getMessages();
723
931
  const agent_btn = this.querySelector('#tp-agent-btn');
724
932
  if (agent_btn)
725
- 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>`;
726
934
  const close_btn = this.querySelector('#tp-close-btn');
727
935
  if (close_btn)
728
- 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>`;
729
937
  const input = this.querySelector('#tp-input');
730
938
  if (input) input.placeholder = m.write_msg;
731
- const subtitle = this.querySelector('#tp-subtitle');
732
- if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
733
939
  }
734
940
 
735
941
  toggleChat() {
736
942
  this.is_open = !this.is_open;
737
- const window_el = this.querySelector('#tp-window');
738
- const bubble = this.querySelector('#tp-bubble');
943
+ const window_el = this.querySelector('#tp-window'),
944
+ bubble = this.querySelector('#tp-bubble');
739
945
  if (this.is_open) {
740
946
  window_el.classList.add('open');
741
- bubble.innerHTML = icon('close', 20, 'color:white');
947
+ bubble.innerHTML = icon('close', 22, 'color:white');
742
948
  if (this.is_minimized) {
743
949
  this.is_minimized = false;
744
950
  window_el.style.height = WINDOW_HEIGHT;
@@ -746,7 +952,7 @@
746
952
  }
747
953
  } else {
748
954
  window_el.classList.remove('open');
749
- bubble.innerHTML = icon('msg', 24, 'color:white');
955
+ bubble.innerHTML = icon('msg', 22, 'color:white');
750
956
  if (this.is_fullscreen) this.exitFullscreen(true);
751
957
  }
752
958
  }
@@ -768,31 +974,20 @@
768
974
  return data.result?.conversation_id;
769
975
  }
770
976
 
771
- // ─── Suggestions ──────────────────────────────────────────────────────────
772
-
773
977
  showSuggestions(suggestions) {
774
978
  const existing = this.querySelector('#tp-suggestions');
775
979
  if (existing) existing.remove();
776
980
  if (!suggestions || suggestions.length === 0) return;
777
981
  const lang = (this.user_info?.language || 'EN').toUpperCase();
778
- const color = this.getThemeColor();
779
982
  const container = this.querySelector('#tp-messages');
780
983
  const wrap = document.createElement('div');
781
984
  wrap.id = 'tp-suggestions';
782
- wrap.style.cssText = 'padding:8px 12px 12px;display:flex;flex-direction:column;gap:6px;';
985
+ wrap.className = 'tp-suggestions';
783
986
  suggestions.forEach(s => {
784
987
  const label = s[lang] || s['EN'];
785
988
  const btn = document.createElement('button');
786
- 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';
787
990
  btn.textContent = `→ ${label}`;
788
- btn.addEventListener('mouseenter', () => {
789
- btn.style.background = color;
790
- btn.style.color = 'white';
791
- });
792
- btn.addEventListener('mouseleave', () => {
793
- btn.style.background = 'white';
794
- btn.style.color = color;
795
- });
796
991
  btn.addEventListener('click', () => {
797
992
  const sugg_el = this.querySelector('#tp-suggestions');
798
993
  if (sugg_el) sugg_el.remove();
@@ -808,8 +1003,6 @@
808
1003
  container.scrollTop = container.scrollHeight;
809
1004
  }
810
1005
 
811
- // ─── Conversation List ─────────────────────────────────────────────────────
812
-
813
1006
  async showConversationList() {
814
1007
  this.view = 'list';
815
1008
  const m = this.getMessages();
@@ -817,94 +1010,107 @@
817
1010
  clearInterval(this.polling_interval);
818
1011
  this.polling_interval = null;
819
1012
  }
820
-
821
1013
  const subtitle = this.querySelector('#tp-subtitle');
822
- if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
1014
+ if (subtitle) subtitle.innerHTML = `${icon('bot', 11)} ${m.virtual}`;
823
1015
  this.querySelector('#tp-back-btn').style.display = 'none';
824
- this.querySelector('#tp-chatbot-input-bar').style.display = 'none';
1016
+ this.querySelector('#tp-input-bar').style.display = 'none';
825
1017
  this.querySelector('#tp-agent-bar').style.display = 'none';
826
1018
  this.querySelector('#tp-close-bar').style.display = 'none';
827
-
1019
+ this.showTabs(true);
828
1020
  const container = this.querySelector('#tp-messages');
829
- container.style.background = '#f5f5f7';
830
- container.innerHTML = `<div style="padding:16px;text-align:center;color:#aaa;font-size:12px;">${m.loading}</div>`;
831
-
1021
+ container.innerHTML = `<div style="padding:24px 16px;text-align:center;color:#ccc;font-size:12px;">${m.loading}</div>`;
832
1022
  try {
833
1023
  const url = `${this.api_url}/chat/conversations?user_id=${encodeURIComponent(this.user_id)}&limit=5`;
834
1024
  const response = await fetch(url, { headers: this.getHeaders() });
835
1025
  const data = await response.json();
836
1026
  const conversations = data.result?.conversations || [];
837
1027
  const color = this.getThemeColor();
838
-
839
1028
  container.innerHTML = '';
840
- container.style.background = 'white';
841
- container.style.padding = '0';
842
-
843
- // Header greeting
844
- const header_el = document.createElement('div');
845
- header_el.style.cssText = 'padding:16px;border-bottom:1px solid #f0f0f0;';
846
- header_el.innerHTML = `
847
- <div style="font-size:14px;font-weight:700;color:#1a1a2e;margin-bottom:2px;">${m.hello} ${this.user_info?.first_name || ''} 👋</div>
848
- <div style="font-size:12px;color:#aaa;">${m.recent_conv}</div>
849
- `;
850
- container.appendChild(header_el);
851
-
852
- if (conversations.length === 0) {
853
- const empty = document.createElement('div');
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}`;
856
- container.appendChild(empty);
857
- } else {
858
- conversations.forEach(conv => {
859
- const item = document.createElement('div');
860
- item.className = 'tp-conv-item';
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;';
863
-
864
- 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;
865
1044
  const status_label = is_closed
866
1045
  ? m.status_closed
867
- : conv.status === 'agent'
1046
+ : last_conv.status === 'agent'
868
1047
  ? m.status_agent
869
- : conv.status === 'waiting_agent'
1048
+ : last_conv.status === 'waiting_agent'
870
1049
  ? m.status_waiting
871
1050
  : m.status_bot;
872
-
873
- const badge_color = is_closed ? color : conv.status === 'waiting_agent' ? '#f59e0b' : color;
874
- const date = new Date(conv.updated_at).toLocaleDateString('fr-FR', {
1051
+ const date = new Date(last_conv.updated_at).toLocaleDateString('fr-FR', {
875
1052
  day: '2-digit',
876
1053
  month: '2-digit',
877
1054
  hour: '2-digit',
878
1055
  minute: '2-digit',
879
1056
  });
880
-
881
- item.innerHTML = `
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>
885
- </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>
889
- `;
890
- 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));
891
1068
  container.appendChild(item);
892
- });
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
+ }
893
1110
  }
894
-
895
- // New conversation button
896
- const new_btn_wrap = document.createElement('div');
897
- new_btn_wrap.style.cssText = 'padding:16px;';
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
- `;
903
- new_btn_wrap.querySelector('button').addEventListener('click', () => this.startNewConversation());
904
- container.appendChild(new_btn_wrap);
905
1111
  } catch (e) {
906
1112
  console.error('showConversationList error:', e);
907
- 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>`;
908
1114
  }
909
1115
  }
910
1116
 
@@ -916,56 +1122,42 @@
916
1122
  this.is_closed = is_closed;
917
1123
  this.agent_mode = false;
918
1124
  this.agent_requested = false;
919
-
920
- const m = this.getMessages();
921
- this.querySelector('#tp-back-btn').style.display = 'block';
1125
+ this.querySelector('#tp-back-btn').style.display = 'flex';
1126
+ this.showTabs(false);
922
1127
  const container = this.querySelector('#tp-messages');
923
- container.style.background = '#f5f5f7';
924
- container.style.padding = '16px';
925
1128
  container.innerHTML = '';
926
-
927
- const input_bar = this.querySelector('#tp-chatbot-input-bar');
928
- const agent_bar = this.querySelector('#tp-agent-bar');
929
- const close_bar = this.querySelector('#tp-close-bar');
930
-
931
1129
  try {
932
1130
  const url = `${this.api_url}/chat/conversations/${conversation_id}?user_id=${encodeURIComponent(this.user_id)}`;
933
1131
  const response = await fetch(url, { headers: this.getHeaders() });
934
1132
  const data = await response.json();
935
1133
  const conv = data.result?.conversation;
936
1134
  if (!conv) return;
937
-
938
1135
  conv.messages.forEach(msg => {
939
- if (['user', 'assistant', 'agent', 'system'].includes(msg.role)) {
940
- this.addMessage(msg.role, msg.content, msg.message_id || null);
941
- }
1136
+ if (['user', 'assistant', 'agent', 'system'].includes(msg.role)) this.addMessage(msg.role, msg.content, msg.message_id || null);
942
1137
  });
943
1138
  this.last_message_count = conv.messages.length;
944
-
945
1139
  if (conv.status === 'closed') {
946
1140
  this.showClosed(conv.csat || null);
947
1141
  return;
948
1142
  }
949
-
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');
950
1146
  if (conv.status === 'bot' || conv.status === 'waiting_agent') {
951
1147
  if (close_bar) close_bar.style.display = 'block';
952
1148
  } else {
953
1149
  if (close_bar) close_bar.style.display = 'none';
954
1150
  }
955
-
956
1151
  if (conv.status === 'agent') {
957
1152
  this.agent_mode = true;
958
- const subtitle = this.querySelector('#tp-subtitle');
959
- if (subtitle) subtitle.innerHTML = `${icon('user', 12, 'margin-right:4px')} ${m.human_agent}`;
1153
+ this._updateSubtitle('agent');
960
1154
  if (agent_bar) agent_bar.style.display = 'none';
961
1155
  if (input_bar) input_bar.style.display = 'flex';
962
1156
  } else {
963
1157
  if (input_bar) input_bar.style.display = 'flex';
964
1158
  if (agent_bar) agent_bar.style.display = 'none';
965
- const subtitle = this.querySelector('#tp-subtitle');
966
- if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
1159
+ this._updateSubtitle('bot');
967
1160
  }
968
-
969
1161
  this.updateUILanguage();
970
1162
  this.polling_interval = setInterval(() => this.pollMessages(), 3000);
971
1163
  } catch (e) {
@@ -973,6 +1165,7 @@
973
1165
  }
974
1166
  }
975
1167
 
1168
+ // ─── FIXED: conv créée au premier sendMessage, pas à l'ouverture ──────────
976
1169
  async startNewConversation() {
977
1170
  this.view = 'chat';
978
1171
  this.is_closed = false;
@@ -980,71 +1173,78 @@
980
1173
  this.agent_requested = false;
981
1174
  this.messages = [];
982
1175
  this.last_message_count = 0;
983
- this.conversation_id = null;
984
-
985
- const m = this.getMessages();
986
- this.querySelector('#tp-back-btn').style.display = 'block';
987
- this.querySelector('#tp-close-bar').style.display = 'block';
1176
+ this.conversation_id = null; // pas encore créée
988
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);
989
1181
  const container = this.querySelector('#tp-messages');
990
- container.style.background = '#f5f5f7';
991
- container.style.padding = '16px';
992
1182
  container.innerHTML = '';
993
-
994
- const subtitle = this.querySelector('#tp-subtitle');
995
- if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
996
-
997
- this.querySelector('#tp-chatbot-input-bar').style.display = 'flex';
1183
+ this._updateSubtitle('bot');
1184
+ this.querySelector('#tp-input-bar').style.display = 'flex';
998
1185
  this.querySelector('#tp-agent-bar').style.display = 'none';
999
1186
  this.updateUILanguage();
1000
-
1001
- this.conversation_id = await this.createConversation();
1002
1187
  this.addMessage('assistant', this.getWelcomeMessage(this.user_info?.first_name || ''));
1003
- this.last_message_count = 1;
1004
-
1005
1188
  const theme = getClientTheme(this.client_id);
1006
1189
  if (theme.suggestions && theme.suggestions.length > 0) this.showSuggestions(theme.suggestions);
1007
-
1008
- this.polling_interval = setInterval(() => this.pollMessages(), 3000);
1190
+ // pas de polling — rien à poller tant que la conv n'est pas créée
1009
1191
  }
1010
1192
 
1011
- // ─── 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
+ }
1012
1201
 
1013
1202
  showGuestForm() {
1014
1203
  const container = this.querySelector('#tp-messages');
1015
1204
  if (!container) return;
1016
- this.querySelector('#tp-chatbot-input-bar').style.display = 'none';
1205
+ this.querySelector('#tp-input-bar').style.display = 'none';
1017
1206
  this.querySelector('#tp-agent-bar').style.display = 'none';
1018
- const color = this.getThemeColor();
1019
- const dark = this.shadeColor(color, -20);
1020
- const form_el = document.createElement('div');
1021
- form_el.id = 'tp-guest-form';
1022
- form_el.innerHTML = `
1023
- <div style="padding:16px;display:flex;flex-direction:column;gap:12px;">
1024
- <p style="margin:0;font-size:13px;color:#1a1a2e;font-weight:600;">Before we start, please identify yourself:</p>
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;" />
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;" />
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;" />
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>
1029
- <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>
1030
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>
1031
1224
  `;
1032
- container.appendChild(form_el);
1225
+ container.appendChild(wrap);
1033
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
+ });
1034
1232
  }
1035
1233
 
1234
+ // ─── FIXED: conv créée au premier sendMessage, pas après le formulaire ────
1036
1235
  async submitGuestForm() {
1037
1236
  const first_name = this.querySelector('#tp-guest-firstname')?.value.trim();
1038
1237
  const company_name = this.querySelector('#tp-guest-company')?.value.trim();
1039
1238
  const email = this.querySelector('#tp-guest-email')?.value.trim();
1040
1239
  const error_el = this.querySelector('#tp-guest-error');
1240
+ const m = this.getMessages();
1041
1241
  if (!first_name || !company_name || !email) {
1042
- error_el.textContent = 'Please fill in all fields.';
1242
+ error_el.textContent = m.guest_fill;
1043
1243
  error_el.style.display = 'block';
1044
1244
  return;
1045
1245
  }
1046
1246
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
1047
- error_el.textContent = 'Please enter a valid email.';
1247
+ error_el.textContent = m.guest_email_invalid;
1048
1248
  error_el.style.display = 'block';
1049
1249
  return;
1050
1250
  }
@@ -1052,66 +1252,49 @@
1052
1252
  this.user_info = { user_id: `guest-${email}`, first_name, last_name: '', company_name, email, language: 'EN', site_id: '' };
1053
1253
  this.user_id = this.user_info.user_id;
1054
1254
 
1055
- const form_el = this.querySelector('#tp-guest-form');
1056
- if (form_el) form_el.remove();
1255
+ const wrap = this.querySelector('#tp-guest-wrap');
1256
+ if (wrap) wrap.remove();
1057
1257
 
1058
1258
  this.view = 'chat';
1059
- this.conversation_id = await this.createConversation();
1060
-
1061
- 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';
1062
1261
  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';
1068
-
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);
1069
1265
  this.updateUILanguage();
1070
1266
  this.addMessage('assistant', this.getWelcomeMessage(first_name));
1071
- this.last_message_count = 1;
1072
-
1073
1267
  const theme = getClientTheme(this.client_id);
1074
1268
  if (theme.suggestions && theme.suggestions.length > 0) this.showSuggestions(theme.suggestions);
1075
-
1076
- this.polling_interval = setInterval(() => this.pollMessages(), 3000);
1269
+ // pas de polling — rien à poller tant que la conv n'est pas créée
1077
1270
  }
1078
1271
 
1079
- // ─── Messages ──────────────────────────────────────────────────────────────
1080
-
1081
- addMessage(role, content, message_id = null) {
1082
- 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() });
1083
1274
  const container = this.querySelector('#tp-messages');
1084
-
1275
+ const m = this.getMessages();
1085
1276
  const role_label =
1086
1277
  role === 'user'
1087
- ? `${icon('user', 11, 'margin-right:3px')} ${this.user_info?.first_name || 'You'}`
1278
+ ? `${icon('user', 11)} ${this.user_info?.first_name || 'You'}`
1088
1279
  : role === 'agent'
1089
- ? `${icon('agent', 11, 'margin-right:3px')} Agent`
1280
+ ? `${icon('agent', 11)} Agent`
1090
1281
  : role === 'system'
1091
1282
  ? 'Info'
1092
- : `${icon('bot', 11, 'margin-right:3px')} Maria`;
1093
-
1283
+ : `${icon('bot', 11)} Maria`;
1094
1284
  const msg_el = document.createElement('div');
1095
- msg_el.className = `tp-chatbot-message ${role}`;
1285
+ msg_el.className = `tp-msg ${role}`;
1096
1286
  const rendered_content =
1097
1287
  role === 'assistant' || role === 'agent' ? (typeof marked !== 'undefined' ? marked.parse(content) : content) : content;
1098
-
1288
+ const now = formatTime(new Date().toISOString());
1099
1289
  const feedback_html =
1100
1290
  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>`
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>`
1105
1292
  : '';
1106
-
1107
- msg_el.innerHTML = `
1108
- <div class="tp-chatbot-bubble-msg">
1109
- <div class="tp-chatbot-role">${role_label}</div>
1110
- <div class="tp-chatbot-content tp-chatbot-markdown">${rendered_content}</div>
1111
- ${feedback_html}
1112
- </div>
1113
- `;
1114
-
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
+ }
1115
1298
  if (role === 'assistant' && message_id) {
1116
1299
  const bar = msg_el.querySelector('.tp-feedback-bar');
1117
1300
  bar.querySelectorAll('.tp-feedback-btn').forEach(btn => {
@@ -1120,9 +1303,11 @@
1120
1303
  bar.dataset.voted = '1';
1121
1304
  const rating = btn.dataset.rating;
1122
1305
  bar.querySelectorAll('.tp-feedback-btn').forEach(b => {
1123
- 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';
1124
1308
  b.style.cursor = 'default';
1125
1309
  });
1310
+ btn.classList.add(rating === 'positive' ? 'voted-positive' : 'voted-negative');
1126
1311
  try {
1127
1312
  await fetch(`${this.api_url}/chat/conversations/${this.conversation_id}/feedback`, {
1128
1313
  method: 'POST',
@@ -1133,19 +1318,17 @@
1133
1318
  });
1134
1319
  });
1135
1320
  }
1136
-
1137
1321
  container.appendChild(msg_el);
1138
1322
  container.scrollTop = container.scrollHeight;
1139
-
1140
1323
  if ((role === 'assistant' || role === 'agent') && this.sound_enabled) playSound();
1141
1324
  }
1142
1325
 
1143
1326
  showTyping() {
1144
1327
  const container = this.querySelector('#tp-messages');
1145
1328
  const typing = document.createElement('div');
1146
- typing.className = 'tp-chatbot-message assistant';
1329
+ typing.className = 'tp-msg assistant';
1147
1330
  typing.id = 'tp-typing';
1148
- 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>`;
1149
1332
  container.appendChild(typing);
1150
1333
  container.scrollTop = container.scrollHeight;
1151
1334
  }
@@ -1161,56 +1344,33 @@
1161
1344
  clearInterval(this.polling_interval);
1162
1345
  this.polling_interval = null;
1163
1346
  }
1164
-
1165
1347
  const m = this.getMessages();
1166
- const subtitle = this.querySelector('#tp-subtitle');
1167
- if (subtitle) subtitle.innerHTML = `${icon('check', 12, 'margin-right:4px')} ${m.terminated}`;
1168
-
1169
- this.querySelector('#tp-chatbot-input-bar').style.display = 'none';
1348
+ this._updateSubtitle('closed');
1349
+ this.querySelector('#tp-input-bar').style.display = 'none';
1170
1350
  this.querySelector('#tp-agent-bar').style.display = 'none';
1171
1351
  this.querySelector('#tp-close-bar').style.display = 'none';
1172
1352
  const sugg_el = this.querySelector('#tp-suggestions');
1173
1353
  if (sugg_el) sugg_el.remove();
1174
-
1175
1354
  const container = this.querySelector('#tp-messages');
1176
- const existing_banner = this.querySelector('#tp-closed-banner');
1355
+ const existing_banner = container.querySelector('#tp-closed-banner');
1177
1356
  if (existing_banner) {
1178
1357
  if (existing_banner.querySelector('#tp-csat-block')) return;
1179
1358
  existing_banner.remove();
1180
1359
  }
1181
-
1182
- const color = this.getThemeColor();
1183
- const dark = this.shadeColor(color, -20);
1184
-
1360
+ const now = new Date().toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
1185
1361
  const csat_html = existing_csat
1186
- ? `<div style="font-size:13px;color:#888;margin-bottom:14px;">${existing_csat === 'positive' ? m.csat_positive : m.csat_negative}</div>`
1187
- : `<div style="font-size:12px;color:#888;margin-bottom:10px;">${m.csat_question}</div>
1188
- <div style="display:flex;gap:10px;justify-content:center;margin-bottom:14px;">
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>
1191
- </div>`;
1192
-
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>`;
1193
1364
  const banner = document.createElement('div');
1194
1365
  banner.id = 'tp-closed-banner';
1195
- banner.innerHTML = `
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>
1200
- <div id="tp-csat-block">${csat_html}</div>
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>
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>
1205
- </div>
1206
- `;
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>`;
1207
1368
  container.appendChild(banner);
1208
1369
  container.scrollTop = container.scrollHeight;
1209
-
1210
1370
  if (!existing_csat) {
1211
1371
  const submit_csat = async rating => {
1212
- const csat_block = this.querySelector('#tp-csat-block');
1213
- if (!csat_block) return;
1372
+ const block = this.querySelector('#tp-csat-block');
1373
+ if (!block) return;
1214
1374
  try {
1215
1375
  await fetch(`${this.api_url}/chat/conversations/${this.conversation_id}/csat`, {
1216
1376
  method: 'POST',
@@ -1220,20 +1380,21 @@
1220
1380
  } catch (e) {
1221
1381
  console.error('CSAT error:', e);
1222
1382
  }
1223
- 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>`;
1224
1384
  };
1225
- this.querySelector('#tp-csat-positive').addEventListener('click', () => submit_csat('positive'));
1226
- 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'));
1227
1387
  }
1228
-
1229
- this.querySelector('#tp-new-conversation').addEventListener('click', () => this.startNewConversation());
1230
- 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
+ });
1231
1393
  }
1232
1394
 
1233
- // ─── Send / Poll ───────────────────────────────────────────────────────────
1234
-
1395
+ // ─── FIXED: createConversation lazy au premier sendMessage ────────────────
1235
1396
  async sendMessage() {
1236
- if (this.is_closed || !this.conversation_id) return;
1397
+ if (this.is_closed) return;
1237
1398
  const m = this.getMessages();
1238
1399
  const input = this.querySelector('#tp-input');
1239
1400
  const query = input.value.trim();
@@ -1245,10 +1406,22 @@
1245
1406
  const user_messages = this.messages.filter(msg => msg.role === 'user').length;
1246
1407
  if (user_messages >= 30) {
1247
1408
  this.addMessage('system', m.limit_reached);
1248
- this.querySelector('#tp-chatbot-input-bar').style.display = 'none';
1409
+ this.querySelector('#tp-input-bar').style.display = 'none';
1249
1410
  return;
1250
1411
  }
1251
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
+
1252
1425
  input.value = '';
1253
1426
  this.is_loading = true;
1254
1427
  this.addMessage('user', query);
@@ -1271,14 +1444,10 @@
1271
1444
 
1272
1445
  this.agent_mode = data.result.agent_mode;
1273
1446
  const source = data.result.source || null;
1274
- const subtitle = this.querySelector('#tp-subtitle');
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}`;
1447
+ this._updateSubtitle(this.agent_mode ? 'agent' : 'bot');
1279
1448
 
1280
1449
  if (data.result.reply) {
1281
- 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);
1282
1451
  this.last_message_count += 2;
1283
1452
  }
1284
1453
 
@@ -1287,18 +1456,11 @@
1287
1456
  const suggest_agent = data.result.suggest_agent || false;
1288
1457
 
1289
1458
  if (agent_bar && !this.agent_requested && !this.agent_mode) {
1290
- if (
1291
- suggest_agent ||
1292
- source === 'fallback' ||
1293
- source === 'clarification' ||
1294
- source === 'no_match' ||
1295
- source === 'out_of_scope' ||
1296
- user_msg_count >= 5
1297
- ) {
1298
- agent_bar.style.display = 'block';
1299
- }
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';
1300
1463
  }
1301
-
1302
1464
  if (suggest_agent && !this.agent_requested) {
1303
1465
  this.addMessage('system', m.suggest_agent_escalation);
1304
1466
  this.last_message_count += 1;
@@ -1307,7 +1469,6 @@
1307
1469
  this.hideTyping();
1308
1470
  this.addMessage('assistant', this.getMessages().error_occurred);
1309
1471
  }
1310
-
1311
1472
  this.is_loading = false;
1312
1473
  }
1313
1474
 
@@ -1316,7 +1477,6 @@
1316
1477
  this.agent_requested = true;
1317
1478
  const agent_bar = this.querySelector('#tp-agent-bar');
1318
1479
  if (agent_bar) agent_bar.style.display = 'none';
1319
-
1320
1480
  try {
1321
1481
  const response = await fetch(`${this.api_url}/chat/conversations/${this.conversation_id}/request-agent`, {
1322
1482
  method: 'POST',
@@ -1324,21 +1484,18 @@
1324
1484
  body: JSON.stringify({ user_id: this.user_id }),
1325
1485
  });
1326
1486
  const data = await response.json();
1327
- const m = this.getMessages();
1328
- const status = data.result?.status;
1329
-
1487
+ const m = this.getMessages(),
1488
+ status = data.result?.status;
1330
1489
  let msg = '';
1331
1490
  if (status === 'outside_hours') msg = m.outside_hours(data.result.next_opening);
1332
1491
  else if (status === 'no_agents') msg = m.no_agents;
1333
1492
  else if (status === 'waiting_agent') msg = m.waiting_agent;
1334
1493
  else if (status === 'waiting_agent_already') msg = m.waiting_agent_already;
1335
1494
  else if (status === 'agent') msg = m.agent_already;
1336
-
1337
1495
  if (msg) {
1338
1496
  this.addMessage('system', msg);
1339
1497
  this.last_message_count += 1;
1340
1498
  }
1341
-
1342
1499
  if (status === 'no_agents' || status === 'outside_hours' || status === 'waiting_agent_already') {
1343
1500
  this.agent_requested = false;
1344
1501
  if (agent_bar) agent_bar.style.display = 'block';
@@ -1373,24 +1530,21 @@
1373
1530
  const data = await response.json();
1374
1531
  const conv = data.result?.conversation;
1375
1532
  if (!conv) return;
1376
-
1377
- const server_messages = conv.messages || [];
1378
- const server_status = conv.status;
1379
- 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;
1380
1536
  const m = this.getMessages();
1381
-
1382
1537
  if (server_status === 'closed' && !this.is_closed) {
1383
1538
  this.showClosed();
1384
1539
  return;
1385
1540
  }
1386
-
1387
1541
  if (is_typing && server_status === 'agent') {
1388
1542
  if (!this.querySelector('#tp-agent-typing')) {
1389
1543
  const container = this.querySelector('#tp-messages');
1390
1544
  const typing = document.createElement('div');
1391
- typing.className = 'tp-chatbot-message agent';
1545
+ typing.className = 'tp-msg agent';
1392
1546
  typing.id = 'tp-agent-typing';
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>`;
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>`;
1394
1548
  container.appendChild(typing);
1395
1549
  container.scrollTop = container.scrollHeight;
1396
1550
  }
@@ -1398,13 +1552,12 @@
1398
1552
  const typing_el = this.querySelector('#tp-agent-typing');
1399
1553
  if (typing_el) typing_el.remove();
1400
1554
  }
1401
-
1402
1555
  if (server_messages.length > this.last_message_count) {
1403
1556
  const new_messages = server_messages.slice(this.last_message_count);
1404
1557
  new_messages.forEach(msg => {
1405
1558
  if (msg.role === 'agent') {
1406
- const typing_el = this.querySelector('#tp-agent-typing');
1407
- if (typing_el) typing_el.remove();
1559
+ const t = this.querySelector('#tp-agent-typing');
1560
+ if (t) t.remove();
1408
1561
  this.addMessage('agent', msg.content, msg.message_id || null);
1409
1562
  } else if (msg.role === 'assistant') {
1410
1563
  this.addMessage('assistant', msg.content, msg.message_id || null);
@@ -1414,22 +1567,23 @@
1414
1567
  });
1415
1568
  this.last_message_count = server_messages.length;
1416
1569
  }
1417
-
1418
1570
  if (server_status === 'agent' && !this.agent_mode) {
1419
1571
  this.agent_mode = true;
1420
- const subtitle = this.querySelector('#tp-subtitle');
1421
- if (subtitle) subtitle.innerHTML = `${icon('user', 12, 'margin-right:4px')} ${m.human_agent}`;
1572
+ this._updateSubtitle('agent');
1422
1573
  const agent_bar = this.querySelector('#tp-agent-bar');
1423
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';
1424
1577
  this.addMessage('system', m.agent_taken);
1425
1578
  this.last_message_count = server_messages.length;
1426
1579
  } else if (server_status === 'bot' && this.agent_mode) {
1427
1580
  this.agent_mode = false;
1428
1581
  this.agent_requested = false;
1429
- const subtitle = this.querySelector('#tp-subtitle');
1430
- if (subtitle) subtitle.innerHTML = `${icon('bot', 12, 'margin-right:4px')} ${m.virtual}`;
1582
+ this._updateSubtitle('bot');
1431
1583
  const agent_bar = this.querySelector('#tp-agent-bar');
1432
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';
1433
1587
  this.addMessage('system', m.agent_released);
1434
1588
  this.last_message_count = server_messages.length;
1435
1589
  }