@cossistant/react 0.0.18 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/hooks/private/use-default-messages.js +1 -1
- package/hooks/private/use-default-messages.js.map +1 -1
- package/hooks/private/use-grouped-messages.js +1 -1
- package/hooks/private/use-grouped-messages.js.map +1 -1
- package/hooks/private/use-rest-client.js +2 -2
- package/hooks/private/use-rest-client.js.map +1 -1
- package/hooks/private/use-visitor-typing-reporter.js +3 -3
- package/hooks/private/use-visitor-typing-reporter.js.map +1 -1
- package/hooks/use-conversation-page.js +1 -1
- package/hooks/use-conversation-page.js.map +1 -1
- package/hooks/use-send-message.js +1 -1
- package/hooks/use-send-message.js.map +1 -1
- package/package.json +3 -3
- package/primitives/multimodal-input.d.ts +2 -2
- package/primitives/multimodal-input.d.ts.map +1 -1
- package/primitives/page-registry.d.ts.map +1 -1
- package/realtime/provider.d.ts +1 -0
- package/realtime/provider.d.ts.map +1 -1
- package/realtime/provider.js +58 -9
- package/realtime/provider.js.map +1 -1
- package/support/components/button.d.ts +1 -1
- package/support/text/runtime.js +1 -1
- package/support/text/runtime.js.map +1 -1
- package/support/utils/time.d.ts +1 -0
- package/support/utils/time.d.ts.map +1 -1
- package/support/utils/time.js +2 -0
- package/support/utils/time.js.map +1 -1
- package/utils/use-render-element.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ import "@cossistant/react/support.css";
|
|
|
48
48
|
|
|
49
49
|
export function App() {
|
|
50
50
|
return (
|
|
51
|
-
<SupportProvider publicKey={process.env.
|
|
51
|
+
<SupportProvider publicKey={process.env.NEXT_PUBLIC_COSSISTANT_API_KEY}>
|
|
52
52
|
<Support />
|
|
53
53
|
</SupportProvider>
|
|
54
54
|
);
|
|
@@ -14,7 +14,7 @@ function useDefaultMessages({ conversationId }) {
|
|
|
14
14
|
const { defaultMessages, availableAIAgents, availableHumanAgents } = useSupport();
|
|
15
15
|
return useMemo(() => defaultMessages.map((message, index) => {
|
|
16
16
|
const messageId = generateMessageId();
|
|
17
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
17
|
+
const timestamp = typeof window !== "undefined" ? (/* @__PURE__ */ new Date()).toISOString() : "";
|
|
18
18
|
return {
|
|
19
19
|
id: messageId,
|
|
20
20
|
conversationId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-default-messages.js","names":[],"sources":["../../../src/hooks/private/use-default-messages.ts"],"sourcesContent":["import { generateMessageId } from \"@cossistant/core\";\nimport { SenderType } from \"@cossistant/types\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport { useMemo } from \"react\";\nimport { useSupport } from \"../../provider\";\n\ntype UseDefaultMessagesParams = {\n\tconversationId: string;\n};\n\n/**\n * Mirrors the provider-configured default messages into timeline items so\n * that welcome content renders immediately while the backend conversation is\n * still being created. Agent fallbacks are resolved against available humans\n * and AI agents exposed by the provider context.\n */\nexport function useDefaultMessages({\n\tconversationId,\n}: UseDefaultMessagesParams): TimelineItem[] {\n\tconst { defaultMessages, availableAIAgents, availableHumanAgents } =\n\t\tuseSupport();\n\n\tconst memoisedDefaultTimelineItems = useMemo(\n\t\t() =>\n\t\t\tdefaultMessages.map((message, index) => {\n\t\t\t\tconst messageId = generateMessageId();\n\t\t\t\tconst timestamp
|
|
1
|
+
{"version":3,"file":"use-default-messages.js","names":[],"sources":["../../../src/hooks/private/use-default-messages.ts"],"sourcesContent":["import { generateMessageId } from \"@cossistant/core\";\nimport { SenderType } from \"@cossistant/types\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport { useMemo } from \"react\";\nimport { useSupport } from \"../../provider\";\n\ntype UseDefaultMessagesParams = {\n\tconversationId: string;\n};\n\n/**\n * Mirrors the provider-configured default messages into timeline items so\n * that welcome content renders immediately while the backend conversation is\n * still being created. Agent fallbacks are resolved against available humans\n * and AI agents exposed by the provider context.\n */\nexport function useDefaultMessages({\n\tconversationId,\n}: UseDefaultMessagesParams): TimelineItem[] {\n\tconst { defaultMessages, availableAIAgents, availableHumanAgents } =\n\t\tuseSupport();\n\n\tconst memoisedDefaultTimelineItems = useMemo(\n\t\t() =>\n\t\t\tdefaultMessages.map((message, index) => {\n\t\t\t\tconst messageId = generateMessageId();\n\t\t\t\tconst timestamp =\n\t\t\t\t\ttypeof window !== \"undefined\" ? new Date().toISOString() : \"\";\n\n\t\t\t\treturn {\n\t\t\t\t\tid: messageId,\n\t\t\t\t\tconversationId,\n\t\t\t\t\torganizationId: \"\", // Not available for default messages\n\t\t\t\t\ttype: \"message\" as const,\n\t\t\t\t\ttext: message.content,\n\t\t\t\t\tparts: [{ type: \"text\" as const, text: message.content }],\n\t\t\t\t\tvisibility: \"public\" as const,\n\t\t\t\t\tuserId:\n\t\t\t\t\t\tmessage.senderType === SenderType.TEAM_MEMBER\n\t\t\t\t\t\t\t? message.senderId || availableHumanAgents[0]?.id || null\n\t\t\t\t\t\t\t: null,\n\t\t\t\t\taiAgentId:\n\t\t\t\t\t\tmessage.senderType === SenderType.AI\n\t\t\t\t\t\t\t? message.senderId || availableAIAgents[0]?.id || null\n\t\t\t\t\t\t\t: null,\n\t\t\t\t\tvisitorId:\n\t\t\t\t\t\tmessage.senderType === SenderType.VISITOR\n\t\t\t\t\t\t\t? message.senderId || null\n\t\t\t\t\t\t\t: null,\n\t\t\t\t\tcreatedAt: timestamp,\n\t\t\t\t\tdeletedAt: null,\n\t\t\t\t} satisfies TimelineItem;\n\t\t\t}),\n\t\t[\n\t\t\tdefaultMessages,\n\t\t\tavailableHumanAgents[0]?.id,\n\t\t\tavailableAIAgents[0]?.id,\n\t\t\tconversationId,\n\t\t]\n\t);\n\n\treturn memoisedDefaultTimelineItems;\n}\n"],"mappings":";;;;;;;;;;;;AAgBA,SAAgB,mBAAmB,EAClC,kBAC4C;CAC5C,MAAM,EAAE,iBAAiB,mBAAmB,yBAC3C,YAAY;AAyCb,QAvCqC,cAEnC,gBAAgB,KAAK,SAAS,UAAU;EACvC,MAAM,YAAY,mBAAmB;EACrC,MAAM,YACL,OAAO,WAAW,+BAAc,IAAI,MAAM,EAAC,aAAa,GAAG;AAE5D,SAAO;GACN,IAAI;GACJ;GACA,gBAAgB;GAChB,MAAM;GACN,MAAM,QAAQ;GACd,OAAO,CAAC;IAAE,MAAM;IAAiB,MAAM,QAAQ;IAAS,CAAC;GACzD,YAAY;GACZ,QACC,QAAQ,eAAe,WAAW,cAC/B,QAAQ,YAAY,qBAAqB,IAAI,MAAM,OACnD;GACJ,WACC,QAAQ,eAAe,WAAW,KAC/B,QAAQ,YAAY,kBAAkB,IAAI,MAAM,OAChD;GACJ,WACC,QAAQ,eAAe,WAAW,UAC/B,QAAQ,YAAY,OACpB;GACJ,WAAW;GACX,WAAW;GACX;GACA,EACH;EACC;EACA,qBAAqB,IAAI;EACzB,kBAAkB,IAAI;EACtB;EACA,CACD"}
|
|
@@ -8,7 +8,7 @@ const getTimestamp = (date) => {
|
|
|
8
8
|
return date.getTime();
|
|
9
9
|
};
|
|
10
10
|
const toDate = (date) => {
|
|
11
|
-
if (!date) return /* @__PURE__ */ new Date();
|
|
11
|
+
if (!date) return typeof window !== "undefined" ? /* @__PURE__ */ new Date() : /* @__PURE__ */ new Date(0);
|
|
12
12
|
if (typeof date === "string") return new Date(date);
|
|
13
13
|
return date;
|
|
14
14
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-grouped-messages.js","names":["EMPTY_STRING_ARRAY: readonly string[]","result: ConversationItem[]","currentGroup: GroupedMessage | 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 ConversationItem =\n\t| GroupedMessage\n\t| TimelineEventItem\n\t| TimelineToolItem;\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\tviewerType?: SenderType; // Type of the current viewer\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 new Date();\n\t}\n\tif (typeof date === \"string\") {\n\t\treturn new Date(date);\n\t}\n\treturn date;\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)\nconst groupTimelineItems = (items: TimelineItem[]): ConversationItem[] => {\n\tconst result: ConversationItem[] = [];\n\tlet currentGroup: GroupedMessage | null = null;\n\n\tfor (const item of items) {\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: toDate(item.createdAt),\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: toDate(item.createdAt),\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\n\t\t\tcurrentGroup.items.push(item);\n\t\t\tcurrentGroup.lastMessageId = item.id || currentGroup.lastMessageId;\n\t\t\tcurrentGroup.lastMessageTime = toDate(item.createdAt);\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: toDate(item.createdAt),\n\t\t\t\tlastMessageTime: toDate(item.createdAt),\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\nconst buildTimelineReadReceiptData = (\n\tseenData: ConversationSeen[],\n\titems: TimelineItem[]\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// Sort items by time to process in order\n\tconst sortedItems = [...items]\n\t\t.filter((item) => item.type === \"message\")\n\t\t.sort((a, b) => getTimestamp(a.createdAt) - getTimestamp(b.createdAt));\n\n\t// Process seen data for each viewer\n\tfor (const seen of seenData) {\n\t\tlet seenTime = getTimestamp(seen.updatedAt);\n\t\tconst viewerId = seen.userId || seen.visitorId || seen.aiAgentId;\n\t\tif (!viewerId) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Find the last message sent by this viewer\n\t\tconst lastItemByViewer = sortedItems\n\t\t\t.filter((item) => {\n\t\t\t\tif (seen.userId) {\n\t\t\t\t\treturn item.userId === viewerId;\n\t\t\t\t}\n\t\t\t\tif (seen.visitorId) {\n\t\t\t\t\treturn item.visitorId === viewerId;\n\t\t\t\t}\n\t\t\t\tif (seen.aiAgentId) {\n\t\t\t\t\treturn item.aiAgentId === viewerId;\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t})\n\t\t\t.at(-1);\n\n\t\tif (lastItemByViewer) {\n\t\t\tconst lastItemTime = getTimestamp(lastItemByViewer.createdAt);\n\t\t\tif (lastItemTime > seenTime) {\n\t\t\t\tseenTime = lastItemTime;\n\t\t\t}\n\t\t}\n\n\t\tlet lastReadItem: TimelineItem | null = null;\n\t\tlet unreadCount = 0;\n\t\tlet hasPassedLastSeen = false;\n\n\t\t// Process items in chronological order\n\t\tfor (const item of sortedItems) {\n\t\t\tconst itemTime = getTimestamp(item.createdAt);\n\n\t\t\tif (itemTime <= seenTime && !hasPassedLastSeen) {\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\thasPassedLastSeen = true;\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\tviewerType,\n}: UseGroupedMessagesOptions) => {\n\treturn useMemo(() => {\n\t\tconst groupedItems = groupTimelineItems(items);\n\n\t\t// Build read receipt data\n\t\tconst { seenByMap, lastReadMessageMap, unreadCountMap } =\n\t\t\tbuildTimelineReadReceiptData(seenData, items);\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\tconst messageIndex = items.findIndex((item) => item.id === messageId);\n\t\t\t\tconst lastReadIndex = items.findIndex((item) => item.id === lastRead);\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[]","currentGroup: GroupedMessage | 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 ConversationItem =\n\t| GroupedMessage\n\t| TimelineEventItem\n\t| TimelineToolItem;\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\tviewerType?: SenderType; // Type of the current viewer\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 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)\nconst groupTimelineItems = (items: TimelineItem[]): ConversationItem[] => {\n\tconst result: ConversationItem[] = [];\n\tlet currentGroup: GroupedMessage | null = null;\n\n\tfor (const item of items) {\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: toDate(item.createdAt),\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: toDate(item.createdAt),\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\n\t\t\tcurrentGroup.items.push(item);\n\t\t\tcurrentGroup.lastMessageId = item.id || currentGroup.lastMessageId;\n\t\t\tcurrentGroup.lastMessageTime = toDate(item.createdAt);\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: toDate(item.createdAt),\n\t\t\t\tlastMessageTime: toDate(item.createdAt),\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\nconst buildTimelineReadReceiptData = (\n\tseenData: ConversationSeen[],\n\titems: TimelineItem[]\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// Sort items by time to process in order\n\tconst sortedItems = [...items]\n\t\t.filter((item) => item.type === \"message\")\n\t\t.sort((a, b) => getTimestamp(a.createdAt) - getTimestamp(b.createdAt));\n\n\t// Process seen data for each viewer\n\tfor (const seen of seenData) {\n\t\tlet seenTime = getTimestamp(seen.updatedAt);\n\t\tconst viewerId = seen.userId || seen.visitorId || seen.aiAgentId;\n\t\tif (!viewerId) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Find the last message sent by this viewer\n\t\tconst lastItemByViewer = sortedItems\n\t\t\t.filter((item) => {\n\t\t\t\tif (seen.userId) {\n\t\t\t\t\treturn item.userId === viewerId;\n\t\t\t\t}\n\t\t\t\tif (seen.visitorId) {\n\t\t\t\t\treturn item.visitorId === viewerId;\n\t\t\t\t}\n\t\t\t\tif (seen.aiAgentId) {\n\t\t\t\t\treturn item.aiAgentId === viewerId;\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t})\n\t\t\t.at(-1);\n\n\t\tif (lastItemByViewer) {\n\t\t\tconst lastItemTime = getTimestamp(lastItemByViewer.createdAt);\n\t\t\tif (lastItemTime > seenTime) {\n\t\t\t\tseenTime = lastItemTime;\n\t\t\t}\n\t\t}\n\n\t\tlet lastReadItem: TimelineItem | null = null;\n\t\tlet unreadCount = 0;\n\t\tlet hasPassedLastSeen = false;\n\n\t\t// Process items in chronological order\n\t\tfor (const item of sortedItems) {\n\t\t\tconst itemTime = getTimestamp(item.createdAt);\n\n\t\t\tif (itemTime <= seenTime && !hasPassedLastSeen) {\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\thasPassedLastSeen = true;\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\tviewerType,\n}: UseGroupedMessagesOptions) => {\n\treturn useMemo(() => {\n\t\tconst groupedItems = groupTimelineItems(items);\n\n\t\t// Build read receipt data\n\t\tconst { seenByMap, lastReadMessageMap, unreadCountMap } =\n\t\t\tbuildTimelineReadReceiptData(seenData, items);\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\tconst messageIndex = items.findIndex((item) => item.id === messageId);\n\t\t\t\tconst lastReadIndex = items.findIndex((item) => item.id === lastRead);\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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"}
|
|
@@ -14,9 +14,9 @@ import { CossistantClient } from "@cossistant/core";
|
|
|
14
14
|
function useClient(publicKey, apiUrl = "https://api.cossistant.com/v1", wsUrl = "wss://api.cossistant.com/ws") {
|
|
15
15
|
return {
|
|
16
16
|
client: useMemo(() => {
|
|
17
|
-
const keyFromEnv = process.env.NEXT_PUBLIC_COSSISTANT_KEY || process.env.
|
|
17
|
+
const keyFromEnv = process.env.NEXT_PUBLIC_COSSISTANT_API_KEY || process.env.NEXT_PUBLIC_COSSISTANT_KEY || process.env.COSSISTANT_API_KEY;
|
|
18
18
|
const keyToUse = publicKey ?? keyFromEnv;
|
|
19
|
-
if (!keyToUse) throw new Error("Public key is required. Please provide it as a prop or set
|
|
19
|
+
if (!keyToUse) throw new Error("Public key is required. Please provide it as a prop or set NEXT_PUBLIC_COSSISTANT_API_KEY environment variable.");
|
|
20
20
|
const config = {
|
|
21
21
|
apiUrl,
|
|
22
22
|
wsUrl,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-rest-client.js","names":["config: CossistantConfig","err: unknown"],"sources":["../../../src/hooks/private/use-rest-client.ts"],"sourcesContent":["\"use client\";\n\nimport { CossistantClient } from \"@cossistant/core\";\nimport type { CossistantConfig } from \"@cossistant/types\";\nimport { useMemo } from \"react\";\n\nexport type UseClientResult = {\n\tclient: CossistantClient;\n\terror: Error | null;\n};\n\n/**\n * Creates a memoised `CossistantClient` instance using the provided endpoints\n * and public key. When no key is passed the hook falls back to environment\n * variables and surfaces missing configuration errors through the returned\n * `error` field.\n */\nexport function useClient(\n\tpublicKey: string | undefined,\n\tapiUrl = \"https://api.cossistant.com/v1\",\n\twsUrl = \"wss://api.cossistant.com/ws\"\n): UseClientResult {\n\tconst client = useMemo(() => {\n\t\tconst keyFromEnv =\n\t\t\tprocess.env.NEXT_PUBLIC_COSSISTANT_KEY ||\n\t\t\tprocess.env.
|
|
1
|
+
{"version":3,"file":"use-rest-client.js","names":["config: CossistantConfig","err: unknown"],"sources":["../../../src/hooks/private/use-rest-client.ts"],"sourcesContent":["\"use client\";\n\nimport { CossistantClient } from \"@cossistant/core\";\nimport type { CossistantConfig } from \"@cossistant/types\";\nimport { useMemo } from \"react\";\n\nexport type UseClientResult = {\n\tclient: CossistantClient;\n\terror: Error | null;\n};\n\n/**\n * Creates a memoised `CossistantClient` instance using the provided endpoints\n * and public key. When no key is passed the hook falls back to environment\n * variables and surfaces missing configuration errors through the returned\n * `error` field.\n */\nexport function useClient(\n\tpublicKey: string | undefined,\n\tapiUrl = \"https://api.cossistant.com/v1\",\n\twsUrl = \"wss://api.cossistant.com/ws\"\n): UseClientResult {\n\tconst client = useMemo(() => {\n\t\tconst keyFromEnv =\n\t\t\tprocess.env.NEXT_PUBLIC_COSSISTANT_API_KEY ||\n\t\t\tprocess.env.NEXT_PUBLIC_COSSISTANT_KEY ||\n\t\t\tprocess.env.COSSISTANT_API_KEY;\n\t\tconst keyToUse = publicKey ?? keyFromEnv;\n\n\t\tif (!keyToUse) {\n\t\t\tthrow new Error(\n\t\t\t\t\"Public key is required. Please provide it as a prop or set NEXT_PUBLIC_COSSISTANT_API_KEY environment variable.\"\n\t\t\t);\n\t\t}\n\n\t\tconst config: CossistantConfig = {\n\t\t\tapiUrl,\n\t\t\twsUrl,\n\t\t\tpublicKey: keyToUse,\n\t\t};\n\n\t\ttry {\n\t\t\treturn new CossistantClient(config);\n\t\t} catch (err: unknown) {\n\t\t\tthrow err instanceof Error\n\t\t\t\t? err\n\t\t\t\t: new Error(\"Failed to initialize Cossistant client\");\n\t\t}\n\t}, [publicKey, apiUrl, wsUrl]);\n\n\treturn { client, error: null };\n}\n"],"mappings":";;;;;;;;;;;;;AAiBA,SAAgB,UACf,WACA,SAAS,iCACT,QAAQ,+BACU;AA6BlB,QAAO;EAAE,QA5BM,cAAc;GAC5B,MAAM,aACL,QAAQ,IAAI,kCACZ,QAAQ,IAAI,8BACZ,QAAQ,IAAI;GACb,MAAM,WAAW,aAAa;AAE9B,OAAI,CAAC,SACJ,OAAM,IAAI,MACT,kHACA;GAGF,MAAMA,SAA2B;IAChC;IACA;IACA,WAAW;IACX;AAED,OAAI;AACH,WAAO,IAAI,iBAAiB,OAAO;YAC3BC,KAAc;AACtB,UAAM,eAAe,QAClB,sBACA,IAAI,MAAM,yCAAyC;;KAErD;GAAC;GAAW;GAAQ;GAAM,CAAC;EAEb,OAAO;EAAM"}
|
|
@@ -68,7 +68,7 @@ function useVisitorTypingReporter({ client, conversationId }) {
|
|
|
68
68
|
if (!(client && conversationId)) return;
|
|
69
69
|
const trimmed = value.trim();
|
|
70
70
|
latestPreviewRef.current = trimmed.slice(0, PREVIEW_MAX_LENGTH);
|
|
71
|
-
const now = Date.now();
|
|
71
|
+
const now = typeof window !== "undefined" ? Date.now() : 0;
|
|
72
72
|
if (trimmed.length === 0) {
|
|
73
73
|
if (typingActiveRef.current) {
|
|
74
74
|
typingActiveRef.current = false;
|
|
@@ -104,7 +104,7 @@ function useVisitorTypingReporter({ client, conversationId }) {
|
|
|
104
104
|
const handleSubmit = useCallback(() => {
|
|
105
105
|
if (!typingActiveRef.current) return;
|
|
106
106
|
typingActiveRef.current = false;
|
|
107
|
-
lastSentAtRef.current = Date.now();
|
|
107
|
+
lastSentAtRef.current = typeof window !== "undefined" ? Date.now() : 0;
|
|
108
108
|
clearKeepAlive();
|
|
109
109
|
clearStopTypingTimeout();
|
|
110
110
|
sendTyping(false);
|
|
@@ -116,7 +116,7 @@ function useVisitorTypingReporter({ client, conversationId }) {
|
|
|
116
116
|
const stop = useCallback(() => {
|
|
117
117
|
if (!typingActiveRef.current) return;
|
|
118
118
|
typingActiveRef.current = false;
|
|
119
|
-
lastSentAtRef.current = Date.now();
|
|
119
|
+
lastSentAtRef.current = typeof window !== "undefined" ? Date.now() : 0;
|
|
120
120
|
clearKeepAlive();
|
|
121
121
|
clearStopTypingTimeout();
|
|
122
122
|
sendTyping(false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-visitor-typing-reporter.js","names":[],"sources":["../../../src/hooks/private/use-visitor-typing-reporter.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport { useCallback, useEffect, useRef } from \"react\";\n\nconst PREVIEW_MAX_LENGTH = 2000;\nconst SEND_INTERVAL_MS = 800;\nconst KEEP_ALIVE_MS = 4000;\nconst STOP_TYPING_DELAY_MS = 2000; // Send isTyping: false after 2 seconds of inactivity\n\ntype UseVisitorTypingReporterOptions = {\n\tclient: CossistantClient | null;\n\tconversationId: string | null;\n};\n\ntype UseVisitorTypingReporterResult = {\n\thandleInputChange: (value: string) => void;\n\thandleSubmit: () => void;\n\tstop: () => void;\n};\n\n/**\n * Tracks visitor composer activity and reports typing previews to the backend.\n *\n * Handles throttling, keep-alive pings, inactivity fallbacks and ensures a\n * `stop` event is emitted when the component unmounts.\n */\nexport function useVisitorTypingReporter({\n\tclient,\n\tconversationId,\n}: UseVisitorTypingReporterOptions): UseVisitorTypingReporterResult {\n\tconst typingActiveRef = useRef(false);\n\tconst lastSentAtRef = useRef(0);\n\tconst latestPreviewRef = useRef<string>(\"\");\n\tconst keepAliveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n\t\tnull\n\t);\n\tconst stopTypingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n\t\tnull\n\t);\n\n\tconst clearKeepAlive = useCallback(() => {\n\t\tif (keepAliveTimeoutRef.current) {\n\t\t\tclearTimeout(keepAliveTimeoutRef.current);\n\t\t\tkeepAliveTimeoutRef.current = null;\n\t\t}\n\t}, []);\n\n\tconst clearStopTypingTimeout = useCallback(() => {\n\t\tif (stopTypingTimeoutRef.current) {\n\t\t\tclearTimeout(stopTypingTimeoutRef.current);\n\t\t\tstopTypingTimeoutRef.current = null;\n\t\t}\n\t}, []);\n\n\tconst sendTyping = useCallback(\n\t\tasync (isTyping: boolean, preview?: string | null) => {\n\t\t\tif (!(client && conversationId)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tawait client.setVisitorTyping({\n\t\t\t\t\tconversationId,\n\t\t\t\t\tisTyping,\n\t\t\t\t\tvisitorPreview: preview ?? undefined,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"[Support] Failed to send typing event\", error);\n\t\t\t}\n\t\t},\n\t\t[client, conversationId]\n\t);\n\n\tconst scheduleKeepAlive = useCallback(() => {\n\t\tclearKeepAlive();\n\t\tkeepAliveTimeoutRef.current = setTimeout(() => {\n\t\t\tif (typingActiveRef.current) {\n\t\t\t\tvoid sendTyping(true, latestPreviewRef.current);\n\t\t\t\tscheduleKeepAlive();\n\t\t\t}\n\t\t}, KEEP_ALIVE_MS);\n\t}, [clearKeepAlive, sendTyping]);\n\n\tconst scheduleStopTyping = useCallback(() => {\n\t\tclearStopTypingTimeout();\n\t\tstopTypingTimeoutRef.current = setTimeout(() => {\n\t\t\tif (typingActiveRef.current) {\n\t\t\t\ttypingActiveRef.current = false;\n\t\t\t\tclearKeepAlive();\n\t\t\t\tvoid sendTyping(false);\n\t\t\t}\n\t\t}, STOP_TYPING_DELAY_MS);\n\t}, [clearStopTypingTimeout, clearKeepAlive, sendTyping]);\n\n\tconst handleInputChange = useCallback(\n\t\t(value: string) => {\n\t\t\tif (!(client && conversationId)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst trimmed = value.trim();\n\t\t\tlatestPreviewRef.current = trimmed.slice(0, PREVIEW_MAX_LENGTH);\n\t\t\tconst now = Date.now();\n\n\t\t\tif (trimmed.length === 0) {\n\t\t\t\tif (typingActiveRef.current) {\n\t\t\t\t\ttypingActiveRef.current = false;\n\t\t\t\t\tlastSentAtRef.current = now;\n\t\t\t\t\tclearKeepAlive();\n\t\t\t\t\tclearStopTypingTimeout();\n\t\t\t\t\tvoid sendTyping(false);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Schedule auto-stop after inactivity\n\t\t\tscheduleStopTyping();\n\n\t\t\tif (!typingActiveRef.current) {\n\t\t\t\ttypingActiveRef.current = true;\n\t\t\t\tlastSentAtRef.current = now;\n\t\t\t\tvoid sendTyping(true, latestPreviewRef.current);\n\t\t\t\tscheduleKeepAlive();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (now - lastSentAtRef.current >= SEND_INTERVAL_MS) {\n\t\t\t\tlastSentAtRef.current = now;\n\t\t\t\tvoid sendTyping(true, latestPreviewRef.current);\n\t\t\t\tscheduleKeepAlive();\n\t\t\t}\n\t\t},\n\t\t[\n\t\t\tclient,\n\t\t\tconversationId,\n\t\t\tsendTyping,\n\t\t\tscheduleKeepAlive,\n\t\t\tscheduleStopTyping,\n\t\t\tclearKeepAlive,\n\t\t\tclearStopTypingTimeout,\n\t\t]\n\t);\n\n\tconst handleSubmit = useCallback(() => {\n\t\tif (!typingActiveRef.current) {\n\t\t\treturn;\n\t\t}\n\n\t\ttypingActiveRef.current = false;\n\t\tlastSentAtRef.current = Date.now();\n\t\tclearKeepAlive();\n\t\tclearStopTypingTimeout();\n\t\tvoid sendTyping(false);\n\t}, [clearKeepAlive, clearStopTypingTimeout, sendTyping]);\n\n\tconst stop = useCallback(() => {\n\t\tif (!typingActiveRef.current) {\n\t\t\treturn;\n\t\t}\n\n\t\ttypingActiveRef.current = false;\n\t\tlastSentAtRef.current = Date.now();\n\t\tclearKeepAlive();\n\t\tclearStopTypingTimeout();\n\t\tvoid sendTyping(false);\n\t}, [clearKeepAlive, clearStopTypingTimeout, sendTyping]);\n\n\tuseEffect(\n\t\t() => () => {\n\t\t\tif (typingActiveRef.current) {\n\t\t\t\tvoid sendTyping(false);\n\t\t\t}\n\t\t\tclearKeepAlive();\n\t\t\tclearStopTypingTimeout();\n\t\t},\n\t\t[clearKeepAlive, clearStopTypingTimeout, sendTyping]\n\t);\n\n\treturn {\n\t\thandleInputChange,\n\t\thandleSubmit,\n\t\tstop,\n\t};\n}\n"],"mappings":";;;AAGA,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AACzB,MAAM,gBAAgB;AACtB,MAAM,uBAAuB;;;;;;;AAmB7B,SAAgB,yBAAyB,EACxC,QACA,kBACmE;CACnE,MAAM,kBAAkB,OAAO,MAAM;CACrC,MAAM,gBAAgB,OAAO,EAAE;CAC/B,MAAM,mBAAmB,OAAe,GAAG;CAC3C,MAAM,sBAAsB,OAC3B,KACA;CACD,MAAM,uBAAuB,OAC5B,KACA;CAED,MAAM,iBAAiB,kBAAkB;AACxC,MAAI,oBAAoB,SAAS;AAChC,gBAAa,oBAAoB,QAAQ;AACzC,uBAAoB,UAAU;;IAE7B,EAAE,CAAC;CAEN,MAAM,yBAAyB,kBAAkB;AAChD,MAAI,qBAAqB,SAAS;AACjC,gBAAa,qBAAqB,QAAQ;AAC1C,wBAAqB,UAAU;;IAE9B,EAAE,CAAC;CAEN,MAAM,aAAa,YAClB,OAAO,UAAmB,YAA4B;AACrD,MAAI,EAAE,UAAU,gBACf;AAGD,MAAI;AACH,SAAM,OAAO,iBAAiB;IAC7B;IACA;IACA,gBAAgB,WAAW;IAC3B,CAAC;WACM,OAAO;AACf,WAAQ,MAAM,yCAAyC,MAAM;;IAG/D,CAAC,QAAQ,eAAe,CACxB;CAED,MAAM,oBAAoB,kBAAkB;AAC3C,kBAAgB;AAChB,sBAAoB,UAAU,iBAAiB;AAC9C,OAAI,gBAAgB,SAAS;AAC5B,IAAK,WAAW,MAAM,iBAAiB,QAAQ;AAC/C,uBAAmB;;KAElB,cAAc;IACf,CAAC,gBAAgB,WAAW,CAAC;CAEhC,MAAM,qBAAqB,kBAAkB;AAC5C,0BAAwB;AACxB,uBAAqB,UAAU,iBAAiB;AAC/C,OAAI,gBAAgB,SAAS;AAC5B,oBAAgB,UAAU;AAC1B,oBAAgB;AAChB,IAAK,WAAW,MAAM;;KAErB,qBAAqB;IACtB;EAAC;EAAwB;EAAgB;EAAW,CAAC;CAExD,MAAM,oBAAoB,aACxB,UAAkB;AAClB,MAAI,EAAE,UAAU,gBACf;EAGD,MAAM,UAAU,MAAM,MAAM;AAC5B,mBAAiB,UAAU,QAAQ,MAAM,GAAG,mBAAmB;EAC/D,MAAM,MAAM,KAAK,KAAK;
|
|
1
|
+
{"version":3,"file":"use-visitor-typing-reporter.js","names":[],"sources":["../../../src/hooks/private/use-visitor-typing-reporter.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport { useCallback, useEffect, useRef } from \"react\";\n\nconst PREVIEW_MAX_LENGTH = 2000;\nconst SEND_INTERVAL_MS = 800;\nconst KEEP_ALIVE_MS = 4000;\nconst STOP_TYPING_DELAY_MS = 2000; // Send isTyping: false after 2 seconds of inactivity\n\ntype UseVisitorTypingReporterOptions = {\n\tclient: CossistantClient | null;\n\tconversationId: string | null;\n};\n\ntype UseVisitorTypingReporterResult = {\n\thandleInputChange: (value: string) => void;\n\thandleSubmit: () => void;\n\tstop: () => void;\n};\n\n/**\n * Tracks visitor composer activity and reports typing previews to the backend.\n *\n * Handles throttling, keep-alive pings, inactivity fallbacks and ensures a\n * `stop` event is emitted when the component unmounts.\n */\nexport function useVisitorTypingReporter({\n\tclient,\n\tconversationId,\n}: UseVisitorTypingReporterOptions): UseVisitorTypingReporterResult {\n\tconst typingActiveRef = useRef(false);\n\tconst lastSentAtRef = useRef(0);\n\tconst latestPreviewRef = useRef<string>(\"\");\n\tconst keepAliveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n\t\tnull\n\t);\n\tconst stopTypingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n\t\tnull\n\t);\n\n\tconst clearKeepAlive = useCallback(() => {\n\t\tif (keepAliveTimeoutRef.current) {\n\t\t\tclearTimeout(keepAliveTimeoutRef.current);\n\t\t\tkeepAliveTimeoutRef.current = null;\n\t\t}\n\t}, []);\n\n\tconst clearStopTypingTimeout = useCallback(() => {\n\t\tif (stopTypingTimeoutRef.current) {\n\t\t\tclearTimeout(stopTypingTimeoutRef.current);\n\t\t\tstopTypingTimeoutRef.current = null;\n\t\t}\n\t}, []);\n\n\tconst sendTyping = useCallback(\n\t\tasync (isTyping: boolean, preview?: string | null) => {\n\t\t\tif (!(client && conversationId)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tawait client.setVisitorTyping({\n\t\t\t\t\tconversationId,\n\t\t\t\t\tisTyping,\n\t\t\t\t\tvisitorPreview: preview ?? undefined,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"[Support] Failed to send typing event\", error);\n\t\t\t}\n\t\t},\n\t\t[client, conversationId]\n\t);\n\n\tconst scheduleKeepAlive = useCallback(() => {\n\t\tclearKeepAlive();\n\t\tkeepAliveTimeoutRef.current = setTimeout(() => {\n\t\t\tif (typingActiveRef.current) {\n\t\t\t\tvoid sendTyping(true, latestPreviewRef.current);\n\t\t\t\tscheduleKeepAlive();\n\t\t\t}\n\t\t}, KEEP_ALIVE_MS);\n\t}, [clearKeepAlive, sendTyping]);\n\n\tconst scheduleStopTyping = useCallback(() => {\n\t\tclearStopTypingTimeout();\n\t\tstopTypingTimeoutRef.current = setTimeout(() => {\n\t\t\tif (typingActiveRef.current) {\n\t\t\t\ttypingActiveRef.current = false;\n\t\t\t\tclearKeepAlive();\n\t\t\t\tvoid sendTyping(false);\n\t\t\t}\n\t\t}, STOP_TYPING_DELAY_MS);\n\t}, [clearStopTypingTimeout, clearKeepAlive, sendTyping]);\n\n\tconst handleInputChange = useCallback(\n\t\t(value: string) => {\n\t\t\tif (!(client && conversationId)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst trimmed = value.trim();\n\t\t\tlatestPreviewRef.current = trimmed.slice(0, PREVIEW_MAX_LENGTH);\n\t\t\tconst now = typeof window !== \"undefined\" ? Date.now() : 0;\n\n\t\t\tif (trimmed.length === 0) {\n\t\t\t\tif (typingActiveRef.current) {\n\t\t\t\t\ttypingActiveRef.current = false;\n\t\t\t\t\tlastSentAtRef.current = now;\n\t\t\t\t\tclearKeepAlive();\n\t\t\t\t\tclearStopTypingTimeout();\n\t\t\t\t\tvoid sendTyping(false);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Schedule auto-stop after inactivity\n\t\t\tscheduleStopTyping();\n\n\t\t\tif (!typingActiveRef.current) {\n\t\t\t\ttypingActiveRef.current = true;\n\t\t\t\tlastSentAtRef.current = now;\n\t\t\t\tvoid sendTyping(true, latestPreviewRef.current);\n\t\t\t\tscheduleKeepAlive();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (now - lastSentAtRef.current >= SEND_INTERVAL_MS) {\n\t\t\t\tlastSentAtRef.current = now;\n\t\t\t\tvoid sendTyping(true, latestPreviewRef.current);\n\t\t\t\tscheduleKeepAlive();\n\t\t\t}\n\t\t},\n\t\t[\n\t\t\tclient,\n\t\t\tconversationId,\n\t\t\tsendTyping,\n\t\t\tscheduleKeepAlive,\n\t\t\tscheduleStopTyping,\n\t\t\tclearKeepAlive,\n\t\t\tclearStopTypingTimeout,\n\t\t]\n\t);\n\n\tconst handleSubmit = useCallback(() => {\n\t\tif (!typingActiveRef.current) {\n\t\t\treturn;\n\t\t}\n\n\t\ttypingActiveRef.current = false;\n\t\tlastSentAtRef.current = typeof window !== \"undefined\" ? Date.now() : 0;\n\t\tclearKeepAlive();\n\t\tclearStopTypingTimeout();\n\t\tvoid sendTyping(false);\n\t}, [clearKeepAlive, clearStopTypingTimeout, sendTyping]);\n\n\tconst stop = useCallback(() => {\n\t\tif (!typingActiveRef.current) {\n\t\t\treturn;\n\t\t}\n\n\t\ttypingActiveRef.current = false;\n\t\tlastSentAtRef.current = typeof window !== \"undefined\" ? Date.now() : 0;\n\t\tclearKeepAlive();\n\t\tclearStopTypingTimeout();\n\t\tvoid sendTyping(false);\n\t}, [clearKeepAlive, clearStopTypingTimeout, sendTyping]);\n\n\tuseEffect(\n\t\t() => () => {\n\t\t\tif (typingActiveRef.current) {\n\t\t\t\tvoid sendTyping(false);\n\t\t\t}\n\t\t\tclearKeepAlive();\n\t\t\tclearStopTypingTimeout();\n\t\t},\n\t\t[clearKeepAlive, clearStopTypingTimeout, sendTyping]\n\t);\n\n\treturn {\n\t\thandleInputChange,\n\t\thandleSubmit,\n\t\tstop,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -91,7 +91,7 @@ function useConversationPage(options) {
|
|
|
91
91
|
userId: null,
|
|
92
92
|
visitorId: visitor?.id ?? null,
|
|
93
93
|
aiAgentId: null,
|
|
94
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
94
|
+
createdAt: typeof window !== "undefined" ? (/* @__PURE__ */ new Date()).toISOString() : "",
|
|
95
95
|
deletedAt: null
|
|
96
96
|
};
|
|
97
97
|
return [...baseItems, identificationItem];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-conversation-page.js","names":["identificationItem: TimelineItem"],"sources":["../../src/hooks/use-conversation-page.ts"],"sourcesContent":["import type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport {\n\tConversationTimelineType,\n\tTimelineItemVisibility,\n} from \"@cossistant/types/enums\";\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { useSupport } from \"../provider\";\nimport { useDefaultMessages } from \"./private/use-default-messages\";\nimport { useConversationAutoSeen } from \"./use-conversation-auto-seen\";\nimport { useConversationLifecycle } from \"./use-conversation-lifecycle\";\nimport { useConversationTimelineItems } from \"./use-conversation-timeline-items\";\nimport { useMessageComposer } from \"./use-message-composer\";\n\nexport type UseConversationPageOptions = {\n\t/**\n\t * Initial conversation ID (from URL params, navigation state, etc.)\n\t * Can be PENDING_CONVERSATION_ID or a real ID.\n\t */\n\tconversationId: string;\n\n\t/**\n\t * Optional initial message to send when the conversation opens.\n\t */\n\tinitialMessage?: string;\n\n\t/**\n\t * Callback when conversation ID changes (e.g., after creation).\n\t * Use this to update navigation state or URL.\n\t */\n\tonConversationIdChange?: (conversationId: string) => void;\n\n\t/**\n\t * Optional timeline items to pass through (e.g., optimistic updates).\n\t */\n\titems?: TimelineItem[];\n\n\t/**\n\t * Whether automatic \"seen\" tracking should be enabled.\n\t * Set to false when the conversation isn't visible (e.g. widget closed).\n\t * Default: true\n\t */\n\tautoSeenEnabled?: boolean;\n};\n\nexport type UseConversationPageReturn = {\n\t// Conversation state\n\tconversationId: string;\n\tisPending: boolean;\n\titems: TimelineItem[];\n\tisLoading: boolean;\n\terror: Error | null;\n\n\t// Message composer\n\tcomposer: {\n\t\tmessage: string;\n\t\tfiles: File[];\n\t\tisSubmitting: boolean;\n\t\tcanSubmit: boolean;\n\t\tsetMessage: (message: string) => void;\n\t\taddFiles: (files: File[]) => void;\n\t\tremoveFile: (index: number) => void;\n\t\tsubmit: () => void;\n\t};\n\n\t// UI helpers\n\thasItems: boolean;\n\tlastTimelineItem: TimelineItem | null;\n};\n\n/**\n * Main orchestrator hook for the conversation page.\n *\n * This hook combines all conversation-related logic:\n * - Lifecycle management (pending → real conversation)\n * - Message fetching and display\n * - Message composition and sending\n * - Automatic seen tracking\n * - Default/welcome messages before conversation is created\n *\n * It provides a clean, simple API for building conversation UIs.\n *\n * @example\n * ```tsx\n * export function ConversationPage({ conversationId: initialId }) {\n * const conversation = useConversationPage({\n * conversationId: initialId,\n * onConversationIdChange: (newId) => {\n * // Update URL or navigation state\n * navigate(`/conversation/${newId}`);\n * },\n * });\n *\n * return (\n * <>\n * <MessageList messages={conversation.messages} />\n * <MessageInput\n * value={conversation.composer.message}\n * onChange={conversation.composer.setMessage}\n * onSubmit={conversation.composer.submit}\n * />\n * </>\n * );\n * }\n * ```\n */\nexport function useConversationPage(\n\toptions: UseConversationPageOptions\n): UseConversationPageReturn {\n\tconst {\n\t\tconversationId: initialConversationId,\n\t\tinitialMessage,\n\t\tonConversationIdChange,\n\t\titems: passedItems = [],\n\t\tautoSeenEnabled = true,\n\t} = options;\n\n\tconst { client, visitor } = useSupport();\n\n\tconst trimmedInitialMessage = initialMessage?.trim() ?? \"\";\n\tconst hasInitialMessage = trimmedInitialMessage.length > 0;\n\n\t// 1. Manage conversation lifecycle (pending vs real)\n\tconst lifecycle = useConversationLifecycle({\n\t\tinitialConversationId,\n\t\tonConversationCreated: onConversationIdChange,\n\t});\n\n\t// 2. Get default timeline items for pending state\n\tconst defaultTimelineItems = useDefaultMessages({\n\t\tconversationId: lifecycle.conversationId,\n\t});\n\n\tconst effectiveDefaultTimelineItems = hasInitialMessage\n\t\t? []\n\t\t: defaultTimelineItems;\n\n\t// 3. Fetch timeline items from backend if real conversation exists\n\tconst timelineQuery = useConversationTimelineItems(lifecycle.conversationId, {\n\t\tenabled: !lifecycle.isPending,\n\t});\n\n\t// 4. Determine which items to display\n\tconst baseItems = useMemo(() => {\n\t\t// If we have fetched timeline items, use them\n\t\tif (timelineQuery.items.length > 0) {\n\t\t\treturn timelineQuery.items;\n\t\t}\n\n\t\t// If pending, use default timeline items\n\t\tif (lifecycle.isPending && effectiveDefaultTimelineItems.length > 0) {\n\t\t\treturn effectiveDefaultTimelineItems;\n\t\t}\n\n\t\t// Use passed items as fallback\n\t\tif (passedItems.length > 0) {\n\t\t\treturn passedItems;\n\t\t}\n\n\t\treturn [];\n\t}, [\n\t\ttimelineQuery.items,\n\t\tlifecycle.isPending,\n\t\teffectiveDefaultTimelineItems,\n\t\tpassedItems,\n\t]);\n\n\tconst shouldShowIdentificationTool = useMemo(() => {\n\t\tif (lifecycle.isPending) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (visitor?.contact) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn !baseItems.some(\n\t\t\t(item) => item.type === ConversationTimelineType.IDENTIFICATION\n\t\t);\n\t}, [baseItems, lifecycle.isPending, visitor?.contact]);\n\n\tconst displayItems = useMemo(() => {\n\t\tif (!shouldShowIdentificationTool) {\n\t\t\treturn baseItems;\n\t\t}\n\n\t\tconst organizationId =\n\t\t\tbaseItems.at(-1)?.organizationId ??\n\t\t\tclient.getConfiguration().organizationId ??\n\t\t\t\"\";\n\n\t\tconst identificationItem: TimelineItem = {\n\t\t\tid: `identification-${lifecycle.conversationId}`,\n\t\t\tconversationId: lifecycle.conversationId,\n\t\t\torganizationId,\n\t\t\tvisibility: TimelineItemVisibility.PUBLIC,\n\t\t\ttype: ConversationTimelineType.IDENTIFICATION,\n\t\t\ttext: null,\n\t\t\ttool: \"identification\",\n\t\t\tparts: [],\n\t\t\tuserId: null,\n\t\t\tvisitorId: visitor?.id ?? null,\n\t\t\taiAgentId: null,\n\t\t\tcreatedAt: new Date().toISOString(),\n\t\t\tdeletedAt: null,\n\t\t};\n\n\t\treturn [...baseItems, identificationItem];\n\t}, [\n\t\tbaseItems,\n\t\tclient,\n\t\tlifecycle.conversationId,\n\t\tshouldShowIdentificationTool,\n\t\tvisitor?.id,\n\t]);\n\n\tconst lastTimelineItem = useMemo(\n\t\t() => displayItems.at(-1) ?? null,\n\t\t[displayItems]\n\t);\n\n\t// 5. Set up message composer\n\tconst composer = useMessageComposer({\n\t\tclient,\n\t\tconversationId: lifecycle.realConversationId,\n\t\tdefaultTimelineItems: effectiveDefaultTimelineItems,\n\t\tvisitorId: visitor?.id,\n\t\tonMessageSent: (newConversationId) => {\n\t\t\t// Transition from pending to real conversation\n\t\t\tif (lifecycle.isPending) {\n\t\t\t\tlifecycle.setConversationId(newConversationId);\n\t\t\t}\n\t\t},\n\t});\n\n\tconst initialMessageSubmittedRef = useRef(false);\n\tconst lastInitialMessageRef = useRef<string | null>(null);\n\n\tuseEffect(() => {\n\t\tif (!hasInitialMessage) {\n\t\t\tinitialMessageSubmittedRef.current = false;\n\t\t\tlastInitialMessageRef.current = null;\n\t\t\treturn;\n\t\t}\n\n\t\tif (lastInitialMessageRef.current !== trimmedInitialMessage) {\n\t\t\tinitialMessageSubmittedRef.current = false;\n\t\t\tlastInitialMessageRef.current = trimmedInitialMessage;\n\t\t}\n\n\t\tif (!lifecycle.isPending) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (composer.message !== trimmedInitialMessage) {\n\t\t\tcomposer.setMessage(trimmedInitialMessage);\n\t\t\treturn;\n\t\t}\n\n\t\tif (\n\t\t\tinitialMessageSubmittedRef.current ||\n\t\t\tcomposer.isSubmitting ||\n\t\t\t!composer.canSubmit\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tinitialMessageSubmittedRef.current = true;\n\t\tcomposer.submit();\n\t}, [\n\t\thasInitialMessage,\n\t\tlifecycle.isPending,\n\t\tcomposer.message,\n\t\tcomposer.setMessage,\n\t\tcomposer.isSubmitting,\n\t\tcomposer.canSubmit,\n\t\tcomposer.submit,\n\t\ttrimmedInitialMessage,\n\t]);\n\n\t// 6. Auto-mark messages as seen\n\tuseConversationAutoSeen({\n\t\tclient,\n\t\tconversationId: lifecycle.realConversationId,\n\t\tvisitorId: visitor?.id,\n\t\tlastTimelineItem,\n\t\tenabled: autoSeenEnabled,\n\t\tisWidgetOpen: autoSeenEnabled,\n\t});\n\n\treturn {\n\t\tconversationId: lifecycle.conversationId,\n\t\tisPending: lifecycle.isPending,\n\t\titems: displayItems,\n\t\tisLoading: timelineQuery.isLoading,\n\t\terror: timelineQuery.error || composer.error,\n\t\tcomposer: {\n\t\t\tmessage: composer.message,\n\t\t\tfiles: composer.files,\n\t\t\tisSubmitting: composer.isSubmitting,\n\t\t\tcanSubmit: composer.canSubmit,\n\t\t\tsetMessage: composer.setMessage,\n\t\t\taddFiles: composer.addFiles,\n\t\t\tremoveFile: composer.removeFile,\n\t\t\tsubmit: composer.submit,\n\t\t},\n\t\thasItems: displayItems.length > 0,\n\t\tlastTimelineItem,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-conversation-page.js","names":["identificationItem: TimelineItem"],"sources":["../../src/hooks/use-conversation-page.ts"],"sourcesContent":["import type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport {\n\tConversationTimelineType,\n\tTimelineItemVisibility,\n} from \"@cossistant/types/enums\";\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { useSupport } from \"../provider\";\nimport { useDefaultMessages } from \"./private/use-default-messages\";\nimport { useConversationAutoSeen } from \"./use-conversation-auto-seen\";\nimport { useConversationLifecycle } from \"./use-conversation-lifecycle\";\nimport { useConversationTimelineItems } from \"./use-conversation-timeline-items\";\nimport { useMessageComposer } from \"./use-message-composer\";\n\nexport type UseConversationPageOptions = {\n\t/**\n\t * Initial conversation ID (from URL params, navigation state, etc.)\n\t * Can be PENDING_CONVERSATION_ID or a real ID.\n\t */\n\tconversationId: string;\n\n\t/**\n\t * Optional initial message to send when the conversation opens.\n\t */\n\tinitialMessage?: string;\n\n\t/**\n\t * Callback when conversation ID changes (e.g., after creation).\n\t * Use this to update navigation state or URL.\n\t */\n\tonConversationIdChange?: (conversationId: string) => void;\n\n\t/**\n\t * Optional timeline items to pass through (e.g., optimistic updates).\n\t */\n\titems?: TimelineItem[];\n\n\t/**\n\t * Whether automatic \"seen\" tracking should be enabled.\n\t * Set to false when the conversation isn't visible (e.g. widget closed).\n\t * Default: true\n\t */\n\tautoSeenEnabled?: boolean;\n};\n\nexport type UseConversationPageReturn = {\n\t// Conversation state\n\tconversationId: string;\n\tisPending: boolean;\n\titems: TimelineItem[];\n\tisLoading: boolean;\n\terror: Error | null;\n\n\t// Message composer\n\tcomposer: {\n\t\tmessage: string;\n\t\tfiles: File[];\n\t\tisSubmitting: boolean;\n\t\tcanSubmit: boolean;\n\t\tsetMessage: (message: string) => void;\n\t\taddFiles: (files: File[]) => void;\n\t\tremoveFile: (index: number) => void;\n\t\tsubmit: () => void;\n\t};\n\n\t// UI helpers\n\thasItems: boolean;\n\tlastTimelineItem: TimelineItem | null;\n};\n\n/**\n * Main orchestrator hook for the conversation page.\n *\n * This hook combines all conversation-related logic:\n * - Lifecycle management (pending → real conversation)\n * - Message fetching and display\n * - Message composition and sending\n * - Automatic seen tracking\n * - Default/welcome messages before conversation is created\n *\n * It provides a clean, simple API for building conversation UIs.\n *\n * @example\n * ```tsx\n * export function ConversationPage({ conversationId: initialId }) {\n * const conversation = useConversationPage({\n * conversationId: initialId,\n * onConversationIdChange: (newId) => {\n * // Update URL or navigation state\n * navigate(`/conversation/${newId}`);\n * },\n * });\n *\n * return (\n * <>\n * <MessageList messages={conversation.messages} />\n * <MessageInput\n * value={conversation.composer.message}\n * onChange={conversation.composer.setMessage}\n * onSubmit={conversation.composer.submit}\n * />\n * </>\n * );\n * }\n * ```\n */\nexport function useConversationPage(\n\toptions: UseConversationPageOptions\n): UseConversationPageReturn {\n\tconst {\n\t\tconversationId: initialConversationId,\n\t\tinitialMessage,\n\t\tonConversationIdChange,\n\t\titems: passedItems = [],\n\t\tautoSeenEnabled = true,\n\t} = options;\n\n\tconst { client, visitor } = useSupport();\n\n\tconst trimmedInitialMessage = initialMessage?.trim() ?? \"\";\n\tconst hasInitialMessage = trimmedInitialMessage.length > 0;\n\n\t// 1. Manage conversation lifecycle (pending vs real)\n\tconst lifecycle = useConversationLifecycle({\n\t\tinitialConversationId,\n\t\tonConversationCreated: onConversationIdChange,\n\t});\n\n\t// 2. Get default timeline items for pending state\n\tconst defaultTimelineItems = useDefaultMessages({\n\t\tconversationId: lifecycle.conversationId,\n\t});\n\n\tconst effectiveDefaultTimelineItems = hasInitialMessage\n\t\t? []\n\t\t: defaultTimelineItems;\n\n\t// 3. Fetch timeline items from backend if real conversation exists\n\tconst timelineQuery = useConversationTimelineItems(lifecycle.conversationId, {\n\t\tenabled: !lifecycle.isPending,\n\t});\n\n\t// 4. Determine which items to display\n\tconst baseItems = useMemo(() => {\n\t\t// If we have fetched timeline items, use them\n\t\tif (timelineQuery.items.length > 0) {\n\t\t\treturn timelineQuery.items;\n\t\t}\n\n\t\t// If pending, use default timeline items\n\t\tif (lifecycle.isPending && effectiveDefaultTimelineItems.length > 0) {\n\t\t\treturn effectiveDefaultTimelineItems;\n\t\t}\n\n\t\t// Use passed items as fallback\n\t\tif (passedItems.length > 0) {\n\t\t\treturn passedItems;\n\t\t}\n\n\t\treturn [];\n\t}, [\n\t\ttimelineQuery.items,\n\t\tlifecycle.isPending,\n\t\teffectiveDefaultTimelineItems,\n\t\tpassedItems,\n\t]);\n\n\tconst shouldShowIdentificationTool = useMemo(() => {\n\t\tif (lifecycle.isPending) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (visitor?.contact) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn !baseItems.some(\n\t\t\t(item) => item.type === ConversationTimelineType.IDENTIFICATION\n\t\t);\n\t}, [baseItems, lifecycle.isPending, visitor?.contact]);\n\n\tconst displayItems = useMemo(() => {\n\t\tif (!shouldShowIdentificationTool) {\n\t\t\treturn baseItems;\n\t\t}\n\n\t\tconst organizationId =\n\t\t\tbaseItems.at(-1)?.organizationId ??\n\t\t\tclient.getConfiguration().organizationId ??\n\t\t\t\"\";\n\n\t\tconst identificationItem: TimelineItem = {\n\t\t\tid: `identification-${lifecycle.conversationId}`,\n\t\t\tconversationId: lifecycle.conversationId,\n\t\t\torganizationId,\n\t\t\tvisibility: TimelineItemVisibility.PUBLIC,\n\t\t\ttype: ConversationTimelineType.IDENTIFICATION,\n\t\t\ttext: null,\n\t\t\ttool: \"identification\",\n\t\t\tparts: [],\n\t\t\tuserId: null,\n\t\t\tvisitorId: visitor?.id ?? null,\n\t\t\taiAgentId: null,\n\t\t\tcreatedAt: typeof window !== \"undefined\" ? new Date().toISOString() : \"\",\n\t\t\tdeletedAt: null,\n\t\t};\n\n\t\treturn [...baseItems, identificationItem];\n\t}, [\n\t\tbaseItems,\n\t\tclient,\n\t\tlifecycle.conversationId,\n\t\tshouldShowIdentificationTool,\n\t\tvisitor?.id,\n\t]);\n\n\tconst lastTimelineItem = useMemo(\n\t\t() => displayItems.at(-1) ?? null,\n\t\t[displayItems]\n\t);\n\n\t// 5. Set up message composer\n\tconst composer = useMessageComposer({\n\t\tclient,\n\t\tconversationId: lifecycle.realConversationId,\n\t\tdefaultTimelineItems: effectiveDefaultTimelineItems,\n\t\tvisitorId: visitor?.id,\n\t\tonMessageSent: (newConversationId) => {\n\t\t\t// Transition from pending to real conversation\n\t\t\tif (lifecycle.isPending) {\n\t\t\t\tlifecycle.setConversationId(newConversationId);\n\t\t\t}\n\t\t},\n\t});\n\n\tconst initialMessageSubmittedRef = useRef(false);\n\tconst lastInitialMessageRef = useRef<string | null>(null);\n\n\tuseEffect(() => {\n\t\tif (!hasInitialMessage) {\n\t\t\tinitialMessageSubmittedRef.current = false;\n\t\t\tlastInitialMessageRef.current = null;\n\t\t\treturn;\n\t\t}\n\n\t\tif (lastInitialMessageRef.current !== trimmedInitialMessage) {\n\t\t\tinitialMessageSubmittedRef.current = false;\n\t\t\tlastInitialMessageRef.current = trimmedInitialMessage;\n\t\t}\n\n\t\tif (!lifecycle.isPending) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (composer.message !== trimmedInitialMessage) {\n\t\t\tcomposer.setMessage(trimmedInitialMessage);\n\t\t\treturn;\n\t\t}\n\n\t\tif (\n\t\t\tinitialMessageSubmittedRef.current ||\n\t\t\tcomposer.isSubmitting ||\n\t\t\t!composer.canSubmit\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tinitialMessageSubmittedRef.current = true;\n\t\tcomposer.submit();\n\t}, [\n\t\thasInitialMessage,\n\t\tlifecycle.isPending,\n\t\tcomposer.message,\n\t\tcomposer.setMessage,\n\t\tcomposer.isSubmitting,\n\t\tcomposer.canSubmit,\n\t\tcomposer.submit,\n\t\ttrimmedInitialMessage,\n\t]);\n\n\t// 6. Auto-mark messages as seen\n\tuseConversationAutoSeen({\n\t\tclient,\n\t\tconversationId: lifecycle.realConversationId,\n\t\tvisitorId: visitor?.id,\n\t\tlastTimelineItem,\n\t\tenabled: autoSeenEnabled,\n\t\tisWidgetOpen: autoSeenEnabled,\n\t});\n\n\treturn {\n\t\tconversationId: lifecycle.conversationId,\n\t\tisPending: lifecycle.isPending,\n\t\titems: displayItems,\n\t\tisLoading: timelineQuery.isLoading,\n\t\terror: timelineQuery.error || composer.error,\n\t\tcomposer: {\n\t\t\tmessage: composer.message,\n\t\t\tfiles: composer.files,\n\t\t\tisSubmitting: composer.isSubmitting,\n\t\t\tcanSubmit: composer.canSubmit,\n\t\t\tsetMessage: composer.setMessage,\n\t\t\taddFiles: composer.addFiles,\n\t\t\tremoveFile: composer.removeFile,\n\t\t\tsubmit: composer.submit,\n\t\t},\n\t\thasItems: displayItems.length > 0,\n\t\tlastTimelineItem,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -9,7 +9,7 @@ function toError(error) {
|
|
|
9
9
|
return /* @__PURE__ */ new Error("Unknown error");
|
|
10
10
|
}
|
|
11
11
|
function buildTimelineItemPayload(body, conversationId, visitorId, messageId) {
|
|
12
|
-
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
12
|
+
const nowIso = typeof window !== "undefined" ? (/* @__PURE__ */ new Date()).toISOString() : "";
|
|
13
13
|
return {
|
|
14
14
|
id: messageId ?? generateMessageId(),
|
|
15
15
|
conversationId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-send-message.js","names":["initialConversation:\n\t\t\t\t\t| CreateConversationResponseBody[\"conversation\"]\n\t\t\t\t\t| undefined","result: SendMessageResult"],"sources":["../../src/hooks/use-send-message.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport { generateMessageId } from \"@cossistant/core\";\nimport type { CreateConversationResponseBody } from \"@cossistant/types/api/conversation\";\nimport type { TimelineItem } 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};\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\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\nfunction buildTimelineItemPayload(\n\tbody: string,\n\tconversationId: string,\n\tvisitorId: string | null,\n\tmessageId?: string\n): TimelineItem {\n\tconst nowIso = new Date().toISOString();\n\tconst id = messageId ?? generateMessageId();\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: [{ type: \"text\" as const, text: body }],\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 * 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 [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\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} = payload;\n\n\t\t\tif (!message.trim()) {\n\t\t\t\tconst emptyMessageError = new Error(\"Message cannot be empty\");\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\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}\n\n\t\t\t\tconst timelineItemPayload = buildTimelineItemPayload(\n\t\t\t\t\tmessage,\n\t\t\t\t\tconversationId,\n\t\t\t\t\tvisitorId ?? null,\n\t\t\t\t\tprovidedMessageId\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}, []);\n\n\treturn {\n\t\tmutate,\n\t\tmutateAsync,\n\t\tisPending,\n\t\terror,\n\t\treset,\n\t};\n}\n"],"mappings":";;;;;AA4CA,SAAS,QAAQ,OAAuB;AACvC,KAAI,iBAAiB,MACpB,QAAO;AAGR,KAAI,OAAO,UAAU,SACpB,QAAO,IAAI,MAAM,MAAM;AAGxB,wBAAO,IAAI,MAAM,gBAAgB;;AAGlC,SAAS,yBACR,MACA,gBACA,WACA,WACe;CACf,MAAM,
|
|
1
|
+
{"version":3,"file":"use-send-message.js","names":["initialConversation:\n\t\t\t\t\t| CreateConversationResponseBody[\"conversation\"]\n\t\t\t\t\t| undefined","result: SendMessageResult"],"sources":["../../src/hooks/use-send-message.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport { generateMessageId } from \"@cossistant/core\";\nimport type { CreateConversationResponseBody } from \"@cossistant/types/api/conversation\";\nimport type { TimelineItem } 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};\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\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\nfunction buildTimelineItemPayload(\n\tbody: string,\n\tconversationId: string,\n\tvisitorId: string | null,\n\tmessageId?: string\n): TimelineItem {\n\tconst nowIso = typeof window !== \"undefined\" ? new Date().toISOString() : \"\";\n\tconst id = messageId ?? generateMessageId();\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: [{ type: \"text\" as const, text: body }],\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 * 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 [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\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} = payload;\n\n\t\t\tif (!message.trim()) {\n\t\t\t\tconst emptyMessageError = new Error(\"Message cannot be empty\");\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\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}\n\n\t\t\t\tconst timelineItemPayload = buildTimelineItemPayload(\n\t\t\t\t\tmessage,\n\t\t\t\t\tconversationId,\n\t\t\t\t\tvisitorId ?? null,\n\t\t\t\t\tprovidedMessageId\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}, []);\n\n\treturn {\n\t\tmutate,\n\t\tmutateAsync,\n\t\tisPending,\n\t\terror,\n\t\treset,\n\t};\n}\n"],"mappings":";;;;;AA4CA,SAAS,QAAQ,OAAuB;AACvC,KAAI,iBAAiB,MACpB,QAAO;AAGR,KAAI,OAAO,UAAU,SACpB,QAAO,IAAI,MAAM,MAAM;AAGxB,wBAAO,IAAI,MAAM,gBAAgB;;AAGlC,SAAS,yBACR,MACA,gBACA,WACA,WACe;CACf,MAAM,SAAS,OAAO,WAAW,+BAAc,IAAI,MAAM,EAAC,aAAa,GAAG;AAG1E,QAAO;EACN,IAHU,aAAa,mBAAmB;EAI1C;EACA,gBAAgB;EAChB,MAAM;EACN,MAAM;EACN,OAAO,CAAC;GAAE,MAAM;GAAiB,MAAM;GAAM,CAAC;EAC9C,YAAY;EACZ,QAAQ;EACR,WAAW;EACX,WAAW,aAAa;EACxB,WAAW;EACX,WAAW;EACX;;;;;;AAOF,SAAgB,eACf,UAAiC,EAAE,EACZ;CACvB,MAAM,EAAE,QAAQ,kBAAkB,YAAY;CAC9C,MAAM,SAAS,QAAQ,UAAU;CAEjC,MAAM,CAAC,WAAW,gBAAgB,SAAS,MAAM;CACjD,MAAM,CAAC,OAAO,YAAY,SAAuB,KAAK;CAEtD,MAAM,cAAc,YACnB,OAAO,YAAmE;EACzE,MAAM,EACL,gBAAgB,wBAChB,SACA,uBAAuB,EAAE,EACzB,WACA,WAAW,mBACX,WACA,YACG;AAEJ,MAAI,CAAC,QAAQ,MAAM,EAAE;GACpB,MAAM,oCAAoB,IAAI,MAAM,0BAA0B;AAC9D,YAAS,kBAAkB;AAC3B,aAAU,kBAAkB;AAC5B,UAAO;;AAGR,eAAa,KAAK;AAClB,WAAS,KAAK;AAEd,MAAI;GACH,IAAI,iBAAiB,0BAA0B;GAC/C,IAAI,+BAA+B;GACnC,IAAIA;AAIJ,OAAI,CAAC,gBAAgB;IACpB,MAAM,YAAY,OAAO,qBAAqB;KAC7C;KACA,WAAW,aAAa;KACxB,CAAC;AACF,qBAAiB,UAAU;AAC3B,mCAA+B,UAAU;AACzC,0BAAsB,UAAU;;GAGjC,MAAM,sBAAsB,yBAC3B,SACA,gBACA,aAAa,MACb,kBACA;GAED,MAAM,WAAW,MAAM,OAAO,YAAY;IACzC;IACA,MAAM;KACL,IAAI,oBAAoB;KACxB,MAAM,oBAAoB,QAAQ;KAClC,MACC,oBAAoB,SAAS,mBAC1B,YACA,oBAAoB;KACxB,YAAY,oBAAoB;KAChC,QAAQ,oBAAoB;KAC5B,WAAW,oBAAoB;KAC/B,WAAW,oBAAoB;KAC/B,WAAW,oBAAoB;KAC/B,OAAO,oBAAoB;KAC3B;IACD,iBAAiB;IACjB,CAAC;GAEF,MAAM,YAAY,SAAS,KAAK;AAEhC,OAAI,CAAC,UACJ,OAAM,IAAI,MAAM,uCAAuC;GAGxD,MAAMC,SAA4B;IACjC;IACA;IACA;AAED,OAAI,kBAAkB,YAAY,SAAS,cAAc;AACxD,WAAO,eAAe,SAAS;AAC/B,WAAO,uBAAuB,SAAS;cAC7B,qBAAqB;AAC/B,WAAO,eAAe;AACtB,WAAO,uBAAuB;;AAG/B,gBAAa,MAAM;AACnB,YAAS,KAAK;AACd,eAAY,OAAO,gBAAgB,OAAO,UAAU;AACpD,UAAO;WACC,KAAK;GACb,MAAM,aAAa,QAAQ,IAAI;AAC/B,gBAAa,MAAM;AACnB,YAAS,WAAW;AACpB,aAAU,WAAW;AACrB,SAAM;;IAGR,CAAC,OAAO,CACR;AAgBD,QAAO;EACN,QAfc,aACb,SAA6B;AAC7B,GAAK,YAAY,KAAK,CAAC,YAAY,GAEjC;KAEH,CAAC,YAAY,CACb;EASA;EACA;EACA;EACA,OAVa,kBAAkB;AAC/B,YAAS,KAAK;AACd,gBAAa,MAAM;KACjB,EAAE,CAAC;EAQL"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cossistant/react",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.20",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Cossistant team",
|
|
7
7
|
"description": "Headless React SDK for building AI-powered support/chat widgets. Hooks + primitives, WS-driven, TypeScript-first. Next.js-ready, Tailwind optional.",
|
|
@@ -88,8 +88,8 @@
|
|
|
88
88
|
"*.css"
|
|
89
89
|
],
|
|
90
90
|
"dependencies": {
|
|
91
|
-
"@cossistant/core": "0.0.
|
|
92
|
-
"@cossistant/types": "0.0.
|
|
91
|
+
"@cossistant/core": "0.0.20",
|
|
92
|
+
"@cossistant/types": "0.0.20",
|
|
93
93
|
"class-variance-authority": "^0.7.1",
|
|
94
94
|
"clsx": "^2.1.1",
|
|
95
95
|
"nanoid": "^5.1.5",
|
|
@@ -16,7 +16,7 @@ type MultimodalInputProps = Omit<React$1.TextareaHTMLAttributes<HTMLTextAreaElem
|
|
|
16
16
|
* clipboard uploads and auto-resizing while remaining composable via
|
|
17
17
|
* `asChild`.
|
|
18
18
|
*/
|
|
19
|
-
declare const MultimodalInput: React$1.ForwardRefExoticComponent<Omit<React$1.TextareaHTMLAttributes<HTMLTextAreaElement>, "
|
|
19
|
+
declare const MultimodalInput: React$1.ForwardRefExoticComponent<Omit<React$1.TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange" | "value"> & {
|
|
20
20
|
value: string;
|
|
21
21
|
onChange: (value: string) => void;
|
|
22
22
|
onSubmit?: () => void;
|
|
@@ -38,7 +38,7 @@ declare const FileInput: React$1.ForwardRefExoticComponent<React$1.InputHTMLAttr
|
|
|
38
38
|
onFileSelect?: (files: File[]) => void;
|
|
39
39
|
asChild?: boolean;
|
|
40
40
|
} & React$1.RefAttributes<HTMLInputElement>>;
|
|
41
|
-
declare const SupportInput: React$1.ForwardRefExoticComponent<Omit<React$1.TextareaHTMLAttributes<HTMLTextAreaElement>, "
|
|
41
|
+
declare const SupportInput: React$1.ForwardRefExoticComponent<Omit<React$1.TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange" | "value"> & {
|
|
42
42
|
value: string;
|
|
43
43
|
onChange: (value: string) => void;
|
|
44
44
|
onSubmit?: () => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multimodal-input.d.ts","names":[],"sources":["../../src/primitives/multimodal-input.tsx"],"sourcesContent":[],"mappings":";;;KAGY,oBAAA,GAAuB,KAClC,OAAA,CAAM,uBAAuB;;EADlB,QAAA,EAAA,CAAA,KAAA,EAAA,MAAoB,EAAA,GAAA,IAAA;EACF,QAAA,CAAA,EAAA,GAAA,GAAA,IAAA;EAA7B,YAAM,CAAA,EAAA,CAAA,KAAA,EAMiB,IANjB,EAAA,EAAA,GAAA,IAAA;EAD4B,OAAA,CAAA,EAAA,OAAA;EAOX,SAAA,CAAA,EAAA,MAAA;EAGf,KAAA,CAAA,EAAA,KAAA,GAAA,IAAA;EAAK,QAAA,CAAA,EAAA,OAAA;AASd,CAAA;;;;;;cAAa,iBAAe,OAAA,CAAA,0BAAA,KAAA,OAAA,CAAA,uBAAA;;EAAA,QAAA,EAAA,CAAA,KAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EAAA,QAAA,CAAA,EAAA,GAAA,GAAA,IAAA;EA4GhB,YAAA,CAAA,EAAA,CAAA,KAAc,EAxHF,IAwHE,EAAA,EAAA,GAAA,IAAA;EAA6B,OAAA,CAAA,EAAA,OAAA;EAA1B,SAAM,CAAA,EAAA,MAAA;EACX,KAAA,CAAA,EAtHf,KAsHe,GAAA,IAAA;EAAI,QAAA,CAAA,EAAA,OAAA;AAQ5B,CAAA,wBAkCI,oBAAA,CAAA,CAAA;AAlCkB,KATV,cAAA,GAAiB,OAAA,CAAM,mBASb,CATiC,gBASjC,CAAA,GAAA;EAAA,YAAA,CAAA,EAAA,CAAA,KAAA,EARE,IAQF,EAAA,EAAA,GAAA,IAAA;EARE,OAAA,CAAA,EAAA,OAAA;;;;;AA6CxB;AAAyB,cArCZ,SAqCY,EArCH,OAAA,CAAA,yBAqCG,CArCH,OAAA,CAAA,mBAqCG,CArCH,gBAqCG,CAAA,GAAA;EAAA,YAAA,CAAA,EAAA,CAAA,KAAA,EA7CD,IA6CC,EAAA,EAAA,GAAA,IAAA;EAAA,OAAA,CAAA,EAAA,OAAA;CAtKD,wBAAA,iBAAA,CAAA,CAAA;AAGf,cAmKI,YAnKJ,EAmKgB,OAAA,CAAA,yBAnKhB,CAmKgB,IAnKhB,CAmKgB,OAAA,CAAA,sBAnKhB,CAmKgB,mBAnKhB,CAAA,EAAA,
|
|
1
|
+
{"version":3,"file":"multimodal-input.d.ts","names":[],"sources":["../../src/primitives/multimodal-input.tsx"],"sourcesContent":[],"mappings":";;;KAGY,oBAAA,GAAuB,KAClC,OAAA,CAAM,uBAAuB;;EADlB,QAAA,EAAA,CAAA,KAAA,EAAA,MAAoB,EAAA,GAAA,IAAA;EACF,QAAA,CAAA,EAAA,GAAA,GAAA,IAAA;EAA7B,YAAM,CAAA,EAAA,CAAA,KAAA,EAMiB,IANjB,EAAA,EAAA,GAAA,IAAA;EAD4B,OAAA,CAAA,EAAA,OAAA;EAOX,SAAA,CAAA,EAAA,MAAA;EAGf,KAAA,CAAA,EAAA,KAAA,GAAA,IAAA;EAAK,QAAA,CAAA,EAAA,OAAA;AASd,CAAA;;;;;;cAAa,iBAAe,OAAA,CAAA,0BAAA,KAAA,OAAA,CAAA,uBAAA;;EAAA,QAAA,EAAA,CAAA,KAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EAAA,QAAA,CAAA,EAAA,GAAA,GAAA,IAAA;EA4GhB,YAAA,CAAA,EAAA,CAAA,KAAc,EAxHF,IAwHE,EAAA,EAAA,GAAA,IAAA;EAA6B,OAAA,CAAA,EAAA,OAAA;EAA1B,SAAM,CAAA,EAAA,MAAA;EACX,KAAA,CAAA,EAtHf,KAsHe,GAAA,IAAA;EAAI,QAAA,CAAA,EAAA,OAAA;AAQ5B,CAAA,wBAkCI,oBAAA,CAAA,CAAA;AAlCkB,KATV,cAAA,GAAiB,OAAA,CAAM,mBASb,CATiC,gBASjC,CAAA,GAAA;EAAA,YAAA,CAAA,EAAA,CAAA,KAAA,EARE,IAQF,EAAA,EAAA,GAAA,IAAA;EARE,OAAA,CAAA,EAAA,OAAA;;;;;AA6CxB;AAAyB,cArCZ,SAqCY,EArCH,OAAA,CAAA,yBAqCG,CArCH,OAAA,CAAA,mBAqCG,CArCH,gBAqCG,CAAA,GAAA;EAAA,YAAA,CAAA,EAAA,CAAA,KAAA,EA7CD,IA6CC,EAAA,EAAA,GAAA,IAAA;EAAA,OAAA,CAAA,EAAA,OAAA;CAtKD,wBAAA,iBAAA,CAAA,CAAA;AAGf,cAmKI,YAnKJ,EAmKgB,OAAA,CAAA,yBAnKhB,CAmKgB,IAnKhB,CAmKgB,OAAA,CAAA,sBAnKhB,CAmKgB,mBAnKhB,CAAA,EAAA,UAAA,GAAA,OAAA,CAAA,GAAA;;;EAmKgB,QAAA,CAAA,EAAA,GAAA,GAAA,IAAA;EAAA,YAAA,CAAA,EAAA,CAAA,KAAA,EAtKD,IAsKC,EAAA,EAAA,GAAA,IAAA;;;UAnKhB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page-registry.d.ts","names":[],"sources":["../../src/primitives/page-registry.tsx"],"sourcesContent":[],"mappings":";;;KAEK,6BAA6B,KAAA,CAAM;WAAyB;AAFC,CAAA,CAAA;AAEb,KAEhD,YAAA,GAAe,GAAH,CAAA,MAAA,EAAe,aAAA,
|
|
1
|
+
{"version":3,"file":"page-registry.d.ts","names":[],"sources":["../../src/primitives/page-registry.tsx"],"sourcesContent":[],"mappings":";;;KAEK,6BAA6B,KAAA,CAAM;WAAyB;AAFC,CAAA,CAAA;AAEb,KAEhD,YAAA,GAAe,GAAH,CAAA,MAAA,EAAe,aAAA,CAAZ;AAapB;AAeA;AAKA;;;;;;;cApBa,sBAAsB,KAAA,CAAM;YAAe,KAAA,CAAM;;;;;cAejD,uBAAe;;;;cAKf,2CAA4C"}
|
package/realtime/provider.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ type RealtimeContextValue = RealtimeConnectionState & {
|
|
|
43
43
|
};
|
|
44
44
|
/**
|
|
45
45
|
* Provides websocket connectivity and heartbeating logic for realtime events.
|
|
46
|
+
* Handles SSR by only initializing the WebSocket connection in the browser.
|
|
46
47
|
*/
|
|
47
48
|
declare function RealtimeProvider({
|
|
48
49
|
children,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","names":[],"sources":["../../src/realtime/provider.tsx"],"sourcesContent":[],"mappings":";;;;KAuBK,gBAAA,WAA2B;KA+B3B,iBAAA;EA/BA,IAAA,EAAA,SAAA;EA+BA,SAAA,EAAA,MAAA,GAAiB,IAAA;EAOjB,SAAA,CAAA,EAAA,MAAA,GAAiB,IAAA;EAOjB,SAAA,CAAA,EAAA,MAAA,GAAkB,IAAA;AAAwC,CAAA;KAP1D,iBAAA,GAmBY;EAEV,IAAA,EAAA,SAAA;EAIY,YAAA,EAAA,MAAA,GAAA,IAAA;EAAK,SAAA,CAAA,EAAA,MAAA,GAAA,IAAA;EAGnB,MAAA,CAAA,EAAA,MAAA,GAAA,IAAA;CAGG;KAxBH,kBAAA,GAAqB,iBAyBX,GAzB+B,iBAyB/B;KAdV,qBAAA,GAgBiB;EACV,QAAA,EAhBD,KAAA,CAAM,SAgBL;EAAgB,KAAA,CAAA,EAAA,MAAA;EAKvB,IAAA,EAnBE,kBAmBkB,GAAA,IAAA;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","names":[],"sources":["../../src/realtime/provider.tsx"],"sourcesContent":[],"mappings":";;;;KAuBK,gBAAA,WAA2B;KA+B3B,iBAAA;EA/BA,IAAA,EAAA,SAAA;EA+BA,SAAA,EAAA,MAAA,GAAiB,IAAA;EAOjB,SAAA,CAAA,EAAA,MAAA,GAAiB,IAAA;EAOjB,SAAA,CAAA,EAAA,MAAA,GAAkB,IAAA;AAAwC,CAAA;KAP1D,iBAAA,GAmBY;EAEV,IAAA,EAAA,SAAA;EAIY,YAAA,EAAA,MAAA,GAAA,IAAA;EAAK,SAAA,CAAA,EAAA,MAAA,GAAA,IAAA;EAGnB,MAAA,CAAA,EAAA,MAAA,GAAA,IAAA;CAGG;KAxBH,kBAAA,GAAqB,iBAyBX,GAzB+B,iBAyB/B;KAdV,qBAAA,GAgBiB;EACV,QAAA,EAhBD,KAAA,CAAM,SAgBL;EAAgB,KAAA,CAAA,EAAA,MAAA;EAKvB,IAAA,EAnBE,kBAmBkB,GAAA,IAAA;EAilBT,WAAA,CAAA,EAAA,OAAgB;EAC/B,SAAA,CAAA,EAAA,GAAA,GAAA,IAAA;EACA,YAAA,CAAA,EAAA,GAAA,GAAA,IAAA;EACA,OAAA,CAAA,EAAA,CAAA,KAAA,EAnmBkB,KAmmBlB,EAAA,GAAA,IAAA;CACA;KAjmBI,uBAAA,GAkmBJ;EACA,WAAA,EAAA,OAAA;EACA,YAAA,EAAA,OAAA;EACE,KAAA,EAlmBK,KAkmBL,GAAA,IAAA;EAAwB,IAAM,EAAA,CAAA,KAAA,EAjmBlB,gBAimBkB,EAAA,GAAA,IAAA;EAAY,OAAA,EAAA,CAAA,IAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EA+D7B,SAAA,EAAA,CAAA,OAAA,EA9pBM,gBA8pBmB,EAAA,GAAA,GAAA,GAAA,IAAA;aA7pB7B;;;;KAKP,oBAAA,GAAuB;;;;;;;;;iBAilBZ,gBAAA;;;;;;;;GAQb,wBAAwB,KAAA,CAAM;;;;iBA+DjB,qBAAA,CAAA,GAAyB"}
|
package/realtime/provider.js
CHANGED
|
@@ -127,14 +127,16 @@ function constructRealtimeEvent(parsed) {
|
|
|
127
127
|
}
|
|
128
128
|
/**
|
|
129
129
|
* Checks if heartbeat has timed out.
|
|
130
|
+
* Only call this function in browser context (inside effects or event handlers).
|
|
130
131
|
*/
|
|
131
132
|
function isHeartbeatTimedOut(lastHeartbeat, timeoutMs) {
|
|
133
|
+
if (typeof window === "undefined") return false;
|
|
132
134
|
return Date.now() - lastHeartbeat > timeoutMs;
|
|
133
135
|
}
|
|
134
136
|
function resolvePublicKey(explicit) {
|
|
135
137
|
const trimmed = explicit?.trim();
|
|
136
138
|
if (trimmed) return trimmed;
|
|
137
|
-
const normalized = (process.env.NEXT_PUBLIC_COSSISTANT_KEY || process.env.
|
|
139
|
+
const normalized = (process.env.NEXT_PUBLIC_COSSISTANT_API_KEY || process.env.NEXT_PUBLIC_COSSISTANT_KEY || process.env.COSSISTANT_API_KEY || null)?.trim();
|
|
138
140
|
return normalized && normalized.length > 0 ? normalized : null;
|
|
139
141
|
}
|
|
140
142
|
function normalizeAuth(auth) {
|
|
@@ -181,13 +183,14 @@ function buildSocketUrl(baseUrl, auth) {
|
|
|
181
183
|
}
|
|
182
184
|
}
|
|
183
185
|
/**
|
|
184
|
-
*
|
|
186
|
+
* Internal component that handles the WebSocket connection.
|
|
187
|
+
* Only rendered in the browser to avoid SSR issues with react-use-websocket.
|
|
185
188
|
*/
|
|
186
|
-
function
|
|
189
|
+
function RealtimeProviderInternal({ children, wsUrl = DEFAULT_WS_URL, auth, autoConnect, onConnect, onDisconnect, onError }) {
|
|
187
190
|
const normalizedAuth = normalizeAuth(auth);
|
|
188
191
|
const socketUrl = buildSocketUrl(wsUrl, normalizedAuth);
|
|
189
192
|
const eventHandlersRef = useRef(/* @__PURE__ */ new Set());
|
|
190
|
-
const lastHeartbeatRef = useRef(
|
|
193
|
+
const lastHeartbeatRef = useRef(0);
|
|
191
194
|
const hasOpenedRef = useRef(false);
|
|
192
195
|
const previousUrlRef = useRef(null);
|
|
193
196
|
const [connectionError, setConnectionError] = useState(null);
|
|
@@ -223,7 +226,7 @@ function RealtimeProvider({ children, wsUrl = DEFAULT_WS_URL, auth, autoConnect
|
|
|
223
226
|
onOpen: () => {
|
|
224
227
|
hasOpenedRef.current = true;
|
|
225
228
|
setConnectionError(null);
|
|
226
|
-
lastHeartbeatRef.current = Date.now();
|
|
229
|
+
lastHeartbeatRef.current = typeof window !== "undefined" ? Date.now() : 0;
|
|
227
230
|
onConnect?.();
|
|
228
231
|
},
|
|
229
232
|
onClose: () => {
|
|
@@ -250,11 +253,11 @@ function RealtimeProvider({ children, wsUrl = DEFAULT_WS_URL, auth, autoConnect
|
|
|
250
253
|
const message = parseWebSocketMessage(decoded.data);
|
|
251
254
|
switch (message.type) {
|
|
252
255
|
case "pong":
|
|
253
|
-
lastHeartbeatRef.current = Date.now();
|
|
256
|
+
lastHeartbeatRef.current = typeof window !== "undefined" ? Date.now() : 0;
|
|
254
257
|
break;
|
|
255
258
|
case "connection-established":
|
|
256
259
|
setConnectionId(message.connectionId);
|
|
257
|
-
lastHeartbeatRef.current = Date.now();
|
|
260
|
+
lastHeartbeatRef.current = typeof window !== "undefined" ? Date.now() : 0;
|
|
258
261
|
break;
|
|
259
262
|
case "error": {
|
|
260
263
|
const err = new Error(message.message);
|
|
@@ -263,7 +266,7 @@ function RealtimeProvider({ children, wsUrl = DEFAULT_WS_URL, auth, autoConnect
|
|
|
263
266
|
break;
|
|
264
267
|
}
|
|
265
268
|
case "event":
|
|
266
|
-
lastHeartbeatRef.current = Date.now();
|
|
269
|
+
lastHeartbeatRef.current = typeof window !== "undefined" ? Date.now() : 0;
|
|
267
270
|
setLastEvent(message.event);
|
|
268
271
|
for (const handler of eventHandlersRef.current) Promise.resolve(handler(message.event)).catch((error) => {
|
|
269
272
|
const err = error instanceof Error ? error : /* @__PURE__ */ new Error(`Subscriber threw an exception: ${String(error)}`);
|
|
@@ -277,7 +280,7 @@ function RealtimeProvider({ children, wsUrl = DEFAULT_WS_URL, auth, autoConnect
|
|
|
277
280
|
if (!canConnect) return;
|
|
278
281
|
const interval = window.setInterval(() => {
|
|
279
282
|
if (readyState !== ReadyState.OPEN) return;
|
|
280
|
-
if (isHeartbeatTimedOut(lastHeartbeatRef.current, heartbeatTimeoutMs)) {
|
|
283
|
+
if (lastHeartbeatRef.current !== 0 && isHeartbeatTimedOut(lastHeartbeatRef.current, heartbeatTimeoutMs)) {
|
|
281
284
|
getWebSocket()?.close(4e3, "Heartbeat timeout");
|
|
282
285
|
return;
|
|
283
286
|
}
|
|
@@ -360,6 +363,52 @@ function RealtimeProvider({ children, wsUrl = DEFAULT_WS_URL, auth, autoConnect
|
|
|
360
363
|
});
|
|
361
364
|
}
|
|
362
365
|
/**
|
|
366
|
+
* Provides websocket connectivity and heartbeating logic for realtime events.
|
|
367
|
+
* Handles SSR by only initializing the WebSocket connection in the browser.
|
|
368
|
+
*/
|
|
369
|
+
function RealtimeProvider({ children, wsUrl = DEFAULT_WS_URL, auth, autoConnect = true, onConnect, onDisconnect, onError }) {
|
|
370
|
+
const [isBrowser, setIsBrowser] = useState(false);
|
|
371
|
+
useEffect(() => {
|
|
372
|
+
setIsBrowser(true);
|
|
373
|
+
}, []);
|
|
374
|
+
const normalizedAuth = normalizeAuth(auth);
|
|
375
|
+
const defaultValue = useMemo(() => ({
|
|
376
|
+
isConnected: false,
|
|
377
|
+
isConnecting: false,
|
|
378
|
+
error: null,
|
|
379
|
+
send: () => {
|
|
380
|
+
throw new Error("Realtime connection is not available during SSR");
|
|
381
|
+
},
|
|
382
|
+
sendRaw: () => {
|
|
383
|
+
throw new Error("Realtime connection is not available during SSR");
|
|
384
|
+
},
|
|
385
|
+
subscribe: () => () => {},
|
|
386
|
+
lastEvent: null,
|
|
387
|
+
connectionId: null,
|
|
388
|
+
reconnect: () => {},
|
|
389
|
+
visitorId: normalizedAuth?.visitorId ?? null,
|
|
390
|
+
websiteId: normalizedAuth?.websiteId ?? null,
|
|
391
|
+
userId: normalizedAuth?.userId ?? null
|
|
392
|
+
}), [
|
|
393
|
+
normalizedAuth?.visitorId,
|
|
394
|
+
normalizedAuth?.websiteId,
|
|
395
|
+
normalizedAuth?.userId
|
|
396
|
+
]);
|
|
397
|
+
if (!isBrowser) return /* @__PURE__ */ jsx(RealtimeContext.Provider, {
|
|
398
|
+
value: defaultValue,
|
|
399
|
+
children
|
|
400
|
+
});
|
|
401
|
+
return /* @__PURE__ */ jsx(RealtimeProviderInternal, {
|
|
402
|
+
auth,
|
|
403
|
+
autoConnect,
|
|
404
|
+
onConnect,
|
|
405
|
+
onDisconnect,
|
|
406
|
+
onError,
|
|
407
|
+
wsUrl,
|
|
408
|
+
children
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
363
412
|
* Returns the realtime connection context.
|
|
364
413
|
*/
|
|
365
414
|
function useRealtimeConnection() {
|
package/realtime/provider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","names":["payload: unknown"],"sources":["../../src/realtime/provider.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n\ttype AnyRealtimeEvent,\n\tisValidEventType,\n\ttype RealtimeEvent,\n\tvalidateRealtimeEvent,\n} from \"@cossistant/types/realtime-events\";\nimport type React from \"react\";\nimport {\n\tcreateContext,\n\tuseCallback,\n\tuseContext,\n\tuseEffect,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from \"react\";\nimport useWebSocket, { ReadyState } from \"react-use-websocket\";\n\nconst DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000;\nconst DEFAULT_HEARTBEAT_TIMEOUT_MS = 45_000;\n\ntype SubscribeHandler = (event: AnyRealtimeEvent) => void;\n\ntype MessageDecodeResult =\n\t| {\n\t\t\ttype: \"raw-text\";\n\t\t\tdata: string;\n\t }\n\t| {\n\t\t\ttype: \"unsupported\";\n\t };\n\ntype ParsedMessage =\n\t| {\n\t\t\ttype: \"pong\";\n\t }\n\t| {\n\t\t\ttype: \"connection-established\";\n\t\t\tconnectionId: string | null;\n\t }\n\t| {\n\t\t\ttype: \"error\";\n\t\t\tmessage: string;\n\t }\n\t| {\n\t\t\ttype: \"event\";\n\t\t\tevent: AnyRealtimeEvent;\n\t }\n\t| {\n\t\t\ttype: \"invalid\";\n\t };\n\ntype VisitorAuthConfig = {\n\tkind: \"visitor\";\n\tvisitorId: string | null;\n\twebsiteId?: string | null;\n\tpublicKey?: string | null;\n};\n\ntype SessionAuthConfig = {\n\tkind: \"session\";\n\tsessionToken: string | null;\n\twebsiteId?: string | null;\n\tuserId?: string | null;\n};\n\ntype RealtimeAuthConfig = VisitorAuthConfig | SessionAuthConfig;\n\ntype ResolvedAuthConfig = {\n\ttype: \"visitor\" | \"session\";\n\tvisitorId: string | null;\n\twebsiteId: string | null;\n\tuserId: string | null;\n\tsessionToken: string | null;\n\tpublicKey: string | null;\n};\n\ntype RealtimeProviderProps = {\n\tchildren: React.ReactNode;\n\twsUrl?: string;\n\tauth: RealtimeAuthConfig | null;\n\tautoConnect?: boolean;\n\tonConnect?: () => void;\n\tonDisconnect?: () => void;\n\tonError?: (error: Error) => void;\n};\n\ntype RealtimeConnectionState = {\n\tisConnected: boolean;\n\tisConnecting: boolean;\n\terror: Error | null;\n\tsend: (event: AnyRealtimeEvent) => void;\n\tsendRaw: (data: string) => void;\n\tsubscribe: (handler: SubscribeHandler) => () => void;\n\tlastEvent: AnyRealtimeEvent | null;\n\tconnectionId: string | null;\n\treconnect: () => void;\n};\n\ntype RealtimeContextValue = RealtimeConnectionState & {\n\tvisitorId: string | null;\n\twebsiteId: string | null;\n\tuserId: string | null;\n};\n\nconst DEFAULT_WS_URL = \"wss://api.cossistant.com/ws\";\n\nconst RealtimeContext = createContext<RealtimeContextValue | null>(null);\n\n/**\n * Decodes WebSocket message data into a string.\n * Handles string, ArrayBuffer, and ArrayBufferView formats.\n */\nfunction decodeMessageData(data: unknown): MessageDecodeResult {\n\tif (typeof data === \"string\") {\n\t\treturn { type: \"raw-text\", data };\n\t}\n\n\tif (data instanceof ArrayBuffer) {\n\t\ttry {\n\t\t\treturn { type: \"raw-text\", data: new TextDecoder().decode(data) };\n\t\t} catch {\n\t\t\treturn { type: \"unsupported\" };\n\t\t}\n\t}\n\n\tif (ArrayBuffer.isView(data)) {\n\t\ttry {\n\t\t\treturn { type: \"raw-text\", data: new TextDecoder().decode(data.buffer) };\n\t\t} catch {\n\t\t\treturn { type: \"unsupported\" };\n\t\t}\n\t}\n\n\treturn { type: \"unsupported\" };\n}\n\n/**\n * Safely parses JSON string, returning null if invalid.\n */\nfunction parseJson(raw: string): unknown {\n\ttry {\n\t\treturn JSON.parse(raw);\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Extracts a string field from an unknown object, with optional validation.\n */\nfunction extractStringField(\n\tobj: unknown,\n\tfield: string,\n\trequired = false\n): string | null {\n\tif (!obj || typeof obj !== \"object\" || !(field in obj)) {\n\t\treturn required ? null : null;\n\t}\n\tconst value = (obj as Record<string, unknown>)[field];\n\tif (typeof value === \"string\" && value.length > 0) {\n\t\treturn value;\n\t}\n\treturn required ? null : null;\n}\n\n/**\n * Parses a WebSocket message and determines its type and content.\n */\nfunction parseWebSocketMessage(rawText: string): ParsedMessage {\n\t// Handle pong heartbeat\n\tif (rawText === \"pong\") {\n\t\treturn { type: \"pong\" };\n\t}\n\n\t// Try to parse as JSON\n\tconst parsed = parseJson(rawText);\n\tif (!parsed || typeof parsed !== \"object\") {\n\t\treturn { type: \"invalid\" };\n\t}\n\n\tconst messageType = extractStringField(parsed, \"type\");\n\n\t// Handle CONNECTION_ESTABLISHED\n\tif (messageType === \"CONNECTION_ESTABLISHED\") {\n\t\tconst payload = (parsed as { payload?: unknown }).payload;\n\t\tconst connectionId = extractStringField(payload, \"connectionId\");\n\t\treturn { type: \"connection-established\", connectionId };\n\t}\n\n\t// Handle error messages\n\tif (\"error\" in parsed && \"message\" in parsed) {\n\t\tconst message =\n\t\t\textractStringField(parsed, \"message\") || \"Realtime connection error\";\n\t\treturn { type: \"error\", message };\n\t}\n\n\t// Handle realtime events\n\tif (messageType && isValidEventType(messageType)) {\n\t\ttry {\n\t\t\tconst event = constructRealtimeEvent(parsed);\n\t\t\tif (!event) {\n\t\t\t\treturn { type: \"invalid\" };\n\t\t\t}\n\t\t\treturn { type: \"event\", event };\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[Realtime] Failed to construct event\", error);\n\t\t\treturn { type: \"invalid\" };\n\t\t}\n\t}\n\n\treturn { type: \"invalid\" };\n}\n\n/**\n * Constructs a RealtimeEvent from parsed JSON data.\n * Returns null if required fields are missing or validation fails.\n */\nfunction constructRealtimeEvent(parsed: unknown): AnyRealtimeEvent | null {\n\tif (!parsed || typeof parsed !== \"object\" || !(\"type\" in parsed)) {\n\t\treturn null;\n\t}\n\n\tconst type = (parsed as { type: unknown }).type;\n\tif (!isValidEventType(type)) {\n\t\treturn null;\n\t}\n\n\tconst eventType = type;\n\n\t// Extract payload directly\n\tconst payloadSource = (parsed as { payload?: unknown }).payload;\n\n\tlet payload: unknown;\n\ttry {\n\t\tpayload = validateRealtimeEvent(eventType, payloadSource);\n\t} catch (error) {\n\t\tconsole.error(\"[Realtime] Received invalid event payload\", error);\n\t\treturn null;\n\t}\n\n\tconst organizationId = extractStringField(\n\t\tpayloadSource,\n\t\t\"organizationId\",\n\t\ttrue\n\t);\n\tconst websiteId = extractStringField(payloadSource, \"websiteId\", true);\n\n\tif (!organizationId) {\n\t\tconsole.error(\"[Realtime] Received event without organizationId\", parsed);\n\t\treturn null;\n\t}\n\n\tif (!websiteId) {\n\t\tconsole.error(\"[Realtime] Received event without websiteId\", parsed);\n\t\treturn null;\n\t}\n\n\tconst visitorId = extractStringField(parsed, \"visitorId\");\n\n\treturn {\n\t\ttype: eventType,\n\t\tpayload,\n\t\torganizationId,\n\t\twebsiteId,\n\t\tvisitorId,\n\t} as AnyRealtimeEvent;\n}\n\n/**\n * Checks if heartbeat has timed out.\n */\nfunction isHeartbeatTimedOut(\n\tlastHeartbeat: number,\n\ttimeoutMs: number\n): boolean {\n\tconst elapsed = Date.now() - lastHeartbeat;\n\treturn elapsed > timeoutMs;\n}\n\nfunction resolvePublicKey(explicit?: string | null): string | null {\n\tconst trimmed = explicit?.trim();\n\tif (trimmed) {\n\t\treturn trimmed;\n\t}\n\n\tconst fromEnv =\n\t\tprocess.env.NEXT_PUBLIC_COSSISTANT_KEY ||\n\t\tprocess.env.COSSISTANT_PUBLIC_KEY ||\n\t\tnull;\n\n\tconst normalized = fromEnv?.trim();\n\treturn normalized && normalized.length > 0 ? normalized : null;\n}\n\nfunction normalizeAuth(\n\tauth: RealtimeAuthConfig | null\n): ResolvedAuthConfig | null {\n\tif (!auth) {\n\t\treturn null;\n\t}\n\n\tif (auth.kind === \"visitor\") {\n\t\tconst visitorId = auth.visitorId?.trim() || null;\n\n\t\tif (!visitorId) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn {\n\t\t\ttype: \"visitor\",\n\t\t\tvisitorId,\n\t\t\twebsiteId: auth.websiteId?.trim() || null,\n\t\t\tuserId: null,\n\t\t\tsessionToken: null,\n\t\t\tpublicKey: resolvePublicKey(auth.publicKey ?? null),\n\t\t} satisfies ResolvedAuthConfig;\n\t}\n\n\tconst sessionToken = auth.sessionToken?.trim() || null;\n\n\tif (!sessionToken) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\ttype: \"session\",\n\t\tvisitorId: null,\n\t\twebsiteId: auth.websiteId?.trim() || null,\n\t\tuserId: auth.userId?.trim() || null,\n\t\tsessionToken,\n\t\tpublicKey: null,\n\t} satisfies ResolvedAuthConfig;\n}\n\nfunction buildSocketUrl(\n\tbaseUrl: string,\n\tauth: ResolvedAuthConfig | null\n): string | null {\n\tif (!auth) {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst url = new URL(baseUrl);\n\n\t\tif (auth.type === \"visitor\") {\n\t\t\turl.searchParams.set(\"visitorId\", auth.visitorId ?? \"\");\n\t\t\tconst publicKey = auth.publicKey;\n\t\t\tif (publicKey) {\n\t\t\t\turl.searchParams.set(\"publicKey\", publicKey);\n\t\t\t}\n\t\t} else {\n\t\t\turl.searchParams.set(\"sessionToken\", auth.sessionToken ?? \"\");\n\t\t\tif (auth.websiteId) {\n\t\t\t\turl.searchParams.set(\"websiteId\", auth.websiteId);\n\t\t\t}\n\t\t}\n\n\t\treturn url.toString();\n\t} catch (error) {\n\t\tconsole.error(\"[Realtime] Failed to build WebSocket URL\", error);\n\t\treturn null;\n\t}\n}\n\n/**\n * Provides websocket connectivity and heartbeating logic for realtime events.\n */\nexport function RealtimeProvider({\n\tchildren,\n\twsUrl = DEFAULT_WS_URL,\n\tauth,\n\tautoConnect = true,\n\tonConnect,\n\tonDisconnect,\n\tonError,\n}: RealtimeProviderProps): React.ReactElement {\n\tconst normalizedAuth = normalizeAuth(auth);\n\n\tconst socketUrl = buildSocketUrl(wsUrl, normalizedAuth);\n\tconst eventHandlersRef = useRef<Set<SubscribeHandler>>(new Set());\n\tconst lastHeartbeatRef = useRef<number>(Date.now());\n\tconst hasOpenedRef = useRef(false);\n\tconst previousUrlRef = useRef<string | null>(null);\n\tconst [connectionError, setConnectionError] = useState<Error | null>(null);\n\tconst [lastEvent, setLastEvent] = useState<AnyRealtimeEvent | null>(null);\n\tconst [connectionId, setConnectionId] = useState<string | null>(null);\n\n\tconst heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS;\n\tconst heartbeatTimeoutMs = DEFAULT_HEARTBEAT_TIMEOUT_MS;\n\n\tconst canConnect = Boolean(autoConnect && socketUrl);\n\tconst connectionUrl = canConnect ? socketUrl : null;\n\n\t// Track URL changes to detect when connection is being replaced\n\tuseEffect(() => {\n\t\tif (connectionUrl !== previousUrlRef.current) {\n\t\t\tpreviousUrlRef.current = connectionUrl;\n\t\t\t// Reset hasOpenedRef when URL changes so we know a new connection is starting\n\t\t\thasOpenedRef.current = false;\n\t\t}\n\t}, [connectionUrl]);\n\n\tconst {\n\t\tsendMessage,\n\t\tsendJsonMessage,\n\t\tlastMessage,\n\t\treadyState,\n\t\tgetWebSocket,\n\t} = useWebSocket(\n\t\tconnectionUrl,\n\t\t{\n\t\t\tshouldReconnect: (closeEvent) => {\n\t\t\t\tif (!canConnect) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\tif (closeEvent.code === 1008 || closeEvent.code === 1011) {\n\t\t\t\t\tconst err = new Error(\n\t\t\t\t\t\tcloseEvent.reason ||\n\t\t\t\t\t\t\t\"Realtime connection closed by server. Please check your credentials.\"\n\t\t\t\t\t);\n\t\t\t\t\tsetConnectionError(err);\n\t\t\t\t\tonError?.(err);\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\treturn true;\n\t\t\t},\n\t\t\treconnectAttempts: autoConnect ? undefined : 0,\n\t\t\treconnectInterval: (attempt) => {\n\t\t\t\tconst base = 500 * 2 ** attempt;\n\t\t\t\treturn Math.min(base, 30_000);\n\t\t\t},\n\t\t\tretryOnError: false,\n\t\t\tonOpen: () => {\n\t\t\t\thasOpenedRef.current = true;\n\t\t\t\tsetConnectionError(null);\n\t\t\t\tlastHeartbeatRef.current = Date.now();\n\t\t\t\tonConnect?.();\n\t\t\t},\n\t\t\tonClose: () => {\n\t\t\t\tsetConnectionId(null);\n\t\t\t\tonDisconnect?.();\n\t\t\t},\n\t\t\tonError: (event) => {\n\t\t\t\tif (!canConnect) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst socketLike = event.target;\n\t\t\t\tconst currentSocket = getWebSocket();\n\t\t\t\tconst isBrowserSocket =\n\t\t\t\t\ttypeof WebSocket !== \"undefined\" && socketLike instanceof WebSocket;\n\t\t\t\tconst socketState = isBrowserSocket ? socketLike.readyState : undefined;\n\n\t\t\t\t// Only suppress errors for THIS provider's socket, not other nested providers\n\t\t\t\t// Check if the errored socket belongs to this provider instance\n\t\t\t\tconst isThisProvidersSocket = currentSocket === socketLike;\n\n\t\t\t\t// Suppress errors if:\n\t\t\t\t// 1. This socket was replaced (URL changed while connecting) - only for this provider\n\t\t\t\t// 2. Connection URL is null (component unmounting or disabled)\n\t\t\t\t// 3. Socket is in CLOSING/CLOSED state and hasn't opened (cleanup/unmount) - only for this provider\n\t\t\t\tif (\n\t\t\t\t\t(!isThisProvidersSocket && currentSocket) ||\n\t\t\t\t\t!connectionUrl ||\n\t\t\t\t\t(isThisProvidersSocket &&\n\t\t\t\t\t\t!hasOpenedRef.current &&\n\t\t\t\t\t\t(socketState === WebSocket.CLOSING ||\n\t\t\t\t\t\t\tsocketState === WebSocket.CLOSED))\n\t\t\t\t) {\n\t\t\t\t\t// Suppress these expected errors during connection replacement or cleanup\n\t\t\t\t\t// But only if it's THIS provider's socket being replaced\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// For errors that occur during CONNECTING state, check if URL changed\n\t\t\t\t// Only suppress if it's this provider's socket\n\t\t\t\tif (\n\t\t\t\t\tisThisProvidersSocket &&\n\t\t\t\t\t!hasOpenedRef.current &&\n\t\t\t\t\tsocketState === WebSocket.CONNECTING &&\n\t\t\t\t\tconnectionUrl !== previousUrlRef.current\n\t\t\t\t) {\n\t\t\t\t\t// URL changed while connecting, suppress error\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst err = new Error(`WebSocket error: ${event.type}`);\n\t\t\t\tsetConnectionError(err);\n\t\t\t\tonError?.(err);\n\t\t\t},\n\t\t},\n\t\tcanConnect\n\t);\n\n\tuseEffect(() => {\n\t\tif (!lastMessage) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Decode message data from various formats\n\t\tconst decoded = decodeMessageData(lastMessage.data);\n\t\tif (decoded.type === \"unsupported\") {\n\t\t\treturn;\n\t\t}\n\n\t\t// Parse the message and determine its type\n\t\tconst message = parseWebSocketMessage(decoded.data);\n\n\t\t// Handle different message types\n\t\tswitch (message.type) {\n\t\t\tcase \"pong\":\n\t\t\t\tlastHeartbeatRef.current = Date.now();\n\t\t\t\tbreak;\n\n\t\t\tcase \"connection-established\":\n\t\t\t\tsetConnectionId(message.connectionId);\n\t\t\t\tlastHeartbeatRef.current = Date.now();\n\t\t\t\tbreak;\n\n\t\t\tcase \"error\": {\n\t\t\t\tconst err = new Error(message.message);\n\t\t\t\tsetConnectionError(err);\n\t\t\t\tonError?.(err);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"event\":\n\t\t\t\tlastHeartbeatRef.current = Date.now();\n\t\t\t\tsetLastEvent(message.event);\n\t\t\t\tfor (const handler of eventHandlersRef.current) {\n\t\t\t\t\tPromise.resolve(handler(message.event)).catch((error) => {\n\t\t\t\t\t\tconst err =\n\t\t\t\t\t\t\terror instanceof Error\n\t\t\t\t\t\t\t\t? error\n\t\t\t\t\t\t\t\t: new Error(`Subscriber threw an exception: ${String(error)}`);\n\t\t\t\t\t\tonError?.(err);\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t// Silently ignore invalid or unknown messages\n\t\t\t\tbreak;\n\t\t}\n\t}, [lastMessage, onError]);\n\n\tuseEffect(() => {\n\t\tif (!canConnect) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst interval = window.setInterval(() => {\n\t\t\tif (readyState !== ReadyState.OPEN) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check if heartbeat has timed out\n\t\t\tif (isHeartbeatTimedOut(lastHeartbeatRef.current, heartbeatTimeoutMs)) {\n\t\t\t\tconst socket = getWebSocket();\n\t\t\t\tsocket?.close(4000, \"Heartbeat timeout\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Send ping to keep connection alive\n\t\t\ttry {\n\t\t\t\tsendMessage(\"ping\");\n\t\t\t} catch {\n\t\t\t\t// Ignore send failures; reconnect logic will handle it\n\t\t\t}\n\t\t}, heartbeatIntervalMs);\n\n\t\treturn () => {\n\t\t\twindow.clearInterval(interval);\n\t\t};\n\t}, [\n\t\tcanConnect,\n\t\theartbeatIntervalMs,\n\t\theartbeatTimeoutMs,\n\t\treadyState,\n\t\tsendMessage,\n\t\tgetWebSocket,\n\t]);\n\n\tconst send = useCallback(\n\t\t(event: AnyRealtimeEvent) => {\n\t\t\tif (!connectionUrl) {\n\t\t\t\tthrow new Error(\"Realtime connection is disabled\");\n\t\t\t}\n\n\t\t\tif (readyState !== ReadyState.OPEN) {\n\t\t\t\tthrow new Error(\"Realtime connection is not established\");\n\t\t\t}\n\n\t\t\tsendJsonMessage(event);\n\t\t},\n\t\t[connectionUrl, readyState, sendJsonMessage]\n\t);\n\n\tconst sendRaw = useCallback(\n\t\t(data: string) => {\n\t\t\tif (!connectionUrl) {\n\t\t\t\tthrow new Error(\"Realtime connection is disabled\");\n\t\t\t}\n\n\t\t\tif (readyState !== ReadyState.OPEN) {\n\t\t\t\tthrow new Error(\"Realtime connection is not established\");\n\t\t\t}\n\n\t\t\tsendMessage(data);\n\t\t},\n\t\t[connectionUrl, readyState, sendMessage]\n\t);\n\n\tconst subscribe = useCallback((handler: SubscribeHandler) => {\n\t\teventHandlersRef.current.add(handler);\n\t\treturn () => {\n\t\t\teventHandlersRef.current.delete(handler);\n\t\t};\n\t}, []);\n\n\tconst reconnect = useCallback(() => {\n\t\tconst socket = getWebSocket();\n\t\tsocket?.close();\n\t}, [getWebSocket]);\n\n\tconst connection = useMemo<RealtimeConnectionState>(\n\t\t() => ({\n\t\t\tisConnected: readyState === ReadyState.OPEN,\n\t\t\tisConnecting: readyState === ReadyState.CONNECTING,\n\t\t\terror: connectionError,\n\t\t\tsend,\n\t\t\tsendRaw,\n\t\t\tsubscribe,\n\t\t\tlastEvent,\n\t\t\tconnectionId,\n\t\t\treconnect,\n\t\t}),\n\t\t[\n\t\t\treadyState,\n\t\t\tconnectionError,\n\t\t\tsend,\n\t\t\tsendRaw,\n\t\t\tsubscribe,\n\t\t\tlastEvent,\n\t\t\tconnectionId,\n\t\t\treconnect,\n\t\t]\n\t);\n\n\tconst value = useMemo<RealtimeContextValue>(\n\t\t() => ({\n\t\t\t...connection,\n\t\t\tvisitorId: normalizedAuth?.visitorId ?? null,\n\t\t\twebsiteId: normalizedAuth?.websiteId ?? null,\n\t\t\tuserId: normalizedAuth?.userId ?? null,\n\t\t}),\n\t\t[\n\t\t\tconnection,\n\t\t\tnormalizedAuth?.visitorId,\n\t\t\tnormalizedAuth?.websiteId,\n\t\t\tnormalizedAuth?.userId,\n\t\t]\n\t);\n\n\treturn (\n\t\t<RealtimeContext.Provider value={value}>\n\t\t\t{children}\n\t\t</RealtimeContext.Provider>\n\t);\n}\n\n/**\n * Returns the realtime connection context.\n */\nexport function useRealtimeConnection(): RealtimeContextValue {\n\tconst context = useContext(RealtimeContext);\n\tif (!context) {\n\t\tthrow new Error(\n\t\t\t\"useRealtimeConnection must be used within RealtimeProvider\"\n\t\t);\n\t}\n\n\treturn context;\n}\n\nexport type { RealtimeContextValue };\nexport type { RealtimeAuthConfig };\nexport type { RealtimeProviderProps };\nexport type { RealtimeEvent } from \"@cossistant/types/realtime-events\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"provider.js","names":["payload: unknown"],"sources":["../../src/realtime/provider.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n\ttype AnyRealtimeEvent,\n\tisValidEventType,\n\ttype RealtimeEvent,\n\tvalidateRealtimeEvent,\n} from \"@cossistant/types/realtime-events\";\nimport type React from \"react\";\nimport {\n\tcreateContext,\n\tuseCallback,\n\tuseContext,\n\tuseEffect,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from \"react\";\nimport useWebSocket, { ReadyState } from \"react-use-websocket\";\n\nconst DEFAULT_HEARTBEAT_INTERVAL_MS = 15_000;\nconst DEFAULT_HEARTBEAT_TIMEOUT_MS = 45_000;\n\ntype SubscribeHandler = (event: AnyRealtimeEvent) => void;\n\ntype MessageDecodeResult =\n\t| {\n\t\t\ttype: \"raw-text\";\n\t\t\tdata: string;\n\t }\n\t| {\n\t\t\ttype: \"unsupported\";\n\t };\n\ntype ParsedMessage =\n\t| {\n\t\t\ttype: \"pong\";\n\t }\n\t| {\n\t\t\ttype: \"connection-established\";\n\t\t\tconnectionId: string | null;\n\t }\n\t| {\n\t\t\ttype: \"error\";\n\t\t\tmessage: string;\n\t }\n\t| {\n\t\t\ttype: \"event\";\n\t\t\tevent: AnyRealtimeEvent;\n\t }\n\t| {\n\t\t\ttype: \"invalid\";\n\t };\n\ntype VisitorAuthConfig = {\n\tkind: \"visitor\";\n\tvisitorId: string | null;\n\twebsiteId?: string | null;\n\tpublicKey?: string | null;\n};\n\ntype SessionAuthConfig = {\n\tkind: \"session\";\n\tsessionToken: string | null;\n\twebsiteId?: string | null;\n\tuserId?: string | null;\n};\n\ntype RealtimeAuthConfig = VisitorAuthConfig | SessionAuthConfig;\n\ntype ResolvedAuthConfig = {\n\ttype: \"visitor\" | \"session\";\n\tvisitorId: string | null;\n\twebsiteId: string | null;\n\tuserId: string | null;\n\tsessionToken: string | null;\n\tpublicKey: string | null;\n};\n\ntype RealtimeProviderProps = {\n\tchildren: React.ReactNode;\n\twsUrl?: string;\n\tauth: RealtimeAuthConfig | null;\n\tautoConnect?: boolean;\n\tonConnect?: () => void;\n\tonDisconnect?: () => void;\n\tonError?: (error: Error) => void;\n};\n\ntype RealtimeConnectionState = {\n\tisConnected: boolean;\n\tisConnecting: boolean;\n\terror: Error | null;\n\tsend: (event: AnyRealtimeEvent) => void;\n\tsendRaw: (data: string) => void;\n\tsubscribe: (handler: SubscribeHandler) => () => void;\n\tlastEvent: AnyRealtimeEvent | null;\n\tconnectionId: string | null;\n\treconnect: () => void;\n};\n\ntype RealtimeContextValue = RealtimeConnectionState & {\n\tvisitorId: string | null;\n\twebsiteId: string | null;\n\tuserId: string | null;\n};\n\nconst DEFAULT_WS_URL = \"wss://api.cossistant.com/ws\";\n\nconst RealtimeContext = createContext<RealtimeContextValue | null>(null);\n\n/**\n * Decodes WebSocket message data into a string.\n * Handles string, ArrayBuffer, and ArrayBufferView formats.\n */\nfunction decodeMessageData(data: unknown): MessageDecodeResult {\n\tif (typeof data === \"string\") {\n\t\treturn { type: \"raw-text\", data };\n\t}\n\n\tif (data instanceof ArrayBuffer) {\n\t\ttry {\n\t\t\treturn { type: \"raw-text\", data: new TextDecoder().decode(data) };\n\t\t} catch {\n\t\t\treturn { type: \"unsupported\" };\n\t\t}\n\t}\n\n\tif (ArrayBuffer.isView(data)) {\n\t\ttry {\n\t\t\treturn { type: \"raw-text\", data: new TextDecoder().decode(data.buffer) };\n\t\t} catch {\n\t\t\treturn { type: \"unsupported\" };\n\t\t}\n\t}\n\n\treturn { type: \"unsupported\" };\n}\n\n/**\n * Safely parses JSON string, returning null if invalid.\n */\nfunction parseJson(raw: string): unknown {\n\ttry {\n\t\treturn JSON.parse(raw);\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Extracts a string field from an unknown object, with optional validation.\n */\nfunction extractStringField(\n\tobj: unknown,\n\tfield: string,\n\trequired = false\n): string | null {\n\tif (!obj || typeof obj !== \"object\" || !(field in obj)) {\n\t\treturn required ? null : null;\n\t}\n\tconst value = (obj as Record<string, unknown>)[field];\n\tif (typeof value === \"string\" && value.length > 0) {\n\t\treturn value;\n\t}\n\treturn required ? null : null;\n}\n\n/**\n * Parses a WebSocket message and determines its type and content.\n */\nfunction parseWebSocketMessage(rawText: string): ParsedMessage {\n\t// Handle pong heartbeat\n\tif (rawText === \"pong\") {\n\t\treturn { type: \"pong\" };\n\t}\n\n\t// Try to parse as JSON\n\tconst parsed = parseJson(rawText);\n\tif (!parsed || typeof parsed !== \"object\") {\n\t\treturn { type: \"invalid\" };\n\t}\n\n\tconst messageType = extractStringField(parsed, \"type\");\n\n\t// Handle CONNECTION_ESTABLISHED\n\tif (messageType === \"CONNECTION_ESTABLISHED\") {\n\t\tconst payload = (parsed as { payload?: unknown }).payload;\n\t\tconst connectionId = extractStringField(payload, \"connectionId\");\n\t\treturn { type: \"connection-established\", connectionId };\n\t}\n\n\t// Handle error messages\n\tif (\"error\" in parsed && \"message\" in parsed) {\n\t\tconst message =\n\t\t\textractStringField(parsed, \"message\") || \"Realtime connection error\";\n\t\treturn { type: \"error\", message };\n\t}\n\n\t// Handle realtime events\n\tif (messageType && isValidEventType(messageType)) {\n\t\ttry {\n\t\t\tconst event = constructRealtimeEvent(parsed);\n\t\t\tif (!event) {\n\t\t\t\treturn { type: \"invalid\" };\n\t\t\t}\n\t\t\treturn { type: \"event\", event };\n\t\t} catch (error) {\n\t\t\tconsole.error(\"[Realtime] Failed to construct event\", error);\n\t\t\treturn { type: \"invalid\" };\n\t\t}\n\t}\n\n\treturn { type: \"invalid\" };\n}\n\n/**\n * Constructs a RealtimeEvent from parsed JSON data.\n * Returns null if required fields are missing or validation fails.\n */\nfunction constructRealtimeEvent(parsed: unknown): AnyRealtimeEvent | null {\n\tif (!parsed || typeof parsed !== \"object\" || !(\"type\" in parsed)) {\n\t\treturn null;\n\t}\n\n\tconst type = (parsed as { type: unknown }).type;\n\tif (!isValidEventType(type)) {\n\t\treturn null;\n\t}\n\n\tconst eventType = type;\n\n\t// Extract payload directly\n\tconst payloadSource = (parsed as { payload?: unknown }).payload;\n\n\tlet payload: unknown;\n\ttry {\n\t\tpayload = validateRealtimeEvent(eventType, payloadSource);\n\t} catch (error) {\n\t\tconsole.error(\"[Realtime] Received invalid event payload\", error);\n\t\treturn null;\n\t}\n\n\tconst organizationId = extractStringField(\n\t\tpayloadSource,\n\t\t\"organizationId\",\n\t\ttrue\n\t);\n\tconst websiteId = extractStringField(payloadSource, \"websiteId\", true);\n\n\tif (!organizationId) {\n\t\tconsole.error(\"[Realtime] Received event without organizationId\", parsed);\n\t\treturn null;\n\t}\n\n\tif (!websiteId) {\n\t\tconsole.error(\"[Realtime] Received event without websiteId\", parsed);\n\t\treturn null;\n\t}\n\n\tconst visitorId = extractStringField(parsed, \"visitorId\");\n\n\treturn {\n\t\ttype: eventType,\n\t\tpayload,\n\t\torganizationId,\n\t\twebsiteId,\n\t\tvisitorId,\n\t} as AnyRealtimeEvent;\n}\n\n/**\n * Checks if heartbeat has timed out.\n * Only call this function in browser context (inside effects or event handlers).\n */\nfunction isHeartbeatTimedOut(\n\tlastHeartbeat: number,\n\ttimeoutMs: number\n): boolean {\n\tif (typeof window === \"undefined\") {\n\t\treturn false;\n\t}\n\tconst elapsed = Date.now() - lastHeartbeat;\n\treturn elapsed > timeoutMs;\n}\n\nfunction resolvePublicKey(explicit?: string | null): string | null {\n\tconst trimmed = explicit?.trim();\n\tif (trimmed) {\n\t\treturn trimmed;\n\t}\n\n\tconst fromEnv =\n\t\tprocess.env.NEXT_PUBLIC_COSSISTANT_API_KEY ||\n\t\tprocess.env.NEXT_PUBLIC_COSSISTANT_KEY ||\n\t\tprocess.env.COSSISTANT_API_KEY ||\n\t\tnull;\n\n\tconst normalized = fromEnv?.trim();\n\treturn normalized && normalized.length > 0 ? normalized : null;\n}\n\nfunction normalizeAuth(\n\tauth: RealtimeAuthConfig | null\n): ResolvedAuthConfig | null {\n\tif (!auth) {\n\t\treturn null;\n\t}\n\n\tif (auth.kind === \"visitor\") {\n\t\tconst visitorId = auth.visitorId?.trim() || null;\n\n\t\tif (!visitorId) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn {\n\t\t\ttype: \"visitor\",\n\t\t\tvisitorId,\n\t\t\twebsiteId: auth.websiteId?.trim() || null,\n\t\t\tuserId: null,\n\t\t\tsessionToken: null,\n\t\t\tpublicKey: resolvePublicKey(auth.publicKey ?? null),\n\t\t} satisfies ResolvedAuthConfig;\n\t}\n\n\tconst sessionToken = auth.sessionToken?.trim() || null;\n\n\tif (!sessionToken) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\ttype: \"session\",\n\t\tvisitorId: null,\n\t\twebsiteId: auth.websiteId?.trim() || null,\n\t\tuserId: auth.userId?.trim() || null,\n\t\tsessionToken,\n\t\tpublicKey: null,\n\t} satisfies ResolvedAuthConfig;\n}\n\nfunction buildSocketUrl(\n\tbaseUrl: string,\n\tauth: ResolvedAuthConfig | null\n): string | null {\n\tif (!auth) {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst url = new URL(baseUrl);\n\n\t\tif (auth.type === \"visitor\") {\n\t\t\turl.searchParams.set(\"visitorId\", auth.visitorId ?? \"\");\n\t\t\tconst publicKey = auth.publicKey;\n\t\t\tif (publicKey) {\n\t\t\t\turl.searchParams.set(\"publicKey\", publicKey);\n\t\t\t}\n\t\t} else {\n\t\t\turl.searchParams.set(\"sessionToken\", auth.sessionToken ?? \"\");\n\t\t\tif (auth.websiteId) {\n\t\t\t\turl.searchParams.set(\"websiteId\", auth.websiteId);\n\t\t\t}\n\t\t}\n\n\t\treturn url.toString();\n\t} catch (error) {\n\t\tconsole.error(\"[Realtime] Failed to build WebSocket URL\", error);\n\t\treturn null;\n\t}\n}\n\n/**\n * Internal component that handles the WebSocket connection.\n * Only rendered in the browser to avoid SSR issues with react-use-websocket.\n */\nfunction RealtimeProviderInternal({\n\tchildren,\n\twsUrl = DEFAULT_WS_URL,\n\tauth,\n\tautoConnect,\n\tonConnect,\n\tonDisconnect,\n\tonError,\n}: RealtimeProviderProps): React.ReactElement {\n\tconst normalizedAuth = normalizeAuth(auth);\n\n\tconst socketUrl = buildSocketUrl(wsUrl, normalizedAuth);\n\tconst eventHandlersRef = useRef<Set<SubscribeHandler>>(new Set());\n\tconst lastHeartbeatRef = useRef<number>(0);\n\tconst hasOpenedRef = useRef(false);\n\tconst previousUrlRef = useRef<string | null>(null);\n\tconst [connectionError, setConnectionError] = useState<Error | null>(null);\n\tconst [lastEvent, setLastEvent] = useState<AnyRealtimeEvent | null>(null);\n\tconst [connectionId, setConnectionId] = useState<string | null>(null);\n\n\tconst heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS;\n\tconst heartbeatTimeoutMs = DEFAULT_HEARTBEAT_TIMEOUT_MS;\n\n\tconst canConnect = Boolean(autoConnect && socketUrl);\n\tconst connectionUrl = canConnect ? socketUrl : null;\n\n\t// Track URL changes to detect when connection is being replaced\n\tuseEffect(() => {\n\t\tif (connectionUrl !== previousUrlRef.current) {\n\t\t\tpreviousUrlRef.current = connectionUrl;\n\t\t\t// Reset hasOpenedRef when URL changes so we know a new connection is starting\n\t\t\thasOpenedRef.current = false;\n\t\t}\n\t}, [connectionUrl]);\n\n\tconst {\n\t\tsendMessage,\n\t\tsendJsonMessage,\n\t\tlastMessage,\n\t\treadyState,\n\t\tgetWebSocket,\n\t} = useWebSocket(\n\t\tconnectionUrl,\n\t\t{\n\t\t\tshouldReconnect: (closeEvent) => {\n\t\t\t\tif (!canConnect) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\tif (closeEvent.code === 1008 || closeEvent.code === 1011) {\n\t\t\t\t\tconst err = new Error(\n\t\t\t\t\t\tcloseEvent.reason ||\n\t\t\t\t\t\t\t\"Realtime connection closed by server. Please check your credentials.\"\n\t\t\t\t\t);\n\t\t\t\t\tsetConnectionError(err);\n\t\t\t\t\tonError?.(err);\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\treturn true;\n\t\t\t},\n\t\t\treconnectAttempts: autoConnect ? undefined : 0,\n\t\t\treconnectInterval: (attempt) => {\n\t\t\t\tconst base = 500 * 2 ** attempt;\n\t\t\t\treturn Math.min(base, 30_000);\n\t\t\t},\n\t\t\tretryOnError: false,\n\t\t\tonOpen: () => {\n\t\t\t\thasOpenedRef.current = true;\n\t\t\t\tsetConnectionError(null);\n\t\t\t\tlastHeartbeatRef.current =\n\t\t\t\t\ttypeof window !== \"undefined\" ? Date.now() : 0;\n\t\t\t\tonConnect?.();\n\t\t\t},\n\t\t\tonClose: () => {\n\t\t\t\tsetConnectionId(null);\n\t\t\t\tonDisconnect?.();\n\t\t\t},\n\t\t\tonError: (event) => {\n\t\t\t\tif (!canConnect) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst socketLike = event.target;\n\t\t\t\tconst currentSocket = getWebSocket();\n\t\t\t\tconst isBrowserSocket =\n\t\t\t\t\ttypeof WebSocket !== \"undefined\" && socketLike instanceof WebSocket;\n\t\t\t\tconst socketState = isBrowserSocket ? socketLike.readyState : undefined;\n\n\t\t\t\t// Only suppress errors for THIS provider's socket, not other nested providers\n\t\t\t\t// Check if the errored socket belongs to this provider instance\n\t\t\t\tconst isThisProvidersSocket = currentSocket === socketLike;\n\n\t\t\t\t// Suppress errors if:\n\t\t\t\t// 1. This socket was replaced (URL changed while connecting) - only for this provider\n\t\t\t\t// 2. Connection URL is null (component unmounting or disabled)\n\t\t\t\t// 3. Socket is in CLOSING/CLOSED state and hasn't opened (cleanup/unmount) - only for this provider\n\t\t\t\tif (\n\t\t\t\t\t(!isThisProvidersSocket && currentSocket) ||\n\t\t\t\t\t!connectionUrl ||\n\t\t\t\t\t(isThisProvidersSocket &&\n\t\t\t\t\t\t!hasOpenedRef.current &&\n\t\t\t\t\t\t(socketState === WebSocket.CLOSING ||\n\t\t\t\t\t\t\tsocketState === WebSocket.CLOSED))\n\t\t\t\t) {\n\t\t\t\t\t// Suppress these expected errors during connection replacement or cleanup\n\t\t\t\t\t// But only if it's THIS provider's socket being replaced\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// For errors that occur during CONNECTING state, check if URL changed\n\t\t\t\t// Only suppress if it's this provider's socket\n\t\t\t\tif (\n\t\t\t\t\tisThisProvidersSocket &&\n\t\t\t\t\t!hasOpenedRef.current &&\n\t\t\t\t\tsocketState === WebSocket.CONNECTING &&\n\t\t\t\t\tconnectionUrl !== previousUrlRef.current\n\t\t\t\t) {\n\t\t\t\t\t// URL changed while connecting, suppress error\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst err = new Error(`WebSocket error: ${event.type}`);\n\t\t\t\tsetConnectionError(err);\n\t\t\t\tonError?.(err);\n\t\t\t},\n\t\t},\n\t\tcanConnect\n\t);\n\n\tuseEffect(() => {\n\t\tif (!lastMessage) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Decode message data from various formats\n\t\tconst decoded = decodeMessageData(lastMessage.data);\n\t\tif (decoded.type === \"unsupported\") {\n\t\t\treturn;\n\t\t}\n\n\t\t// Parse the message and determine its type\n\t\tconst message = parseWebSocketMessage(decoded.data);\n\n\t\t// Handle different message types\n\t\tswitch (message.type) {\n\t\t\tcase \"pong\":\n\t\t\t\tlastHeartbeatRef.current =\n\t\t\t\t\ttypeof window !== \"undefined\" ? Date.now() : 0;\n\t\t\t\tbreak;\n\n\t\t\tcase \"connection-established\":\n\t\t\t\tsetConnectionId(message.connectionId);\n\t\t\t\tlastHeartbeatRef.current =\n\t\t\t\t\ttypeof window !== \"undefined\" ? Date.now() : 0;\n\t\t\t\tbreak;\n\n\t\t\tcase \"error\": {\n\t\t\t\tconst err = new Error(message.message);\n\t\t\t\tsetConnectionError(err);\n\t\t\t\tonError?.(err);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"event\":\n\t\t\t\tlastHeartbeatRef.current =\n\t\t\t\t\ttypeof window !== \"undefined\" ? Date.now() : 0;\n\t\t\t\tsetLastEvent(message.event);\n\t\t\t\tfor (const handler of eventHandlersRef.current) {\n\t\t\t\t\tPromise.resolve(handler(message.event)).catch((error) => {\n\t\t\t\t\t\tconst err =\n\t\t\t\t\t\t\terror instanceof Error\n\t\t\t\t\t\t\t\t? error\n\t\t\t\t\t\t\t\t: new Error(`Subscriber threw an exception: ${String(error)}`);\n\t\t\t\t\t\tonError?.(err);\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t// Silently ignore invalid or unknown messages\n\t\t\t\tbreak;\n\t\t}\n\t}, [lastMessage, onError]);\n\n\tuseEffect(() => {\n\t\tif (!canConnect) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst interval = window.setInterval(() => {\n\t\t\tif (readyState !== ReadyState.OPEN) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check if heartbeat has timed out (skip if connection hasn't opened yet)\n\t\t\tif (\n\t\t\t\tlastHeartbeatRef.current !== 0 &&\n\t\t\t\tisHeartbeatTimedOut(lastHeartbeatRef.current, heartbeatTimeoutMs)\n\t\t\t) {\n\t\t\t\tconst socket = getWebSocket();\n\t\t\t\tsocket?.close(4000, \"Heartbeat timeout\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Send ping to keep connection alive\n\t\t\ttry {\n\t\t\t\tsendMessage(\"ping\");\n\t\t\t} catch {\n\t\t\t\t// Ignore send failures; reconnect logic will handle it\n\t\t\t}\n\t\t}, heartbeatIntervalMs);\n\n\t\treturn () => {\n\t\t\twindow.clearInterval(interval);\n\t\t};\n\t}, [\n\t\tcanConnect,\n\t\theartbeatIntervalMs,\n\t\theartbeatTimeoutMs,\n\t\treadyState,\n\t\tsendMessage,\n\t\tgetWebSocket,\n\t]);\n\n\tconst send = useCallback(\n\t\t(event: AnyRealtimeEvent) => {\n\t\t\tif (!connectionUrl) {\n\t\t\t\tthrow new Error(\"Realtime connection is disabled\");\n\t\t\t}\n\n\t\t\tif (readyState !== ReadyState.OPEN) {\n\t\t\t\tthrow new Error(\"Realtime connection is not established\");\n\t\t\t}\n\n\t\t\tsendJsonMessage(event);\n\t\t},\n\t\t[connectionUrl, readyState, sendJsonMessage]\n\t);\n\n\tconst sendRaw = useCallback(\n\t\t(data: string) => {\n\t\t\tif (!connectionUrl) {\n\t\t\t\tthrow new Error(\"Realtime connection is disabled\");\n\t\t\t}\n\n\t\t\tif (readyState !== ReadyState.OPEN) {\n\t\t\t\tthrow new Error(\"Realtime connection is not established\");\n\t\t\t}\n\n\t\t\tsendMessage(data);\n\t\t},\n\t\t[connectionUrl, readyState, sendMessage]\n\t);\n\n\tconst subscribe = useCallback((handler: SubscribeHandler) => {\n\t\teventHandlersRef.current.add(handler);\n\t\treturn () => {\n\t\t\teventHandlersRef.current.delete(handler);\n\t\t};\n\t}, []);\n\n\tconst reconnect = useCallback(() => {\n\t\tconst socket = getWebSocket();\n\t\tsocket?.close();\n\t}, [getWebSocket]);\n\n\tconst connection = useMemo<RealtimeConnectionState>(\n\t\t() => ({\n\t\t\tisConnected: readyState === ReadyState.OPEN,\n\t\t\tisConnecting: readyState === ReadyState.CONNECTING,\n\t\t\terror: connectionError,\n\t\t\tsend,\n\t\t\tsendRaw,\n\t\t\tsubscribe,\n\t\t\tlastEvent,\n\t\t\tconnectionId,\n\t\t\treconnect,\n\t\t}),\n\t\t[\n\t\t\treadyState,\n\t\t\tconnectionError,\n\t\t\tsend,\n\t\t\tsendRaw,\n\t\t\tsubscribe,\n\t\t\tlastEvent,\n\t\t\tconnectionId,\n\t\t\treconnect,\n\t\t]\n\t);\n\n\tconst value = useMemo<RealtimeContextValue>(\n\t\t() => ({\n\t\t\t...connection,\n\t\t\tvisitorId: normalizedAuth?.visitorId ?? null,\n\t\t\twebsiteId: normalizedAuth?.websiteId ?? null,\n\t\t\tuserId: normalizedAuth?.userId ?? null,\n\t\t}),\n\t\t[\n\t\t\tconnection,\n\t\t\tnormalizedAuth?.visitorId,\n\t\t\tnormalizedAuth?.websiteId,\n\t\t\tnormalizedAuth?.userId,\n\t\t]\n\t);\n\n\treturn (\n\t\t<RealtimeContext.Provider value={value}>\n\t\t\t{children}\n\t\t</RealtimeContext.Provider>\n\t);\n}\n\n/**\n * Provides websocket connectivity and heartbeating logic for realtime events.\n * Handles SSR by only initializing the WebSocket connection in the browser.\n */\nexport function RealtimeProvider({\n\tchildren,\n\twsUrl = DEFAULT_WS_URL,\n\tauth,\n\tautoConnect = true,\n\tonConnect,\n\tonDisconnect,\n\tonError,\n}: RealtimeProviderProps): React.ReactElement {\n\tconst [isBrowser, setIsBrowser] = useState(false);\n\n\tuseEffect(() => {\n\t\tsetIsBrowser(true);\n\t}, []);\n\n\tconst normalizedAuth = normalizeAuth(auth);\n\n\t// Create a default context value for SSR\n\tconst defaultValue = useMemo<RealtimeContextValue>(\n\t\t() => ({\n\t\t\tisConnected: false,\n\t\t\tisConnecting: false,\n\t\t\terror: null,\n\t\t\tsend: () => {\n\t\t\t\tthrow new Error(\"Realtime connection is not available during SSR\");\n\t\t\t},\n\t\t\tsendRaw: () => {\n\t\t\t\tthrow new Error(\"Realtime connection is not available during SSR\");\n\t\t\t},\n\t\t\tsubscribe: () => () => {},\n\t\t\tlastEvent: null,\n\t\t\tconnectionId: null,\n\t\t\treconnect: () => {},\n\t\t\tvisitorId: normalizedAuth?.visitorId ?? null,\n\t\t\twebsiteId: normalizedAuth?.websiteId ?? null,\n\t\t\tuserId: normalizedAuth?.userId ?? null,\n\t\t}),\n\t\t[\n\t\t\tnormalizedAuth?.visitorId,\n\t\t\tnormalizedAuth?.websiteId,\n\t\t\tnormalizedAuth?.userId,\n\t\t]\n\t);\n\n\t// During SSR or before hydration, provide a default context\n\tif (!isBrowser) {\n\t\treturn (\n\t\t\t<RealtimeContext.Provider value={defaultValue}>\n\t\t\t\t{children}\n\t\t\t</RealtimeContext.Provider>\n\t\t);\n\t}\n\n\t// In the browser, use the full implementation\n\treturn (\n\t\t<RealtimeProviderInternal\n\t\t\tauth={auth}\n\t\t\tautoConnect={autoConnect}\n\t\t\tonConnect={onConnect}\n\t\t\tonDisconnect={onDisconnect}\n\t\t\tonError={onError}\n\t\t\twsUrl={wsUrl}\n\t\t>\n\t\t\t{children}\n\t\t</RealtimeProviderInternal>\n\t);\n}\n\n/**\n * Returns the realtime connection context.\n */\nexport function useRealtimeConnection(): RealtimeContextValue {\n\tconst context = useContext(RealtimeContext);\n\tif (!context) {\n\t\tthrow new Error(\n\t\t\t\"useRealtimeConnection must be used within RealtimeProvider\"\n\t\t);\n\t}\n\n\treturn context;\n}\n\nexport type { RealtimeContextValue };\nexport type { RealtimeAuthConfig };\nexport type { RealtimeProviderProps };\nexport type { RealtimeEvent } from \"@cossistant/types/realtime-events\";\n"],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import * as class_variance_authority_dist_types0 from "class-variance-authority/
|
|
|
4
4
|
|
|
5
5
|
//#region src/support/components/button.d.ts
|
|
6
6
|
declare const coButtonVariants: (props?: ({
|
|
7
|
-
variant?: "
|
|
7
|
+
variant?: "tab" | "default" | "secondary" | "ghost" | "outline" | "tab-selected" | null | undefined;
|
|
8
8
|
size?: "default" | "large" | "icon" | null | undefined;
|
|
9
9
|
} & class_variance_authority_dist_types0.ClassProp) | undefined) => string;
|
|
10
10
|
type CossistantButtonProps = React$1.ComponentProps<"button"> & VariantProps<typeof coButtonVariants>;
|
package/support/text/runtime.js
CHANGED
|
@@ -70,7 +70,7 @@ function createTextUtils(locale, isHydrated = false) {
|
|
|
70
70
|
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
71
71
|
},
|
|
72
72
|
timeOfDay: () => {
|
|
73
|
-
if (!isHydrated) return {
|
|
73
|
+
if (!isHydrated || typeof window === "undefined") return {
|
|
74
74
|
token: "morning",
|
|
75
75
|
label: dayPeriodLabels.morning
|
|
76
76
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","names":["BUILTIN_LOCALES: Record<SupportLocale, SupportLocaleMessages>","TIME_OF_DAY_THRESHOLDS: Array<{\n\ttoken: SupportTimeOfDayToken;\n\thour: number;\n}>","fallback: Record<SupportTimeOfDayToken, string>","labels: Partial<Record<SupportTimeOfDayToken, string>>","token: SupportTimeOfDayToken","chain: string[]","map: NormalizedOverrides"],"sources":["../../../src/support/text/runtime.ts"],"sourcesContent":["/** biome-ignore-all lint/nursery/useMaxParams: ok here */\nimport en from \"./locales/en\";\nimport es from \"./locales/es\";\nimport fr from \"./locales/fr\";\nimport type {\n\tSupportLocale,\n\tSupportLocaleMessages,\n\tSupportTextContentOverrides,\n\tSupportTextContext,\n\tSupportTextKey,\n\tSupportTextUtils,\n\tSupportTextVariables,\n\tSupportTimeOfDayToken,\n} from \"./locales/keys\";\n\nexport type NormalizedOverride<K extends SupportTextKey> = {\n\tanyLocale?: SupportLocaleMessages[K];\n\tbyLocale: Map<string, SupportLocaleMessages[K]>;\n};\n\nexport type NormalizedOverrides = Map<\n\tSupportTextKey,\n\tNormalizedOverride<SupportTextKey>\n>;\n\nexport const BUILTIN_LOCALES: Record<SupportLocale, SupportLocaleMessages> = {\n\ten,\n\tfr,\n\tes,\n};\n\nconst TIME_OF_DAY_THRESHOLDS: Array<{\n\ttoken: SupportTimeOfDayToken;\n\thour: number;\n}> = [\n\t{ token: \"morning\", hour: 9 },\n\t{ token: \"afternoon\", hour: 15 },\n\t{ token: \"evening\", hour: 20 },\n];\n\nfunction buildDayPeriodLabels(\n\tlocale: string\n): Record<SupportTimeOfDayToken, string> {\n\tconst fallback: Record<SupportTimeOfDayToken, string> = {\n\t\tmorning: \"morning\",\n\t\tafternoon: \"afternoon\",\n\t\tevening: \"evening\",\n\t};\n\n\ttry {\n\t\tconst formatter = new Intl.DateTimeFormat(locale, {\n\t\t\thour: \"numeric\",\n\t\t\thour12: true,\n\t\t});\n\n\t\tconst labels: Partial<Record<SupportTimeOfDayToken, string>> = {};\n\t\tfor (const { token, hour } of TIME_OF_DAY_THRESHOLDS) {\n\t\t\tconst parts = formatter.formatToParts(new Date(2020, 0, 1, hour));\n\t\t\tconst part = parts.find((segment) => segment.type === \"dayPeriod\");\n\t\t\tif (part?.value) {\n\t\t\t\tconst normalized = part.value.trim();\n\t\t\t\tif (normalized) {\n\t\t\t\t\tlabels[token] = normalized;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tmorning: labels.morning ?? fallback.morning,\n\t\t\tafternoon: labels.afternoon ?? fallback.afternoon,\n\t\t\tevening: labels.evening ?? fallback.evening,\n\t\t};\n\t} catch {\n\t\treturn fallback;\n\t}\n}\n\n/**\n * Builds formatting helpers tailored for the visitor locale. The utilities are\n * memoized by callers and include number formatting, pluralization, title\n * casing and dynamic time-of-day descriptions.\n */\nexport function createTextUtils(\n\tlocale: string,\n\tisHydrated = false\n): SupportTextUtils {\n\tconst numberFormatter = new Intl.NumberFormat(locale);\n\tconst pluralRules = new Intl.PluralRules(locale);\n\tconst dayPeriodLabels = buildDayPeriodLabels(locale);\n\n\treturn {\n\t\tformatNumber: (value: number, options?: Intl.NumberFormatOptions) =>\n\t\t\toptions\n\t\t\t\t? new Intl.NumberFormat(locale, options).format(value)\n\t\t\t\t: numberFormatter.format(value),\n\t\tpluralize: (count: number, options: { one: string; other: string }) => {\n\t\t\tconst rule = pluralRules.select(count);\n\t\t\treturn rule === \"one\" ? options.one : options.other;\n\t\t},\n\t\ttitleCase: (value: string) => {\n\t\t\tif (!value) {\n\t\t\t\treturn \"\";\n\t\t\t}\n\t\t\treturn value.charAt(0).toUpperCase() + value.slice(1);\n\t\t},\n\t\ttimeOfDay: () => {\n\t\t\t// Return a stable default during SSR to avoid hydration mismatches\n\t\t\tif (!isHydrated) {\n\t\t\t\treturn {\n\t\t\t\t\ttoken: \"morning\" as SupportTimeOfDayToken,\n\t\t\t\t\tlabel: dayPeriodLabels.morning,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst hour = new Date().getHours();\n\t\t\tlet token: SupportTimeOfDayToken;\n\t\t\tif (hour < 12) {\n\t\t\t\ttoken = \"morning\";\n\t\t\t} else if (hour < 18) {\n\t\t\t\ttoken = \"afternoon\";\n\t\t\t} else {\n\t\t\t\ttoken = \"evening\";\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttoken,\n\t\t\t\tlabel: dayPeriodLabels[token],\n\t\t\t};\n\t\t},\n\t};\n}\n\n/**\n * Normalize a locale string to its base language code\n * Examples:\n * - \"en-US\" -> \"en\"\n * - \"en-GB\" -> \"en\"\n * - \"fr-FR\" -> \"fr\"\n */\nfunction normalizeLocaleString(locale: string): string {\n\tconst [base] = locale.toLowerCase().split(\"-\");\n\treturn base || locale.toLowerCase();\n}\n\n/**\n * Derives the locale preference order from visitor/provider hints while\n * guaranteeing an English fallback.\n */\nexport function buildLocaleChain(\n\tpreferences: Array<string | null | undefined>\n): string[] {\n\tconst seen = new Set<string>();\n\tconst chain: string[] = [];\n\n\tconst pushLocale = (value: string) => {\n\t\t// Always normalize to base language (en-US -> en, en-GB -> en)\n\t\tconst normalized = normalizeLocaleString(value);\n\t\tif (!seen.has(normalized)) {\n\t\t\tseen.add(normalized);\n\t\t\tchain.push(normalized);\n\t\t}\n\t};\n\n\tfor (const candidate of preferences) {\n\t\tif (!candidate) {\n\t\t\tcontinue;\n\t\t}\n\t\tpushLocale(candidate);\n\t}\n\n\t// Always fallback to English\n\tif (!seen.has(\"en\")) {\n\t\tchain.push(\"en\");\n\t}\n\n\treturn chain;\n}\n\n/**\n * Canonicalizes text override definitions into a lookup map that can be\n * consumed efficiently at runtime.\n */\nexport function normalizeOverrides(\n\toverrides?: SupportTextContentOverrides<string>\n): NormalizedOverrides {\n\tconst map: NormalizedOverrides = new Map();\n\n\tif (!overrides) {\n\t\treturn map;\n\t}\n\n\tfor (const key of Object.keys(overrides) as SupportTextKey[]) {\n\t\tconst value = overrides[key];\n\t\tif (!value) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (typeof value === \"string\" || typeof value === \"function\") {\n\t\t\tmap.set(key, {\n\t\t\t\tanyLocale: value as SupportLocaleMessages[typeof key],\n\t\t\t\tbyLocale: new Map(),\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst byLocale = new Map<string, SupportLocaleMessages[typeof key]>();\n\t\tfor (const [locale, localizedValue] of Object.entries(value)) {\n\t\t\tif (!localizedValue) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbyLocale.set(\n\t\t\t\tlocale.toLowerCase(),\n\t\t\t\tlocalizedValue as SupportLocaleMessages[typeof key]\n\t\t\t);\n\t\t}\n\n\t\tmap.set(key, { byLocale });\n\t}\n\n\treturn map;\n}\n\n/**\n * Finds the best matching localized string for a key, consulting overrides\n * before bundled locale dictionaries.\n */\nexport function resolveMessage<K extends SupportTextKey>(\n\tkey: K,\n\tlocaleChain: string[],\n\toverrides: NormalizedOverrides\n): SupportLocaleMessages[K] {\n\tconst override = overrides.get(key) as NormalizedOverride<K> | undefined;\n\tif (override) {\n\t\tfor (const locale of localeChain) {\n\t\t\tconst localized = override.byLocale.get(locale);\n\t\t\tif (localized) {\n\t\t\t\treturn localized;\n\t\t\t}\n\t\t}\n\t\tif (override.anyLocale) {\n\t\t\treturn override.anyLocale;\n\t\t}\n\t}\n\n\tfor (const locale of localeChain) {\n\t\tif (locale in BUILTIN_LOCALES) {\n\t\t\treturn BUILTIN_LOCALES[locale as SupportLocale][key];\n\t\t}\n\t}\n\n\treturn BUILTIN_LOCALES.en[key];\n}\n\n/**\n * Produces the final rendered string by executing function overrides or\n * interpolating variables into template literals.\n */\nexport function evaluateMessage<K extends SupportTextKey>(\n\tkey: K,\n\tmessage: SupportLocaleMessages[K],\n\tvariables: SupportTextVariables<K> | undefined,\n\tcontext: SupportTextContext,\n\tutils: SupportTextUtils\n): string {\n\tif (typeof message === \"function\") {\n\t\treturn message({\n\t\t\tvariables: variables as never,\n\t\t\tcontext,\n\t\t\tutils,\n\t\t});\n\t}\n\n\treturn message;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"runtime.js","names":["BUILTIN_LOCALES: Record<SupportLocale, SupportLocaleMessages>","TIME_OF_DAY_THRESHOLDS: Array<{\n\ttoken: SupportTimeOfDayToken;\n\thour: number;\n}>","fallback: Record<SupportTimeOfDayToken, string>","labels: Partial<Record<SupportTimeOfDayToken, string>>","token: SupportTimeOfDayToken","chain: string[]","map: NormalizedOverrides"],"sources":["../../../src/support/text/runtime.ts"],"sourcesContent":["/** biome-ignore-all lint/nursery/useMaxParams: ok here */\nimport en from \"./locales/en\";\nimport es from \"./locales/es\";\nimport fr from \"./locales/fr\";\nimport type {\n\tSupportLocale,\n\tSupportLocaleMessages,\n\tSupportTextContentOverrides,\n\tSupportTextContext,\n\tSupportTextKey,\n\tSupportTextUtils,\n\tSupportTextVariables,\n\tSupportTimeOfDayToken,\n} from \"./locales/keys\";\n\nexport type NormalizedOverride<K extends SupportTextKey> = {\n\tanyLocale?: SupportLocaleMessages[K];\n\tbyLocale: Map<string, SupportLocaleMessages[K]>;\n};\n\nexport type NormalizedOverrides = Map<\n\tSupportTextKey,\n\tNormalizedOverride<SupportTextKey>\n>;\n\nexport const BUILTIN_LOCALES: Record<SupportLocale, SupportLocaleMessages> = {\n\ten,\n\tfr,\n\tes,\n};\n\nconst TIME_OF_DAY_THRESHOLDS: Array<{\n\ttoken: SupportTimeOfDayToken;\n\thour: number;\n}> = [\n\t{ token: \"morning\", hour: 9 },\n\t{ token: \"afternoon\", hour: 15 },\n\t{ token: \"evening\", hour: 20 },\n];\n\nfunction buildDayPeriodLabels(\n\tlocale: string\n): Record<SupportTimeOfDayToken, string> {\n\tconst fallback: Record<SupportTimeOfDayToken, string> = {\n\t\tmorning: \"morning\",\n\t\tafternoon: \"afternoon\",\n\t\tevening: \"evening\",\n\t};\n\n\ttry {\n\t\tconst formatter = new Intl.DateTimeFormat(locale, {\n\t\t\thour: \"numeric\",\n\t\t\thour12: true,\n\t\t});\n\n\t\tconst labels: Partial<Record<SupportTimeOfDayToken, string>> = {};\n\t\tfor (const { token, hour } of TIME_OF_DAY_THRESHOLDS) {\n\t\t\tconst parts = formatter.formatToParts(new Date(2020, 0, 1, hour));\n\t\t\tconst part = parts.find((segment) => segment.type === \"dayPeriod\");\n\t\t\tif (part?.value) {\n\t\t\t\tconst normalized = part.value.trim();\n\t\t\t\tif (normalized) {\n\t\t\t\t\tlabels[token] = normalized;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tmorning: labels.morning ?? fallback.morning,\n\t\t\tafternoon: labels.afternoon ?? fallback.afternoon,\n\t\t\tevening: labels.evening ?? fallback.evening,\n\t\t};\n\t} catch {\n\t\treturn fallback;\n\t}\n}\n\n/**\n * Builds formatting helpers tailored for the visitor locale. The utilities are\n * memoized by callers and include number formatting, pluralization, title\n * casing and dynamic time-of-day descriptions.\n */\nexport function createTextUtils(\n\tlocale: string,\n\tisHydrated = false\n): SupportTextUtils {\n\tconst numberFormatter = new Intl.NumberFormat(locale);\n\tconst pluralRules = new Intl.PluralRules(locale);\n\tconst dayPeriodLabels = buildDayPeriodLabels(locale);\n\n\treturn {\n\t\tformatNumber: (value: number, options?: Intl.NumberFormatOptions) =>\n\t\t\toptions\n\t\t\t\t? new Intl.NumberFormat(locale, options).format(value)\n\t\t\t\t: numberFormatter.format(value),\n\t\tpluralize: (count: number, options: { one: string; other: string }) => {\n\t\t\tconst rule = pluralRules.select(count);\n\t\t\treturn rule === \"one\" ? options.one : options.other;\n\t\t},\n\t\ttitleCase: (value: string) => {\n\t\t\tif (!value) {\n\t\t\t\treturn \"\";\n\t\t\t}\n\t\t\treturn value.charAt(0).toUpperCase() + value.slice(1);\n\t\t},\n\t\ttimeOfDay: () => {\n\t\t\t// Return a stable default during SSR to avoid hydration mismatches\n\t\t\tif (!isHydrated || typeof window === \"undefined\") {\n\t\t\t\treturn {\n\t\t\t\t\ttoken: \"morning\" as SupportTimeOfDayToken,\n\t\t\t\t\tlabel: dayPeriodLabels.morning,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst hour = new Date().getHours();\n\t\t\tlet token: SupportTimeOfDayToken;\n\t\t\tif (hour < 12) {\n\t\t\t\ttoken = \"morning\";\n\t\t\t} else if (hour < 18) {\n\t\t\t\ttoken = \"afternoon\";\n\t\t\t} else {\n\t\t\t\ttoken = \"evening\";\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttoken,\n\t\t\t\tlabel: dayPeriodLabels[token],\n\t\t\t};\n\t\t},\n\t};\n}\n\n/**\n * Normalize a locale string to its base language code\n * Examples:\n * - \"en-US\" -> \"en\"\n * - \"en-GB\" -> \"en\"\n * - \"fr-FR\" -> \"fr\"\n */\nfunction normalizeLocaleString(locale: string): string {\n\tconst [base] = locale.toLowerCase().split(\"-\");\n\treturn base || locale.toLowerCase();\n}\n\n/**\n * Derives the locale preference order from visitor/provider hints while\n * guaranteeing an English fallback.\n */\nexport function buildLocaleChain(\n\tpreferences: Array<string | null | undefined>\n): string[] {\n\tconst seen = new Set<string>();\n\tconst chain: string[] = [];\n\n\tconst pushLocale = (value: string) => {\n\t\t// Always normalize to base language (en-US -> en, en-GB -> en)\n\t\tconst normalized = normalizeLocaleString(value);\n\t\tif (!seen.has(normalized)) {\n\t\t\tseen.add(normalized);\n\t\t\tchain.push(normalized);\n\t\t}\n\t};\n\n\tfor (const candidate of preferences) {\n\t\tif (!candidate) {\n\t\t\tcontinue;\n\t\t}\n\t\tpushLocale(candidate);\n\t}\n\n\t// Always fallback to English\n\tif (!seen.has(\"en\")) {\n\t\tchain.push(\"en\");\n\t}\n\n\treturn chain;\n}\n\n/**\n * Canonicalizes text override definitions into a lookup map that can be\n * consumed efficiently at runtime.\n */\nexport function normalizeOverrides(\n\toverrides?: SupportTextContentOverrides<string>\n): NormalizedOverrides {\n\tconst map: NormalizedOverrides = new Map();\n\n\tif (!overrides) {\n\t\treturn map;\n\t}\n\n\tfor (const key of Object.keys(overrides) as SupportTextKey[]) {\n\t\tconst value = overrides[key];\n\t\tif (!value) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (typeof value === \"string\" || typeof value === \"function\") {\n\t\t\tmap.set(key, {\n\t\t\t\tanyLocale: value as SupportLocaleMessages[typeof key],\n\t\t\t\tbyLocale: new Map(),\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst byLocale = new Map<string, SupportLocaleMessages[typeof key]>();\n\t\tfor (const [locale, localizedValue] of Object.entries(value)) {\n\t\t\tif (!localizedValue) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbyLocale.set(\n\t\t\t\tlocale.toLowerCase(),\n\t\t\t\tlocalizedValue as SupportLocaleMessages[typeof key]\n\t\t\t);\n\t\t}\n\n\t\tmap.set(key, { byLocale });\n\t}\n\n\treturn map;\n}\n\n/**\n * Finds the best matching localized string for a key, consulting overrides\n * before bundled locale dictionaries.\n */\nexport function resolveMessage<K extends SupportTextKey>(\n\tkey: K,\n\tlocaleChain: string[],\n\toverrides: NormalizedOverrides\n): SupportLocaleMessages[K] {\n\tconst override = overrides.get(key) as NormalizedOverride<K> | undefined;\n\tif (override) {\n\t\tfor (const locale of localeChain) {\n\t\t\tconst localized = override.byLocale.get(locale);\n\t\t\tif (localized) {\n\t\t\t\treturn localized;\n\t\t\t}\n\t\t}\n\t\tif (override.anyLocale) {\n\t\t\treturn override.anyLocale;\n\t\t}\n\t}\n\n\tfor (const locale of localeChain) {\n\t\tif (locale in BUILTIN_LOCALES) {\n\t\t\treturn BUILTIN_LOCALES[locale as SupportLocale][key];\n\t\t}\n\t}\n\n\treturn BUILTIN_LOCALES.en[key];\n}\n\n/**\n * Produces the final rendered string by executing function overrides or\n * interpolating variables into template literals.\n */\nexport function evaluateMessage<K extends SupportTextKey>(\n\tkey: K,\n\tmessage: SupportLocaleMessages[K],\n\tvariables: SupportTextVariables<K> | undefined,\n\tcontext: SupportTextContext,\n\tutils: SupportTextUtils\n): string {\n\tif (typeof message === \"function\") {\n\t\treturn message({\n\t\t\tvariables: variables as never,\n\t\t\tcontext,\n\t\t\tutils,\n\t\t});\n\t}\n\n\treturn message;\n}\n"],"mappings":";;;;;;AAyBA,MAAaA,kBAAgE;CAC5E;CACA;CACA;CACA;AAED,MAAMC,yBAGD;CACJ;EAAE,OAAO;EAAW,MAAM;EAAG;CAC7B;EAAE,OAAO;EAAa,MAAM;EAAI;CAChC;EAAE,OAAO;EAAW,MAAM;EAAI;CAC9B;AAED,SAAS,qBACR,QACwC;CACxC,MAAMC,WAAkD;EACvD,SAAS;EACT,WAAW;EACX,SAAS;EACT;AAED,KAAI;EACH,MAAM,YAAY,IAAI,KAAK,eAAe,QAAQ;GACjD,MAAM;GACN,QAAQ;GACR,CAAC;EAEF,MAAMC,SAAyD,EAAE;AACjE,OAAK,MAAM,EAAE,OAAO,UAAU,wBAAwB;GAErD,MAAM,OADQ,UAAU,cAAc,IAAI,KAAK,MAAM,GAAG,GAAG,KAAK,CAAC,CAC9C,MAAM,YAAY,QAAQ,SAAS,YAAY;AAClE,OAAI,MAAM,OAAO;IAChB,MAAM,aAAa,KAAK,MAAM,MAAM;AACpC,QAAI,WACH,QAAO,SAAS;;;AAKnB,SAAO;GACN,SAAS,OAAO,WAAW,SAAS;GACpC,WAAW,OAAO,aAAa,SAAS;GACxC,SAAS,OAAO,WAAW,SAAS;GACpC;SACM;AACP,SAAO;;;;;;;;AAST,SAAgB,gBACf,QACA,aAAa,OACM;CACnB,MAAM,kBAAkB,IAAI,KAAK,aAAa,OAAO;CACrD,MAAM,cAAc,IAAI,KAAK,YAAY,OAAO;CAChD,MAAM,kBAAkB,qBAAqB,OAAO;AAEpD,QAAO;EACN,eAAe,OAAe,YAC7B,UACG,IAAI,KAAK,aAAa,QAAQ,QAAQ,CAAC,OAAO,MAAM,GACpD,gBAAgB,OAAO,MAAM;EACjC,YAAY,OAAe,YAA4C;AAEtE,UADa,YAAY,OAAO,MAAM,KACtB,QAAQ,QAAQ,MAAM,QAAQ;;EAE/C,YAAY,UAAkB;AAC7B,OAAI,CAAC,MACJ,QAAO;AAER,UAAO,MAAM,OAAO,EAAE,CAAC,aAAa,GAAG,MAAM,MAAM,EAAE;;EAEtD,iBAAiB;AAEhB,OAAI,CAAC,cAAc,OAAO,WAAW,YACpC,QAAO;IACN,OAAO;IACP,OAAO,gBAAgB;IACvB;GAGF,MAAM,wBAAO,IAAI,MAAM,EAAC,UAAU;GAClC,IAAIC;AACJ,OAAI,OAAO,GACV,SAAQ;YACE,OAAO,GACjB,SAAQ;OAER,SAAQ;AAGT,UAAO;IACN;IACA,OAAO,gBAAgB;IACvB;;EAEF;;;;;;;;;AAUF,SAAS,sBAAsB,QAAwB;CACtD,MAAM,CAAC,QAAQ,OAAO,aAAa,CAAC,MAAM,IAAI;AAC9C,QAAO,QAAQ,OAAO,aAAa;;;;;;AAOpC,SAAgB,iBACf,aACW;CACX,MAAM,uBAAO,IAAI,KAAa;CAC9B,MAAMC,QAAkB,EAAE;CAE1B,MAAM,cAAc,UAAkB;EAErC,MAAM,aAAa,sBAAsB,MAAM;AAC/C,MAAI,CAAC,KAAK,IAAI,WAAW,EAAE;AAC1B,QAAK,IAAI,WAAW;AACpB,SAAM,KAAK,WAAW;;;AAIxB,MAAK,MAAM,aAAa,aAAa;AACpC,MAAI,CAAC,UACJ;AAED,aAAW,UAAU;;AAItB,KAAI,CAAC,KAAK,IAAI,KAAK,CAClB,OAAM,KAAK,KAAK;AAGjB,QAAO;;;;;;AAOR,SAAgB,mBACf,WACsB;CACtB,MAAMC,sBAA2B,IAAI,KAAK;AAE1C,KAAI,CAAC,UACJ,QAAO;AAGR,MAAK,MAAM,OAAO,OAAO,KAAK,UAAU,EAAsB;EAC7D,MAAM,QAAQ,UAAU;AACxB,MAAI,CAAC,MACJ;AAGD,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY;AAC7D,OAAI,IAAI,KAAK;IACZ,WAAW;IACX,0BAAU,IAAI,KAAK;IACnB,CAAC;AACF;;EAGD,MAAM,2BAAW,IAAI,KAAgD;AACrE,OAAK,MAAM,CAAC,QAAQ,mBAAmB,OAAO,QAAQ,MAAM,EAAE;AAC7D,OAAI,CAAC,eACJ;AAED,YAAS,IACR,OAAO,aAAa,EACpB,eACA;;AAGF,MAAI,IAAI,KAAK,EAAE,UAAU,CAAC;;AAG3B,QAAO;;;;;;AAOR,SAAgB,eACf,KACA,aACA,WAC2B;CAC3B,MAAM,WAAW,UAAU,IAAI,IAAI;AACnC,KAAI,UAAU;AACb,OAAK,MAAM,UAAU,aAAa;GACjC,MAAM,YAAY,SAAS,SAAS,IAAI,OAAO;AAC/C,OAAI,UACH,QAAO;;AAGT,MAAI,SAAS,UACZ,QAAO,SAAS;;AAIlB,MAAK,MAAM,UAAU,YACpB,KAAI,UAAU,gBACb,QAAO,gBAAgB,QAAyB;AAIlD,QAAO,gBAAgB,GAAG;;;;;;AAO3B,SAAgB,gBACf,KACA,SACA,WACA,SACA,OACS;AACT,KAAI,OAAO,YAAY,WACtB,QAAO,QAAQ;EACH;EACX;EACA;EACA,CAAC;AAGH,QAAO"}
|
package/support/utils/time.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//#region src/support/utils/time.d.ts
|
|
2
2
|
/**
|
|
3
3
|
* Friendly relative time formatter used throughout the support widget.
|
|
4
|
+
* Only use this in browser context (components with effects, not during SSR).
|
|
4
5
|
*/
|
|
5
6
|
declare function formatTimeAgo(date: Date | string): string;
|
|
6
7
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time.d.ts","names":[],"sources":["../../../src/support/utils/time.ts"],"sourcesContent":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"time.d.ts","names":[],"sources":["../../../src/support/utils/time.ts"],"sourcesContent":[],"mappings":";;AAIA;;;iBAAgB,aAAA,OAAoB"}
|
package/support/utils/time.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
//#region src/support/utils/time.ts
|
|
2
2
|
/**
|
|
3
3
|
* Friendly relative time formatter used throughout the support widget.
|
|
4
|
+
* Only use this in browser context (components with effects, not during SSR).
|
|
4
5
|
*/
|
|
5
6
|
function formatTimeAgo(date) {
|
|
7
|
+
if (typeof window === "undefined") return "";
|
|
6
8
|
const now = /* @__PURE__ */ new Date();
|
|
7
9
|
const messageDate = typeof date === "string" ? new Date(date) : date;
|
|
8
10
|
const diffMs = now.getTime() - messageDate.getTime();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time.js","names":[],"sources":["../../../src/support/utils/time.ts"],"sourcesContent":["/**\n * Friendly relative time formatter used throughout the support widget.\n */\nexport function formatTimeAgo(date: Date | string): string {\n\tconst now = new Date();\n\tconst messageDate = typeof date === \"string\" ? new Date(date) : date;\n\tconst diffMs = now.getTime() - messageDate.getTime();\n\tconst diffMins = Math.floor(diffMs / (1000 * 60));\n\tconst diffHours = Math.floor(diffMs / (1000 * 60 * 60));\n\tconst diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));\n\n\tif (diffMins < 1) {\n\t\treturn \"now\";\n\t}\n\tif (diffMins < 60) {\n\t\treturn `${diffMins}m ago`;\n\t}\n\tif (diffHours < 24) {\n\t\treturn `${diffHours}h ago`;\n\t}\n\tif (diffDays === 1) {\n\t\treturn \"Yesterday\";\n\t}\n\tif (diffDays < 7) {\n\t\treturn `${diffDays} days ago`;\n\t}\n\tif (diffDays < 30) {\n\t\tconst weeks = Math.floor(diffDays / 7);\n\t\treturn `${weeks} ${weeks === 1 ? \"week\" : \"weeks\"} ago`;\n\t}\n\tif (diffDays < 365) {\n\t\tconst months = Math.floor(diffDays / 30);\n\t\treturn `${months} ${months === 1 ? \"month\" : \"months\"} ago`;\n\t}\n\tconst years = Math.floor(diffDays / 365);\n\treturn `${years} ${years === 1 ? \"year\" : \"years\"} ago`;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"time.js","names":[],"sources":["../../../src/support/utils/time.ts"],"sourcesContent":["/**\n * Friendly relative time formatter used throughout the support widget.\n * Only use this in browser context (components with effects, not during SSR).\n */\nexport function formatTimeAgo(date: Date | string): string {\n\t// Guard against SSR - return empty string or static fallback\n\tif (typeof window === \"undefined\") {\n\t\treturn \"\";\n\t}\n\n\tconst now = new Date();\n\tconst messageDate = typeof date === \"string\" ? new Date(date) : date;\n\tconst diffMs = now.getTime() - messageDate.getTime();\n\tconst diffMins = Math.floor(diffMs / (1000 * 60));\n\tconst diffHours = Math.floor(diffMs / (1000 * 60 * 60));\n\tconst diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));\n\n\tif (diffMins < 1) {\n\t\treturn \"now\";\n\t}\n\tif (diffMins < 60) {\n\t\treturn `${diffMins}m ago`;\n\t}\n\tif (diffHours < 24) {\n\t\treturn `${diffHours}h ago`;\n\t}\n\tif (diffDays === 1) {\n\t\treturn \"Yesterday\";\n\t}\n\tif (diffDays < 7) {\n\t\treturn `${diffDays} days ago`;\n\t}\n\tif (diffDays < 30) {\n\t\tconst weeks = Math.floor(diffDays / 7);\n\t\treturn `${weeks} ${weeks === 1 ? \"week\" : \"weeks\"} ago`;\n\t}\n\tif (diffDays < 365) {\n\t\tconst months = Math.floor(diffDays / 30);\n\t\treturn `${months} ${months === 1 ? \"month\" : \"months\"} ago`;\n\t}\n\tconst years = Math.floor(diffDays / 365);\n\treturn `${years} ${years === 1 ? \"year\" : \"years\"} ago`;\n}\n"],"mappings":";;;;;AAIA,SAAgB,cAAc,MAA6B;AAE1D,KAAI,OAAO,WAAW,YACrB,QAAO;CAGR,MAAM,sBAAM,IAAI,MAAM;CACtB,MAAM,cAAc,OAAO,SAAS,WAAW,IAAI,KAAK,KAAK,GAAG;CAChE,MAAM,SAAS,IAAI,SAAS,GAAG,YAAY,SAAS;CACpD,MAAM,WAAW,KAAK,MAAM,UAAU,MAAO,IAAI;CACjD,MAAM,YAAY,KAAK,MAAM,UAAU,MAAO,KAAK,IAAI;CACvD,MAAM,WAAW,KAAK,MAAM,UAAU,MAAO,KAAK,KAAK,IAAI;AAE3D,KAAI,WAAW,EACd,QAAO;AAER,KAAI,WAAW,GACd,QAAO,GAAG,SAAS;AAEpB,KAAI,YAAY,GACf,QAAO,GAAG,UAAU;AAErB,KAAI,aAAa,EAChB,QAAO;AAER,KAAI,WAAW,EACd,QAAO,GAAG,SAAS;AAEpB,KAAI,WAAW,IAAI;EAClB,MAAM,QAAQ,KAAK,MAAM,WAAW,EAAE;AACtC,SAAO,GAAG,MAAM,GAAG,UAAU,IAAI,SAAS,QAAQ;;AAEnD,KAAI,WAAW,KAAK;EACnB,MAAM,SAAS,KAAK,MAAM,WAAW,GAAG;AACxC,SAAO,GAAG,OAAO,GAAG,WAAW,IAAI,UAAU,SAAS;;CAEvD,MAAM,QAAQ,KAAK,MAAM,WAAW,IAAI;AACxC,QAAO,GAAG,MAAM,GAAG,UAAU,IAAI,SAAS,QAAQ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-render-element.d.ts","names":[],"sources":["../../src/utils/use-render-element.tsx"],"sourcesContent":[],"mappings":";;;;AAI+B,KAE1B,YAAA,GAAY,MAAS,GAAA,CAAI,iBAAA;AAAiB,KAE1C,SAAA,CAAA,KAAS,CAAA,GAAA,MAAA,
|
|
1
|
+
{"version":3,"file":"use-render-element.d.ts","names":[],"sources":["../../src/utils/use-render-element.tsx"],"sourcesContent":[],"mappings":";;;;AAI+B,KAE1B,YAAA,GAAY,MAAS,GAAA,CAAI,iBAAA;AAAiB,KAE1C,SAAA,CAAA,KAAS,CAAA,GAAA,MAAA,GAA4B,CAAA,CAAA,KAAK,EAAL,KAAK,EAAA,GAAA,MAAA,CAAA;AAAA,KAE1C,QAAA,CAAA,KAAQ,EAAA,KAAA,CAAA,GAAA,CAAA,KAAA,EACL,KADK,EAAA,KAAA,EAEL,KAFK,EAAA,GAGR,OAAA,CAAM,YAHE;KAKR,WAJG,CAAA,KAAA,EAAA,YAI4B,YAJ5B,CAAA,GAAA;EACA,MAAA,CAAA,EAIE,OAAA,CAAM,YAJR,GAIuB,QAJvB,CAIgC,GAAA,CAAI,iBAJpC,CAIsD,GAJtD,CAAA,EAI4D,KAJ5D,CAAA;EACH,SAAM,CAAA,EAIE,SAJF,CAIY,KAJZ,CAAA;EAAY,OAAA,CAAA,EAAA,OAAA;AAAA,CAAA;KAQlB,YAN+B,CAAA,KAAA,EAAA,YAMC,YAND,CAAA,GAAA;EAC1B,KAAM,CAAA,EAMP,KANO;EAAwB,GAAI,CAAA,EAOrC,OAAA,CAAM,GAP+B,CAAA,GAAA,CAAA;EAAkB,KAAA,CAAA,EAQrD,OARqD,CAQ7C,GAAA,CAAI,iBARyC,CAQvB,GARuB,CAAA,CAAA;EAAM,OAAA,CAAA,EAAA,OAAA;CAArC;;;;AAK1B,iBAyBW,gBAzBC,CAAA,cA0BF,MA1BE,CAAA,MAAA,EAAA,GAAA,CAAA,EAAA,YA2BJ,YA3BI,CAAA,CAAA,GAAA,EA6BX,GA7BW,EAAA,cAAA,EA8BA,WA9BA,CA8BY,KA9BZ,EA8BmB,GA9BnB,CAAA,EAAA,MAAA,CAAA,EA+BP,YA/BO,CA+BM,KA/BN,EA+Ba,GA/Bb,CAAA,CAAA,EAgCd,OAAA,CAAM,YAhCQ,GAAA,IAAA"}
|