@pocketping/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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1045 @@
1
+ // src/index.ts
2
+ import { render, h as h2 } from "preact";
3
+
4
+ // src/components/ChatWidget.tsx
5
+ import { Fragment } from "preact";
6
+ import { useState, useEffect, useRef, useCallback } from "preact/hooks";
7
+
8
+ // src/components/styles.ts
9
+ function styles(primaryColor, theme) {
10
+ const isDark = theme === "dark";
11
+ const colors = {
12
+ bg: isDark ? "#1f2937" : "#ffffff",
13
+ bgSecondary: isDark ? "#374151" : "#f3f4f6",
14
+ text: isDark ? "#f9fafb" : "#111827",
15
+ textSecondary: isDark ? "#9ca3af" : "#6b7280",
16
+ border: isDark ? "#4b5563" : "#e5e7eb",
17
+ messageBg: isDark ? "#374151" : "#f3f4f6"
18
+ };
19
+ return `
20
+ #pocketping-container {
21
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
22
+ font-size: 14px;
23
+ line-height: 1.5;
24
+ color: ${colors.text};
25
+ }
26
+
27
+ .pp-toggle {
28
+ position: fixed;
29
+ width: 56px;
30
+ height: 56px;
31
+ border-radius: 50%;
32
+ background: ${primaryColor};
33
+ color: white;
34
+ border: none;
35
+ cursor: pointer;
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
40
+ transition: transform 0.2s, box-shadow 0.2s;
41
+ z-index: 9999;
42
+ }
43
+
44
+ .pp-toggle:hover {
45
+ transform: scale(1.05);
46
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
47
+ }
48
+
49
+ .pp-toggle svg {
50
+ width: 24px;
51
+ height: 24px;
52
+ }
53
+
54
+ .pp-toggle.pp-bottom-right {
55
+ bottom: 20px;
56
+ right: 20px;
57
+ }
58
+
59
+ .pp-toggle.pp-bottom-left {
60
+ bottom: 20px;
61
+ left: 20px;
62
+ }
63
+
64
+ .pp-online-dot {
65
+ position: absolute;
66
+ top: 4px;
67
+ right: 4px;
68
+ width: 12px;
69
+ height: 12px;
70
+ background: #22c55e;
71
+ border-radius: 50%;
72
+ border: 2px solid white;
73
+ }
74
+
75
+ .pp-window {
76
+ position: fixed;
77
+ width: 380px;
78
+ height: 520px;
79
+ max-height: calc(100vh - 100px);
80
+ background: ${colors.bg};
81
+ border-radius: 16px;
82
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
83
+ display: flex;
84
+ flex-direction: column;
85
+ overflow: hidden;
86
+ z-index: 9998;
87
+ }
88
+
89
+ .pp-window.pp-bottom-right {
90
+ bottom: 88px;
91
+ right: 20px;
92
+ }
93
+
94
+ .pp-window.pp-bottom-left {
95
+ bottom: 88px;
96
+ left: 20px;
97
+ }
98
+
99
+ @media (max-width: 480px) {
100
+ .pp-window {
101
+ width: calc(100vw - 20px);
102
+ height: calc(100vh - 100px);
103
+ bottom: 80px;
104
+ right: 10px;
105
+ left: 10px;
106
+ border-radius: 12px;
107
+ }
108
+ }
109
+
110
+ .pp-header {
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: space-between;
114
+ padding: 16px;
115
+ background: ${primaryColor};
116
+ color: white;
117
+ }
118
+
119
+ .pp-header-info {
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 12px;
123
+ }
124
+
125
+ .pp-avatar {
126
+ width: 40px;
127
+ height: 40px;
128
+ border-radius: 50%;
129
+ object-fit: cover;
130
+ }
131
+
132
+ .pp-header-title {
133
+ font-weight: 600;
134
+ font-size: 16px;
135
+ }
136
+
137
+ .pp-header-status {
138
+ font-size: 12px;
139
+ opacity: 0.9;
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 4px;
143
+ }
144
+
145
+ .pp-status-dot {
146
+ width: 8px;
147
+ height: 8px;
148
+ border-radius: 50%;
149
+ background: rgba(255, 255, 255, 0.5);
150
+ }
151
+
152
+ .pp-status-dot.pp-online {
153
+ background: #22c55e;
154
+ }
155
+
156
+ .pp-close-btn {
157
+ background: transparent;
158
+ border: none;
159
+ color: white;
160
+ cursor: pointer;
161
+ padding: 4px;
162
+ border-radius: 4px;
163
+ opacity: 0.8;
164
+ transition: opacity 0.2s;
165
+ }
166
+
167
+ .pp-close-btn:hover {
168
+ opacity: 1;
169
+ }
170
+
171
+ .pp-close-btn svg {
172
+ width: 20px;
173
+ height: 20px;
174
+ }
175
+
176
+ .pp-messages {
177
+ flex: 1;
178
+ overflow-y: auto;
179
+ padding: 16px;
180
+ display: flex;
181
+ flex-direction: column;
182
+ gap: 12px;
183
+ }
184
+
185
+ .pp-welcome {
186
+ text-align: center;
187
+ color: ${colors.textSecondary};
188
+ padding: 24px;
189
+ font-size: 13px;
190
+ }
191
+
192
+ .pp-message {
193
+ max-width: 80%;
194
+ padding: 10px 14px;
195
+ border-radius: 16px;
196
+ word-wrap: break-word;
197
+ }
198
+
199
+ .pp-message-visitor {
200
+ align-self: flex-end;
201
+ background: ${primaryColor};
202
+ color: white;
203
+ border-bottom-right-radius: 4px;
204
+ }
205
+
206
+ .pp-message-operator,
207
+ .pp-message-ai {
208
+ align-self: flex-start;
209
+ background: ${colors.messageBg};
210
+ color: ${colors.text};
211
+ border-bottom-left-radius: 4px;
212
+ }
213
+
214
+ .pp-message-content {
215
+ margin-bottom: 4px;
216
+ }
217
+
218
+ .pp-message-time {
219
+ font-size: 11px;
220
+ opacity: 0.7;
221
+ display: flex;
222
+ align-items: center;
223
+ gap: 4px;
224
+ }
225
+
226
+ .pp-ai-badge {
227
+ background: rgba(0, 0, 0, 0.1);
228
+ padding: 1px 4px;
229
+ border-radius: 4px;
230
+ font-size: 10px;
231
+ font-weight: 600;
232
+ }
233
+
234
+ .pp-status {
235
+ display: inline-flex;
236
+ align-items: center;
237
+ margin-left: 4px;
238
+ }
239
+
240
+ .pp-status svg {
241
+ width: 14px;
242
+ height: 14px;
243
+ }
244
+
245
+ .pp-check,
246
+ .pp-check-double {
247
+ stroke: rgba(255, 255, 255, 0.7);
248
+ }
249
+
250
+ .pp-check-read {
251
+ stroke: #34b7f1;
252
+ }
253
+
254
+ .pp-status-sending .pp-check {
255
+ opacity: 0.5;
256
+ }
257
+
258
+ .pp-typing {
259
+ display: flex;
260
+ gap: 4px;
261
+ padding: 14px 18px;
262
+ }
263
+
264
+ .pp-typing span {
265
+ width: 8px;
266
+ height: 8px;
267
+ background: ${colors.textSecondary};
268
+ border-radius: 50%;
269
+ animation: pp-bounce 1.4s infinite ease-in-out both;
270
+ }
271
+
272
+ .pp-typing span:nth-child(1) { animation-delay: -0.32s; }
273
+ .pp-typing span:nth-child(2) { animation-delay: -0.16s; }
274
+
275
+ @keyframes pp-bounce {
276
+ 0%, 80%, 100% { transform: scale(0); }
277
+ 40% { transform: scale(1); }
278
+ }
279
+
280
+ .pp-input-form {
281
+ display: flex;
282
+ padding: 12px;
283
+ gap: 8px;
284
+ border-top: 1px solid ${colors.border};
285
+ }
286
+
287
+ .pp-input {
288
+ flex: 1;
289
+ padding: 10px 14px;
290
+ border: 1px solid ${colors.border};
291
+ border-radius: 20px;
292
+ background: ${colors.bg};
293
+ color: ${colors.text};
294
+ font-size: 14px;
295
+ outline: none;
296
+ transition: border-color 0.2s;
297
+ }
298
+
299
+ .pp-input:focus {
300
+ border-color: ${primaryColor};
301
+ }
302
+
303
+ .pp-input::placeholder {
304
+ color: ${colors.textSecondary};
305
+ }
306
+
307
+ .pp-send-btn {
308
+ width: 40px;
309
+ height: 40px;
310
+ border-radius: 50%;
311
+ background: ${primaryColor};
312
+ color: white;
313
+ border: none;
314
+ cursor: pointer;
315
+ display: flex;
316
+ align-items: center;
317
+ justify-content: center;
318
+ transition: opacity 0.2s;
319
+ }
320
+
321
+ .pp-send-btn:disabled {
322
+ opacity: 0.5;
323
+ cursor: not-allowed;
324
+ }
325
+
326
+ .pp-send-btn svg {
327
+ width: 18px;
328
+ height: 18px;
329
+ }
330
+
331
+ .pp-footer {
332
+ text-align: center;
333
+ padding: 8px;
334
+ font-size: 11px;
335
+ color: ${colors.textSecondary};
336
+ border-top: 1px solid ${colors.border};
337
+ }
338
+
339
+ .pp-footer a {
340
+ color: ${primaryColor};
341
+ text-decoration: none;
342
+ }
343
+
344
+ .pp-footer a:hover {
345
+ text-decoration: underline;
346
+ }
347
+ `;
348
+ }
349
+
350
+ // src/components/ChatWidget.tsx
351
+ import { Fragment as Fragment2, jsx, jsxs } from "preact/jsx-runtime";
352
+ function ChatWidget({ client: client2, config }) {
353
+ const [isOpen, setIsOpen] = useState(false);
354
+ const [messages, setMessages] = useState([]);
355
+ const [inputValue, setInputValue] = useState("");
356
+ const [isTyping, setIsTyping] = useState(false);
357
+ const [operatorOnline, setOperatorOnline] = useState(false);
358
+ const [isConnected, setIsConnected] = useState(false);
359
+ const messagesEndRef = useRef(null);
360
+ const inputRef = useRef(null);
361
+ useEffect(() => {
362
+ const unsubOpen = client2.on("openChange", setIsOpen);
363
+ const unsubMessage = client2.on("message", () => {
364
+ setMessages([...client2.getMessages()]);
365
+ });
366
+ const unsubTyping = client2.on("typing", (data) => {
367
+ setIsTyping(data.isTyping);
368
+ });
369
+ const unsubPresence = client2.on("presence", (data) => {
370
+ setOperatorOnline(data.online);
371
+ });
372
+ const unsubConnect = client2.on("connect", () => {
373
+ setIsConnected(true);
374
+ setMessages(client2.getMessages());
375
+ setOperatorOnline(client2.getSession()?.operatorOnline ?? false);
376
+ });
377
+ if (client2.isConnected()) {
378
+ setIsConnected(true);
379
+ setMessages(client2.getMessages());
380
+ setOperatorOnline(client2.getSession()?.operatorOnline ?? false);
381
+ }
382
+ return () => {
383
+ unsubOpen();
384
+ unsubMessage();
385
+ unsubTyping();
386
+ unsubPresence();
387
+ unsubConnect();
388
+ };
389
+ }, [client2]);
390
+ useEffect(() => {
391
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
392
+ }, [messages]);
393
+ useEffect(() => {
394
+ if (isOpen) {
395
+ inputRef.current?.focus();
396
+ }
397
+ }, [isOpen]);
398
+ const markMessagesAsRead = useCallback(() => {
399
+ if (!isOpen || !isConnected) return;
400
+ const unreadMessages = messages.filter(
401
+ (msg) => msg.sender !== "visitor" && msg.status !== "read"
402
+ );
403
+ if (unreadMessages.length > 0) {
404
+ const messageIds = unreadMessages.map((msg) => msg.id);
405
+ client2.sendReadStatus(messageIds, "read");
406
+ }
407
+ }, [isOpen, isConnected, messages, client2]);
408
+ useEffect(() => {
409
+ if (!isOpen || !isConnected) return;
410
+ const timer = setTimeout(() => {
411
+ markMessagesAsRead();
412
+ }, 1e3);
413
+ return () => clearTimeout(timer);
414
+ }, [isOpen, isConnected, messages, markMessagesAsRead]);
415
+ useEffect(() => {
416
+ const handleVisibilityChange = () => {
417
+ if (document.visibilityState === "visible" && isOpen) {
418
+ markMessagesAsRead();
419
+ }
420
+ };
421
+ document.addEventListener("visibilitychange", handleVisibilityChange);
422
+ return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
423
+ }, [isOpen, markMessagesAsRead]);
424
+ useEffect(() => {
425
+ const unsubRead = client2.on(
426
+ "read",
427
+ () => {
428
+ setMessages([...client2.getMessages()]);
429
+ }
430
+ );
431
+ return () => unsubRead();
432
+ }, [client2]);
433
+ const shouldShow = checkPageVisibility(config);
434
+ if (!shouldShow) return null;
435
+ const handleSubmit = async (e) => {
436
+ e.preventDefault();
437
+ if (!inputValue.trim()) return;
438
+ const content = inputValue;
439
+ setInputValue("");
440
+ try {
441
+ await client2.sendMessage(content);
442
+ } catch (err) {
443
+ console.error("[PocketPing] Failed to send message:", err);
444
+ }
445
+ };
446
+ const handleInputChange = (e) => {
447
+ const target = e.target;
448
+ setInputValue(target.value);
449
+ client2.sendTyping(true);
450
+ };
451
+ const position = config.position ?? "bottom-right";
452
+ const theme = getTheme(config.theme ?? "auto");
453
+ const primaryColor = config.primaryColor ?? "#6366f1";
454
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
455
+ /* @__PURE__ */ jsx("style", { children: styles(primaryColor, theme) }),
456
+ /* @__PURE__ */ jsxs(
457
+ "button",
458
+ {
459
+ class: `pp-toggle pp-${position}`,
460
+ onClick: () => client2.toggleOpen(),
461
+ "aria-label": isOpen ? "Close chat" : "Open chat",
462
+ children: [
463
+ isOpen ? /* @__PURE__ */ jsx(CloseIcon, {}) : /* @__PURE__ */ jsx(ChatIcon, {}),
464
+ !isOpen && operatorOnline && /* @__PURE__ */ jsx("span", { class: "pp-online-dot" })
465
+ ]
466
+ }
467
+ ),
468
+ isOpen && /* @__PURE__ */ jsxs("div", { class: `pp-window pp-${position} pp-theme-${theme}`, children: [
469
+ /* @__PURE__ */ jsxs("div", { class: "pp-header", children: [
470
+ /* @__PURE__ */ jsxs("div", { class: "pp-header-info", children: [
471
+ config.operatorAvatar && /* @__PURE__ */ jsx("img", { src: config.operatorAvatar, alt: "", class: "pp-avatar" }),
472
+ /* @__PURE__ */ jsxs("div", { children: [
473
+ /* @__PURE__ */ jsx("div", { class: "pp-header-title", children: config.operatorName ?? "Support" }),
474
+ /* @__PURE__ */ jsx("div", { class: "pp-header-status", children: operatorOnline ? /* @__PURE__ */ jsxs(Fragment2, { children: [
475
+ /* @__PURE__ */ jsx("span", { class: "pp-status-dot pp-online" }),
476
+ " Online"
477
+ ] }) : /* @__PURE__ */ jsxs(Fragment2, { children: [
478
+ /* @__PURE__ */ jsx("span", { class: "pp-status-dot" }),
479
+ " Away"
480
+ ] }) })
481
+ ] })
482
+ ] }),
483
+ /* @__PURE__ */ jsx(
484
+ "button",
485
+ {
486
+ class: "pp-close-btn",
487
+ onClick: () => client2.setOpen(false),
488
+ "aria-label": "Close chat",
489
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
490
+ }
491
+ )
492
+ ] }),
493
+ /* @__PURE__ */ jsxs("div", { class: "pp-messages", children: [
494
+ config.welcomeMessage && messages.length === 0 && /* @__PURE__ */ jsx("div", { class: "pp-welcome", children: config.welcomeMessage }),
495
+ messages.map((msg) => /* @__PURE__ */ jsxs(
496
+ "div",
497
+ {
498
+ class: `pp-message pp-message-${msg.sender}`,
499
+ children: [
500
+ /* @__PURE__ */ jsx("div", { class: "pp-message-content", children: msg.content }),
501
+ /* @__PURE__ */ jsxs("div", { class: "pp-message-time", children: [
502
+ formatTime(msg.timestamp),
503
+ msg.sender === "ai" && /* @__PURE__ */ jsx("span", { class: "pp-ai-badge", children: "AI" }),
504
+ msg.sender === "visitor" && /* @__PURE__ */ jsx("span", { class: `pp-status pp-status-${msg.status ?? "sent"}`, children: /* @__PURE__ */ jsx(StatusIcon, { status: msg.status }) })
505
+ ] })
506
+ ]
507
+ },
508
+ msg.id
509
+ )),
510
+ isTyping && /* @__PURE__ */ jsxs("div", { class: "pp-message pp-message-operator pp-typing", children: [
511
+ /* @__PURE__ */ jsx("span", {}),
512
+ /* @__PURE__ */ jsx("span", {}),
513
+ /* @__PURE__ */ jsx("span", {})
514
+ ] }),
515
+ /* @__PURE__ */ jsx("div", { ref: messagesEndRef })
516
+ ] }),
517
+ /* @__PURE__ */ jsxs("form", { class: "pp-input-form", onSubmit: handleSubmit, children: [
518
+ /* @__PURE__ */ jsx(
519
+ "input",
520
+ {
521
+ ref: inputRef,
522
+ type: "text",
523
+ class: "pp-input",
524
+ placeholder: config.placeholder ?? "Type a message...",
525
+ value: inputValue,
526
+ onInput: handleInputChange,
527
+ disabled: !isConnected
528
+ }
529
+ ),
530
+ /* @__PURE__ */ jsx(
531
+ "button",
532
+ {
533
+ type: "submit",
534
+ class: "pp-send-btn",
535
+ disabled: !inputValue.trim() || !isConnected,
536
+ "aria-label": "Send message",
537
+ children: /* @__PURE__ */ jsx(SendIcon, {})
538
+ }
539
+ )
540
+ ] }),
541
+ /* @__PURE__ */ jsxs("div", { class: "pp-footer", children: [
542
+ "Powered by ",
543
+ /* @__PURE__ */ jsx("a", { href: "https://github.com/pocketping/pocketping", target: "_blank", rel: "noopener", children: "PocketPing" })
544
+ ] })
545
+ ] })
546
+ ] });
547
+ }
548
+ function checkPageVisibility(config) {
549
+ const path = window.location.pathname;
550
+ if (config.hideOnPages?.some((pattern) => new RegExp(pattern).test(path))) {
551
+ return false;
552
+ }
553
+ if (config.showOnPages?.length) {
554
+ return config.showOnPages.some((pattern) => new RegExp(pattern).test(path));
555
+ }
556
+ return true;
557
+ }
558
+ function getTheme(theme) {
559
+ if (theme === "auto") {
560
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
561
+ }
562
+ return theme;
563
+ }
564
+ function formatTime(timestamp) {
565
+ const date = new Date(timestamp);
566
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
567
+ }
568
+ function ChatIcon() {
569
+ return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ 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" }) });
570
+ }
571
+ function CloseIcon() {
572
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
573
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
574
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
575
+ ] });
576
+ }
577
+ function SendIcon() {
578
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
579
+ /* @__PURE__ */ jsx("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
580
+ /* @__PURE__ */ jsx("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
581
+ ] });
582
+ }
583
+ function StatusIcon({ status }) {
584
+ if (!status || status === "sending" || status === "sent") {
585
+ return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", "stroke-width": "2", class: "pp-check", children: /* @__PURE__ */ jsx("polyline", { points: "3 8 7 12 13 4" }) });
586
+ }
587
+ if (status === "delivered") {
588
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 20 16", fill: "none", stroke: "currentColor", "stroke-width": "2", class: "pp-check-double", children: [
589
+ /* @__PURE__ */ jsx("polyline", { points: "1 8 5 12 11 4" }),
590
+ /* @__PURE__ */ jsx("polyline", { points: "7 8 11 12 17 4" })
591
+ ] });
592
+ }
593
+ if (status === "read") {
594
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 20 16", fill: "none", stroke: "currentColor", "stroke-width": "2", class: "pp-check-double pp-check-read", children: [
595
+ /* @__PURE__ */ jsx("polyline", { points: "1 8 5 12 11 4" }),
596
+ /* @__PURE__ */ jsx("polyline", { points: "7 8 11 12 17 4" })
597
+ ] });
598
+ }
599
+ return null;
600
+ }
601
+
602
+ // src/client.ts
603
+ var PocketPingClient = class {
604
+ constructor(config) {
605
+ this.session = null;
606
+ this.ws = null;
607
+ this.isOpen = false;
608
+ this.listeners = /* @__PURE__ */ new Map();
609
+ this.reconnectAttempts = 0;
610
+ this.maxReconnectAttempts = 5;
611
+ this.reconnectTimeout = null;
612
+ this.config = config;
613
+ }
614
+ // ─────────────────────────────────────────────────────────────────
615
+ // Public API
616
+ // ─────────────────────────────────────────────────────────────────
617
+ async connect() {
618
+ const visitorId = this.getOrCreateVisitorId();
619
+ const storedSessionId = this.getStoredSessionId();
620
+ const response = await this.fetch("/connect", {
621
+ method: "POST",
622
+ body: JSON.stringify({
623
+ visitorId,
624
+ sessionId: storedSessionId,
625
+ metadata: {
626
+ url: window.location.href,
627
+ referrer: document.referrer || void 0,
628
+ pageTitle: document.title || void 0,
629
+ userAgent: navigator.userAgent,
630
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
631
+ language: navigator.language,
632
+ screenResolution: `${window.screen.width}x${window.screen.height}`
633
+ }
634
+ })
635
+ });
636
+ this.session = {
637
+ sessionId: response.sessionId,
638
+ visitorId: response.visitorId,
639
+ operatorOnline: response.operatorOnline ?? false,
640
+ messages: response.messages ?? []
641
+ };
642
+ this.storeSessionId(response.sessionId);
643
+ this.connectWebSocket();
644
+ this.emit("connect", this.session);
645
+ this.config.onConnect?.(response.sessionId);
646
+ return this.session;
647
+ }
648
+ disconnect() {
649
+ this.ws?.close();
650
+ this.ws = null;
651
+ this.session = null;
652
+ if (this.reconnectTimeout) {
653
+ clearTimeout(this.reconnectTimeout);
654
+ }
655
+ }
656
+ async sendMessage(content) {
657
+ if (!this.session) {
658
+ throw new Error("Not connected");
659
+ }
660
+ const tempId = `temp-${this.generateId()}`;
661
+ const tempMessage = {
662
+ id: tempId,
663
+ sessionId: this.session.sessionId,
664
+ content,
665
+ sender: "visitor",
666
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
667
+ status: "sending"
668
+ };
669
+ this.session.messages.push(tempMessage);
670
+ this.emit("message", tempMessage);
671
+ try {
672
+ const response = await this.fetch("/message", {
673
+ method: "POST",
674
+ body: JSON.stringify({
675
+ sessionId: this.session.sessionId,
676
+ content,
677
+ sender: "visitor"
678
+ })
679
+ });
680
+ const messageIndex = this.session.messages.findIndex((m) => m.id === tempId);
681
+ if (messageIndex >= 0) {
682
+ this.session.messages[messageIndex].id = response.messageId;
683
+ this.session.messages[messageIndex].timestamp = response.timestamp;
684
+ this.session.messages[messageIndex].status = "sent";
685
+ this.emit("message", this.session.messages[messageIndex]);
686
+ }
687
+ const message = this.session.messages[messageIndex] || {
688
+ id: response.messageId,
689
+ sessionId: this.session.sessionId,
690
+ content,
691
+ sender: "visitor",
692
+ timestamp: response.timestamp,
693
+ status: "sent"
694
+ };
695
+ this.config.onMessage?.(message);
696
+ return message;
697
+ } catch (error) {
698
+ const messageIndex = this.session.messages.findIndex((m) => m.id === tempId);
699
+ if (messageIndex >= 0) {
700
+ this.session.messages.splice(messageIndex, 1);
701
+ this.emit("message", tempMessage);
702
+ }
703
+ throw error;
704
+ }
705
+ }
706
+ async fetchMessages(after) {
707
+ if (!this.session) {
708
+ throw new Error("Not connected");
709
+ }
710
+ const params = new URLSearchParams({
711
+ sessionId: this.session.sessionId
712
+ });
713
+ if (after) {
714
+ params.set("after", after);
715
+ }
716
+ const response = await this.fetch(
717
+ `/messages?${params}`,
718
+ { method: "GET" }
719
+ );
720
+ return response.messages;
721
+ }
722
+ async sendTyping(isTyping = true) {
723
+ if (!this.session) return;
724
+ await this.fetch("/typing", {
725
+ method: "POST",
726
+ body: JSON.stringify({
727
+ sessionId: this.session.sessionId,
728
+ sender: "visitor",
729
+ isTyping
730
+ })
731
+ });
732
+ }
733
+ async sendReadStatus(messageIds, status) {
734
+ if (!this.session || messageIds.length === 0) return;
735
+ try {
736
+ await this.fetch("/read", {
737
+ method: "POST",
738
+ body: JSON.stringify({
739
+ sessionId: this.session.sessionId,
740
+ messageIds,
741
+ status
742
+ })
743
+ });
744
+ for (const msg of this.session.messages) {
745
+ if (messageIds.includes(msg.id)) {
746
+ msg.status = status;
747
+ if (status === "delivered") {
748
+ msg.deliveredAt = (/* @__PURE__ */ new Date()).toISOString();
749
+ } else if (status === "read") {
750
+ msg.readAt = (/* @__PURE__ */ new Date()).toISOString();
751
+ }
752
+ }
753
+ }
754
+ this.emit("readStatusSent", { messageIds, status });
755
+ } catch (err) {
756
+ console.error("[PocketPing] Failed to send read status:", err);
757
+ }
758
+ }
759
+ async getPresence() {
760
+ return this.fetch("/presence", { method: "GET" });
761
+ }
762
+ // ─────────────────────────────────────────────────────────────────
763
+ // State
764
+ // ─────────────────────────────────────────────────────────────────
765
+ getSession() {
766
+ return this.session;
767
+ }
768
+ getMessages() {
769
+ return this.session?.messages ?? [];
770
+ }
771
+ isConnected() {
772
+ return this.session !== null;
773
+ }
774
+ isWidgetOpen() {
775
+ return this.isOpen;
776
+ }
777
+ setOpen(open2) {
778
+ this.isOpen = open2;
779
+ this.emit("openChange", open2);
780
+ if (open2) {
781
+ this.config.onOpen?.();
782
+ } else {
783
+ this.config.onClose?.();
784
+ }
785
+ }
786
+ toggleOpen() {
787
+ this.setOpen(!this.isOpen);
788
+ }
789
+ // ─────────────────────────────────────────────────────────────────
790
+ // Events
791
+ // ─────────────────────────────────────────────────────────────────
792
+ on(event, listener) {
793
+ if (!this.listeners.has(event)) {
794
+ this.listeners.set(event, /* @__PURE__ */ new Set());
795
+ }
796
+ this.listeners.get(event).add(listener);
797
+ return () => {
798
+ this.listeners.get(event)?.delete(listener);
799
+ };
800
+ }
801
+ emit(event, data) {
802
+ this.listeners.get(event)?.forEach((listener) => listener(data));
803
+ }
804
+ // ─────────────────────────────────────────────────────────────────
805
+ // WebSocket
806
+ // ─────────────────────────────────────────────────────────────────
807
+ connectWebSocket() {
808
+ if (!this.session) return;
809
+ const wsUrl = this.config.endpoint.replace(/^http/, "ws").replace(/\/$/, "") + `/stream?sessionId=${this.session.sessionId}`;
810
+ try {
811
+ this.ws = new WebSocket(wsUrl);
812
+ this.ws.onopen = () => {
813
+ this.reconnectAttempts = 0;
814
+ this.emit("wsConnected", null);
815
+ };
816
+ this.ws.onmessage = (event) => {
817
+ try {
818
+ const wsEvent = JSON.parse(event.data);
819
+ this.handleWebSocketEvent(wsEvent);
820
+ } catch (err) {
821
+ console.error("[PocketPing] Failed to parse WS message:", err);
822
+ }
823
+ };
824
+ this.ws.onclose = () => {
825
+ this.emit("wsDisconnected", null);
826
+ this.scheduleReconnect();
827
+ };
828
+ this.ws.onerror = (err) => {
829
+ console.error("[PocketPing] WebSocket error:", err);
830
+ };
831
+ } catch (err) {
832
+ console.warn("[PocketPing] WebSocket unavailable, using polling");
833
+ this.startPolling();
834
+ }
835
+ }
836
+ handleWebSocketEvent(event) {
837
+ switch (event.type) {
838
+ case "message":
839
+ const message = event.data;
840
+ if (this.session) {
841
+ let existingIndex = this.session.messages.findIndex((m) => m.id === message.id);
842
+ if (existingIndex < 0 && message.sender === "visitor") {
843
+ existingIndex = this.session.messages.findIndex(
844
+ (m) => m.id.startsWith("temp-") && m.content === message.content && m.sender === "visitor"
845
+ );
846
+ if (existingIndex >= 0) {
847
+ this.session.messages[existingIndex].id = message.id;
848
+ }
849
+ }
850
+ if (existingIndex < 0 && message.sender !== "visitor") {
851
+ const msgTime = new Date(message.timestamp).getTime();
852
+ existingIndex = this.session.messages.findIndex(
853
+ (m) => m.sender === message.sender && m.content === message.content && Math.abs(new Date(m.timestamp).getTime() - msgTime) < 2e3
854
+ );
855
+ }
856
+ if (existingIndex >= 0) {
857
+ const existing = this.session.messages[existingIndex];
858
+ if (message.status && message.status !== existing.status) {
859
+ existing.status = message.status;
860
+ if (message.deliveredAt) existing.deliveredAt = message.deliveredAt;
861
+ if (message.readAt) existing.readAt = message.readAt;
862
+ this.emit("read", { messageIds: [message.id], status: message.status });
863
+ }
864
+ } else {
865
+ this.session.messages.push(message);
866
+ this.emit("message", message);
867
+ this.config.onMessage?.(message);
868
+ }
869
+ }
870
+ if (message.sender !== "visitor") {
871
+ this.emit("typing", { isTyping: false });
872
+ }
873
+ break;
874
+ case "typing":
875
+ const typingData = event.data;
876
+ if (typingData.sender !== "visitor") {
877
+ this.emit("typing", { isTyping: typingData.isTyping });
878
+ }
879
+ break;
880
+ case "presence":
881
+ if (this.session) {
882
+ this.session.operatorOnline = event.data.online;
883
+ }
884
+ this.emit("presence", event.data);
885
+ break;
886
+ case "ai_takeover":
887
+ this.emit("aiTakeover", event.data);
888
+ break;
889
+ case "read":
890
+ const readData = event.data;
891
+ if (this.session) {
892
+ for (const msg of this.session.messages) {
893
+ if (readData.messageIds.includes(msg.id)) {
894
+ msg.status = readData.status;
895
+ if (readData.deliveredAt) msg.deliveredAt = readData.deliveredAt;
896
+ if (readData.readAt) msg.readAt = readData.readAt;
897
+ }
898
+ }
899
+ }
900
+ this.emit("read", readData);
901
+ break;
902
+ }
903
+ }
904
+ scheduleReconnect() {
905
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
906
+ console.warn("[PocketPing] Max reconnect attempts reached, switching to polling");
907
+ this.startPolling();
908
+ return;
909
+ }
910
+ const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
911
+ this.reconnectAttempts++;
912
+ this.reconnectTimeout = setTimeout(() => {
913
+ this.connectWebSocket();
914
+ }, delay);
915
+ }
916
+ startPolling() {
917
+ const poll = async () => {
918
+ if (!this.session) return;
919
+ try {
920
+ const lastMessageId = this.session.messages[this.session.messages.length - 1]?.id;
921
+ const newMessages = await this.fetchMessages(lastMessageId);
922
+ for (const message of newMessages) {
923
+ if (!this.session.messages.find((m) => m.id === message.id)) {
924
+ this.session.messages.push(message);
925
+ this.emit("message", message);
926
+ this.config.onMessage?.(message);
927
+ }
928
+ }
929
+ } catch (err) {
930
+ console.error("[PocketPing] Polling error:", err);
931
+ }
932
+ if (this.session) {
933
+ setTimeout(poll, 3e3);
934
+ }
935
+ };
936
+ poll();
937
+ }
938
+ // ─────────────────────────────────────────────────────────────────
939
+ // HTTP
940
+ // ─────────────────────────────────────────────────────────────────
941
+ async fetch(path, options) {
942
+ const url = this.config.endpoint.replace(/\/$/, "") + path;
943
+ const response = await fetch(url, {
944
+ ...options,
945
+ headers: {
946
+ "Content-Type": "application/json",
947
+ ...options.headers
948
+ }
949
+ });
950
+ if (!response.ok) {
951
+ const error = await response.text();
952
+ throw new Error(`PocketPing API error: ${response.status} ${error}`);
953
+ }
954
+ return response.json();
955
+ }
956
+ // ─────────────────────────────────────────────────────────────────
957
+ // Storage
958
+ // ─────────────────────────────────────────────────────────────────
959
+ getOrCreateVisitorId() {
960
+ const key = "pocketping_visitor_id";
961
+ let visitorId = localStorage.getItem(key);
962
+ if (!visitorId) {
963
+ visitorId = this.generateId();
964
+ localStorage.setItem(key, visitorId);
965
+ }
966
+ return visitorId;
967
+ }
968
+ getStoredSessionId() {
969
+ return localStorage.getItem("pocketping_session_id");
970
+ }
971
+ storeSessionId(sessionId) {
972
+ localStorage.setItem("pocketping_session_id", sessionId);
973
+ }
974
+ generateId() {
975
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`;
976
+ }
977
+ };
978
+
979
+ // src/index.ts
980
+ var client = null;
981
+ var container = null;
982
+ function init(config) {
983
+ if (client) {
984
+ console.warn("[PocketPing] Already initialized");
985
+ return client;
986
+ }
987
+ if (!config.endpoint) {
988
+ throw new Error("[PocketPing] endpoint is required");
989
+ }
990
+ client = new PocketPingClient(config);
991
+ container = document.createElement("div");
992
+ container.id = "pocketping-container";
993
+ document.body.appendChild(container);
994
+ render(h2(ChatWidget, { client, config }), container);
995
+ client.connect().catch((err) => {
996
+ console.error("[PocketPing] Failed to connect:", err);
997
+ });
998
+ return client;
999
+ }
1000
+ function destroy() {
1001
+ if (container) {
1002
+ render(null, container);
1003
+ container.remove();
1004
+ container = null;
1005
+ }
1006
+ if (client) {
1007
+ client.disconnect();
1008
+ client = null;
1009
+ }
1010
+ }
1011
+ function open() {
1012
+ client?.setOpen(true);
1013
+ }
1014
+ function close() {
1015
+ client?.setOpen(false);
1016
+ }
1017
+ function toggle() {
1018
+ client?.toggleOpen();
1019
+ }
1020
+ function sendMessage(content) {
1021
+ if (!client) {
1022
+ throw new Error("[PocketPing] Not initialized");
1023
+ }
1024
+ return client.sendMessage(content);
1025
+ }
1026
+ if (typeof document !== "undefined") {
1027
+ const script = document.currentScript;
1028
+ if (script?.dataset.endpoint) {
1029
+ init({
1030
+ endpoint: script.dataset.endpoint,
1031
+ theme: script.dataset.theme || "auto",
1032
+ position: script.dataset.position || "bottom-right"
1033
+ });
1034
+ }
1035
+ }
1036
+ var index_default = { init, destroy, open, close, toggle, sendMessage };
1037
+ export {
1038
+ close,
1039
+ index_default as default,
1040
+ destroy,
1041
+ init,
1042
+ open,
1043
+ sendMessage,
1044
+ toggle
1045
+ };