@cossistant/react 0.0.31 → 0.0.33
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/hooks/index.d.ts +2 -2
- package/hooks/index.js +2 -2
- package/hooks/private/use-grouped-messages.d.ts +27 -2
- package/hooks/private/use-grouped-messages.d.ts.map +1 -1
- package/hooks/private/use-grouped-messages.js +156 -103
- package/hooks/private/use-grouped-messages.js.map +1 -1
- package/hooks/use-new-message-sound.d.ts.map +1 -1
- package/hooks/use-new-message-sound.js +2 -2
- package/hooks/use-new-message-sound.js.map +1 -1
- package/hooks/use-send-message.js +1 -1
- package/hooks/use-send-message.js.map +1 -1
- package/hooks/use-typing-sound.d.ts.map +1 -1
- package/hooks/use-typing-sound.js +2 -2
- package/hooks/use-typing-sound.js.map +1 -1
- package/index.d.ts +2 -2
- package/index.js +2 -2
- package/package.json +4 -6
- package/packages/tiny-markdown/src/context/index.d.ts +1 -0
- package/packages/tiny-markdown/src/context/tiny-markdown-context.d.ts +3 -0
- package/packages/tiny-markdown/src/hooks/index.d.ts +4 -0
- package/packages/tiny-markdown/src/hooks/use-caret-position.d.ts +1 -0
- package/packages/tiny-markdown/src/hooks/use-tiny-markdown.d.ts +1 -0
- package/packages/tiny-markdown/src/hooks/use-tiny-mention.d.ts +1 -0
- package/packages/tiny-markdown/src/hooks/use-tiny-shortcuts.d.ts +1 -0
- package/packages/tiny-markdown/src/index.d.ts +4 -0
- package/packages/tiny-markdown/src/types.d.ts +75 -0
- package/packages/tiny-markdown/src/types.d.ts.map +1 -0
- package/packages/tiny-markdown/src/utils/index.d.ts +3 -0
- package/packages/tiny-markdown/src/utils/markdown-parser.d.ts +1 -0
- package/packages/tiny-markdown/src/utils/mention-parser.d.ts +1 -0
- package/packages/tiny-markdown/src/utils/merge-refs.d.ts +1 -0
- package/packages/types/src/api/conversation.d.ts +316 -12
- package/packages/types/src/api/conversation.d.ts.map +1 -1
- package/packages/types/src/api/timeline-item.d.ts +236 -9
- package/packages/types/src/api/timeline-item.d.ts.map +1 -1
- package/packages/types/src/realtime-events.d.ts +259 -13
- package/packages/types/src/realtime-events.d.ts.map +1 -1
- package/packages/types/src/schemas.d.ts +79 -3
- package/packages/types/src/schemas.d.ts.map +1 -1
- package/primitives/avatar/image.d.ts +1 -1
- package/primitives/command-block-utils.d.ts +26 -0
- package/primitives/command-block-utils.d.ts.map +1 -0
- package/primitives/command-block-utils.js +310 -0
- package/primitives/command-block-utils.js.map +1 -0
- package/primitives/index.d.ts +7 -3
- package/primitives/index.js +11 -2
- package/primitives/index.parts.d.ts +6 -2
- package/primitives/index.parts.js +5 -1
- package/primitives/multimodal-input.d.ts +2 -2
- package/primitives/multimodal-input.d.ts.map +1 -1
- package/primitives/timeline-code-block.d.ts +32 -0
- package/primitives/timeline-code-block.d.ts.map +1 -0
- package/primitives/timeline-code-block.js +66 -0
- package/primitives/timeline-code-block.js.map +1 -0
- package/primitives/timeline-command-block.d.ts +29 -0
- package/primitives/timeline-command-block.d.ts.map +1 -0
- package/primitives/timeline-command-block.js +97 -0
- package/primitives/timeline-command-block.js.map +1 -0
- package/primitives/timeline-item-group.d.ts.map +1 -1
- package/primitives/timeline-item-group.js +5 -15
- package/primitives/timeline-item-group.js.map +1 -1
- package/primitives/timeline-item.d.ts +23 -3
- package/primitives/timeline-item.d.ts.map +1 -1
- package/primitives/timeline-item.js +151 -80
- package/primitives/timeline-item.js.map +1 -1
- package/primitives/timeline-message-layout.d.ts +9 -0
- package/primitives/timeline-message-layout.d.ts.map +1 -0
- package/primitives/timeline-message-layout.js +20 -0
- package/primitives/timeline-message-layout.js.map +1 -0
- package/provider.d.ts.map +1 -1
- package/provider.js +6 -3
- package/provider.js.map +1 -1
- package/realtime/event-filter.js +4 -3
- package/realtime/event-filter.js.map +1 -1
- package/sounds/sound-data.d.ts +6 -0
- package/sounds/sound-data.d.ts.map +1 -0
- package/sounds/sound-data.js +7 -0
- package/sounds/sound-data.js.map +1 -0
- package/support/components/avatar-stack.js +1 -1
- package/support/components/avatar-stack.js.map +1 -1
- package/support/components/avatar.d.ts +1 -2
- package/support/components/avatar.d.ts.map +1 -1
- package/support/components/avatar.js +9 -7
- package/support/components/avatar.js.map +1 -1
- package/support/components/button.d.ts +2 -2
- package/support/components/button.d.ts.map +1 -1
- package/support/components/button.js +1 -0
- package/support/components/button.js.map +1 -1
- package/support/components/conversation-button-link.js +2 -1
- package/support/components/conversation-button-link.js.map +1 -1
- package/support/components/conversation-event.d.ts +3 -0
- package/support/components/conversation-event.d.ts.map +1 -1
- package/support/components/conversation-event.js +46 -15
- package/support/components/conversation-event.js.map +1 -1
- package/support/components/conversation-resolved-feedback.d.ts +1 -1
- package/support/components/conversation-resolved-feedback.d.ts.map +1 -1
- package/support/components/conversation-resolved-feedback.js +56 -13
- package/support/components/conversation-resolved-feedback.js.map +1 -1
- package/support/components/conversation-timeline.d.ts.map +1 -1
- package/support/components/conversation-timeline.js +12 -0
- package/support/components/conversation-timeline.js.map +1 -1
- package/support/components/index.d.ts +2 -1
- package/support/components/index.js +2 -1
- package/support/components/timeline-activity-group.d.ts +25 -0
- package/support/components/timeline-activity-group.d.ts.map +1 -0
- package/support/components/timeline-activity-group.js +104 -0
- package/support/components/timeline-activity-group.js.map +1 -0
- package/support/components/timeline-code-block.d.ts +14 -0
- package/support/components/timeline-code-block.d.ts.map +1 -0
- package/support/components/timeline-code-block.js +44 -0
- package/support/components/timeline-code-block.js.map +1 -0
- package/support/components/timeline-command-block.d.ts +12 -0
- package/support/components/timeline-command-block.d.ts.map +1 -0
- package/support/components/timeline-command-block.js +42 -0
- package/support/components/timeline-command-block.js.map +1 -0
- package/support/components/timeline-message-item.d.ts +2 -1
- package/support/components/timeline-message-item.d.ts.map +1 -1
- package/support/components/timeline-message-item.js +23 -3
- package/support/components/timeline-message-item.js.map +1 -1
- package/support/components/typing-indicator.d.ts.map +1 -1
- package/support/components/typing-indicator.js +15 -7
- package/support/components/typing-indicator.js.map +1 -1
- package/support/index.d.ts +4 -4
- package/support/pages/conversation-history.js +1 -1
- package/support/pages/conversation.js +4 -2
- package/support/pages/conversation.js.map +1 -1
- package/support/pages/home.js +1 -1
- package/support/store/support-store.d.ts +5 -5
- package/support/text/locales/en.js +3 -0
- package/support/text/locales/en.js.map +1 -1
- package/support/text/locales/es.js +3 -0
- package/support/text/locales/es.js.map +1 -1
- package/support/text/locales/fr.js +3 -0
- package/support/text/locales/fr.js.map +1 -1
- package/support/text/locales/keys.d.ts +9 -0
- package/support/text/locales/keys.d.ts.map +1 -1
- package/support/text/locales/keys.js +3 -0
- package/support/text/locales/keys.js.map +1 -1
- package/utils/metadata-hash.d.ts +1 -1
- package/utils/metadata-hash.js +9 -4
- package/utils/metadata-hash.js.map +1 -1
- package/utils/timeline-item-sender.d.ts +17 -0
- package/utils/timeline-item-sender.d.ts.map +1 -0
- package/utils/timeline-item-sender.js +43 -0
- package/utils/timeline-item-sender.js.map +1 -0
package/hooks/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useClientQuery } from "./private/use-client-query.js";
|
|
2
2
|
import { useDefaultMessages } from "./private/use-default-messages.js";
|
|
3
|
-
import { ConversationItem, DaySeparatorItem, GroupedMessage, TimelineEventItem, TimelineToolItem, UseGroupedMessagesOptions, UseGroupedMessagesProps, useGroupedMessages } from "./private/use-grouped-messages.js";
|
|
3
|
+
import { ConversationItem, DaySeparatorItem, GroupedActivity, GroupedMessage, PreparedTimelineItems, TIMELINE_GROUP_WINDOW_MS, TimelineEventItem, TimelineToolItem, UseGroupedMessagesOptions, UseGroupedMessagesProps, buildTimelineReadReceiptData, groupTimelineItems, prepareTimelineItems, useGroupedMessages } from "./private/use-grouped-messages.js";
|
|
4
4
|
import { UseMultimodalInputOptions, UseMultimodalInputReturn, useMultimodalInput } from "./private/use-multimodal-input.js";
|
|
5
5
|
import { ConfigurationError, UseClientResult, useClient } from "./private/use-rest-client.js";
|
|
6
6
|
import { UseComposerRefocusOptions, UseComposerRefocusReturn, useComposerRefocus } from "./use-composer-refocus.js";
|
|
@@ -27,4 +27,4 @@ import { UseSoundEffectOptions, UseSoundEffectReturn, useSoundEffect } from "./u
|
|
|
27
27
|
import { useTypingSound } from "./use-typing-sound.js";
|
|
28
28
|
import { UseVisitorReturn, useVisitor } from "./use-visitor.js";
|
|
29
29
|
import { WindowVisibilityFocusState, useWindowVisibilityFocus } from "./use-window-visibility-focus.js";
|
|
30
|
-
export { CONVERSATION_AUTO_SEEN_DELAY_MS, ConfigurationError, ConversationItem, ConversationLifecycleState, ConversationPreviewAssignedAgent, ConversationPreviewLastMessage, ConversationPreviewTypingParticipant, ConversationPreviewTypingState, ConversationTimelineTypingParticipant, ConversationTypingParticipant, CreateConversationVariables, DaySeparatorItem, FileUploadPart, GroupedMessage, SendMessageOptions, SendMessageResult, TimelineEventItem, TimelineToolItem, UseClientResult, UseComposerRefocusOptions, UseComposerRefocusReturn, UseConversationAutoSeenOptions, UseConversationHistoryPageOptions, UseConversationHistoryPageReturn, UseConversationLifecycleOptions, UseConversationLifecycleReturn, UseConversationOptions, UseConversationPageOptions, UseConversationPageReturn, UseConversationPreviewOptions, UseConversationPreviewReturn, UseConversationResult, UseConversationTimelineItemsOptions, UseConversationTimelineItemsResult, UseConversationTimelineOptions, UseConversationTimelineReturn, UseConversationsOptions, UseConversationsResult, UseCreateConversationOptions, UseCreateConversationResult, UseFileUploadOptions, UseFileUploadReturn, UseGroupedMessagesOptions, UseGroupedMessagesProps, UseHomePageOptions, UseHomePageReturn, UseMessageComposerOptions, UseMessageComposerReturn, UseMultimodalInputOptions, UseMultimodalInputReturn, UseRealtimeSupportOptions, UseRealtimeSupportResult, UseScrollMaskOptions, UseScrollMaskReturn, UseSendMessageOptions, UseSendMessageResult, UseSoundEffectOptions, UseSoundEffectReturn, UseVisitorReturn, WindowVisibilityFocusState, useClient, useClientQuery, useComposerRefocus, useConversation, useConversationAutoSeen, useConversationHistoryPage, useConversationLifecycle, useConversationPage, useConversationPreview, useConversationSeen, useConversationTimeline, useConversationTimelineItems, useConversationTyping, useConversations, useCreateConversation, useDebouncedConversationSeen, useDefaultMessages, useFileUpload, useGroupedMessages, useHomePage, useMessageComposer, useMultimodalInput, useNewMessageSound, useRealtimeSupport, useScrollMask, useSendMessage, useSoundEffect, useTypingSound, useVisitor, useWindowVisibilityFocus };
|
|
30
|
+
export { CONVERSATION_AUTO_SEEN_DELAY_MS, ConfigurationError, ConversationItem, ConversationLifecycleState, ConversationPreviewAssignedAgent, ConversationPreviewLastMessage, ConversationPreviewTypingParticipant, ConversationPreviewTypingState, ConversationTimelineTypingParticipant, ConversationTypingParticipant, CreateConversationVariables, DaySeparatorItem, FileUploadPart, GroupedActivity, GroupedMessage, PreparedTimelineItems, SendMessageOptions, SendMessageResult, TIMELINE_GROUP_WINDOW_MS, TimelineEventItem, TimelineToolItem, UseClientResult, UseComposerRefocusOptions, UseComposerRefocusReturn, UseConversationAutoSeenOptions, UseConversationHistoryPageOptions, UseConversationHistoryPageReturn, UseConversationLifecycleOptions, UseConversationLifecycleReturn, UseConversationOptions, UseConversationPageOptions, UseConversationPageReturn, UseConversationPreviewOptions, UseConversationPreviewReturn, UseConversationResult, UseConversationTimelineItemsOptions, UseConversationTimelineItemsResult, UseConversationTimelineOptions, UseConversationTimelineReturn, UseConversationsOptions, UseConversationsResult, UseCreateConversationOptions, UseCreateConversationResult, UseFileUploadOptions, UseFileUploadReturn, UseGroupedMessagesOptions, UseGroupedMessagesProps, UseHomePageOptions, UseHomePageReturn, UseMessageComposerOptions, UseMessageComposerReturn, UseMultimodalInputOptions, UseMultimodalInputReturn, UseRealtimeSupportOptions, UseRealtimeSupportResult, UseScrollMaskOptions, UseScrollMaskReturn, UseSendMessageOptions, UseSendMessageResult, UseSoundEffectOptions, UseSoundEffectReturn, UseVisitorReturn, WindowVisibilityFocusState, buildTimelineReadReceiptData, groupTimelineItems, prepareTimelineItems, useClient, useClientQuery, useComposerRefocus, useConversation, useConversationAutoSeen, useConversationHistoryPage, useConversationLifecycle, useConversationPage, useConversationPreview, useConversationSeen, useConversationTimeline, useConversationTimelineItems, useConversationTyping, useConversations, useCreateConversation, useDebouncedConversationSeen, useDefaultMessages, useFileUpload, useGroupedMessages, useHomePage, useMessageComposer, useMultimodalInput, useNewMessageSound, useRealtimeSupport, useScrollMask, useSendMessage, useSoundEffect, useTypingSound, useVisitor, useWindowVisibilityFocus };
|
package/hooks/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { useMultimodalInput } from "./private/use-multimodal-input.js";
|
|
|
12
12
|
import { useSendMessage } from "./use-send-message.js";
|
|
13
13
|
import { useMessageComposer } from "./use-message-composer.js";
|
|
14
14
|
import { useConversationPage } from "./use-conversation-page.js";
|
|
15
|
-
import { useGroupedMessages } from "./private/use-grouped-messages.js";
|
|
15
|
+
import { TIMELINE_GROUP_WINDOW_MS, buildTimelineReadReceiptData, groupTimelineItems, prepareTimelineItems, useGroupedMessages } from "./private/use-grouped-messages.js";
|
|
16
16
|
import { useConversationSeen, useDebouncedConversationSeen } from "./use-conversation-seen.js";
|
|
17
17
|
import { useConversationTyping } from "./use-conversation-typing.js";
|
|
18
18
|
import { useConversationTimeline } from "./use-conversation-timeline.js";
|
|
@@ -28,4 +28,4 @@ import { useCreateConversation } from "./use-create-conversation.js";
|
|
|
28
28
|
import { useFileUpload } from "./use-file-upload.js";
|
|
29
29
|
import { useRealtimeSupport } from "./use-realtime-support.js";
|
|
30
30
|
|
|
31
|
-
export { CONVERSATION_AUTO_SEEN_DELAY_MS, useClient, useClientQuery, useComposerRefocus, useConversation, useConversationAutoSeen, useConversationHistoryPage, useConversationLifecycle, useConversationPage, useConversationPreview, useConversationSeen, useConversationTimeline, useConversationTimelineItems, useConversationTyping, useConversations, useCreateConversation, useDebouncedConversationSeen, useDefaultMessages, useFileUpload, useGroupedMessages, useHomePage, useMessageComposer, useMultimodalInput, useNewMessageSound, useRealtimeSupport, useScrollMask, useSendMessage, useSoundEffect, useTypingSound, useVisitor, useWindowVisibilityFocus };
|
|
31
|
+
export { CONVERSATION_AUTO_SEEN_DELAY_MS, TIMELINE_GROUP_WINDOW_MS, buildTimelineReadReceiptData, groupTimelineItems, prepareTimelineItems, useClient, useClientQuery, useComposerRefocus, useConversation, useConversationAutoSeen, useConversationHistoryPage, useConversationLifecycle, useConversationPage, useConversationPreview, useConversationSeen, useConversationTimeline, useConversationTimelineItems, useConversationTyping, useConversations, useCreateConversation, useDebouncedConversationSeen, useDefaultMessages, useFileUpload, useGroupedMessages, useHomePage, useMessageComposer, useMultimodalInput, useNewMessageSound, useRealtimeSupport, useScrollMask, useSendMessage, useSoundEffect, useTypingSound, useVisitor, useWindowVisibilityFocus };
|
|
@@ -13,6 +13,18 @@ type GroupedMessage = {
|
|
|
13
13
|
firstMessageTime: Date;
|
|
14
14
|
lastMessageTime: Date;
|
|
15
15
|
};
|
|
16
|
+
type GroupedActivity = {
|
|
17
|
+
type: "activity_group";
|
|
18
|
+
senderId: string;
|
|
19
|
+
senderType: SenderType;
|
|
20
|
+
items: TimelineItem[];
|
|
21
|
+
firstItemId: string;
|
|
22
|
+
lastItemId: string;
|
|
23
|
+
firstItemTime: Date;
|
|
24
|
+
lastItemTime: Date;
|
|
25
|
+
hasEvent: boolean;
|
|
26
|
+
hasTool: boolean;
|
|
27
|
+
};
|
|
16
28
|
type TimelineEventItem = {
|
|
17
29
|
type: "timeline_event";
|
|
18
30
|
item: TimelineItem;
|
|
@@ -29,13 +41,26 @@ type DaySeparatorItem = {
|
|
|
29
41
|
date: Date;
|
|
30
42
|
dateString: string;
|
|
31
43
|
};
|
|
32
|
-
type ConversationItem = GroupedMessage | TimelineEventItem | TimelineToolItem | DaySeparatorItem;
|
|
44
|
+
type ConversationItem = GroupedMessage | GroupedActivity | TimelineEventItem | TimelineToolItem | DaySeparatorItem;
|
|
33
45
|
type UseGroupedMessagesOptions = {
|
|
34
46
|
items: TimelineItem[];
|
|
35
47
|
seenData?: ConversationSeen[];
|
|
36
48
|
currentViewerId?: string;
|
|
37
49
|
};
|
|
38
50
|
type UseGroupedMessagesProps = UseGroupedMessagesOptions;
|
|
51
|
+
type PreparedTimelineItems = {
|
|
52
|
+
items: TimelineItem[];
|
|
53
|
+
times: number[];
|
|
54
|
+
didSort: boolean;
|
|
55
|
+
};
|
|
56
|
+
declare const TIMELINE_GROUP_WINDOW_MS: number;
|
|
57
|
+
declare const prepareTimelineItems: (items: TimelineItem[]) => PreparedTimelineItems;
|
|
58
|
+
declare const groupTimelineItems: (items: TimelineItem[], itemTimes: number[]) => ConversationItem[];
|
|
59
|
+
declare const buildTimelineReadReceiptData: (seenData: ConversationSeen[], sortedMessageItems: TimelineItem[], sortedMessageTimes: number[]) => {
|
|
60
|
+
seenByMap: Map<string, Set<string>>;
|
|
61
|
+
lastReadMessageMap: Map<string, string>;
|
|
62
|
+
unreadCountMap: Map<string, number>;
|
|
63
|
+
};
|
|
39
64
|
/**
|
|
40
65
|
* Batches sequential timeline items from the same sender into groups and enriches
|
|
41
66
|
* them with read-receipt helpers so UIs can render conversation timelines with
|
|
@@ -59,5 +84,5 @@ declare const useGroupedMessages: ({
|
|
|
59
84
|
hasUnreadAfter: (messageId: string, userId: string) => boolean;
|
|
60
85
|
};
|
|
61
86
|
//#endregion
|
|
62
|
-
export { ConversationItem, DaySeparatorItem, GroupedMessage, TimelineEventItem, TimelineToolItem, UseGroupedMessagesOptions, UseGroupedMessagesProps, useGroupedMessages };
|
|
87
|
+
export { ConversationItem, DaySeparatorItem, GroupedActivity, GroupedMessage, PreparedTimelineItems, TIMELINE_GROUP_WINDOW_MS, TimelineEventItem, TimelineToolItem, UseGroupedMessagesOptions, UseGroupedMessagesProps, buildTimelineReadReceiptData, groupTimelineItems, prepareTimelineItems, useGroupedMessages };
|
|
63
88
|
//# sourceMappingURL=use-grouped-messages.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-grouped-messages.d.ts","names":[],"sources":["../../../src/hooks/private/use-grouped-messages.ts"],"sourcesContent":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"use-grouped-messages.d.ts","names":[],"sources":["../../../src/hooks/private/use-grouped-messages.ts"],"sourcesContent":[],"mappings":";;;;;KAMY,cAAA;;EAAA,QAAA,EAAA,MAAA;EAGC,UAAA,EAAA,UAAA;EACL,KAAA,EAAA,YAAA,EAAA;EAGW,cAAA,EAAA,MAAA;EACD,aAAA,EAAA,MAAA;EAAI,gBAAA,EADH,IACG;EAGV,eAAA,EAHM,IAGS;CAGd;AACL,KAJI,eAAA,GAIJ;EAGQ,IAAA,EAAA,gBAAA;EACD,QAAA,EAAA,MAAA;EAAI,UAAA,EALN,UAKM;EAKP,KAAA,EATJ,YASI,EAAiB;EAMjB,WAAA,EAAA,MAAgB;EAOhB,UAAA,EAAA,MAAA;EAMA,aAAA,EAzBI,IAyBY;EACzB,YAAA,EAzBY,IAyBZ;EACA,QAAA,EAAA,OAAA;EACA,OAAA,EAAA,OAAA;CACA;AACA,KAxBS,iBAAA,GAwBT;EAAgB,IAAA,EAAA,gBAAA;EAEP,IAAA,EAxBL,YAwBK;EAMA,SAAA,EA7BA,IA6BA;AAEZ,CAAA;AAMa,KAlCD,gBAAA,GAkCyC;EA2ExC,IAAA,EAAA,eAAA;EAmEA,IAAA,EA9KN,YA8KM;EAiKA,IAAA,EAAA,MAAA,GAAA,IAAA;EACF,SAAA,EA9UC,IA8UD;CACU;KA5UT,gBAAA;;QAEL;;;AA0YM,KAtYD,gBAAA,GACT,cAyeF,GAxeE,eAweF,GAveE,iBAueF,GAteE,gBAseF,GAreE,gBAqeF;AApGkC,KA/XvB,yBAAA,GA+XuB;EAAA,KAAA,EA9X3B,YA8X2B,EAAA;EAAA,QAAA,CAAA,EA7XvB,gBA6XuB,EAAA;EAIhC,eAAA,CAAA,EAAA,MAAA;;KA7XS,uBAAA,GAA0B;KAE1B,qBAAA;SACJ;;;;cAKK;cA2EA,8BACL,mBACL;cAiEU,4BACL,wCAEL;cA8JU,yCACF,wCACU;;;;;;;;;;;cAgER;;;;GAIV"}
|
|
@@ -1,17 +1,13 @@
|
|
|
1
|
+
import { getTimelineItemSender } from "../../utils/timeline-item-sender.js";
|
|
1
2
|
import { useMemo } from "react";
|
|
2
|
-
import { SenderType } from "@cossistant/types";
|
|
3
3
|
|
|
4
4
|
//#region src/hooks/private/use-grouped-messages.ts
|
|
5
|
+
const TIMELINE_GROUP_WINDOW_MS = 300 * 1e3;
|
|
5
6
|
const getTimestamp = (date) => {
|
|
6
7
|
if (!date) return 0;
|
|
7
8
|
if (typeof date === "string") return new Date(date).getTime();
|
|
8
9
|
return date.getTime();
|
|
9
10
|
};
|
|
10
|
-
const toDate = (date) => {
|
|
11
|
-
if (!date) return typeof window !== "undefined" ? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date(0);
|
|
12
|
-
if (typeof date === "string") return new Date(date);
|
|
13
|
-
return date;
|
|
14
|
-
};
|
|
15
11
|
const getDateString = (date) => {
|
|
16
12
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
17
13
|
};
|
|
@@ -19,80 +15,104 @@ const createDayDate = (dateString) => {
|
|
|
19
15
|
const [year, month, day] = dateString.split("-").map(Number);
|
|
20
16
|
return new Date(year ?? 0, (month ?? 1) - 1, day ?? 1, 0, 0, 0, 0);
|
|
21
17
|
};
|
|
22
|
-
const
|
|
23
|
-
if (item.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
const getToolNameFromTimelineItem = (item) => {
|
|
19
|
+
if (item.tool) return item.tool;
|
|
20
|
+
for (const part of item.parts) if (typeof part === "object" && part !== null && "type" in part && "toolName" in part && typeof part.type === "string" && part.type.startsWith("tool-") && typeof part.toolName === "string") return part.toolName;
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
23
|
+
const EMPTY_STRING_ARRAY = Object.freeze([]);
|
|
24
|
+
function getGroupableTimelineItemType(item) {
|
|
25
|
+
if (item.type === "message") return "message";
|
|
26
|
+
if (item.type === "event" || item.type === "tool") return "activity";
|
|
27
|
+
if (item.type === "identification") return "standalone_tool";
|
|
28
|
+
return "standalone_event";
|
|
29
|
+
}
|
|
30
|
+
const prepareTimelineItems = (items) => {
|
|
31
|
+
if (items.length <= 1) return {
|
|
32
|
+
items,
|
|
33
|
+
times: items.map((item) => getTimestamp(item.createdAt)),
|
|
34
|
+
didSort: false
|
|
30
35
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
const times = new Array(items.length);
|
|
37
|
+
let isSorted = true;
|
|
38
|
+
for (let index = 0; index < items.length; index++) {
|
|
39
|
+
const item = items[index];
|
|
40
|
+
const time = getTimestamp(item?.createdAt);
|
|
41
|
+
times[index] = time;
|
|
42
|
+
if (index === 0) continue;
|
|
43
|
+
const previousTime = times[index - 1];
|
|
44
|
+
if (previousTime !== void 0 && time !== void 0 && time < previousTime) isSorted = false;
|
|
45
|
+
}
|
|
46
|
+
if (isSorted) return {
|
|
47
|
+
items,
|
|
48
|
+
times,
|
|
49
|
+
didSort: false
|
|
34
50
|
};
|
|
51
|
+
const entries = items.map((item, index) => ({
|
|
52
|
+
item,
|
|
53
|
+
time: times[index] ?? 0,
|
|
54
|
+
index
|
|
55
|
+
}));
|
|
56
|
+
entries.sort((a, b) => {
|
|
57
|
+
if (a.time === b.time) return a.index - b.index;
|
|
58
|
+
return a.time - b.time;
|
|
59
|
+
});
|
|
35
60
|
return {
|
|
36
|
-
|
|
37
|
-
|
|
61
|
+
items: entries.map((entry) => entry.item),
|
|
62
|
+
times: entries.map((entry) => entry.time),
|
|
63
|
+
didSort: true
|
|
38
64
|
};
|
|
39
65
|
};
|
|
40
|
-
const
|
|
41
|
-
const groupTimelineItems = (items) => {
|
|
66
|
+
const isWithinGroupingWindow = (previousTimestamp, currentTimestamp) => currentTimestamp - previousTimestamp <= TIMELINE_GROUP_WINDOW_MS;
|
|
67
|
+
const groupTimelineItems = (items, itemTimes) => {
|
|
42
68
|
const result = [];
|
|
43
|
-
let
|
|
69
|
+
let currentMessageGroup = null;
|
|
70
|
+
let currentActivityGroup = null;
|
|
44
71
|
let currentDayString = null;
|
|
72
|
+
const flushMessageGroup = () => {
|
|
73
|
+
if (!currentMessageGroup) return;
|
|
74
|
+
result.push(currentMessageGroup);
|
|
75
|
+
currentMessageGroup = null;
|
|
76
|
+
};
|
|
77
|
+
const flushActivityGroup = () => {
|
|
78
|
+
if (!currentActivityGroup) return;
|
|
79
|
+
result.push(currentActivityGroup);
|
|
80
|
+
currentActivityGroup = null;
|
|
81
|
+
};
|
|
82
|
+
const flushAllGroups = () => {
|
|
83
|
+
flushMessageGroup();
|
|
84
|
+
flushActivityGroup();
|
|
85
|
+
};
|
|
45
86
|
const maybeInsertDaySeparator = (itemDate) => {
|
|
46
87
|
const itemDayString = getDateString(itemDate);
|
|
47
|
-
if (currentDayString
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
dateString: itemDayString
|
|
56
|
-
});
|
|
57
|
-
currentDayString = itemDayString;
|
|
58
|
-
}
|
|
88
|
+
if (currentDayString === itemDayString) return;
|
|
89
|
+
flushAllGroups();
|
|
90
|
+
result.push({
|
|
91
|
+
type: "day_separator",
|
|
92
|
+
date: createDayDate(itemDayString),
|
|
93
|
+
dateString: itemDayString
|
|
94
|
+
});
|
|
95
|
+
currentDayString = itemDayString;
|
|
59
96
|
};
|
|
60
|
-
for (
|
|
61
|
-
const
|
|
97
|
+
for (let index = 0; index < items.length; index++) {
|
|
98
|
+
const item = items[index];
|
|
99
|
+
if (!item) continue;
|
|
100
|
+
const itemTimestamp = itemTimes[index] ?? getTimestamp(item.createdAt);
|
|
101
|
+
const itemDate = new Date(itemTimestamp);
|
|
62
102
|
maybeInsertDaySeparator(itemDate);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
103
|
+
const groupableType = getGroupableTimelineItemType(item);
|
|
104
|
+
if (groupableType === "message") {
|
|
105
|
+
flushActivityGroup();
|
|
106
|
+
const { senderId, senderType } = getTimelineItemSender(item);
|
|
107
|
+
const previousTimestamp = currentMessageGroup?.lastMessageTime.getTime();
|
|
108
|
+
if (Boolean(currentMessageGroup && currentMessageGroup.senderId === senderId && previousTimestamp !== void 0 && isWithinGroupingWindow(previousTimestamp, itemTimestamp)) && currentMessageGroup) {
|
|
109
|
+
currentMessageGroup.items.push(item);
|
|
110
|
+
currentMessageGroup.lastMessageId = item.id || currentMessageGroup.lastMessageId;
|
|
111
|
+
currentMessageGroup.lastMessageTime = itemDate;
|
|
112
|
+
continue;
|
|
67
113
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
item,
|
|
71
|
-
timestamp: itemDate
|
|
72
|
-
});
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
if (item.type === "identification") {
|
|
76
|
-
if (currentGroup) {
|
|
77
|
-
result.push(currentGroup);
|
|
78
|
-
currentGroup = null;
|
|
79
|
-
}
|
|
80
|
-
result.push({
|
|
81
|
-
type: "timeline_tool",
|
|
82
|
-
item,
|
|
83
|
-
tool: item.tool ?? null,
|
|
84
|
-
timestamp: itemDate
|
|
85
|
-
});
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
const { senderId, senderType } = getSenderIdAndTypeFromTimelineItem(item);
|
|
89
|
-
if (currentGroup && currentGroup.senderId === senderId) {
|
|
90
|
-
currentGroup.items.push(item);
|
|
91
|
-
currentGroup.lastMessageId = item.id || currentGroup.lastMessageId;
|
|
92
|
-
currentGroup.lastMessageTime = itemDate;
|
|
93
|
-
} else {
|
|
94
|
-
if (currentGroup) result.push(currentGroup);
|
|
95
|
-
currentGroup = {
|
|
114
|
+
flushMessageGroup();
|
|
115
|
+
currentMessageGroup = {
|
|
96
116
|
type: "message_group",
|
|
97
117
|
senderId,
|
|
98
118
|
senderType,
|
|
@@ -102,16 +122,64 @@ const groupTimelineItems = (items) => {
|
|
|
102
122
|
firstMessageTime: itemDate,
|
|
103
123
|
lastMessageTime: itemDate
|
|
104
124
|
};
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (groupableType === "activity") {
|
|
128
|
+
flushMessageGroup();
|
|
129
|
+
const { senderId, senderType } = getTimelineItemSender(item);
|
|
130
|
+
const previousTimestamp = currentActivityGroup?.lastItemTime.getTime();
|
|
131
|
+
if (Boolean(currentActivityGroup && currentActivityGroup.senderId === senderId && previousTimestamp !== void 0 && isWithinGroupingWindow(previousTimestamp, itemTimestamp)) && currentActivityGroup) {
|
|
132
|
+
currentActivityGroup.items.push(item);
|
|
133
|
+
currentActivityGroup.lastItemId = item.id || currentActivityGroup.lastItemId;
|
|
134
|
+
currentActivityGroup.lastItemTime = itemDate;
|
|
135
|
+
currentActivityGroup.hasEvent = currentActivityGroup.hasEvent || item.type === "event";
|
|
136
|
+
currentActivityGroup.hasTool = currentActivityGroup.hasTool || item.type === "tool";
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
flushActivityGroup();
|
|
140
|
+
currentActivityGroup = {
|
|
141
|
+
type: "activity_group",
|
|
142
|
+
senderId,
|
|
143
|
+
senderType,
|
|
144
|
+
items: [item],
|
|
145
|
+
firstItemId: item.id || "",
|
|
146
|
+
lastItemId: item.id || "",
|
|
147
|
+
firstItemTime: itemDate,
|
|
148
|
+
lastItemTime: itemDate,
|
|
149
|
+
hasEvent: item.type === "event",
|
|
150
|
+
hasTool: item.type === "tool"
|
|
151
|
+
};
|
|
152
|
+
continue;
|
|
105
153
|
}
|
|
154
|
+
flushAllGroups();
|
|
155
|
+
if (groupableType === "standalone_tool") {
|
|
156
|
+
result.push({
|
|
157
|
+
type: "timeline_tool",
|
|
158
|
+
item,
|
|
159
|
+
tool: getToolNameFromTimelineItem(item),
|
|
160
|
+
timestamp: itemDate
|
|
161
|
+
});
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
result.push({
|
|
165
|
+
type: "timeline_event",
|
|
166
|
+
item,
|
|
167
|
+
timestamp: itemDate
|
|
168
|
+
});
|
|
106
169
|
}
|
|
107
|
-
|
|
170
|
+
flushAllGroups();
|
|
108
171
|
return result;
|
|
109
172
|
};
|
|
110
|
-
const buildTimelineReadReceiptData = (seenData,
|
|
173
|
+
const buildTimelineReadReceiptData = (seenData, sortedMessageItems, sortedMessageTimes) => {
|
|
111
174
|
const seenByMap = /* @__PURE__ */ new Map();
|
|
112
175
|
const lastReadMessageMap = /* @__PURE__ */ new Map();
|
|
113
176
|
const unreadCountMap = /* @__PURE__ */ new Map();
|
|
114
|
-
for (const item of
|
|
177
|
+
for (const item of sortedMessageItems) if (item.id) seenByMap.set(item.id, /* @__PURE__ */ new Set());
|
|
178
|
+
if (seenData.length === 0 || sortedMessageItems.length === 0) return {
|
|
179
|
+
seenByMap,
|
|
180
|
+
lastReadMessageMap,
|
|
181
|
+
unreadCountMap
|
|
182
|
+
};
|
|
115
183
|
for (const seen of seenData) {
|
|
116
184
|
const seenTime = getTimestamp(seen.lastSeenAt);
|
|
117
185
|
const viewerId = seen.userId || seen.visitorId || seen.aiAgentId;
|
|
@@ -122,12 +190,11 @@ const buildTimelineReadReceiptData = (seenData, items, sortedMessageItems, sorte
|
|
|
122
190
|
const item = sortedMessageItems[index];
|
|
123
191
|
if (!item) continue;
|
|
124
192
|
if ((sortedMessageTimes[index] ?? getTimestamp(item.createdAt)) <= seenTime) {
|
|
125
|
-
if (item.id)
|
|
126
|
-
const seenBy = seenByMap.get(item.id);
|
|
127
|
-
if (seenBy) seenBy.add(viewerId);
|
|
128
|
-
}
|
|
193
|
+
if (item.id) seenByMap.get(item.id)?.add(viewerId);
|
|
129
194
|
lastReadItem = item;
|
|
130
|
-
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
unreadCount++;
|
|
131
198
|
}
|
|
132
199
|
if (lastReadItem?.id) lastReadMessageMap.set(viewerId, lastReadItem.id);
|
|
133
200
|
unreadCountMap.set(viewerId, unreadCount);
|
|
@@ -146,34 +213,20 @@ const buildTimelineReadReceiptData = (seenData, items, sortedMessageItems, sorte
|
|
|
146
213
|
*/
|
|
147
214
|
const useGroupedMessages = ({ items, seenData = [], currentViewerId }) => {
|
|
148
215
|
return useMemo(() => {
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
let isSorted = true;
|
|
154
|
-
for (let index = 1; index < sortedMessageTimes.length; index++) {
|
|
155
|
-
const currentTime = sortedMessageTimes[index];
|
|
156
|
-
const previousTime = sortedMessageTimes[index - 1];
|
|
157
|
-
if (currentTime !== void 0 && previousTime !== void 0 && currentTime < previousTime) {
|
|
158
|
-
isSorted = false;
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (!isSorted) {
|
|
163
|
-
const itemsWithTimes = messageItems.map((item, index) => ({
|
|
164
|
-
item,
|
|
165
|
-
time: sortedMessageTimes[index] ?? 0
|
|
166
|
-
}));
|
|
167
|
-
itemsWithTimes.sort((a, b) => a.time - b.time);
|
|
168
|
-
sortedMessageItems = itemsWithTimes.map((entry) => entry.item);
|
|
169
|
-
sortedMessageTimes = itemsWithTimes.map((entry) => entry.time);
|
|
170
|
-
}
|
|
216
|
+
const preparedItems = prepareTimelineItems(items);
|
|
217
|
+
const groupedItems = groupTimelineItems(preparedItems.items, preparedItems.times);
|
|
218
|
+
const sortedMessageItems = [];
|
|
219
|
+
const sortedMessageTimes = [];
|
|
171
220
|
const messageIndexMap = /* @__PURE__ */ new Map();
|
|
172
|
-
for (let
|
|
173
|
-
const item =
|
|
174
|
-
if (item?.
|
|
221
|
+
for (let index = 0; index < preparedItems.items.length; index++) {
|
|
222
|
+
const item = preparedItems.items[index];
|
|
223
|
+
if (item?.type !== "message") continue;
|
|
224
|
+
const messageIndex = sortedMessageItems.length;
|
|
225
|
+
sortedMessageItems.push(item);
|
|
226
|
+
sortedMessageTimes.push(preparedItems.times[index] ?? getTimestamp(item.createdAt));
|
|
227
|
+
if (item.id) messageIndexMap.set(item.id, messageIndex);
|
|
175
228
|
}
|
|
176
|
-
const { seenByMap, lastReadMessageMap, unreadCountMap } = buildTimelineReadReceiptData(seenData,
|
|
229
|
+
const { seenByMap, lastReadMessageMap, unreadCountMap } = buildTimelineReadReceiptData(seenData, sortedMessageItems, sortedMessageTimes);
|
|
177
230
|
const seenByArrayCache = /* @__PURE__ */ new Map();
|
|
178
231
|
return {
|
|
179
232
|
items: groupedItems,
|
|
@@ -216,5 +269,5 @@ const useGroupedMessages = ({ items, seenData = [], currentViewerId }) => {
|
|
|
216
269
|
};
|
|
217
270
|
|
|
218
271
|
//#endregion
|
|
219
|
-
export { useGroupedMessages };
|
|
272
|
+
export { TIMELINE_GROUP_WINDOW_MS, buildTimelineReadReceiptData, groupTimelineItems, prepareTimelineItems, useGroupedMessages };
|
|
220
273
|
//# sourceMappingURL=use-grouped-messages.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-grouped-messages.js","names":["EMPTY_STRING_ARRAY: readonly string[]","result: ConversationItem[]","currentGroup: GroupedMessage | null","currentDayString: string | null","lastReadItem: TimelineItem | null"],"sources":["../../../src/hooks/private/use-grouped-messages.ts"],"sourcesContent":["import { SenderType } from \"@cossistant/types\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport type { ConversationSeen } from \"@cossistant/types/schemas\";\nimport { useMemo } from \"react\";\n\nexport type GroupedMessage = {\n\ttype: \"message_group\";\n\tsenderId: string;\n\tsenderType: SenderType;\n\titems: TimelineItem[];\n\tfirstMessageId: string;\n\tlastMessageId: string;\n\tfirstMessageTime: Date;\n\tlastMessageTime: Date;\n};\n\nexport type TimelineEventItem = {\n\ttype: \"timeline_event\";\n\titem: TimelineItem;\n\ttimestamp: Date;\n};\n\nexport type TimelineToolItem = {\n\ttype: \"timeline_tool\";\n\titem: TimelineItem;\n\ttool: string | null;\n\ttimestamp: Date;\n};\n\nexport type DaySeparatorItem = {\n\ttype: \"day_separator\";\n\tdate: Date;\n\tdateString: string; // ISO date string (YYYY-MM-DD) for stable keys\n};\n\nexport type ConversationItem =\n\t| GroupedMessage\n\t| TimelineEventItem\n\t| TimelineToolItem\n\t| DaySeparatorItem;\n\nexport type UseGroupedMessagesOptions = {\n\titems: TimelineItem[];\n\tseenData?: ConversationSeen[];\n\tcurrentViewerId?: string; // The ID of the current viewer (visitor, user, or AI agent)\n};\n\nexport type UseGroupedMessagesProps = UseGroupedMessagesOptions;\n\n// Helper function to safely get timestamp from Date or string\nconst getTimestamp = (date: Date | string | null | undefined): number => {\n\tif (!date) {\n\t\treturn 0;\n\t}\n\tif (typeof date === \"string\") {\n\t\treturn new Date(date).getTime();\n\t}\n\treturn date.getTime();\n};\n\n// Helper function to safely convert to Date\nconst toDate = (date: Date | string | null | undefined): Date => {\n\tif (!date) {\n\t\treturn typeof window !== \"undefined\" ? new Date() : new Date(0);\n\t}\n\tif (typeof date === \"string\") {\n\t\treturn new Date(date);\n\t}\n\treturn date;\n};\n\n// Helper to extract the date string (YYYY-MM-DD) from a Date for day comparison\nconst getDateString = (date: Date): string => {\n\tconst year = date.getFullYear();\n\tconst month = String(date.getMonth() + 1).padStart(2, \"0\");\n\tconst day = String(date.getDate()).padStart(2, \"0\");\n\treturn `${year}-${month}-${day}`;\n};\n\n// Helper to create a Date at midnight for a given date string\nconst createDayDate = (dateString: string): Date => {\n\tconst [year, month, day] = dateString.split(\"-\").map(Number);\n\treturn new Date(year ?? 0, (month ?? 1) - 1, day ?? 1, 0, 0, 0, 0);\n};\n\n// Helper to determine sender ID and type from a timeline item\nconst getSenderIdAndTypeFromTimelineItem = (\n\titem: TimelineItem\n): { senderId: string; senderType: SenderType } => {\n\tif (item.visitorId) {\n\t\treturn { senderId: item.visitorId, senderType: SenderType.VISITOR };\n\t}\n\tif (item.aiAgentId) {\n\t\treturn { senderId: item.aiAgentId, senderType: SenderType.AI };\n\t}\n\tif (item.userId) {\n\t\treturn { senderId: item.userId, senderType: SenderType.TEAM_MEMBER };\n\t}\n\n\t// Fallback\n\treturn {\n\t\tsenderId: item.id || \"default-sender\",\n\t\tsenderType: SenderType.TEAM_MEMBER,\n\t};\n};\n\nconst EMPTY_STRING_ARRAY: readonly string[] = Object.freeze([]);\n\n// Helper function to group timeline items (messages only, events stay separate)\n// Also inserts day separators when the day changes between items\nconst groupTimelineItems = (items: TimelineItem[]): ConversationItem[] => {\n\tconst result: ConversationItem[] = [];\n\tlet currentGroup: GroupedMessage | null = null;\n\tlet currentDayString: string | null = null;\n\n\tconst maybeInsertDaySeparator = (itemDate: Date): void => {\n\t\tconst itemDayString = getDateString(itemDate);\n\n\t\tif (currentDayString !== itemDayString) {\n\t\t\t// Finalize any existing group before inserting day separator\n\t\t\tif (currentGroup) {\n\t\t\t\tresult.push(currentGroup);\n\t\t\t\tcurrentGroup = null;\n\t\t\t}\n\n\t\t\t// Insert day separator\n\t\t\tresult.push({\n\t\t\t\ttype: \"day_separator\",\n\t\t\t\tdate: createDayDate(itemDayString),\n\t\t\t\tdateString: itemDayString,\n\t\t\t});\n\n\t\t\tcurrentDayString = itemDayString;\n\t\t}\n\t};\n\n\tfor (const item of items) {\n\t\tconst itemDate = toDate(item.createdAt);\n\n\t\t// Check for day boundary before processing any item\n\t\tmaybeInsertDaySeparator(itemDate);\n\n\t\t// Events don't get grouped\n\t\tif (item.type === \"event\") {\n\t\t\t// Finalize any existing group\n\t\t\tif (currentGroup) {\n\t\t\t\tresult.push(currentGroup);\n\t\t\t\tcurrentGroup = null;\n\t\t\t}\n\n\t\t\t// Add event as standalone item\n\t\t\tresult.push({\n\t\t\t\ttype: \"timeline_event\",\n\t\t\t\titem,\n\t\t\t\ttimestamp: itemDate,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (item.type === \"identification\") {\n\t\t\t// Finalize any existing group\n\t\t\tif (currentGroup) {\n\t\t\t\tresult.push(currentGroup);\n\t\t\t\tcurrentGroup = null;\n\t\t\t}\n\n\t\t\t// Add tool item as standalone entry\n\t\t\tresult.push({\n\t\t\t\ttype: \"timeline_tool\",\n\t\t\t\titem,\n\t\t\t\ttool: item.tool ?? null,\n\t\t\t\ttimestamp: itemDate,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Group messages by sender\n\t\tconst { senderId, senderType } = getSenderIdAndTypeFromTimelineItem(item);\n\n\t\tif (currentGroup && currentGroup.senderId === senderId) {\n\t\t\t// Add to existing group (day boundary already handled above)\n\t\t\tcurrentGroup.items.push(item);\n\t\t\tcurrentGroup.lastMessageId = item.id || currentGroup.lastMessageId;\n\t\t\tcurrentGroup.lastMessageTime = itemDate;\n\t\t} else {\n\t\t\t// Finalize previous group if exists\n\t\t\tif (currentGroup) {\n\t\t\t\tresult.push(currentGroup);\n\t\t\t}\n\n\t\t\t// Start new group\n\t\t\tcurrentGroup = {\n\t\t\t\ttype: \"message_group\",\n\t\t\t\tsenderId,\n\t\t\t\tsenderType,\n\t\t\t\titems: [item],\n\t\t\t\tfirstMessageId: item.id || \"\",\n\t\t\t\tlastMessageId: item.id || \"\",\n\t\t\t\tfirstMessageTime: itemDate,\n\t\t\t\tlastMessageTime: itemDate,\n\t\t\t};\n\t\t}\n\t}\n\n\tif (currentGroup) {\n\t\tresult.push(currentGroup);\n\t}\n\n\treturn result;\n};\n\n// Build read receipt data for timeline items\n// Accepts pre-sorted message items for performance\nconst buildTimelineReadReceiptData = (\n\tseenData: ConversationSeen[],\n\titems: TimelineItem[],\n\tsortedMessageItems: TimelineItem[],\n\tsortedMessageTimes: number[]\n) => {\n\tconst seenByMap = new Map<string, Set<string>>();\n\tconst lastReadMessageMap = new Map<string, string>();\n\tconst unreadCountMap = new Map<string, number>();\n\n\t// Initialize map for all message-type timeline items\n\tfor (const item of items) {\n\t\tif (item.type === \"message\" && item.id) {\n\t\t\tseenByMap.set(item.id, new Set());\n\t\t}\n\t}\n\n\t// Process seen data for each viewer\n\tfor (const seen of seenData) {\n\t\tconst seenTime = getTimestamp(seen.lastSeenAt);\n\t\tconst viewerId = seen.userId || seen.visitorId || seen.aiAgentId;\n\t\tif (!viewerId) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet lastReadItem: TimelineItem | null = null;\n\t\tlet unreadCount = 0;\n\n\t\t// Process items in chronological order (using pre-sorted array)\n\t\tfor (let index = 0; index < sortedMessageItems.length; index++) {\n\t\t\tconst item = sortedMessageItems[index];\n\t\t\tif (!item) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst itemTime =\n\t\t\t\tsortedMessageTimes[index] ?? getTimestamp(item.createdAt);\n\n\t\t\tif (itemTime <= seenTime) {\n\t\t\t\t// This item has been seen\n\t\t\t\tif (item.id) {\n\t\t\t\t\tconst seenBy = seenByMap.get(item.id);\n\t\t\t\t\tif (seenBy) {\n\t\t\t\t\t\tseenBy.add(viewerId);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlastReadItem = item;\n\t\t\t} else {\n\t\t\t\t// This item is unread\n\t\t\t\tunreadCount++;\n\t\t\t}\n\t\t}\n\n\t\t// Store the last read item for this viewer\n\t\tif (lastReadItem?.id) {\n\t\t\tlastReadMessageMap.set(viewerId, lastReadItem.id);\n\t\t}\n\n\t\t// Store unread count\n\t\tunreadCountMap.set(viewerId, unreadCount);\n\t}\n\n\treturn { seenByMap, lastReadMessageMap, unreadCountMap };\n};\n\n/**\n * Batches sequential timeline items from the same sender into groups and enriches\n * them with read-receipt helpers so UIs can render conversation timelines with\n * minimal effort. Seen data is normalised into quick lookup maps for unread\n * indicators.\n */\nexport const useGroupedMessages = ({\n\titems,\n\tseenData = [],\n\tcurrentViewerId,\n}: UseGroupedMessagesOptions) => {\n\treturn useMemo(() => {\n\t\tconst groupedItems = groupTimelineItems(items);\n\n\t\t// Pre-compute message items and timestamps once for reuse\n\t\tconst messageItems = items.filter((item) => item.type === \"message\");\n\t\tlet sortedMessageItems = messageItems;\n\t\tlet sortedMessageTimes = messageItems.map((item) =>\n\t\t\tgetTimestamp(item.createdAt)\n\t\t);\n\n\t\t// Avoid sorting if items are already in chronological order\n\t\tlet isSorted = true;\n\t\tfor (let index = 1; index < sortedMessageTimes.length; index++) {\n\t\t\tconst currentTime = sortedMessageTimes[index];\n\t\t\tconst previousTime = sortedMessageTimes[index - 1];\n\t\t\tif (\n\t\t\t\tcurrentTime !== undefined &&\n\t\t\t\tpreviousTime !== undefined &&\n\t\t\t\tcurrentTime < previousTime\n\t\t\t) {\n\t\t\t\tisSorted = false;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (!isSorted) {\n\t\t\tconst itemsWithTimes = messageItems.map((item, index) => ({\n\t\t\t\titem,\n\t\t\t\ttime: sortedMessageTimes[index] ?? 0,\n\t\t\t}));\n\n\t\t\titemsWithTimes.sort((a, b) => a.time - b.time);\n\n\t\t\tsortedMessageItems = itemsWithTimes.map((entry) => entry.item);\n\t\t\tsortedMessageTimes = itemsWithTimes.map((entry) => entry.time);\n\t\t}\n\n\t\t// Build index map from sorted items for O(1) chronological lookups\n\t\t// Must use sortedMessageItems (not raw items) to ensure indices reflect time order\n\t\tconst messageIndexMap = new Map<string, number>();\n\t\tfor (let i = 0; i < sortedMessageItems.length; i++) {\n\t\t\tconst item = sortedMessageItems[i];\n\t\t\tif (item?.id) {\n\t\t\t\tmessageIndexMap.set(item.id, i);\n\t\t\t}\n\t\t}\n\n\t\t// Build read receipt data with pre-sorted items\n\t\tconst { seenByMap, lastReadMessageMap, unreadCountMap } =\n\t\t\tbuildTimelineReadReceiptData(\n\t\t\t\tseenData,\n\t\t\t\titems,\n\t\t\t\tsortedMessageItems,\n\t\t\t\tsortedMessageTimes\n\t\t\t);\n\n\t\t// Cache for turning seen sets into stable arrays across renders\n\t\tconst seenByArrayCache = new Map<string, readonly string[]>();\n\n\t\treturn {\n\t\t\titems: groupedItems,\n\t\t\tseenByMap,\n\t\t\tlastReadMessageMap,\n\t\t\tunreadCountMap,\n\n\t\t\tisMessageSeenByViewer: (messageId: string): boolean => {\n\t\t\t\tif (!currentViewerId) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst seenBy = seenByMap.get(messageId);\n\t\t\t\treturn seenBy ? seenBy.has(currentViewerId) : false;\n\t\t\t},\n\n\t\t\tgetMessageSeenBy: (messageId: string): readonly string[] => {\n\t\t\t\tif (seenByArrayCache.has(messageId)) {\n\t\t\t\t\treturn seenByArrayCache.get(messageId) ?? EMPTY_STRING_ARRAY;\n\t\t\t\t}\n\n\t\t\t\tconst seenBy = seenByMap.get(messageId);\n\t\t\t\tif (!seenBy || seenBy.size === 0) {\n\t\t\t\t\tseenByArrayCache.set(messageId, EMPTY_STRING_ARRAY);\n\t\t\t\t\treturn EMPTY_STRING_ARRAY;\n\t\t\t\t}\n\n\t\t\t\tconst result = Object.freeze(Array.from(seenBy)) as readonly string[];\n\t\t\t\tseenByArrayCache.set(messageId, result);\n\t\t\t\treturn result;\n\t\t\t},\n\n\t\t\tgetLastReadMessageId: (userId: string): string | undefined =>\n\t\t\t\tlastReadMessageMap.get(userId),\n\n\t\t\tisLastReadMessage: (messageId: string, userId: string): boolean =>\n\t\t\t\tlastReadMessageMap.get(userId) === messageId,\n\n\t\t\tgetUnreadCount: (userId: string): number =>\n\t\t\t\tunreadCountMap.get(userId) || 0,\n\n\t\t\thasUnreadAfter: (messageId: string, userId: string): boolean => {\n\t\t\t\tconst lastRead = lastReadMessageMap.get(userId);\n\t\t\t\tif (!lastRead) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\t// Use index map for O(1) lookups instead of findIndex O(n)\n\t\t\t\tconst messageIndex = messageIndexMap.get(messageId);\n\t\t\t\tconst lastReadIndex = messageIndexMap.get(lastRead);\n\n\t\t\t\tif (messageIndex === undefined || lastReadIndex === undefined) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\treturn messageIndex < lastReadIndex;\n\t\t\t},\n\t\t};\n\t}, [items, seenData, currentViewerId]);\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-grouped-messages.js","names":["EMPTY_STRING_ARRAY: readonly string[]","result: ConversationItem[]","currentMessageGroup: GroupedMessage | null","currentActivityGroup: GroupedActivity | null","currentDayString: string | null","lastReadItem: TimelineItem | null","sortedMessageItems: TimelineItem[]","sortedMessageTimes: number[]"],"sources":["../../../src/hooks/private/use-grouped-messages.ts"],"sourcesContent":["import type { SenderType } from \"@cossistant/types\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport type { ConversationSeen } from \"@cossistant/types/schemas\";\nimport { useMemo } from \"react\";\nimport { getTimelineItemSender } from \"../../utils/timeline-item-sender\";\n\nexport type GroupedMessage = {\n\ttype: \"message_group\";\n\tsenderId: string;\n\tsenderType: SenderType;\n\titems: TimelineItem[];\n\tfirstMessageId: string;\n\tlastMessageId: string;\n\tfirstMessageTime: Date;\n\tlastMessageTime: Date;\n};\n\nexport type GroupedActivity = {\n\ttype: \"activity_group\";\n\tsenderId: string;\n\tsenderType: SenderType;\n\titems: TimelineItem[];\n\tfirstItemId: string;\n\tlastItemId: string;\n\tfirstItemTime: Date;\n\tlastItemTime: Date;\n\thasEvent: boolean;\n\thasTool: boolean;\n};\n\nexport type TimelineEventItem = {\n\ttype: \"timeline_event\";\n\titem: TimelineItem;\n\ttimestamp: Date;\n};\n\nexport type TimelineToolItem = {\n\ttype: \"timeline_tool\";\n\titem: TimelineItem;\n\ttool: string | null;\n\ttimestamp: Date;\n};\n\nexport type DaySeparatorItem = {\n\ttype: \"day_separator\";\n\tdate: Date;\n\tdateString: string; // ISO date string (YYYY-MM-DD) for stable keys\n};\n\nexport type ConversationItem =\n\t| GroupedMessage\n\t| GroupedActivity\n\t| TimelineEventItem\n\t| TimelineToolItem\n\t| DaySeparatorItem;\n\nexport type UseGroupedMessagesOptions = {\n\titems: TimelineItem[];\n\tseenData?: ConversationSeen[];\n\tcurrentViewerId?: string; // The ID of the current viewer (visitor, user, or AI agent)\n};\n\nexport type UseGroupedMessagesProps = UseGroupedMessagesOptions;\n\nexport type PreparedTimelineItems = {\n\titems: TimelineItem[];\n\ttimes: number[];\n\tdidSort: boolean;\n};\n\nexport const TIMELINE_GROUP_WINDOW_MS = 5 * 60 * 1000;\n\n// Helper function to safely get timestamp from Date or string\nconst getTimestamp = (date: Date | string | null | undefined): number => {\n\tif (!date) {\n\t\treturn 0;\n\t}\n\tif (typeof date === \"string\") {\n\t\treturn new Date(date).getTime();\n\t}\n\treturn date.getTime();\n};\n\n// Helper to extract the date string (YYYY-MM-DD) from a Date for day comparison\nconst getDateString = (date: Date): string => {\n\tconst year = date.getFullYear();\n\tconst month = String(date.getMonth() + 1).padStart(2, \"0\");\n\tconst day = String(date.getDate()).padStart(2, \"0\");\n\treturn `${year}-${month}-${day}`;\n};\n\n// Helper to create a Date at midnight for a given date string\nconst createDayDate = (dateString: string): Date => {\n\tconst [year, month, day] = dateString.split(\"-\").map(Number);\n\treturn new Date(year ?? 0, (month ?? 1) - 1, day ?? 1, 0, 0, 0, 0);\n};\n\nconst getToolNameFromTimelineItem = (item: TimelineItem): string | null => {\n\tif (item.tool) {\n\t\treturn item.tool;\n\t}\n\n\tfor (const part of item.parts) {\n\t\tif (\n\t\t\ttypeof part === \"object\" &&\n\t\t\tpart !== null &&\n\t\t\t\"type\" in part &&\n\t\t\t\"toolName\" in part &&\n\t\t\ttypeof part.type === \"string\" &&\n\t\t\tpart.type.startsWith(\"tool-\") &&\n\t\t\ttypeof part.toolName === \"string\"\n\t\t) {\n\t\t\treturn part.toolName;\n\t\t}\n\t}\n\n\treturn null;\n};\n\nconst EMPTY_STRING_ARRAY: readonly string[] = Object.freeze([]);\n\ntype GroupableTimelineItemType =\n\t| \"message\"\n\t| \"activity\"\n\t| \"standalone_tool\"\n\t| \"standalone_event\";\n\nfunction getGroupableTimelineItemType(\n\titem: TimelineItem\n): GroupableTimelineItemType {\n\tif (item.type === \"message\") {\n\t\treturn \"message\";\n\t}\n\n\tif (item.type === \"event\" || item.type === \"tool\") {\n\t\treturn \"activity\";\n\t}\n\n\tif (item.type === \"identification\") {\n\t\treturn \"standalone_tool\";\n\t}\n\n\treturn \"standalone_event\";\n}\n\nexport const prepareTimelineItems = (\n\titems: TimelineItem[]\n): PreparedTimelineItems => {\n\tif (items.length <= 1) {\n\t\treturn {\n\t\t\titems,\n\t\t\ttimes: items.map((item) => getTimestamp(item.createdAt)),\n\t\t\tdidSort: false,\n\t\t};\n\t}\n\n\tconst times = new Array<number>(items.length);\n\tlet isSorted = true;\n\n\tfor (let index = 0; index < items.length; index++) {\n\t\tconst item = items[index];\n\t\tconst time = getTimestamp(item?.createdAt);\n\t\ttimes[index] = time;\n\n\t\tif (index === 0) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst previousTime = times[index - 1];\n\t\tif (\n\t\t\tpreviousTime !== undefined &&\n\t\t\ttime !== undefined &&\n\t\t\ttime < previousTime\n\t\t) {\n\t\t\tisSorted = false;\n\t\t}\n\t}\n\n\tif (isSorted) {\n\t\treturn { items, times, didSort: false };\n\t}\n\n\tconst entries = items.map((item, index) => ({\n\t\titem,\n\t\ttime: times[index] ?? 0,\n\t\tindex,\n\t}));\n\n\tentries.sort((a, b) => {\n\t\tif (a.time === b.time) {\n\t\t\treturn a.index - b.index;\n\t\t}\n\t\treturn a.time - b.time;\n\t});\n\n\treturn {\n\t\titems: entries.map((entry) => entry.item),\n\t\ttimes: entries.map((entry) => entry.time),\n\t\tdidSort: true,\n\t};\n};\n\nconst isWithinGroupingWindow = (\n\tpreviousTimestamp: number,\n\tcurrentTimestamp: number\n): boolean => currentTimestamp - previousTimestamp <= TIMELINE_GROUP_WINDOW_MS;\n\n// Helper function to group timeline items with a sender + time window policy.\n// - message items group with messages only\n// - event + tool items group together\n// - identification remains standalone to preserve the interactive identification form\n// Also inserts day separators when the day changes between items.\nexport const groupTimelineItems = (\n\titems: TimelineItem[],\n\titemTimes: number[]\n): ConversationItem[] => {\n\tconst result: ConversationItem[] = [];\n\tlet currentMessageGroup: GroupedMessage | null = null;\n\tlet currentActivityGroup: GroupedActivity | null = null;\n\tlet currentDayString: string | null = null;\n\n\tconst flushMessageGroup = () => {\n\t\tif (!currentMessageGroup) {\n\t\t\treturn;\n\t\t}\n\n\t\tresult.push(currentMessageGroup);\n\t\tcurrentMessageGroup = null;\n\t};\n\n\tconst flushActivityGroup = () => {\n\t\tif (!currentActivityGroup) {\n\t\t\treturn;\n\t\t}\n\n\t\tresult.push(currentActivityGroup);\n\t\tcurrentActivityGroup = null;\n\t};\n\n\tconst flushAllGroups = () => {\n\t\tflushMessageGroup();\n\t\tflushActivityGroup();\n\t};\n\n\tconst maybeInsertDaySeparator = (itemDate: Date): void => {\n\t\tconst itemDayString = getDateString(itemDate);\n\n\t\tif (currentDayString === itemDayString) {\n\t\t\treturn;\n\t\t}\n\n\t\tflushAllGroups();\n\t\tresult.push({\n\t\t\ttype: \"day_separator\",\n\t\t\tdate: createDayDate(itemDayString),\n\t\t\tdateString: itemDayString,\n\t\t});\n\t\tcurrentDayString = itemDayString;\n\t};\n\n\tfor (let index = 0; index < items.length; index++) {\n\t\tconst item = items[index];\n\t\tif (!item) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst itemTimestamp = itemTimes[index] ?? getTimestamp(item.createdAt);\n\t\tconst itemDate = new Date(itemTimestamp);\n\n\t\tmaybeInsertDaySeparator(itemDate);\n\n\t\tconst groupableType = getGroupableTimelineItemType(item);\n\n\t\tif (groupableType === \"message\") {\n\t\t\tflushActivityGroup();\n\n\t\t\tconst { senderId, senderType } = getTimelineItemSender(item);\n\t\t\tconst previousTimestamp = currentMessageGroup?.lastMessageTime.getTime();\n\t\t\tconst canAppendToCurrentGroup = Boolean(\n\t\t\t\tcurrentMessageGroup &&\n\t\t\t\t\tcurrentMessageGroup.senderId === senderId &&\n\t\t\t\t\tpreviousTimestamp !== undefined &&\n\t\t\t\t\tisWithinGroupingWindow(previousTimestamp, itemTimestamp)\n\t\t\t);\n\n\t\t\tif (canAppendToCurrentGroup && currentMessageGroup) {\n\t\t\t\tcurrentMessageGroup.items.push(item);\n\t\t\t\tcurrentMessageGroup.lastMessageId =\n\t\t\t\t\titem.id || currentMessageGroup.lastMessageId;\n\t\t\t\tcurrentMessageGroup.lastMessageTime = itemDate;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tflushMessageGroup();\n\t\t\tcurrentMessageGroup = {\n\t\t\t\ttype: \"message_group\",\n\t\t\t\tsenderId,\n\t\t\t\tsenderType,\n\t\t\t\titems: [item],\n\t\t\t\tfirstMessageId: item.id || \"\",\n\t\t\t\tlastMessageId: item.id || \"\",\n\t\t\t\tfirstMessageTime: itemDate,\n\t\t\t\tlastMessageTime: itemDate,\n\t\t\t};\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (groupableType === \"activity\") {\n\t\t\tflushMessageGroup();\n\n\t\t\tconst { senderId, senderType } = getTimelineItemSender(item);\n\t\t\tconst previousTimestamp = currentActivityGroup?.lastItemTime.getTime();\n\t\t\tconst canAppendToCurrentGroup = Boolean(\n\t\t\t\tcurrentActivityGroup &&\n\t\t\t\t\tcurrentActivityGroup.senderId === senderId &&\n\t\t\t\t\tpreviousTimestamp !== undefined &&\n\t\t\t\t\tisWithinGroupingWindow(previousTimestamp, itemTimestamp)\n\t\t\t);\n\n\t\t\tif (canAppendToCurrentGroup && currentActivityGroup) {\n\t\t\t\tcurrentActivityGroup.items.push(item);\n\t\t\t\tcurrentActivityGroup.lastItemId =\n\t\t\t\t\titem.id || currentActivityGroup.lastItemId;\n\t\t\t\tcurrentActivityGroup.lastItemTime = itemDate;\n\t\t\t\tcurrentActivityGroup.hasEvent =\n\t\t\t\t\tcurrentActivityGroup.hasEvent || item.type === \"event\";\n\t\t\t\tcurrentActivityGroup.hasTool =\n\t\t\t\t\tcurrentActivityGroup.hasTool || item.type === \"tool\";\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tflushActivityGroup();\n\t\t\tcurrentActivityGroup = {\n\t\t\t\ttype: \"activity_group\",\n\t\t\t\tsenderId,\n\t\t\t\tsenderType,\n\t\t\t\titems: [item],\n\t\t\t\tfirstItemId: item.id || \"\",\n\t\t\t\tlastItemId: item.id || \"\",\n\t\t\t\tfirstItemTime: itemDate,\n\t\t\t\tlastItemTime: itemDate,\n\t\t\t\thasEvent: item.type === \"event\",\n\t\t\t\thasTool: item.type === \"tool\",\n\t\t\t};\n\t\t\tcontinue;\n\t\t}\n\n\t\tflushAllGroups();\n\n\t\tif (groupableType === \"standalone_tool\") {\n\t\t\tresult.push({\n\t\t\t\ttype: \"timeline_tool\",\n\t\t\t\titem,\n\t\t\t\ttool: getToolNameFromTimelineItem(item),\n\t\t\t\ttimestamp: itemDate,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tresult.push({\n\t\t\ttype: \"timeline_event\",\n\t\t\titem,\n\t\t\ttimestamp: itemDate,\n\t\t});\n\t}\n\n\tflushAllGroups();\n\n\treturn result;\n};\n\n// Build read receipt data for timeline items.\n// Accepts pre-sorted message items and timestamps for performance.\nexport const buildTimelineReadReceiptData = (\n\tseenData: ConversationSeen[],\n\tsortedMessageItems: TimelineItem[],\n\tsortedMessageTimes: number[]\n) => {\n\tconst seenByMap = new Map<string, Set<string>>();\n\tconst lastReadMessageMap = new Map<string, string>();\n\tconst unreadCountMap = new Map<string, number>();\n\n\tfor (const item of sortedMessageItems) {\n\t\tif (item.id) {\n\t\t\tseenByMap.set(item.id, new Set());\n\t\t}\n\t}\n\n\tif (seenData.length === 0 || sortedMessageItems.length === 0) {\n\t\treturn { seenByMap, lastReadMessageMap, unreadCountMap };\n\t}\n\n\tfor (const seen of seenData) {\n\t\tconst seenTime = getTimestamp(seen.lastSeenAt);\n\t\tconst viewerId = seen.userId || seen.visitorId || seen.aiAgentId;\n\t\tif (!viewerId) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet lastReadItem: TimelineItem | null = null;\n\t\tlet unreadCount = 0;\n\n\t\tfor (let index = 0; index < sortedMessageItems.length; index++) {\n\t\t\tconst item = sortedMessageItems[index];\n\t\t\tif (!item) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst itemTime =\n\t\t\t\tsortedMessageTimes[index] ?? getTimestamp(item.createdAt);\n\n\t\t\tif (itemTime <= seenTime) {\n\t\t\t\tif (item.id) {\n\t\t\t\t\tconst seenBy = seenByMap.get(item.id);\n\t\t\t\t\tseenBy?.add(viewerId);\n\t\t\t\t}\n\t\t\t\tlastReadItem = item;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tunreadCount++;\n\t\t}\n\n\t\tif (lastReadItem?.id) {\n\t\t\tlastReadMessageMap.set(viewerId, lastReadItem.id);\n\t\t}\n\n\t\tunreadCountMap.set(viewerId, unreadCount);\n\t}\n\n\treturn { seenByMap, lastReadMessageMap, unreadCountMap };\n};\n\n/**\n * Batches sequential timeline items from the same sender into groups and enriches\n * them with read-receipt helpers so UIs can render conversation timelines with\n * minimal effort. Seen data is normalised into quick lookup maps for unread\n * indicators.\n */\nexport const useGroupedMessages = ({\n\titems,\n\tseenData = [],\n\tcurrentViewerId,\n}: UseGroupedMessagesOptions) => {\n\treturn useMemo(() => {\n\t\tconst preparedItems = prepareTimelineItems(items);\n\t\tconst groupedItems = groupTimelineItems(\n\t\t\tpreparedItems.items,\n\t\t\tpreparedItems.times\n\t\t);\n\n\t\tconst sortedMessageItems: TimelineItem[] = [];\n\t\tconst sortedMessageTimes: number[] = [];\n\t\tconst messageIndexMap = new Map<string, number>();\n\n\t\tfor (let index = 0; index < preparedItems.items.length; index++) {\n\t\t\tconst item = preparedItems.items[index];\n\t\t\tif (item?.type !== \"message\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst messageIndex = sortedMessageItems.length;\n\t\t\tsortedMessageItems.push(item);\n\t\t\tsortedMessageTimes.push(\n\t\t\t\tpreparedItems.times[index] ?? getTimestamp(item.createdAt)\n\t\t\t);\n\n\t\t\tif (item.id) {\n\t\t\t\tmessageIndexMap.set(item.id, messageIndex);\n\t\t\t}\n\t\t}\n\n\t\tconst { seenByMap, lastReadMessageMap, unreadCountMap } =\n\t\t\tbuildTimelineReadReceiptData(\n\t\t\t\tseenData,\n\t\t\t\tsortedMessageItems,\n\t\t\t\tsortedMessageTimes\n\t\t\t);\n\n\t\t// Cache for turning seen sets into stable arrays across renders\n\t\tconst seenByArrayCache = new Map<string, readonly string[]>();\n\n\t\treturn {\n\t\t\titems: groupedItems,\n\t\t\tseenByMap,\n\t\t\tlastReadMessageMap,\n\t\t\tunreadCountMap,\n\n\t\t\tisMessageSeenByViewer: (messageId: string): boolean => {\n\t\t\t\tif (!currentViewerId) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst seenBy = seenByMap.get(messageId);\n\t\t\t\treturn seenBy ? seenBy.has(currentViewerId) : false;\n\t\t\t},\n\n\t\t\tgetMessageSeenBy: (messageId: string): readonly string[] => {\n\t\t\t\tif (seenByArrayCache.has(messageId)) {\n\t\t\t\t\treturn seenByArrayCache.get(messageId) ?? EMPTY_STRING_ARRAY;\n\t\t\t\t}\n\n\t\t\t\tconst seenBy = seenByMap.get(messageId);\n\t\t\t\tif (!seenBy || seenBy.size === 0) {\n\t\t\t\t\tseenByArrayCache.set(messageId, EMPTY_STRING_ARRAY);\n\t\t\t\t\treturn EMPTY_STRING_ARRAY;\n\t\t\t\t}\n\n\t\t\t\tconst result = Object.freeze(Array.from(seenBy)) as readonly string[];\n\t\t\t\tseenByArrayCache.set(messageId, result);\n\t\t\t\treturn result;\n\t\t\t},\n\n\t\t\tgetLastReadMessageId: (userId: string): string | undefined =>\n\t\t\t\tlastReadMessageMap.get(userId),\n\n\t\t\tisLastReadMessage: (messageId: string, userId: string): boolean =>\n\t\t\t\tlastReadMessageMap.get(userId) === messageId,\n\n\t\t\tgetUnreadCount: (userId: string): number =>\n\t\t\t\tunreadCountMap.get(userId) || 0,\n\n\t\t\thasUnreadAfter: (messageId: string, userId: string): boolean => {\n\t\t\t\tconst lastRead = lastReadMessageMap.get(userId);\n\t\t\t\tif (!lastRead) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\t// Use index map for O(1) lookups instead of findIndex O(n)\n\t\t\t\tconst messageIndex = messageIndexMap.get(messageId);\n\t\t\t\tconst lastReadIndex = messageIndexMap.get(lastRead);\n\n\t\t\t\tif (messageIndex === undefined || lastReadIndex === undefined) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\treturn messageIndex < lastReadIndex;\n\t\t\t},\n\t\t};\n\t}, [items, seenData, currentViewerId]);\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-new-message-sound.d.ts","names":[],"sources":["../../src/hooks/use-new-message-sound.ts"],"sourcesContent":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"use-new-message-sound.d.ts","names":[],"sources":["../../src/hooks/use-new-message-sound.ts"],"sourcesContent":[],"mappings":";;AAmBA;;;;;;;;;;;;;;iBAAgB,kBAAA"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { NEW_MESSAGE_SOUND_DATA_URL } from "../sounds/sound-data.js";
|
|
1
2
|
import { useSoundEffect } from "./use-sound-effect.js";
|
|
2
3
|
import { useCallback } from "react";
|
|
3
4
|
|
|
4
5
|
//#region src/hooks/use-new-message-sound.ts
|
|
5
|
-
const NEW_MESSAGE_SOUND_PATH = "/sounds/new-message.wav";
|
|
6
6
|
/**
|
|
7
7
|
* Hook to play a sound when a new message arrives.
|
|
8
8
|
*
|
|
@@ -19,7 +19,7 @@ const NEW_MESSAGE_SOUND_PATH = "/sounds/new-message.wav";
|
|
|
19
19
|
* }, [hasNewMessage]);
|
|
20
20
|
*/
|
|
21
21
|
function useNewMessageSound(options) {
|
|
22
|
-
const { play } = useSoundEffect(
|
|
22
|
+
const { play } = useSoundEffect(NEW_MESSAGE_SOUND_DATA_URL, {
|
|
23
23
|
loop: false,
|
|
24
24
|
volume: options?.volume ?? .7,
|
|
25
25
|
playbackRate: options?.playbackRate ?? 1
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-new-message-sound.js","names":[],"sources":["../../src/hooks/use-new-message-sound.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport {
|
|
1
|
+
{"version":3,"file":"use-new-message-sound.js","names":[],"sources":["../../src/hooks/use-new-message-sound.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport { NEW_MESSAGE_SOUND_DATA_URL } from \"../sounds/sound-data\";\nimport { useSoundEffect } from \"./use-sound-effect\";\n\n/**\n * Hook to play a sound when a new message arrives.\n *\n * @param options - Optional configuration for volume and playback speed\n * @returns Function to play the new message sound\n *\n * @example\n * const playNewMessageSound = useNewMessageSound({ volume: 0.8, playbackRate: 1.1 });\n *\n * useEffect(() => {\n * if (hasNewMessage) {\n * playNewMessageSound();\n * }\n * }, [hasNewMessage]);\n */\nexport function useNewMessageSound(options?: {\n\tvolume?: number;\n\tplaybackRate?: number;\n}): () => void {\n\tconst { play } = useSoundEffect(NEW_MESSAGE_SOUND_DATA_URL, {\n\t\tloop: false,\n\t\tvolume: options?.volume ?? 0.7,\n\t\tplaybackRate: options?.playbackRate ?? 1.0,\n\t});\n\n\treturn useCallback(() => {\n\t\tplay();\n\t}, [play]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,mBAAmB,SAGpB;CACd,MAAM,EAAE,SAAS,eAAe,4BAA4B;EAC3D,MAAM;EACN,QAAQ,SAAS,UAAU;EAC3B,cAAc,SAAS,gBAAgB;EACvC,CAAC;AAEF,QAAO,kBAAkB;AACxB,QAAM;IACJ,CAAC,KAAK,CAAC"}
|
|
@@ -118,7 +118,7 @@ function useSendMessage(options = {}) {
|
|
|
118
118
|
item: {
|
|
119
119
|
id: timelineItemPayload.id,
|
|
120
120
|
text: timelineItemPayload.text ?? "",
|
|
121
|
-
type: timelineItemPayload.type === "
|
|
121
|
+
type: timelineItemPayload.type === "event" ? "event" : "message",
|
|
122
122
|
visibility: timelineItemPayload.visibility,
|
|
123
123
|
userId: timelineItemPayload.userId,
|
|
124
124
|
aiAgentId: timelineItemPayload.aiAgentId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-send-message.js","names":["parts: TimelineItemParts","initialConversation:\n\t\t\t\t\t| CreateConversationResponseBody[\"conversation\"]\n\t\t\t\t\t| undefined","fileParts: Array<TimelinePartImage | TimelinePartFile>","result: SendMessageResult"],"sources":["../../src/hooks/use-send-message.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport {\n\tgenerateMessageId,\n\tisImageMimeType,\n\tvalidateFiles,\n} from \"@cossistant/core\";\nimport type { CreateConversationResponseBody } from \"@cossistant/types/api/conversation\";\nimport type {\n\tTimelineItem,\n\tTimelineItemParts,\n\tTimelinePartFile,\n\tTimelinePartImage,\n} from \"@cossistant/types/api/timeline-item\";\nimport { useCallback, useState } from \"react\";\n\nimport { useSupport } from \"../provider\";\n\nexport type SendMessageOptions = {\n\tconversationId?: string | null;\n\tmessage: string;\n\tfiles?: File[];\n\tdefaultTimelineItems?: TimelineItem[];\n\tvisitorId?: string;\n\t/**\n\t * Optional message ID to use for the optimistic update and API request.\n\t * When not provided, a ULID will be generated on the client.\n\t */\n\tmessageId?: string;\n\tonSuccess?: (conversationId: string, messageId: string) => void;\n\tonError?: (error: Error) => void;\n\t/**\n\t * Called immediately after a new conversation is initiated (before API call).\n\t * Use this to immediately switch the UI to the new conversation ID for\n\t * proper optimistic updates display.\n\t */\n\tonConversationInitiated?: (conversationId: string) => void;\n};\n\nexport type SendMessageResult = {\n\tconversationId: string;\n\tmessageId: string;\n\tconversation?: CreateConversationResponseBody[\"conversation\"];\n\tinitialTimelineItems?: CreateConversationResponseBody[\"initialTimelineItems\"];\n};\n\nexport type UseSendMessageResult = {\n\tmutate: (options: SendMessageOptions) => void;\n\tmutateAsync: (\n\t\toptions: SendMessageOptions\n\t) => Promise<SendMessageResult | null>;\n\tisPending: boolean;\n\tisUploading: boolean;\n\terror: Error | null;\n\treset: () => void;\n};\n\nexport type UseSendMessageOptions = {\n\tclient?: CossistantClient;\n};\n\nfunction toError(error: unknown): Error {\n\tif (error instanceof Error) {\n\t\treturn error;\n\t}\n\n\tif (typeof error === \"string\") {\n\t\treturn new Error(error);\n\t}\n\n\treturn new Error(\"Unknown error\");\n}\n\ntype BuildTimelineItemPayloadOptions = {\n\tbody: string;\n\tconversationId: string;\n\tvisitorId: string | null;\n\tmessageId?: string;\n\tfileParts?: Array<TimelinePartImage | TimelinePartFile>;\n};\n\nfunction buildTimelineItemPayload({\n\tbody,\n\tconversationId,\n\tvisitorId,\n\tmessageId,\n\tfileParts,\n}: BuildTimelineItemPayloadOptions): TimelineItem {\n\tconst nowIso = typeof window !== \"undefined\" ? new Date().toISOString() : \"\";\n\tconst id = messageId ?? generateMessageId();\n\n\t// Build parts array: text first, then any file/image parts\n\tconst parts: TimelineItemParts = [{ type: \"text\" as const, text: body }];\n\n\tif (fileParts && fileParts.length > 0) {\n\t\tparts.push(...fileParts);\n\t}\n\n\treturn {\n\t\tid,\n\t\tconversationId,\n\t\torganizationId: \"\", // Will be set by backend\n\t\ttype: \"message\" as const,\n\t\ttext: body,\n\t\tparts,\n\t\tvisibility: \"public\" as const,\n\t\tuserId: null,\n\t\taiAgentId: null,\n\t\tvisitorId: visitorId ?? null,\n\t\tcreatedAt: nowIso,\n\t\tdeletedAt: null,\n\t} satisfies TimelineItem;\n}\n\n/**\n * Upload files and return timeline parts for inclusion in a message.\n */\nasync function uploadFilesForMessage(\n\tclient: CossistantClient,\n\tfiles: File[],\n\tconversationId: string\n): Promise<Array<TimelinePartImage | TimelinePartFile>> {\n\tif (files.length === 0) {\n\t\treturn [];\n\t}\n\n\t// Validate files first\n\tconst validationError = validateFiles(files);\n\tif (validationError) {\n\t\tthrow new Error(validationError);\n\t}\n\n\t// Upload files in parallel\n\tconst uploadPromises = files.map(async (file) => {\n\t\t// Generate presigned URL\n\t\tconst uploadInfo = await client.generateUploadUrl({\n\t\t\tconversationId,\n\t\t\tcontentType: file.type,\n\t\t\tfileName: file.name,\n\t\t});\n\n\t\t// Upload file to S3\n\t\tawait client.uploadFile(file, uploadInfo.uploadUrl, file.type);\n\n\t\t// Return timeline part based on file type\n\t\tconst isImage = isImageMimeType(file.type);\n\n\t\tif (isImage) {\n\t\t\treturn {\n\t\t\t\ttype: \"image\" as const,\n\t\t\t\turl: uploadInfo.publicUrl,\n\t\t\t\tmediaType: file.type,\n\t\t\t\tfilename: file.name,\n\t\t\t\tsize: file.size,\n\t\t\t} satisfies TimelinePartImage;\n\t\t}\n\n\t\treturn {\n\t\t\ttype: \"file\" as const,\n\t\t\turl: uploadInfo.publicUrl,\n\t\t\tmediaType: file.type,\n\t\t\tfilename: file.name,\n\t\t\tsize: file.size,\n\t\t} satisfies TimelinePartFile;\n\t});\n\n\treturn Promise.all(uploadPromises);\n}\n\n/**\n * Sends visitor messages while handling optimistic pending conversations and\n * exposing react-query-like mutation state.\n */\nexport function useSendMessage(\n\toptions: UseSendMessageOptions = {}\n): UseSendMessageResult {\n\tconst { client: contextClient } = useSupport();\n\tconst client = options.client ?? contextClient;\n\n\tconst [isPending, setIsPending] = useState(false);\n\tconst [isUploading, setIsUploading] = useState(false);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\tconst mutateAsync = useCallback(\n\t\tasync (payload: SendMessageOptions): Promise<SendMessageResult | null> => {\n\t\t\tconst {\n\t\t\t\tconversationId: providedConversationId,\n\t\t\t\tmessage,\n\t\t\t\tfiles = [],\n\t\t\t\tdefaultTimelineItems = [],\n\t\t\t\tvisitorId,\n\t\t\t\tmessageId: providedMessageId,\n\t\t\t\tonSuccess,\n\t\t\t\tonError,\n\t\t\t\tonConversationInitiated,\n\t\t\t} = payload;\n\n\t\t\t// Allow empty message if there are files\n\t\t\tif (!message.trim() && files.length === 0) {\n\t\t\t\tconst emptyMessageError = new Error(\n\t\t\t\t\t\"Message cannot be empty (or attach files)\"\n\t\t\t\t);\n\t\t\t\tsetError(emptyMessageError);\n\t\t\t\tonError?.(emptyMessageError);\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tsetIsPending(true);\n\t\t\tsetError(null);\n\n\t\t\ttry {\n\t\t\t\tif (!client) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\"Cossistant client is not available. Please ensure you have configured your API key.\"\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tlet conversationId = providedConversationId ?? undefined;\n\t\t\t\tlet preparedDefaultTimelineItems = defaultTimelineItems;\n\t\t\t\tlet initialConversation:\n\t\t\t\t\t| CreateConversationResponseBody[\"conversation\"]\n\t\t\t\t\t| undefined;\n\n\t\t\t\tif (!conversationId) {\n\t\t\t\t\tconst initiated = client.initiateConversation({\n\t\t\t\t\t\tdefaultTimelineItems,\n\t\t\t\t\t\tvisitorId: visitorId ?? undefined,\n\t\t\t\t\t});\n\t\t\t\t\tconversationId = initiated.conversationId;\n\t\t\t\t\tpreparedDefaultTimelineItems = initiated.defaultTimelineItems;\n\t\t\t\t\tinitialConversation = initiated.conversation;\n\t\t\t\t\t// Immediately notify about the new conversation ID so UI can switch\n\t\t\t\t\t// to reading from the right store key for optimistic updates\n\t\t\t\t\tonConversationInitiated?.(conversationId);\n\t\t\t\t}\n\n\t\t\t\t// Upload files BEFORE sending the message\n\t\t\t\tlet fileParts: Array<TimelinePartImage | TimelinePartFile> = [];\n\t\t\t\tif (files.length > 0) {\n\t\t\t\t\tsetIsUploading(true);\n\t\t\t\t\ttry {\n\t\t\t\t\t\tfileParts = await uploadFilesForMessage(\n\t\t\t\t\t\t\tclient,\n\t\t\t\t\t\t\tfiles,\n\t\t\t\t\t\t\tconversationId\n\t\t\t\t\t\t);\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tsetIsUploading(false);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst timelineItemPayload = buildTimelineItemPayload({\n\t\t\t\t\tbody: message,\n\t\t\t\t\tconversationId,\n\t\t\t\t\tvisitorId: visitorId ?? null,\n\t\t\t\t\tmessageId: providedMessageId,\n\t\t\t\t\tfileParts,\n\t\t\t\t});\n\n\t\t\t\tconst response = await client.sendMessage({\n\t\t\t\t\tconversationId,\n\t\t\t\t\titem: {\n\t\t\t\t\t\tid: timelineItemPayload.id,\n\t\t\t\t\t\ttext: timelineItemPayload.text ?? \"\",\n\t\t\t\t\t\ttype:\n\t\t\t\t\t\t\ttimelineItemPayload.type === \"identification\"\n\t\t\t\t\t\t\t\t? \"message\"\n\t\t\t\t\t\t\t\t: timelineItemPayload.type,\n\t\t\t\t\t\tvisibility: timelineItemPayload.visibility,\n\t\t\t\t\t\tuserId: timelineItemPayload.userId,\n\t\t\t\t\t\taiAgentId: timelineItemPayload.aiAgentId,\n\t\t\t\t\t\tvisitorId: timelineItemPayload.visitorId,\n\t\t\t\t\t\tcreatedAt: timelineItemPayload.createdAt,\n\t\t\t\t\t\tparts: timelineItemPayload.parts,\n\t\t\t\t\t},\n\t\t\t\t\tcreateIfPending: true,\n\t\t\t\t});\n\n\t\t\t\tconst messageId = response.item.id;\n\n\t\t\t\tif (!messageId) {\n\t\t\t\t\tthrow new Error(\"SendMessage response missing item.id\");\n\t\t\t\t}\n\n\t\t\t\tconst result: SendMessageResult = {\n\t\t\t\t\tconversationId,\n\t\t\t\t\tmessageId,\n\t\t\t\t};\n\n\t\t\t\tif (\"conversation\" in response && response.conversation) {\n\t\t\t\t\tresult.conversation = response.conversation;\n\t\t\t\t\tresult.initialTimelineItems = response.initialTimelineItems;\n\t\t\t\t} else if (initialConversation) {\n\t\t\t\t\tresult.conversation = initialConversation;\n\t\t\t\t\tresult.initialTimelineItems = preparedDefaultTimelineItems;\n\t\t\t\t}\n\n\t\t\t\tsetIsPending(false);\n\t\t\t\tsetError(null);\n\t\t\t\tonSuccess?.(result.conversationId, result.messageId);\n\t\t\t\treturn result;\n\t\t\t} catch (raw) {\n\t\t\t\tconst normalised = toError(raw);\n\t\t\t\tsetIsPending(false);\n\t\t\t\tsetError(normalised);\n\t\t\t\tonError?.(normalised);\n\t\t\t\tthrow normalised;\n\t\t\t}\n\t\t},\n\t\t[client]\n\t);\n\n\tconst mutate = useCallback(\n\t\t(opts: SendMessageOptions) => {\n\t\t\tvoid mutateAsync(opts).catch(() => {\n\t\t\t\t// Swallow errors to mimic react-query behaviour for mutate\n\t\t\t});\n\t\t},\n\t\t[mutateAsync]\n\t);\n\n\tconst reset = useCallback(() => {\n\t\tsetError(null);\n\t\tsetIsPending(false);\n\t\tsetIsUploading(false);\n\t}, []);\n\n\treturn {\n\t\tmutate,\n\t\tmutateAsync,\n\t\tisPending,\n\t\tisUploading,\n\t\terror,\n\t\treset,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-send-message.js","names":["parts: TimelineItemParts","initialConversation:\n\t\t\t\t\t| CreateConversationResponseBody[\"conversation\"]\n\t\t\t\t\t| undefined","fileParts: Array<TimelinePartImage | TimelinePartFile>","result: SendMessageResult"],"sources":["../../src/hooks/use-send-message.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport {\n\tgenerateMessageId,\n\tisImageMimeType,\n\tvalidateFiles,\n} from \"@cossistant/core\";\nimport type { CreateConversationResponseBody } from \"@cossistant/types/api/conversation\";\nimport type {\n\tTimelineItem,\n\tTimelineItemParts,\n\tTimelinePartFile,\n\tTimelinePartImage,\n} from \"@cossistant/types/api/timeline-item\";\nimport { useCallback, useState } from \"react\";\n\nimport { useSupport } from \"../provider\";\n\nexport type SendMessageOptions = {\n\tconversationId?: string | null;\n\tmessage: string;\n\tfiles?: File[];\n\tdefaultTimelineItems?: TimelineItem[];\n\tvisitorId?: string;\n\t/**\n\t * Optional message ID to use for the optimistic update and API request.\n\t * When not provided, a ULID will be generated on the client.\n\t */\n\tmessageId?: string;\n\tonSuccess?: (conversationId: string, messageId: string) => void;\n\tonError?: (error: Error) => void;\n\t/**\n\t * Called immediately after a new conversation is initiated (before API call).\n\t * Use this to immediately switch the UI to the new conversation ID for\n\t * proper optimistic updates display.\n\t */\n\tonConversationInitiated?: (conversationId: string) => void;\n};\n\nexport type SendMessageResult = {\n\tconversationId: string;\n\tmessageId: string;\n\tconversation?: CreateConversationResponseBody[\"conversation\"];\n\tinitialTimelineItems?: CreateConversationResponseBody[\"initialTimelineItems\"];\n};\n\nexport type UseSendMessageResult = {\n\tmutate: (options: SendMessageOptions) => void;\n\tmutateAsync: (\n\t\toptions: SendMessageOptions\n\t) => Promise<SendMessageResult | null>;\n\tisPending: boolean;\n\tisUploading: boolean;\n\terror: Error | null;\n\treset: () => void;\n};\n\nexport type UseSendMessageOptions = {\n\tclient?: CossistantClient;\n};\n\nfunction toError(error: unknown): Error {\n\tif (error instanceof Error) {\n\t\treturn error;\n\t}\n\n\tif (typeof error === \"string\") {\n\t\treturn new Error(error);\n\t}\n\n\treturn new Error(\"Unknown error\");\n}\n\ntype BuildTimelineItemPayloadOptions = {\n\tbody: string;\n\tconversationId: string;\n\tvisitorId: string | null;\n\tmessageId?: string;\n\tfileParts?: Array<TimelinePartImage | TimelinePartFile>;\n};\n\nfunction buildTimelineItemPayload({\n\tbody,\n\tconversationId,\n\tvisitorId,\n\tmessageId,\n\tfileParts,\n}: BuildTimelineItemPayloadOptions): TimelineItem {\n\tconst nowIso = typeof window !== \"undefined\" ? new Date().toISOString() : \"\";\n\tconst id = messageId ?? generateMessageId();\n\n\t// Build parts array: text first, then any file/image parts\n\tconst parts: TimelineItemParts = [{ type: \"text\" as const, text: body }];\n\n\tif (fileParts && fileParts.length > 0) {\n\t\tparts.push(...fileParts);\n\t}\n\n\treturn {\n\t\tid,\n\t\tconversationId,\n\t\torganizationId: \"\", // Will be set by backend\n\t\ttype: \"message\" as const,\n\t\ttext: body,\n\t\tparts,\n\t\tvisibility: \"public\" as const,\n\t\tuserId: null,\n\t\taiAgentId: null,\n\t\tvisitorId: visitorId ?? null,\n\t\tcreatedAt: nowIso,\n\t\tdeletedAt: null,\n\t} satisfies TimelineItem;\n}\n\n/**\n * Upload files and return timeline parts for inclusion in a message.\n */\nasync function uploadFilesForMessage(\n\tclient: CossistantClient,\n\tfiles: File[],\n\tconversationId: string\n): Promise<Array<TimelinePartImage | TimelinePartFile>> {\n\tif (files.length === 0) {\n\t\treturn [];\n\t}\n\n\t// Validate files first\n\tconst validationError = validateFiles(files);\n\tif (validationError) {\n\t\tthrow new Error(validationError);\n\t}\n\n\t// Upload files in parallel\n\tconst uploadPromises = files.map(async (file) => {\n\t\t// Generate presigned URL\n\t\tconst uploadInfo = await client.generateUploadUrl({\n\t\t\tconversationId,\n\t\t\tcontentType: file.type,\n\t\t\tfileName: file.name,\n\t\t});\n\n\t\t// Upload file to S3\n\t\tawait client.uploadFile(file, uploadInfo.uploadUrl, file.type);\n\n\t\t// Return timeline part based on file type\n\t\tconst isImage = isImageMimeType(file.type);\n\n\t\tif (isImage) {\n\t\t\treturn {\n\t\t\t\ttype: \"image\" as const,\n\t\t\t\turl: uploadInfo.publicUrl,\n\t\t\t\tmediaType: file.type,\n\t\t\t\tfilename: file.name,\n\t\t\t\tsize: file.size,\n\t\t\t} satisfies TimelinePartImage;\n\t\t}\n\n\t\treturn {\n\t\t\ttype: \"file\" as const,\n\t\t\turl: uploadInfo.publicUrl,\n\t\t\tmediaType: file.type,\n\t\t\tfilename: file.name,\n\t\t\tsize: file.size,\n\t\t} satisfies TimelinePartFile;\n\t});\n\n\treturn Promise.all(uploadPromises);\n}\n\n/**\n * Sends visitor messages while handling optimistic pending conversations and\n * exposing react-query-like mutation state.\n */\nexport function useSendMessage(\n\toptions: UseSendMessageOptions = {}\n): UseSendMessageResult {\n\tconst { client: contextClient } = useSupport();\n\tconst client = options.client ?? contextClient;\n\n\tconst [isPending, setIsPending] = useState(false);\n\tconst [isUploading, setIsUploading] = useState(false);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\tconst mutateAsync = useCallback(\n\t\tasync (payload: SendMessageOptions): Promise<SendMessageResult | null> => {\n\t\t\tconst {\n\t\t\t\tconversationId: providedConversationId,\n\t\t\t\tmessage,\n\t\t\t\tfiles = [],\n\t\t\t\tdefaultTimelineItems = [],\n\t\t\t\tvisitorId,\n\t\t\t\tmessageId: providedMessageId,\n\t\t\t\tonSuccess,\n\t\t\t\tonError,\n\t\t\t\tonConversationInitiated,\n\t\t\t} = payload;\n\n\t\t\t// Allow empty message if there are files\n\t\t\tif (!message.trim() && files.length === 0) {\n\t\t\t\tconst emptyMessageError = new Error(\n\t\t\t\t\t\"Message cannot be empty (or attach files)\"\n\t\t\t\t);\n\t\t\t\tsetError(emptyMessageError);\n\t\t\t\tonError?.(emptyMessageError);\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tsetIsPending(true);\n\t\t\tsetError(null);\n\n\t\t\ttry {\n\t\t\t\tif (!client) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\"Cossistant client is not available. Please ensure you have configured your API key.\"\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tlet conversationId = providedConversationId ?? undefined;\n\t\t\t\tlet preparedDefaultTimelineItems = defaultTimelineItems;\n\t\t\t\tlet initialConversation:\n\t\t\t\t\t| CreateConversationResponseBody[\"conversation\"]\n\t\t\t\t\t| undefined;\n\n\t\t\t\tif (!conversationId) {\n\t\t\t\t\tconst initiated = client.initiateConversation({\n\t\t\t\t\t\tdefaultTimelineItems,\n\t\t\t\t\t\tvisitorId: visitorId ?? undefined,\n\t\t\t\t\t});\n\t\t\t\t\tconversationId = initiated.conversationId;\n\t\t\t\t\tpreparedDefaultTimelineItems = initiated.defaultTimelineItems;\n\t\t\t\t\tinitialConversation = initiated.conversation;\n\t\t\t\t\t// Immediately notify about the new conversation ID so UI can switch\n\t\t\t\t\t// to reading from the right store key for optimistic updates\n\t\t\t\t\tonConversationInitiated?.(conversationId);\n\t\t\t\t}\n\n\t\t\t\t// Upload files BEFORE sending the message\n\t\t\t\tlet fileParts: Array<TimelinePartImage | TimelinePartFile> = [];\n\t\t\t\tif (files.length > 0) {\n\t\t\t\t\tsetIsUploading(true);\n\t\t\t\t\ttry {\n\t\t\t\t\t\tfileParts = await uploadFilesForMessage(\n\t\t\t\t\t\t\tclient,\n\t\t\t\t\t\t\tfiles,\n\t\t\t\t\t\t\tconversationId\n\t\t\t\t\t\t);\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tsetIsUploading(false);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst timelineItemPayload = buildTimelineItemPayload({\n\t\t\t\t\tbody: message,\n\t\t\t\t\tconversationId,\n\t\t\t\t\tvisitorId: visitorId ?? null,\n\t\t\t\t\tmessageId: providedMessageId,\n\t\t\t\t\tfileParts,\n\t\t\t\t});\n\n\t\t\t\tconst response = await client.sendMessage({\n\t\t\t\t\tconversationId,\n\t\t\t\t\titem: {\n\t\t\t\t\t\tid: timelineItemPayload.id,\n\t\t\t\t\t\ttext: timelineItemPayload.text ?? \"\",\n\t\t\t\t\t\ttype: timelineItemPayload.type === \"event\" ? \"event\" : \"message\",\n\t\t\t\t\t\tvisibility: timelineItemPayload.visibility,\n\t\t\t\t\t\tuserId: timelineItemPayload.userId,\n\t\t\t\t\t\taiAgentId: timelineItemPayload.aiAgentId,\n\t\t\t\t\t\tvisitorId: timelineItemPayload.visitorId,\n\t\t\t\t\t\tcreatedAt: timelineItemPayload.createdAt,\n\t\t\t\t\t\tparts: timelineItemPayload.parts,\n\t\t\t\t\t},\n\t\t\t\t\tcreateIfPending: true,\n\t\t\t\t});\n\n\t\t\t\tconst messageId = response.item.id;\n\n\t\t\t\tif (!messageId) {\n\t\t\t\t\tthrow new Error(\"SendMessage response missing item.id\");\n\t\t\t\t}\n\n\t\t\t\tconst result: SendMessageResult = {\n\t\t\t\t\tconversationId,\n\t\t\t\t\tmessageId,\n\t\t\t\t};\n\n\t\t\t\tif (\"conversation\" in response && response.conversation) {\n\t\t\t\t\tresult.conversation = response.conversation;\n\t\t\t\t\tresult.initialTimelineItems = response.initialTimelineItems;\n\t\t\t\t} else if (initialConversation) {\n\t\t\t\t\tresult.conversation = initialConversation;\n\t\t\t\t\tresult.initialTimelineItems = preparedDefaultTimelineItems;\n\t\t\t\t}\n\n\t\t\t\tsetIsPending(false);\n\t\t\t\tsetError(null);\n\t\t\t\tonSuccess?.(result.conversationId, result.messageId);\n\t\t\t\treturn result;\n\t\t\t} catch (raw) {\n\t\t\t\tconst normalised = toError(raw);\n\t\t\t\tsetIsPending(false);\n\t\t\t\tsetError(normalised);\n\t\t\t\tonError?.(normalised);\n\t\t\t\tthrow normalised;\n\t\t\t}\n\t\t},\n\t\t[client]\n\t);\n\n\tconst mutate = useCallback(\n\t\t(opts: SendMessageOptions) => {\n\t\t\tvoid mutateAsync(opts).catch(() => {\n\t\t\t\t// Swallow errors to mimic react-query behaviour for mutate\n\t\t\t});\n\t\t},\n\t\t[mutateAsync]\n\t);\n\n\tconst reset = useCallback(() => {\n\t\tsetError(null);\n\t\tsetIsPending(false);\n\t\tsetIsUploading(false);\n\t}, []);\n\n\treturn {\n\t\tmutate,\n\t\tmutateAsync,\n\t\tisPending,\n\t\tisUploading,\n\t\terror,\n\t\treset,\n\t};\n}\n"],"mappings":"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"}
|