@jokkoo/sdk-react 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.js ADDED
@@ -0,0 +1,2772 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ ConversationScreen: () => ConversationScreen,
24
+ FloatingButton: () => FloatingButton,
25
+ JokkooButton: () => FloatingButton,
26
+ JokkooProvider: () => JokkooProvider,
27
+ JokkooWidget: () => JokkooWidget,
28
+ NEW_CONVERSATION_ID: () => import_sdk_web3.NEW_CONVERSATION_ID,
29
+ RequestListScreen: () => RequestListScreen,
30
+ WidgetPanel: () => WidgetPanel,
31
+ defaultTheme: () => defaultTheme,
32
+ jokkooQueryKeys: () => jokkooQueryKeys,
33
+ mergeTheme: () => mergeTheme,
34
+ themeToCssVars: () => themeToCssVars,
35
+ useConversations: () => useConversations,
36
+ useFileUpload: () => useFileUpload,
37
+ useJokkooContext: () => useJokkooContext,
38
+ useMessages: () => useMessages,
39
+ useRealtimeStatus: () => useRealtimeStatus,
40
+ useSendMessage: () => useSendMessage,
41
+ useUnreadCount: () => useUnreadCount,
42
+ useVoiceRecorder: () => useVoiceRecorder
43
+ });
44
+ module.exports = __toCommonJS(src_exports);
45
+
46
+ // src/context/JokkooProvider.tsx
47
+ var import_react_query = require("@tanstack/react-query");
48
+ var import_sdk_web3 = require("@jokkoo/sdk-web");
49
+ var import_react2 = require("react");
50
+
51
+ // src/hooks/useGlobalRealtimeListener.ts
52
+ var import_react = require("react");
53
+ var import_sdk_web2 = require("@jokkoo/sdk-web");
54
+
55
+ // src/lib/realtime-cache-utils.ts
56
+ var import_sdk_web = require("@jokkoo/sdk-web");
57
+
58
+ // src/hooks/query-keys.ts
59
+ var jokkooQueryKeys = {
60
+ conversations: ["jokkoo", "conversations"],
61
+ conversationPolicies: ["jokkoo", "conversation-policies"],
62
+ messages: (conversationId) => ["jokkoo", "conversations", conversationId, "messages"]
63
+ };
64
+
65
+ // src/lib/message-cache.ts
66
+ var DEFAULT_PAGE_SIZE = 50;
67
+ function createMessagesCacheWithMessage(message, pageSize = DEFAULT_PAGE_SIZE) {
68
+ return {
69
+ pages: [
70
+ {
71
+ data: [message],
72
+ total: 1,
73
+ limit: pageSize,
74
+ offset: 0
75
+ }
76
+ ],
77
+ pageParams: ["latest"]
78
+ };
79
+ }
80
+ function prependMessageToMessagesCache(data, message) {
81
+ if (!data || data.pages.length === 0) {
82
+ return createMessagesCacheWithMessage(message);
83
+ }
84
+ const alreadyExists = data.pages.some(
85
+ (page) => page.data.some((item) => item.id === message.id)
86
+ );
87
+ if (alreadyExists) {
88
+ return data;
89
+ }
90
+ const pages = [...data.pages];
91
+ const firstPage = pages[0];
92
+ if (!firstPage) {
93
+ return createMessagesCacheWithMessage(message);
94
+ }
95
+ pages[0] = {
96
+ ...firstPage,
97
+ data: [message, ...firstPage.data],
98
+ total: firstPage.total + 1
99
+ };
100
+ return {
101
+ ...data,
102
+ pages
103
+ };
104
+ }
105
+
106
+ // src/lib/realtime-cache-utils.ts
107
+ function sortConversationsByUpdatedAt(conversations) {
108
+ return [...conversations].sort(
109
+ (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
110
+ );
111
+ }
112
+ function prependConversationToListCache(queryClient, conversation) {
113
+ queryClient.setQueryData(
114
+ jokkooQueryKeys.conversations,
115
+ (current) => {
116
+ if (!current || current.pages.length === 0) {
117
+ return {
118
+ pages: [
119
+ {
120
+ data: [conversation],
121
+ total: 1,
122
+ limit: 20,
123
+ offset: 0
124
+ }
125
+ ],
126
+ pageParams: [0]
127
+ };
128
+ }
129
+ const firstPage = current.pages[0];
130
+ if (!firstPage) {
131
+ return current;
132
+ }
133
+ const withoutDuplicate = firstPage.data.filter((item) => item.id !== conversation.id);
134
+ const nextData = sortConversationsByUpdatedAt([conversation, ...withoutDuplicate]);
135
+ return {
136
+ ...current,
137
+ pages: [
138
+ {
139
+ ...firstPage,
140
+ data: nextData,
141
+ total: withoutDuplicate.length === firstPage.data.length ? firstPage.total + 1 : firstPage.total
142
+ },
143
+ ...current.pages.slice(1)
144
+ ]
145
+ };
146
+ }
147
+ );
148
+ }
149
+ function upsertConversationInListCache(queryClient, conversation) {
150
+ queryClient.setQueryData(
151
+ jokkooQueryKeys.conversations,
152
+ (current) => {
153
+ if (!current || current.pages.length === 0) {
154
+ return current;
155
+ }
156
+ let found = false;
157
+ const pages = current.pages.map((page) => {
158
+ const nextData = page.data.map((item) => {
159
+ if (item.id !== conversation.id) {
160
+ return item;
161
+ }
162
+ found = true;
163
+ return {
164
+ ...item,
165
+ ...conversation,
166
+ assignedAgentName: conversation.assignedAgentName ?? item.assignedAgentName ?? null
167
+ };
168
+ });
169
+ return {
170
+ ...page,
171
+ data: sortConversationsByUpdatedAt(nextData)
172
+ };
173
+ });
174
+ if (!found) {
175
+ return current;
176
+ }
177
+ return {
178
+ ...current,
179
+ pages
180
+ };
181
+ }
182
+ );
183
+ }
184
+ function updateConversationStatusInListCache(queryClient, conversationId, status) {
185
+ queryClient.setQueryData(
186
+ jokkooQueryKeys.conversations,
187
+ (current) => {
188
+ if (!current || current.pages.length === 0) {
189
+ return current;
190
+ }
191
+ let found = false;
192
+ const pages = current.pages.map((page) => {
193
+ const nextData = page.data.map((item) => {
194
+ if (item.id !== conversationId) {
195
+ return item;
196
+ }
197
+ found = true;
198
+ return {
199
+ ...item,
200
+ status
201
+ };
202
+ });
203
+ return {
204
+ ...page,
205
+ data: nextData
206
+ };
207
+ });
208
+ if (!found) {
209
+ return current;
210
+ }
211
+ return {
212
+ ...current,
213
+ pages
214
+ };
215
+ }
216
+ );
217
+ }
218
+ function updateConversationPreviewFromMessage(queryClient, message) {
219
+ queryClient.setQueryData(
220
+ jokkooQueryKeys.conversations,
221
+ (current) => {
222
+ if (!current || current.pages.length === 0) {
223
+ return current;
224
+ }
225
+ let found = false;
226
+ const pages = current.pages.map((page) => {
227
+ const nextData = page.data.map((item) => {
228
+ if (item.id !== message.conversationId) {
229
+ return item;
230
+ }
231
+ found = true;
232
+ return {
233
+ ...item,
234
+ lastMessageText: message.content,
235
+ lastMessageSenderType: message.senderType,
236
+ updatedAt: message.createdAt
237
+ };
238
+ });
239
+ return {
240
+ ...page,
241
+ data: sortConversationsByUpdatedAt(nextData)
242
+ };
243
+ });
244
+ if (!found) {
245
+ return current;
246
+ }
247
+ return {
248
+ ...current,
249
+ pages
250
+ };
251
+ }
252
+ );
253
+ }
254
+ function appendMessageToCache(queryClient, message) {
255
+ queryClient.setQueryData(
256
+ jokkooQueryKeys.messages(message.conversationId),
257
+ (current) => prependMessageToMessagesCache(current, message)
258
+ );
259
+ }
260
+
261
+ // src/hooks/useGlobalRealtimeListener.ts
262
+ function isViewingConversation(isChatOpen, currentScreen, conversationId) {
263
+ return isChatOpen && currentScreen.name === "conversation" && currentScreen.conversationId === conversationId;
264
+ }
265
+ function useGlobalRealtimeListener(options) {
266
+ const { apiClient, realtimeClient, queryClient, isChatOpen, currentScreen } = options;
267
+ const [status, setStatus] = (0, import_react.useState)(realtimeClient.getStatus());
268
+ const [unreadConversationIds, setUnreadConversationIds] = (0, import_react.useState)(
269
+ () => /* @__PURE__ */ new Set()
270
+ );
271
+ const isChatOpenRef = (0, import_react.useRef)(isChatOpen);
272
+ const currentScreenRef = (0, import_react.useRef)(currentScreen);
273
+ isChatOpenRef.current = isChatOpen;
274
+ currentScreenRef.current = currentScreen;
275
+ const unreadCount = unreadConversationIds.size;
276
+ const isConversationUnread = (0, import_react.useCallback)(
277
+ (conversationId) => unreadConversationIds.has(conversationId),
278
+ [unreadConversationIds]
279
+ );
280
+ (0, import_react.useEffect)(() => {
281
+ return realtimeClient.onStatusChange(setStatus);
282
+ }, [realtimeClient]);
283
+ const markConversationRead = (0, import_react.useCallback)(
284
+ (conversationId) => {
285
+ setUnreadConversationIds((current) => {
286
+ if (!current.has(conversationId)) {
287
+ return current;
288
+ }
289
+ const next = new Set(current);
290
+ next.delete(conversationId);
291
+ return next;
292
+ });
293
+ void apiClient.markAsRead(conversationId).catch(() => {
294
+ });
295
+ },
296
+ [apiClient]
297
+ );
298
+ const addUnreadConversation = (0, import_react.useCallback)((conversationId) => {
299
+ setUnreadConversationIds((current) => {
300
+ if (current.has(conversationId)) {
301
+ return current;
302
+ }
303
+ const next = new Set(current);
304
+ next.add(conversationId);
305
+ return next;
306
+ });
307
+ }, []);
308
+ (0, import_react.useEffect)(() => {
309
+ let cancelled = false;
310
+ void apiClient.getUnreadCount().then((response) => {
311
+ if (cancelled) {
312
+ return;
313
+ }
314
+ setUnreadConversationIds(new Set(response.conversationIds));
315
+ });
316
+ return () => {
317
+ cancelled = true;
318
+ };
319
+ }, [apiClient]);
320
+ (0, import_react.useEffect)(() => {
321
+ if (status !== "connected") {
322
+ return;
323
+ }
324
+ const socket = realtimeClient.getSocket();
325
+ if (!socket) {
326
+ return;
327
+ }
328
+ const handleConversationCreated = (payload) => {
329
+ prependConversationToListCache(
330
+ queryClient,
331
+ (0, import_sdk_web.mapRealtimeConversationPayload)(payload.conversation)
332
+ );
333
+ };
334
+ const handleConversationUpdated = (payload) => {
335
+ const conversation = (0, import_sdk_web.mapRealtimeConversationPayload)(payload.conversation);
336
+ upsertConversationInListCache(queryClient, conversation);
337
+ if (conversation.status === "closed") {
338
+ markConversationRead(conversation.id);
339
+ }
340
+ };
341
+ const handleMessageCreated = (payload) => {
342
+ if (!payload?.message) {
343
+ return;
344
+ }
345
+ const message = (0, import_sdk_web.normalizeRealtimeMessage)(payload.message);
346
+ appendMessageToCache(queryClient, message);
347
+ updateConversationPreviewFromMessage(queryClient, message);
348
+ if (message.senderType === "end_user") {
349
+ markConversationRead(message.conversationId);
350
+ return;
351
+ }
352
+ if (isViewingConversation(
353
+ isChatOpenRef.current,
354
+ currentScreenRef.current,
355
+ message.conversationId
356
+ )) {
357
+ markConversationRead(message.conversationId);
358
+ return;
359
+ }
360
+ if ((0, import_sdk_web2.shouldCountMessageAsUnread)(message)) {
361
+ addUnreadConversation(message.conversationId);
362
+ }
363
+ };
364
+ socket.on(import_sdk_web2.REALTIME_EVENTS.messageCreated, handleMessageCreated);
365
+ socket.on(import_sdk_web2.REALTIME_EVENTS.conversationCreated, handleConversationCreated);
366
+ socket.on(import_sdk_web2.REALTIME_EVENTS.conversationUpdated, handleConversationUpdated);
367
+ return () => {
368
+ socket.off(import_sdk_web2.REALTIME_EVENTS.messageCreated, handleMessageCreated);
369
+ socket.off(import_sdk_web2.REALTIME_EVENTS.conversationCreated, handleConversationCreated);
370
+ socket.off(import_sdk_web2.REALTIME_EVENTS.conversationUpdated, handleConversationUpdated);
371
+ };
372
+ }, [addUnreadConversation, markConversationRead, queryClient, realtimeClient, status]);
373
+ return { unreadCount, markConversationRead, isConversationUnread };
374
+ }
375
+
376
+ // src/theme.ts
377
+ var defaultTheme = {
378
+ colors: {
379
+ primary: "#1C1C1C",
380
+ background: "#FFFFFF",
381
+ surface: "#F5F6F8",
382
+ text: "#1C1C1C",
383
+ textMuted: "#8A8A9A",
384
+ border: "#E4E4E8",
385
+ onPrimary: "#FFFFFF",
386
+ chatHeaderBackground: "#1C1C1C",
387
+ chatHeaderText: "#FFFFFF",
388
+ chatHeaderButtonBackground: "#404040",
389
+ chatBackground: "#FFFFFF",
390
+ chatSurface: "#FFFFFF",
391
+ chatText: "#1C1C1C",
392
+ chatTextMuted: "#8A8A9A",
393
+ chatBorder: "#E4E4E8",
394
+ inputBackground: "#F5F6F8",
395
+ inputPlaceholder: "rgba(28, 28, 28, 0.5)",
396
+ userBubbleBackground: "#1C1C1C",
397
+ userBubbleText: "#FFFFFF",
398
+ agentBubbleBackground: "#F5F6F8",
399
+ agentBubbleText: "#1C1C1C",
400
+ statusActiveBackground: "#DBEAFE",
401
+ statusActiveText: "#3B82F6",
402
+ statusEndedBackground: "#D1FAE5",
403
+ statusEndedText: "#10B981",
404
+ discussionEndedBackground: "#ECFDF5",
405
+ resolvedBackground: "rgba(76, 175, 80, 0.08)",
406
+ resolvedText: "#4CAF50",
407
+ unreadIndicator: "#EF4444"
408
+ },
409
+ spacing: {
410
+ xs: 4,
411
+ sm: 8,
412
+ md: 16,
413
+ lg: 24,
414
+ xl: 32
415
+ },
416
+ borderRadius: 12,
417
+ cardBorderRadius: 16,
418
+ badgeBorderRadius: 8,
419
+ bubbleBorderRadius: 20,
420
+ panelBorderRadius: 20,
421
+ typography: {
422
+ headingSize: 24,
423
+ headingWeight: "600",
424
+ headingLetterSpacing: -1,
425
+ titleSize: 20,
426
+ titleWeight: "600",
427
+ smallSize: 14,
428
+ smallWeight: "600",
429
+ bodySize: 14,
430
+ bodyWeight: "400",
431
+ captionSize: 12,
432
+ captionWeight: "400",
433
+ messageSize: 12,
434
+ timestampSize: 10
435
+ }
436
+ };
437
+ function mergeTheme(partial) {
438
+ if (!partial) {
439
+ return defaultTheme;
440
+ }
441
+ return {
442
+ colors: { ...defaultTheme.colors, ...partial.colors },
443
+ spacing: { ...defaultTheme.spacing, ...partial.spacing },
444
+ borderRadius: partial.borderRadius ?? defaultTheme.borderRadius,
445
+ cardBorderRadius: partial.cardBorderRadius ?? defaultTheme.cardBorderRadius,
446
+ badgeBorderRadius: partial.badgeBorderRadius ?? defaultTheme.badgeBorderRadius,
447
+ bubbleBorderRadius: partial.bubbleBorderRadius ?? defaultTheme.bubbleBorderRadius,
448
+ panelBorderRadius: partial.panelBorderRadius ?? defaultTheme.panelBorderRadius,
449
+ typography: { ...defaultTheme.typography, ...partial.typography }
450
+ };
451
+ }
452
+ function themeToCssVars(theme) {
453
+ return {
454
+ "--jk-color-primary": theme.colors.primary,
455
+ "--jk-color-background": theme.colors.background,
456
+ "--jk-color-surface": theme.colors.surface,
457
+ "--jk-color-border": theme.colors.border,
458
+ "--jk-color-text": theme.colors.text,
459
+ "--jk-color-text-secondary": theme.colors.textMuted,
460
+ "--jk-color-on-primary": theme.colors.onPrimary,
461
+ "--jk-color-user-bubble": theme.colors.userBubbleBackground,
462
+ "--jk-color-agent-bubble": theme.colors.agentBubbleBackground,
463
+ "--jk-color-status-blue-bg": theme.colors.statusActiveBackground,
464
+ "--jk-color-status-blue-text": theme.colors.statusActiveText,
465
+ "--jk-color-status-green-bg": theme.colors.statusEndedBackground,
466
+ "--jk-color-status-green-text": theme.colors.statusEndedText,
467
+ "--jk-color-resolved-bg": theme.colors.resolvedBackground,
468
+ "--jk-color-resolved-text": theme.colors.resolvedText,
469
+ "--jk-color-header": theme.colors.chatHeaderBackground,
470
+ "--jk-color-header-text": theme.colors.chatHeaderText,
471
+ "--jk-color-unread": theme.colors.unreadIndicator,
472
+ "--jk-radius-panel": `${theme.panelBorderRadius}px`,
473
+ "--jk-radius-bubble": `${theme.bubbleBorderRadius}px`,
474
+ "--jk-radius-card": `${theme.cardBorderRadius}px`,
475
+ "--jk-radius-badge": `${theme.badgeBorderRadius}px`,
476
+ "--jk-radius-input": `${theme.bubbleBorderRadius}px`
477
+ };
478
+ }
479
+
480
+ // src/context/JokkooProvider.tsx
481
+ var import_jsx_runtime = require("react/jsx-runtime");
482
+ var initialScreen = { name: "requestList" };
483
+ var JokkooContext = (0, import_react2.createContext)(null);
484
+ function createDefaultQueryClient() {
485
+ return new import_react_query.QueryClient({
486
+ defaultOptions: {
487
+ queries: {
488
+ retry: 1,
489
+ refetchOnWindowFocus: false
490
+ }
491
+ }
492
+ });
493
+ }
494
+ function JokkooProvider({
495
+ children,
496
+ theme,
497
+ clientToken,
498
+ userTokenProvider,
499
+ baseUrl,
500
+ locale,
501
+ debug,
502
+ tokenRefreshBufferSeconds,
503
+ queryClient: externalQueryClient
504
+ }) {
505
+ const [isChatOpen, setIsChatOpen] = (0, import_react2.useState)(false);
506
+ const [isExpanded, setIsExpanded] = (0, import_react2.useState)(false);
507
+ const [currentScreen, setCurrentScreen] = (0, import_react2.useState)(initialScreen);
508
+ const resolvedTheme = (0, import_react2.useMemo)(() => mergeTheme(theme), [theme]);
509
+ const themeStyle = (0, import_react2.useMemo)(
510
+ () => themeToCssVars(resolvedTheme),
511
+ [resolvedTheme]
512
+ );
513
+ const tokenProviderRef = (0, import_react2.useRef)(userTokenProvider);
514
+ tokenProviderRef.current = userTokenProvider;
515
+ const internalQueryClientRef = (0, import_react2.useRef)(null);
516
+ if (!externalQueryClient && !internalQueryClientRef.current) {
517
+ internalQueryClientRef.current = createDefaultQueryClient();
518
+ }
519
+ const queryClient = externalQueryClient ?? internalQueryClientRef.current;
520
+ const translations = (0, import_react2.useMemo)(() => (0, import_sdk_web3.getTranslations)(locale), [locale]);
521
+ const apiClient = (0, import_react2.useMemo)(() => {
522
+ import_sdk_web3.Jokkoo.init({
523
+ clientToken,
524
+ baseUrl,
525
+ locale,
526
+ debug,
527
+ tokenRefreshBufferSeconds,
528
+ userTokenProvider: () => tokenProviderRef.current()
529
+ });
530
+ void import_sdk_web3.Jokkoo.getRealtimeClient().connect();
531
+ return new import_sdk_web3.JokkooApiClient(import_sdk_web3.Jokkoo.getConfig(), import_sdk_web3.Jokkoo.getTokenManager());
532
+ }, [clientToken, baseUrl, locale, debug, tokenRefreshBufferSeconds]);
533
+ const realtimeClient = (0, import_react2.useMemo)(() => import_sdk_web3.Jokkoo.getRealtimeClient(), [apiClient]);
534
+ const { unreadCount, markConversationRead, isConversationUnread } = useGlobalRealtimeListener({
535
+ apiClient,
536
+ realtimeClient,
537
+ queryClient,
538
+ isChatOpen,
539
+ currentScreen
540
+ });
541
+ (0, import_react2.useEffect)(() => {
542
+ return () => {
543
+ import_sdk_web3.Jokkoo.destroy();
544
+ };
545
+ }, []);
546
+ const resetNavigation = (0, import_react2.useCallback)(() => {
547
+ setCurrentScreen(initialScreen);
548
+ setIsExpanded(false);
549
+ }, []);
550
+ const openChat = (0, import_react2.useCallback)(() => {
551
+ if (!import_sdk_web3.Jokkoo.isInitialized()) {
552
+ throw new Error(
553
+ "Jokkoo SDK is not initialized. Ensure <JokkooProvider> is mounted with valid config."
554
+ );
555
+ }
556
+ resetNavigation();
557
+ setIsChatOpen(true);
558
+ }, [resetNavigation]);
559
+ const closeChat = (0, import_react2.useCallback)(() => {
560
+ setIsChatOpen(false);
561
+ resetNavigation();
562
+ }, [resetNavigation]);
563
+ const toggleExpanded = (0, import_react2.useCallback)(() => {
564
+ setIsExpanded((current) => !current);
565
+ }, []);
566
+ const navigateTo = (0, import_react2.useCallback)((screen) => {
567
+ setCurrentScreen(screen);
568
+ }, []);
569
+ const goBack = (0, import_react2.useCallback)(() => {
570
+ setCurrentScreen(initialScreen);
571
+ }, []);
572
+ const value = (0, import_react2.useMemo)(
573
+ () => ({
574
+ theme: resolvedTheme,
575
+ themeStyle,
576
+ t: translations,
577
+ locale,
578
+ isChatOpen,
579
+ isExpanded,
580
+ currentScreen,
581
+ apiClient,
582
+ realtimeClient,
583
+ openChat,
584
+ closeChat,
585
+ toggleExpanded,
586
+ navigateTo,
587
+ goBack,
588
+ unreadCount,
589
+ markConversationRead,
590
+ isConversationUnread
591
+ }),
592
+ [
593
+ resolvedTheme,
594
+ themeStyle,
595
+ translations,
596
+ locale,
597
+ isChatOpen,
598
+ isExpanded,
599
+ currentScreen,
600
+ apiClient,
601
+ realtimeClient,
602
+ openChat,
603
+ closeChat,
604
+ toggleExpanded,
605
+ navigateTo,
606
+ goBack,
607
+ unreadCount,
608
+ markConversationRead,
609
+ isConversationUnread
610
+ ]
611
+ );
612
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_query.QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(JokkooContext.Provider, { value, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "jokkoo-widget", style: themeStyle, children }) }) });
613
+ }
614
+ function useJokkooContext() {
615
+ const context = (0, import_react2.useContext)(JokkooContext);
616
+ if (!context) {
617
+ throw new Error("Jokkoo UI components must be wrapped in <JokkooProvider>.");
618
+ }
619
+ return context;
620
+ }
621
+
622
+ // src/hooks/useRealtimeConversations.ts
623
+ var import_react_query2 = require("@tanstack/react-query");
624
+ var import_sdk_web4 = require("@jokkoo/sdk-web");
625
+ var import_react4 = require("react");
626
+
627
+ // src/hooks/useRealtimeStatus.ts
628
+ var import_react3 = require("react");
629
+ function useRealtimeStatus() {
630
+ const { realtimeClient } = useJokkooContext();
631
+ const [status, setStatus] = (0, import_react3.useState)(realtimeClient.getStatus());
632
+ (0, import_react3.useEffect)(() => {
633
+ return realtimeClient.onStatusChange(setStatus);
634
+ }, [realtimeClient]);
635
+ return status;
636
+ }
637
+
638
+ // src/hooks/useRealtimeConversations.ts
639
+ function useRealtimeConversations() {
640
+ const { realtimeClient } = useJokkooContext();
641
+ const queryClient = (0, import_react_query2.useQueryClient)();
642
+ const status = useRealtimeStatus();
643
+ (0, import_react4.useEffect)(() => {
644
+ if (status !== "connected") {
645
+ return;
646
+ }
647
+ const socket = realtimeClient.getSocket();
648
+ if (!socket) {
649
+ return;
650
+ }
651
+ const handleConversationCreated = (payload) => {
652
+ prependConversationToListCache(
653
+ queryClient,
654
+ (0, import_sdk_web.mapRealtimeConversationPayload)(payload.conversation)
655
+ );
656
+ };
657
+ const handleConversationUpdated = (payload) => {
658
+ upsertConversationInListCache(
659
+ queryClient,
660
+ (0, import_sdk_web.mapRealtimeConversationPayload)(payload.conversation)
661
+ );
662
+ };
663
+ const handleMessageCreated = (payload) => {
664
+ const message = (0, import_sdk_web.normalizeRealtimeMessage)(payload.message);
665
+ updateConversationPreviewFromMessage(queryClient, message);
666
+ };
667
+ socket.on(import_sdk_web4.REALTIME_EVENTS.messageCreated, handleMessageCreated);
668
+ socket.on(import_sdk_web4.REALTIME_EVENTS.conversationCreated, handleConversationCreated);
669
+ socket.on(import_sdk_web4.REALTIME_EVENTS.conversationUpdated, handleConversationUpdated);
670
+ return () => {
671
+ socket.off(import_sdk_web4.REALTIME_EVENTS.messageCreated, handleMessageCreated);
672
+ socket.off(import_sdk_web4.REALTIME_EVENTS.conversationCreated, handleConversationCreated);
673
+ socket.off(import_sdk_web4.REALTIME_EVENTS.conversationUpdated, handleConversationUpdated);
674
+ };
675
+ }, [queryClient, realtimeClient, status]);
676
+ }
677
+
678
+ // src/components/ConversationScreen.tsx
679
+ var import_react_query7 = require("@tanstack/react-query");
680
+ var import_sdk_web16 = require("@jokkoo/sdk-web");
681
+ var import_react16 = require("react");
682
+
683
+ // src/hooks/useClosedConversationPolicy.ts
684
+ var import_react_query3 = require("@tanstack/react-query");
685
+ function useClosedConversationPolicy() {
686
+ const { apiClient } = useJokkooContext();
687
+ return (0, import_react_query3.useQuery)({
688
+ queryKey: jokkooQueryKeys.conversationPolicies,
689
+ queryFn: () => apiClient.getConversationPolicies(),
690
+ staleTime: 5 * 60 * 1e3
691
+ });
692
+ }
693
+
694
+ // src/hooks/useConversations.ts
695
+ var import_react_query4 = require("@tanstack/react-query");
696
+ var import_react5 = require("react");
697
+ var DEFAULT_PAGE_SIZE2 = 20;
698
+ function useConversations(options) {
699
+ const { apiClient } = useJokkooContext();
700
+ const pageSize = options?.pageSize ?? DEFAULT_PAGE_SIZE2;
701
+ const query = (0, import_react_query4.useInfiniteQuery)({
702
+ queryKey: jokkooQueryKeys.conversations,
703
+ initialPageParam: 0,
704
+ staleTime: 3e4,
705
+ queryFn: ({ pageParam }) => apiClient.getConversations(pageSize, pageParam),
706
+ getNextPageParam: (lastPage) => {
707
+ const nextOffset = lastPage.offset + lastPage.limit;
708
+ return nextOffset < lastPage.total ? nextOffset : void 0;
709
+ }
710
+ });
711
+ const conversations = (0, import_react5.useMemo)(
712
+ () => query.data?.pages.flatMap((page) => page.data) ?? [],
713
+ [query.data]
714
+ );
715
+ return {
716
+ conversations,
717
+ fetchNextPage: () => {
718
+ void query.fetchNextPage();
719
+ },
720
+ hasNextPage: query.hasNextPage,
721
+ isFetchingNextPage: query.isFetchingNextPage,
722
+ isLoading: query.isLoading,
723
+ isError: query.isError,
724
+ refetch: () => {
725
+ void query.refetch();
726
+ }
727
+ };
728
+ }
729
+
730
+ // src/hooks/useFileUpload.ts
731
+ var import_sdk_web5 = require("@jokkoo/sdk-web");
732
+ var import_react6 = require("react");
733
+ var ACCEPT = "image/jpeg,image/png,image/gif,image/webp,video/mp4,video/quicktime,video/webm,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document";
734
+ function useFileUpload() {
735
+ const { apiClient, t } = useJokkooContext();
736
+ const [pendingAttachments, setPendingAttachments] = (0, import_react6.useState)([]);
737
+ const inputRef = (0, import_react6.useRef)(null);
738
+ const ensureInput = (0, import_react6.useCallback)(() => {
739
+ if (inputRef.current) {
740
+ return inputRef.current;
741
+ }
742
+ const input = document.createElement("input");
743
+ input.type = "file";
744
+ input.accept = ACCEPT;
745
+ input.multiple = true;
746
+ input.style.display = "none";
747
+ document.body.appendChild(input);
748
+ inputRef.current = input;
749
+ return input;
750
+ }, []);
751
+ const uploadPendingAttachment = (0, import_react6.useCallback)(
752
+ async (attachment) => {
753
+ setPendingAttachments(
754
+ (current) => current.map(
755
+ (item) => item.id === attachment.id ? { ...item, status: "uploading", progress: 5 } : item
756
+ )
757
+ );
758
+ try {
759
+ const uploaded = await (0, import_sdk_web5.uploadAttachmentFile)(apiClient, attachment, (progress, status) => {
760
+ setPendingAttachments(
761
+ (current) => current.map(
762
+ (item) => item.id === attachment.id ? { ...item, progress, status } : item
763
+ )
764
+ );
765
+ });
766
+ setPendingAttachments(
767
+ (current) => current.map((item) => item.id === attachment.id ? uploaded : item)
768
+ );
769
+ } catch {
770
+ setPendingAttachments(
771
+ (current) => current.map(
772
+ (item) => item.id === attachment.id ? {
773
+ ...item,
774
+ status: "error",
775
+ progress: 0,
776
+ error: t.uploadFailed
777
+ } : item
778
+ )
779
+ );
780
+ }
781
+ },
782
+ [apiClient, t.uploadFailed]
783
+ );
784
+ const appendAttachment = (0, import_react6.useCallback)(
785
+ async (attachment) => {
786
+ if (!(0, import_sdk_web5.canAddMoreAttachments)(pendingAttachments.length)) {
787
+ window.alert(t.maxAttachmentsReached);
788
+ return;
789
+ }
790
+ setPendingAttachments((current) => [...current, attachment]);
791
+ if (attachment.status === "pending") {
792
+ await uploadPendingAttachment(attachment);
793
+ }
794
+ },
795
+ [pendingAttachments.length, t.maxAttachmentsReached, uploadPendingAttachment]
796
+ );
797
+ const pickAttachments = (0, import_react6.useCallback)(() => {
798
+ const input = ensureInput();
799
+ input.onchange = () => {
800
+ const files = Array.from(input.files ?? []);
801
+ input.value = "";
802
+ for (const file of files) {
803
+ const uri = URL.createObjectURL(file);
804
+ const attachment = (0, import_sdk_web5.createPendingAttachment)({
805
+ fileName: file.name,
806
+ mimeType: file.type || "application/octet-stream",
807
+ uri,
808
+ size: file.size
809
+ });
810
+ void appendAttachment(attachment);
811
+ }
812
+ };
813
+ input.click();
814
+ }, [appendAttachment, ensureInput]);
815
+ const retryAttachment = (0, import_react6.useCallback)(
816
+ (id) => {
817
+ const attachment = pendingAttachments.find((item) => item.id === id);
818
+ if (!attachment || attachment.status !== "error") {
819
+ return;
820
+ }
821
+ const resetAttachment = {
822
+ ...attachment,
823
+ status: "pending",
824
+ progress: 0,
825
+ error: void 0
826
+ };
827
+ setPendingAttachments(
828
+ (current) => current.map((item) => item.id === id ? resetAttachment : item)
829
+ );
830
+ void uploadPendingAttachment(resetAttachment);
831
+ },
832
+ [pendingAttachments, uploadPendingAttachment]
833
+ );
834
+ const removeAttachment = (0, import_react6.useCallback)((id) => {
835
+ setPendingAttachments((current) => {
836
+ const target = current.find((item) => item.id === id);
837
+ if (target?.uri.startsWith("blob:")) {
838
+ URL.revokeObjectURL(target.uri);
839
+ }
840
+ return current.filter((item) => item.id !== id);
841
+ });
842
+ }, []);
843
+ const clearAttachments = (0, import_react6.useCallback)(() => {
844
+ setPendingAttachments((current) => {
845
+ for (const item of current) {
846
+ if (item.uri.startsWith("blob:")) {
847
+ URL.revokeObjectURL(item.uri);
848
+ }
849
+ }
850
+ return [];
851
+ });
852
+ }, []);
853
+ const confirmedFileIds = pendingAttachments.filter((item) => item.status === "confirmed" && item.confirmedFileId).map((item) => item.confirmedFileId);
854
+ const isUploading = pendingAttachments.some(
855
+ (item) => item.status === "uploading" || item.status === "confirming" || item.status === "pending"
856
+ );
857
+ return {
858
+ pendingAttachments,
859
+ isUploading,
860
+ confirmedFileIds,
861
+ pickAttachments,
862
+ removeAttachment,
863
+ retryAttachment,
864
+ clearAttachments
865
+ };
866
+ }
867
+
868
+ // src/hooks/useMessages.ts
869
+ var import_react_query5 = require("@tanstack/react-query");
870
+ var import_sdk_web6 = require("@jokkoo/sdk-web");
871
+ var import_react7 = require("react");
872
+ var DEFAULT_PAGE_SIZE3 = 50;
873
+ function useMessages(conversationId, options) {
874
+ const { apiClient } = useJokkooContext();
875
+ const pageSize = options?.pageSize ?? DEFAULT_PAGE_SIZE3;
876
+ const enabled = (options?.enabled ?? true) && conversationId !== import_sdk_web6.NEW_CONVERSATION_ID;
877
+ const query = (0, import_react_query5.useInfiniteQuery)({
878
+ queryKey: jokkooQueryKeys.messages(conversationId),
879
+ initialPageParam: "latest",
880
+ enabled,
881
+ staleTime: 0,
882
+ queryFn: ({ pageParam }) => (0, import_sdk_web6.fetchMessagesPage)(apiClient, conversationId, pageSize, pageParam),
883
+ getNextPageParam: (lastPage) => (0, import_sdk_web6.getOlderMessagesPageParam)(lastPage)
884
+ });
885
+ const messages = (0, import_react7.useMemo)(
886
+ () => query.data?.pages.flatMap((page) => page.data) ?? [],
887
+ [query.data]
888
+ );
889
+ return {
890
+ messages,
891
+ fetchNextPage: () => {
892
+ void query.fetchNextPage();
893
+ },
894
+ hasNextPage: query.hasNextPage,
895
+ isFetchingNextPage: query.isFetchingNextPage,
896
+ isLoading: query.isLoading,
897
+ isError: query.isError
898
+ };
899
+ }
900
+
901
+ // src/hooks/useRealtimeMessages.ts
902
+ var import_react_query6 = require("@tanstack/react-query");
903
+ var import_sdk_web7 = require("@jokkoo/sdk-web");
904
+ var import_react8 = require("react");
905
+ function useRealtimeMessages(conversationId) {
906
+ const { realtimeClient } = useJokkooContext();
907
+ const queryClient = (0, import_react_query6.useQueryClient)();
908
+ const status = useRealtimeStatus();
909
+ const [pendingNewMessages, setPendingNewMessages] = (0, import_react8.useState)(0);
910
+ const isAtBottomRef = (0, import_react8.useRef)(true);
911
+ const conversationIdRef = (0, import_react8.useRef)(conversationId);
912
+ conversationIdRef.current = conversationId;
913
+ const clearPendingNewMessages = (0, import_react8.useCallback)(() => {
914
+ setPendingNewMessages(0);
915
+ }, []);
916
+ const notifyScrolledToBottom = (0, import_react8.useCallback)(() => {
917
+ isAtBottomRef.current = true;
918
+ setPendingNewMessages(0);
919
+ }, []);
920
+ const notifyScrolledAwayFromBottom = (0, import_react8.useCallback)(() => {
921
+ isAtBottomRef.current = false;
922
+ }, []);
923
+ (0, import_react8.useEffect)(() => {
924
+ setPendingNewMessages(0);
925
+ isAtBottomRef.current = true;
926
+ }, [conversationId]);
927
+ (0, import_react8.useEffect)(() => {
928
+ if (status !== "connected" || conversationId === import_sdk_web7.NEW_CONVERSATION_ID) {
929
+ return;
930
+ }
931
+ const socket = realtimeClient.getSocket();
932
+ if (!socket) {
933
+ return;
934
+ }
935
+ const handleMessageCreated = (payload) => {
936
+ if (!payload?.message) {
937
+ return;
938
+ }
939
+ const message = (0, import_sdk_web.normalizeRealtimeMessage)(payload.message);
940
+ if (message.conversationId !== conversationIdRef.current) {
941
+ return;
942
+ }
943
+ appendMessageToCache(queryClient, message);
944
+ updateConversationPreviewFromMessage(queryClient, message);
945
+ if ((0, import_sdk_web7.isSatisfactionRequestMessage)(message)) {
946
+ updateConversationStatusInListCache(queryClient, message.conversationId, "pending_closure");
947
+ }
948
+ if (message.senderType === "agent" || message.senderType === "system") {
949
+ if (!isAtBottomRef.current) {
950
+ setPendingNewMessages((count) => count + 1);
951
+ }
952
+ }
953
+ };
954
+ socket.on(import_sdk_web7.REALTIME_EVENTS.messageCreated, handleMessageCreated);
955
+ return () => {
956
+ socket.off(import_sdk_web7.REALTIME_EVENTS.messageCreated, handleMessageCreated);
957
+ };
958
+ }, [conversationId, queryClient, realtimeClient, status]);
959
+ return {
960
+ pendingNewMessages,
961
+ clearPendingNewMessages,
962
+ notifyScrolledToBottom,
963
+ notifyScrolledAwayFromBottom
964
+ };
965
+ }
966
+
967
+ // src/hooks/useVoiceRecorder.ts
968
+ var import_sdk_web8 = require("@jokkoo/sdk-web");
969
+ var import_react9 = require("react");
970
+ var MIN_VOICE_DURATION_MS = 500;
971
+ var METERING_SAMPLE_INTERVAL_MS = 55;
972
+ var VOICE_METERING_HISTORY_SIZE = 32;
973
+ function pickMimeType() {
974
+ if (typeof MediaRecorder === "undefined") {
975
+ return "audio/webm";
976
+ }
977
+ if (MediaRecorder.isTypeSupported("audio/webm;codecs=opus")) {
978
+ return "audio/webm;codecs=opus";
979
+ }
980
+ if (MediaRecorder.isTypeSupported("audio/webm")) {
981
+ return "audio/webm";
982
+ }
983
+ if (MediaRecorder.isTypeSupported("audio/mp4")) {
984
+ return "audio/mp4";
985
+ }
986
+ return "audio/webm";
987
+ }
988
+ function useVoiceRecorder() {
989
+ const { apiClient, t } = useJokkooContext();
990
+ const [state, setState] = (0, import_react9.useState)("idle");
991
+ const [durationMs, setDurationMs] = (0, import_react9.useState)(0);
992
+ const [meteringLevels, setMeteringLevels] = (0, import_react9.useState)(() => (0, import_sdk_web8.createIdleMeteringLevels)());
993
+ const [uploadProgress, setUploadProgress] = (0, import_react9.useState)(0);
994
+ const [confirmedFileId, setConfirmedFileId] = (0, import_react9.useState)(null);
995
+ const [audioDurationMs, setAudioDurationMs] = (0, import_react9.useState)(0);
996
+ const mediaRecorderRef = (0, import_react9.useRef)(null);
997
+ const mediaStreamRef = (0, import_react9.useRef)(null);
998
+ const chunksRef = (0, import_react9.useRef)([]);
999
+ const mimeTypeRef = (0, import_react9.useRef)("audio/webm");
1000
+ const durationStartedAtRef = (0, import_react9.useRef)(null);
1001
+ const durationTimerRef = (0, import_react9.useRef)(null);
1002
+ const analyserRef = (0, import_react9.useRef)(null);
1003
+ const audioContextRef = (0, import_react9.useRef)(null);
1004
+ const meteringTimerRef = (0, import_react9.useRef)(null);
1005
+ const smoothedMeteringRef = (0, import_react9.useRef)((0, import_sdk_web8.createIdleMeteringLevels)()[0]);
1006
+ const sampleIndexRef = (0, import_react9.useRef)(0);
1007
+ const clearTimers = (0, import_react9.useCallback)(() => {
1008
+ if (durationTimerRef.current) {
1009
+ clearInterval(durationTimerRef.current);
1010
+ durationTimerRef.current = null;
1011
+ }
1012
+ if (meteringTimerRef.current) {
1013
+ clearInterval(meteringTimerRef.current);
1014
+ meteringTimerRef.current = null;
1015
+ }
1016
+ }, []);
1017
+ const stopTracks = (0, import_react9.useCallback)(() => {
1018
+ mediaStreamRef.current?.getTracks().forEach((track) => track.stop());
1019
+ mediaStreamRef.current = null;
1020
+ void audioContextRef.current?.close().catch(() => void 0);
1021
+ audioContextRef.current = null;
1022
+ analyserRef.current = null;
1023
+ mediaRecorderRef.current = null;
1024
+ }, []);
1025
+ const reset = (0, import_react9.useCallback)(() => {
1026
+ clearTimers();
1027
+ stopTracks();
1028
+ chunksRef.current = [];
1029
+ setState("idle");
1030
+ setDurationMs(0);
1031
+ setMeteringLevels((0, import_sdk_web8.createIdleMeteringLevels)());
1032
+ setUploadProgress(0);
1033
+ setConfirmedFileId(null);
1034
+ setAudioDurationMs(0);
1035
+ durationStartedAtRef.current = null;
1036
+ smoothedMeteringRef.current = (0, import_sdk_web8.createIdleMeteringLevels)()[0];
1037
+ sampleIndexRef.current = 0;
1038
+ }, [clearTimers, stopTracks]);
1039
+ (0, import_react9.useEffect)(() => {
1040
+ return () => {
1041
+ clearTimers();
1042
+ stopTracks();
1043
+ };
1044
+ }, [clearTimers, stopTracks]);
1045
+ const startRecording = (0, import_react9.useCallback)(async () => {
1046
+ if (state === "recording" || state === "uploading") {
1047
+ return;
1048
+ }
1049
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
1050
+ window.alert(t.microphonePermissionDenied);
1051
+ return;
1052
+ }
1053
+ try {
1054
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
1055
+ mediaStreamRef.current = stream;
1056
+ mimeTypeRef.current = pickMimeType();
1057
+ const audioContext = new AudioContext();
1058
+ audioContextRef.current = audioContext;
1059
+ const source = audioContext.createMediaStreamSource(stream);
1060
+ const analyser = audioContext.createAnalyser();
1061
+ analyser.fftSize = 256;
1062
+ source.connect(analyser);
1063
+ analyserRef.current = analyser;
1064
+ const recorder = new MediaRecorder(stream, { mimeType: mimeTypeRef.current });
1065
+ mediaRecorderRef.current = recorder;
1066
+ chunksRef.current = [];
1067
+ recorder.ondataavailable = (event) => {
1068
+ if (event.data.size > 0) {
1069
+ chunksRef.current.push(event.data);
1070
+ }
1071
+ };
1072
+ setDurationMs(0);
1073
+ setMeteringLevels((0, import_sdk_web8.createIdleMeteringLevels)());
1074
+ setUploadProgress(0);
1075
+ setConfirmedFileId(null);
1076
+ setAudioDurationMs(0);
1077
+ durationStartedAtRef.current = Date.now();
1078
+ smoothedMeteringRef.current = (0, import_sdk_web8.createIdleMeteringLevels)()[0];
1079
+ sampleIndexRef.current = 0;
1080
+ recorder.start(100);
1081
+ setState("recording");
1082
+ durationTimerRef.current = setInterval(() => {
1083
+ if (durationStartedAtRef.current) {
1084
+ setDurationMs(Date.now() - durationStartedAtRef.current);
1085
+ }
1086
+ }, 200);
1087
+ meteringTimerRef.current = setInterval(() => {
1088
+ const analyserNode = analyserRef.current;
1089
+ if (!analyserNode) {
1090
+ return;
1091
+ }
1092
+ const data = new Uint8Array(analyserNode.frequencyBinCount);
1093
+ analyserNode.getByteTimeDomainData(data);
1094
+ let sum = 0;
1095
+ for (let i = 0; i < data.length; i += 1) {
1096
+ const centered = (data[i] - 128) / 128;
1097
+ sum += centered * centered;
1098
+ }
1099
+ const rms = Math.sqrt(sum / data.length);
1100
+ const rawLevel = Math.max(0.14, Math.min(1, rms * 4));
1101
+ smoothedMeteringRef.current = (0, import_sdk_web8.smoothMeteringSample)(smoothedMeteringRef.current, rawLevel);
1102
+ const variedLevel = (0, import_sdk_web8.applyLiveBarVariation)(
1103
+ smoothedMeteringRef.current,
1104
+ sampleIndexRef.current
1105
+ );
1106
+ sampleIndexRef.current += 1;
1107
+ setMeteringLevels((previous) => {
1108
+ const shifted = (0, import_sdk_web8.shiftMeteringLevel)(previous, variedLevel);
1109
+ const sized = shifted.length >= VOICE_METERING_HISTORY_SIZE ? shifted.slice(shifted.length - VOICE_METERING_HISTORY_SIZE) : [
1110
+ ...(0, import_sdk_web8.createIdleMeteringLevels)(VOICE_METERING_HISTORY_SIZE - shifted.length),
1111
+ ...shifted
1112
+ ];
1113
+ return (0, import_sdk_web8.blendLiveWaveformLevels)(sized);
1114
+ });
1115
+ }, METERING_SAMPLE_INTERVAL_MS);
1116
+ } catch {
1117
+ window.alert(t.microphonePermissionDenied);
1118
+ reset();
1119
+ }
1120
+ }, [reset, state, t.microphonePermissionDenied]);
1121
+ const cancelRecording = (0, import_react9.useCallback)(async () => {
1122
+ if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
1123
+ mediaRecorderRef.current.stop();
1124
+ }
1125
+ reset();
1126
+ }, [reset]);
1127
+ const stopAndUpload = (0, import_react9.useCallback)(async () => {
1128
+ if (state !== "recording" || !mediaRecorderRef.current) {
1129
+ return null;
1130
+ }
1131
+ const recorder = mediaRecorderRef.current;
1132
+ const elapsedMs = durationStartedAtRef.current ? Date.now() - durationStartedAtRef.current : durationMs;
1133
+ clearTimers();
1134
+ const blob = await new Promise((resolve) => {
1135
+ recorder.onstop = () => {
1136
+ const type = mimeTypeRef.current.split(";")[0] ?? "audio/webm";
1137
+ resolve(new Blob(chunksRef.current, { type }));
1138
+ };
1139
+ recorder.stop();
1140
+ });
1141
+ stopTracks();
1142
+ if (!blob || elapsedMs < MIN_VOICE_DURATION_MS) {
1143
+ window.alert(t.voiceMessageTooShort);
1144
+ reset();
1145
+ return null;
1146
+ }
1147
+ setAudioDurationMs(elapsedMs);
1148
+ setState("uploading");
1149
+ setUploadProgress(5);
1150
+ const extension = blob.type.includes("mp4") ? "m4a" : "webm";
1151
+ try {
1152
+ const fileId = await (0, import_sdk_web8.uploadVoiceBlob)(
1153
+ apiClient,
1154
+ blob,
1155
+ (0, import_sdk_web8.createVoiceMessageFileName)(extension),
1156
+ blob.type || "audio/webm",
1157
+ setUploadProgress
1158
+ );
1159
+ setConfirmedFileId(fileId);
1160
+ setState("confirmed");
1161
+ return {
1162
+ fileId,
1163
+ audioDurationMs: elapsedMs
1164
+ };
1165
+ } catch {
1166
+ window.alert(t.uploadFailed);
1167
+ reset();
1168
+ return null;
1169
+ }
1170
+ }, [
1171
+ apiClient,
1172
+ clearTimers,
1173
+ durationMs,
1174
+ reset,
1175
+ state,
1176
+ stopTracks,
1177
+ t.uploadFailed,
1178
+ t.voiceMessageTooShort
1179
+ ]);
1180
+ return {
1181
+ state,
1182
+ durationMs,
1183
+ meteringLevels,
1184
+ uploadProgress,
1185
+ confirmedFileId,
1186
+ audioDurationMs,
1187
+ isRecordingActive: state === "recording" || state === "uploading",
1188
+ startRecording,
1189
+ cancelRecording,
1190
+ stopAndUpload,
1191
+ reset
1192
+ };
1193
+ }
1194
+
1195
+ // src/icons/index.tsx
1196
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1197
+ function baseStyle(size, color) {
1198
+ return { width: size, height: size, color, display: "block", flexShrink: 0 };
1199
+ }
1200
+ function ArrowLeftIcon({ color = "currentColor", size = 20, ...rest }) {
1201
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", style: baseStyle(size, color), "aria-hidden": true, ...rest, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1202
+ "path",
1203
+ {
1204
+ d: "M15 18l-6-6 6-6",
1205
+ stroke: "currentColor",
1206
+ strokeWidth: "2",
1207
+ strokeLinecap: "round",
1208
+ strokeLinejoin: "round"
1209
+ }
1210
+ ) });
1211
+ }
1212
+ function CloseIcon({ color = "currentColor", size = 16, ...rest }) {
1213
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", style: baseStyle(size, color), "aria-hidden": true, ...rest, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1214
+ "path",
1215
+ {
1216
+ d: "M18 6L6 18M6 6l12 12",
1217
+ stroke: "currentColor",
1218
+ strokeWidth: "2",
1219
+ strokeLinecap: "round"
1220
+ }
1221
+ ) });
1222
+ }
1223
+ function ExpandIcon({ color = "currentColor", size = 16, ...rest }) {
1224
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", style: baseStyle(size, color), "aria-hidden": true, ...rest, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1225
+ "path",
1226
+ {
1227
+ d: "M9 3H3v6M15 3h6v6M9 21H3v-6M21 15v6h-6",
1228
+ stroke: "currentColor",
1229
+ strokeWidth: "2",
1230
+ strokeLinecap: "round",
1231
+ strokeLinejoin: "round"
1232
+ }
1233
+ ) });
1234
+ }
1235
+ function PaperclipIcon({ color = "currentColor", size = 16, ...rest }) {
1236
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", style: baseStyle(size, color), "aria-hidden": true, ...rest, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1237
+ "path",
1238
+ {
1239
+ d: "M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48",
1240
+ stroke: "currentColor",
1241
+ strokeWidth: "2",
1242
+ strokeLinecap: "round",
1243
+ strokeLinejoin: "round"
1244
+ }
1245
+ ) });
1246
+ }
1247
+ function MicIcon({ color = "currentColor", size = 24, ...rest }) {
1248
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", style: baseStyle(size, color), "aria-hidden": true, ...rest, children: [
1249
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "9", y: "2", width: "6", height: "12", rx: "3", stroke: "currentColor", strokeWidth: "2" }),
1250
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1251
+ "path",
1252
+ {
1253
+ d: "M5 10a7 7 0 0014 0M12 17v4M8 21h8",
1254
+ stroke: "currentColor",
1255
+ strokeWidth: "2",
1256
+ strokeLinecap: "round"
1257
+ }
1258
+ )
1259
+ ] });
1260
+ }
1261
+ function ChatIcon({ color = "currentColor", size = 20, ...rest }) {
1262
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", style: baseStyle(size, color), "aria-hidden": true, ...rest, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1263
+ "path",
1264
+ {
1265
+ d: "M21 12a8.5 8.5 0 01-8.5 8.5H5l-2 2V12A8.5 8.5 0 0111.5 3.5 8.5 8.5 0 0121 12z",
1266
+ stroke: "currentColor",
1267
+ strokeWidth: "2",
1268
+ strokeLinejoin: "round"
1269
+ }
1270
+ ) });
1271
+ }
1272
+ function ClockIcon({ color = "currentColor", size = 11, ...rest }) {
1273
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", style: baseStyle(size, color), "aria-hidden": true, ...rest, children: [
1274
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "9", stroke: "currentColor", strokeWidth: "2" }),
1275
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 7v5l3 2", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })
1276
+ ] });
1277
+ }
1278
+ function CheckCircleIcon({ color = "currentColor", size = 14, ...rest }) {
1279
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { viewBox: "0 0 24 24", fill: "none", style: baseStyle(size, color), "aria-hidden": true, ...rest, children: [
1280
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "9", stroke: "currentColor", strokeWidth: "2" }),
1281
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1282
+ "path",
1283
+ {
1284
+ d: "M8 12l2.5 2.5L16 9",
1285
+ stroke: "currentColor",
1286
+ strokeWidth: "2",
1287
+ strokeLinecap: "round",
1288
+ strokeLinejoin: "round"
1289
+ }
1290
+ )
1291
+ ] });
1292
+ }
1293
+ function SendIcon({ color = "currentColor", size = 22, ...rest }) {
1294
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", style: baseStyle(size, color), "aria-hidden": true, ...rest, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1295
+ "path",
1296
+ {
1297
+ d: "M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z",
1298
+ stroke: "currentColor",
1299
+ strokeWidth: "2",
1300
+ strokeLinecap: "round",
1301
+ strokeLinejoin: "round"
1302
+ }
1303
+ ) });
1304
+ }
1305
+ function PlayIcon({ color = "currentColor", size = 16, ...rest }) {
1306
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { viewBox: "0 0 24 24", fill: "currentColor", style: baseStyle(size, color), "aria-hidden": true, ...rest, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8 5v14l11-7L8 5z" }) });
1307
+ }
1308
+ function PauseIcon({ color = "currentColor", size = 16, ...rest }) {
1309
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { viewBox: "0 0 24 24", fill: "currentColor", style: baseStyle(size, color), "aria-hidden": true, ...rest, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M6 5h4v14H6V5zm8 0h4v14h-4V5z" }) });
1310
+ }
1311
+ function ArrowDownIcon({ color = "currentColor", size = 20, ...rest }) {
1312
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", style: baseStyle(size, color), "aria-hidden": true, ...rest, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1313
+ "path",
1314
+ {
1315
+ d: "M12 5v14M5 12l7 7 7-7",
1316
+ stroke: "currentColor",
1317
+ strokeWidth: "2",
1318
+ strokeLinecap: "round",
1319
+ strokeLinejoin: "round"
1320
+ }
1321
+ ) });
1322
+ }
1323
+
1324
+ // src/lib/status-badge.ts
1325
+ function getStatusBadgeInfo(status, t) {
1326
+ switch (status) {
1327
+ case "pending":
1328
+ return { variant: "new", label: t.statusNew };
1329
+ case "open":
1330
+ case "snoozed":
1331
+ return { variant: "in-progress", label: t.statusInProgress };
1332
+ case "pending_closure":
1333
+ return { variant: "resolved", label: t.statusResolved };
1334
+ case "closed":
1335
+ return { variant: "ended", label: t.statusEnded };
1336
+ default:
1337
+ return { variant: "in-progress", label: t.statusInProgress };
1338
+ }
1339
+ }
1340
+
1341
+ // src/components/StatusBadge.tsx
1342
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1343
+ function StatusBadge({ status, t }) {
1344
+ const info = getStatusBadgeInfo(status, t);
1345
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: `jk-status-badge jk-status-badge--${info.variant}`, children: info.label });
1346
+ }
1347
+
1348
+ // src/components/ConversationHeader.tsx
1349
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1350
+ function ConversationHeader({ title, status, t, onBack }) {
1351
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "jk-conversation-header", children: [
1352
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1353
+ "button",
1354
+ {
1355
+ type: "button",
1356
+ className: "jk-icon-btn jk-icon-btn--dark",
1357
+ "aria-label": "Back",
1358
+ onClick: onBack,
1359
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ArrowLeftIcon, { color: "currentColor", size: 16 })
1360
+ }
1361
+ ),
1362
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h2", { className: "jk-conversation-header__title", children: title }),
1363
+ status ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(StatusBadge, { status, t }) : null
1364
+ ] });
1365
+ }
1366
+
1367
+ // src/components/MessageInput.tsx
1368
+ var import_sdk_web10 = require("@jokkoo/sdk-web");
1369
+
1370
+ // src/components/PendingAttachmentList.tsx
1371
+ var import_sdk_web9 = require("@jokkoo/sdk-web");
1372
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1373
+ function getStatusLabel(attachment, t) {
1374
+ switch (attachment.status) {
1375
+ case "uploading":
1376
+ case "confirming":
1377
+ return `${t.uploadingAttachment} ${attachment.progress}%`;
1378
+ case "confirmed":
1379
+ return t.attachmentReady;
1380
+ case "error":
1381
+ return attachment.error ?? t.uploadFailed;
1382
+ default:
1383
+ return t.uploadingAttachment;
1384
+ }
1385
+ }
1386
+ function PendingAttachmentList({
1387
+ attachments,
1388
+ t,
1389
+ onRemove,
1390
+ onRetry
1391
+ }) {
1392
+ if (attachments.length === 0) {
1393
+ return null;
1394
+ }
1395
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "jk-pending-list", children: attachments.map((attachment) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "jk-pending-item", children: [
1396
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "jk-pending-item__thumb", children: (0, import_sdk_web9.isImageMimeType)(attachment.mimeType) ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("img", { src: attachment.uri, alt: attachment.fileName }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: attachment.mimeType.startsWith("video/") ? "\u25B6" : "\u{1F4C4}" }) }),
1397
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "jk-pending-item__details", children: [
1398
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "jk-pending-item__name", children: attachment.fileName }),
1399
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1400
+ "div",
1401
+ {
1402
+ className: `jk-pending-item__status${attachment.status === "error" ? " jk-pending-item__status--error" : attachment.status === "confirmed" ? " jk-pending-item__status--ready" : ""}`,
1403
+ children: getStatusLabel(attachment, t)
1404
+ }
1405
+ ),
1406
+ attachment.status === "uploading" || attachment.status === "confirming" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "jk-pending-item__progress", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1407
+ "div",
1408
+ {
1409
+ className: "jk-pending-item__progress-fill",
1410
+ style: { width: `${Math.max(attachment.progress, 8)}%` }
1411
+ }
1412
+ ) }) : null
1413
+ ] }),
1414
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "jk-pending-item__actions", children: [
1415
+ attachment.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", onClick: () => onRetry(attachment.id), children: t.retryUpload }) : attachment.status === "uploading" || attachment.status === "confirming" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "jk-spinner", style: { width: 16, height: 16 } }) : null,
1416
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", onClick: () => onRemove(attachment.id), children: t.cancel })
1417
+ ] })
1418
+ ] }, attachment.id)) });
1419
+ }
1420
+
1421
+ // src/components/MessageInput.tsx
1422
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1423
+ function MessageInput({
1424
+ t,
1425
+ apiClient,
1426
+ value,
1427
+ onChange,
1428
+ pendingAttachments,
1429
+ onRemoveAttachment,
1430
+ onRetryAttachment,
1431
+ onPickAttachments,
1432
+ canSend,
1433
+ canRecordVoice,
1434
+ onSend,
1435
+ onStartRecording
1436
+ }) {
1437
+ const isOverLimit = value.length > import_sdk_web10.MESSAGE_CONTENT_MAX_LENGTH;
1438
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "jk-input-bar", children: [
1439
+ pendingAttachments.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1440
+ PendingAttachmentList,
1441
+ {
1442
+ attachments: pendingAttachments,
1443
+ apiClient,
1444
+ t,
1445
+ onRemove: onRemoveAttachment,
1446
+ onRetry: onRetryAttachment
1447
+ }
1448
+ ) : null,
1449
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "jk-input-shell", children: [
1450
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1451
+ "button",
1452
+ {
1453
+ type: "button",
1454
+ className: "jk-input-shell__icon",
1455
+ "aria-label": t.attachFile,
1456
+ onClick: onPickAttachments,
1457
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PaperclipIcon, { color: "currentColor", size: 16 })
1458
+ }
1459
+ ),
1460
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "jk-input-shell__field", children: [
1461
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1462
+ "textarea",
1463
+ {
1464
+ rows: 1,
1465
+ maxLength: import_sdk_web10.MESSAGE_CONTENT_MAX_LENGTH,
1466
+ placeholder: t.typeYourMessage,
1467
+ value,
1468
+ onChange: (event) => onChange(event.target.value),
1469
+ onKeyDown: (event) => {
1470
+ if (event.key === "Enter" && !event.shiftKey) {
1471
+ event.preventDefault();
1472
+ if (canSend) {
1473
+ onSend();
1474
+ }
1475
+ }
1476
+ }
1477
+ }
1478
+ ),
1479
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1480
+ "div",
1481
+ {
1482
+ className: `jk-input-shell__counter${isOverLimit ? " jk-input-shell__counter--error" : ""}`,
1483
+ children: [
1484
+ value.length,
1485
+ "/",
1486
+ import_sdk_web10.MESSAGE_CONTENT_MAX_LENGTH
1487
+ ]
1488
+ }
1489
+ )
1490
+ ] }),
1491
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1492
+ "button",
1493
+ {
1494
+ type: "button",
1495
+ className: "jk-input-shell__icon",
1496
+ "aria-label": canSend ? t.sendMessage : t.recordVoiceMessage,
1497
+ disabled: !canSend && !canRecordVoice,
1498
+ onClick: () => {
1499
+ if (canSend) {
1500
+ onSend();
1501
+ return;
1502
+ }
1503
+ if (canRecordVoice) {
1504
+ onStartRecording();
1505
+ }
1506
+ },
1507
+ children: canSend ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SendIcon, { color: "currentColor", size: 22 }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicIcon, { color: "currentColor", size: 22 })
1508
+ }
1509
+ )
1510
+ ] })
1511
+ ] });
1512
+ }
1513
+
1514
+ // src/components/MessageList.tsx
1515
+ var import_react14 = require("react");
1516
+
1517
+ // src/components/ActivityEvent.tsx
1518
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1519
+ function ActivityEvent({ text, timestamp }) {
1520
+ const trimmed = text.trim();
1521
+ if (trimmed.length === 0) {
1522
+ return null;
1523
+ }
1524
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "jk-activity", children: [
1525
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: trimmed }),
1526
+ timestamp ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "jk-activity__time", children: timestamp }) : null
1527
+ ] });
1528
+ }
1529
+
1530
+ // src/components/MessageBubble.tsx
1531
+ var import_sdk_web14 = require("@jokkoo/sdk-web");
1532
+
1533
+ // src/components/Avatar.tsx
1534
+ var import_react10 = require("react");
1535
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1536
+ function Avatar({
1537
+ initials,
1538
+ size = 40,
1539
+ showUnreadIndicator = false,
1540
+ imageUrl
1541
+ }) {
1542
+ const [imgError, setImgError] = (0, import_react10.useState)(false);
1543
+ const showImage = Boolean(imageUrl) && !imgError;
1544
+ const indicatorSize = Math.max(10, Math.round(size * 0.25));
1545
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "jk-avatar", children: [
1546
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1547
+ "div",
1548
+ {
1549
+ className: "jk-avatar__circle",
1550
+ style: { width: size, height: size, fontSize: size * 0.34 },
1551
+ children: showImage ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1552
+ "img",
1553
+ {
1554
+ className: "jk-avatar__img",
1555
+ src: imageUrl,
1556
+ alt: "",
1557
+ onError: () => setImgError(true)
1558
+ }
1559
+ ) : initials
1560
+ }
1561
+ ),
1562
+ showUnreadIndicator ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1563
+ "span",
1564
+ {
1565
+ className: "jk-avatar__unread",
1566
+ style: { width: indicatorSize, height: indicatorSize }
1567
+ }
1568
+ ) : null
1569
+ ] });
1570
+ }
1571
+
1572
+ // src/components/MessageAttachments.tsx
1573
+ var import_sdk_web11 = require("@jokkoo/sdk-web");
1574
+ var import_react11 = require("react");
1575
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1576
+ function AttachmentDoc({
1577
+ attachment,
1578
+ onPress
1579
+ }) {
1580
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("button", { type: "button", className: "jk-attachments__doc", onClick: onPress, children: [
1581
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { children: (0, import_sdk_web11.isVideoMimeType)(attachment.mimeType) ? "Video" : "Document" }),
1582
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "jk-attachments__doc-name", children: attachment.fileName })
1583
+ ] });
1584
+ }
1585
+ function AttachmentRow({
1586
+ attachment,
1587
+ apiClient,
1588
+ onPress
1589
+ }) {
1590
+ const [downloadUrl, setDownloadUrl] = (0, import_react11.useState)(null);
1591
+ (0, import_react11.useEffect)(() => {
1592
+ if (!(0, import_sdk_web11.canPreviewInApp)(attachment.mimeType)) {
1593
+ return;
1594
+ }
1595
+ let cancelled = false;
1596
+ void apiClient.getDownloadUrl(attachment.fileId).then((response) => {
1597
+ if (!cancelled) {
1598
+ setDownloadUrl(response.downloadUrl);
1599
+ }
1600
+ });
1601
+ return () => {
1602
+ cancelled = true;
1603
+ };
1604
+ }, [apiClient, attachment.fileId, attachment.mimeType]);
1605
+ if ((0, import_sdk_web11.isImageMimeType)(attachment.mimeType)) {
1606
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", onClick: onPress, style: { padding: 0, background: "none", border: "none" }, children: downloadUrl ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("img", { className: "jk-attachments__img", src: downloadUrl, alt: attachment.fileName }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "jk-attachments__img", style: { display: "flex", alignItems: "center", justifyContent: "center", background: "var(--jk-color-surface)" }, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "jk-spinner" }) }) });
1607
+ }
1608
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(AttachmentDoc, { attachment, onPress });
1609
+ }
1610
+ function MessageAttachments({
1611
+ attachments,
1612
+ apiClient
1613
+ }) {
1614
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "jk-attachments", children: attachments.map((attachment) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1615
+ AttachmentRow,
1616
+ {
1617
+ attachment,
1618
+ apiClient,
1619
+ onPress: () => {
1620
+ void apiClient.getDownloadUrl(attachment.fileId).then((response) => {
1621
+ window.open(response.downloadUrl, "_blank", "noopener,noreferrer");
1622
+ });
1623
+ }
1624
+ },
1625
+ attachment.fileId
1626
+ )) });
1627
+ }
1628
+
1629
+ // src/components/VoiceMessagePlayer.tsx
1630
+ var import_sdk_web13 = require("@jokkoo/sdk-web");
1631
+ var import_react13 = require("react");
1632
+
1633
+ // src/components/VoiceWaveform.tsx
1634
+ var import_sdk_web12 = require("@jokkoo/sdk-web");
1635
+ var import_react12 = require("react");
1636
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1637
+ var DEFAULT_BAR_COUNT = 32;
1638
+ function VoiceWaveform({
1639
+ barCount = DEFAULT_BAR_COUNT,
1640
+ progress = 0,
1641
+ levels,
1642
+ activeColor,
1643
+ inactiveColor,
1644
+ height = 24
1645
+ }) {
1646
+ const playbackHeights = (0, import_react12.useMemo)(() => (0, import_sdk_web12.createPlaybackBarHeights)(barCount, 3), [barCount]);
1647
+ const clampedProgress = Math.max(0, Math.min(1, progress));
1648
+ const isLiveMode = levels !== void 0;
1649
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "jk-waveform", style: { height }, children: isLiveMode ? levels.map((level, index) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1650
+ "span",
1651
+ {
1652
+ className: "jk-waveform__bar",
1653
+ style: {
1654
+ height: Math.max(3, level * height),
1655
+ backgroundColor: activeColor
1656
+ }
1657
+ },
1658
+ `live-${index}`
1659
+ )) : playbackHeights.map((baseHeight, index) => {
1660
+ const position = barCount <= 1 ? 0 : index / (barCount - 1);
1661
+ const isActive = position <= clampedProgress;
1662
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1663
+ "span",
1664
+ {
1665
+ className: "jk-waveform__bar",
1666
+ style: {
1667
+ height: Math.max(3, baseHeight * height),
1668
+ backgroundColor: isActive ? activeColor : inactiveColor
1669
+ }
1670
+ },
1671
+ `play-${index}`
1672
+ );
1673
+ }) });
1674
+ }
1675
+
1676
+ // src/components/VoiceMessagePlayer.tsx
1677
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1678
+ function VoiceMessagePlayerReady({
1679
+ downloadUrl,
1680
+ durationMs,
1681
+ colors
1682
+ }) {
1683
+ const audioRef = (0, import_react13.useRef)(null);
1684
+ const [playing, setPlaying] = (0, import_react13.useState)(false);
1685
+ const [currentMs, setCurrentMs] = (0, import_react13.useState)(0);
1686
+ const [totalMs, setTotalMs] = (0, import_react13.useState)(durationMs);
1687
+ (0, import_react13.useEffect)(() => {
1688
+ const audio = new Audio(downloadUrl);
1689
+ audioRef.current = audio;
1690
+ const onTime = () => setCurrentMs(audio.currentTime * 1e3);
1691
+ const onMeta = () => {
1692
+ if (audio.duration && Number.isFinite(audio.duration)) {
1693
+ setTotalMs(audio.duration * 1e3);
1694
+ }
1695
+ };
1696
+ const onEnded = () => {
1697
+ setPlaying(false);
1698
+ setCurrentMs(0);
1699
+ };
1700
+ audio.addEventListener("timeupdate", onTime);
1701
+ audio.addEventListener("loadedmetadata", onMeta);
1702
+ audio.addEventListener("ended", onEnded);
1703
+ return () => {
1704
+ audio.pause();
1705
+ audio.removeEventListener("timeupdate", onTime);
1706
+ audio.removeEventListener("loadedmetadata", onMeta);
1707
+ audio.removeEventListener("ended", onEnded);
1708
+ audioRef.current = null;
1709
+ };
1710
+ }, [downloadUrl]);
1711
+ const toggle = (0, import_react13.useCallback)(async () => {
1712
+ const audio = audioRef.current;
1713
+ if (!audio) {
1714
+ return;
1715
+ }
1716
+ if (playing) {
1717
+ audio.pause();
1718
+ setPlaying(false);
1719
+ return;
1720
+ }
1721
+ if (audio.currentTime >= audio.duration && audio.duration > 0) {
1722
+ audio.currentTime = 0;
1723
+ }
1724
+ await audio.play();
1725
+ setPlaying(true);
1726
+ }, [playing]);
1727
+ const progress = totalMs > 0 ? currentMs / totalMs : 0;
1728
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "jk-voice-player", children: [
1729
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1730
+ "button",
1731
+ {
1732
+ type: "button",
1733
+ className: "jk-voice-player__btn",
1734
+ style: { background: colors.buttonBackground, color: colors.primary },
1735
+ "aria-label": playing ? "Pause voice message" : "Play voice message",
1736
+ onClick: () => {
1737
+ void toggle();
1738
+ },
1739
+ children: playing ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PauseIcon, { color: colors.primary, size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PlayIcon, { color: colors.primary, size: 16 })
1740
+ }
1741
+ ),
1742
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "jk-voice-player__content", children: [
1743
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1744
+ VoiceWaveform,
1745
+ {
1746
+ progress,
1747
+ activeColor: colors.primary,
1748
+ inactiveColor: colors.secondary,
1749
+ height: 22
1750
+ }
1751
+ ),
1752
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "jk-voice-player__duration", style: { color: colors.secondary }, children: [
1753
+ (0, import_sdk_web13.formatAudioDuration)(currentMs),
1754
+ " / ",
1755
+ (0, import_sdk_web13.formatAudioDuration)(totalMs)
1756
+ ] })
1757
+ ] })
1758
+ ] });
1759
+ }
1760
+ function VoiceMessagePlayer({
1761
+ fileId,
1762
+ durationMs,
1763
+ apiClient,
1764
+ variant
1765
+ }) {
1766
+ const [downloadUrl, setDownloadUrl] = (0, import_react13.useState)(null);
1767
+ const [isLoadingUrl, setIsLoadingUrl] = (0, import_react13.useState)(true);
1768
+ (0, import_react13.useEffect)(() => {
1769
+ let cancelled = false;
1770
+ void (async () => {
1771
+ try {
1772
+ const response = await apiClient.getDownloadUrl(fileId);
1773
+ if (!cancelled) {
1774
+ setDownloadUrl(response.downloadUrl);
1775
+ }
1776
+ } catch {
1777
+ if (!cancelled) {
1778
+ setDownloadUrl(null);
1779
+ }
1780
+ } finally {
1781
+ if (!cancelled) {
1782
+ setIsLoadingUrl(false);
1783
+ }
1784
+ }
1785
+ })();
1786
+ return () => {
1787
+ cancelled = true;
1788
+ };
1789
+ }, [apiClient, fileId]);
1790
+ const colors = (0, import_react13.useMemo)(() => {
1791
+ if (variant === "user") {
1792
+ return {
1793
+ primary: "#ffffff",
1794
+ secondary: "rgba(255, 255, 255, 0.5)",
1795
+ buttonBackground: "rgba(255, 255, 255, 0.18)"
1796
+ };
1797
+ }
1798
+ return {
1799
+ primary: "var(--jk-color-text)",
1800
+ secondary: "var(--jk-color-text-secondary)",
1801
+ buttonBackground: "var(--jk-color-background)"
1802
+ };
1803
+ }, [variant]);
1804
+ if (isLoadingUrl) {
1805
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "jk-voice-player", children: [
1806
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "jk-voice-player__btn", style: { background: colors.buttonBackground }, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "jk-spinner", style: { width: 16, height: 16 } }) }),
1807
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "jk-voice-player__content", children: [
1808
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(VoiceWaveform, { progress: 0, activeColor: colors.primary, inactiveColor: colors.secondary, height: 22 }),
1809
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "jk-voice-player__duration", style: { color: colors.secondary }, children: [
1810
+ (0, import_sdk_web13.formatAudioDuration)(0),
1811
+ " / ",
1812
+ (0, import_sdk_web13.formatAudioDuration)(durationMs)
1813
+ ] })
1814
+ ] })
1815
+ ] });
1816
+ }
1817
+ if (!downloadUrl) {
1818
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "jk-voice-player", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { style: { color: colors.secondary, fontSize: 12 }, children: "Unable to load voice message" }) });
1819
+ }
1820
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(VoiceMessagePlayerReady, { downloadUrl, durationMs, colors });
1821
+ }
1822
+
1823
+ // src/components/MessageBubble.tsx
1824
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1825
+ function MessageBubble({
1826
+ message,
1827
+ apiClient,
1828
+ showAgentMeta = true,
1829
+ variant = "default"
1830
+ }) {
1831
+ const isUser = message.sender === "user";
1832
+ const isVoiceMessage = (0, import_sdk_web14.isVoiceMessageMetadata)(message.metadata);
1833
+ const voiceAttachment = message.attachments?.find(
1834
+ (attachment) => attachment.mimeType.startsWith("audio/")
1835
+ );
1836
+ const voiceDurationMs = typeof message.metadata?.audioDurationMs === "number" ? message.metadata.audioDurationMs : 0;
1837
+ const renderBody = () => {
1838
+ if (isVoiceMessage && voiceAttachment) {
1839
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1840
+ VoiceMessagePlayer,
1841
+ {
1842
+ fileId: voiceAttachment.fileId,
1843
+ durationMs: voiceDurationMs,
1844
+ apiClient,
1845
+ variant: isUser ? "user" : "agent"
1846
+ }
1847
+ );
1848
+ }
1849
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
1850
+ message.text.trim().length > 0 ? message.text : null,
1851
+ message.attachments && message.attachments.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MessageAttachments, { attachments: message.attachments, apiClient }) : null
1852
+ ] });
1853
+ };
1854
+ if (isUser) {
1855
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "jk-bubble-row jk-bubble-row--user", children: [
1856
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "jk-bubble jk-bubble--user", children: renderBody() }),
1857
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "jk-bubble__time", children: message.timestamp })
1858
+ ] });
1859
+ }
1860
+ if (variant === "welcome") {
1861
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "jk-bubble-row jk-bubble-row--agent", children: [
1862
+ message.agentInitials ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Avatar, { initials: message.agentInitials, size: 28, imageUrl: message.agentAvatarUrl }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: { width: 28 } }),
1863
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "jk-bubble jk-bubble--welcome", children: message.text })
1864
+ ] });
1865
+ }
1866
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "jk-bubble-row jk-bubble-row--agent", children: [
1867
+ showAgentMeta && message.agentInitials ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Avatar, { initials: message.agentInitials, size: 28, imageUrl: message.agentAvatarUrl }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: { width: 28 } }),
1868
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "jk-bubble-row__agent-body", children: [
1869
+ showAgentMeta && message.agentName ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "jk-bubble-row__agent-name", children: message.agentName }) : null,
1870
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "jk-bubble jk-bubble--agent", children: renderBody() }),
1871
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "jk-bubble__time", children: message.timestamp })
1872
+ ] })
1873
+ ] });
1874
+ }
1875
+
1876
+ // src/components/NewMessageIndicator.tsx
1877
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1878
+ function formatBadgeLabel(count) {
1879
+ if (count > 99) {
1880
+ return "99+";
1881
+ }
1882
+ return String(count);
1883
+ }
1884
+ function NewMessageIndicator({
1885
+ count,
1886
+ t,
1887
+ onPress,
1888
+ bottomOffset = 88
1889
+ }) {
1890
+ if (count <= 0) {
1891
+ return null;
1892
+ }
1893
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1894
+ "button",
1895
+ {
1896
+ type: "button",
1897
+ className: "jk-new-msg-indicator",
1898
+ style: { bottom: bottomOffset },
1899
+ "aria-label": t.newMessageIndicator(count),
1900
+ onClick: onPress,
1901
+ children: [
1902
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ArrowDownIcon, { color: "currentColor", size: 20 }),
1903
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "jk-new-msg-indicator__badge", children: formatBadgeLabel(count) })
1904
+ ]
1905
+ }
1906
+ );
1907
+ }
1908
+
1909
+ // src/components/MessageList.tsx
1910
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1911
+ var SCROLL_BOTTOM_THRESHOLD = 48;
1912
+ function MessageList({
1913
+ messages,
1914
+ apiClient,
1915
+ t,
1916
+ isNewSupport,
1917
+ isLoading,
1918
+ isError,
1919
+ isFetchingNextPage,
1920
+ pendingNewMessages,
1921
+ onLoadOlder,
1922
+ onScrolledToBottom,
1923
+ onScrolledAwayFromBottom,
1924
+ onJumpToLatest
1925
+ }) {
1926
+ const listRef = (0, import_react14.useRef)(null);
1927
+ const stickToBottomRef = (0, import_react14.useRef)(true);
1928
+ (0, import_react14.useEffect)(() => {
1929
+ if (!stickToBottomRef.current || !listRef.current) {
1930
+ return;
1931
+ }
1932
+ listRef.current.scrollTop = listRef.current.scrollHeight;
1933
+ }, [messages.length]);
1934
+ const handleScroll = () => {
1935
+ const el = listRef.current;
1936
+ if (!el || isNewSupport) {
1937
+ return;
1938
+ }
1939
+ const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
1940
+ if (distanceFromBottom <= SCROLL_BOTTOM_THRESHOLD) {
1941
+ stickToBottomRef.current = true;
1942
+ onScrolledToBottom();
1943
+ } else {
1944
+ stickToBottomRef.current = false;
1945
+ onScrolledAwayFromBottom();
1946
+ }
1947
+ if (el.scrollTop < 40) {
1948
+ onLoadOlder();
1949
+ }
1950
+ };
1951
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "jk-conversation__body", children: [
1952
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1953
+ "div",
1954
+ {
1955
+ ref: listRef,
1956
+ className: `jk-messages${isNewSupport ? " jk-messages--new" : ""}`,
1957
+ onScroll: handleScroll,
1958
+ children: [
1959
+ isFetchingNextPage ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: { display: "flex", justifyContent: "center", padding: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "jk-spinner", style: { width: 18, height: 18 } }) }) : null,
1960
+ messages.length === 0 ? isLoading ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "jk-empty", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "jk-spinner" }) }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "jk-empty", children: t.unableToLoadMessages }) : null : messages.map((item, index) => {
1961
+ if (item.sender === "system") {
1962
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ActivityEvent, { text: item.text, timestamp: item.timestamp }, item.id);
1963
+ }
1964
+ const olderMessage = index > 0 ? messages[index - 1] : void 0;
1965
+ const showAgentMeta = item.sender === "agent" && olderMessage?.sender !== "agent";
1966
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1967
+ MessageBubble,
1968
+ {
1969
+ message: item,
1970
+ apiClient,
1971
+ showAgentMeta,
1972
+ variant: isNewSupport && item.id === "welcome" ? "welcome" : "default"
1973
+ },
1974
+ item.id
1975
+ );
1976
+ })
1977
+ ]
1978
+ }
1979
+ ),
1980
+ !isNewSupport ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1981
+ NewMessageIndicator,
1982
+ {
1983
+ count: pendingNewMessages,
1984
+ t,
1985
+ bottomOffset: 96,
1986
+ onPress: () => {
1987
+ stickToBottomRef.current = true;
1988
+ if (listRef.current) {
1989
+ listRef.current.scrollTop = listRef.current.scrollHeight;
1990
+ }
1991
+ onJumpToLatest();
1992
+ }
1993
+ }
1994
+ ) : null
1995
+ ] });
1996
+ }
1997
+
1998
+ // src/components/ResolvedBanner.tsx
1999
+ var import_jsx_runtime15 = require("react/jsx-runtime");
2000
+ function ResolvedBanner({
2001
+ t,
2002
+ onNewRequest,
2003
+ canReplyInConversation = false
2004
+ }) {
2005
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "jk-resolved-banner", children: [
2006
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "jk-resolved-banner__row", children: [
2007
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "jk-resolved-banner__icon", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CheckCircleIcon, { color: "currentColor", size: 14 }) }),
2008
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { children: canReplyInConversation ? t.discussionClosedCanReply : t.ticketResolved })
2009
+ ] }),
2010
+ !canReplyInConversation ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
2011
+ "button",
2012
+ {
2013
+ type: "button",
2014
+ className: "jk-resolved-banner__cta",
2015
+ "aria-label": t.newSupportRequest,
2016
+ onClick: onNewRequest,
2017
+ children: [
2018
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChatIcon, { color: "currentColor", size: 16 }),
2019
+ t.newSupportRequest
2020
+ ]
2021
+ }
2022
+ ) : null
2023
+ ] });
2024
+ }
2025
+
2026
+ // src/components/SatisfactionRating.tsx
2027
+ var import_react15 = require("react");
2028
+ var import_jsx_runtime16 = require("react/jsx-runtime");
2029
+ var RATING_OPTIONS = [
2030
+ { score: 1, emoji: "\u{1F61E}", labelKey: "ratingVeryUnsatisfied" },
2031
+ { score: 2, emoji: "\u{1F61F}", labelKey: "ratingUnsatisfied" },
2032
+ { score: 3, emoji: "\u{1F610}", labelKey: "ratingNeutral" },
2033
+ { score: 4, emoji: "\u{1F60A}", labelKey: "ratingSatisfied" },
2034
+ { score: 5, emoji: "\u{1F604}", labelKey: "ratingVerySatisfied" }
2035
+ ];
2036
+ function SatisfactionRating({ t, message, onSubmit }) {
2037
+ const [selectedScore, setSelectedScore] = (0, import_react15.useState)(null);
2038
+ const [isSubmitting, setIsSubmitting] = (0, import_react15.useState)(false);
2039
+ const [isSubmitted, setIsSubmitted] = (0, import_react15.useState)(false);
2040
+ const handleSelect = (0, import_react15.useCallback)(
2041
+ async (score) => {
2042
+ if (isSubmitting || isSubmitted) {
2043
+ return;
2044
+ }
2045
+ setSelectedScore(score);
2046
+ setIsSubmitting(true);
2047
+ try {
2048
+ await onSubmit(score);
2049
+ setIsSubmitted(true);
2050
+ } catch {
2051
+ setSelectedScore(null);
2052
+ } finally {
2053
+ setIsSubmitting(false);
2054
+ }
2055
+ },
2056
+ [isSubmitted, isSubmitting, onSubmit]
2057
+ );
2058
+ if (isSubmitted) {
2059
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "jk-satisfaction", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "jk-satisfaction__card", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "jk-satisfaction__title", children: t.thankYouForRating }) }) });
2060
+ }
2061
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "jk-satisfaction", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "jk-satisfaction__card", children: [
2062
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "jk-satisfaction__title", children: message ?? t.rateConversation }),
2063
+ !message ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "jk-satisfaction__desc", children: t.rateConversationDescription }) : null,
2064
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "jk-satisfaction__row", children: RATING_OPTIONS.map((option) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
2065
+ "button",
2066
+ {
2067
+ type: "button",
2068
+ className: `jk-satisfaction__btn${selectedScore === option.score ? " jk-satisfaction__btn--selected" : ""}`,
2069
+ "aria-label": t[option.labelKey],
2070
+ disabled: isSubmitting,
2071
+ onClick: () => {
2072
+ void handleSelect(option.score);
2073
+ },
2074
+ children: option.emoji
2075
+ },
2076
+ option.score
2077
+ )) }),
2078
+ isSubmitting ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "jk-spinner", style: { margin: "4px auto 0" } }) : null
2079
+ ] }) });
2080
+ }
2081
+
2082
+ // src/components/VoiceRecordingOverlay.tsx
2083
+ var import_sdk_web15 = require("@jokkoo/sdk-web");
2084
+ var import_jsx_runtime17 = require("react/jsx-runtime");
2085
+ function VoiceRecordingOverlay({
2086
+ t,
2087
+ durationMs,
2088
+ meteringLevels,
2089
+ isUploading,
2090
+ uploadProgress,
2091
+ onCancel,
2092
+ onSend
2093
+ }) {
2094
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "jk-voice-overlay", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "jk-voice-overlay__panel", children: [
2095
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2096
+ "button",
2097
+ {
2098
+ type: "button",
2099
+ className: "jk-input-shell__icon",
2100
+ "aria-label": t.cancelRecording,
2101
+ disabled: isUploading,
2102
+ onClick: onCancel,
2103
+ children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(CloseIcon, { color: "currentColor", size: 14 })
2104
+ }
2105
+ ),
2106
+ isUploading ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "jk-voice-overlay__uploading", children: [
2107
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "jk-spinner", style: { width: 16, height: 16 } }),
2108
+ `${t.uploadingAttachment} ${uploadProgress}%`
2109
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
2110
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "jk-voice-overlay__rec", children: [
2111
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "jk-voice-overlay__dot" }),
2112
+ (0, import_sdk_web15.formatAudioDuration)(durationMs)
2113
+ ] }),
2114
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2115
+ VoiceWaveform,
2116
+ {
2117
+ levels: meteringLevels,
2118
+ activeColor: "var(--jk-color-primary)",
2119
+ inactiveColor: "rgba(28,28,28,0.2)",
2120
+ height: 28
2121
+ }
2122
+ )
2123
+ ] }),
2124
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2125
+ "button",
2126
+ {
2127
+ type: "button",
2128
+ className: "jk-input-shell__icon",
2129
+ "aria-label": t.sendRecording,
2130
+ disabled: isUploading,
2131
+ onClick: onSend,
2132
+ children: isUploading ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "jk-spinner", style: { width: 16, height: 16 } }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SendIcon, { color: "currentColor", size: 22 })
2133
+ }
2134
+ )
2135
+ ] }) });
2136
+ }
2137
+
2138
+ // src/components/ConversationScreen.tsx
2139
+ var import_jsx_runtime18 = require("react/jsx-runtime");
2140
+ function ConversationScreen({ conversationId }) {
2141
+ const { t, locale, goBack, navigateTo, apiClient, markConversationRead } = useJokkooContext();
2142
+ const queryClient = (0, import_react_query7.useQueryClient)();
2143
+ const [message, setMessage] = (0, import_react16.useState)("");
2144
+ const [isSending, setIsSending] = (0, import_react16.useState)(false);
2145
+ const [optimisticMessages, setOptimisticMessages] = (0, import_react16.useState)([]);
2146
+ const {
2147
+ pendingAttachments,
2148
+ isUploading,
2149
+ confirmedFileIds,
2150
+ pickAttachments,
2151
+ removeAttachment,
2152
+ retryAttachment,
2153
+ clearAttachments
2154
+ } = useFileUpload();
2155
+ const {
2156
+ state: voiceRecorderState,
2157
+ durationMs: voiceDurationMs,
2158
+ meteringLevels: voiceMeteringLevels,
2159
+ uploadProgress: voiceUploadProgress,
2160
+ isRecordingActive,
2161
+ startRecording,
2162
+ cancelRecording,
2163
+ stopAndUpload,
2164
+ reset: resetVoiceRecorder
2165
+ } = useVoiceRecorder();
2166
+ const isNewSupport = conversationId === import_sdk_web16.NEW_CONVERSATION_ID;
2167
+ const {
2168
+ pendingNewMessages,
2169
+ notifyScrolledToBottom,
2170
+ notifyScrolledAwayFromBottom
2171
+ } = useRealtimeMessages(conversationId);
2172
+ const {
2173
+ messages: fetchedMessages,
2174
+ fetchNextPage,
2175
+ hasNextPage,
2176
+ isFetchingNextPage,
2177
+ isLoading,
2178
+ isError
2179
+ } = useMessages(conversationId, { enabled: !isNewSupport });
2180
+ const { conversations } = useConversations();
2181
+ const { data: closedConversationPolicy } = useClosedConversationPolicy();
2182
+ const conversation = (0, import_react16.useMemo)(
2183
+ () => conversations.find((item) => item.id === conversationId),
2184
+ [conversations, conversationId]
2185
+ );
2186
+ const isClosed = conversation?.status === "closed";
2187
+ const canReplyInClosedConversation = (0, import_react16.useMemo)(
2188
+ () => (0, import_sdk_web16.canReplyToClosedConversation)(conversation ?? { status: "open" }, closedConversationPolicy),
2189
+ [closedConversationPolicy, conversation]
2190
+ );
2191
+ const isEnded = Boolean(isClosed && !canReplyInClosedConversation);
2192
+ const isAwaitingRating = (0, import_sdk_web16.conversationNeedsRating)(conversation?.status, fetchedMessages);
2193
+ const satisfactionMessage = (0, import_react16.useMemo)(
2194
+ () => (0, import_sdk_web16.getSatisfactionSurveyMessage)(fetchedMessages),
2195
+ [fetchedMessages]
2196
+ );
2197
+ const headerTitle = isNewSupport ? t.newSupportRequestHeader : conversation ? (0, import_sdk_web16.getConversationDisplayName)(conversation, t) : t.support;
2198
+ const assignedAgentName = conversation?.assignedAgentName ?? null;
2199
+ const assignedAgentAvatarUrl = conversation?.assignedAgentAvatarUrl ? apiClient.resolveUrl(conversation.assignedAgentAvatarUrl) : null;
2200
+ const trimmedLength = message.trim().length;
2201
+ const isOverLimit = message.length > import_sdk_web16.MESSAGE_CONTENT_MAX_LENGTH;
2202
+ const attachmentsReady = pendingAttachments.length === 0 || pendingAttachments.every((item) => item.status === "confirmed");
2203
+ const canSend = attachmentsReady && (trimmedLength >= import_sdk_web16.MESSAGE_CONTENT_MIN_LENGTH || confirmedFileIds.length > 0) && !isOverLimit && !isSending && !isUploading && !isAwaitingRating && !isEnded && !isRecordingActive;
2204
+ const canRecordVoice = !canSend && trimmedLength === 0 && pendingAttachments.length === 0 && !isSending && !isUploading && !isAwaitingRating && !isEnded && !isRecordingActive;
2205
+ const messages = (0, import_react16.useMemo)(() => {
2206
+ const visibleFetchedMessages = fetchedMessages.filter(
2207
+ (item) => !(0, import_sdk_web16.isSatisfactionRequestMessage)(item)
2208
+ );
2209
+ const baseMessages = isNewSupport ? [(0, import_sdk_web16.createWelcomeMessage)(t)] : visibleFetchedMessages.map(
2210
+ (item) => (0, import_sdk_web16.mapMessageToChatMessage)(item, t, locale, assignedAgentName, assignedAgentAvatarUrl)
2211
+ );
2212
+ const pendingOptimistic = optimisticMessages.filter(
2213
+ (optimistic) => !baseMessages.some(
2214
+ (base) => base.sender === optimistic.sender && base.text === optimistic.text
2215
+ )
2216
+ );
2217
+ if (isNewSupport) {
2218
+ return [...baseMessages, ...pendingOptimistic];
2219
+ }
2220
+ return [...baseMessages, ...pendingOptimistic];
2221
+ }, [
2222
+ assignedAgentName,
2223
+ assignedAgentAvatarUrl,
2224
+ fetchedMessages,
2225
+ isNewSupport,
2226
+ locale,
2227
+ optimisticMessages,
2228
+ t
2229
+ ]);
2230
+ const cancelRecordingRef = (0, import_react16.useRef)(cancelRecording);
2231
+ const resetVoiceRecorderRef = (0, import_react16.useRef)(resetVoiceRecorder);
2232
+ cancelRecordingRef.current = cancelRecording;
2233
+ resetVoiceRecorderRef.current = resetVoiceRecorder;
2234
+ (0, import_react16.useEffect)(() => {
2235
+ setMessage("");
2236
+ setOptimisticMessages([]);
2237
+ clearAttachments();
2238
+ void cancelRecordingRef.current();
2239
+ resetVoiceRecorderRef.current();
2240
+ }, [clearAttachments, conversationId]);
2241
+ (0, import_react16.useEffect)(() => {
2242
+ if (!isNewSupport) {
2243
+ markConversationRead(conversationId);
2244
+ }
2245
+ }, [conversationId, isNewSupport, markConversationRead]);
2246
+ (0, import_react16.useEffect)(() => {
2247
+ if (!isNewSupport && isEnded) {
2248
+ markConversationRead(conversationId);
2249
+ }
2250
+ }, [conversationId, isEnded, isNewSupport, markConversationRead]);
2251
+ const handleEndReached = (0, import_react16.useCallback)(() => {
2252
+ if (!isNewSupport && hasNextPage && !isFetchingNextPage) {
2253
+ fetchNextPage();
2254
+ }
2255
+ }, [fetchNextPage, hasNextPage, isFetchingNextPage, isNewSupport]);
2256
+ const handleSend = (0, import_react16.useCallback)(async () => {
2257
+ const trimmed = message.trim();
2258
+ const content = trimmed.length >= import_sdk_web16.MESSAGE_CONTENT_MIN_LENGTH ? trimmed : t.sharedFiles;
2259
+ if (trimmed.length < import_sdk_web16.MESSAGE_CONTENT_MIN_LENGTH && confirmedFileIds.length === 0) {
2260
+ return;
2261
+ }
2262
+ if (content.length > import_sdk_web16.MESSAGE_CONTENT_MAX_LENGTH || isSending) {
2263
+ return;
2264
+ }
2265
+ const optimisticId = `optimistic-${Date.now()}`;
2266
+ const optimisticMessage = {
2267
+ id: optimisticId,
2268
+ sender: "user",
2269
+ text: content,
2270
+ timestamp: (0, import_sdk_web16.formatMessageTimestamp)(/* @__PURE__ */ new Date(), locale),
2271
+ attachments: pendingAttachments.filter((item) => item.status === "confirmed").map((item) => ({
2272
+ fileId: item.confirmedFileId ?? item.id,
2273
+ fileName: item.fileName,
2274
+ mimeType: item.mimeType,
2275
+ size: item.size
2276
+ }))
2277
+ };
2278
+ setOptimisticMessages((previous) => [...previous, optimisticMessage]);
2279
+ setMessage("");
2280
+ clearAttachments();
2281
+ setIsSending(true);
2282
+ try {
2283
+ const sentMessage = await apiClient.sendMessage({
2284
+ content,
2285
+ conversationId: isNewSupport ? void 0 : conversationId,
2286
+ forceNew: isNewSupport ? true : void 0,
2287
+ fileIds: confirmedFileIds.length > 0 ? confirmedFileIds : void 0
2288
+ });
2289
+ if (isNewSupport) {
2290
+ queryClient.setQueryData(
2291
+ jokkooQueryKeys.messages(sentMessage.conversationId),
2292
+ createMessagesCacheWithMessage(sentMessage)
2293
+ );
2294
+ setOptimisticMessages((previous) => previous.filter((item) => item.id !== optimisticId));
2295
+ navigateTo({
2296
+ name: "conversation",
2297
+ conversationId: sentMessage.conversationId
2298
+ });
2299
+ } else {
2300
+ queryClient.setQueryData(
2301
+ jokkooQueryKeys.messages(conversationId),
2302
+ (current) => prependMessageToMessagesCache(current, sentMessage)
2303
+ );
2304
+ setOptimisticMessages((previous) => previous.filter((item) => item.id !== optimisticId));
2305
+ }
2306
+ void queryClient.invalidateQueries({ queryKey: jokkooQueryKeys.conversations });
2307
+ } catch (error) {
2308
+ setOptimisticMessages((previous) => previous.filter((item) => item.id !== optimisticId));
2309
+ setMessage(trimmed);
2310
+ const errorMessage = error instanceof import_sdk_web16.JokkooApiError && error.message.trim().length > 0 ? error.message : t.sendMessageError;
2311
+ window.alert(errorMessage);
2312
+ } finally {
2313
+ setIsSending(false);
2314
+ }
2315
+ }, [
2316
+ apiClient,
2317
+ clearAttachments,
2318
+ confirmedFileIds,
2319
+ conversationId,
2320
+ isNewSupport,
2321
+ isSending,
2322
+ locale,
2323
+ message,
2324
+ navigateTo,
2325
+ pendingAttachments,
2326
+ queryClient,
2327
+ t
2328
+ ]);
2329
+ const handleSendVoiceMessage = (0, import_react16.useCallback)(async () => {
2330
+ const uploaded = await stopAndUpload();
2331
+ if (!uploaded) {
2332
+ return;
2333
+ }
2334
+ const optimisticId = `optimistic-voice-${Date.now()}`;
2335
+ const optimisticMessage = {
2336
+ id: optimisticId,
2337
+ sender: "user",
2338
+ text: t.voiceMessage,
2339
+ timestamp: (0, import_sdk_web16.formatMessageTimestamp)(/* @__PURE__ */ new Date(), locale),
2340
+ metadata: {
2341
+ isVoiceMessage: true,
2342
+ audioDurationMs: uploaded.audioDurationMs
2343
+ },
2344
+ attachments: [
2345
+ {
2346
+ fileId: uploaded.fileId,
2347
+ fileName: "voice.webm",
2348
+ mimeType: "audio/webm"
2349
+ }
2350
+ ]
2351
+ };
2352
+ setOptimisticMessages((previous) => [...previous, optimisticMessage]);
2353
+ setIsSending(true);
2354
+ try {
2355
+ const sentMessage = await apiClient.sendMessage({
2356
+ content: t.voiceMessage,
2357
+ conversationId: isNewSupport ? void 0 : conversationId,
2358
+ forceNew: isNewSupport ? true : void 0,
2359
+ fileIds: [uploaded.fileId],
2360
+ metadata: {
2361
+ isVoiceMessage: true,
2362
+ audioDurationMs: uploaded.audioDurationMs
2363
+ }
2364
+ });
2365
+ if (isNewSupport) {
2366
+ queryClient.setQueryData(
2367
+ jokkooQueryKeys.messages(sentMessage.conversationId),
2368
+ createMessagesCacheWithMessage(sentMessage)
2369
+ );
2370
+ setOptimisticMessages((previous) => previous.filter((item) => item.id !== optimisticId));
2371
+ navigateTo({
2372
+ name: "conversation",
2373
+ conversationId: sentMessage.conversationId
2374
+ });
2375
+ } else {
2376
+ queryClient.setQueryData(
2377
+ jokkooQueryKeys.messages(conversationId),
2378
+ (current) => prependMessageToMessagesCache(current, sentMessage)
2379
+ );
2380
+ setOptimisticMessages((previous) => previous.filter((item) => item.id !== optimisticId));
2381
+ }
2382
+ void queryClient.invalidateQueries({ queryKey: jokkooQueryKeys.conversations });
2383
+ } catch (error) {
2384
+ setOptimisticMessages((previous) => previous.filter((item) => item.id !== optimisticId));
2385
+ const errorMessage = error instanceof import_sdk_web16.JokkooApiError && error.message.trim().length > 0 ? error.message : t.sendMessageError;
2386
+ window.alert(errorMessage);
2387
+ } finally {
2388
+ resetVoiceRecorder();
2389
+ setIsSending(false);
2390
+ }
2391
+ }, [
2392
+ apiClient,
2393
+ conversationId,
2394
+ isNewSupport,
2395
+ locale,
2396
+ navigateTo,
2397
+ queryClient,
2398
+ resetVoiceRecorder,
2399
+ stopAndUpload,
2400
+ t.sendMessageError,
2401
+ t.voiceMessage
2402
+ ]);
2403
+ const handleSatisfactionSubmit = (0, import_react16.useCallback)(
2404
+ async (score) => {
2405
+ await apiClient.recordSatisfaction(conversationId, score);
2406
+ markConversationRead(conversationId);
2407
+ await queryClient.invalidateQueries({ queryKey: jokkooQueryKeys.conversations });
2408
+ await queryClient.invalidateQueries({ queryKey: jokkooQueryKeys.messages(conversationId) });
2409
+ },
2410
+ [apiClient, conversationId, markConversationRead, queryClient]
2411
+ );
2412
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "jk-conversation", children: [
2413
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2414
+ ConversationHeader,
2415
+ {
2416
+ title: headerTitle,
2417
+ status: isNewSupport ? void 0 : conversation?.status,
2418
+ t,
2419
+ onBack: goBack
2420
+ }
2421
+ ),
2422
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2423
+ MessageList,
2424
+ {
2425
+ messages,
2426
+ apiClient,
2427
+ t,
2428
+ isNewSupport,
2429
+ isLoading,
2430
+ isError,
2431
+ isFetchingNextPage,
2432
+ pendingNewMessages,
2433
+ onLoadOlder: handleEndReached,
2434
+ onScrolledToBottom: notifyScrolledToBottom,
2435
+ onScrolledAwayFromBottom: notifyScrolledAwayFromBottom,
2436
+ onJumpToLatest: notifyScrolledToBottom
2437
+ }
2438
+ ),
2439
+ isAwaitingRating ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2440
+ SatisfactionRating,
2441
+ {
2442
+ t,
2443
+ message: satisfactionMessage,
2444
+ onSubmit: handleSatisfactionSubmit
2445
+ }
2446
+ ) : isRecordingActive ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2447
+ VoiceRecordingOverlay,
2448
+ {
2449
+ t,
2450
+ durationMs: voiceDurationMs,
2451
+ meteringLevels: voiceMeteringLevels,
2452
+ isUploading: voiceRecorderState === "uploading",
2453
+ uploadProgress: voiceUploadProgress,
2454
+ onCancel: () => {
2455
+ void cancelRecording();
2456
+ },
2457
+ onSend: () => {
2458
+ void handleSendVoiceMessage();
2459
+ }
2460
+ }
2461
+ ) : isEnded ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2462
+ ResolvedBanner,
2463
+ {
2464
+ t,
2465
+ onNewRequest: () => navigateTo({ name: "conversation", conversationId: import_sdk_web16.NEW_CONVERSATION_ID })
2466
+ }
2467
+ ) : /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
2468
+ canReplyInClosedConversation ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2469
+ ResolvedBanner,
2470
+ {
2471
+ t,
2472
+ canReplyInConversation: true,
2473
+ onNewRequest: () => navigateTo({ name: "conversation", conversationId: import_sdk_web16.NEW_CONVERSATION_ID })
2474
+ }
2475
+ ) : null,
2476
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2477
+ MessageInput,
2478
+ {
2479
+ t,
2480
+ apiClient,
2481
+ value: message,
2482
+ onChange: setMessage,
2483
+ pendingAttachments,
2484
+ onRemoveAttachment: removeAttachment,
2485
+ onRetryAttachment: retryAttachment,
2486
+ onPickAttachments: pickAttachments,
2487
+ canSend,
2488
+ canRecordVoice,
2489
+ onSend: () => {
2490
+ void handleSend();
2491
+ },
2492
+ onStartRecording: () => {
2493
+ void startRecording();
2494
+ }
2495
+ }
2496
+ )
2497
+ ] })
2498
+ ] });
2499
+ }
2500
+
2501
+ // src/components/FloatingButton.tsx
2502
+ var import_jsx_runtime19 = require("react/jsx-runtime");
2503
+ function formatBadgeLabel2(count) {
2504
+ if (count > 99) {
2505
+ return "99+";
2506
+ }
2507
+ return String(count);
2508
+ }
2509
+ function FloatingButton({
2510
+ position = "bottom-right",
2511
+ label,
2512
+ onClick,
2513
+ showBadge = true,
2514
+ className
2515
+ }) {
2516
+ const { t, openChat, unreadCount } = useJokkooContext();
2517
+ const displayLabel = label ?? t.newSupportRequest;
2518
+ const badgeVisible = showBadge && unreadCount > 0;
2519
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
2520
+ "button",
2521
+ {
2522
+ type: "button",
2523
+ className: `jk-floating-btn jk-floating-btn--${position}${className ? ` ${className}` : ""}`,
2524
+ "aria-label": badgeVisible ? `Open Jokkoo support, ${unreadCount} unread conversations` : "Open Jokkoo support",
2525
+ onClick: onClick ?? openChat,
2526
+ children: [
2527
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "jk-floating-btn__icon", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(ChatIcon, { color: "currentColor", size: 20 }) }),
2528
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "jk-floating-btn__label", children: displayLabel }),
2529
+ badgeVisible ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "jk-floating-btn__badge", children: formatBadgeLabel2(unreadCount) }) : null
2530
+ ]
2531
+ }
2532
+ );
2533
+ }
2534
+
2535
+ // src/components/RequestListScreen.tsx
2536
+ var import_sdk_web18 = require("@jokkoo/sdk-web");
2537
+ var import_react17 = require("react");
2538
+
2539
+ // src/components/RequestCard.tsx
2540
+ var import_sdk_web17 = require("@jokkoo/sdk-web");
2541
+ var import_jsx_runtime20 = require("react/jsx-runtime");
2542
+ function RequestCard({
2543
+ conversation,
2544
+ t,
2545
+ locale,
2546
+ isUnread = false,
2547
+ onPress,
2548
+ apiClient
2549
+ }) {
2550
+ const title = (0, import_sdk_web17.getConversationDisplayName)(conversation, t);
2551
+ const avatarInitials = (0, import_sdk_web17.getAgentInitials)(conversation.assignedAgentName, "SP");
2552
+ const avatarUrl = conversation.assignedAgentAvatarUrl ? apiClient.resolveUrl(conversation.assignedAgentAvatarUrl) : null;
2553
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
2554
+ "button",
2555
+ {
2556
+ type: "button",
2557
+ className: `jk-request-card${isUnread ? " jk-request-card--unread" : ""}`,
2558
+ "aria-label": isUnread ? `${title}, ${conversation.lastMessageText ?? t.noMessagesYet}, unread` : `${title}, ${conversation.lastMessageText ?? t.noMessagesYet}`,
2559
+ onClick: () => onPress(conversation.id),
2560
+ children: [
2561
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2562
+ Avatar,
2563
+ {
2564
+ initials: avatarInitials,
2565
+ size: 48,
2566
+ showUnreadIndicator: isUnread,
2567
+ imageUrl: avatarUrl
2568
+ }
2569
+ ),
2570
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "jk-request-card__content", children: [
2571
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "jk-request-card__row", children: [
2572
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { className: "jk-request-card__title", children: title }),
2573
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(StatusBadge, { status: conversation.status, t })
2574
+ ] }),
2575
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2576
+ "p",
2577
+ {
2578
+ className: `jk-request-card__preview${isUnread ? " jk-request-card__preview--unread" : ""}`,
2579
+ children: conversation.lastMessageText ?? t.noMessagesYet
2580
+ }
2581
+ ),
2582
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "jk-request-card__time", children: [
2583
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ClockIcon, { color: "currentColor", size: 11 }),
2584
+ (0, import_sdk_web17.formatRelativeTimestamp)(conversation.updatedAt, {
2585
+ locale,
2586
+ nowLabel: t.now
2587
+ })
2588
+ ] })
2589
+ ] })
2590
+ ]
2591
+ }
2592
+ );
2593
+ }
2594
+
2595
+ // src/components/RequestListScreen.tsx
2596
+ var import_jsx_runtime21 = require("react/jsx-runtime");
2597
+ function RequestListScreen() {
2598
+ const { t, locale, navigateTo, isConversationUnread, apiClient } = useJokkooContext();
2599
+ const {
2600
+ conversations,
2601
+ fetchNextPage,
2602
+ hasNextPage,
2603
+ isFetchingNextPage,
2604
+ isLoading,
2605
+ isError,
2606
+ refetch
2607
+ } = useConversations();
2608
+ const activeCount = conversations.filter((conversation) => conversation.status === "open").length;
2609
+ const handleScroll = (0, import_react17.useCallback)(
2610
+ (event) => {
2611
+ const target = event.currentTarget;
2612
+ const nearBottom = target.scrollHeight - target.scrollTop - target.clientHeight < 80;
2613
+ if (nearBottom && hasNextPage && !isFetchingNextPage) {
2614
+ fetchNextPage();
2615
+ }
2616
+ },
2617
+ [fetchNextPage, hasNextPage, isFetchingNextPage]
2618
+ );
2619
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "jk-request-list", children: [
2620
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "jk-request-list__header", children: [
2621
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("h2", { className: "jk-request-list__title", children: t.supportCenter }),
2622
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { className: "jk-request-list__subtitle", children: t.activeConversation(activeCount) }),
2623
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { className: "jk-request-list__section", children: t.myRequests })
2624
+ ] }),
2625
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "jk-request-list__items", onScroll: handleScroll, children: [
2626
+ conversations.length === 0 ? isLoading ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "jk-empty", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "jk-spinner" }) }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "jk-empty", children: [
2627
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: t.unableToLoadConversations }),
2628
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("button", { type: "button", className: "jk-empty__retry", onClick: refetch, children: t.retry })
2629
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "jk-empty", children: t.noConversationsYet }) : conversations.map((item) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2630
+ RequestCard,
2631
+ {
2632
+ conversation: item,
2633
+ t,
2634
+ locale,
2635
+ isUnread: isConversationUnread(item.id),
2636
+ onPress: (conversationId) => navigateTo({ name: "conversation", conversationId }),
2637
+ apiClient
2638
+ },
2639
+ item.id
2640
+ )),
2641
+ isFetchingNextPage ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "jk-empty", style: { paddingTop: 16 }, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "jk-spinner" }) }) : null
2642
+ ] }),
2643
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "jk-request-list__footer", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
2644
+ "button",
2645
+ {
2646
+ type: "button",
2647
+ className: "jk-floating-btn",
2648
+ style: { position: "static" },
2649
+ "aria-label": t.newSupportRequest,
2650
+ onClick: () => navigateTo({ name: "conversation", conversationId: import_sdk_web18.NEW_CONVERSATION_ID }),
2651
+ children: [
2652
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "jk-floating-btn__icon", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ChatIcon, { color: "currentColor", size: 20 }) }),
2653
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "jk-floating-btn__label", children: t.newSupportRequest })
2654
+ ]
2655
+ }
2656
+ ) })
2657
+ ] });
2658
+ }
2659
+
2660
+ // src/components/PanelHeader.tsx
2661
+ var import_jsx_runtime22 = require("react/jsx-runtime");
2662
+ function PanelHeader({
2663
+ variant = "light",
2664
+ onExpand,
2665
+ onClose,
2666
+ expandLabel = "Expand",
2667
+ closeLabel = "Close"
2668
+ }) {
2669
+ const dark = variant === "dark";
2670
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: `jk-panel-header${dark ? " jk-panel-header--dark" : ""}`, children: [
2671
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2672
+ "button",
2673
+ {
2674
+ type: "button",
2675
+ className: `jk-icon-btn${dark ? " jk-icon-btn--dark" : ""}`,
2676
+ "aria-label": expandLabel,
2677
+ onClick: onExpand,
2678
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(ExpandIcon, { color: "currentColor", size: 14 })
2679
+ }
2680
+ ),
2681
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2682
+ "button",
2683
+ {
2684
+ type: "button",
2685
+ className: `jk-icon-btn${dark ? " jk-icon-btn--dark" : ""}`,
2686
+ "aria-label": closeLabel,
2687
+ onClick: onClose,
2688
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(CloseIcon, { color: "currentColor", size: 14 })
2689
+ }
2690
+ )
2691
+ ] });
2692
+ }
2693
+
2694
+ // src/components/WidgetPanel.tsx
2695
+ var import_jsx_runtime23 = require("react/jsx-runtime");
2696
+ function WidgetPanel({ children, headerVariant = "light" }) {
2697
+ const { isExpanded, toggleExpanded, closeChat } = useJokkooContext();
2698
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: `jk-panel${isExpanded ? " jk-panel--expanded" : ""}`, role: "dialog", "aria-modal": true, children: [
2699
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2700
+ PanelHeader,
2701
+ {
2702
+ variant: headerVariant,
2703
+ onExpand: toggleExpanded,
2704
+ onClose: closeChat
2705
+ }
2706
+ ),
2707
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "jk-panel-body", children })
2708
+ ] });
2709
+ }
2710
+
2711
+ // src/components/JokkooWidget.tsx
2712
+ var import_jsx_runtime24 = require("react/jsx-runtime");
2713
+ function JokkooWidget({ position = "bottom-right", launcherLabel }) {
2714
+ const { isChatOpen, currentScreen } = useJokkooContext();
2715
+ useRealtimeConversations();
2716
+ const headerVariant = currentScreen.name === "conversation" ? "dark" : "light";
2717
+ if (!isChatOpen) {
2718
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(FloatingButton, { position, label: launcherLabel });
2719
+ }
2720
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(WidgetPanel, { headerVariant, children: currentScreen.name === "requestList" ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(RequestListScreen, {}) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
2721
+ ConversationScreen,
2722
+ {
2723
+ conversationId: currentScreen.conversationId
2724
+ },
2725
+ currentScreen.conversationId
2726
+ ) });
2727
+ }
2728
+
2729
+ // src/hooks/useSendMessage.ts
2730
+ var import_react_query8 = require("@tanstack/react-query");
2731
+ function useSendMessage() {
2732
+ const { apiClient } = useJokkooContext();
2733
+ const queryClient = (0, import_react_query8.useQueryClient)();
2734
+ return (0, import_react_query8.useMutation)({
2735
+ mutationFn: (payload) => apiClient.sendMessage(payload),
2736
+ onSuccess: (message) => {
2737
+ void queryClient.invalidateQueries({ queryKey: jokkooQueryKeys.conversations });
2738
+ void queryClient.invalidateQueries({
2739
+ queryKey: jokkooQueryKeys.messages(message.conversationId)
2740
+ });
2741
+ }
2742
+ });
2743
+ }
2744
+
2745
+ // src/hooks/useUnreadCount.ts
2746
+ function useUnreadCount() {
2747
+ return useJokkooContext().unreadCount;
2748
+ }
2749
+ // Annotate the CommonJS export names for ESM import in node:
2750
+ 0 && (module.exports = {
2751
+ ConversationScreen,
2752
+ FloatingButton,
2753
+ JokkooButton,
2754
+ JokkooProvider,
2755
+ JokkooWidget,
2756
+ NEW_CONVERSATION_ID,
2757
+ RequestListScreen,
2758
+ WidgetPanel,
2759
+ defaultTheme,
2760
+ jokkooQueryKeys,
2761
+ mergeTheme,
2762
+ themeToCssVars,
2763
+ useConversations,
2764
+ useFileUpload,
2765
+ useJokkooContext,
2766
+ useMessages,
2767
+ useRealtimeStatus,
2768
+ useSendMessage,
2769
+ useUnreadCount,
2770
+ useVoiceRecorder
2771
+ });
2772
+ //# sourceMappingURL=index.js.map