@baseportal/chat-widget 0.1.0

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.
@@ -0,0 +1,2286 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ BaseportalChat: () => BaseportalChat,
34
+ default: () => src_default
35
+ });
36
+ module.exports = __toCommonJS(src_exports);
37
+
38
+ // src/api/client.ts
39
+ var ApiClient = class {
40
+ constructor(channelToken, apiUrl) {
41
+ this.channelToken = channelToken;
42
+ this.baseUrl = `${apiUrl}/public/chat`;
43
+ }
44
+ setVisitorIdentity(email, hash) {
45
+ this.visitorEmail = email;
46
+ this.visitorHash = hash;
47
+ }
48
+ clearVisitorIdentity() {
49
+ this.visitorEmail = void 0;
50
+ this.visitorHash = void 0;
51
+ }
52
+ headers() {
53
+ const h2 = {
54
+ "Content-Type": "application/json",
55
+ "x-channel-token": this.channelToken
56
+ };
57
+ if (this.visitorEmail) h2["x-visitor-email"] = this.visitorEmail;
58
+ if (this.visitorHash) h2["x-visitor-hash"] = this.visitorHash;
59
+ return h2;
60
+ }
61
+ async request(method, path, body) {
62
+ const res = await fetch(`${this.baseUrl}${path}`, {
63
+ method,
64
+ headers: this.headers(),
65
+ body: body ? JSON.stringify(body) : void 0
66
+ });
67
+ if (!res.ok) {
68
+ const text = await res.text().catch(() => "");
69
+ throw new Error(`[BaseportalChat] API error ${res.status}: ${text}`);
70
+ }
71
+ return res.json();
72
+ }
73
+ async getChannelInfo() {
74
+ return this.request("GET", "/channel-info");
75
+ }
76
+ async initConversation(data) {
77
+ return this.request("POST", "/conversations", {
78
+ ...data,
79
+ channelToken: this.channelToken
80
+ });
81
+ }
82
+ async getMessages(conversationId, params) {
83
+ const qs = new URLSearchParams();
84
+ if (params?.limit) qs.set("limit", String(params.limit));
85
+ if (params?.page) qs.set("page", String(params.page));
86
+ const query = qs.toString() ? `?${qs.toString()}` : "";
87
+ return this.request("GET", `/conversations/${conversationId}/messages${query}`);
88
+ }
89
+ async uploadFile(conversationId, file) {
90
+ const formData = new FormData();
91
+ formData.append("file", file);
92
+ const headers = {
93
+ "x-channel-token": this.channelToken
94
+ };
95
+ if (this.visitorEmail) headers["x-visitor-email"] = this.visitorEmail;
96
+ if (this.visitorHash) headers["x-visitor-hash"] = this.visitorHash;
97
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/upload`, {
98
+ method: "POST",
99
+ headers,
100
+ body: formData
101
+ });
102
+ if (!res.ok) {
103
+ const text = await res.text().catch(() => "");
104
+ throw new Error(`[BaseportalChat] Upload error ${res.status}: ${text}`);
105
+ }
106
+ return res.json();
107
+ }
108
+ async sendMessage(conversationId, data) {
109
+ return this.request(
110
+ "POST",
111
+ `/conversations/${conversationId}/messages`,
112
+ data
113
+ );
114
+ }
115
+ async getVisitorConversations() {
116
+ return this.request("GET", "/conversations");
117
+ }
118
+ async reopenConversation(conversationId) {
119
+ return this.request("POST", `/conversations/${conversationId}/reopen`);
120
+ }
121
+ async getAblyToken(conversationId) {
122
+ return this.request("POST", "/ably-token", { conversationId });
123
+ }
124
+ };
125
+
126
+ // src/realtime/ably-client.ts
127
+ var Ably = __toESM(require("ably"));
128
+ var RealtimeClient = class {
129
+ constructor(apiClient) {
130
+ this.client = null;
131
+ this.channel = null;
132
+ this.conversationId = null;
133
+ this.handlers = null;
134
+ this.apiClient = apiClient;
135
+ }
136
+ async subscribe(conversationId, handlers) {
137
+ this.unsubscribe();
138
+ this.conversationId = conversationId;
139
+ this.handlers = handlers;
140
+ try {
141
+ const tokenRequest = await this.apiClient.getAblyToken(conversationId);
142
+ this.client = new Ably.Realtime({
143
+ authCallback: (_data, callback) => {
144
+ callback(null, tokenRequest);
145
+ },
146
+ clientId: `visitor-${conversationId}`
147
+ });
148
+ const channelName = `conversation-${conversationId}`;
149
+ this.channel = this.client.channels.get(channelName);
150
+ this.channel.subscribe((msg) => {
151
+ if (!msg.data) return;
152
+ try {
153
+ const data = typeof msg.data === "string" ? JSON.parse(msg.data) : msg.data;
154
+ if (data.text === "conversation_status_updated" && data.metadata) {
155
+ handlers.onConversationStatusUpdate(data.metadata);
156
+ } else if (data.text === "created_or_updated_message" && data.metadata) {
157
+ handlers.onMessage(data.metadata);
158
+ }
159
+ } catch (e) {
160
+ console.error("[BaseportalChat] Error parsing realtime message:", e);
161
+ }
162
+ });
163
+ } catch (e) {
164
+ console.error("[BaseportalChat] Error connecting to realtime:", e);
165
+ }
166
+ }
167
+ unsubscribe() {
168
+ if (this.channel) {
169
+ this.channel.unsubscribe();
170
+ this.channel = null;
171
+ }
172
+ if (this.client) {
173
+ this.client.close();
174
+ this.client = null;
175
+ }
176
+ this.conversationId = null;
177
+ this.handlers = null;
178
+ }
179
+ isConnected() {
180
+ return this.client?.connection.state === "connected";
181
+ }
182
+ };
183
+
184
+ // src/ui/i18n.ts
185
+ var pt = {
186
+ prechat: {
187
+ title: "Iniciar conversa",
188
+ description: "Preencha os dados abaixo para iniciar o atendimento.",
189
+ name: "Nome",
190
+ namePlaceholder: "Seu nome",
191
+ email: "E-mail",
192
+ emailPlaceholder: "seu@email.com",
193
+ start: "Iniciar conversa",
194
+ loading: "Iniciando...",
195
+ privacyPrefix: "Ao enviar, voc\xEA concorda com nossa",
196
+ privacyLink: "Pol\xEDtica de Privacidade"
197
+ },
198
+ chat: {
199
+ placeholder: "Digite uma mensagem...",
200
+ closed: "Esta conversa foi encerrada.",
201
+ reopen: "Reabrir conversa",
202
+ attachFile: "Anexar arquivo",
203
+ uploading: "Enviando...",
204
+ fileTooLarge: "Arquivo muito grande (m\xE1x. 25MB)",
205
+ download: "Baixar"
206
+ },
207
+ conversations: {
208
+ title: "Atendimento",
209
+ newConversation: "Nova conversa",
210
+ empty: "Nenhuma conversa encontrada.",
211
+ open: "Aberta",
212
+ closed: "Fechada",
213
+ noMessages: "Nenhuma mensagem ainda"
214
+ }
215
+ };
216
+ var en = {
217
+ prechat: {
218
+ title: "Start a conversation",
219
+ description: "Fill in the details below to start chatting.",
220
+ name: "Name",
221
+ namePlaceholder: "Your name",
222
+ email: "Email",
223
+ emailPlaceholder: "you@email.com",
224
+ start: "Start conversation",
225
+ loading: "Starting...",
226
+ privacyPrefix: "By sending, you agree to our",
227
+ privacyLink: "Privacy Policy"
228
+ },
229
+ chat: {
230
+ placeholder: "Type a message...",
231
+ closed: "This conversation has been closed.",
232
+ reopen: "Reopen conversation",
233
+ attachFile: "Attach file",
234
+ uploading: "Uploading...",
235
+ fileTooLarge: "File too large (max 25MB)",
236
+ download: "Download"
237
+ },
238
+ conversations: {
239
+ title: "Support",
240
+ newConversation: "New conversation",
241
+ empty: "No conversations found.",
242
+ open: "Open",
243
+ closed: "Closed",
244
+ noMessages: "No messages yet"
245
+ }
246
+ };
247
+ var es = {
248
+ prechat: {
249
+ title: "Iniciar conversaci\xF3n",
250
+ description: "Complete los datos a continuaci\xF3n para iniciar la atenci\xF3n.",
251
+ name: "Nombre",
252
+ namePlaceholder: "Tu nombre",
253
+ email: "Correo electr\xF3nico",
254
+ emailPlaceholder: "tu@email.com",
255
+ start: "Iniciar conversaci\xF3n",
256
+ loading: "Iniciando...",
257
+ privacyPrefix: "Al enviar, aceptas nuestra",
258
+ privacyLink: "Pol\xEDtica de Privacidad"
259
+ },
260
+ chat: {
261
+ placeholder: "Escribe un mensaje...",
262
+ closed: "Esta conversaci\xF3n ha sido cerrada.",
263
+ reopen: "Reabrir conversaci\xF3n",
264
+ attachFile: "Adjuntar archivo",
265
+ uploading: "Subiendo...",
266
+ fileTooLarge: "Archivo demasiado grande (m\xE1x. 25MB)",
267
+ download: "Descargar"
268
+ },
269
+ conversations: {
270
+ title: "Atenci\xF3n",
271
+ newConversation: "Nueva conversaci\xF3n",
272
+ empty: "No se encontraron conversaciones.",
273
+ open: "Abierta",
274
+ closed: "Cerrada",
275
+ noMessages: "Sin mensajes a\xFAn"
276
+ }
277
+ };
278
+ var locales = { pt, en, es };
279
+ function getTranslations(locale) {
280
+ return locales[locale] || locales["pt"];
281
+ }
282
+
283
+ // src/ui/mount.ts
284
+ var import_preact = require("preact");
285
+
286
+ // src/ui/App.tsx
287
+ var import_hooks6 = require("preact/hooks");
288
+
289
+ // src/ui/icons.tsx
290
+ var import_jsx_runtime = require("preact/jsx-runtime");
291
+ var IconChat = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) });
292
+ var IconX = () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
293
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
294
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
295
+ ] });
296
+ var IconArrowLeft = () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
297
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "19", y1: "12", x2: "5", y2: "12" }),
298
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "12 19 5 12 12 5" })
299
+ ] });
300
+ var IconSend = () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
301
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
302
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
303
+ ] });
304
+ var IconPlus = () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
305
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", y1: "5", x2: "12", y2: "19" }),
306
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "5", y1: "12", x2: "19", y2: "12" })
307
+ ] });
308
+ var IconPaperclip = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" }) });
309
+ var IconDownload = () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
310
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
311
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "7 10 12 15 17 10" }),
312
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", y1: "15", x2: "12", y2: "3" })
313
+ ] });
314
+ var IconFile = () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
315
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
316
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: "14 2 14 8 20 8" })
317
+ ] });
318
+
319
+ // src/ui/components/ChatBubble.tsx
320
+ var import_jsx_runtime2 = require("preact/jsx-runtime");
321
+ function ChatBubble({
322
+ isOpen,
323
+ position,
324
+ unreadCount,
325
+ onClick
326
+ }) {
327
+ const posClass = position === "bottom-left" ? "bp-bubble--left" : "bp-bubble--right";
328
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
329
+ "button",
330
+ {
331
+ class: `bp-bubble ${posClass}`,
332
+ onClick,
333
+ "aria-label": isOpen ? "Close chat" : "Open chat",
334
+ children: [
335
+ isOpen ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IconX, {}) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(IconChat, {}),
336
+ !isOpen && unreadCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { class: "bp-bubble__badge", children: unreadCount > 99 ? "99+" : unreadCount })
337
+ ]
338
+ }
339
+ );
340
+ }
341
+
342
+ // src/ui/components/ChatWindow.tsx
343
+ var import_hooks5 = require("preact/hooks");
344
+
345
+ // src/ui/components/ConversationList.tsx
346
+ var import_jsx_runtime3 = require("preact/jsx-runtime");
347
+ function ConversationList({
348
+ conversations,
349
+ channelInfo,
350
+ loading,
351
+ onSelect,
352
+ onNew,
353
+ t
354
+ }) {
355
+ const hasOpen = conversations.some((c) => c.open);
356
+ const canReopen = channelInfo.config.allowReopenConversation;
357
+ if (loading) {
358
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { class: "bp-loading", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { class: "bp-spinner" }) });
359
+ }
360
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { class: "bp-convlist", children: [
361
+ !hasOpen && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { class: "bp-convlist__new", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { class: "bp-convlist__new-btn", onClick: onNew, children: [
362
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(IconPlus, {}),
363
+ t.conversations.newConversation
364
+ ] }) }),
365
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { class: "bp-convlist__items", children: conversations.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { class: "bp-convlist__empty", children: t.conversations.empty }) : conversations.map((conv) => {
366
+ const isClickable = conv.open || canReopen;
367
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
368
+ "button",
369
+ {
370
+ class: "bp-convlist__item",
371
+ onClick: () => isClickable && onSelect(conv),
372
+ disabled: !isClickable,
373
+ children: [
374
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { class: "bp-convlist__item-top", children: [
375
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { class: "bp-convlist__item-title", children: channelInfo.name }),
376
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
377
+ "span",
378
+ {
379
+ class: `bp-convlist__item-status ${conv.open ? "bp-convlist__item-status--open" : "bp-convlist__item-status--closed"}`,
380
+ children: conv.open ? t.conversations.open : t.conversations.closed
381
+ }
382
+ )
383
+ ] }),
384
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { class: "bp-convlist__item-preview", children: conv.lastMessage?.content || t.conversations.noMessages })
385
+ ]
386
+ },
387
+ conv.id
388
+ );
389
+ }) })
390
+ ] });
391
+ }
392
+
393
+ // src/ui/components/MessageInput.tsx
394
+ var import_hooks = require("preact/hooks");
395
+ var import_jsx_runtime4 = require("preact/jsx-runtime");
396
+ function MessageInput({
397
+ value,
398
+ onChange,
399
+ onSend,
400
+ onFileSelect,
401
+ onFileRemove,
402
+ attachedFile,
403
+ uploading,
404
+ disabled,
405
+ placeholder,
406
+ t
407
+ }) {
408
+ const textareaRef = (0, import_hooks.useRef)(null);
409
+ const fileInputRef = (0, import_hooks.useRef)(null);
410
+ const handleKeyDown = (0, import_hooks.useCallback)(
411
+ (e) => {
412
+ if (e.key === "Enter" && !e.shiftKey) {
413
+ e.preventDefault();
414
+ onSend();
415
+ }
416
+ },
417
+ [onSend]
418
+ );
419
+ const handleInput = (0, import_hooks.useCallback)(
420
+ (e) => {
421
+ const target = e.target;
422
+ onChange(target.value);
423
+ target.style.height = "auto";
424
+ target.style.height = `${Math.min(target.scrollHeight, 100)}px`;
425
+ },
426
+ [onChange]
427
+ );
428
+ const handleAttachClick = (0, import_hooks.useCallback)(() => {
429
+ fileInputRef.current?.click();
430
+ }, []);
431
+ const handleFileChange = (0, import_hooks.useCallback)(
432
+ (e) => {
433
+ const input = e.target;
434
+ const file = input.files?.[0];
435
+ if (file) {
436
+ onFileSelect(file);
437
+ }
438
+ input.value = "";
439
+ },
440
+ [onFileSelect]
441
+ );
442
+ const isImage = attachedFile?.file.type.startsWith("image/");
443
+ const canSend = (value.trim() || attachedFile) && !disabled && !uploading;
444
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { class: "bp-composer", children: [
445
+ attachedFile && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { class: "bp-composer__preview", children: [
446
+ isImage && attachedFile.preview ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
447
+ "img",
448
+ {
449
+ src: attachedFile.preview,
450
+ alt: attachedFile.file.name,
451
+ class: "bp-composer__preview-thumb"
452
+ }
453
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { class: "bp-composer__preview-icon", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IconFile, {}) }),
454
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { class: "bp-composer__preview-info", children: [
455
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { class: "bp-composer__preview-name", children: attachedFile.file.name }),
456
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { class: "bp-composer__preview-status", children: uploading ? t.chat.uploading : formatSize(attachedFile.file.size) })
457
+ ] }),
458
+ !uploading && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
459
+ "button",
460
+ {
461
+ class: "bp-composer__preview-remove",
462
+ onClick: onFileRemove,
463
+ "aria-label": "Remove file",
464
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IconX, {})
465
+ }
466
+ )
467
+ ] }),
468
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { class: "bp-composer__row", children: [
469
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
470
+ "button",
471
+ {
472
+ class: "bp-composer__attach",
473
+ onClick: handleAttachClick,
474
+ disabled: disabled || uploading || !!attachedFile,
475
+ "aria-label": t.chat.attachFile,
476
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IconPaperclip, {})
477
+ }
478
+ ),
479
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
480
+ "textarea",
481
+ {
482
+ ref: textareaRef,
483
+ class: "bp-composer__field",
484
+ value,
485
+ onInput: handleInput,
486
+ onKeyDown: handleKeyDown,
487
+ placeholder: placeholder || t.chat.placeholder,
488
+ disabled,
489
+ rows: 1
490
+ }
491
+ ),
492
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
493
+ "button",
494
+ {
495
+ class: "bp-composer__send",
496
+ onClick: onSend,
497
+ disabled: !canSend,
498
+ "aria-label": "Send message",
499
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(IconSend, {})
500
+ }
501
+ )
502
+ ] }),
503
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
504
+ "input",
505
+ {
506
+ type: "file",
507
+ ref: fileInputRef,
508
+ onChange: handleFileChange,
509
+ style: { display: "none" },
510
+ accept: "image/*,video/mp4,audio/*,.pdf,.doc,.docx,.xls,.xlsx,.txt"
511
+ }
512
+ )
513
+ ] });
514
+ }
515
+ function formatSize(bytes) {
516
+ if (bytes < 1024) return `${bytes} B`;
517
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
518
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
519
+ }
520
+
521
+ // src/ui/components/MessageList.tsx
522
+ var import_hooks3 = require("preact/hooks");
523
+
524
+ // src/ui/components/ImageLightbox.tsx
525
+ var import_hooks2 = require("preact/hooks");
526
+ var import_jsx_runtime5 = require("preact/jsx-runtime");
527
+ function ImageLightbox({ src, alt, onClose }) {
528
+ const handleKeyDown = (0, import_hooks2.useCallback)(
529
+ (e) => {
530
+ if (e.key === "Escape") onClose();
531
+ },
532
+ [onClose]
533
+ );
534
+ (0, import_hooks2.useEffect)(() => {
535
+ document.addEventListener("keydown", handleKeyDown);
536
+ return () => document.removeEventListener("keydown", handleKeyDown);
537
+ }, [handleKeyDown]);
538
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { class: "bp-lightbox", onClick: onClose, children: [
539
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { class: "bp-lightbox__close", onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(IconX, {}) }),
540
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
541
+ "img",
542
+ {
543
+ src,
544
+ alt: alt || "",
545
+ class: "bp-lightbox__img",
546
+ onClick: (e) => e.stopPropagation()
547
+ }
548
+ )
549
+ ] });
550
+ }
551
+
552
+ // src/ui/components/MessageMedia.tsx
553
+ var import_jsx_runtime6 = require("preact/jsx-runtime");
554
+ function MessageMedia({ media, onImageClick, t }) {
555
+ const mimeType = (media.mimeType || "").toLowerCase();
556
+ if (mimeType.startsWith("image/") || media.kind === "image") {
557
+ const thumbSrc = media.streamUrlData?.small || media.url;
558
+ const fullSrc = media.streamUrlData?.large || media.url;
559
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
560
+ "img",
561
+ {
562
+ src: thumbSrc,
563
+ alt: media.name,
564
+ class: "bp-media-img",
565
+ onClick: () => onImageClick(fullSrc)
566
+ }
567
+ );
568
+ }
569
+ if (mimeType.startsWith("video/")) {
570
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("video", { controls: true, class: "bp-media-video", preload: "metadata", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("source", { src: media.url, type: mimeType }) });
571
+ }
572
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
573
+ "a",
574
+ {
575
+ href: media.url,
576
+ target: "_blank",
577
+ rel: "noopener noreferrer",
578
+ class: "bp-media-file",
579
+ download: media.name,
580
+ children: [
581
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { class: "bp-media-file__icon", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(IconFile, {}) }),
582
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { class: "bp-media-file__name", children: media.name }),
583
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { class: "bp-media-file__download", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(IconDownload, {}) })
584
+ ]
585
+ }
586
+ );
587
+ }
588
+
589
+ // src/ui/components/MessageList.tsx
590
+ var import_jsx_runtime7 = require("preact/jsx-runtime");
591
+ function MessageList({ messages, loading, t }) {
592
+ const endRef = (0, import_hooks3.useRef)(null);
593
+ const [lightboxSrc, setLightboxSrc] = (0, import_hooks3.useState)(null);
594
+ (0, import_hooks3.useEffect)(() => {
595
+ endRef.current?.scrollIntoView({ behavior: "smooth" });
596
+ }, [messages]);
597
+ if (loading) {
598
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { class: "bp-loading", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { class: "bp-spinner" }) });
599
+ }
600
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { class: "bp-messages", children: [
601
+ messages.map((msg) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
602
+ MessageBubble,
603
+ {
604
+ message: msg,
605
+ onImageClick: setLightboxSrc,
606
+ t
607
+ },
608
+ msg.id
609
+ )),
610
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: endRef }),
611
+ lightboxSrc && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
612
+ ImageLightbox,
613
+ {
614
+ src: lightboxSrc,
615
+ onClose: () => setLightboxSrc(null)
616
+ }
617
+ )
618
+ ] });
619
+ }
620
+ function MessageBubble({
621
+ message,
622
+ onImageClick,
623
+ t
624
+ }) {
625
+ const isClient = message.role === "client";
626
+ const cls = isClient ? "bp-msg bp-msg--client" : "bp-msg bp-msg--agent";
627
+ const time = new Date(message.createdAt).toLocaleTimeString([], {
628
+ hour: "2-digit",
629
+ minute: "2-digit"
630
+ });
631
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { class: cls, children: [
632
+ !isClient && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { class: "bp-msg__avatar", children: message.user?.avatar?.url ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
633
+ "img",
634
+ {
635
+ src: message.user.avatar.url,
636
+ alt: message.user.firstName || "Agent"
637
+ }
638
+ ) : (message.user?.firstName?.[0] || "A").toUpperCase() }),
639
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { class: "bp-msg__body", children: [
640
+ message.media && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
641
+ MessageMedia,
642
+ {
643
+ media: message.media,
644
+ onImageClick,
645
+ t
646
+ }
647
+ ),
648
+ message.content && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { class: "bp-msg__content", children: message.content }),
649
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { class: "bp-msg__time", children: time })
650
+ ] })
651
+ ] });
652
+ }
653
+
654
+ // src/ui/components/PreChatForm.tsx
655
+ var import_hooks4 = require("preact/hooks");
656
+ var import_jsx_runtime8 = require("preact/jsx-runtime");
657
+ function PreChatForm({
658
+ channelInfo,
659
+ onSubmit,
660
+ loading,
661
+ t
662
+ }) {
663
+ const [name, setName] = (0, import_hooks4.useState)("");
664
+ const [email, setEmail] = (0, import_hooks4.useState)("");
665
+ const { requireName, requireEmail, privacyPolicyUrl } = channelInfo.config;
666
+ const handleSubmit = (e) => {
667
+ e.preventDefault();
668
+ onSubmit({
669
+ name: name.trim() || void 0,
670
+ email: email.trim() || void 0
671
+ });
672
+ };
673
+ const isValid = (!requireName || name.trim()) && (!requireEmail || email.trim());
674
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("form", { class: "bp-prechat", onSubmit: handleSubmit, children: [
675
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { class: "bp-prechat__title", children: t.prechat.title }),
676
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { class: "bp-prechat__desc", children: t.prechat.description }),
677
+ requireName && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { class: "bp-prechat__field", children: [
678
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("label", { class: "bp-prechat__label", children: t.prechat.name }),
679
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
680
+ "input",
681
+ {
682
+ class: "bp-prechat__input",
683
+ type: "text",
684
+ value: name,
685
+ onInput: (e) => setName(e.target.value),
686
+ placeholder: t.prechat.namePlaceholder,
687
+ required: true
688
+ }
689
+ )
690
+ ] }),
691
+ requireEmail && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { class: "bp-prechat__field", children: [
692
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("label", { class: "bp-prechat__label", children: t.prechat.email }),
693
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
694
+ "input",
695
+ {
696
+ class: "bp-prechat__input",
697
+ type: "email",
698
+ value: email,
699
+ onInput: (e) => setEmail(e.target.value),
700
+ placeholder: t.prechat.emailPlaceholder,
701
+ required: true
702
+ }
703
+ )
704
+ ] }),
705
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
706
+ "button",
707
+ {
708
+ class: "bp-prechat__submit",
709
+ type: "submit",
710
+ disabled: !isValid || loading,
711
+ children: loading ? t.prechat.loading : t.prechat.start
712
+ }
713
+ ),
714
+ privacyPolicyUrl && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { class: "bp-prechat__privacy", children: [
715
+ t.prechat.privacyPrefix,
716
+ " ",
717
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("a", { href: privacyPolicyUrl, target: "_blank", rel: "noopener noreferrer", children: t.prechat.privacyLink })
718
+ ] })
719
+ ] });
720
+ }
721
+
722
+ // src/ui/components/ChatWindow.tsx
723
+ var import_jsx_runtime9 = require("preact/jsx-runtime");
724
+ function ChatWindow({
725
+ channelInfo,
726
+ apiClient,
727
+ realtimeClient,
728
+ storage,
729
+ events,
730
+ visitor,
731
+ isAuthenticated,
732
+ position,
733
+ onClose,
734
+ t,
735
+ initialConversationId
736
+ }) {
737
+ const [view, setView] = (0, import_hooks5.useState)("chat");
738
+ const [conversation, setConversation] = (0, import_hooks5.useState)(null);
739
+ const [conversations, setConversations] = (0, import_hooks5.useState)([]);
740
+ const [messages, setMessages] = (0, import_hooks5.useState)([]);
741
+ const [inputValue, setInputValue] = (0, import_hooks5.useState)("");
742
+ const [loading, setLoading] = (0, import_hooks5.useState)(true);
743
+ const [sending, setSending] = (0, import_hooks5.useState)(false);
744
+ const [attachedFile, setAttachedFile] = (0, import_hooks5.useState)(null);
745
+ const [uploadedFileId, setUploadedFileId] = (0, import_hooks5.useState)(null);
746
+ const [uploading, setUploading] = (0, import_hooks5.useState)(false);
747
+ const isOpen = conversation?.open !== false;
748
+ const allowViewHistory = channelInfo.config.allowViewHistory && isAuthenticated;
749
+ (0, import_hooks5.useEffect)(() => {
750
+ const init = async () => {
751
+ setLoading(true);
752
+ try {
753
+ if (allowViewHistory) {
754
+ const convs = await apiClient.getVisitorConversations();
755
+ setConversations(convs);
756
+ if (initialConversationId) {
757
+ const conv = convs.find((c) => c.id === initialConversationId);
758
+ if (conv) {
759
+ await openConversation(conv);
760
+ return;
761
+ }
762
+ }
763
+ if (convs.length > 0) {
764
+ setView("conversations");
765
+ } else {
766
+ if (needsPreChat()) {
767
+ setView("prechat");
768
+ } else {
769
+ await startNewConversation();
770
+ }
771
+ }
772
+ } else {
773
+ const storedId = initialConversationId || storage.getConversationId();
774
+ if (storedId) {
775
+ try {
776
+ const msgs = await apiClient.getMessages(storedId, { limit: 50 });
777
+ setMessages(Array.isArray(msgs) ? msgs.reverse() : []);
778
+ setConversation({ id: storedId, open: true });
779
+ setView("chat");
780
+ connectRealtime(storedId);
781
+ } catch {
782
+ storage.clear();
783
+ if (needsPreChat()) {
784
+ setView("prechat");
785
+ } else {
786
+ await startNewConversation();
787
+ }
788
+ }
789
+ } else {
790
+ if (needsPreChat()) {
791
+ setView("prechat");
792
+ } else {
793
+ await startNewConversation();
794
+ }
795
+ }
796
+ }
797
+ } catch (e) {
798
+ console.error("[BaseportalChat] Error initializing:", e);
799
+ } finally {
800
+ setLoading(false);
801
+ }
802
+ };
803
+ init();
804
+ return () => {
805
+ realtimeClient.unsubscribe();
806
+ };
807
+ }, []);
808
+ const needsPreChat = (0, import_hooks5.useCallback)(() => {
809
+ if (visitor?.name && visitor?.email) return false;
810
+ return channelInfo.config.requireName || channelInfo.config.requireEmail;
811
+ }, [channelInfo, visitor]);
812
+ const connectRealtime = (0, import_hooks5.useCallback)(
813
+ (convId) => {
814
+ realtimeClient.subscribe(convId, {
815
+ onMessage: (msg) => {
816
+ setMessages((prev) => {
817
+ if (prev.some((m) => m.id === msg.id)) {
818
+ return prev.map(
819
+ (m) => m.id === msg.id ? { ...m, ...msg } : m
820
+ );
821
+ }
822
+ const withoutTemp = prev.filter(
823
+ (m) => !String(m.id).startsWith("temp-") || m.content !== msg.content
824
+ );
825
+ return [...withoutTemp, msg];
826
+ });
827
+ events.emit("message:received", msg);
828
+ },
829
+ onConversationStatusUpdate: (conv) => {
830
+ setConversation(
831
+ (prev) => prev ? { ...prev, open: conv.open } : prev
832
+ );
833
+ if (!conv.open) {
834
+ events.emit("conversation:closed", conv);
835
+ }
836
+ }
837
+ });
838
+ },
839
+ [realtimeClient, events]
840
+ );
841
+ const openConversation = (0, import_hooks5.useCallback)(
842
+ async (conv) => {
843
+ setLoading(true);
844
+ try {
845
+ const msgs = await apiClient.getMessages(conv.id, { limit: 50 });
846
+ setMessages(Array.isArray(msgs) ? msgs.reverse() : []);
847
+ setConversation(conv);
848
+ setView("chat");
849
+ storage.setConversationId(conv.id);
850
+ connectRealtime(conv.id);
851
+ } catch (e) {
852
+ console.error("[BaseportalChat] Error opening conversation:", e);
853
+ } finally {
854
+ setLoading(false);
855
+ }
856
+ },
857
+ [apiClient, storage, connectRealtime]
858
+ );
859
+ const startNewConversation = (0, import_hooks5.useCallback)(async () => {
860
+ setLoading(true);
861
+ try {
862
+ const result = await apiClient.initConversation({
863
+ name: visitor?.name,
864
+ email: visitor?.email
865
+ });
866
+ setConversation(result);
867
+ setMessages(result.messages || []);
868
+ setView("chat");
869
+ storage.setConversationId(result.id);
870
+ connectRealtime(result.id);
871
+ events.emit("conversation:started", result);
872
+ } catch (e) {
873
+ console.error("[BaseportalChat] Error starting conversation:", e);
874
+ } finally {
875
+ setLoading(false);
876
+ }
877
+ }, [apiClient, visitor, storage, connectRealtime, events]);
878
+ const handlePreChatSubmit = (0, import_hooks5.useCallback)(
879
+ async (data) => {
880
+ storage.setVisitor({ ...visitor, ...data });
881
+ const result = await apiClient.initConversation(data);
882
+ setConversation(result);
883
+ setMessages(result.messages || []);
884
+ setView("chat");
885
+ storage.setConversationId(result.id);
886
+ connectRealtime(result.id);
887
+ events.emit("conversation:started", result);
888
+ },
889
+ [apiClient, visitor, storage, connectRealtime, events]
890
+ );
891
+ const handleFileSelect = (0, import_hooks5.useCallback)(
892
+ async (file) => {
893
+ if (!conversation) return;
894
+ const MAX_SIZE = 25 * 1024 * 1024;
895
+ if (file.size > MAX_SIZE) {
896
+ console.warn("[BaseportalChat] File too large");
897
+ return;
898
+ }
899
+ const preview = file.type.startsWith("image/") ? URL.createObjectURL(file) : void 0;
900
+ setAttachedFile({ file, preview });
901
+ setUploading(true);
902
+ try {
903
+ const uploaded = await apiClient.uploadFile(conversation.id, file);
904
+ setUploadedFileId(uploaded.id);
905
+ } catch (e) {
906
+ console.error("[BaseportalChat] Error uploading file:", e);
907
+ setAttachedFile(null);
908
+ if (preview) URL.revokeObjectURL(preview);
909
+ } finally {
910
+ setUploading(false);
911
+ }
912
+ },
913
+ [apiClient, conversation]
914
+ );
915
+ const handleFileRemove = (0, import_hooks5.useCallback)(() => {
916
+ if (attachedFile?.preview) URL.revokeObjectURL(attachedFile.preview);
917
+ setAttachedFile(null);
918
+ setUploadedFileId(null);
919
+ }, [attachedFile]);
920
+ const handleSend = (0, import_hooks5.useCallback)(async () => {
921
+ const content = inputValue.trim();
922
+ if (!content && !uploadedFileId || !conversation || sending) return;
923
+ const tempId = `temp-${Date.now()}`;
924
+ const optimistic = {
925
+ id: tempId,
926
+ content,
927
+ role: "client",
928
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
929
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
930
+ };
931
+ const mediaId = uploadedFileId || void 0;
932
+ setInputValue("");
933
+ setAttachedFile(null);
934
+ setUploadedFileId(null);
935
+ setSending(true);
936
+ setMessages((prev) => [...prev, optimistic]);
937
+ try {
938
+ const msg = await apiClient.sendMessage(conversation.id, {
939
+ content: content || void 0,
940
+ mediaId
941
+ });
942
+ setMessages((prev) => prev.map((m) => m.id === tempId ? msg : m));
943
+ events.emit("message:sent", msg);
944
+ } catch (e) {
945
+ console.error("[BaseportalChat] Error sending message:", e);
946
+ setMessages((prev) => prev.filter((m) => m.id !== tempId));
947
+ setInputValue(content);
948
+ } finally {
949
+ setSending(false);
950
+ }
951
+ }, [inputValue, uploadedFileId, conversation, sending, apiClient, events]);
952
+ const handleReopen = (0, import_hooks5.useCallback)(async () => {
953
+ if (!conversation) return;
954
+ try {
955
+ const updated = await apiClient.reopenConversation(conversation.id);
956
+ setConversation((prev) => prev ? { ...prev, open: updated.open ?? true } : prev);
957
+ } catch (e) {
958
+ console.error("[BaseportalChat] Error reopening conversation:", e);
959
+ }
960
+ }, [conversation, apiClient]);
961
+ const handleBack = (0, import_hooks5.useCallback)(() => {
962
+ if (allowViewHistory && view === "chat") {
963
+ realtimeClient.unsubscribe();
964
+ setView("conversations");
965
+ setConversation(null);
966
+ setMessages([]);
967
+ apiClient.getVisitorConversations().then(setConversations).catch(() => {
968
+ });
969
+ } else {
970
+ onClose();
971
+ }
972
+ }, [allowViewHistory, view, realtimeClient, apiClient, onClose]);
973
+ const posClass = position === "bottom-left" ? "bp-window--left" : "bp-window--right";
974
+ const headerTitle = view === "conversations" ? t.conversations.title : channelInfo.name;
975
+ const showBack = allowViewHistory && view === "chat" || view === "prechat";
976
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { class: `bp-window ${posClass}`, children: [
977
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { class: "bp-header", children: [
978
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { class: "bp-header__title", children: [
979
+ showBack && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { class: "bp-header__back", onClick: handleBack, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(IconArrowLeft, {}) }),
980
+ headerTitle
981
+ ] }),
982
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { class: "bp-header__close", onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(IconX, {}) })
983
+ ] }),
984
+ loading && view !== "chat" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { class: "bp-loading", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { class: "bp-spinner" }) }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
985
+ view === "prechat" && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
986
+ PreChatForm,
987
+ {
988
+ channelInfo,
989
+ onSubmit: handlePreChatSubmit,
990
+ loading,
991
+ t
992
+ }
993
+ ),
994
+ view === "conversations" && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
995
+ ConversationList,
996
+ {
997
+ conversations,
998
+ channelInfo,
999
+ loading,
1000
+ onSelect: openConversation,
1001
+ onNew: needsPreChat() ? () => setView("prechat") : startNewConversation,
1002
+ t
1003
+ }
1004
+ ),
1005
+ view === "chat" && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
1006
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(MessageList, { messages, loading, t }),
1007
+ isOpen ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1008
+ MessageInput,
1009
+ {
1010
+ value: inputValue,
1011
+ onChange: setInputValue,
1012
+ onSend: handleSend,
1013
+ onFileSelect: handleFileSelect,
1014
+ onFileRemove: handleFileRemove,
1015
+ attachedFile,
1016
+ uploading,
1017
+ disabled: sending || loading,
1018
+ placeholder: t.chat.placeholder,
1019
+ t
1020
+ }
1021
+ ) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { class: "bp-closed-banner", children: [
1022
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { class: "bp-closed-banner__text", children: t.chat.closed }),
1023
+ channelInfo.config.allowReopenConversation && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { class: "bp-closed-banner__reopen", onClick: handleReopen, children: t.chat.reopen })
1024
+ ] }),
1025
+ channelInfo.config.privacyPolicyUrl && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { class: "bp-privacy-footer", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1026
+ "a",
1027
+ {
1028
+ href: channelInfo.config.privacyPolicyUrl,
1029
+ target: "_blank",
1030
+ rel: "noopener noreferrer",
1031
+ children: t.prechat.privacyLink
1032
+ }
1033
+ ) })
1034
+ ] })
1035
+ ] })
1036
+ ] });
1037
+ }
1038
+
1039
+ // src/ui/App.tsx
1040
+ var import_jsx_runtime10 = require("preact/jsx-runtime");
1041
+ function App({
1042
+ channelInfo,
1043
+ apiClient,
1044
+ realtimeClient,
1045
+ storage,
1046
+ events,
1047
+ visitor,
1048
+ isAuthenticated,
1049
+ position,
1050
+ hidden,
1051
+ t,
1052
+ isOpenRef,
1053
+ setIsOpen
1054
+ }) {
1055
+ const [isOpen, setIsOpenState] = (0, import_hooks6.useState)(isOpenRef.current);
1056
+ const [isHidden, setIsHidden] = (0, import_hooks6.useState)(hidden);
1057
+ const [unreadCount] = (0, import_hooks6.useState)(0);
1058
+ (0, import_hooks6.useEffect)(() => {
1059
+ const onOpen = () => {
1060
+ setIsOpenState(true);
1061
+ isOpenRef.current = true;
1062
+ setIsOpen(true);
1063
+ };
1064
+ const onClose = () => {
1065
+ setIsOpenState(false);
1066
+ isOpenRef.current = false;
1067
+ setIsOpen(false);
1068
+ };
1069
+ const onShow = () => setIsHidden(false);
1070
+ const onHide = () => {
1071
+ setIsHidden(true);
1072
+ setIsOpenState(false);
1073
+ isOpenRef.current = false;
1074
+ setIsOpen(false);
1075
+ };
1076
+ events.on("_open", onOpen);
1077
+ events.on("_close", onClose);
1078
+ events.on("show", onShow);
1079
+ events.on("hide", onHide);
1080
+ return () => {
1081
+ events.off("_open", onOpen);
1082
+ events.off("_close", onClose);
1083
+ events.off("show", onShow);
1084
+ events.off("hide", onHide);
1085
+ };
1086
+ }, [events, isOpenRef, setIsOpen]);
1087
+ const handleToggle = () => {
1088
+ const next = !isOpen;
1089
+ setIsOpenState(next);
1090
+ isOpenRef.current = next;
1091
+ setIsOpen(next);
1092
+ events.emit(next ? "open" : "close");
1093
+ };
1094
+ const handleClose = () => {
1095
+ setIsOpenState(false);
1096
+ isOpenRef.current = false;
1097
+ setIsOpen(false);
1098
+ events.emit("close");
1099
+ };
1100
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
1101
+ !isHidden && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1102
+ ChatBubble,
1103
+ {
1104
+ isOpen,
1105
+ position,
1106
+ unreadCount,
1107
+ onClick: handleToggle
1108
+ }
1109
+ ),
1110
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1111
+ ChatWindow,
1112
+ {
1113
+ channelInfo,
1114
+ apiClient,
1115
+ realtimeClient,
1116
+ storage,
1117
+ events,
1118
+ visitor,
1119
+ isAuthenticated,
1120
+ position,
1121
+ onClose: handleClose,
1122
+ t
1123
+ }
1124
+ )
1125
+ ] });
1126
+ }
1127
+
1128
+ // src/ui/styles/widget-css.ts
1129
+ var widget_css_default = `#baseportal-chat-widget {
1130
+ display: block;
1131
+ position: static;
1132
+ width: 0;
1133
+ height: 0;
1134
+ overflow: visible;
1135
+ padding: 0;
1136
+ margin: 0;
1137
+ border: none;
1138
+
1139
+ --bp-primary: #6366f1;
1140
+ --bp-primary-contrast: #ffffff;
1141
+ --bp-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
1142
+ 'Helvetica Neue', Arial, sans-serif;
1143
+ --bp-radius: 16px;
1144
+ --bp-radius-sm: 8px;
1145
+ --bp-bubble-size: 60px;
1146
+ --bp-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
1147
+ --bp-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.1);
1148
+ --bp-gray-50: #f9fafb;
1149
+ --bp-gray-100: #f3f4f6;
1150
+ --bp-gray-200: #e5e7eb;
1151
+ --bp-gray-300: #d1d5db;
1152
+ --bp-gray-500: #6b7280;
1153
+ --bp-gray-700: #374151;
1154
+ --bp-gray-900: #111827;
1155
+ --bp-transition: 0.2s ease;
1156
+ }
1157
+
1158
+ #baseportal-chat-widget *,
1159
+ #baseportal-chat-widget *::before,
1160
+ #baseportal-chat-widget *::after {
1161
+ box-sizing: border-box;
1162
+ }
1163
+
1164
+ /* ===== Chat Bubble (FAB) ===== */
1165
+ .bp-bubble {
1166
+ pointer-events: auto;
1167
+ position: fixed;
1168
+ bottom: 24px;
1169
+ z-index: 2147483646;
1170
+ width: var(--bp-bubble-size);
1171
+ height: var(--bp-bubble-size);
1172
+ border-radius: 50%;
1173
+ background: var(--bp-primary);
1174
+ color: var(--bp-primary-contrast);
1175
+ border: none;
1176
+ cursor: pointer;
1177
+ display: flex;
1178
+ align-items: center;
1179
+ justify-content: center;
1180
+ box-shadow: var(--bp-shadow);
1181
+ transition: transform var(--bp-transition), box-shadow var(--bp-transition);
1182
+ font-family: var(--bp-font-family);
1183
+ font-size: 14px;
1184
+ line-height: 1.5;
1185
+ margin: 0;
1186
+ padding: 0;
1187
+ }
1188
+
1189
+ .bp-bubble:hover {
1190
+ transform: scale(1.08);
1191
+ box-shadow: 0 6px 32px rgba(0, 0, 0, 0.2);
1192
+ }
1193
+
1194
+ .bp-bubble--right {
1195
+ right: 24px;
1196
+ }
1197
+
1198
+ .bp-bubble--left {
1199
+ left: 24px;
1200
+ }
1201
+
1202
+ .bp-bubble svg {
1203
+ width: 28px;
1204
+ height: 28px;
1205
+ fill: currentColor;
1206
+ }
1207
+
1208
+ .bp-bubble__badge {
1209
+ position: absolute;
1210
+ top: -2px;
1211
+ right: -2px;
1212
+ min-width: 20px;
1213
+ height: 20px;
1214
+ border-radius: 10px;
1215
+ background: #ef4444;
1216
+ color: #fff;
1217
+ font-size: 11px;
1218
+ font-weight: 600;
1219
+ display: flex;
1220
+ align-items: center;
1221
+ justify-content: center;
1222
+ padding: 0 5px;
1223
+ }
1224
+
1225
+ /* ===== Chat Window ===== */
1226
+ .bp-window {
1227
+ pointer-events: auto;
1228
+ position: fixed;
1229
+ bottom: 96px;
1230
+ z-index: 2147483647;
1231
+ width: 380px;
1232
+ height: 520px;
1233
+ border-radius: var(--bp-radius);
1234
+ background: #fff;
1235
+ box-shadow: var(--bp-shadow);
1236
+ display: flex;
1237
+ flex-direction: column;
1238
+ overflow: hidden;
1239
+ animation: bp-slide-up 0.25s ease-out;
1240
+ font-family: var(--bp-font-family);
1241
+ font-size: 14px;
1242
+ line-height: 1.5;
1243
+ color: var(--bp-gray-900);
1244
+ }
1245
+
1246
+ .bp-window--right {
1247
+ right: 24px;
1248
+ }
1249
+
1250
+ .bp-window--left {
1251
+ left: 24px;
1252
+ }
1253
+
1254
+ @keyframes bp-slide-up {
1255
+ from {
1256
+ opacity: 0;
1257
+ transform: translateY(16px);
1258
+ }
1259
+ to {
1260
+ opacity: 1;
1261
+ transform: translateY(0);
1262
+ }
1263
+ }
1264
+
1265
+ /* Mobile fullscreen */
1266
+ @media (max-width: 480px) {
1267
+ .bp-window {
1268
+ top: 0;
1269
+ left: 0;
1270
+ right: 0;
1271
+ bottom: 0;
1272
+ width: 100%;
1273
+ height: 100%;
1274
+ border-radius: 0;
1275
+ }
1276
+ }
1277
+
1278
+ /* ===== Header ===== */
1279
+ .bp-header {
1280
+ padding: 16px;
1281
+ background: var(--bp-primary);
1282
+ color: var(--bp-primary-contrast);
1283
+ display: flex;
1284
+ align-items: center;
1285
+ justify-content: space-between;
1286
+ flex-shrink: 0;
1287
+ }
1288
+
1289
+ .bp-header__title {
1290
+ font-size: 16px;
1291
+ font-weight: 600;
1292
+ color: inherit;
1293
+ display: flex;
1294
+ align-items: center;
1295
+ gap: 8px;
1296
+ }
1297
+
1298
+ .bp-header__back,
1299
+ .bp-header__close {
1300
+ background: none;
1301
+ border: none;
1302
+ color: inherit;
1303
+ cursor: pointer;
1304
+ padding: 4px;
1305
+ border-radius: 4px;
1306
+ display: flex;
1307
+ align-items: center;
1308
+ justify-content: center;
1309
+ transition: background var(--bp-transition);
1310
+ }
1311
+
1312
+ .bp-header__back:hover,
1313
+ .bp-header__close:hover {
1314
+ background: rgba(255, 255, 255, 0.15);
1315
+ }
1316
+
1317
+ .bp-header__back svg,
1318
+ .bp-header__close svg {
1319
+ width: 18px;
1320
+ height: 18px;
1321
+ fill: currentColor;
1322
+ }
1323
+
1324
+ /* ===== Message List ===== */
1325
+ .bp-messages {
1326
+ flex: 1;
1327
+ overflow-y: auto;
1328
+ padding: 16px;
1329
+ display: flex;
1330
+ flex-direction: column;
1331
+ gap: 8px;
1332
+ background: var(--bp-gray-50);
1333
+ }
1334
+
1335
+ .bp-messages::-webkit-scrollbar {
1336
+ width: 4px;
1337
+ }
1338
+
1339
+ .bp-messages::-webkit-scrollbar-thumb {
1340
+ background: var(--bp-gray-300);
1341
+ border-radius: 2px;
1342
+ }
1343
+
1344
+ /* ===== Message Bubble ===== */
1345
+ .bp-msg {
1346
+ display: flex;
1347
+ max-width: 80%;
1348
+ }
1349
+
1350
+ .bp-msg--client {
1351
+ align-self: flex-end;
1352
+ flex-direction: row-reverse;
1353
+ }
1354
+
1355
+ .bp-msg--agent {
1356
+ align-self: flex-start;
1357
+ }
1358
+
1359
+ .bp-msg__avatar {
1360
+ width: 28px;
1361
+ height: 28px;
1362
+ border-radius: 50%;
1363
+ background: var(--bp-primary);
1364
+ color: var(--bp-primary-contrast);
1365
+ font-size: 12px;
1366
+ font-weight: 600;
1367
+ display: flex;
1368
+ align-items: center;
1369
+ justify-content: center;
1370
+ flex-shrink: 0;
1371
+ margin-top: auto;
1372
+ }
1373
+
1374
+ .bp-msg__avatar img {
1375
+ width: 100%;
1376
+ height: 100%;
1377
+ border-radius: 50%;
1378
+ object-fit: cover;
1379
+ }
1380
+
1381
+ .bp-msg__body {
1382
+ margin: 0 8px;
1383
+ }
1384
+
1385
+ .bp-msg__content {
1386
+ padding: 10px 14px;
1387
+ border-radius: 16px;
1388
+ white-space: pre-wrap;
1389
+ word-break: break-word;
1390
+ font-size: 14px;
1391
+ line-height: 1.4;
1392
+ }
1393
+
1394
+ .bp-msg--client .bp-msg__content {
1395
+ background: var(--bp-primary);
1396
+ color: var(--bp-primary-contrast);
1397
+ border-bottom-right-radius: 4px;
1398
+ }
1399
+
1400
+ .bp-msg--agent .bp-msg__content {
1401
+ background: var(--bp-gray-100);
1402
+ color: var(--bp-gray-900);
1403
+ border-bottom-left-radius: 4px;
1404
+ }
1405
+
1406
+ .bp-msg__time {
1407
+ font-size: 11px;
1408
+ color: var(--bp-gray-500);
1409
+ margin-top: 2px;
1410
+ padding: 0 4px;
1411
+ }
1412
+
1413
+ .bp-msg--client .bp-msg__time {
1414
+ text-align: right;
1415
+ }
1416
+
1417
+ /* ===== Composer (Message Input) ===== */
1418
+ .bp-composer {
1419
+ margin: 8px 12px 12px;
1420
+ border: 1px solid var(--bp-gray-200);
1421
+ border-radius: 12px;
1422
+ background: #fff;
1423
+ flex-shrink: 0;
1424
+ overflow: hidden;
1425
+ transition: border-color var(--bp-transition);
1426
+ }
1427
+
1428
+ .bp-composer:focus-within {
1429
+ border-color: var(--bp-primary);
1430
+ }
1431
+
1432
+ .bp-composer__preview {
1433
+ padding: 8px 12px;
1434
+ border-bottom: 1px solid var(--bp-gray-100);
1435
+ display: flex;
1436
+ align-items: center;
1437
+ gap: 8px;
1438
+ background: var(--bp-gray-50);
1439
+ }
1440
+
1441
+ .bp-composer__preview-thumb {
1442
+ width: 40px;
1443
+ height: 40px;
1444
+ border-radius: 6px;
1445
+ object-fit: cover;
1446
+ flex-shrink: 0;
1447
+ }
1448
+
1449
+ .bp-composer__preview-icon {
1450
+ width: 36px;
1451
+ height: 36px;
1452
+ border-radius: 6px;
1453
+ background: var(--bp-gray-100);
1454
+ display: flex;
1455
+ align-items: center;
1456
+ justify-content: center;
1457
+ flex-shrink: 0;
1458
+ color: var(--bp-gray-500);
1459
+ }
1460
+
1461
+ .bp-composer__preview-icon svg {
1462
+ width: 18px;
1463
+ height: 18px;
1464
+ }
1465
+
1466
+ .bp-composer__preview-info {
1467
+ flex: 1;
1468
+ min-width: 0;
1469
+ }
1470
+
1471
+ .bp-composer__preview-name {
1472
+ font-size: 12px;
1473
+ font-weight: 500;
1474
+ color: var(--bp-gray-700);
1475
+ overflow: hidden;
1476
+ text-overflow: ellipsis;
1477
+ white-space: nowrap;
1478
+ }
1479
+
1480
+ .bp-composer__preview-status {
1481
+ font-size: 11px;
1482
+ color: var(--bp-gray-500);
1483
+ }
1484
+
1485
+ .bp-composer__preview-remove {
1486
+ background: none;
1487
+ border: none;
1488
+ cursor: pointer;
1489
+ padding: 2px;
1490
+ color: var(--bp-gray-500);
1491
+ display: flex;
1492
+ align-items: center;
1493
+ justify-content: center;
1494
+ flex-shrink: 0;
1495
+ }
1496
+
1497
+ .bp-composer__preview-remove svg {
1498
+ width: 14px;
1499
+ height: 14px;
1500
+ }
1501
+
1502
+ .bp-composer__preview-remove:hover {
1503
+ color: var(--bp-gray-700);
1504
+ }
1505
+
1506
+ .bp-composer__row {
1507
+ display: flex;
1508
+ align-items: flex-end;
1509
+ padding: 4px;
1510
+ }
1511
+
1512
+ .bp-composer__attach {
1513
+ width: 32px;
1514
+ height: 32px;
1515
+ background: none;
1516
+ border: none;
1517
+ cursor: pointer;
1518
+ color: var(--bp-gray-500);
1519
+ display: flex;
1520
+ align-items: center;
1521
+ justify-content: center;
1522
+ flex-shrink: 0;
1523
+ border-radius: 6px;
1524
+ transition: color var(--bp-transition), background var(--bp-transition);
1525
+ }
1526
+
1527
+ .bp-composer__attach:hover {
1528
+ color: var(--bp-primary);
1529
+ background: var(--bp-gray-50);
1530
+ }
1531
+
1532
+ .bp-composer__attach:disabled {
1533
+ opacity: 0.4;
1534
+ cursor: not-allowed;
1535
+ }
1536
+
1537
+ .bp-composer__attach svg {
1538
+ width: 18px;
1539
+ height: 18px;
1540
+ }
1541
+
1542
+ .bp-composer__field {
1543
+ flex: 1;
1544
+ border: none;
1545
+ padding: 6px 4px;
1546
+ font-size: 14px;
1547
+ font-family: inherit;
1548
+ line-height: 1.4;
1549
+ resize: none;
1550
+ outline: none;
1551
+ max-height: 100px;
1552
+ overflow-y: auto;
1553
+ color: var(--bp-gray-900);
1554
+ background: transparent;
1555
+ }
1556
+
1557
+ .bp-composer__field::placeholder {
1558
+ color: var(--bp-gray-500);
1559
+ }
1560
+
1561
+ .bp-composer__field:disabled {
1562
+ cursor: not-allowed;
1563
+ }
1564
+
1565
+ .bp-composer__send {
1566
+ width: 32px;
1567
+ height: 32px;
1568
+ border-radius: 50%;
1569
+ background: var(--bp-primary);
1570
+ color: var(--bp-primary-contrast);
1571
+ border: none;
1572
+ cursor: pointer;
1573
+ display: flex;
1574
+ align-items: center;
1575
+ justify-content: center;
1576
+ flex-shrink: 0;
1577
+ transition: opacity var(--bp-transition);
1578
+ }
1579
+
1580
+ .bp-composer__send:disabled {
1581
+ opacity: 0.4;
1582
+ cursor: not-allowed;
1583
+ }
1584
+
1585
+ .bp-composer__send svg {
1586
+ width: 16px;
1587
+ height: 16px;
1588
+ }
1589
+
1590
+ /* ===== Message Media ===== */
1591
+ .bp-media-img {
1592
+ max-width: 100%;
1593
+ max-height: 240px;
1594
+ border-radius: 8px;
1595
+ cursor: pointer;
1596
+ display: block;
1597
+ object-fit: contain;
1598
+ }
1599
+
1600
+ .bp-media-video {
1601
+ max-width: 100%;
1602
+ max-height: 240px;
1603
+ border-radius: 8px;
1604
+ display: block;
1605
+ }
1606
+
1607
+ .bp-media-file {
1608
+ display: flex;
1609
+ align-items: center;
1610
+ gap: 8px;
1611
+ padding: 8px 12px;
1612
+ background: var(--bp-gray-50);
1613
+ border-radius: 8px;
1614
+ text-decoration: none;
1615
+ color: var(--bp-gray-700);
1616
+ transition: background var(--bp-transition);
1617
+ }
1618
+
1619
+ .bp-media-file:hover {
1620
+ background: var(--bp-gray-100);
1621
+ }
1622
+
1623
+ .bp-media-file__icon {
1624
+ width: 32px;
1625
+ height: 32px;
1626
+ border-radius: 6px;
1627
+ background: var(--bp-gray-200);
1628
+ display: flex;
1629
+ align-items: center;
1630
+ justify-content: center;
1631
+ flex-shrink: 0;
1632
+ color: var(--bp-gray-500);
1633
+ }
1634
+
1635
+ .bp-media-file__icon svg {
1636
+ width: 16px;
1637
+ height: 16px;
1638
+ }
1639
+
1640
+ .bp-media-file__name {
1641
+ flex: 1;
1642
+ font-size: 13px;
1643
+ font-weight: 500;
1644
+ overflow: hidden;
1645
+ text-overflow: ellipsis;
1646
+ white-space: nowrap;
1647
+ min-width: 0;
1648
+ }
1649
+
1650
+ .bp-media-file__download svg {
1651
+ width: 16px;
1652
+ height: 16px;
1653
+ color: var(--bp-primary);
1654
+ }
1655
+
1656
+ /* ===== Image Lightbox ===== */
1657
+ .bp-lightbox {
1658
+ position: fixed;
1659
+ inset: 0;
1660
+ z-index: 2147483647;
1661
+ background: rgba(0, 0, 0, 0.85);
1662
+ display: flex;
1663
+ align-items: center;
1664
+ justify-content: center;
1665
+ cursor: pointer;
1666
+ }
1667
+
1668
+ .bp-lightbox__img {
1669
+ max-width: 90vw;
1670
+ max-height: 90vh;
1671
+ object-fit: contain;
1672
+ border-radius: 4px;
1673
+ cursor: default;
1674
+ }
1675
+
1676
+ .bp-lightbox__close {
1677
+ position: absolute;
1678
+ top: 16px;
1679
+ right: 16px;
1680
+ width: 36px;
1681
+ height: 36px;
1682
+ border-radius: 50%;
1683
+ background: rgba(255, 255, 255, 0.15);
1684
+ color: #fff;
1685
+ border: none;
1686
+ cursor: pointer;
1687
+ display: flex;
1688
+ align-items: center;
1689
+ justify-content: center;
1690
+ transition: background var(--bp-transition);
1691
+ }
1692
+
1693
+ .bp-lightbox__close:hover {
1694
+ background: rgba(255, 255, 255, 0.3);
1695
+ }
1696
+
1697
+ .bp-lightbox__close svg {
1698
+ width: 20px;
1699
+ height: 20px;
1700
+ }
1701
+
1702
+ /* ===== Pre-Chat Form ===== */
1703
+ .bp-prechat {
1704
+ flex: 1;
1705
+ padding: 24px 16px;
1706
+ display: flex;
1707
+ flex-direction: column;
1708
+ gap: 16px;
1709
+ overflow-y: auto;
1710
+ }
1711
+
1712
+ .bp-prechat__title {
1713
+ font-size: 16px;
1714
+ font-weight: 600;
1715
+ color: var(--bp-gray-900);
1716
+ }
1717
+
1718
+ .bp-prechat__desc {
1719
+ font-size: 13px;
1720
+ color: var(--bp-gray-500);
1721
+ }
1722
+
1723
+ .bp-prechat__field {
1724
+ display: flex;
1725
+ flex-direction: column;
1726
+ gap: 4px;
1727
+ }
1728
+
1729
+ .bp-prechat__label {
1730
+ font-size: 13px;
1731
+ font-weight: 500;
1732
+ color: var(--bp-gray-700);
1733
+ }
1734
+
1735
+ .bp-prechat__input {
1736
+ border: 1px solid var(--bp-gray-200);
1737
+ border-radius: var(--bp-radius-sm);
1738
+ padding: 10px 12px;
1739
+ font-size: 14px;
1740
+ font-family: inherit;
1741
+ outline: none;
1742
+ transition: border-color var(--bp-transition);
1743
+ color: var(--bp-gray-900);
1744
+ background: #fff;
1745
+ }
1746
+
1747
+ .bp-prechat__input:focus {
1748
+ border-color: var(--bp-primary);
1749
+ }
1750
+
1751
+ .bp-prechat__submit {
1752
+ padding: 10px 20px;
1753
+ border-radius: var(--bp-radius-sm);
1754
+ background: var(--bp-primary);
1755
+ color: var(--bp-primary-contrast);
1756
+ border: none;
1757
+ font-size: 14px;
1758
+ font-weight: 500;
1759
+ font-family: inherit;
1760
+ cursor: pointer;
1761
+ transition: opacity var(--bp-transition);
1762
+ }
1763
+
1764
+ .bp-prechat__submit:disabled {
1765
+ opacity: 0.6;
1766
+ cursor: not-allowed;
1767
+ }
1768
+
1769
+ .bp-prechat__privacy {
1770
+ font-size: 12px;
1771
+ color: var(--bp-gray-500);
1772
+ text-align: center;
1773
+ margin-top: auto;
1774
+ }
1775
+
1776
+ .bp-prechat__privacy a {
1777
+ color: var(--bp-primary);
1778
+ text-decoration: underline;
1779
+ }
1780
+
1781
+ /* ===== Conversation List ===== */
1782
+ .bp-convlist {
1783
+ flex: 1;
1784
+ overflow-y: auto;
1785
+ display: flex;
1786
+ flex-direction: column;
1787
+ }
1788
+
1789
+ .bp-convlist__new {
1790
+ padding: 12px 16px;
1791
+ border-bottom: 1px solid var(--bp-gray-200);
1792
+ }
1793
+
1794
+ .bp-convlist__new-btn {
1795
+ width: 100%;
1796
+ padding: 10px 16px;
1797
+ border-radius: var(--bp-radius-sm);
1798
+ background: rgba(99, 102, 241, 0.08);
1799
+ border: none;
1800
+ cursor: pointer;
1801
+ display: flex;
1802
+ align-items: center;
1803
+ gap: 8px;
1804
+ font-size: 14px;
1805
+ font-weight: 500;
1806
+ font-family: inherit;
1807
+ color: var(--bp-primary);
1808
+ transition: background var(--bp-transition);
1809
+ }
1810
+
1811
+ .bp-convlist__new-btn svg {
1812
+ width: 16px;
1813
+ height: 16px;
1814
+ flex-shrink: 0;
1815
+ }
1816
+
1817
+ .bp-convlist__new-btn:hover {
1818
+ background: rgba(99, 102, 241, 0.15);
1819
+ }
1820
+
1821
+ .bp-convlist__items {
1822
+ flex: 1;
1823
+ overflow-y: auto;
1824
+ padding: 8px;
1825
+ }
1826
+
1827
+ .bp-convlist__item {
1828
+ width: 100%;
1829
+ padding: 12px;
1830
+ border-radius: var(--bp-radius-sm);
1831
+ background: none;
1832
+ border: none;
1833
+ cursor: pointer;
1834
+ display: flex;
1835
+ flex-direction: column;
1836
+ gap: 4px;
1837
+ text-align: left;
1838
+ font-family: inherit;
1839
+ transition: background var(--bp-transition);
1840
+ color: var(--bp-gray-900);
1841
+ }
1842
+
1843
+ .bp-convlist__item:hover {
1844
+ background: var(--bp-gray-50);
1845
+ }
1846
+
1847
+ .bp-convlist__item:disabled {
1848
+ opacity: 0.5;
1849
+ cursor: not-allowed;
1850
+ }
1851
+
1852
+ .bp-convlist__item-top {
1853
+ display: flex;
1854
+ align-items: center;
1855
+ justify-content: space-between;
1856
+ gap: 8px;
1857
+ }
1858
+
1859
+ .bp-convlist__item-title {
1860
+ font-size: 14px;
1861
+ font-weight: 500;
1862
+ overflow: hidden;
1863
+ text-overflow: ellipsis;
1864
+ white-space: nowrap;
1865
+ flex: 1;
1866
+ }
1867
+
1868
+ .bp-convlist__item-status {
1869
+ font-size: 11px;
1870
+ font-weight: 500;
1871
+ padding: 2px 8px;
1872
+ border-radius: 10px;
1873
+ flex-shrink: 0;
1874
+ }
1875
+
1876
+ .bp-convlist__item-status--open {
1877
+ background: #dcfce7;
1878
+ color: #166534;
1879
+ }
1880
+
1881
+ .bp-convlist__item-status--closed {
1882
+ background: var(--bp-gray-100);
1883
+ color: var(--bp-gray-500);
1884
+ }
1885
+
1886
+ .bp-convlist__item-preview {
1887
+ font-size: 13px;
1888
+ color: var(--bp-gray-500);
1889
+ overflow: hidden;
1890
+ text-overflow: ellipsis;
1891
+ white-space: nowrap;
1892
+ }
1893
+
1894
+ .bp-convlist__empty {
1895
+ padding: 32px 16px;
1896
+ text-align: center;
1897
+ color: var(--bp-gray-500);
1898
+ font-size: 14px;
1899
+ }
1900
+
1901
+ /* ===== Closed conversation banner ===== */
1902
+ .bp-closed-banner {
1903
+ padding: 14px 16px;
1904
+ border-top: 1px solid var(--bp-gray-200);
1905
+ background: var(--bp-gray-50);
1906
+ flex-shrink: 0;
1907
+ display: flex;
1908
+ align-items: center;
1909
+ justify-content: center;
1910
+ gap: 6px;
1911
+ flex-wrap: wrap;
1912
+ }
1913
+
1914
+ .bp-closed-banner__text {
1915
+ font-size: 13px;
1916
+ color: var(--bp-gray-500);
1917
+ }
1918
+
1919
+ .bp-closed-banner__reopen {
1920
+ background: none;
1921
+ border: none;
1922
+ padding: 0;
1923
+ font-size: 13px;
1924
+ font-weight: 500;
1925
+ font-family: inherit;
1926
+ color: var(--bp-primary);
1927
+ cursor: pointer;
1928
+ text-decoration: underline;
1929
+ text-underline-offset: 2px;
1930
+ }
1931
+
1932
+ .bp-closed-banner__reopen:hover {
1933
+ opacity: 0.8;
1934
+ }
1935
+
1936
+ /* ===== Privacy footer ===== */
1937
+ .bp-privacy-footer {
1938
+ padding: 4px 16px 8px;
1939
+ text-align: center;
1940
+ font-size: 11px;
1941
+ color: var(--bp-gray-500);
1942
+ flex-shrink: 0;
1943
+ }
1944
+
1945
+ .bp-privacy-footer a {
1946
+ color: var(--bp-primary);
1947
+ text-decoration: underline;
1948
+ }
1949
+
1950
+ /* ===== Loading ===== */
1951
+ .bp-loading {
1952
+ flex: 1;
1953
+ display: flex;
1954
+ align-items: center;
1955
+ justify-content: center;
1956
+ }
1957
+
1958
+ .bp-spinner {
1959
+ width: 32px;
1960
+ height: 32px;
1961
+ border: 3px solid var(--bp-gray-200);
1962
+ border-top-color: var(--bp-primary);
1963
+ border-radius: 50%;
1964
+ animation: bp-spin 0.6s linear infinite;
1965
+ }
1966
+
1967
+ @keyframes bp-spin {
1968
+ to {
1969
+ transform: rotate(360deg);
1970
+ }
1971
+ }
1972
+
1973
+ /* ===== Hidden ===== */
1974
+ .bp-hidden {
1975
+ display: none !important;
1976
+ }
1977
+ `;
1978
+
1979
+ // src/ui/mount.ts
1980
+ var styleElement = null;
1981
+ var hostElement = null;
1982
+ function mount(options) {
1983
+ if (!styleElement) {
1984
+ styleElement = document.createElement("style");
1985
+ styleElement.id = "baseportal-chat-styles";
1986
+ styleElement.textContent = widget_css_default;
1987
+ document.head.appendChild(styleElement);
1988
+ }
1989
+ hostElement = document.createElement("div");
1990
+ hostElement.id = "baseportal-chat-widget";
1991
+ const target = options.container || document.body;
1992
+ target.appendChild(hostElement);
1993
+ const primaryColor = options.channelInfo.theme?.primaryColor || "#6366f1";
1994
+ const contrastColor = getContrastColor(primaryColor);
1995
+ hostElement.style.setProperty("--bp-primary", primaryColor);
1996
+ hostElement.style.setProperty("--bp-primary-contrast", contrastColor);
1997
+ (0, import_preact.render)(
1998
+ (0, import_preact.h)(App, {
1999
+ channelInfo: options.channelInfo,
2000
+ apiClient: options.apiClient,
2001
+ realtimeClient: options.realtimeClient,
2002
+ storage: options.storage,
2003
+ events: options.events,
2004
+ visitor: options.visitor,
2005
+ isAuthenticated: options.isAuthenticated,
2006
+ position: options.position,
2007
+ hidden: options.hidden,
2008
+ t: options.t,
2009
+ isOpenRef: options.isOpenRef,
2010
+ setIsOpen: options.setIsOpen
2011
+ }),
2012
+ hostElement
2013
+ );
2014
+ }
2015
+ function unmount() {
2016
+ if (hostElement) {
2017
+ (0, import_preact.render)(null, hostElement);
2018
+ hostElement.remove();
2019
+ hostElement = null;
2020
+ }
2021
+ if (styleElement) {
2022
+ styleElement.remove();
2023
+ styleElement = null;
2024
+ }
2025
+ }
2026
+ function updateTheme(primaryColor) {
2027
+ if (!hostElement) return;
2028
+ hostElement.style.setProperty("--bp-primary", primaryColor);
2029
+ hostElement.style.setProperty(
2030
+ "--bp-primary-contrast",
2031
+ getContrastColor(primaryColor)
2032
+ );
2033
+ }
2034
+ function getContrastColor(hex) {
2035
+ const r = parseInt(hex.slice(1, 3), 16);
2036
+ const g = parseInt(hex.slice(3, 5), 16);
2037
+ const b = parseInt(hex.slice(5, 7), 16);
2038
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
2039
+ return luminance > 0.5 ? "#000000" : "#ffffff";
2040
+ }
2041
+
2042
+ // src/utils/events.ts
2043
+ var EventEmitter = class {
2044
+ constructor() {
2045
+ this.listeners = /* @__PURE__ */ new Map();
2046
+ }
2047
+ on(event, callback) {
2048
+ if (!this.listeners.has(event)) {
2049
+ this.listeners.set(event, /* @__PURE__ */ new Set());
2050
+ }
2051
+ this.listeners.get(event).add(callback);
2052
+ }
2053
+ off(event, callback) {
2054
+ this.listeners.get(event)?.delete(callback);
2055
+ }
2056
+ emit(event, ...args) {
2057
+ this.listeners.get(event)?.forEach((cb) => {
2058
+ try {
2059
+ cb(...args);
2060
+ } catch (e) {
2061
+ console.error(`[BaseportalChat] Error in ${event} handler:`, e);
2062
+ }
2063
+ });
2064
+ }
2065
+ removeAllListeners() {
2066
+ this.listeners.clear();
2067
+ }
2068
+ };
2069
+
2070
+ // src/utils/storage.ts
2071
+ var Storage = class {
2072
+ constructor(channelToken, email) {
2073
+ this.prefix = email ? `bp_chat_${channelToken}_${email}` : `bp_chat_${channelToken}`;
2074
+ }
2075
+ get() {
2076
+ try {
2077
+ const raw = localStorage.getItem(this.prefix);
2078
+ return raw ? JSON.parse(raw) : {};
2079
+ } catch {
2080
+ return {};
2081
+ }
2082
+ }
2083
+ set(data) {
2084
+ try {
2085
+ const current = this.get();
2086
+ localStorage.setItem(this.prefix, JSON.stringify({ ...current, ...data }));
2087
+ } catch {
2088
+ }
2089
+ }
2090
+ getConversationId() {
2091
+ return this.get().conversationId;
2092
+ }
2093
+ setConversationId(id) {
2094
+ this.set({ conversationId: id });
2095
+ }
2096
+ getVisitor() {
2097
+ return this.get().visitor;
2098
+ }
2099
+ setVisitor(visitor) {
2100
+ this.set({ visitor });
2101
+ }
2102
+ clear() {
2103
+ try {
2104
+ localStorage.removeItem(this.prefix);
2105
+ } catch {
2106
+ }
2107
+ }
2108
+ };
2109
+
2110
+ // src/widget.ts
2111
+ var DEFAULT_API_URL = "https://api.baseportal.io";
2112
+ var BaseportalChat = class {
2113
+ constructor(config) {
2114
+ this.events = new EventEmitter();
2115
+ this.channelInfo = null;
2116
+ this.visitor = null;
2117
+ this.isAuthenticated = false;
2118
+ this.isOpenRef = { current: false };
2119
+ this.mounted = false;
2120
+ this.config = {
2121
+ ...config,
2122
+ apiUrl: config.apiUrl || DEFAULT_API_URL,
2123
+ position: config.position || "bottom-right",
2124
+ locale: config.locale || "pt"
2125
+ };
2126
+ this.hidden = config.hideOnLoad || false;
2127
+ this.visitor = config.visitor || null;
2128
+ this.isAuthenticated = !!config.visitor?.email;
2129
+ this.apiClient = new ApiClient(
2130
+ this.config.channelToken,
2131
+ this.config.apiUrl
2132
+ );
2133
+ if (this.isAuthenticated && this.visitor?.email) {
2134
+ this.apiClient.setVisitorIdentity(this.visitor.email, this.visitor.hash);
2135
+ }
2136
+ this.storage = new Storage(
2137
+ this.config.channelToken,
2138
+ this.isAuthenticated ? this.visitor?.email : void 0
2139
+ );
2140
+ this.realtimeClient = new RealtimeClient(this.apiClient);
2141
+ if (!this.visitor) {
2142
+ this.visitor = this.storage.getVisitor() || null;
2143
+ }
2144
+ this.init();
2145
+ }
2146
+ async init() {
2147
+ try {
2148
+ this.channelInfo = await this.apiClient.getChannelInfo();
2149
+ const primaryColor = this.config.theme?.primaryColor || this.channelInfo.theme?.primaryColor || "#6366f1";
2150
+ const t = getTranslations(this.config.locale);
2151
+ mount({
2152
+ channelInfo: this.channelInfo,
2153
+ apiClient: this.apiClient,
2154
+ realtimeClient: this.realtimeClient,
2155
+ storage: this.storage,
2156
+ events: this.events,
2157
+ visitor: this.visitor,
2158
+ isAuthenticated: this.isAuthenticated,
2159
+ position: this.config.position,
2160
+ hidden: this.hidden,
2161
+ t,
2162
+ container: this.config.container,
2163
+ isOpenRef: this.isOpenRef,
2164
+ setIsOpen: (open) => {
2165
+ this.isOpenRef.current = open;
2166
+ }
2167
+ });
2168
+ if (primaryColor !== "#6366f1") {
2169
+ updateTheme(primaryColor);
2170
+ }
2171
+ this.mounted = true;
2172
+ this.events.emit("ready");
2173
+ } catch (e) {
2174
+ console.error("[BaseportalChat] Failed to initialize:", e);
2175
+ }
2176
+ }
2177
+ // --- Visibility ---
2178
+ open() {
2179
+ if (!this.mounted) return;
2180
+ this.events.emit("_open");
2181
+ this.events.emit("open");
2182
+ }
2183
+ close() {
2184
+ if (!this.mounted) return;
2185
+ this.events.emit("_close");
2186
+ this.events.emit("close");
2187
+ }
2188
+ toggle() {
2189
+ if (this.isOpenRef.current) {
2190
+ this.close();
2191
+ } else {
2192
+ this.open();
2193
+ }
2194
+ }
2195
+ show() {
2196
+ this.hidden = false;
2197
+ this.events.emit("show");
2198
+ }
2199
+ hide() {
2200
+ this.hidden = true;
2201
+ this.events.emit("hide");
2202
+ }
2203
+ isOpen() {
2204
+ return this.isOpenRef.current;
2205
+ }
2206
+ // --- Visitor ---
2207
+ identify(visitor) {
2208
+ this.visitor = visitor;
2209
+ this.isAuthenticated = true;
2210
+ this.apiClient.setVisitorIdentity(visitor.email, visitor.hash);
2211
+ this.storage = new Storage(this.config.channelToken, visitor.email);
2212
+ this.storage.setVisitor(visitor);
2213
+ this.events.emit("identified", visitor);
2214
+ if (this.mounted) {
2215
+ this.remount();
2216
+ }
2217
+ }
2218
+ updateVisitor(data) {
2219
+ if (this.visitor) {
2220
+ this.visitor = { ...this.visitor, ...data };
2221
+ this.storage.setVisitor(this.visitor);
2222
+ }
2223
+ }
2224
+ clearVisitor() {
2225
+ this.visitor = null;
2226
+ this.isAuthenticated = false;
2227
+ this.apiClient.clearVisitorIdentity();
2228
+ this.storage.clear();
2229
+ this.storage = new Storage(this.config.channelToken);
2230
+ this.realtimeClient.unsubscribe();
2231
+ if (this.mounted) {
2232
+ this.remount();
2233
+ }
2234
+ }
2235
+ // --- Actions ---
2236
+ sendMessage(content) {
2237
+ this.events.emit("_sendMessage", content);
2238
+ }
2239
+ setConversationId(id) {
2240
+ this.events.emit("_setConversationId", id);
2241
+ }
2242
+ newConversation() {
2243
+ this.events.emit("_newConversation");
2244
+ }
2245
+ // --- Config ---
2246
+ setTheme(theme) {
2247
+ if (theme.primaryColor) {
2248
+ updateTheme(theme.primaryColor);
2249
+ }
2250
+ }
2251
+ setPosition(position) {
2252
+ this.config.position = position;
2253
+ if (this.mounted) {
2254
+ this.remount();
2255
+ }
2256
+ }
2257
+ setLocale(locale) {
2258
+ this.config.locale = locale;
2259
+ if (this.mounted) {
2260
+ this.remount();
2261
+ }
2262
+ }
2263
+ // --- Events ---
2264
+ on(event, callback) {
2265
+ this.events.on(event, callback);
2266
+ }
2267
+ off(event, callback) {
2268
+ this.events.off(event, callback);
2269
+ }
2270
+ // --- Lifecycle ---
2271
+ destroy() {
2272
+ this.realtimeClient.unsubscribe();
2273
+ this.events.removeAllListeners();
2274
+ unmount();
2275
+ this.mounted = false;
2276
+ }
2277
+ remount() {
2278
+ unmount();
2279
+ this.mounted = false;
2280
+ this.init();
2281
+ }
2282
+ };
2283
+
2284
+ // src/index.ts
2285
+ var src_default = BaseportalChat;
2286
+ //# sourceMappingURL=index.cjs.js.map