simple-support-chat 0.3.3 → 0.4.1
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/README.md +197 -3
- package/dist/client/index.cjs +205 -103
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +86 -22
- package/dist/client/index.d.ts +86 -22
- package/dist/client/index.js +205 -104
- package/dist/client/index.js.map +1 -1
- package/dist/server/index.cjs +203 -7
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +120 -1
- package/dist/server/index.d.ts +120 -1
- package/dist/server/index.js +201 -8
- package/dist/server/index.js.map +1 -1
- package/package.json +87 -87
package/dist/client/index.d.ts
CHANGED
|
@@ -31,6 +31,10 @@ interface ChatBubbleProps {
|
|
|
31
31
|
user?: ChatUser;
|
|
32
32
|
/** URL to poll for team replies (e.g., '/api/support/replies'). Enables bidirectional chat. */
|
|
33
33
|
repliesUrl?: string;
|
|
34
|
+
/** URL for SSE endpoint (optional — enables real-time delivery when provided). */
|
|
35
|
+
sseUrl?: string;
|
|
36
|
+
/** Polling interval in milliseconds. Default: 4000 (4 seconds). */
|
|
37
|
+
pollInterval?: number;
|
|
34
38
|
/**
|
|
35
39
|
* Controlled mode: when provided, overrides internal open/close state.
|
|
36
40
|
* The chat panel is open when `isOpen` is true, closed when false.
|
|
@@ -98,6 +102,10 @@ interface SupportChatModalProps {
|
|
|
98
102
|
user?: ChatUser;
|
|
99
103
|
/** URL to poll for team replies (e.g., '/api/support/replies'). Enables bidirectional chat. */
|
|
100
104
|
repliesUrl?: string;
|
|
105
|
+
/** URL for SSE endpoint (optional — enables real-time delivery when provided). */
|
|
106
|
+
sseUrl?: string;
|
|
107
|
+
/** Polling interval in milliseconds. Default: 4000 (4 seconds). */
|
|
108
|
+
pollInterval?: number;
|
|
101
109
|
}
|
|
102
110
|
/** Anonymous context collected automatically from the browser */
|
|
103
111
|
interface AnonymousContext {
|
|
@@ -120,7 +128,7 @@ interface AnonymousContext {
|
|
|
120
128
|
* <ChatBubble apiUrl="/api/support" />
|
|
121
129
|
* ```
|
|
122
130
|
*/
|
|
123
|
-
declare function ChatBubble({ apiUrl, position, color, title, placeholder, show, user, repliesUrl, isOpen: isOpenProp, onOpenChange, badgeColor, unreadCount, }: ChatBubbleProps): react_jsx_runtime.JSX.Element;
|
|
131
|
+
declare function ChatBubble({ apiUrl, position, color, title, placeholder, show, user, repliesUrl, sseUrl, pollInterval, isOpen: isOpenProp, onOpenChange, badgeColor, unreadCount, }: ChatBubbleProps): react_jsx_runtime.JSX.Element;
|
|
124
132
|
|
|
125
133
|
/**
|
|
126
134
|
* SupportChatModal -- modal-based support chat.
|
|
@@ -152,7 +160,7 @@ declare function ChatBubble({ apiUrl, position, color, title, placeholder, show,
|
|
|
152
160
|
* }
|
|
153
161
|
* ```
|
|
154
162
|
*/
|
|
155
|
-
declare function SupportChatModal({ apiUrl, isOpen, onClose, color, title, placeholder, user, repliesUrl, }: SupportChatModalProps): react_jsx_runtime.JSX.Element | null;
|
|
163
|
+
declare function SupportChatModal({ apiUrl, isOpen, onClose, color, title, placeholder, user, repliesUrl, sseUrl, pollInterval, }: SupportChatModalProps): react_jsx_runtime.JSX.Element | null;
|
|
156
164
|
|
|
157
165
|
/**
|
|
158
166
|
* Hook that returns controls for opening/closing the support chat.
|
|
@@ -166,6 +174,50 @@ declare function SupportChatModal({ apiUrl, isOpen, onClose, color, title, place
|
|
|
166
174
|
*/
|
|
167
175
|
declare function useSupportChat(): SupportChatState;
|
|
168
176
|
|
|
177
|
+
/** Transport mode for reply delivery */
|
|
178
|
+
type TransportMode = "sse" | "polling" | "disconnected";
|
|
179
|
+
/** Options for useReplyTransport */
|
|
180
|
+
interface ReplyTransportOptions {
|
|
181
|
+
/** URL for SSE endpoint (optional — enables SSE transport when provided) */
|
|
182
|
+
sseUrl?: string;
|
|
183
|
+
/** URL for polling replies (optional — used as fallback or primary when no sseUrl) */
|
|
184
|
+
repliesUrl?: string;
|
|
185
|
+
/** Session identifier for the chat session */
|
|
186
|
+
sessionId: string;
|
|
187
|
+
/** Whether the transport should be active (connected/polling) */
|
|
188
|
+
isActive: boolean;
|
|
189
|
+
/** Polling interval in milliseconds. Default: 4000 (4 seconds). */
|
|
190
|
+
pollInterval?: number;
|
|
191
|
+
}
|
|
192
|
+
/** Return value from useReplyTransport */
|
|
193
|
+
interface ReplyTransportState {
|
|
194
|
+
/** Accumulated replies from the transport */
|
|
195
|
+
replies: ChatMessage[];
|
|
196
|
+
/** Current transport mode */
|
|
197
|
+
transport: TransportMode;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Internal hook that manages reply delivery via SSE or polling fallback.
|
|
201
|
+
*
|
|
202
|
+
* When `sseUrl` is provided and `isActive` is true, connects via EventSource
|
|
203
|
+
* for real-time reply delivery. If the EventSource connection fails, automatically
|
|
204
|
+
* falls back to polling via `repliesUrl`.
|
|
205
|
+
*
|
|
206
|
+
* When only `repliesUrl` is provided (no `sseUrl`), uses polling directly,
|
|
207
|
+
* preserving full backward compatibility with existing setups.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* const { replies, transport } = useReplyTransport({
|
|
212
|
+
* sseUrl: '/api/support/sse',
|
|
213
|
+
* repliesUrl: '/api/support/replies',
|
|
214
|
+
* sessionId: 'user-123',
|
|
215
|
+
* isActive: true,
|
|
216
|
+
* });
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
declare function useReplyTransport({ sseUrl, repliesUrl, sessionId, isActive, pollInterval, }: ReplyTransportOptions): ReplyTransportState;
|
|
220
|
+
|
|
169
221
|
/** Options for initializing the chat engine */
|
|
170
222
|
interface ChatEngineOptions {
|
|
171
223
|
/** URL of the support chat API endpoint */
|
|
@@ -174,8 +226,12 @@ interface ChatEngineOptions {
|
|
|
174
226
|
user?: ChatUser;
|
|
175
227
|
/** URL to poll for team replies. When set, polling starts while chat is active. */
|
|
176
228
|
repliesUrl?: string;
|
|
229
|
+
/** URL for SSE endpoint (optional — enables real-time delivery when provided). */
|
|
230
|
+
sseUrl?: string;
|
|
177
231
|
/** Whether the chat panel is currently visible (controls polling lifecycle). Defaults to true. */
|
|
178
232
|
isOpen?: boolean;
|
|
233
|
+
/** Polling interval in milliseconds. Default: 4000 (4 seconds). */
|
|
234
|
+
pollInterval?: number;
|
|
179
235
|
}
|
|
180
236
|
/** Return value from useChatEngine */
|
|
181
237
|
interface ChatEngineState {
|
|
@@ -191,56 +247,64 @@ interface ChatEngineState {
|
|
|
191
247
|
sendMessage: () => Promise<void>;
|
|
192
248
|
/** Handle keydown on the input (Enter to send) */
|
|
193
249
|
handleKeyDown: (e: React.KeyboardEvent) => void;
|
|
250
|
+
/** Current transport mode */
|
|
251
|
+
transport: TransportMode;
|
|
194
252
|
}
|
|
195
253
|
/**
|
|
196
254
|
* Shared chat engine hook used by both ChatBubble and SupportChatModal.
|
|
197
|
-
* Manages message state, input, API communication, and reply
|
|
255
|
+
* Manages message state, input, API communication, and reply delivery
|
|
256
|
+
* via useReplyTransport (SSE with polling fallback).
|
|
198
257
|
*/
|
|
199
|
-
declare function useChatEngine({ apiUrl, user, repliesUrl, isOpen, }: ChatEngineOptions): ChatEngineState;
|
|
258
|
+
declare function useChatEngine({ apiUrl, user, repliesUrl, sseUrl, isOpen, pollInterval, }: ChatEngineOptions): ChatEngineState;
|
|
200
259
|
|
|
201
|
-
/** Options for
|
|
260
|
+
/** Options for useUnreadCount */
|
|
202
261
|
interface UnreadCountOptions {
|
|
203
|
-
/** URL
|
|
204
|
-
|
|
205
|
-
/**
|
|
262
|
+
/** URL for SSE endpoint (optional — enables real-time delivery when provided). */
|
|
263
|
+
sseUrl?: string;
|
|
264
|
+
/** URL to poll for team replies (e.g., '/api/support/replies'). */
|
|
265
|
+
repliesUrl?: string;
|
|
266
|
+
/** Authenticated user info (optional — uses user.id as sessionId if provided). */
|
|
206
267
|
user?: ChatUser;
|
|
207
|
-
/** Whether the chat panel is currently open.
|
|
268
|
+
/** Whether the chat panel is currently open. When open, unread count resets to 0. */
|
|
208
269
|
isOpen: boolean;
|
|
209
270
|
}
|
|
210
271
|
/** Return value from useUnreadCount */
|
|
211
272
|
interface UnreadCountState {
|
|
212
|
-
/** Number of unread
|
|
273
|
+
/** Number of unread replies received while the chat was closed */
|
|
213
274
|
unreadCount: number;
|
|
214
275
|
/** Convenience boolean: true when unreadCount > 0 */
|
|
215
276
|
hasUnread: boolean;
|
|
216
|
-
/** Pre-
|
|
277
|
+
/** Pre-cached pending replies for instant display when chat opens */
|
|
217
278
|
pendingReplies: ChatMessage[];
|
|
218
279
|
/** Reset unreadCount to 0 and clear pendingReplies */
|
|
219
280
|
markAsRead: () => void;
|
|
281
|
+
/** Current transport mode */
|
|
282
|
+
transport: TransportMode;
|
|
220
283
|
}
|
|
221
284
|
/**
|
|
222
|
-
* Hook that tracks unread reply count
|
|
285
|
+
* Hook that tracks unread reply count when the chat is closed.
|
|
223
286
|
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* -
|
|
227
|
-
*
|
|
228
|
-
*
|
|
287
|
+
* Uses useReplyTransport internally to receive replies via SSE or polling.
|
|
288
|
+
* The transport stays active even when the chat is closed so replies arrive
|
|
289
|
+
* in real-time and the unread badge updates immediately.
|
|
290
|
+
*
|
|
291
|
+
* When the chat opens (isOpen becomes true), the unread count resets to 0.
|
|
292
|
+
* The pendingReplies array provides pre-cached replies for instant display.
|
|
229
293
|
*
|
|
230
294
|
* @example
|
|
231
|
-
* ```
|
|
232
|
-
* const { unreadCount, hasUnread, markAsRead } = useUnreadCount({
|
|
295
|
+
* ```ts
|
|
296
|
+
* const { unreadCount, hasUnread, pendingReplies, markAsRead } = useUnreadCount({
|
|
297
|
+
* sseUrl: '/api/support/sse',
|
|
233
298
|
* repliesUrl: '/api/support/replies',
|
|
234
|
-
* user: currentUser,
|
|
235
299
|
* isOpen: false,
|
|
236
300
|
* });
|
|
237
301
|
* ```
|
|
238
302
|
*/
|
|
239
|
-
declare function useUnreadCount({ repliesUrl, user, isOpen, }: UnreadCountOptions): UnreadCountState;
|
|
303
|
+
declare function useUnreadCount({ sseUrl, repliesUrl, user, isOpen, }: UnreadCountOptions): UnreadCountState;
|
|
240
304
|
|
|
241
305
|
/** Get or create a persistent session ID from localStorage */
|
|
242
306
|
declare function getSessionId(): string;
|
|
243
307
|
/** Collect anonymous browser context */
|
|
244
308
|
declare function collectAnonymousContext(): AnonymousContext;
|
|
245
309
|
|
|
246
|
-
export { type AnonymousContext, type BubblePosition, ChatBubble, type ChatBubbleProps, type ChatEngineOptions, type ChatEngineState, type ChatMessage, type ChatUser, SupportChatModal, type SupportChatModalProps, type SupportChatState, type UnreadCountOptions, type UnreadCountState, collectAnonymousContext, getSessionId, useChatEngine, useSupportChat, useUnreadCount };
|
|
310
|
+
export { type AnonymousContext, type BubblePosition, ChatBubble, type ChatBubbleProps, type ChatEngineOptions, type ChatEngineState, type ChatMessage, type ChatUser, type ReplyTransportOptions, type ReplyTransportState, SupportChatModal, type SupportChatModalProps, type SupportChatState, type TransportMode, type UnreadCountOptions, type UnreadCountState, collectAnonymousContext, getSessionId, useChatEngine, useReplyTransport, useSupportChat, useUnreadCount };
|
package/dist/client/index.js
CHANGED
|
@@ -46,20 +46,183 @@ function collectAnonymousContext() {
|
|
|
46
46
|
sessionId
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
+
var DEFAULT_POLL_INTERVAL = 4e3;
|
|
50
|
+
function useReplyTransport({
|
|
51
|
+
sseUrl,
|
|
52
|
+
repliesUrl,
|
|
53
|
+
sessionId,
|
|
54
|
+
isActive,
|
|
55
|
+
pollInterval = DEFAULT_POLL_INTERVAL
|
|
56
|
+
}) {
|
|
57
|
+
const [replies, setReplies] = useState([]);
|
|
58
|
+
const [transport, setTransport] = useState("disconnected");
|
|
59
|
+
const knownReplyIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
60
|
+
const lastReplyTimestampRef = useRef(null);
|
|
61
|
+
const sseFailedRef = useRef(false);
|
|
62
|
+
const eventSourceRef = useRef(null);
|
|
63
|
+
const pollIntervalRef = useRef(null);
|
|
64
|
+
const needsCatchUpRef = useRef(false);
|
|
65
|
+
const addReplies = useCallback(
|
|
66
|
+
(newReplies) => {
|
|
67
|
+
const deduplicated = newReplies.filter(
|
|
68
|
+
(r) => !knownReplyIdsRef.current.has(r.id)
|
|
69
|
+
);
|
|
70
|
+
if (deduplicated.length === 0) return;
|
|
71
|
+
for (const r of deduplicated) {
|
|
72
|
+
knownReplyIdsRef.current.add(r.id);
|
|
73
|
+
}
|
|
74
|
+
const latestTimestamp = deduplicated.reduce((latest, r) => {
|
|
75
|
+
return r.timestamp > latest ? r.timestamp : latest;
|
|
76
|
+
}, lastReplyTimestampRef.current ?? "");
|
|
77
|
+
lastReplyTimestampRef.current = latestTimestamp;
|
|
78
|
+
const replyMessages = deduplicated.map((r) => ({
|
|
79
|
+
id: r.id,
|
|
80
|
+
text: r.text,
|
|
81
|
+
sender: "received",
|
|
82
|
+
timestamp: new Date(r.timestamp).getTime()
|
|
83
|
+
}));
|
|
84
|
+
setReplies((prev) => [...prev, ...replyMessages]);
|
|
85
|
+
},
|
|
86
|
+
[]
|
|
87
|
+
);
|
|
88
|
+
const fetchReplies = useCallback(async () => {
|
|
89
|
+
if (!repliesUrl) return;
|
|
90
|
+
try {
|
|
91
|
+
const params = new URLSearchParams({ sessionId });
|
|
92
|
+
if (lastReplyTimestampRef.current) {
|
|
93
|
+
params.set("since", lastReplyTimestampRef.current);
|
|
94
|
+
}
|
|
95
|
+
const response = await fetch(`${repliesUrl}?${params.toString()}`);
|
|
96
|
+
if (!response.ok) return;
|
|
97
|
+
const data = await response.json();
|
|
98
|
+
if (data.replies.length === 0) return;
|
|
99
|
+
addReplies(data.replies);
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
}, [repliesUrl, sessionId, addReplies]);
|
|
103
|
+
const startPolling = useCallback(() => {
|
|
104
|
+
if (!repliesUrl) return;
|
|
105
|
+
if (pollIntervalRef.current !== null) {
|
|
106
|
+
clearInterval(pollIntervalRef.current);
|
|
107
|
+
}
|
|
108
|
+
setTransport("polling");
|
|
109
|
+
void fetchReplies();
|
|
110
|
+
pollIntervalRef.current = setInterval(
|
|
111
|
+
() => void fetchReplies(),
|
|
112
|
+
pollInterval
|
|
113
|
+
);
|
|
114
|
+
}, [repliesUrl, fetchReplies]);
|
|
115
|
+
const stopPolling = useCallback(() => {
|
|
116
|
+
if (pollIntervalRef.current !== null) {
|
|
117
|
+
clearInterval(pollIntervalRef.current);
|
|
118
|
+
pollIntervalRef.current = null;
|
|
119
|
+
}
|
|
120
|
+
}, []);
|
|
121
|
+
const connectSSE = useCallback(() => {
|
|
122
|
+
if (!sseUrl || sseFailedRef.current) return;
|
|
123
|
+
if (eventSourceRef.current) {
|
|
124
|
+
eventSourceRef.current.close();
|
|
125
|
+
eventSourceRef.current = null;
|
|
126
|
+
}
|
|
127
|
+
const url = `${sseUrl}?sessionId=${encodeURIComponent(sessionId)}`;
|
|
128
|
+
const es = new EventSource(url);
|
|
129
|
+
eventSourceRef.current = es;
|
|
130
|
+
es.addEventListener("reply", (event) => {
|
|
131
|
+
try {
|
|
132
|
+
const data = JSON.parse(event.data);
|
|
133
|
+
addReplies([data.reply]);
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
es.addEventListener("heartbeat", () => {
|
|
138
|
+
});
|
|
139
|
+
es.onopen = () => {
|
|
140
|
+
setTransport("sse");
|
|
141
|
+
if (needsCatchUpRef.current) {
|
|
142
|
+
needsCatchUpRef.current = false;
|
|
143
|
+
void fetchReplies();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
es.onerror = () => {
|
|
147
|
+
if (es.readyState === EventSource.CLOSED) {
|
|
148
|
+
sseFailedRef.current = true;
|
|
149
|
+
es.close();
|
|
150
|
+
eventSourceRef.current = null;
|
|
151
|
+
startPolling();
|
|
152
|
+
} else {
|
|
153
|
+
needsCatchUpRef.current = true;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}, [sseUrl, sessionId, addReplies, fetchReplies, startPolling]);
|
|
157
|
+
const disconnectSSE = useCallback(() => {
|
|
158
|
+
if (eventSourceRef.current) {
|
|
159
|
+
eventSourceRef.current.close();
|
|
160
|
+
eventSourceRef.current = null;
|
|
161
|
+
}
|
|
162
|
+
}, []);
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (!isActive) {
|
|
165
|
+
disconnectSSE();
|
|
166
|
+
stopPolling();
|
|
167
|
+
setTransport("disconnected");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (!sseUrl && !repliesUrl) {
|
|
171
|
+
setTransport("disconnected");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (sseUrl && !sseFailedRef.current) {
|
|
175
|
+
connectSSE();
|
|
176
|
+
} else if (repliesUrl) {
|
|
177
|
+
startPolling();
|
|
178
|
+
}
|
|
179
|
+
return () => {
|
|
180
|
+
disconnectSSE();
|
|
181
|
+
stopPolling();
|
|
182
|
+
};
|
|
183
|
+
}, [
|
|
184
|
+
isActive,
|
|
185
|
+
sseUrl,
|
|
186
|
+
repliesUrl,
|
|
187
|
+
connectSSE,
|
|
188
|
+
disconnectSSE,
|
|
189
|
+
startPolling,
|
|
190
|
+
stopPolling
|
|
191
|
+
]);
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
sseFailedRef.current = false;
|
|
194
|
+
}, [sseUrl]);
|
|
195
|
+
return { replies, transport };
|
|
196
|
+
}
|
|
49
197
|
|
|
50
198
|
// src/client/useChatEngine.ts
|
|
51
|
-
var POLL_INTERVAL = 4e3;
|
|
52
199
|
function useChatEngine({
|
|
53
200
|
apiUrl,
|
|
54
201
|
user,
|
|
55
202
|
repliesUrl,
|
|
56
|
-
|
|
203
|
+
sseUrl,
|
|
204
|
+
isOpen = true,
|
|
205
|
+
pollInterval
|
|
57
206
|
}) {
|
|
58
207
|
const [messages, setMessages] = useState([]);
|
|
59
208
|
const [input, setInput] = useState("");
|
|
60
209
|
const [sending, setSending] = useState(false);
|
|
61
|
-
const
|
|
62
|
-
const
|
|
210
|
+
const sessionId = user?.id ?? getSessionId();
|
|
211
|
+
const { replies, transport } = useReplyTransport({
|
|
212
|
+
sseUrl,
|
|
213
|
+
repliesUrl,
|
|
214
|
+
sessionId,
|
|
215
|
+
isActive: isOpen,
|
|
216
|
+
pollInterval
|
|
217
|
+
});
|
|
218
|
+
const processedReplyCountRef = useRef(0);
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (replies.length > processedReplyCountRef.current) {
|
|
221
|
+
const newReplies = replies.slice(processedReplyCountRef.current);
|
|
222
|
+
processedReplyCountRef.current = replies.length;
|
|
223
|
+
setMessages((prev) => [...prev, ...newReplies]);
|
|
224
|
+
}
|
|
225
|
+
}, [replies]);
|
|
63
226
|
const sendMessage = useCallback(async () => {
|
|
64
227
|
const text = input.trim();
|
|
65
228
|
if (!text || sending) return;
|
|
@@ -80,7 +243,7 @@ function useChatEngine({
|
|
|
80
243
|
body: JSON.stringify({
|
|
81
244
|
message: text,
|
|
82
245
|
user: user ?? void 0,
|
|
83
|
-
sessionId
|
|
246
|
+
sessionId,
|
|
84
247
|
context
|
|
85
248
|
})
|
|
86
249
|
});
|
|
@@ -108,7 +271,7 @@ function useChatEngine({
|
|
|
108
271
|
} finally {
|
|
109
272
|
setSending(false);
|
|
110
273
|
}
|
|
111
|
-
}, [input, sending, apiUrl, user]);
|
|
274
|
+
}, [input, sending, apiUrl, user, sessionId]);
|
|
112
275
|
const handleKeyDown = useCallback(
|
|
113
276
|
(e) => {
|
|
114
277
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
@@ -118,45 +281,7 @@ function useChatEngine({
|
|
|
118
281
|
},
|
|
119
282
|
[sendMessage]
|
|
120
283
|
);
|
|
121
|
-
|
|
122
|
-
if (!repliesUrl || !isOpen) return;
|
|
123
|
-
const sessionId = user?.id ?? getSessionId();
|
|
124
|
-
const fetchReplies = async () => {
|
|
125
|
-
try {
|
|
126
|
-
const params = new URLSearchParams({ sessionId });
|
|
127
|
-
if (lastReplyTimestampRef.current) {
|
|
128
|
-
params.set("since", lastReplyTimestampRef.current);
|
|
129
|
-
}
|
|
130
|
-
const response = await fetch(`${repliesUrl}?${params.toString()}`);
|
|
131
|
-
if (!response.ok) return;
|
|
132
|
-
const data = await response.json();
|
|
133
|
-
if (data.replies.length === 0) return;
|
|
134
|
-
const newReplies = data.replies.filter(
|
|
135
|
-
(r) => !knownReplyIdsRef.current.has(r.id)
|
|
136
|
-
);
|
|
137
|
-
if (newReplies.length === 0) return;
|
|
138
|
-
for (const r of newReplies) {
|
|
139
|
-
knownReplyIdsRef.current.add(r.id);
|
|
140
|
-
}
|
|
141
|
-
const latestTimestamp = newReplies.reduce((latest, r) => {
|
|
142
|
-
return r.timestamp > latest ? r.timestamp : latest;
|
|
143
|
-
}, lastReplyTimestampRef.current ?? "");
|
|
144
|
-
lastReplyTimestampRef.current = latestTimestamp;
|
|
145
|
-
const replyMessages = newReplies.map((r) => ({
|
|
146
|
-
id: r.id,
|
|
147
|
-
text: r.text,
|
|
148
|
-
sender: "received",
|
|
149
|
-
timestamp: new Date(r.timestamp).getTime()
|
|
150
|
-
}));
|
|
151
|
-
setMessages((prev) => [...prev, ...replyMessages]);
|
|
152
|
-
} catch {
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
void fetchReplies();
|
|
156
|
-
const intervalId = setInterval(() => void fetchReplies(), POLL_INTERVAL);
|
|
157
|
-
return () => clearInterval(intervalId);
|
|
158
|
-
}, [repliesUrl, isOpen, user]);
|
|
159
|
-
return { messages, input, setInput, sending, sendMessage, handleKeyDown };
|
|
284
|
+
return { messages, input, setInput, sending, sendMessage, handleKeyDown, transport };
|
|
160
285
|
}
|
|
161
286
|
function useColorScheme() {
|
|
162
287
|
const [scheme, setScheme] = useState(() => {
|
|
@@ -231,6 +356,8 @@ function ChatBubble({
|
|
|
231
356
|
show = true,
|
|
232
357
|
user,
|
|
233
358
|
repliesUrl,
|
|
359
|
+
sseUrl,
|
|
360
|
+
pollInterval,
|
|
234
361
|
isOpen: isOpenProp,
|
|
235
362
|
onOpenChange,
|
|
236
363
|
badgeColor = "#eab308",
|
|
@@ -253,7 +380,7 @@ function ChatBubble({
|
|
|
253
380
|
);
|
|
254
381
|
const colorScheme = useColorScheme();
|
|
255
382
|
const theme = useMemo(() => getThemeTokens(colorScheme), [colorScheme]);
|
|
256
|
-
const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user, repliesUrl, isOpen });
|
|
383
|
+
const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user, repliesUrl, sseUrl, isOpen, pollInterval });
|
|
257
384
|
const [panelState, setPanelState] = useState(
|
|
258
385
|
"closed"
|
|
259
386
|
);
|
|
@@ -623,9 +750,11 @@ function SupportChatModal({
|
|
|
623
750
|
title = "Contact Us",
|
|
624
751
|
placeholder = "Type a message...",
|
|
625
752
|
user,
|
|
626
|
-
repliesUrl
|
|
753
|
+
repliesUrl,
|
|
754
|
+
sseUrl,
|
|
755
|
+
pollInterval
|
|
627
756
|
}) {
|
|
628
|
-
const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user, repliesUrl, isOpen });
|
|
757
|
+
const { messages, input, setInput, sending, sendMessage, handleKeyDown } = useChatEngine({ apiUrl, user, repliesUrl, sseUrl, isOpen, pollInterval });
|
|
629
758
|
const colorScheme = useColorScheme();
|
|
630
759
|
const theme = useMemo(() => getThemeTokens(colorScheme), [colorScheme]);
|
|
631
760
|
const modalRef = useRef(null);
|
|
@@ -908,75 +1037,47 @@ function useSupportChat() {
|
|
|
908
1037
|
const toggle = useCallback(() => setIsOpen((prev) => !prev), []);
|
|
909
1038
|
return { open, close, toggle, isOpen };
|
|
910
1039
|
}
|
|
911
|
-
var POLL_INTERVAL2 = 4e3;
|
|
912
1040
|
function useUnreadCount({
|
|
1041
|
+
sseUrl,
|
|
913
1042
|
repliesUrl,
|
|
914
1043
|
user,
|
|
915
1044
|
isOpen
|
|
916
1045
|
}) {
|
|
917
|
-
const
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
1046
|
+
const sessionId = user?.id ?? getSessionId();
|
|
1047
|
+
const { replies, transport } = useReplyTransport({
|
|
1048
|
+
sseUrl,
|
|
1049
|
+
repliesUrl,
|
|
1050
|
+
sessionId,
|
|
1051
|
+
isActive: true
|
|
1052
|
+
});
|
|
1053
|
+
const [unreadCount, setUnreadCount] = useState(0);
|
|
1054
|
+
const [pendingReplies, setPendingReplies] = useState([]);
|
|
1055
|
+
const countedReplyCountRef = useRef(0);
|
|
1056
|
+
const wasOpenRef = useRef(isOpen);
|
|
921
1057
|
useEffect(() => {
|
|
922
|
-
if (isOpen && !
|
|
923
|
-
|
|
924
|
-
|
|
1058
|
+
if (isOpen && !wasOpenRef.current) {
|
|
1059
|
+
setUnreadCount(0);
|
|
1060
|
+
setPendingReplies([]);
|
|
925
1061
|
}
|
|
926
|
-
|
|
1062
|
+
wasOpenRef.current = isOpen;
|
|
927
1063
|
}, [isOpen]);
|
|
928
1064
|
const markAsRead = useCallback(() => {
|
|
929
|
-
|
|
930
|
-
|
|
1065
|
+
setUnreadCount(0);
|
|
1066
|
+
setPendingReplies([]);
|
|
931
1067
|
}, []);
|
|
932
1068
|
useEffect(() => {
|
|
933
|
-
if (
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
const response = await fetch(`${repliesUrl}?${params.toString()}`);
|
|
940
|
-
if (!response.ok || cancelled) return;
|
|
941
|
-
const data = await response.json();
|
|
942
|
-
if (cancelled) return;
|
|
943
|
-
const currentTotal = data.replies.length;
|
|
944
|
-
totalCountRef.current = currentTotal;
|
|
945
|
-
if (baselineCountRef.current === null) {
|
|
946
|
-
baselineCountRef.current = currentTotal;
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
const newCount = currentTotal - baselineCountRef.current;
|
|
950
|
-
if (newCount > 0) {
|
|
951
|
-
const newReplies = data.replies.slice(-newCount);
|
|
952
|
-
setAllReplies(
|
|
953
|
-
newReplies.map((r) => ({
|
|
954
|
-
id: r.id,
|
|
955
|
-
text: r.text,
|
|
956
|
-
sender: "received",
|
|
957
|
-
timestamp: new Date(r.timestamp).getTime()
|
|
958
|
-
}))
|
|
959
|
-
);
|
|
960
|
-
}
|
|
961
|
-
} catch {
|
|
1069
|
+
if (replies.length > countedReplyCountRef.current) {
|
|
1070
|
+
const newReplies = replies.slice(countedReplyCountRef.current);
|
|
1071
|
+
countedReplyCountRef.current = replies.length;
|
|
1072
|
+
if (!isOpen) {
|
|
1073
|
+
setUnreadCount((prev) => prev + newReplies.length);
|
|
1074
|
+
setPendingReplies((prev) => [...prev, ...newReplies]);
|
|
962
1075
|
}
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
return () => {
|
|
967
|
-
cancelled = true;
|
|
968
|
-
clearInterval(intervalId);
|
|
969
|
-
};
|
|
970
|
-
}, [repliesUrl, isOpen, user]);
|
|
971
|
-
const unreadCount = baselineCountRef.current !== null ? Math.max(0, totalCountRef.current - baselineCountRef.current) : 0;
|
|
972
|
-
return {
|
|
973
|
-
unreadCount,
|
|
974
|
-
hasUnread: unreadCount > 0,
|
|
975
|
-
pendingReplies: allReplies,
|
|
976
|
-
markAsRead
|
|
977
|
-
};
|
|
1076
|
+
}
|
|
1077
|
+
}, [replies, isOpen]);
|
|
1078
|
+
return { unreadCount, hasUnread: unreadCount > 0, pendingReplies, markAsRead, transport };
|
|
978
1079
|
}
|
|
979
1080
|
|
|
980
|
-
export { ChatBubble, SupportChatModal, collectAnonymousContext, getSessionId, useChatEngine, useSupportChat, useUnreadCount };
|
|
1081
|
+
export { ChatBubble, SupportChatModal, collectAnonymousContext, getSessionId, useChatEngine, useReplyTransport, useSupportChat, useUnreadCount };
|
|
981
1082
|
//# sourceMappingURL=index.js.map
|
|
982
1083
|
//# sourceMappingURL=index.js.map
|