@spacinbox/sdk 2.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,1478 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/widget/state.ts
12
+ import { signal } from "@preact/signals";
13
+ var appId, visitorId, sessionToken, tokenExpiresAt, isOpen, currentUser, metadata, isOnline, isSetup, refetchSessionCounter;
14
+ var init_state = __esm({
15
+ "src/widget/state.ts"() {
16
+ "use strict";
17
+ appId = signal(null);
18
+ visitorId = signal(null);
19
+ sessionToken = signal(null);
20
+ tokenExpiresAt = signal(null);
21
+ isOpen = signal(false);
22
+ currentUser = signal(null);
23
+ metadata = signal({});
24
+ isOnline = signal(true);
25
+ isSetup = signal(false);
26
+ refetchSessionCounter = signal(0);
27
+ }
28
+ });
29
+
30
+ // src/widget/modules/api/mock.ts
31
+ function scheduleAutoClose(conversationId) {
32
+ const existing = autoCloseTimers.get(conversationId);
33
+ if (existing) clearTimeout(existing);
34
+ const timer = setTimeout(() => {
35
+ const conv = CONVERSATIONS.find((c) => c.id === conversationId);
36
+ if (conv) conv.status = "CLOSED";
37
+ autoCloseTimers.delete(conversationId);
38
+ }, 8e3);
39
+ autoCloseTimers.set(conversationId, timer);
40
+ }
41
+ var AGENTS, CONVERSATIONS, MESSAGES, autoCloseTimers, delay, mockService;
42
+ var init_mock = __esm({
43
+ "src/widget/modules/api/mock.ts"() {
44
+ "use strict";
45
+ AGENTS = [
46
+ { id: "agent-1", name: "Alice Martin", picture: void 0 },
47
+ { id: "agent-2", name: "Bob Chen", picture: void 0 }
48
+ ];
49
+ CONVERSATIONS = [
50
+ {
51
+ id: "conv-1",
52
+ agentId: AGENTS[1].id,
53
+ agent: AGENTS[1],
54
+ status: "OPEN",
55
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
56
+ unread_contact_count: 0
57
+ },
58
+ {
59
+ id: "conv-2",
60
+ agentId: AGENTS[0].id,
61
+ agent: AGENTS[0],
62
+ status: "OPEN",
63
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
64
+ unread_contact_count: 0
65
+ },
66
+ {
67
+ id: "conv-3",
68
+ agentId: AGENTS[1].id,
69
+ agent: AGENTS[1],
70
+ status: "OPEN",
71
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
72
+ unread_contact_count: 0
73
+ },
74
+ {
75
+ id: "conv-4",
76
+ agentId: AGENTS[0].id,
77
+ agent: AGENTS[0],
78
+ status: "OPEN",
79
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
80
+ unread_contact_count: 0
81
+ }
82
+ ];
83
+ MESSAGES = [];
84
+ autoCloseTimers = /* @__PURE__ */ new Map();
85
+ delay = (ms = 300) => new Promise((res) => setTimeout(res, ms));
86
+ mockService = {
87
+ async createSession(_dto) {
88
+ await delay();
89
+ return {
90
+ access_token: `mock-token-${Date.now()}`,
91
+ token_type: "Bearer",
92
+ expires_in: 3600,
93
+ visitor_id: _dto.visitorId
94
+ };
95
+ },
96
+ async getSession() {
97
+ return { count_notifications: 1 };
98
+ },
99
+ async identifyVisitor(_dto) {
100
+ await delay();
101
+ return { visitor_id: null, access_token: null };
102
+ },
103
+ async getAgents() {
104
+ await delay();
105
+ return AGENTS;
106
+ },
107
+ async getConversation(conversationId) {
108
+ await delay();
109
+ const conv = CONVERSATIONS.find((c) => c.id === conversationId);
110
+ if (!conv) throw new Error(`Conversation ${conversationId} not found`);
111
+ return conv;
112
+ },
113
+ async reopenConversation(conversationId) {
114
+ await delay();
115
+ const conv = CONVERSATIONS.find((c) => c.id === conversationId);
116
+ if (conv) conv.status = "OPEN";
117
+ },
118
+ async getConversationSocketToken(conversationId) {
119
+ await delay();
120
+ return { access_token: `mock-conv-socket-token-${conversationId}-${Date.now()}` };
121
+ },
122
+ async createConversation(dto) {
123
+ await delay();
124
+ const conversation = {
125
+ id: `conv-${Date.now()}`,
126
+ agentId: dto.agentId,
127
+ status: "OPEN",
128
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
129
+ messages: [],
130
+ unread_contact_count: 0
131
+ };
132
+ if (dto.text) {
133
+ const message = {
134
+ id: `msg-${Date.now()}`,
135
+ sender: "user",
136
+ text: dto.text,
137
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
138
+ conversationId: conversation.id
139
+ };
140
+ conversation.messages?.push(message);
141
+ scheduleAutoClose(conversation.id);
142
+ }
143
+ CONVERSATIONS.push(conversation);
144
+ return conversation;
145
+ },
146
+ async getConversations() {
147
+ await delay();
148
+ return CONVERSATIONS.map((conv) => {
149
+ const all = [
150
+ ...conv.messages ?? [],
151
+ ...MESSAGES.filter((m) => m.conversationId === conv.id)
152
+ ];
153
+ const lastMessage = all.sort(
154
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
155
+ )[0];
156
+ return { ...conv, lastMessage };
157
+ });
158
+ },
159
+ async getMessages(conversationId, _params) {
160
+ await delay();
161
+ const messages = MESSAGES.filter((m) => m.conversationId === conversationId);
162
+ return { messages, hasMore: false, nextCursor: null };
163
+ },
164
+ async sendMessage(conversationId, dto) {
165
+ await delay();
166
+ const message = {
167
+ id: `msg-${Date.now()}`,
168
+ conversationId,
169
+ text: dto.text,
170
+ sender: "user",
171
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
172
+ };
173
+ MESSAGES.push(message);
174
+ scheduleAutoClose(conversationId);
175
+ return message;
176
+ }
177
+ };
178
+ }
179
+ });
180
+
181
+ // src/widget/modules/visitor/service.ts
182
+ var service_exports = {};
183
+ __export(service_exports, {
184
+ VisitorService: () => VisitorService
185
+ });
186
+ var STORAGE_KEY, REFRESH_THRESHOLD_MS, VisitorService;
187
+ var init_service = __esm({
188
+ "src/widget/modules/visitor/service.ts"() {
189
+ "use strict";
190
+ init_state();
191
+ init_service2();
192
+ STORAGE_KEY = "spacinbox_vid";
193
+ REFRESH_THRESHOLD_MS = 6e4;
194
+ VisitorService = {
195
+ async init() {
196
+ let id = localStorage.getItem(STORAGE_KEY);
197
+ if (!id) {
198
+ id = crypto.randomUUID();
199
+ localStorage.setItem(STORAGE_KEY, id);
200
+ }
201
+ visitorId.value = id;
202
+ await this.refreshSession(id);
203
+ isSetup.value = true;
204
+ },
205
+ async refreshSession(visitor_id = visitorId.value) {
206
+ if (!visitor_id) {
207
+ return;
208
+ }
209
+ try {
210
+ const response = await ApiService.createSession({ visitorId: visitor_id });
211
+ sessionToken.value = response.access_token;
212
+ visitorId.value = response.visitor_id;
213
+ localStorage.setItem(STORAGE_KEY, response.visitor_id);
214
+ tokenExpiresAt.value = Date.now() + response.expires_in * 1e3;
215
+ } catch (err) {
216
+ isOnline.value = false;
217
+ throw err;
218
+ }
219
+ },
220
+ isTokenExpiringSoon() {
221
+ if (!tokenExpiresAt.value) return true;
222
+ return tokenExpiresAt.value - Date.now() < REFRESH_THRESHOLD_MS;
223
+ },
224
+ identify(user) {
225
+ ApiService.identifyVisitor({
226
+ externalId: user.userId,
227
+ name: user.name,
228
+ email: user.email
229
+ }).then(({ visitor_id, access_token }) => {
230
+ if (access_token) {
231
+ sessionToken.value = access_token;
232
+ }
233
+ if (visitor_id && visitor_id !== visitorId.value) {
234
+ visitorId.value = visitor_id;
235
+ localStorage.setItem(STORAGE_KEY, visitor_id);
236
+ }
237
+ });
238
+ },
239
+ clear() {
240
+ localStorage.removeItem(STORAGE_KEY);
241
+ visitorId.value = null;
242
+ sessionToken.value = null;
243
+ tokenExpiresAt.value = null;
244
+ isSetup.value = false;
245
+ }
246
+ };
247
+ }
248
+ });
249
+
250
+ // src/widget/modules/api/service.ts
251
+ async function doFetch(method, path, body) {
252
+ if (!appId.value) throw new Error("Spacinbox: boot() must be called before any API request");
253
+ const headers = {
254
+ "Content-Type": "application/json",
255
+ "x-app-id": appId.value,
256
+ "x-visitor-id": visitorId.value ?? ""
257
+ };
258
+ if (sessionToken.value) {
259
+ headers["Authorization"] = `Bearer ${sessionToken.value}`;
260
+ }
261
+ return fetch(`${BASE_URL}/widget${path}`, {
262
+ method,
263
+ headers,
264
+ body: body ? JSON.stringify(body) : void 0
265
+ });
266
+ }
267
+ async function request(method, path, body) {
268
+ if (path !== "/sessions") {
269
+ const { VisitorService: VisitorService2 } = await Promise.resolve().then(() => (init_service(), service_exports));
270
+ if (VisitorService2.isTokenExpiringSoon()) {
271
+ await VisitorService2.refreshSession();
272
+ }
273
+ }
274
+ let res = await doFetch(method, path, body);
275
+ if (res.status === 401 && path !== "/sessions") {
276
+ const { VisitorService: VisitorService2 } = await Promise.resolve().then(() => (init_service(), service_exports));
277
+ await VisitorService2.refreshSession();
278
+ res = await doFetch(method, path, body);
279
+ }
280
+ if (!res.ok) throw new Error(`[Spacinbox] ${method} ${path} failed with status ${res.status}`);
281
+ return res.json();
282
+ }
283
+ var BASE_URL, IS_MOCKED, ApiService;
284
+ var init_service2 = __esm({
285
+ "src/widget/modules/api/service.ts"() {
286
+ "use strict";
287
+ init_state();
288
+ init_mock();
289
+ BASE_URL = "https://api.spacinbox.com";
290
+ IS_MOCKED = false;
291
+ ApiService = IS_MOCKED ? mockService : {
292
+ createSession(dto) {
293
+ return request("POST", "/sessions", dto);
294
+ },
295
+ getSession() {
296
+ return request("GET", "/sessions");
297
+ },
298
+ identifyVisitor(dto) {
299
+ return request("POST", "/visitors/identify", dto);
300
+ },
301
+ getAgents() {
302
+ return request("GET", "/agents");
303
+ },
304
+ getConversations() {
305
+ return request("GET", "/conversations");
306
+ },
307
+ createConversation(dto) {
308
+ return request("POST", "/conversations", dto);
309
+ },
310
+ getConversation(conversationId) {
311
+ return request("GET", `/conversations/${conversationId}`);
312
+ },
313
+ reopenConversation(conversationId) {
314
+ return request("POST", `/conversations/${conversationId}/reopen`);
315
+ },
316
+ getConversationSocketToken(conversationId) {
317
+ return request("GET", `/conversations/${conversationId}/token`);
318
+ },
319
+ getMessages(conversationId, params) {
320
+ const qs = new URLSearchParams();
321
+ if (params?.before) qs.set("before", params.before);
322
+ if (params?.limit) qs.set("limit", String(params.limit));
323
+ const query = qs.toString();
324
+ return request(
325
+ "GET",
326
+ `/conversations/${conversationId}/messages${query ? `?${query}` : ""}`
327
+ );
328
+ },
329
+ sendMessage(conversationId, dto) {
330
+ return request("POST", `/conversations/${conversationId}/messages`, dto);
331
+ }
332
+ };
333
+ }
334
+ });
335
+
336
+ // src/widget/mount.tsx
337
+ import { render } from "preact";
338
+
339
+ // src/widget/Widget.tsx
340
+ import { MessageCircle, X } from "lucide-preact";
341
+ import { useEffect as useEffect10 } from "preact/hooks";
342
+
343
+ // src/widget/components/Layout.tsx
344
+ init_state();
345
+
346
+ // src/widget/ui/Loader.tsx
347
+ import { Loader2 } from "lucide-preact";
348
+ import { jsx } from "preact/jsx-runtime";
349
+ var Loader = ({ size = 20, class: cls }) => /* @__PURE__ */ jsx(Loader2, { size, class: `animate-spin text-inherit ${cls ?? ""}` });
350
+
351
+ // src/widget/components/Layout.tsx
352
+ import { jsx as jsx2, jsxs } from "preact/jsx-runtime";
353
+ var Layout = ({ isOpen: isOpen2, children }) => {
354
+ return /* @__PURE__ */ jsxs(
355
+ "div",
356
+ {
357
+ class: `fixed bottom-20 right-4 w-80 h-112 bg-white rounded-xl shadow-2xl border border-gray-200 flex flex-col overflow-hidden ${!isOpen2 && "hidden"}`,
358
+ children: [
359
+ !isOnline.value && /* @__PURE__ */ jsxs("div", { class: "bg-amber-50 border-b border-amber-200 px-3 py-2 text-xs text-amber-700 text-center", children: [
360
+ "Connecting... ",
361
+ /* @__PURE__ */ jsx2(Loader, {})
362
+ ] }),
363
+ /* @__PURE__ */ jsx2("div", { class: "flex-1 overflow-y-auto", children })
364
+ ]
365
+ }
366
+ );
367
+ };
368
+
369
+ // src/widget/hooks/useNewMessage.tsx
370
+ import { useEffect } from "preact/hooks";
371
+
372
+ // src/widget/modules/socket/service.ts
373
+ init_state();
374
+ init_service2();
375
+ init_service();
376
+ import { signal as signal2 } from "@preact/signals";
377
+ import { io } from "socket.io-client";
378
+ var socket = signal2(null);
379
+ var SocketService = {
380
+ async connect() {
381
+ const token = sessionToken.value;
382
+ socket.value = io("https://ws.spacinbox.com", {
383
+ auth: { token },
384
+ reconnection: true
385
+ });
386
+ socket.value.on("connect", () => {
387
+ isOnline.value = true;
388
+ });
389
+ socket.value.on("disconnect", (reason) => {
390
+ if (reason !== "io client disconnect") {
391
+ isOnline.value = false;
392
+ }
393
+ });
394
+ socket.value.on("connect_error", async (err) => {
395
+ if (err.message === "Missing token" || err.message === "Invalid token") {
396
+ await VisitorService.refreshSession();
397
+ if (socket.value) {
398
+ socket.value.auth = { token: sessionToken.value };
399
+ socket.value.connect();
400
+ }
401
+ } else {
402
+ isOnline.value = false;
403
+ }
404
+ });
405
+ },
406
+ async joinRoom(conversationId) {
407
+ if (!socket.value) {
408
+ return;
409
+ }
410
+ const { access_token: token } = await ApiService.getConversationSocketToken(conversationId);
411
+ return new Promise((resolve, reject) => {
412
+ socket.value.emit("join-room", { token }, (response) => {
413
+ if (!response?.ok) {
414
+ reject(new Error());
415
+ } else {
416
+ resolve();
417
+ }
418
+ });
419
+ });
420
+ },
421
+ leaveRoom(conversationId) {
422
+ socket.value?.emit("leave-room", { room_id: `conversation:${conversationId}` });
423
+ },
424
+ typingStart(conversationId) {
425
+ socket.value?.emit("typing-start", { room_id: `conversation:${conversationId}` });
426
+ },
427
+ typingStop(conversationId) {
428
+ socket.value?.emit("typing-stop", { room_id: `conversation:${conversationId}` });
429
+ },
430
+ on(event, handler) {
431
+ socket.value?.on(event, handler);
432
+ },
433
+ off(event, handler) {
434
+ socket.value?.off(event, handler);
435
+ },
436
+ disconnect() {
437
+ socket.value?.disconnect();
438
+ socket.value = null;
439
+ }
440
+ };
441
+
442
+ // src/widget/hooks/useNewMessage.tsx
443
+ var useOnNewMessage = (handler) => {
444
+ useEffect(() => {
445
+ if (!socket.value) {
446
+ return;
447
+ }
448
+ SocketService.on("new-message", () => handler());
449
+ return () => {
450
+ SocketService.off("new-message", () => handler());
451
+ };
452
+ }, [socket.value]);
453
+ };
454
+
455
+ // src/widget/hooks/useQuery.ts
456
+ import { useCallback, useEffect as useEffect2, useState } from "preact/hooks";
457
+ var cache = /* @__PURE__ */ new Map();
458
+ var invalidateQuery = (queryKey) => cache.delete(serializeKey(queryKey));
459
+ function serializeKey(key) {
460
+ return typeof key === "string" ? key : JSON.stringify(key);
461
+ }
462
+ function useQuery({ queryKey, queryFn, enabled = true, cache: useCache = true, onSuccess, onError, onSettled }) {
463
+ const key = serializeKey(queryKey);
464
+ const cached = useCache ? cache.get(key) : void 0;
465
+ const [refetchCounter, setRefetchCounter] = useState(0);
466
+ const [state, setState] = useState(
467
+ cached !== void 0 ? { data: cached, loading: false, error: null, isSuccess: true, isFetched: true } : { data: null, loading: enabled, error: null, isSuccess: false, isFetched: false }
468
+ );
469
+ useEffect2(() => {
470
+ if (!enabled) return;
471
+ let cancelled = false;
472
+ const isRefetch = refetchCounter > 0;
473
+ if (cached === void 0 || isRefetch) {
474
+ setState((s) => ({ ...s, loading: true, error: null, isSuccess: false }));
475
+ }
476
+ queryFn().then((data) => {
477
+ if (cancelled) return;
478
+ if (useCache) cache.set(key, data);
479
+ setState({ data, loading: false, error: null, isSuccess: true, isFetched: true });
480
+ onSuccess?.(data);
481
+ onSettled?.(data, null);
482
+ }).catch((error) => {
483
+ if (cancelled) return;
484
+ setState((s) => ({ ...s, loading: false, error, isSuccess: false, isFetched: true }));
485
+ onError?.(error);
486
+ onSettled?.(null, error);
487
+ });
488
+ return () => {
489
+ cancelled = true;
490
+ };
491
+ }, [key, enabled, refetchCounter]);
492
+ const refetch = useCallback(() => setRefetchCounter((c) => c + 1), []);
493
+ return { ...state, refetch };
494
+ }
495
+
496
+ // src/widget/Widget.tsx
497
+ init_service2();
498
+
499
+ // src/widget/pages/conversation.tsx
500
+ import { useSignal } from "@preact/signals";
501
+ import { useEffect as useEffect8 } from "preact/hooks";
502
+
503
+ // src/widget/hooks/useMessages.ts
504
+ init_service2();
505
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef, useState as useState2 } from "preact/hooks";
506
+ function useMessages(conversationId, limit = 30) {
507
+ const [state, setState] = useState2({
508
+ messages: [],
509
+ hasMore: false,
510
+ nextCursor: null,
511
+ loading: !!conversationId,
512
+ loadingMore: false
513
+ });
514
+ const stateRef = useRef(state);
515
+ stateRef.current = state;
516
+ useEffect3(() => {
517
+ if (!conversationId) return;
518
+ let cancelled = false;
519
+ setState({ messages: [], hasMore: false, nextCursor: null, loading: true, loadingMore: false });
520
+ ApiService.getMessages(conversationId, { limit }).then(({ messages, hasMore, nextCursor }) => {
521
+ if (cancelled) {
522
+ return;
523
+ }
524
+ setState({ messages, hasMore, nextCursor, loading: false, loadingMore: false });
525
+ }).catch(() => {
526
+ if (cancelled) return;
527
+ setState((s) => ({ ...s, loading: false }));
528
+ });
529
+ return () => {
530
+ cancelled = true;
531
+ };
532
+ }, [conversationId]);
533
+ const loadMore = useCallback2(async () => {
534
+ const { hasMore, nextCursor, loadingMore } = stateRef.current;
535
+ if (!hasMore || loadingMore || !nextCursor || !conversationId) return;
536
+ setState((s) => ({ ...s, loadingMore: true }));
537
+ try {
538
+ const {
539
+ messages: older,
540
+ hasMore: newHasMore,
541
+ nextCursor: newCursor
542
+ } = await ApiService.getMessages(conversationId, { before: nextCursor, limit });
543
+ setState((s) => ({
544
+ ...s,
545
+ messages: [...s.messages, ...older],
546
+ hasMore: newHasMore,
547
+ nextCursor: newCursor,
548
+ loadingMore: false
549
+ }));
550
+ } catch {
551
+ setState((s) => ({ ...s, loadingMore: false }));
552
+ }
553
+ }, [conversationId]);
554
+ const append = useCallback2((message) => {
555
+ setState((item) => {
556
+ if (item.messages.some((m) => m.id === message.id)) {
557
+ return item;
558
+ }
559
+ return { ...item, messages: [message, ...item.messages] };
560
+ });
561
+ }, []);
562
+ const replace = useCallback2((tempId, message) => {
563
+ setState((s) => ({
564
+ ...s,
565
+ messages: s.messages.filter((m) => m.id !== message.id).map((m) => m.id === tempId ? message : m)
566
+ }));
567
+ }, []);
568
+ const remove = useCallback2((id) => {
569
+ setState((s) => ({ ...s, messages: s.messages.filter((m) => m.id !== id) }));
570
+ }, []);
571
+ return { ...state, loadMore, append, replace, remove };
572
+ }
573
+
574
+ // src/widget/hooks/useMutation.ts
575
+ import { useState as useState3 } from "preact/hooks";
576
+ function useMutation({ mutationFn, onSuccess, onError, onSettled }) {
577
+ const [state, setState] = useState3({
578
+ data: null,
579
+ loading: false,
580
+ error: null,
581
+ isSuccess: false,
582
+ isFetched: false
583
+ });
584
+ const mutate = async (variables) => {
585
+ setState({ data: null, loading: true, error: null, isSuccess: false, isFetched: false });
586
+ try {
587
+ const data = await mutationFn(variables);
588
+ setState({ data, loading: false, error: null, isSuccess: true, isFetched: true });
589
+ onSuccess?.(data, variables);
590
+ onSettled?.(data, null, variables);
591
+ return data;
592
+ } catch (error) {
593
+ const err = error;
594
+ setState({ data: null, loading: false, error: err, isSuccess: false, isFetched: true });
595
+ onError?.(err, variables);
596
+ onSettled?.(null, err, variables);
597
+ }
598
+ };
599
+ return { ...state, mutate };
600
+ }
601
+
602
+ // src/widget/pages/conversation.tsx
603
+ init_service2();
604
+
605
+ // src/widget/modules/conversation/components/Header.tsx
606
+ import { ArrowLeft } from "lucide-preact";
607
+
608
+ // src/widget/router.ts
609
+ import { computed, signal as signal3 } from "@preact/signals";
610
+ var pageStack = signal3([{ page: "home" }]);
611
+ var currentRoute = computed(() => pageStack.value.at(-1));
612
+ var currentPage = computed(() => currentRoute.value.page);
613
+ var canGoBack = computed(() => pageStack.value.length > 1);
614
+ var navigate = (page, params) => {
615
+ pageStack.value = [...pageStack.value, { page, params }];
616
+ };
617
+ var goBack = () => {
618
+ if (pageStack.value.length > 1)
619
+ pageStack.value = pageStack.value.slice(0, -1);
620
+ };
621
+ var resetRouter = () => {
622
+ pageStack.value = [{ page: "home" }];
623
+ };
624
+
625
+ // src/widget/ui/Avatar.tsx
626
+ import { jsx as jsx3 } from "preact/jsx-runtime";
627
+ var sizes = {
628
+ sm: "w-7 h-7 text-xs",
629
+ md: "w-9 h-9 text-sm",
630
+ lg: "w-12 h-12 text-base"
631
+ };
632
+ var Avatar = ({ src, children, size = "md", class: cls }) => {
633
+ const sizeClass = typeof size === "number" ? "" : sizes[size];
634
+ const sizeStyle = typeof size === "number" ? { width: `${size}px`, height: `${size}px`, fontSize: `${Math.round(size * 0.4)}px` } : void 0;
635
+ const base = `inline-flex items-center justify-center rounded-full overflow-hidden shrink-0 bg-blue-100 text-blue-700 font-medium border-2 border-blue-400 ${sizeClass} ${cls ?? ""}`;
636
+ if (src) {
637
+ return /* @__PURE__ */ jsx3("img", { src, class: `${base} object-cover`, style: sizeStyle, alt: children ?? "" });
638
+ }
639
+ return /* @__PURE__ */ jsx3("span", { class: base, style: sizeStyle, children: children ? children.charAt(0).toUpperCase() : "?" });
640
+ };
641
+
642
+ // src/widget/ui/Button.tsx
643
+ import { jsx as jsx4 } from "preact/jsx-runtime";
644
+ var variants = {
645
+ filled: "bg-blue-600 hover:bg-blue-700 text-white",
646
+ subtle: "bg-neutral-100 hover:bg-neutral-200 text-neutral-800",
647
+ light: "bg-transparent hover:text-neutral-800 text-neutral-500",
648
+ outline: "border border-neutral-200 hover:bg-neutral-50 text-neutral-800"
649
+ };
650
+ var Button = ({ children, onClick, variant = "filled", fullWidth, class: cls }) => /* @__PURE__ */ jsx4(
651
+ "button",
652
+ {
653
+ onClick,
654
+ class: `
655
+ inline-flex items-center justify-center gap-2
656
+ px-4 py-2 rounded-lg text-sm font-medium
657
+ transition-colors cursor-pointer
658
+ ${variants[variant]}
659
+ ${fullWidth ? "w-full" : ""}
660
+ ${cls ?? ""}
661
+ `,
662
+ children
663
+ }
664
+ );
665
+
666
+ // src/widget/ui/Group.tsx
667
+ import { jsx as jsx5 } from "preact/jsx-runtime";
668
+ var gaps = {
669
+ xs: "gap-1",
670
+ sm: "gap-2",
671
+ md: "gap-4",
672
+ lg: "gap-6"
673
+ };
674
+ var justifies = {
675
+ start: "justify-start",
676
+ center: "justify-center",
677
+ end: "justify-end",
678
+ between: "justify-between"
679
+ };
680
+ var aligns = {
681
+ start: "items-start",
682
+ center: "items-center",
683
+ end: "items-end"
684
+ };
685
+ var Group = ({ children, gap = "sm", justify = "start", align = "center", class: cls }) => {
686
+ const gapClass = typeof gap === "number" ? "" : gaps[gap];
687
+ const gapStyle = typeof gap === "number" ? { gap: `${gap * 4}px` } : {};
688
+ return /* @__PURE__ */ jsx5("div", { class: `flex flex-row ${gapClass} ${justifies[justify]} ${aligns[align]} ${cls ?? ""}`, style: gapStyle, children });
689
+ };
690
+
691
+ // src/widget/ui/Indicator.tsx
692
+ import { jsx as jsx6, jsxs as jsxs2 } from "preact/jsx-runtime";
693
+ var colors = {
694
+ default: "bg-blue-500",
695
+ green: "bg-green-500",
696
+ red: "bg-red-500"
697
+ };
698
+ var positions = {
699
+ "top-right": "top-0 right-0 translate-x-1/2 -translate-y-1/2",
700
+ "bottom-right": "bottom-0 right-0"
701
+ };
702
+ var sizes2 = {
703
+ sm: { dot: "w-2.5 h-2.5", badge: "h-4 min-w-4 text-[10px]" },
704
+ md: { dot: "w-3.5 h-3.5", badge: "h-5 min-w-5 text-xs" },
705
+ lg: { dot: "w-4 h-4", badge: "h-6 min-w-6 text-sm" }
706
+ };
707
+ var Indicator = ({
708
+ children,
709
+ active = true,
710
+ visible = true,
711
+ color = "default",
712
+ label,
713
+ size = "sm",
714
+ position = "bottom-right",
715
+ offset
716
+ }) => {
717
+ const show = active && visible;
718
+ const offsetStyle = offset ? { transform: `translate(calc(50% + ${offset.x ?? 0}px), calc(-50% + ${offset.y ?? 0}px))` } : void 0;
719
+ return /* @__PURE__ */ jsxs2("div", { class: "relative inline-flex", children: [
720
+ children,
721
+ show && (label !== void 0 ? /* @__PURE__ */ jsx6(
722
+ "span",
723
+ {
724
+ style: offsetStyle,
725
+ class: `absolute ${positions[position]} ${sizes2[size].badge} px-1 ${colors[color]} text-white font-semibold rounded-full flex items-center justify-center`,
726
+ children: label
727
+ }
728
+ ) : /* @__PURE__ */ jsx6(
729
+ "span",
730
+ {
731
+ style: offsetStyle,
732
+ class: `absolute ${positions[position]} ${sizes2[size].dot} ${colors[color]} border-2 border-white rounded-full`
733
+ }
734
+ ))
735
+ ] });
736
+ };
737
+
738
+ // src/widget/ui/Text.tsx
739
+ import { jsx as jsx7 } from "preact/jsx-runtime";
740
+ var sizes3 = {
741
+ xs: "text-xs",
742
+ sm: "text-sm",
743
+ md: "text-base",
744
+ lg: "text-lg",
745
+ xl: "text-xl"
746
+ };
747
+ var weights = {
748
+ normal: "font-normal",
749
+ medium: "font-medium",
750
+ semibold: "font-semibold"
751
+ };
752
+ var colors2 = {
753
+ default: "text-neutral-900",
754
+ dimmed: "text-neutral-400"
755
+ };
756
+ var Text = ({
757
+ children,
758
+ size = "sm",
759
+ weight = "normal",
760
+ color = "default",
761
+ lineClamp,
762
+ class: cls
763
+ }) => {
764
+ const clampStyle = lineClamp ? {
765
+ overflow: "hidden",
766
+ display: "-webkit-box",
767
+ WebkitLineClamp: lineClamp,
768
+ WebkitBoxOrient: "vertical"
769
+ } : void 0;
770
+ return /* @__PURE__ */ jsx7("p", { class: `${sizes3[size]} ${weights[weight]} ${colors2[color]} ${cls ?? ""}`, style: clampStyle, children });
771
+ };
772
+
773
+ // src/widget/modules/conversation/components/Header.tsx
774
+ import { Fragment, jsx as jsx8, jsxs as jsxs3 } from "preact/jsx-runtime";
775
+ var Header = ({ agent, loading }) => /* @__PURE__ */ jsxs3(Group, { class: "border-b border-neutral-100 p-2", children: [
776
+ /* @__PURE__ */ jsx8(Button, { variant: "light", onClick: goBack, "aria-label": "Back", children: /* @__PURE__ */ jsx8(ArrowLeft, {}) }),
777
+ loading ? /* @__PURE__ */ jsx8("div", { class: "w-8 h-8 rounded-full bg-neutral-200 animate-pulse shrink-0" }) : agent ? /* @__PURE__ */ jsxs3(Fragment, { children: [
778
+ /* @__PURE__ */ jsx8(Indicator, { color: "green", children: /* @__PURE__ */ jsx8(Avatar, { size: "sm", src: agent.picture, children: agent.name.charAt(0) }) }),
779
+ /* @__PURE__ */ jsx8(Text, { weight: "medium", children: agent.name })
780
+ ] }) : null
781
+ ] });
782
+
783
+ // src/widget/modules/conversation/components/InputMessage.tsx
784
+ import { useEffect as useEffect4, useRef as useRef2 } from "preact/hooks";
785
+ import { Send } from "lucide-preact";
786
+ import { jsx as jsx9, jsxs as jsxs4 } from "preact/jsx-runtime";
787
+ var InputMessage = ({ onSend, onType, disabled }) => {
788
+ const ref = useRef2(null);
789
+ useEffect4(() => {
790
+ if (!disabled) ref.current?.focus();
791
+ }, [disabled]);
792
+ const resize = () => {
793
+ const el = ref.current;
794
+ if (!el) return;
795
+ el.style.height = "auto";
796
+ el.style.height = `${el.scrollHeight}px`;
797
+ };
798
+ const submit = () => {
799
+ const el = ref.current;
800
+ if (!el || disabled) return;
801
+ const text = el.value.trim();
802
+ if (!text) return;
803
+ onSend(text);
804
+ el.value = "";
805
+ el.style.height = "auto";
806
+ };
807
+ const onKeyDown = (e) => {
808
+ if (e.key !== "Enter") return;
809
+ if (e.metaKey || e.ctrlKey) {
810
+ e.preventDefault();
811
+ const el = ref.current;
812
+ const start = el.selectionStart ?? el.value.length;
813
+ const end = el.selectionEnd ?? el.value.length;
814
+ el.value = el.value.slice(0, start) + "\n" + el.value.slice(end);
815
+ el.selectionStart = el.selectionEnd = start + 1;
816
+ resize();
817
+ } else if (!e.shiftKey) {
818
+ e.preventDefault();
819
+ submit();
820
+ }
821
+ };
822
+ return /* @__PURE__ */ jsxs4("div", { class: "flex items-end gap-2 px-3 py-3 border-t border-neutral-100", children: [
823
+ /* @__PURE__ */ jsx9(
824
+ "textarea",
825
+ {
826
+ ref,
827
+ rows: 1,
828
+ placeholder: "Write a message...",
829
+ onInput: () => {
830
+ resize();
831
+ onType?.();
832
+ },
833
+ onKeyDown,
834
+ disabled,
835
+ class: "flex-1 text-sm bg-neutral-100 rounded-2xl px-4 py-2 outline-none placeholder:text-neutral-400 disabled:opacity-50 resize-none overflow-y-auto leading-5",
836
+ style: { maxHeight: "120px" }
837
+ }
838
+ ),
839
+ /* @__PURE__ */ jsx9(
840
+ "button",
841
+ {
842
+ onClick: submit,
843
+ disabled,
844
+ class: "w-8 h-8 flex items-center justify-center bg-blue-600 hover:bg-blue-700 text-white rounded-full transition-colors cursor-pointer shrink-0 disabled:opacity-50 disabled:cursor-not-allowed mb-0.5",
845
+ "aria-label": "Send",
846
+ children: /* @__PURE__ */ jsx9(Send, { size: 14 })
847
+ }
848
+ )
849
+ ] });
850
+ };
851
+
852
+ // src/widget/modules/conversation/components/MessageList.tsx
853
+ import { Fragment as Fragment2 } from "preact";
854
+ import { useEffect as useEffect5, useRef as useRef3 } from "preact/hooks";
855
+
856
+ // src/widget/modules/conversation/components/DateSeparator.tsx
857
+ import { jsx as jsx10, jsxs as jsxs5 } from "preact/jsx-runtime";
858
+ function formatLabel(dateStr) {
859
+ const date = new Date(dateStr);
860
+ const today = /* @__PURE__ */ new Date();
861
+ const yesterday = new Date(today);
862
+ yesterday.setDate(today.getDate() - 1);
863
+ const sameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
864
+ if (sameDay(date, today)) return "Today";
865
+ if (sameDay(date, yesterday)) return "Yesterday";
866
+ return date.toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short" }).replace(",", "");
867
+ }
868
+ var DateSeparator = ({ date }) => /* @__PURE__ */ jsxs5("div", { class: "flex items-center gap-2 my-3", children: [
869
+ /* @__PURE__ */ jsx10("div", { class: "flex-1 h-px bg-neutral-200" }),
870
+ /* @__PURE__ */ jsx10("span", { class: "text-xs text-neutral-400 font-medium px-1", children: formatLabel(date) }),
871
+ /* @__PURE__ */ jsx10("div", { class: "flex-1 h-px bg-neutral-200" })
872
+ ] });
873
+
874
+ // src/widget/modules/conversation/components/MessageBubble.tsx
875
+ import { jsx as jsx11, jsxs as jsxs6 } from "preact/jsx-runtime";
876
+ function formatTime(dateStr) {
877
+ return new Date(dateStr).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
878
+ }
879
+ var MessageBubble = ({ message, agent, isFirstInGroup }) => {
880
+ const isUser = message.sender === "user";
881
+ return /* @__PURE__ */ jsxs6("div", { class: `flex gap-2 items-end ${isUser ? "flex-row-reverse" : "flex-row"}`, children: [
882
+ !isUser && (isFirstInGroup ? /* @__PURE__ */ jsx11("div", { class: "w-7 h-7 rounded-full bg-neutral-200 shrink-0 overflow-hidden flex items-center justify-center text-xs font-semibold text-neutral-600 self-end mb-0.5", children: agent?.picture ? /* @__PURE__ */ jsx11("img", { src: agent.picture, alt: agent.name, class: "w-full h-full object-cover" }) : agent?.name?.charAt(0) ?? "?" }) : /* @__PURE__ */ jsx11("div", { class: "w-7 shrink-0" })),
883
+ /* @__PURE__ */ jsxs6("div", { class: `flex flex-col gap-0.5 max-w-[72%] ${isUser ? "items-end" : "items-start"}`, children: [
884
+ !isUser && isFirstInGroup && /* @__PURE__ */ jsx11("span", { class: "text-xs text-neutral-500 font-semibold px-1", children: agent?.name }),
885
+ /* @__PURE__ */ jsxs6("div", { class: `relative px-3 pt-2 pb-5 min-w-18 rounded-2xl text-sm leading-snug whitespace-pre-wrap ${isUser ? "bg-blue-600 text-white rounded-br-sm" : "bg-neutral-100 text-neutral-900 rounded-bl-sm"}`, children: [
886
+ message.text,
887
+ /* @__PURE__ */ jsx11("span", { class: `absolute bottom-1.5 right-2.5 text-[10px] leading-none ${isUser ? "text-blue-200" : "text-neutral-400"}`, children: formatTime(message.createdAt) })
888
+ ] })
889
+ ] })
890
+ ] });
891
+ };
892
+
893
+ // src/widget/modules/conversation/components/MessageList.tsx
894
+ import { jsx as jsx12, jsxs as jsxs7 } from "preact/jsx-runtime";
895
+ function isSameDay(a, b) {
896
+ return new Date(a).toDateString() === new Date(b).toDateString();
897
+ }
898
+ var MessageList = ({
899
+ messages,
900
+ agent,
901
+ scrollKey,
902
+ hasMore,
903
+ loadingMore,
904
+ onLoadMore,
905
+ isTyping
906
+ }) => {
907
+ const containerRef = useRef3(null);
908
+ useEffect5(() => {
909
+ const container = containerRef.current;
910
+ if (!container) {
911
+ return;
912
+ }
913
+ const onScroll = () => {
914
+ const canTrigger = Math.abs(container.scrollTop) > container.scrollHeight - container.clientHeight - 100;
915
+ if (hasMore && !loadingMore && canTrigger) {
916
+ onLoadMore?.();
917
+ }
918
+ };
919
+ container.addEventListener("scroll", onScroll, { passive: true });
920
+ return () => container.removeEventListener("scroll", onScroll);
921
+ }, [hasMore, loadingMore, onLoadMore]);
922
+ return /* @__PURE__ */ jsx12("div", { class: "flex-1 flex flex-col overflow-hidden", children: /* @__PURE__ */ jsxs7("div", { ref: containerRef, class: "flex-1 overflow-y-auto flex flex-col-reverse gap-1 px-3 py-4", children: [
923
+ isTyping && /* @__PURE__ */ jsxs7("div", { class: "flex gap-2 items-end", children: [
924
+ /* @__PURE__ */ jsx12("div", { class: "w-7 h-7 rounded-full bg-neutral-200 shrink-0 overflow-hidden flex items-center justify-center text-xs font-semibold text-neutral-600 self-end mb-0.5", children: agent?.picture ? /* @__PURE__ */ jsx12("img", { src: agent.picture, alt: agent.name, class: "w-full h-full object-cover" }) : agent?.name?.charAt(0) ?? "?" }),
925
+ /* @__PURE__ */ jsxs7("div", { class: "bg-neutral-100 rounded-2xl rounded-bl-sm px-3 py-2.5 flex gap-1 items-center", children: [
926
+ /* @__PURE__ */ jsx12("span", { class: "w-1.5 h-1.5 rounded-full bg-neutral-400 animate-bounce", style: { animationDelay: "0ms" } }),
927
+ /* @__PURE__ */ jsx12("span", { class: "w-1.5 h-1.5 rounded-full bg-neutral-400 animate-bounce", style: { animationDelay: "150ms" } }),
928
+ /* @__PURE__ */ jsx12("span", { class: "w-1.5 h-1.5 rounded-full bg-neutral-400 animate-bounce", style: { animationDelay: "300ms" } })
929
+ ] })
930
+ ] }),
931
+ messages.map((msg, i) => {
932
+ const next = messages[i + 1];
933
+ const showDate = !next || !isSameDay(next.createdAt, msg.createdAt);
934
+ const isFirstInGroup = showDate || next?.sender !== msg.sender;
935
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
936
+ /* @__PURE__ */ jsx12(MessageBubble, { message: msg, agent, isFirstInGroup }),
937
+ showDate && /* @__PURE__ */ jsx12(DateSeparator, { date: msg.createdAt })
938
+ ] }, msg.id);
939
+ }),
940
+ /* @__PURE__ */ jsx12(
941
+ "div",
942
+ {
943
+ class: `flex justify-center py-2 border-b border-neutral-100 ${!loadingMore && "opacity-0"}`,
944
+ children: /* @__PURE__ */ jsx12(Loader, { size: 16 })
945
+ }
946
+ )
947
+ ] }) });
948
+ };
949
+
950
+ // src/widget/modules/conversation/components/ResolvedBanner.tsx
951
+ import { CheckCircle } from "lucide-preact";
952
+ import { useEffect as useEffect6, useState as useState4 } from "preact/hooks";
953
+
954
+ // src/widget/ui/Stack.tsx
955
+ import { jsx as jsx13 } from "preact/jsx-runtime";
956
+ var gaps2 = {
957
+ xs: "gap-1",
958
+ sm: "gap-2",
959
+ md: "gap-4",
960
+ lg: "gap-6"
961
+ };
962
+ var aligns2 = {
963
+ start: "items-start",
964
+ center: "items-center",
965
+ end: "items-end",
966
+ stretch: "items-stretch"
967
+ };
968
+ var justifies2 = {
969
+ start: "justify-start",
970
+ center: "justify-center",
971
+ end: "justify-end",
972
+ between: "justify-between"
973
+ };
974
+ var Stack = ({ children, gap = "md", align, justify, class: cls }) => {
975
+ const gapClass = gaps2[gap] ?? `gap-${gap}`;
976
+ return /* @__PURE__ */ jsx13("div", { class: `flex flex-col ${gapClass} ${align ? aligns2[align] : ""} ${justify ? justifies2[justify] : ""} ${cls ?? ""}`, children });
977
+ };
978
+
979
+ // src/widget/modules/conversation/components/ResolvedBanner.tsx
980
+ import { jsx as jsx14, jsxs as jsxs8 } from "preact/jsx-runtime";
981
+ var ResolvedBanner = ({ onYes, onNo }) => {
982
+ const [thanked, setThanked] = useState4(false);
983
+ useEffect6(() => {
984
+ if (!thanked) return;
985
+ const timer = setTimeout(onYes, 3e3);
986
+ return () => clearTimeout(timer);
987
+ }, [thanked]);
988
+ if (thanked) {
989
+ return /* @__PURE__ */ jsxs8(Stack, { class: "border-t border-neutral-100 p-4 min-h-[175px]", align: "center", children: [
990
+ /* @__PURE__ */ jsx14(CheckCircle, { size: 20, class: "text-green-500" }),
991
+ /* @__PURE__ */ jsx14(Text, { size: "sm", weight: "semibold", children: "Glad we could help" }),
992
+ /* @__PURE__ */ jsx14(Text, { size: "sm", color: "dimmed", children: "See you next time" })
993
+ ] });
994
+ }
995
+ return /* @__PURE__ */ jsxs8(Stack, { class: "border-t border-neutral-100 p-4 min-h-[175px]", align: "center", children: [
996
+ /* @__PURE__ */ jsx14(CheckCircle, { size: 20, class: "text-green-500" }),
997
+ /* @__PURE__ */ jsxs8(Stack, { gap: 0, align: "center", children: [
998
+ /* @__PURE__ */ jsx14(Text, { size: "sm", children: "This conversation has been resolved" }),
999
+ /* @__PURE__ */ jsx14(Text, { size: "sm", color: "dimmed", children: "Did we answer your question?" })
1000
+ ] }),
1001
+ /* @__PURE__ */ jsxs8(Group, { class: "w-full", children: [
1002
+ /* @__PURE__ */ jsx14(Button, { class: "flex-1", variant: "outline", fullWidth: true, onClick: onNo, children: /* @__PURE__ */ jsxs8(Stack, { gap: 0, children: [
1003
+ /* @__PURE__ */ jsx14(Text, { children: "No" }),
1004
+ /* @__PURE__ */ jsx14(Text, { size: "xs", color: "dimmed", children: "I need more help" })
1005
+ ] }) }),
1006
+ /* @__PURE__ */ jsx14(Button, { class: "flex-1", onClick: () => setThanked(true), children: /* @__PURE__ */ jsxs8(Stack, { gap: 0, children: [
1007
+ /* @__PURE__ */ jsx14(Text, { class: "text-white", weight: "semibold", children: "Yes" }),
1008
+ /* @__PURE__ */ jsx14(Text, { size: "xs", class: "text-white", children: "Thanks" })
1009
+ ] }) })
1010
+ ] })
1011
+ ] });
1012
+ };
1013
+
1014
+ // src/widget/modules/socket/useOnTyping.ts
1015
+ import { useEffect as useEffect7, useRef as useRef4, useState as useState5 } from "preact/hooks";
1016
+ var TYPING_TIMEOUT_MS = 1e4;
1017
+ var useOnTyping = ({ room_id }) => {
1018
+ const [isTyping, setIsTyping] = useState5(false);
1019
+ const stopTimerRef = useRef4(null);
1020
+ const clearStopTimer = () => {
1021
+ if (stopTimerRef.current) {
1022
+ clearTimeout(stopTimerRef.current);
1023
+ stopTimerRef.current = null;
1024
+ }
1025
+ };
1026
+ const stopTyping = () => {
1027
+ clearStopTimer();
1028
+ setIsTyping(false);
1029
+ };
1030
+ useEffect7(() => {
1031
+ if (!socket.value) {
1032
+ return;
1033
+ }
1034
+ const handlerStart = (payload) => {
1035
+ const canSkip = room_id && !payload.room_id.includes(room_id);
1036
+ if (canSkip) {
1037
+ return;
1038
+ }
1039
+ setIsTyping(true);
1040
+ clearStopTimer();
1041
+ stopTimerRef.current = setTimeout(stopTyping, TYPING_TIMEOUT_MS);
1042
+ };
1043
+ const handlerStop = (payload) => {
1044
+ const canSkip = room_id && !payload.room_id.includes(room_id);
1045
+ if (canSkip) {
1046
+ return;
1047
+ }
1048
+ stopTyping();
1049
+ };
1050
+ socket.value.on("typing-start", handlerStart);
1051
+ socket.value.on("typing-stop", handlerStop);
1052
+ return () => {
1053
+ socket.value?.off("typing-start", handlerStart);
1054
+ socket.value?.off("typing-stop", handlerStop);
1055
+ clearStopTimer();
1056
+ };
1057
+ }, [room_id, socket]);
1058
+ return { isTyping };
1059
+ };
1060
+
1061
+ // src/widget/modules/socket/useTyping.ts
1062
+ import { useRef as useRef5 } from "preact/hooks";
1063
+ var TYPING_START_DELAY_MS = 3e3;
1064
+ var TYPING_HEARTBEAT_MS = 5e3;
1065
+ var TYPING_STOP_DELAY_MS = 5e3;
1066
+ var useTyping = ({ room_id }) => {
1067
+ const startTimerRef = useRef5(null);
1068
+ const stopTimerRef = useRef5(null);
1069
+ const lastEmitRef = useRef5(0);
1070
+ const onType = () => {
1071
+ if (!socket.value) {
1072
+ return;
1073
+ }
1074
+ const now = Date.now();
1075
+ if (lastEmitRef.current > 0 && now - lastEmitRef.current >= TYPING_HEARTBEAT_MS) {
1076
+ SocketService.typingStart(room_id);
1077
+ lastEmitRef.current = now;
1078
+ }
1079
+ if (lastEmitRef.current === 0 && !startTimerRef.current) {
1080
+ startTimerRef.current = setTimeout(() => {
1081
+ SocketService.typingStart(room_id);
1082
+ lastEmitRef.current = Date.now();
1083
+ startTimerRef.current = null;
1084
+ }, TYPING_START_DELAY_MS);
1085
+ }
1086
+ if (stopTimerRef.current) {
1087
+ clearTimeout(stopTimerRef.current);
1088
+ }
1089
+ stopTimerRef.current = setTimeout(() => {
1090
+ if (startTimerRef.current) {
1091
+ clearTimeout(startTimerRef.current);
1092
+ startTimerRef.current = null;
1093
+ }
1094
+ if (lastEmitRef.current > 0) {
1095
+ SocketService.typingStop(room_id);
1096
+ lastEmitRef.current = 0;
1097
+ }
1098
+ stopTimerRef.current = null;
1099
+ }, TYPING_STOP_DELAY_MS);
1100
+ };
1101
+ const stopTyping = () => {
1102
+ if (startTimerRef.current) {
1103
+ clearTimeout(startTimerRef.current);
1104
+ startTimerRef.current = null;
1105
+ }
1106
+ if (stopTimerRef.current) {
1107
+ clearTimeout(stopTimerRef.current);
1108
+ stopTimerRef.current = null;
1109
+ }
1110
+ if (lastEmitRef.current > 0) {
1111
+ SocketService.typingStop(room_id);
1112
+ lastEmitRef.current = 0;
1113
+ }
1114
+ };
1115
+ return { onType, stopTyping };
1116
+ };
1117
+
1118
+ // src/widget/pages/conversation.tsx
1119
+ init_state();
1120
+ import { jsx as jsx15, jsxs as jsxs9 } from "preact/jsx-runtime";
1121
+ var Conversation = (props) => {
1122
+ const conversationId = useSignal(props.conversationId);
1123
+ const isCreating = useSignal(false);
1124
+ const agentsQuery = useQuery({
1125
+ queryKey: ["agents"],
1126
+ queryFn: () => ApiService.getAgents()
1127
+ });
1128
+ const conversationQuery = useQuery({
1129
+ queryKey: [`conversation-${conversationId}`],
1130
+ queryFn: () => ApiService.getConversation(conversationId.value),
1131
+ enabled: !!conversationId.value
1132
+ });
1133
+ const { messages, hasMore, loadingMore, loadMore, append, replace, remove } = useMessages(
1134
+ conversationId.value
1135
+ );
1136
+ const reopenMutation = useMutation({
1137
+ mutationFn: () => ApiService.reopenConversation(conversationId.value),
1138
+ onSuccess: () => conversationQuery.refetch()
1139
+ });
1140
+ const agent = conversationQuery.data?.agent ?? agentsQuery.data?.[0];
1141
+ const isResolved = conversationQuery.data?.status === "CLOSED";
1142
+ const isOpen2 = conversationQuery.data?.status === "OPEN";
1143
+ const canConnectSocket = conversationId.value && isOpen2;
1144
+ const onNewMessage = (message) => {
1145
+ append(message);
1146
+ };
1147
+ useEffect8(() => {
1148
+ if (!conversationId.value) {
1149
+ return;
1150
+ }
1151
+ SocketService.on("conversation-updated", () => conversationQuery.refetch());
1152
+ return () => {
1153
+ SocketService.off("conversation-updated", () => conversationQuery.refetch());
1154
+ };
1155
+ }, [conversationId.value]);
1156
+ useEffect8(() => {
1157
+ if (!canConnectSocket) {
1158
+ return;
1159
+ }
1160
+ const id = conversationId.value;
1161
+ SocketService.joinRoom(id).then(() => {
1162
+ refetchSessionCounter.value += 1;
1163
+ }).catch((error) => {
1164
+ });
1165
+ SocketService.on("new-message", onNewMessage);
1166
+ return () => {
1167
+ SocketService.leaveRoom(id);
1168
+ SocketService.off("new-message", onNewMessage);
1169
+ };
1170
+ }, [canConnectSocket]);
1171
+ const send = async (text) => {
1172
+ if (!agent || isCreating.value) return;
1173
+ stopTyping();
1174
+ const tempId = `temp-${Date.now()}`;
1175
+ const optimistic = {
1176
+ id: tempId,
1177
+ conversationId: conversationId.value ?? "pending",
1178
+ text,
1179
+ sender: "user",
1180
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1181
+ };
1182
+ append(optimistic);
1183
+ try {
1184
+ if (!conversationId.value) {
1185
+ isCreating.value = true;
1186
+ const conversation = await ApiService.createConversation({ agentId: agent.id, text });
1187
+ conversationId.value = conversation.id;
1188
+ invalidateQuery(["conversations"]);
1189
+ } else {
1190
+ const message = await ApiService.sendMessage(conversationId.value, { text });
1191
+ replace(tempId, message);
1192
+ }
1193
+ } catch {
1194
+ remove(tempId);
1195
+ } finally {
1196
+ isCreating.value = false;
1197
+ }
1198
+ };
1199
+ const handleYes = () => {
1200
+ goBack();
1201
+ };
1202
+ const handleNo = () => {
1203
+ reopenMutation.mutate();
1204
+ };
1205
+ const { onType, stopTyping } = useTyping({ room_id: conversationId.value ?? "" });
1206
+ const { isTyping } = useOnTyping({ room_id: conversationId.value });
1207
+ const disabled = !agent || isCreating.value;
1208
+ return /* @__PURE__ */ jsxs9("div", { class: "flex flex-col h-full", children: [
1209
+ /* @__PURE__ */ jsx15(
1210
+ Header,
1211
+ {
1212
+ agent,
1213
+ loading: !agent && (agentsQuery.loading || conversationQuery.loading)
1214
+ }
1215
+ ),
1216
+ /* @__PURE__ */ jsx15(
1217
+ MessageList,
1218
+ {
1219
+ messages,
1220
+ agent,
1221
+ scrollKey: isResolved,
1222
+ hasMore,
1223
+ loadingMore,
1224
+ onLoadMore: loadMore,
1225
+ isTyping
1226
+ }
1227
+ ),
1228
+ isResolved ? /* @__PURE__ */ jsx15(ResolvedBanner, { onYes: handleYes, onNo: handleNo }) : /* @__PURE__ */ jsx15(InputMessage, { onSend: send, onType, disabled })
1229
+ ] });
1230
+ };
1231
+
1232
+ // src/widget/pages/home.tsx
1233
+ import { ChevronRight, SendHorizonal } from "lucide-preact";
1234
+ import { useEffect as useEffect9 } from "preact/hooks";
1235
+ init_service2();
1236
+ init_state();
1237
+
1238
+ // src/widget/ui/Card.tsx
1239
+ import { jsx as jsx16 } from "preact/jsx-runtime";
1240
+ var Card = ({ children, onClick, class: cls }) => /* @__PURE__ */ jsx16(
1241
+ "div",
1242
+ {
1243
+ onClick,
1244
+ class: `
1245
+ p-4 rounded-lg border border-neutral-200 bg-white
1246
+ ${onClick ? "cursor-pointer hover:bg-neutral-50 transition-colors" : ""}
1247
+ ${cls ?? ""}
1248
+ `,
1249
+ children
1250
+ }
1251
+ );
1252
+
1253
+ // src/widget/ui/Title.tsx
1254
+ import { jsx as jsx17 } from "preact/jsx-runtime";
1255
+ var styles = {
1256
+ 1: { tag: "h1", cls: "text-2xl font-semibold" },
1257
+ 2: { tag: "h2", cls: "text-lg font-semibold" },
1258
+ 3: { tag: "h3", cls: "text-base font-medium" }
1259
+ };
1260
+ var colors3 = {
1261
+ default: "text-neutral-800",
1262
+ dimmed: "text-neutral-400"
1263
+ };
1264
+ var Title = ({ children, order = 1, color = "default", lineClamp, class: cls }) => {
1265
+ const { tag: Tag, cls: base } = styles[order];
1266
+ const colorClass = colors3[color] ?? color;
1267
+ const clampStyle = lineClamp ? { overflow: "hidden", display: "-webkit-box", WebkitLineClamp: lineClamp, WebkitBoxOrient: "vertical" } : void 0;
1268
+ return /* @__PURE__ */ jsx17(Tag, { class: `${base} ${cls ?? ""} ${colorClass}`, style: clampStyle, children });
1269
+ };
1270
+
1271
+ // src/widget/pages/home.tsx
1272
+ import { jsx as jsx18, jsxs as jsxs10 } from "preact/jsx-runtime";
1273
+ var Home = () => {
1274
+ const agentsQuery = useQuery({ queryKey: ["agents"], queryFn: () => ApiService.getAgents() });
1275
+ const conversationsQuery = useQuery({
1276
+ queryKey: ["conversations"],
1277
+ queryFn: () => ApiService.getConversations()
1278
+ });
1279
+ const agents = agentsQuery.data ?? [];
1280
+ const conversations = conversationsQuery.data ?? [];
1281
+ const userName = currentUser.value?.name;
1282
+ useOnNewMessage(() => {
1283
+ conversationsQuery.refetch();
1284
+ });
1285
+ useEffect9(() => {
1286
+ conversationsQuery.refetch();
1287
+ }, [visitorId.value]);
1288
+ return /* @__PURE__ */ jsxs10(Stack, { class: "flex flex-col p-4 gradient-header", children: [
1289
+ /* @__PURE__ */ jsx18(Group, { justify: "end", children: agents?.map((agent) => /* @__PURE__ */ jsx18(Indicator, { color: "green", children: /* @__PURE__ */ jsx18(Avatar, { src: agent.picture, children: agent.name }) }, agent.id)) }),
1290
+ /* @__PURE__ */ jsxs10(Stack, { gap: 0, class: "mt-12", children: [
1291
+ /* @__PURE__ */ jsx18(Title, { color: "dimmed", lineClamp: 2, children: userName ? `Hey ${userName} \u{1F44B}` : `Hey \u{1F44B}` }),
1292
+ /* @__PURE__ */ jsx18(Title, { children: "How can we help?" })
1293
+ ] }),
1294
+ /* @__PURE__ */ jsx18(Card, { class: "mb-8", onClick: () => navigate("conversation"), children: /* @__PURE__ */ jsxs10(Group, { align: "center", justify: "between", children: [
1295
+ /* @__PURE__ */ jsxs10(Stack, { gap: 0, children: [
1296
+ /* @__PURE__ */ jsx18(Text, { class: "font-semibold", children: "Send us a message" }),
1297
+ /* @__PURE__ */ jsx18(Text, { color: "dimmed", children: "Usual reply time is a few minutes" })
1298
+ ] }),
1299
+ /* @__PURE__ */ jsx18(SendHorizonal, {})
1300
+ ] }) }),
1301
+ conversations && conversations.length > 0 && /* @__PURE__ */ jsxs10(Stack, { gap: 2, children: [
1302
+ /* @__PURE__ */ jsx18(Text, { color: "dimmed", class: "text-xs font-medium uppercase tracking-wide px-1", children: "Previous conversations" }),
1303
+ /* @__PURE__ */ jsx18(Stack, { gap: 1, children: conversations.map((conv) => {
1304
+ const agent = conv.agent;
1305
+ return /* @__PURE__ */ jsx18(
1306
+ Card,
1307
+ {
1308
+ onClick: () => navigate("conversation", { conversationId: conv.id }),
1309
+ children: /* @__PURE__ */ jsxs10(Group, { align: "center", justify: "between", children: [
1310
+ /* @__PURE__ */ jsxs10(Group, { align: "center", gap: 2, children: [
1311
+ /* @__PURE__ */ jsx18(Avatar, { size: "sm", src: agent?.picture, children: agent?.name }),
1312
+ /* @__PURE__ */ jsxs10(Stack, { gap: 0, children: [
1313
+ /* @__PURE__ */ jsx18(Text, { class: "font-medium text-xs", children: agent?.name ?? "Support" }),
1314
+ /* @__PURE__ */ jsx18(Text, { color: "dimmed", class: "text-xs truncate max-w-[180px]", children: conv.lastMessage?.text ?? "No messages yet" })
1315
+ ] })
1316
+ ] }),
1317
+ /* @__PURE__ */ jsx18(
1318
+ Indicator,
1319
+ {
1320
+ color: "red",
1321
+ label: conv.unread_contact_count,
1322
+ visible: conv.unread_contact_count > 0,
1323
+ position: "top-right",
1324
+ offset: { x: -36, y: 15 },
1325
+ children: /* @__PURE__ */ jsx18(ChevronRight, { size: 14, class: "text-neutral-400 shrink-0" })
1326
+ }
1327
+ )
1328
+ ] })
1329
+ },
1330
+ conv.id
1331
+ );
1332
+ }) })
1333
+ ] })
1334
+ ] });
1335
+ };
1336
+
1337
+ // src/widget/Widget.tsx
1338
+ init_state();
1339
+ import { Fragment as Fragment3, jsx as jsx19, jsxs as jsxs11 } from "preact/jsx-runtime";
1340
+ var renderPage = () => {
1341
+ const route = currentRoute.value;
1342
+ switch (route.page) {
1343
+ case "home":
1344
+ return /* @__PURE__ */ jsx19(Home, {});
1345
+ case "conversation":
1346
+ return /* @__PURE__ */ jsx19(Conversation, { conversationId: route.params?.conversationId });
1347
+ }
1348
+ };
1349
+ var Internal = ({ hideButton }) => {
1350
+ const sessionQuery = useQuery({
1351
+ queryFn: () => ApiService.getSession(),
1352
+ queryKey: ["session"],
1353
+ enabled: !!sessionToken.value
1354
+ });
1355
+ useOnNewMessage(() => {
1356
+ sessionQuery.refetch();
1357
+ });
1358
+ useEffect10(() => {
1359
+ if (sessionQuery.loading) {
1360
+ return;
1361
+ }
1362
+ sessionQuery.refetch();
1363
+ }, [refetchSessionCounter.value]);
1364
+ const countNotifications = sessionQuery.data?.count_notifications ?? 0;
1365
+ return /* @__PURE__ */ jsxs11("div", { children: [
1366
+ /* @__PURE__ */ jsx19(Layout, { isOpen: isOpen.value, children: renderPage() }),
1367
+ !hideButton && /* @__PURE__ */ jsx19(
1368
+ "button",
1369
+ {
1370
+ onClick: () => isOpen.value = !isOpen.value,
1371
+ class: "fixed bottom-4 right-4 w-14 h-14 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg flex items-center justify-center text-2xl transition-colors cursor-pointer",
1372
+ "aria-label": isOpen.value ? "Close support chat" : "Open support chat",
1373
+ children: /* @__PURE__ */ jsx19(
1374
+ Indicator,
1375
+ {
1376
+ label: countNotifications,
1377
+ visible: countNotifications > 0,
1378
+ position: "top-right",
1379
+ size: "md",
1380
+ color: "red",
1381
+ children: isOpen.value ? /* @__PURE__ */ jsx19(X, {}) : /* @__PURE__ */ jsx19(MessageCircle, {})
1382
+ }
1383
+ )
1384
+ }
1385
+ )
1386
+ ] });
1387
+ };
1388
+ var Widget = (props) => {
1389
+ if (!isSetup.value) {
1390
+ return /* @__PURE__ */ jsx19(Fragment3, {});
1391
+ }
1392
+ return /* @__PURE__ */ jsx19(Internal, { ...props });
1393
+ };
1394
+
1395
+ // src/widget/compiled-css.ts
1396
+ var compiled_css_default = '/*! tailwindcss v4.2.1 | MIT License | https://tailwindcss.com */\n@layer properties;\n@layer theme, base, components, utilities;\n@layer theme {\n :root, :host {\n --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",\n "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";\n --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",\n "Courier New", monospace;\n --color-red-500: oklch(63.7% 0.237 25.331);\n --color-amber-50: oklch(98.7% 0.022 95.277);\n --color-amber-200: oklch(92.4% 0.12 95.746);\n --color-amber-700: oklch(55.5% 0.163 48.998);\n --color-green-500: oklch(72.3% 0.219 149.579);\n --color-blue-100: oklch(93.2% 0.032 255.585);\n --color-blue-200: oklch(88.2% 0.059 254.128);\n --color-blue-400: oklch(70.7% 0.165 254.624);\n --color-blue-500: oklch(62.3% 0.214 259.815);\n --color-blue-600: oklch(54.6% 0.245 262.881);\n --color-blue-700: oklch(48.8% 0.243 264.376);\n --color-gray-200: oklch(92.8% 0.006 264.531);\n --color-neutral-50: oklch(98.5% 0 0);\n --color-neutral-100: oklch(97% 0 0);\n --color-neutral-200: oklch(92.2% 0 0);\n --color-neutral-300: oklch(87% 0 0);\n --color-neutral-400: oklch(70.8% 0 0);\n --color-neutral-500: oklch(55.6% 0 0);\n --color-neutral-600: oklch(43.9% 0 0);\n --color-neutral-800: oklch(26.9% 0 0);\n --color-neutral-900: oklch(20.5% 0 0);\n --color-white: #fff;\n --spacing: 0.25rem;\n --text-xs: 0.75rem;\n --text-xs--line-height: calc(1 / 0.75);\n --text-sm: 0.875rem;\n --text-sm--line-height: calc(1.25 / 0.875);\n --text-base: 1rem;\n --text-base--line-height: calc(1.5 / 1);\n --text-lg: 1.125rem;\n --text-lg--line-height: calc(1.75 / 1.125);\n --text-xl: 1.25rem;\n --text-xl--line-height: calc(1.75 / 1.25);\n --text-2xl: 1.5rem;\n --text-2xl--line-height: calc(2 / 1.5);\n --font-weight-normal: 400;\n --font-weight-medium: 500;\n --font-weight-semibold: 600;\n --tracking-wide: 0.025em;\n --leading-snug: 1.375;\n --radius-sm: 0.25rem;\n --radius-lg: 0.5rem;\n --radius-xl: 0.75rem;\n --radius-2xl: 1rem;\n --animate-spin: spin 1s linear infinite;\n --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;\n --animate-bounce: bounce 1s infinite;\n --default-transition-duration: 150ms;\n --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n --default-font-family: var(--font-sans);\n --default-mono-font-family: var(--font-mono);\n }\n}\n@layer base {\n *, ::after, ::before, ::backdrop, ::file-selector-button {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n border: 0 solid;\n }\n html, :host {\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n tab-size: 4;\n font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");\n font-feature-settings: var(--default-font-feature-settings, normal);\n font-variation-settings: var(--default-font-variation-settings, normal);\n -webkit-tap-highlight-color: transparent;\n }\n hr {\n height: 0;\n color: inherit;\n border-top-width: 1px;\n }\n abbr:where([title]) {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n }\n h1, h2, h3, h4, h5, h6 {\n font-size: inherit;\n font-weight: inherit;\n }\n a {\n color: inherit;\n -webkit-text-decoration: inherit;\n text-decoration: inherit;\n }\n b, strong {\n font-weight: bolder;\n }\n code, kbd, samp, pre {\n font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);\n font-feature-settings: var(--default-mono-font-feature-settings, normal);\n font-variation-settings: var(--default-mono-font-variation-settings, normal);\n font-size: 1em;\n }\n small {\n font-size: 80%;\n }\n sub, sup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n }\n sub {\n bottom: -0.25em;\n }\n sup {\n top: -0.5em;\n }\n table {\n text-indent: 0;\n border-color: inherit;\n border-collapse: collapse;\n }\n :-moz-focusring {\n outline: auto;\n }\n progress {\n vertical-align: baseline;\n }\n summary {\n display: list-item;\n }\n ol, ul, menu {\n list-style: none;\n }\n img, svg, video, canvas, audio, iframe, embed, object {\n display: block;\n vertical-align: middle;\n }\n img, video {\n max-width: 100%;\n height: auto;\n }\n button, input, select, optgroup, textarea, ::file-selector-button {\n font: inherit;\n font-feature-settings: inherit;\n font-variation-settings: inherit;\n letter-spacing: inherit;\n color: inherit;\n border-radius: 0;\n background-color: transparent;\n opacity: 1;\n }\n :where(select:is([multiple], [size])) optgroup {\n font-weight: bolder;\n }\n :where(select:is([multiple], [size])) optgroup option {\n padding-inline-start: 20px;\n }\n ::file-selector-button {\n margin-inline-end: 4px;\n }\n ::placeholder {\n opacity: 1;\n }\n @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {\n ::placeholder {\n color: currentcolor;\n @supports (color: color-mix(in lab, red, red)) {\n color: color-mix(in oklab, currentcolor 50%, transparent);\n }\n }\n }\n textarea {\n resize: vertical;\n }\n ::-webkit-search-decoration {\n -webkit-appearance: none;\n }\n ::-webkit-date-and-time-value {\n min-height: 1lh;\n text-align: inherit;\n }\n ::-webkit-datetime-edit {\n display: inline-flex;\n }\n ::-webkit-datetime-edit-fields-wrapper {\n padding: 0;\n }\n ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {\n padding-block: 0;\n }\n ::-webkit-calendar-picker-indicator {\n line-height: 1;\n }\n :-moz-ui-invalid {\n box-shadow: none;\n }\n button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {\n appearance: button;\n }\n ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {\n height: auto;\n }\n [hidden]:where(:not([hidden="until-found"])) {\n display: none !important;\n }\n}\n@layer utilities {\n .visible {\n visibility: visible;\n }\n .absolute {\n position: absolute;\n }\n .fixed {\n position: fixed;\n }\n .relative {\n position: relative;\n }\n .start {\n inset-inline-start: var(--spacing);\n }\n .end {\n inset-inline-end: var(--spacing);\n }\n .top-0 {\n top: calc(var(--spacing) * 0);\n }\n .right-0 {\n right: calc(var(--spacing) * 0);\n }\n .right-2\\.5 {\n right: calc(var(--spacing) * 2.5);\n }\n .right-4 {\n right: calc(var(--spacing) * 4);\n }\n .bottom-0 {\n bottom: calc(var(--spacing) * 0);\n }\n .bottom-1\\.5 {\n bottom: calc(var(--spacing) * 1.5);\n }\n .bottom-4 {\n bottom: calc(var(--spacing) * 4);\n }\n .bottom-20 {\n bottom: calc(var(--spacing) * 20);\n }\n .container {\n width: 100%;\n @media (width >= 40rem) {\n max-width: 40rem;\n }\n @media (width >= 48rem) {\n max-width: 48rem;\n }\n @media (width >= 64rem) {\n max-width: 64rem;\n }\n @media (width >= 80rem) {\n max-width: 80rem;\n }\n @media (width >= 96rem) {\n max-width: 96rem;\n }\n }\n .my-3 {\n margin-block: calc(var(--spacing) * 3);\n }\n .mt-12 {\n margin-top: calc(var(--spacing) * 12);\n }\n .mb-0\\.5 {\n margin-bottom: calc(var(--spacing) * 0.5);\n }\n .mb-8 {\n margin-bottom: calc(var(--spacing) * 8);\n }\n .flex {\n display: flex;\n }\n .hidden {\n display: none;\n }\n .inline-flex {\n display: inline-flex;\n }\n .h-1\\.5 {\n height: calc(var(--spacing) * 1.5);\n }\n .h-2\\.5 {\n height: calc(var(--spacing) * 2.5);\n }\n .h-3\\.5 {\n height: calc(var(--spacing) * 3.5);\n }\n .h-4 {\n height: calc(var(--spacing) * 4);\n }\n .h-5 {\n height: calc(var(--spacing) * 5);\n }\n .h-6 {\n height: calc(var(--spacing) * 6);\n }\n .h-7 {\n height: calc(var(--spacing) * 7);\n }\n .h-8 {\n height: calc(var(--spacing) * 8);\n }\n .h-9 {\n height: calc(var(--spacing) * 9);\n }\n .h-12 {\n height: calc(var(--spacing) * 12);\n }\n .h-14 {\n height: calc(var(--spacing) * 14);\n }\n .h-112 {\n height: calc(var(--spacing) * 112);\n }\n .h-full {\n height: 100%;\n }\n .h-px {\n height: 1px;\n }\n .min-h-\\[175px\\] {\n min-height: 175px;\n }\n .w-1\\.5 {\n width: calc(var(--spacing) * 1.5);\n }\n .w-2\\.5 {\n width: calc(var(--spacing) * 2.5);\n }\n .w-3\\.5 {\n width: calc(var(--spacing) * 3.5);\n }\n .w-4 {\n width: calc(var(--spacing) * 4);\n }\n .w-7 {\n width: calc(var(--spacing) * 7);\n }\n .w-8 {\n width: calc(var(--spacing) * 8);\n }\n .w-9 {\n width: calc(var(--spacing) * 9);\n }\n .w-12 {\n width: calc(var(--spacing) * 12);\n }\n .w-14 {\n width: calc(var(--spacing) * 14);\n }\n .w-80 {\n width: calc(var(--spacing) * 80);\n }\n .w-full {\n width: 100%;\n }\n .max-w-\\[72\\%\\] {\n max-width: 72%;\n }\n .max-w-\\[180px\\] {\n max-width: 180px;\n }\n .min-w-4 {\n min-width: calc(var(--spacing) * 4);\n }\n .min-w-5 {\n min-width: calc(var(--spacing) * 5);\n }\n .min-w-6 {\n min-width: calc(var(--spacing) * 6);\n }\n .min-w-18 {\n min-width: calc(var(--spacing) * 18);\n }\n .flex-1 {\n flex: 1;\n }\n .shrink-0 {\n flex-shrink: 0;\n }\n .translate-x-1\\/2 {\n --tw-translate-x: calc(1 / 2 * 100%);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .-translate-y-1\\/2 {\n --tw-translate-y: calc(calc(1 / 2 * 100%) * -1);\n translate: var(--tw-translate-x) var(--tw-translate-y);\n }\n .transform {\n transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);\n }\n .animate-bounce {\n animation: var(--animate-bounce);\n }\n .animate-pulse {\n animation: var(--animate-pulse);\n }\n .animate-spin {\n animation: var(--animate-spin);\n }\n .cursor-pointer {\n cursor: pointer;\n }\n .resize {\n resize: both;\n }\n .resize-none {\n resize: none;\n }\n .flex-col {\n flex-direction: column;\n }\n .flex-col-reverse {\n flex-direction: column-reverse;\n }\n .flex-row {\n flex-direction: row;\n }\n .flex-row-reverse {\n flex-direction: row-reverse;\n }\n .flex-wrap {\n flex-wrap: wrap;\n }\n .items-center {\n align-items: center;\n }\n .items-end {\n align-items: flex-end;\n }\n .items-start {\n align-items: flex-start;\n }\n .items-stretch {\n align-items: stretch;\n }\n .justify-between {\n justify-content: space-between;\n }\n .justify-center {\n justify-content: center;\n }\n .justify-end {\n justify-content: flex-end;\n }\n .justify-start {\n justify-content: flex-start;\n }\n .gap-0\\.5 {\n gap: calc(var(--spacing) * 0.5);\n }\n .gap-1 {\n gap: calc(var(--spacing) * 1);\n }\n .gap-2 {\n gap: calc(var(--spacing) * 2);\n }\n .gap-4 {\n gap: calc(var(--spacing) * 4);\n }\n .gap-6 {\n gap: calc(var(--spacing) * 6);\n }\n .self-end {\n align-self: flex-end;\n }\n .truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .overflow-hidden {\n overflow: hidden;\n }\n .overflow-y-auto {\n overflow-y: auto;\n }\n .rounded-2xl {\n border-radius: var(--radius-2xl);\n }\n .rounded-full {\n border-radius: calc(infinity * 1px);\n }\n .rounded-lg {\n border-radius: var(--radius-lg);\n }\n .rounded-xl {\n border-radius: var(--radius-xl);\n }\n .rounded-br-sm {\n border-bottom-right-radius: var(--radius-sm);\n }\n .rounded-bl-sm {\n border-bottom-left-radius: var(--radius-sm);\n }\n .border {\n border-style: var(--tw-border-style);\n border-width: 1px;\n }\n .border-2 {\n border-style: var(--tw-border-style);\n border-width: 2px;\n }\n .border-t {\n border-top-style: var(--tw-border-style);\n border-top-width: 1px;\n }\n .border-b {\n border-bottom-style: var(--tw-border-style);\n border-bottom-width: 1px;\n }\n .border-amber-200 {\n border-color: var(--color-amber-200);\n }\n .border-blue-400 {\n border-color: var(--color-blue-400);\n }\n .border-gray-200 {\n border-color: var(--color-gray-200);\n }\n .border-neutral-100 {\n border-color: var(--color-neutral-100);\n }\n .border-neutral-200 {\n border-color: var(--color-neutral-200);\n }\n .border-white {\n border-color: var(--color-white);\n }\n .bg-amber-50 {\n background-color: var(--color-amber-50);\n }\n .bg-blue-100 {\n background-color: var(--color-blue-100);\n }\n .bg-blue-500 {\n background-color: var(--color-blue-500);\n }\n .bg-blue-600 {\n background-color: var(--color-blue-600);\n }\n .bg-green-500 {\n background-color: var(--color-green-500);\n }\n .bg-neutral-100 {\n background-color: var(--color-neutral-100);\n }\n .bg-neutral-200 {\n background-color: var(--color-neutral-200);\n }\n .bg-neutral-400 {\n background-color: var(--color-neutral-400);\n }\n .bg-red-500 {\n background-color: var(--color-red-500);\n }\n .bg-transparent {\n background-color: transparent;\n }\n .bg-white {\n background-color: var(--color-white);\n }\n .object-cover {\n object-fit: cover;\n }\n .p-2 {\n padding: calc(var(--spacing) * 2);\n }\n .p-4 {\n padding: calc(var(--spacing) * 4);\n }\n .px-1 {\n padding-inline: calc(var(--spacing) * 1);\n }\n .px-3 {\n padding-inline: calc(var(--spacing) * 3);\n }\n .px-4 {\n padding-inline: calc(var(--spacing) * 4);\n }\n .py-2 {\n padding-block: calc(var(--spacing) * 2);\n }\n .py-2\\.5 {\n padding-block: calc(var(--spacing) * 2.5);\n }\n .py-3 {\n padding-block: calc(var(--spacing) * 3);\n }\n .py-4 {\n padding-block: calc(var(--spacing) * 4);\n }\n .pt-2 {\n padding-top: calc(var(--spacing) * 2);\n }\n .pb-5 {\n padding-bottom: calc(var(--spacing) * 5);\n }\n .text-center {\n text-align: center;\n }\n .text-2xl {\n font-size: var(--text-2xl);\n line-height: var(--tw-leading, var(--text-2xl--line-height));\n }\n .text-base {\n font-size: var(--text-base);\n line-height: var(--tw-leading, var(--text-base--line-height));\n }\n .text-lg {\n font-size: var(--text-lg);\n line-height: var(--tw-leading, var(--text-lg--line-height));\n }\n .text-sm {\n font-size: var(--text-sm);\n line-height: var(--tw-leading, var(--text-sm--line-height));\n }\n .text-xl {\n font-size: var(--text-xl);\n line-height: var(--tw-leading, var(--text-xl--line-height));\n }\n .text-xs {\n font-size: var(--text-xs);\n line-height: var(--tw-leading, var(--text-xs--line-height));\n }\n .text-\\[10px\\] {\n font-size: 10px;\n }\n .leading-5 {\n --tw-leading: calc(var(--spacing) * 5);\n line-height: calc(var(--spacing) * 5);\n }\n .leading-none {\n --tw-leading: 1;\n line-height: 1;\n }\n .leading-snug {\n --tw-leading: var(--leading-snug);\n line-height: var(--leading-snug);\n }\n .font-medium {\n --tw-font-weight: var(--font-weight-medium);\n font-weight: var(--font-weight-medium);\n }\n .font-normal {\n --tw-font-weight: var(--font-weight-normal);\n font-weight: var(--font-weight-normal);\n }\n .font-semibold {\n --tw-font-weight: var(--font-weight-semibold);\n font-weight: var(--font-weight-semibold);\n }\n .tracking-wide {\n --tw-tracking: var(--tracking-wide);\n letter-spacing: var(--tracking-wide);\n }\n .whitespace-pre-wrap {\n white-space: pre-wrap;\n }\n .text-amber-700 {\n color: var(--color-amber-700);\n }\n .text-blue-200 {\n color: var(--color-blue-200);\n }\n .text-blue-700 {\n color: var(--color-blue-700);\n }\n .text-green-500 {\n color: var(--color-green-500);\n }\n .text-inherit {\n color: inherit;\n }\n .text-neutral-400 {\n color: var(--color-neutral-400);\n }\n .text-neutral-500 {\n color: var(--color-neutral-500);\n }\n .text-neutral-600 {\n color: var(--color-neutral-600);\n }\n .text-neutral-800 {\n color: var(--color-neutral-800);\n }\n .text-neutral-900 {\n color: var(--color-neutral-900);\n }\n .text-white {\n color: var(--color-white);\n }\n .uppercase {\n text-transform: uppercase;\n }\n .opacity-0 {\n opacity: 0%;\n }\n .shadow {\n --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .shadow-2xl {\n --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .shadow-lg {\n --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);\n }\n .outline {\n outline-style: var(--tw-outline-style);\n outline-width: 1px;\n }\n .transition-colors {\n transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;\n transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));\n transition-duration: var(--tw-duration, var(--default-transition-duration));\n }\n .outline-none {\n --tw-outline-style: none;\n outline-style: none;\n }\n .placeholder\\:text-neutral-400 {\n &::placeholder {\n color: var(--color-neutral-400);\n }\n }\n .hover\\:bg-blue-700 {\n &:hover {\n @media (hover: hover) {\n background-color: var(--color-blue-700);\n }\n }\n }\n .hover\\:bg-neutral-50 {\n &:hover {\n @media (hover: hover) {\n background-color: var(--color-neutral-50);\n }\n }\n }\n .hover\\:bg-neutral-200 {\n &:hover {\n @media (hover: hover) {\n background-color: var(--color-neutral-200);\n }\n }\n }\n .hover\\:text-neutral-800 {\n &:hover {\n @media (hover: hover) {\n color: var(--color-neutral-800);\n }\n }\n }\n .disabled\\:cursor-not-allowed {\n &:disabled {\n cursor: not-allowed;\n }\n }\n .disabled\\:opacity-50 {\n &:disabled {\n opacity: 50%;\n }\n }\n}\n*, ::before, ::after {\n --tw-border-style: solid;\n}\n.gradient-header {\n background: linear-gradient(to bottom, var(--color-neutral-200) 0%, var(--color-white) 60%);\n}\n.lucide {\n height: 1em;\n width: 1em;\n display: inline-block;\n}\n::-webkit-scrollbar {\n width: 4px;\n}\n::-webkit-scrollbar-track {\n background: transparent;\n}\n::-webkit-scrollbar-thumb {\n background: var(--color-neutral-300);\n border-radius: 9999px;\n}\n@property --tw-translate-x {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-y {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-translate-z {\n syntax: "*";\n inherits: false;\n initial-value: 0;\n}\n@property --tw-rotate-x {\n syntax: "*";\n inherits: false;\n}\n@property --tw-rotate-y {\n syntax: "*";\n inherits: false;\n}\n@property --tw-rotate-z {\n syntax: "*";\n inherits: false;\n}\n@property --tw-skew-x {\n syntax: "*";\n inherits: false;\n}\n@property --tw-skew-y {\n syntax: "*";\n inherits: false;\n}\n@property --tw-border-style {\n syntax: "*";\n inherits: false;\n initial-value: solid;\n}\n@property --tw-leading {\n syntax: "*";\n inherits: false;\n}\n@property --tw-font-weight {\n syntax: "*";\n inherits: false;\n}\n@property --tw-tracking {\n syntax: "*";\n inherits: false;\n}\n@property --tw-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-shadow-alpha {\n syntax: "<percentage>";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-inset-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-shadow-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-inset-shadow-alpha {\n syntax: "<percentage>";\n inherits: false;\n initial-value: 100%;\n}\n@property --tw-ring-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ring-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-inset-ring-color {\n syntax: "*";\n inherits: false;\n}\n@property --tw-inset-ring-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-ring-inset {\n syntax: "*";\n inherits: false;\n}\n@property --tw-ring-offset-width {\n syntax: "<length>";\n inherits: false;\n initial-value: 0px;\n}\n@property --tw-ring-offset-color {\n syntax: "*";\n inherits: false;\n initial-value: #fff;\n}\n@property --tw-ring-offset-shadow {\n syntax: "*";\n inherits: false;\n initial-value: 0 0 #0000;\n}\n@property --tw-outline-style {\n syntax: "*";\n inherits: false;\n initial-value: solid;\n}\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n@keyframes pulse {\n 50% {\n opacity: 0.5;\n }\n}\n@keyframes bounce {\n 0%, 100% {\n transform: translateY(-25%);\n animation-timing-function: cubic-bezier(0.8, 0, 1, 1);\n }\n 50% {\n transform: none;\n animation-timing-function: cubic-bezier(0, 0, 0.2, 1);\n }\n}\n@layer properties {\n @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {\n *, ::before, ::after, ::backdrop {\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-translate-z: 0;\n --tw-rotate-x: initial;\n --tw-rotate-y: initial;\n --tw-rotate-z: initial;\n --tw-skew-x: initial;\n --tw-skew-y: initial;\n --tw-border-style: solid;\n --tw-leading: initial;\n --tw-font-weight: initial;\n --tw-tracking: initial;\n --tw-shadow: 0 0 #0000;\n --tw-shadow-color: initial;\n --tw-shadow-alpha: 100%;\n --tw-inset-shadow: 0 0 #0000;\n --tw-inset-shadow-color: initial;\n --tw-inset-shadow-alpha: 100%;\n --tw-ring-color: initial;\n --tw-ring-shadow: 0 0 #0000;\n --tw-inset-ring-color: initial;\n --tw-inset-ring-shadow: 0 0 #0000;\n --tw-ring-inset: initial;\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-outline-style: solid;\n }\n }\n}\n';
1397
+
1398
+ // src/widget/mount.tsx
1399
+ init_service();
1400
+ init_state();
1401
+ import { jsx as jsx20 } from "preact/jsx-runtime";
1402
+ function mountWidget(options) {
1403
+ const { appId: id, hideButton = false } = options;
1404
+ appId.value = id;
1405
+ VisitorService.init().then(() => SocketService.connect());
1406
+ const host = document.createElement("div");
1407
+ host.id = "spacinbox-widget";
1408
+ document.body.appendChild(host);
1409
+ const shadow = host.attachShadow({ mode: "open" });
1410
+ const sheet = new CSSStyleSheet();
1411
+ sheet.replaceSync(compiled_css_default);
1412
+ shadow.adoptedStyleSheets = [sheet];
1413
+ const mountPoint = document.createElement("div");
1414
+ shadow.appendChild(mountPoint);
1415
+ render(/* @__PURE__ */ jsx20(Widget, { hideButton }), mountPoint);
1416
+ return {
1417
+ open() {
1418
+ isOpen.value = true;
1419
+ },
1420
+ close() {
1421
+ isOpen.value = false;
1422
+ },
1423
+ identify(user) {
1424
+ currentUser.value = user;
1425
+ VisitorService.identify(user);
1426
+ },
1427
+ setMetadata(data) {
1428
+ metadata.value = { ...metadata.value, ...data };
1429
+ },
1430
+ shutdown() {
1431
+ render(null, mountPoint);
1432
+ host.remove();
1433
+ isOpen.value = false;
1434
+ currentUser.value = null;
1435
+ metadata.value = {};
1436
+ visitorId.value = null;
1437
+ sessionToken.value = null;
1438
+ resetRouter();
1439
+ VisitorService.clear();
1440
+ SocketService.disconnect();
1441
+ }
1442
+ };
1443
+ }
1444
+
1445
+ // src/index.ts
1446
+ var controller = null;
1447
+ var Spacinbox = {
1448
+ boot(options) {
1449
+ if (typeof document === "undefined") {
1450
+ return;
1451
+ }
1452
+ if (controller) {
1453
+ return;
1454
+ }
1455
+ controller = mountWidget(options);
1456
+ },
1457
+ identify(user) {
1458
+ controller?.identify(user);
1459
+ },
1460
+ setMetadata(data) {
1461
+ controller?.setMetadata(data);
1462
+ },
1463
+ open() {
1464
+ controller?.open();
1465
+ },
1466
+ close() {
1467
+ controller?.close();
1468
+ },
1469
+ shutdown() {
1470
+ controller?.shutdown();
1471
+ controller = null;
1472
+ }
1473
+ };
1474
+ var index_default = Spacinbox;
1475
+ export {
1476
+ index_default as default
1477
+ };
1478
+ //# sourceMappingURL=index.js.map