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