@cossistant/react 0.0.28 → 0.0.30

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.
Files changed (301) hide show
  1. package/README.md +1 -1
  2. package/_virtual/rolldown_runtime.js +9 -23
  3. package/hooks/index.d.ts +3 -3
  4. package/hooks/private/store/use-conversations-store.d.ts +2 -0
  5. package/hooks/private/store/use-conversations-store.d.ts.map +1 -1
  6. package/hooks/private/store/use-conversations-store.js +15 -8
  7. package/hooks/private/store/use-conversations-store.js.map +1 -1
  8. package/hooks/private/store/use-store-selector.d.ts +3 -0
  9. package/hooks/private/store/use-store-selector.d.ts.map +1 -1
  10. package/hooks/private/store/use-store-selector.js +4 -8
  11. package/hooks/private/store/use-store-selector.js.map +1 -1
  12. package/hooks/private/store/use-website-store.d.ts +3 -1
  13. package/hooks/private/store/use-website-store.d.ts.map +1 -1
  14. package/hooks/private/store/use-website-store.js +14 -6
  15. package/hooks/private/store/use-website-store.js.map +1 -1
  16. package/hooks/private/use-client-query.d.ts +1 -1
  17. package/hooks/private/use-client-query.d.ts.map +1 -1
  18. package/hooks/private/use-client-query.js +1 -0
  19. package/hooks/private/use-client-query.js.map +1 -1
  20. package/hooks/private/use-default-messages.d.ts +1 -1
  21. package/hooks/private/use-grouped-messages.d.ts +10 -7
  22. package/hooks/private/use-grouped-messages.d.ts.map +1 -1
  23. package/hooks/private/use-grouped-messages.js +44 -11
  24. package/hooks/private/use-grouped-messages.js.map +1 -1
  25. package/hooks/private/use-rest-client.d.ts +13 -3
  26. package/hooks/private/use-rest-client.d.ts.map +1 -1
  27. package/hooks/private/use-rest-client.js +49 -22
  28. package/hooks/private/use-rest-client.js.map +1 -1
  29. package/hooks/private/use-visitor-typing-reporter.d.ts +1 -1
  30. package/hooks/use-conversation-auto-seen.d.ts +1 -1
  31. package/hooks/use-conversation-page.d.ts +1 -1
  32. package/hooks/use-conversation-page.d.ts.map +1 -1
  33. package/hooks/use-conversation-page.js +10 -3
  34. package/hooks/use-conversation-page.js.map +1 -1
  35. package/hooks/use-conversation-preview.d.ts +3 -1
  36. package/hooks/use-conversation-preview.d.ts.map +1 -1
  37. package/hooks/use-conversation-preview.js +6 -3
  38. package/hooks/use-conversation-preview.js.map +1 -1
  39. package/hooks/use-conversation-seen.d.ts +1 -1
  40. package/hooks/use-conversation-seen.js +1 -1
  41. package/hooks/use-conversation-seen.js.map +1 -1
  42. package/hooks/use-conversation-timeline-items.d.ts +1 -1
  43. package/hooks/use-conversation-timeline-items.js +2 -3
  44. package/hooks/use-conversation-timeline-items.js.map +1 -1
  45. package/hooks/use-conversation-timeline.d.ts +1 -1
  46. package/hooks/use-conversation-timeline.d.ts.map +1 -1
  47. package/hooks/use-conversation-timeline.js +1 -3
  48. package/hooks/use-conversation-timeline.js.map +1 -1
  49. package/hooks/use-conversation.d.ts +1 -1
  50. package/hooks/use-conversation.js +2 -3
  51. package/hooks/use-conversation.js.map +1 -1
  52. package/hooks/use-conversations.d.ts +1 -1
  53. package/hooks/use-conversations.js +5 -3
  54. package/hooks/use-conversations.js.map +1 -1
  55. package/hooks/use-create-conversation.d.ts +3 -3
  56. package/hooks/use-create-conversation.js +1 -0
  57. package/hooks/use-create-conversation.js.map +1 -1
  58. package/hooks/use-file-upload.d.ts +1 -1
  59. package/hooks/use-file-upload.js +3 -3
  60. package/hooks/use-file-upload.js.map +1 -1
  61. package/hooks/use-home-page.js +3 -3
  62. package/hooks/use-home-page.js.map +1 -1
  63. package/hooks/use-message-composer.d.ts +10 -3
  64. package/hooks/use-message-composer.d.ts.map +1 -1
  65. package/hooks/use-message-composer.js +5 -2
  66. package/hooks/use-message-composer.js.map +1 -1
  67. package/hooks/use-realtime-support.d.ts +1 -1
  68. package/hooks/use-send-message.d.ts +8 -2
  69. package/hooks/use-send-message.d.ts.map +1 -1
  70. package/hooks/use-send-message.js +5 -3
  71. package/hooks/use-send-message.js.map +1 -1
  72. package/hooks/use-visitor.js +2 -2
  73. package/hooks/use-visitor.js.map +1 -1
  74. package/identify-visitor.d.ts.map +1 -1
  75. package/identify-visitor.js +15 -1
  76. package/identify-visitor.js.map +1 -1
  77. package/index.d.ts +3 -3
  78. package/index.js +1 -1
  79. package/package.json +5 -3
  80. package/{conversation.d.ts → packages/types/src/api/conversation.d.ts} +368 -64
  81. package/packages/types/src/api/conversation.d.ts.map +1 -0
  82. package/packages/types/src/api/timeline-item.d.ts +460 -0
  83. package/packages/types/src/api/timeline-item.d.ts.map +1 -0
  84. package/packages/types/src/realtime-events.d.ts +1004 -0
  85. package/packages/types/src/realtime-events.d.ts.map +1 -0
  86. package/{schemas3.d.ts → packages/types/src/schemas.d.ts} +95 -19
  87. package/packages/types/src/schemas.d.ts.map +1 -0
  88. package/primitives/avatar/avatar.js +1 -1
  89. package/primitives/avatar/avatar.js.map +1 -1
  90. package/primitives/avatar/fallback.d.ts.map +1 -1
  91. package/primitives/avatar/fallback.js +2 -2
  92. package/primitives/avatar/fallback.js.map +1 -1
  93. package/primitives/avatar/image.d.ts +1 -1
  94. package/primitives/avatar/image.js +1 -1
  95. package/primitives/avatar/image.js.map +1 -1
  96. package/primitives/button.js +1 -1
  97. package/primitives/button.js.map +1 -1
  98. package/primitives/conversation-timeline.d.ts +1 -1
  99. package/primitives/conversation-timeline.js +4 -4
  100. package/primitives/conversation-timeline.js.map +1 -1
  101. package/primitives/day-separator.d.ts +76 -0
  102. package/primitives/day-separator.d.ts.map +1 -0
  103. package/primitives/day-separator.js +111 -0
  104. package/primitives/day-separator.js.map +1 -0
  105. package/primitives/index.d.ts +3 -2
  106. package/primitives/index.js +6 -1
  107. package/primitives/index.parts.d.ts +2 -1
  108. package/primitives/index.parts.js +2 -1
  109. package/primitives/multimodal-input.d.ts +2 -2
  110. package/primitives/multimodal-input.d.ts.map +1 -1
  111. package/primitives/multimodal-input.js +2 -2
  112. package/primitives/multimodal-input.js.map +1 -1
  113. package/primitives/timeline-item-attachments.d.ts +1 -1
  114. package/primitives/timeline-item-attachments.js +6 -7
  115. package/primitives/timeline-item-attachments.js.map +1 -1
  116. package/primitives/timeline-item-group.d.ts +1 -1
  117. package/primitives/timeline-item-group.d.ts.map +1 -1
  118. package/primitives/timeline-item-group.js +8 -8
  119. package/primitives/timeline-item-group.js.map +1 -1
  120. package/primitives/timeline-item.d.ts +1 -1
  121. package/primitives/timeline-item.d.ts.map +1 -1
  122. package/primitives/timeline-item.js +33 -8
  123. package/primitives/timeline-item.js.map +1 -1
  124. package/primitives/trigger.js +1 -1
  125. package/primitives/trigger.js.map +1 -1
  126. package/primitives/window.js +1 -1
  127. package/primitives/window.js.map +1 -1
  128. package/provider.d.ts +4 -2
  129. package/provider.d.ts.map +1 -1
  130. package/provider.js +56 -8
  131. package/provider.js.map +1 -1
  132. package/realtime/event-filter.d.ts +4 -1
  133. package/realtime/event-filter.d.ts.map +1 -1
  134. package/realtime/event-filter.js +14 -0
  135. package/realtime/event-filter.js.map +1 -1
  136. package/realtime/provider.d.ts +1 -1
  137. package/realtime/provider.d.ts.map +1 -1
  138. package/realtime/provider.js +1 -2
  139. package/realtime/provider.js.map +1 -1
  140. package/realtime/seen-store.d.ts +2 -2
  141. package/realtime/support-provider.js +6 -1
  142. package/realtime/support-provider.js.map +1 -1
  143. package/realtime/typing-store.d.ts +1 -1
  144. package/realtime/use-realtime.d.ts +1 -1
  145. package/support/components/avatar-stack.d.ts.map +1 -1
  146. package/support/components/avatar-stack.js +32 -12
  147. package/support/components/avatar-stack.js.map +1 -1
  148. package/support/components/avatar.d.ts +34 -3
  149. package/support/components/avatar.d.ts.map +1 -1
  150. package/support/components/avatar.js +61 -8
  151. package/support/components/avatar.js.map +1 -1
  152. package/support/components/button.d.ts +4 -2
  153. package/support/components/button.d.ts.map +1 -1
  154. package/support/components/button.js +3 -3
  155. package/support/components/button.js.map +1 -1
  156. package/support/components/configuration-error.d.ts +16 -0
  157. package/support/components/configuration-error.d.ts.map +1 -0
  158. package/support/components/configuration-error.js +162 -0
  159. package/support/components/configuration-error.js.map +1 -0
  160. package/support/components/content.js +1 -2
  161. package/support/components/content.js.map +1 -1
  162. package/support/components/conversation-button-link.js +18 -23
  163. package/support/components/conversation-button-link.js.map +1 -1
  164. package/support/components/conversation-event.d.ts.map +1 -1
  165. package/support/components/conversation-event.js +7 -5
  166. package/support/components/conversation-event.js.map +1 -1
  167. package/support/components/conversation-timeline.d.ts +6 -1
  168. package/support/components/conversation-timeline.d.ts.map +1 -1
  169. package/support/components/conversation-timeline.js +22 -2
  170. package/support/components/conversation-timeline.js.map +1 -1
  171. package/support/components/header.js +1 -1
  172. package/support/components/image-lightbox.d.ts +1 -1
  173. package/support/components/image-lightbox.js +1 -2
  174. package/support/components/image-lightbox.js.map +1 -1
  175. package/support/components/index.js +1 -1
  176. package/support/components/multimodal-input.js +0 -1
  177. package/support/components/multimodal-input.js.map +1 -1
  178. package/support/components/navigation-tab.js +1 -1
  179. package/support/components/online-indicator.d.ts +50 -0
  180. package/support/components/online-indicator.d.ts.map +1 -0
  181. package/support/components/online-indicator.js +65 -0
  182. package/support/components/online-indicator.js.map +1 -0
  183. package/support/components/root.js +0 -1
  184. package/support/components/root.js.map +1 -1
  185. package/support/components/timeline-identification-tool.js +4 -4
  186. package/support/components/timeline-identification-tool.js.map +1 -1
  187. package/support/components/timeline-message-group.d.ts +1 -1
  188. package/support/components/timeline-message-group.d.ts.map +1 -1
  189. package/support/components/timeline-message-group.js +6 -4
  190. package/support/components/timeline-message-group.js.map +1 -1
  191. package/support/components/timeline-message-item.d.ts +1 -1
  192. package/support/components/timeline-message-item.js +4 -4
  193. package/support/components/timeline-message-item.js.map +1 -1
  194. package/support/components/trigger.js +1 -2
  195. package/support/components/trigger.js.map +1 -1
  196. package/support/components/typing-indicator.js +1 -1
  197. package/support/components/typing-indicator.js.map +1 -1
  198. package/support/context/controlled-state.js +0 -1
  199. package/support/context/controlled-state.js.map +1 -1
  200. package/support/context/events.d.ts +1 -1
  201. package/support/context/events.js +0 -1
  202. package/support/context/events.js.map +1 -1
  203. package/support/context/handle.js +0 -1
  204. package/support/context/handle.js.map +1 -1
  205. package/support/context/identification.d.ts +33 -0
  206. package/support/context/identification.d.ts.map +1 -0
  207. package/support/context/identification.js +34 -0
  208. package/support/context/identification.js.map +1 -0
  209. package/support/context/positioning.js +0 -1
  210. package/support/context/positioning.js.map +1 -1
  211. package/support/context/slots.js +0 -1
  212. package/support/context/slots.js.map +1 -1
  213. package/support/context/websocket.d.ts +1 -1
  214. package/support/context/websocket.js +0 -1
  215. package/support/context/websocket.js.map +1 -1
  216. package/support/index.d.ts.map +1 -1
  217. package/support/index.js +51 -18
  218. package/support/index.js.map +1 -1
  219. package/support/pages/conversation-history.js +2 -1
  220. package/support/pages/conversation-history.js.map +1 -1
  221. package/support/pages/conversation.d.ts +1 -1
  222. package/support/pages/conversation.js +1 -1
  223. package/support/pages/conversation.js.map +1 -1
  224. package/support/pages/home.js +5 -3
  225. package/support/pages/home.js.map +1 -1
  226. package/support/router.d.ts.map +1 -1
  227. package/support/router.js +4 -0
  228. package/support/router.js.map +1 -1
  229. package/support/store/support-store.js +0 -1
  230. package/support/store/support-store.js.map +1 -1
  231. package/support/{support-C7Xaw-N6.css → support-DmViRaga.css} +2 -2
  232. package/support/{support-C7Xaw-N6.css.map → support-DmViRaga.css.map} +1 -1
  233. package/support/text/index.d.ts +1 -1
  234. package/support/text/index.d.ts.map +1 -1
  235. package/support/text/index.js +1 -1
  236. package/support/text/index.js.map +1 -1
  237. package/support/text/locales/en.js +1 -1
  238. package/support/text/locales/en.js.map +1 -1
  239. package/support/text/locales/es.js +1 -1
  240. package/support/text/locales/es.js.map +1 -1
  241. package/support/text/locales/fr.js +1 -1
  242. package/support/text/locales/fr.js.map +1 -1
  243. package/support/utils/index.d.ts +1 -1
  244. package/support-config.js +0 -1
  245. package/support-config.js.map +1 -1
  246. package/support.css +1 -1
  247. package/tailwind.css +1 -1
  248. package/utils/conversation.d.ts.map +1 -1
  249. package/utils/conversation.js +1 -3
  250. package/utils/conversation.js.map +1 -1
  251. package/utils/use-render-element.d.ts.map +1 -1
  252. package/utils/use-render-element.js +20 -5
  253. package/utils/use-render-element.js.map +1 -1
  254. package/api.d.ts +0 -71
  255. package/api.d.ts.map +0 -1
  256. package/checks.d.ts +0 -189
  257. package/checks.d.ts.map +0 -1
  258. package/clsx.d.ts +0 -7
  259. package/clsx.d.ts.map +0 -1
  260. package/coerce.d.ts +0 -9
  261. package/coerce.d.ts.map +0 -1
  262. package/conversation.d.ts.map +0 -1
  263. package/core.d.ts +0 -35
  264. package/core.d.ts.map +0 -1
  265. package/errors.d.ts +0 -121
  266. package/errors.d.ts.map +0 -1
  267. package/errors2.d.ts +0 -24
  268. package/errors2.d.ts.map +0 -1
  269. package/index2.d.ts +0 -4
  270. package/index3.d.ts +0 -1
  271. package/metadata.d.ts +0 -1
  272. package/openapi-generator.d.ts +0 -1
  273. package/openapi-generator2.d.ts +0 -1
  274. package/openapi-generator3.d.ts +0 -1
  275. package/openapi30.d.ts +0 -125
  276. package/openapi30.d.ts.map +0 -1
  277. package/openapi31.d.ts +0 -131
  278. package/openapi31.d.ts.map +0 -1
  279. package/parse.d.ts +0 -17
  280. package/parse.d.ts.map +0 -1
  281. package/realtime-events.d.ts +0 -482
  282. package/realtime-events.d.ts.map +0 -1
  283. package/registries.d.ts +0 -32
  284. package/registries.d.ts.map +0 -1
  285. package/schemas.d.ts +0 -673
  286. package/schemas.d.ts.map +0 -1
  287. package/schemas2.d.ts +0 -320
  288. package/schemas2.d.ts.map +0 -1
  289. package/schemas3.d.ts.map +0 -1
  290. package/specification-extension.d.ts +0 -9
  291. package/specification-extension.d.ts.map +0 -1
  292. package/standard-schema.d.ts +0 -59
  293. package/standard-schema.d.ts.map +0 -1
  294. package/timeline-item.d.ts +0 -227
  295. package/timeline-item.d.ts.map +0 -1
  296. package/util.d.ts +0 -41
  297. package/util.d.ts.map +0 -1
  298. package/versions.d.ts +0 -9
  299. package/versions.d.ts.map +0 -1
  300. package/zod-extensions.d.ts +0 -39
  301. package/zod-extensions.d.ts.map +0 -1
@@ -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 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\tconst seenTime = getTimestamp(seen.lastSeenAt);\n\t\tconst viewerId = seen.userId || seen.visitorId || seen.aiAgentId;\n\t\tif (!viewerId) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet lastReadItem: TimelineItem | null = null;\n\t\tlet unreadCount = 0;\n\n\t\t// Process items in chronological order\n\t\tfor (const item of sortedItems) {\n\t\t\tconst itemTime = getTimestamp(item.createdAt);\n\n\t\t\tif (itemTime <= seenTime) {\n\t\t\t\t// This item has been seen\n\t\t\t\tif (item.id) {\n\t\t\t\t\tconst seenBy = seenByMap.get(item.id);\n\t\t\t\t\tif (seenBy) {\n\t\t\t\t\t\tseenBy.add(viewerId);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlastReadItem = item;\n\t\t\t} else {\n\t\t\t\t// This item is unread\n\t\t\t\tunreadCount++;\n\t\t\t}\n\t\t}\n\n\t\t// Store the last read item for this viewer\n\t\tif (lastReadItem?.id) {\n\t\t\tlastReadMessageMap.set(viewerId, lastReadItem.id);\n\t\t}\n\n\t\t// Store unread count\n\t\tunreadCountMap.set(viewerId, unreadCount);\n\t}\n\n\treturn { seenByMap, lastReadMessageMap, unreadCountMap };\n};\n\n/**\n * Batches sequential timeline items from the same sender into groups and enriches\n * them with read-receipt helpers so UIs can render conversation timelines with\n * minimal effort. Seen data is normalised into quick lookup maps for unread\n * indicators.\n */\nexport const useGroupedMessages = ({\n\titems,\n\tseenData = [],\n\tcurrentViewerId,\n\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","currentDayString: string | null","lastReadItem: TimelineItem | null"],"sources":["../../../src/hooks/private/use-grouped-messages.ts"],"sourcesContent":["import { SenderType } from \"@cossistant/types\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport type { ConversationSeen } from \"@cossistant/types/schemas\";\nimport { useMemo } from \"react\";\n\nexport type GroupedMessage = {\n\ttype: \"message_group\";\n\tsenderId: string;\n\tsenderType: SenderType;\n\titems: TimelineItem[];\n\tfirstMessageId: string;\n\tlastMessageId: string;\n\tfirstMessageTime: Date;\n\tlastMessageTime: Date;\n};\n\nexport type TimelineEventItem = {\n\ttype: \"timeline_event\";\n\titem: TimelineItem;\n\ttimestamp: Date;\n};\n\nexport type TimelineToolItem = {\n\ttype: \"timeline_tool\";\n\titem: TimelineItem;\n\ttool: string | null;\n\ttimestamp: Date;\n};\n\nexport type DaySeparatorItem = {\n\ttype: \"day_separator\";\n\tdate: Date;\n\tdateString: string; // ISO date string (YYYY-MM-DD) for stable keys\n};\n\nexport type ConversationItem =\n\t| GroupedMessage\n\t| TimelineEventItem\n\t| TimelineToolItem\n\t| DaySeparatorItem;\n\nexport type UseGroupedMessagesOptions = {\n\titems: TimelineItem[];\n\tseenData?: ConversationSeen[];\n\tcurrentViewerId?: string; // The ID of the current viewer (visitor, user, or AI agent)\n};\n\nexport type UseGroupedMessagesProps = UseGroupedMessagesOptions;\n\n// Helper function to safely get timestamp from Date or string\nconst getTimestamp = (date: Date | string | null | undefined): number => {\n\tif (!date) {\n\t\treturn 0;\n\t}\n\tif (typeof date === \"string\") {\n\t\treturn new Date(date).getTime();\n\t}\n\treturn date.getTime();\n};\n\n// Helper function to safely convert to Date\nconst toDate = (date: Date | string | null | undefined): Date => {\n\tif (!date) {\n\t\treturn typeof window !== \"undefined\" ? new Date() : new Date(0);\n\t}\n\tif (typeof date === \"string\") {\n\t\treturn new Date(date);\n\t}\n\treturn date;\n};\n\n// Helper to extract the date string (YYYY-MM-DD) from a Date for day comparison\nconst getDateString = (date: Date): string => {\n\tconst year = date.getFullYear();\n\tconst month = String(date.getMonth() + 1).padStart(2, \"0\");\n\tconst day = String(date.getDate()).padStart(2, \"0\");\n\treturn `${year}-${month}-${day}`;\n};\n\n// Helper to create a Date at midnight for a given date string\nconst createDayDate = (dateString: string): Date => {\n\tconst [year, month, day] = dateString.split(\"-\").map(Number);\n\treturn new Date(year ?? 0, (month ?? 1) - 1, day ?? 1, 0, 0, 0, 0);\n};\n\n// Helper to determine sender ID and type from a timeline item\nconst getSenderIdAndTypeFromTimelineItem = (\n\titem: TimelineItem\n): { senderId: string; senderType: SenderType } => {\n\tif (item.visitorId) {\n\t\treturn { senderId: item.visitorId, senderType: SenderType.VISITOR };\n\t}\n\tif (item.aiAgentId) {\n\t\treturn { senderId: item.aiAgentId, senderType: SenderType.AI };\n\t}\n\tif (item.userId) {\n\t\treturn { senderId: item.userId, senderType: SenderType.TEAM_MEMBER };\n\t}\n\n\t// Fallback\n\treturn {\n\t\tsenderId: item.id || \"default-sender\",\n\t\tsenderType: SenderType.TEAM_MEMBER,\n\t};\n};\n\nconst EMPTY_STRING_ARRAY: readonly string[] = Object.freeze([]);\n\n// Helper function to group timeline items (messages only, events stay separate)\n// Also inserts day separators when the day changes between items\nconst groupTimelineItems = (items: TimelineItem[]): ConversationItem[] => {\n\tconst result: ConversationItem[] = [];\n\tlet currentGroup: GroupedMessage | null = null;\n\tlet currentDayString: string | null = null;\n\n\tconst maybeInsertDaySeparator = (itemDate: Date): void => {\n\t\tconst itemDayString = getDateString(itemDate);\n\n\t\tif (currentDayString !== itemDayString) {\n\t\t\t// Finalize any existing group before inserting day separator\n\t\t\tif (currentGroup) {\n\t\t\t\tresult.push(currentGroup);\n\t\t\t\tcurrentGroup = null;\n\t\t\t}\n\n\t\t\t// Insert day separator\n\t\t\tresult.push({\n\t\t\t\ttype: \"day_separator\",\n\t\t\t\tdate: createDayDate(itemDayString),\n\t\t\t\tdateString: itemDayString,\n\t\t\t});\n\n\t\t\tcurrentDayString = itemDayString;\n\t\t}\n\t};\n\n\tfor (const item of items) {\n\t\tconst itemDate = toDate(item.createdAt);\n\n\t\t// Check for day boundary before processing any item\n\t\tmaybeInsertDaySeparator(itemDate);\n\n\t\t// Events don't get grouped\n\t\tif (item.type === \"event\") {\n\t\t\t// Finalize any existing group\n\t\t\tif (currentGroup) {\n\t\t\t\tresult.push(currentGroup);\n\t\t\t\tcurrentGroup = null;\n\t\t\t}\n\n\t\t\t// Add event as standalone item\n\t\t\tresult.push({\n\t\t\t\ttype: \"timeline_event\",\n\t\t\t\titem,\n\t\t\t\ttimestamp: itemDate,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (item.type === \"identification\") {\n\t\t\t// Finalize any existing group\n\t\t\tif (currentGroup) {\n\t\t\t\tresult.push(currentGroup);\n\t\t\t\tcurrentGroup = null;\n\t\t\t}\n\n\t\t\t// Add tool item as standalone entry\n\t\t\tresult.push({\n\t\t\t\ttype: \"timeline_tool\",\n\t\t\t\titem,\n\t\t\t\ttool: item.tool ?? null,\n\t\t\t\ttimestamp: itemDate,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Group messages by sender\n\t\tconst { senderId, senderType } = getSenderIdAndTypeFromTimelineItem(item);\n\n\t\tif (currentGroup && currentGroup.senderId === senderId) {\n\t\t\t// Add to existing group (day boundary already handled above)\n\t\t\tcurrentGroup.items.push(item);\n\t\t\tcurrentGroup.lastMessageId = item.id || currentGroup.lastMessageId;\n\t\t\tcurrentGroup.lastMessageTime = itemDate;\n\t\t} else {\n\t\t\t// Finalize previous group if exists\n\t\t\tif (currentGroup) {\n\t\t\t\tresult.push(currentGroup);\n\t\t\t}\n\n\t\t\t// Start new group\n\t\t\tcurrentGroup = {\n\t\t\t\ttype: \"message_group\",\n\t\t\t\tsenderId,\n\t\t\t\tsenderType,\n\t\t\t\titems: [item],\n\t\t\t\tfirstMessageId: item.id || \"\",\n\t\t\t\tlastMessageId: item.id || \"\",\n\t\t\t\tfirstMessageTime: itemDate,\n\t\t\t\tlastMessageTime: itemDate,\n\t\t\t};\n\t\t}\n\t}\n\n\tif (currentGroup) {\n\t\tresult.push(currentGroup);\n\t}\n\n\treturn result;\n};\n\n// Build read receipt data for timeline items\n// Accepts pre-sorted message items for performance\nconst buildTimelineReadReceiptData = (\n\tseenData: ConversationSeen[],\n\titems: TimelineItem[],\n\tsortedMessageItems: TimelineItem[]\n) => {\n\tconst seenByMap = new Map<string, Set<string>>();\n\tconst lastReadMessageMap = new Map<string, string>();\n\tconst unreadCountMap = new Map<string, number>();\n\n\t// Initialize map for all message-type timeline items\n\tfor (const item of items) {\n\t\tif (item.type === \"message\" && item.id) {\n\t\t\tseenByMap.set(item.id, new Set());\n\t\t}\n\t}\n\n\t// Process seen data for each viewer\n\tfor (const seen of seenData) {\n\t\tconst seenTime = getTimestamp(seen.lastSeenAt);\n\t\tconst viewerId = seen.userId || seen.visitorId || seen.aiAgentId;\n\t\tif (!viewerId) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet lastReadItem: TimelineItem | null = null;\n\t\tlet unreadCount = 0;\n\n\t\t// Process items in chronological order (using pre-sorted array)\n\t\tfor (const item of sortedMessageItems) {\n\t\t\tconst itemTime = getTimestamp(item.createdAt);\n\n\t\t\tif (itemTime <= seenTime) {\n\t\t\t\t// This item has been seen\n\t\t\t\tif (item.id) {\n\t\t\t\t\tconst seenBy = seenByMap.get(item.id);\n\t\t\t\t\tif (seenBy) {\n\t\t\t\t\t\tseenBy.add(viewerId);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlastReadItem = item;\n\t\t\t} else {\n\t\t\t\t// This item is unread\n\t\t\t\tunreadCount++;\n\t\t\t}\n\t\t}\n\n\t\t// Store the last read item for this viewer\n\t\tif (lastReadItem?.id) {\n\t\t\tlastReadMessageMap.set(viewerId, lastReadItem.id);\n\t\t}\n\n\t\t// Store unread count\n\t\tunreadCountMap.set(viewerId, unreadCount);\n\t}\n\n\treturn { seenByMap, lastReadMessageMap, unreadCountMap };\n};\n\n/**\n * Batches sequential timeline items from the same sender into groups and enriches\n * them with read-receipt helpers so UIs can render conversation timelines with\n * minimal effort. Seen data is normalised into quick lookup maps for unread\n * indicators.\n */\nexport const useGroupedMessages = ({\n\titems,\n\tseenData = [],\n\tcurrentViewerId,\n}: UseGroupedMessagesOptions) => {\n\treturn useMemo(() => {\n\t\tconst groupedItems = groupTimelineItems(items);\n\n\t\t// Pre-sort message items once for reuse (performance optimization)\n\t\tconst sortedMessageItems = items\n\t\t\t.filter((item) => item.type === \"message\")\n\t\t\t.sort((a, b) => getTimestamp(a.createdAt) - getTimestamp(b.createdAt));\n\n\t\t// Build index map from sorted items for O(1) chronological lookups\n\t\t// Must use sortedMessageItems (not raw items) to ensure indices reflect time order\n\t\tconst messageIndexMap = new Map<string, number>();\n\t\tfor (let i = 0; i < sortedMessageItems.length; i++) {\n\t\t\tconst item = sortedMessageItems[i];\n\t\t\tif (item?.id) {\n\t\t\t\tmessageIndexMap.set(item.id, i);\n\t\t\t}\n\t\t}\n\n\t\t// Build read receipt data with pre-sorted items\n\t\tconst { seenByMap, lastReadMessageMap, unreadCountMap } =\n\t\t\tbuildTimelineReadReceiptData(seenData, items, sortedMessageItems);\n\n\t\t// Cache for turning seen sets into stable arrays across renders\n\t\tconst seenByArrayCache = new Map<string, readonly string[]>();\n\n\t\treturn {\n\t\t\titems: groupedItems,\n\t\t\tseenByMap,\n\t\t\tlastReadMessageMap,\n\t\t\tunreadCountMap,\n\n\t\t\tisMessageSeenByViewer: (messageId: string): boolean => {\n\t\t\t\tif (!currentViewerId) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst seenBy = seenByMap.get(messageId);\n\t\t\t\treturn seenBy ? seenBy.has(currentViewerId) : false;\n\t\t\t},\n\n\t\t\tgetMessageSeenBy: (messageId: string): readonly string[] => {\n\t\t\t\tif (seenByArrayCache.has(messageId)) {\n\t\t\t\t\treturn seenByArrayCache.get(messageId) ?? EMPTY_STRING_ARRAY;\n\t\t\t\t}\n\n\t\t\t\tconst seenBy = seenByMap.get(messageId);\n\t\t\t\tif (!seenBy || seenBy.size === 0) {\n\t\t\t\t\tseenByArrayCache.set(messageId, EMPTY_STRING_ARRAY);\n\t\t\t\t\treturn EMPTY_STRING_ARRAY;\n\t\t\t\t}\n\n\t\t\t\tconst result = Object.freeze(Array.from(seenBy)) as readonly string[];\n\t\t\t\tseenByArrayCache.set(messageId, result);\n\t\t\t\treturn result;\n\t\t\t},\n\n\t\t\tgetLastReadMessageId: (userId: string): string | undefined =>\n\t\t\t\tlastReadMessageMap.get(userId),\n\n\t\t\tisLastReadMessage: (messageId: string, userId: string): boolean =>\n\t\t\t\tlastReadMessageMap.get(userId) === messageId,\n\n\t\t\tgetUnreadCount: (userId: string): number =>\n\t\t\t\tunreadCountMap.get(userId) || 0,\n\n\t\t\thasUnreadAfter: (messageId: string, userId: string): boolean => {\n\t\t\t\tconst lastRead = lastReadMessageMap.get(userId);\n\t\t\t\tif (!lastRead) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\t// Use index map for O(1) lookups instead of findIndex O(n)\n\t\t\t\tconst messageIndex = messageIndexMap.get(messageId);\n\t\t\t\tconst lastReadIndex = messageIndexMap.get(lastRead);\n\n\t\t\t\tif (messageIndex === undefined || lastReadIndex === undefined) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\treturn messageIndex < lastReadIndex;\n\t\t\t},\n\t\t};\n\t}, [items, seenData, currentViewerId]);\n};\n"],"mappings":"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"}
@@ -1,17 +1,27 @@
1
1
  import { CossistantClient } from "@cossistant/core";
2
2
 
3
3
  //#region src/hooks/private/use-rest-client.d.ts
4
+ type ConfigurationError = {
5
+ type: "missing_api_key" | "invalid_api_key";
6
+ message: string;
7
+ envVarName: string;
8
+ };
4
9
  type UseClientResult = {
5
10
  client: CossistantClient;
6
- error: Error | null;
11
+ error: null;
12
+ configurationError: null;
13
+ } | {
14
+ client: null;
15
+ error: null;
16
+ configurationError: ConfigurationError;
7
17
  };
8
18
  /**
9
19
  * Creates a memoised `CossistantClient` instance using the provided endpoints
10
20
  * and public key. When no key is passed the hook falls back to environment
11
21
  * variables and surfaces missing configuration errors through the returned
12
- * `error` field.
22
+ * `configurationError` field instead of throwing.
13
23
  */
14
24
  declare function useClient(publicKey: string | undefined, apiUrl?: string, wsUrl?: string): UseClientResult;
15
25
  //#endregion
16
- export { UseClientResult, useClient };
26
+ export { ConfigurationError, UseClientResult, useClient };
17
27
  //# sourceMappingURL=use-rest-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-rest-client.d.ts","names":[],"sources":["../../../src/hooks/private/use-rest-client.ts"],"sourcesContent":[],"mappings":";;;KAMY,eAAA;UACH;EADG,KAAA,EAEJ,KAFI,GAAA,IAAe;AAW3B,CAAA;;;;;;;iBAAgB,SAAA,kEAIb"}
1
+ {"version":3,"file":"use-rest-client.d.ts","names":[],"sources":["../../../src/hooks/private/use-rest-client.ts"],"sourcesContent":[],"mappings":";;;KAMY,kBAAA;;EAAA,OAAA,EAAA,MAAA;EAMA,UAAA,EAAA,MAAe;AA8B3B,CAAA;KA9BY,eAAA;UAED;;;;;;sBAOY;;;;;;;;iBAqBP,SAAA,kEAIb"}
@@ -1,39 +1,66 @@
1
1
  "use client";
2
2
 
3
-
4
3
  import { useMemo } from "react";
5
4
  import { CossistantClient } from "@cossistant/core";
6
5
 
7
6
  //#region src/hooks/private/use-rest-client.ts
8
7
  /**
8
+ * Detect if running in a Next.js environment.
9
+ */
10
+ function isNextJSEnvironment() {
11
+ if (typeof window !== "undefined") return "__NEXT_DATA__" in window;
12
+ return typeof process !== "undefined" && "__NEXT_RUNTIME" in process.env;
13
+ }
14
+ /**
9
15
  * Creates a memoised `CossistantClient` instance using the provided endpoints
10
16
  * and public key. When no key is passed the hook falls back to environment
11
17
  * variables and surfaces missing configuration errors through the returned
12
- * `error` field.
18
+ * `configurationError` field instead of throwing.
13
19
  */
14
20
  function useClient(publicKey, apiUrl = "https://api.cossistant.com/v1", wsUrl = "wss://api.cossistant.com/ws") {
15
- return {
16
- client: useMemo(() => {
17
- const keyFromEnv = process.env.NEXT_PUBLIC_COSSISTANT_API_KEY || process.env.NEXT_PUBLIC_COSSISTANT_KEY || process.env.COSSISTANT_API_KEY;
18
- const keyToUse = publicKey ?? keyFromEnv;
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
- const config = {
21
- apiUrl,
22
- wsUrl,
23
- publicKey: keyToUse
21
+ return useMemo(() => {
22
+ const keyFromEnv = process.env.NEXT_PUBLIC_COSSISTANT_API_KEY || process.env.COSSISTANT_API_KEY;
23
+ const keyToUse = publicKey ?? keyFromEnv;
24
+ if (!keyToUse) {
25
+ const envVarName = isNextJSEnvironment() ? "NEXT_PUBLIC_COSSISTANT_API_KEY" : "COSSISTANT_API_KEY";
26
+ return {
27
+ client: null,
28
+ error: null,
29
+ configurationError: {
30
+ type: "missing_api_key",
31
+ message: `Public API key is required. Add ${envVarName} to your environment variables.`,
32
+ envVarName
33
+ }
24
34
  };
25
- try {
26
- return new CossistantClient(config);
27
- } catch (err) {
28
- throw err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to initialize Cossistant client");
29
- }
30
- }, [
31
- publicKey,
35
+ }
36
+ const config = {
32
37
  apiUrl,
33
- wsUrl
34
- ]),
35
- error: null
36
- };
38
+ wsUrl,
39
+ publicKey: keyToUse
40
+ };
41
+ try {
42
+ return {
43
+ client: new CossistantClient(config),
44
+ error: null,
45
+ configurationError: null
46
+ };
47
+ } catch (err) {
48
+ const envVarName = isNextJSEnvironment() ? "NEXT_PUBLIC_COSSISTANT_API_KEY" : "COSSISTANT_API_KEY";
49
+ return {
50
+ client: null,
51
+ error: null,
52
+ configurationError: {
53
+ type: "missing_api_key",
54
+ message: err instanceof Error ? err.message : "Failed to initialize Cossistant client",
55
+ envVarName
56
+ }
57
+ };
58
+ }
59
+ }, [
60
+ publicKey,
61
+ apiUrl,
62
+ wsUrl
63
+ ]);
37
64
  }
38
65
 
39
66
  //#endregion
@@ -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_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"}
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 ConfigurationError = {\n\ttype: \"missing_api_key\" | \"invalid_api_key\";\n\tmessage: string;\n\tenvVarName: string;\n};\n\nexport type UseClientResult =\n\t| {\n\t\t\tclient: CossistantClient;\n\t\t\terror: null;\n\t\t\tconfigurationError: null;\n\t }\n\t| {\n\t\t\tclient: null;\n\t\t\terror: null;\n\t\t\tconfigurationError: ConfigurationError;\n\t };\n\n/**\n * Detect if running in a Next.js environment.\n */\nfunction isNextJSEnvironment(): boolean {\n\tif (typeof window !== \"undefined\") {\n\t\t// Client-side: check for Next.js data\n\t\treturn \"__NEXT_DATA__\" in window;\n\t}\n\t// Server-side: check for Next.js runtime\n\treturn typeof process !== \"undefined\" && \"__NEXT_RUNTIME\" in process.env;\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 * `configurationError` field instead of throwing.\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\treturn useMemo(() => {\n\t\tconst keyFromEnv =\n\t\t\tprocess.env.NEXT_PUBLIC_COSSISTANT_API_KEY ||\n\t\t\tprocess.env.COSSISTANT_API_KEY;\n\t\tconst keyToUse = publicKey ?? keyFromEnv;\n\n\t\tif (!keyToUse) {\n\t\t\tconst isNextJS = isNextJSEnvironment();\n\t\t\tconst envVarName = isNextJS\n\t\t\t\t? \"NEXT_PUBLIC_COSSISTANT_API_KEY\"\n\t\t\t\t: \"COSSISTANT_API_KEY\";\n\n\t\t\treturn {\n\t\t\t\tclient: null,\n\t\t\t\terror: null,\n\t\t\t\tconfigurationError: {\n\t\t\t\t\ttype: \"missing_api_key\",\n\t\t\t\t\tmessage: `Public API key is required. Add ${envVarName} to your environment variables.`,\n\t\t\t\t\tenvVarName,\n\t\t\t\t},\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\tconst client = new CossistantClient(config);\n\t\t\treturn { client, error: null, configurationError: null };\n\t\t} catch (err: unknown) {\n\t\t\tconst isNextJS = isNextJSEnvironment();\n\t\t\tconst envVarName = isNextJS\n\t\t\t\t? \"NEXT_PUBLIC_COSSISTANT_API_KEY\"\n\t\t\t\t: \"COSSISTANT_API_KEY\";\n\n\t\t\treturn {\n\t\t\t\tclient: null,\n\t\t\t\terror: null,\n\t\t\t\tconfigurationError: {\n\t\t\t\t\ttype: \"missing_api_key\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\terr instanceof Error\n\t\t\t\t\t\t\t? err.message\n\t\t\t\t\t\t\t: \"Failed to initialize Cossistant client\",\n\t\t\t\t\tenvVarName,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t}, [publicKey, apiUrl, wsUrl]);\n}\n"],"mappings":";;;;;;;;;AA2BA,SAAS,sBAA+B;AACvC,KAAI,OAAO,WAAW,YAErB,QAAO,mBAAmB;AAG3B,QAAO,OAAO,YAAY,eAAe,oBAAoB,QAAQ;;;;;;;;AAStE,SAAgB,UACf,WACA,SAAS,iCACT,QAAQ,+BACU;AAClB,QAAO,cAAc;EACpB,MAAM,aACL,QAAQ,IAAI,kCACZ,QAAQ,IAAI;EACb,MAAM,WAAW,aAAa;AAE9B,MAAI,CAAC,UAAU;GAEd,MAAM,aADW,qBAAqB,GAEnC,mCACA;AAEH,UAAO;IACN,QAAQ;IACR,OAAO;IACP,oBAAoB;KACnB,MAAM;KACN,SAAS,mCAAmC,WAAW;KACvD;KACA;IACD;;EAGF,MAAMA,SAA2B;GAChC;GACA;GACA,WAAW;GACX;AAED,MAAI;AAEH,UAAO;IAAE,QADM,IAAI,iBAAiB,OAAO;IAC1B,OAAO;IAAM,oBAAoB;IAAM;WAChDC,KAAc;GAEtB,MAAM,aADW,qBAAqB,GAEnC,mCACA;AAEH,UAAO;IACN,QAAQ;IACR,OAAO;IACP,oBAAoB;KACnB,MAAM;KACN,SACC,eAAe,QACZ,IAAI,UACJ;KACJ;KACA;IACD;;IAEA;EAAC;EAAW;EAAQ;EAAM,CAAC"}
@@ -1,4 +1,4 @@
1
- import { AnyRealtimeEvent } from "../../realtime-events.js";
1
+ import { AnyRealtimeEvent } from "../../packages/types/src/realtime-events.js";
2
2
  import { CossistantClient } from "@cossistant/core";
3
3
 
4
4
  //#region src/hooks/private/use-visitor-typing-reporter.d.ts
@@ -1,4 +1,4 @@
1
- import { TimelineItem } from "../timeline-item.js";
1
+ import { TimelineItem } from "../packages/types/src/api/timeline-item.js";
2
2
  import { CossistantClient } from "@cossistant/core";
3
3
 
4
4
  //#region src/hooks/use-conversation-auto-seen.d.ts
@@ -1,4 +1,4 @@
1
- import { TimelineItem } from "../timeline-item.js";
1
+ import { TimelineItem } from "../packages/types/src/api/timeline-item.js";
2
2
 
3
3
  //#region src/hooks/use-conversation-page.d.ts
4
4
  type UseConversationPageOptions = {
@@ -1 +1 @@
1
- {"version":3,"file":"use-conversation-page.d.ts","names":[],"sources":["../../src/hooks/use-conversation-page.ts"],"sourcesContent":[],"mappings":";;;KAcY,0BAAA;;AAAZ;AA+BA;;EAMQ,cAAA,EAAA,MAAA;EAKC;;;EAYsB,cAAA,CAAA,EAAA,MAAA;EAuCf;;;;;;;;UAxEP;;;;;;;;KAUG,yBAAA;;;SAIJ;;SAEA;;;WAKC;;;;;sBAKW;;;;;oBAOD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuCH,mBAAA,UACN,6BACP"}
1
+ {"version":3,"file":"use-conversation-page.d.ts","names":[],"sources":["../../src/hooks/use-conversation-page.ts"],"sourcesContent":[],"mappings":";;;KAgBY,0BAAA;;AAAZ;AA+BA;;EAMQ,cAAA,EAAA,MAAA;EAKC;;;EAYsB,cAAA,CAAA,EAAA,MAAA;EAuCf;;;;;;;;UAxEP;;;;;;;;KAUG,yBAAA;;;SAIJ;;SAEA;;;WAKC;;;;;sBAKW;;;;;oBAOD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuCH,mBAAA,UACN,6BACP"}
@@ -1,3 +1,4 @@
1
+ import { useIdentificationState } from "../support/context/identification.js";
1
2
  import { useWebSocketSafe } from "../support/context/websocket.js";
2
3
  import { useConversationAutoSeen } from "./use-conversation-auto-seen.js";
3
4
  import { useConversationLifecycle } from "./use-conversation-lifecycle.js";
@@ -49,6 +50,7 @@ function useConversationPage(options) {
49
50
  const { conversationId: initialConversationId, initialMessage, onConversationIdChange, items: passedItems = [], autoSeenEnabled = true } = options;
50
51
  const { client, visitor } = useSupport();
51
52
  const websocket = useWebSocketSafe();
53
+ const identificationState = useIdentificationState();
52
54
  const trimmedInitialMessage = initialMessage?.trim() ?? "";
53
55
  const hasInitialMessage = trimmedInitialMessage.length > 0;
54
56
  const lifecycle = useConversationLifecycle({
@@ -71,16 +73,18 @@ function useConversationPage(options) {
71
73
  ]);
72
74
  const shouldShowIdentificationTool = useMemo(() => {
73
75
  if (lifecycle.isPending) return false;
76
+ if (identificationState?.isIdentifying) return false;
74
77
  if (visitor?.contact) return false;
75
78
  return !baseItems.some((item) => item.type === ConversationTimelineType.IDENTIFICATION);
76
79
  }, [
77
80
  baseItems,
78
81
  lifecycle.isPending,
79
- visitor?.contact
82
+ visitor?.contact,
83
+ identificationState?.isIdentifying
80
84
  ]);
81
85
  const displayItems = useMemo(() => {
82
86
  if (!shouldShowIdentificationTool) return baseItems;
83
- const organizationId = baseItems.at(-1)?.organizationId ?? client.getConfiguration().organizationId ?? "";
87
+ const organizationId = baseItems.at(-1)?.organizationId ?? client?.getConfiguration().organizationId ?? "";
84
88
  const identificationItem = {
85
89
  id: `identification-${lifecycle.conversationId}`,
86
90
  conversationId: lifecycle.conversationId,
@@ -106,10 +110,13 @@ function useConversationPage(options) {
106
110
  ]);
107
111
  const lastTimelineItem = useMemo(() => displayItems.at(-1) ?? null, [displayItems]);
108
112
  const composer = useMessageComposer({
109
- client,
113
+ client: client ?? void 0,
110
114
  conversationId: lifecycle.realConversationId,
111
115
  defaultTimelineItems: effectiveDefaultTimelineItems,
112
116
  visitorId: visitor?.id,
117
+ onConversationInitiated: (newConversationId) => {
118
+ if (lifecycle.isPending) lifecycle.setConversationId(newConversationId);
119
+ },
113
120
  onMessageSent: (newConversationId) => {
114
121
  if (lifecycle.isPending) lifecycle.setConversationId(newConversationId);
115
122
  },
@@ -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 { useWebSocketSafe } from \"../support/context/websocket\";\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\tisUploading: 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\tconst websocket = useWebSocketSafe();\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\t// Pass WebSocket connection for real-time typing events\n\t\trealtimeSend: websocket?.send ?? null,\n\t\tisRealtimeConnected: websocket?.isConnected ?? false,\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\tisUploading: composer.isUploading,\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 { CossistantClient } from \"@cossistant/core\";\nimport 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 { useIdentificationState } from \"../support/context/identification\";\nimport { useWebSocketSafe } from \"../support/context/websocket\";\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\tisUploading: 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\tconst websocket = useWebSocketSafe();\n\tconst identificationState = useIdentificationState();\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\t// Don't show identification form while identification is in progress\n\t\t// This prevents the form from flashing when an authenticated user opens the widget\n\t\tif (identificationState?.isIdentifying) {\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}, [\n\t\tbaseItems,\n\t\tlifecycle.isPending,\n\t\tvisitor?.contact,\n\t\tidentificationState?.isIdentifying,\n\t]);\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: client ?? undefined,\n\t\tconversationId: lifecycle.realConversationId,\n\t\tdefaultTimelineItems: effectiveDefaultTimelineItems,\n\t\tvisitorId: visitor?.id,\n\t\tonConversationInitiated: (newConversationId) => {\n\t\t\t// Immediately switch to new conversation ID for optimistic updates\n\t\t\t// This happens BEFORE the API call, so the UI starts reading from\n\t\t\t// the correct store key right away\n\t\t\tif (lifecycle.isPending) {\n\t\t\t\tlifecycle.setConversationId(newConversationId);\n\t\t\t}\n\t\t},\n\t\tonMessageSent: (newConversationId) => {\n\t\t\t// Also handle this for completeness (API call completed)\n\t\t\tif (lifecycle.isPending) {\n\t\t\t\tlifecycle.setConversationId(newConversationId);\n\t\t\t}\n\t\t},\n\t\t// Pass WebSocket connection for real-time typing events\n\t\trealtimeSend: websocket?.send ?? null,\n\t\tisRealtimeConnected: websocket?.isConnected ?? false,\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\tisUploading: composer.isUploading,\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,4 +1,4 @@
1
- import { TimelineItem } from "../timeline-item.js";
1
+ import { TimelineItem } from "../packages/types/src/api/timeline-item.js";
2
2
  import { PreviewTypingParticipant } from "./private/typing.js";
3
3
  import { useConversationTimelineItems } from "./use-conversation-timeline-items.js";
4
4
  import { Conversation } from "@cossistant/types";
@@ -15,6 +15,8 @@ type ConversationPreviewAssignedAgent = {
15
15
  name: string;
16
16
  image: string | null;
17
17
  type: "human" | "ai" | "fallback";
18
+ /** Last seen timestamp for human agents, used for online status indicator */
19
+ lastSeenAt?: string | null;
18
20
  };
19
21
  type ConversationPreviewTypingParticipant = PreviewTypingParticipant;
20
22
  type ConversationPreviewTypingState = {
@@ -1 +1 @@
1
- {"version":3,"file":"use-conversation-preview.d.ts","names":[],"sources":["../../src/hooks/use-conversation-preview.ts"],"sourcesContent":[],"mappings":";;;;;;KAcY,8BAAA;;EAAA,IAAA,EAAA,MAAA;EAQA,aAAA,EAAA,OAAA;EAMA,UAAA,CAAA,EAAA,MAAA;EAEA,WAAA,CAAA,EAAA,MAAA,GAAA,IAAA;AAOZ,CAAA;AAsBY,KArCA,gCAAA,GAqC4B;EACzB,IAAA,EAAA,MAAA;EAED,KAAA,EAAA,MAAA,GAAA,IAAA;EACE,IAAA,EAAA,OAAA,GAAA,IAAA,GAAA,UAAA;CACP;AACoB,KArCjB,oCAAA,GAAuC,wBAqCtB;AAAlB,KAnCC,8BAAA,GAmCD;EAAU,YAAA,EAlCN,oCAkCM,EAAA;EA0BL,kBAAA,EA3DK,oCA4DX,GAAA,IAAA;;;;KAvDE,6BAAA;gBACG;;;;;;;;;;yBAUS;;;;;;;;;;KAWZ,4BAAA;gBACG;;eAED;iBACE;UACP;YACE,kBAAkB;;;;;;iBA0Bb,sBAAA,UACN,gCACP"}
1
+ {"version":3,"file":"use-conversation-preview.d.ts","names":[],"sources":["../../src/hooks/use-conversation-preview.ts"],"sourcesContent":[],"mappings":";;;;;;KAcY,8BAAA;;EAAA,IAAA,EAAA,MAAA;EAQA,aAAA,EAAA,OAAA;EAQA,UAAA,CAAA,EAAA,MAAA;EAEA,WAAA,CAAA,EAAA,MAAA,GAAA,IAAA;AAOZ,CAAA;AAsBY,KAvCA,gCAAA,GAuC4B;EACzB,IAAA,EAAA,MAAA;EAED,KAAA,EAAA,MAAA,GAAA,IAAA;EACE,IAAA,EAAA,OAAA,GAAA,IAAA,GAAA,UAAA;EACP;EACoB,UAAA,CAAA,EAAA,MAAA,GAAA,IAAA;CAAlB;AAAU,KArCT,oCAAA,GAAuC,wBAqC9B;AA0BL,KA7DJ,8BAAA,GA8DF;gBA7DK;sBACM;;;;KAKT,6BAAA;gBACG;;;;;;;;;;yBAUS;;;;;;;;;;KAWZ,4BAAA;gBACG;;eAED;iBACE;UACP;YACE,kBAAkB;;;;;;iBA0Bb,sBAAA,UACN,gCACP"}
@@ -76,12 +76,14 @@ function useConversationPreview(options) {
76
76
  if (human) return {
77
77
  type: "human",
78
78
  name: human.name,
79
- image: human.image ?? null
79
+ image: human.image ?? null,
80
+ lastSeenAt: human.lastSeenAt ?? null
80
81
  };
81
82
  return {
82
83
  type: "human",
83
84
  name: supportFallbackName,
84
- image: null
85
+ image: null,
86
+ lastSeenAt: null
85
87
  };
86
88
  }
87
89
  if (lastAgentItem?.aiAgentId) {
@@ -101,7 +103,8 @@ function useConversationPreview(options) {
101
103
  if (fallbackHuman) return {
102
104
  type: "human",
103
105
  name: fallbackHuman.name,
104
- image: fallbackHuman.image ?? null
106
+ image: fallbackHuman.image ?? null,
107
+ lastSeenAt: fallbackHuman.lastSeenAt ?? null
105
108
  };
106
109
  const fallbackAi = availableAIAgents[0];
107
110
  if (fallbackAi) return {
@@ -1 +1 @@
1
- {"version":3,"file":"use-conversation-preview.js","names":["senderImage: string | null","typingState: ConversationPreviewTypingState"],"sources":["../../src/hooks/use-conversation-preview.ts"],"sourcesContent":["import type { Conversation } from \"@cossistant/types\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport { useMemo } from \"react\";\n\nimport { useSupport } from \"../provider\";\nimport { useSupportText } from \"../support/text\";\nimport { formatTimeAgo } from \"../support/utils/time\";\nimport {\n\tmapTypingEntriesToPreviewParticipants,\n\ttype PreviewTypingParticipant,\n} from \"./private/typing\";\nimport { useConversationTimelineItems } from \"./use-conversation-timeline-items\";\nimport { useConversationTyping } from \"./use-conversation-typing\";\n\nexport type ConversationPreviewLastMessage = {\n\tcontent: string;\n\ttime: string;\n\tisFromVisitor: boolean;\n\tsenderName?: string;\n\tsenderImage?: string | null;\n};\n\nexport type ConversationPreviewAssignedAgent = {\n\tname: string;\n\timage: string | null;\n\ttype: \"human\" | \"ai\" | \"fallback\";\n};\n\nexport type ConversationPreviewTypingParticipant = PreviewTypingParticipant;\n\nexport type ConversationPreviewTypingState = {\n\tparticipants: ConversationPreviewTypingParticipant[];\n\tprimaryParticipant: ConversationPreviewTypingParticipant | null;\n\tlabel: string | null;\n\tisTyping: boolean;\n};\n\nexport type UseConversationPreviewOptions = {\n\tconversation: Conversation;\n\t/**\n\t * Whether the hook should fetch timeline items for the conversation.\n\t * Disabled by default to reduce API calls - conversation.lastTimelineItem\n\t * is typically sufficient for previews.\n\t */\n\tincludeTimelineItems?: boolean;\n\t/**\n\t * Optional timeline items to merge with the live ones (e.g. optimistic items).\n\t */\n\tinitialTimelineItems?: TimelineItem[];\n\t/**\n\t * Typing state configuration (mainly exclusions for the current visitor).\n\t */\n\ttyping?: {\n\t\texcludeVisitorId?: string | null;\n\t\texcludeUserId?: string | null;\n\t\texcludeAiAgentId?: string | null;\n\t};\n};\n\nexport type UseConversationPreviewReturn = {\n\tconversation: Conversation;\n\ttitle: string;\n\tlastMessage: ConversationPreviewLastMessage | null;\n\tassignedAgent: ConversationPreviewAssignedAgent;\n\ttyping: ConversationPreviewTypingState;\n\ttimeline: ReturnType<typeof useConversationTimelineItems>;\n};\n\nfunction resolveLastTimelineMessage(\n\titems: TimelineItem[],\n\tfallback: TimelineItem | null\n) {\n\tfor (let index = items.length - 1; index >= 0; index--) {\n\t\tconst item = items[index];\n\n\t\tif (item?.type === \"message\") {\n\t\t\treturn item;\n\t\t}\n\t}\n\n\tif (fallback?.type === \"message\") {\n\t\treturn fallback;\n\t}\n\n\treturn null;\n}\n\n/**\n * Composes conversation metadata including derived titles, last message\n * snippets and typing state for use in lists.\n */\nexport function useConversationPreview(\n\toptions: UseConversationPreviewOptions\n): UseConversationPreviewReturn {\n\tconst {\n\t\tconversation,\n\t\tincludeTimelineItems = false,\n\t\tinitialTimelineItems = [],\n\t\ttyping,\n\t} = options;\n\tconst { availableHumanAgents, availableAIAgents, visitor } = useSupport();\n\tconst text = useSupportText();\n\n\tconst timeline = useConversationTimelineItems(conversation.id, {\n\t\tenabled: includeTimelineItems,\n\t});\n\n\tconst mergedTimelineItems = useMemo(() => {\n\t\tif (timeline.items.length > 0) {\n\t\t\treturn timeline.items;\n\t\t}\n\n\t\tif (initialTimelineItems.length > 0) {\n\t\t\treturn initialTimelineItems;\n\t\t}\n\n\t\treturn [] as TimelineItem[];\n\t}, [timeline.items, initialTimelineItems]);\n\n\tconst knownTimelineItems = useMemo(() => {\n\t\tconst items = [...mergedTimelineItems];\n\n\t\tif (\n\t\t\tconversation.lastTimelineItem &&\n\t\t\t!items.some((item) => item.id === conversation.lastTimelineItem?.id)\n\t\t) {\n\t\t\titems.push(conversation.lastTimelineItem);\n\t\t}\n\n\t\treturn items;\n\t}, [mergedTimelineItems, conversation.lastTimelineItem]);\n\n\tconst lastTimelineMessage = useMemo(\n\t\t() =>\n\t\t\tresolveLastTimelineMessage(\n\t\t\t\tmergedTimelineItems,\n\t\t\t\tconversation.lastTimelineItem ?? null\n\t\t\t),\n\t\t[mergedTimelineItems, conversation.lastTimelineItem]\n\t);\n\n\tconst lastMessage = useMemo<ConversationPreviewLastMessage | null>(() => {\n\t\tif (!lastTimelineMessage) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst isFromVisitor = lastTimelineMessage.visitorId !== null;\n\n\t\tlet senderName = text(\"common.fallbacks.unknown\");\n\t\tlet senderImage: string | null = null;\n\n\t\tif (isFromVisitor) {\n\t\t\tsenderName = text(\"common.fallbacks.you\");\n\t\t} else if (lastTimelineMessage.userId) {\n\t\t\tconst agent = availableHumanAgents.find(\n\t\t\t\t(a) => a.id === lastTimelineMessage.userId\n\t\t\t);\n\t\t\tif (agent) {\n\t\t\t\tsenderName = agent.name;\n\t\t\t\tsenderImage = agent.image;\n\t\t\t} else {\n\t\t\t\tsenderName = text(\"common.fallbacks.supportTeam\");\n\t\t\t}\n\t\t} else if (lastTimelineMessage.aiAgentId) {\n\t\t\tconst aiAgent = availableAIAgents.find(\n\t\t\t\t(agent) => agent.id === lastTimelineMessage.aiAgentId\n\t\t\t);\n\t\t\tif (aiAgent) {\n\t\t\t\tsenderName = aiAgent.name;\n\t\t\t\tsenderImage = aiAgent.image;\n\t\t\t} else {\n\t\t\t\tsenderName = text(\"common.fallbacks.aiAssistant\");\n\t\t\t}\n\t\t} else {\n\t\t\tsenderName = text(\"common.fallbacks.supportTeam\");\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: lastTimelineMessage.text || \"\",\n\t\t\ttime: formatTimeAgo(lastTimelineMessage.createdAt),\n\t\t\tisFromVisitor,\n\t\t\tsenderName,\n\t\t\tsenderImage,\n\t\t};\n\t}, [lastTimelineMessage, availableHumanAgents, availableAIAgents, text]);\n\n\tconst assignedAgent = useMemo<ConversationPreviewAssignedAgent>(() => {\n\t\tconst supportFallbackName = text(\"common.fallbacks.supportTeam\");\n\t\tconst aiFallbackName = text(\"common.fallbacks.aiAssistant\");\n\n\t\tconst lastAgentItem = [...knownTimelineItems]\n\t\t\t.reverse()\n\t\t\t.find((item) => item.userId !== null || item.aiAgentId !== null);\n\n\t\tif (lastAgentItem?.userId) {\n\t\t\tconst human = availableHumanAgents.find(\n\t\t\t\t(agent) => agent.id === lastAgentItem.userId\n\t\t\t);\n\n\t\t\tif (human) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"human\" as const,\n\t\t\t\t\tname: human.name,\n\t\t\t\t\timage: human.image ?? null,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttype: \"human\" as const,\n\t\t\t\tname: supportFallbackName,\n\t\t\t\timage: null,\n\t\t\t};\n\t\t}\n\n\t\tif (lastAgentItem?.aiAgentId) {\n\t\t\tconst ai = availableAIAgents.find(\n\t\t\t\t(agent) => agent.id === lastAgentItem.aiAgentId\n\t\t\t);\n\n\t\t\tif (ai) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"ai\" as const,\n\t\t\t\t\tname: ai.name,\n\t\t\t\t\timage: ai.image ?? null,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttype: \"ai\" as const,\n\t\t\t\tname: aiFallbackName,\n\t\t\t\timage: null,\n\t\t\t};\n\t\t}\n\n\t\tconst fallbackHuman = availableHumanAgents[0];\n\t\tif (fallbackHuman) {\n\t\t\treturn {\n\t\t\t\ttype: \"human\" as const,\n\t\t\t\tname: fallbackHuman.name,\n\t\t\t\timage: fallbackHuman.image ?? null,\n\t\t\t};\n\t\t}\n\n\t\tconst fallbackAi = availableAIAgents[0];\n\t\tif (fallbackAi) {\n\t\t\treturn {\n\t\t\t\ttype: \"ai\" as const,\n\t\t\t\tname: fallbackAi.name,\n\t\t\t\timage: fallbackAi.image ?? null,\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\ttype: \"fallback\" as const,\n\t\t\tname: supportFallbackName,\n\t\t\timage: null,\n\t\t};\n\t}, [knownTimelineItems, availableHumanAgents, availableAIAgents, text]);\n\n\tconst typingEntries = useConversationTyping(conversation.id, {\n\t\texcludeVisitorId: typing?.excludeVisitorId ?? visitor?.id ?? null,\n\t\texcludeUserId: typing?.excludeUserId ?? null,\n\t\texcludeAiAgentId: typing?.excludeAiAgentId ?? null,\n\t});\n\n\tconst typingParticipants = useMemo(\n\t\t() =>\n\t\t\tmapTypingEntriesToPreviewParticipants(typingEntries, {\n\t\t\t\tavailableHumanAgents,\n\t\t\t\tavailableAIAgents,\n\t\t\t\ttext,\n\t\t\t}),\n\t\t[typingEntries, availableHumanAgents, availableAIAgents, text]\n\t);\n\n\tconst primaryTypingParticipant = typingParticipants[0] ?? null;\n\n\tconst typingLabel = useMemo(() => {\n\t\tif (!primaryTypingParticipant) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn text(\"component.conversationButtonLink.typing\", {\n\t\t\tname: primaryTypingParticipant.name,\n\t\t});\n\t}, [primaryTypingParticipant, text]);\n\n\tconst typingState: ConversationPreviewTypingState = useMemo(\n\t\t() => ({\n\t\t\tparticipants: typingParticipants,\n\t\t\tprimaryParticipant: primaryTypingParticipant,\n\t\t\tlabel: typingLabel,\n\t\t\tisTyping: typingParticipants.length > 0,\n\t\t}),\n\t\t[typingParticipants, primaryTypingParticipant, typingLabel]\n\t);\n\n\tconst title = useMemo(() => {\n\t\tif (conversation.title) {\n\t\t\treturn conversation.title;\n\t\t}\n\n\t\tif (lastMessage?.content) {\n\t\t\treturn lastMessage.content;\n\t\t}\n\n\t\treturn text(\"component.conversationButtonLink.fallbackTitle\");\n\t}, [conversation.title, lastMessage?.content, text]);\n\n\treturn {\n\t\tconversation,\n\t\ttitle,\n\t\tlastMessage,\n\t\tassignedAgent,\n\t\ttyping: typingState,\n\t\ttimeline,\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-conversation-preview.js","names":["senderImage: string | null","typingState: ConversationPreviewTypingState"],"sources":["../../src/hooks/use-conversation-preview.ts"],"sourcesContent":["import type { Conversation } from \"@cossistant/types\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport { useMemo } from \"react\";\n\nimport { useSupport } from \"../provider\";\nimport { useSupportText } from \"../support/text\";\nimport { formatTimeAgo } from \"../support/utils/time\";\nimport {\n\tmapTypingEntriesToPreviewParticipants,\n\ttype PreviewTypingParticipant,\n} from \"./private/typing\";\nimport { useConversationTimelineItems } from \"./use-conversation-timeline-items\";\nimport { useConversationTyping } from \"./use-conversation-typing\";\n\nexport type ConversationPreviewLastMessage = {\n\tcontent: string;\n\ttime: string;\n\tisFromVisitor: boolean;\n\tsenderName?: string;\n\tsenderImage?: string | null;\n};\n\nexport type ConversationPreviewAssignedAgent = {\n\tname: string;\n\timage: string | null;\n\ttype: \"human\" | \"ai\" | \"fallback\";\n\t/** Last seen timestamp for human agents, used for online status indicator */\n\tlastSeenAt?: string | null;\n};\n\nexport type ConversationPreviewTypingParticipant = PreviewTypingParticipant;\n\nexport type ConversationPreviewTypingState = {\n\tparticipants: ConversationPreviewTypingParticipant[];\n\tprimaryParticipant: ConversationPreviewTypingParticipant | null;\n\tlabel: string | null;\n\tisTyping: boolean;\n};\n\nexport type UseConversationPreviewOptions = {\n\tconversation: Conversation;\n\t/**\n\t * Whether the hook should fetch timeline items for the conversation.\n\t * Disabled by default to reduce API calls - conversation.lastTimelineItem\n\t * is typically sufficient for previews.\n\t */\n\tincludeTimelineItems?: boolean;\n\t/**\n\t * Optional timeline items to merge with the live ones (e.g. optimistic items).\n\t */\n\tinitialTimelineItems?: TimelineItem[];\n\t/**\n\t * Typing state configuration (mainly exclusions for the current visitor).\n\t */\n\ttyping?: {\n\t\texcludeVisitorId?: string | null;\n\t\texcludeUserId?: string | null;\n\t\texcludeAiAgentId?: string | null;\n\t};\n};\n\nexport type UseConversationPreviewReturn = {\n\tconversation: Conversation;\n\ttitle: string;\n\tlastMessage: ConversationPreviewLastMessage | null;\n\tassignedAgent: ConversationPreviewAssignedAgent;\n\ttyping: ConversationPreviewTypingState;\n\ttimeline: ReturnType<typeof useConversationTimelineItems>;\n};\n\nfunction resolveLastTimelineMessage(\n\titems: TimelineItem[],\n\tfallback: TimelineItem | null\n) {\n\tfor (let index = items.length - 1; index >= 0; index--) {\n\t\tconst item = items[index];\n\n\t\tif (item?.type === \"message\") {\n\t\t\treturn item;\n\t\t}\n\t}\n\n\tif (fallback?.type === \"message\") {\n\t\treturn fallback;\n\t}\n\n\treturn null;\n}\n\n/**\n * Composes conversation metadata including derived titles, last message\n * snippets and typing state for use in lists.\n */\nexport function useConversationPreview(\n\toptions: UseConversationPreviewOptions\n): UseConversationPreviewReturn {\n\tconst {\n\t\tconversation,\n\t\tincludeTimelineItems = false,\n\t\tinitialTimelineItems = [],\n\t\ttyping,\n\t} = options;\n\tconst { availableHumanAgents, availableAIAgents, visitor } = useSupport();\n\tconst text = useSupportText();\n\n\tconst timeline = useConversationTimelineItems(conversation.id, {\n\t\tenabled: includeTimelineItems,\n\t});\n\n\tconst mergedTimelineItems = useMemo(() => {\n\t\tif (timeline.items.length > 0) {\n\t\t\treturn timeline.items;\n\t\t}\n\n\t\tif (initialTimelineItems.length > 0) {\n\t\t\treturn initialTimelineItems;\n\t\t}\n\n\t\treturn [] as TimelineItem[];\n\t}, [timeline.items, initialTimelineItems]);\n\n\tconst knownTimelineItems = useMemo(() => {\n\t\tconst items = [...mergedTimelineItems];\n\n\t\tif (\n\t\t\tconversation.lastTimelineItem &&\n\t\t\t!items.some((item) => item.id === conversation.lastTimelineItem?.id)\n\t\t) {\n\t\t\titems.push(conversation.lastTimelineItem);\n\t\t}\n\n\t\treturn items;\n\t}, [mergedTimelineItems, conversation.lastTimelineItem]);\n\n\tconst lastTimelineMessage = useMemo(\n\t\t() =>\n\t\t\tresolveLastTimelineMessage(\n\t\t\t\tmergedTimelineItems,\n\t\t\t\tconversation.lastTimelineItem ?? null\n\t\t\t),\n\t\t[mergedTimelineItems, conversation.lastTimelineItem]\n\t);\n\n\tconst lastMessage = useMemo<ConversationPreviewLastMessage | null>(() => {\n\t\tif (!lastTimelineMessage) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst isFromVisitor = lastTimelineMessage.visitorId !== null;\n\n\t\tlet senderName = text(\"common.fallbacks.unknown\");\n\t\tlet senderImage: string | null = null;\n\n\t\tif (isFromVisitor) {\n\t\t\tsenderName = text(\"common.fallbacks.you\");\n\t\t} else if (lastTimelineMessage.userId) {\n\t\t\tconst agent = availableHumanAgents.find(\n\t\t\t\t(a) => a.id === lastTimelineMessage.userId\n\t\t\t);\n\t\t\tif (agent) {\n\t\t\t\tsenderName = agent.name;\n\t\t\t\tsenderImage = agent.image;\n\t\t\t} else {\n\t\t\t\tsenderName = text(\"common.fallbacks.supportTeam\");\n\t\t\t}\n\t\t} else if (lastTimelineMessage.aiAgentId) {\n\t\t\tconst aiAgent = availableAIAgents.find(\n\t\t\t\t(agent) => agent.id === lastTimelineMessage.aiAgentId\n\t\t\t);\n\t\t\tif (aiAgent) {\n\t\t\t\tsenderName = aiAgent.name;\n\t\t\t\tsenderImage = aiAgent.image;\n\t\t\t} else {\n\t\t\t\tsenderName = text(\"common.fallbacks.aiAssistant\");\n\t\t\t}\n\t\t} else {\n\t\t\tsenderName = text(\"common.fallbacks.supportTeam\");\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: lastTimelineMessage.text || \"\",\n\t\t\ttime: formatTimeAgo(lastTimelineMessage.createdAt),\n\t\t\tisFromVisitor,\n\t\t\tsenderName,\n\t\t\tsenderImage,\n\t\t};\n\t}, [lastTimelineMessage, availableHumanAgents, availableAIAgents, text]);\n\n\tconst assignedAgent = useMemo<ConversationPreviewAssignedAgent>(() => {\n\t\tconst supportFallbackName = text(\"common.fallbacks.supportTeam\");\n\t\tconst aiFallbackName = text(\"common.fallbacks.aiAssistant\");\n\n\t\tconst lastAgentItem = [...knownTimelineItems]\n\t\t\t.reverse()\n\t\t\t.find((item) => item.userId !== null || item.aiAgentId !== null);\n\n\t\tif (lastAgentItem?.userId) {\n\t\t\tconst human = availableHumanAgents.find(\n\t\t\t\t(agent) => agent.id === lastAgentItem.userId\n\t\t\t);\n\n\t\t\tif (human) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"human\" as const,\n\t\t\t\t\tname: human.name,\n\t\t\t\t\timage: human.image ?? null,\n\t\t\t\t\tlastSeenAt: human.lastSeenAt ?? null,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttype: \"human\" as const,\n\t\t\t\tname: supportFallbackName,\n\t\t\t\timage: null,\n\t\t\t\tlastSeenAt: null,\n\t\t\t};\n\t\t}\n\n\t\tif (lastAgentItem?.aiAgentId) {\n\t\t\tconst ai = availableAIAgents.find(\n\t\t\t\t(agent) => agent.id === lastAgentItem.aiAgentId\n\t\t\t);\n\n\t\t\tif (ai) {\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"ai\" as const,\n\t\t\t\t\tname: ai.name,\n\t\t\t\t\timage: ai.image ?? null,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\ttype: \"ai\" as const,\n\t\t\t\tname: aiFallbackName,\n\t\t\t\timage: null,\n\t\t\t};\n\t\t}\n\n\t\tconst fallbackHuman = availableHumanAgents[0];\n\t\tif (fallbackHuman) {\n\t\t\treturn {\n\t\t\t\ttype: \"human\" as const,\n\t\t\t\tname: fallbackHuman.name,\n\t\t\t\timage: fallbackHuman.image ?? null,\n\t\t\t\tlastSeenAt: fallbackHuman.lastSeenAt ?? null,\n\t\t\t};\n\t\t}\n\n\t\tconst fallbackAi = availableAIAgents[0];\n\t\tif (fallbackAi) {\n\t\t\treturn {\n\t\t\t\ttype: \"ai\" as const,\n\t\t\t\tname: fallbackAi.name,\n\t\t\t\timage: fallbackAi.image ?? null,\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\ttype: \"fallback\" as const,\n\t\t\tname: supportFallbackName,\n\t\t\timage: null,\n\t\t};\n\t}, [knownTimelineItems, availableHumanAgents, availableAIAgents, text]);\n\n\tconst typingEntries = useConversationTyping(conversation.id, {\n\t\texcludeVisitorId: typing?.excludeVisitorId ?? visitor?.id ?? null,\n\t\texcludeUserId: typing?.excludeUserId ?? null,\n\t\texcludeAiAgentId: typing?.excludeAiAgentId ?? null,\n\t});\n\n\tconst typingParticipants = useMemo(\n\t\t() =>\n\t\t\tmapTypingEntriesToPreviewParticipants(typingEntries, {\n\t\t\t\tavailableHumanAgents,\n\t\t\t\tavailableAIAgents,\n\t\t\t\ttext,\n\t\t\t}),\n\t\t[typingEntries, availableHumanAgents, availableAIAgents, text]\n\t);\n\n\tconst primaryTypingParticipant = typingParticipants[0] ?? null;\n\n\tconst typingLabel = useMemo(() => {\n\t\tif (!primaryTypingParticipant) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn text(\"component.conversationButtonLink.typing\", {\n\t\t\tname: primaryTypingParticipant.name,\n\t\t});\n\t}, [primaryTypingParticipant, text]);\n\n\tconst typingState: ConversationPreviewTypingState = useMemo(\n\t\t() => ({\n\t\t\tparticipants: typingParticipants,\n\t\t\tprimaryParticipant: primaryTypingParticipant,\n\t\t\tlabel: typingLabel,\n\t\t\tisTyping: typingParticipants.length > 0,\n\t\t}),\n\t\t[typingParticipants, primaryTypingParticipant, typingLabel]\n\t);\n\n\tconst title = useMemo(() => {\n\t\tif (conversation.title) {\n\t\t\treturn conversation.title;\n\t\t}\n\n\t\tif (lastMessage?.content) {\n\t\t\treturn lastMessage.content;\n\t\t}\n\n\t\treturn text(\"component.conversationButtonLink.fallbackTitle\");\n\t}, [conversation.title, lastMessage?.content, text]);\n\n\treturn {\n\t\tconversation,\n\t\ttitle,\n\t\tlastMessage,\n\t\tassignedAgent,\n\t\ttyping: typingState,\n\t\ttimeline,\n\t};\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { ConversationSeen } from "../schemas3.js";
1
+ import { ConversationSeen } from "../packages/types/src/schemas.js";
2
2
 
3
3
  //#region src/hooks/use-conversation-seen.d.ts
4
4
  type UseConversationSeenOptions = {
@@ -47,7 +47,7 @@ function useConversationSeen(conversationId, options = {}) {
47
47
  */
48
48
  function useDebouncedConversationSeen(conversationId, options = {}, delay = 500) {
49
49
  const seenData = useConversationSeen(conversationId, options);
50
- const [debouncedSeenData, setDebouncedSeenData] = useState(seenData);
50
+ const [debouncedSeenData, setDebouncedSeenData] = useState(() => seenData);
51
51
  const timeoutRef = useRef(null);
52
52
  useEffect(() => {
53
53
  if (timeoutRef.current) clearTimeout(timeoutRef.current);
@@ -1 +1 @@
1
- {"version":3,"file":"use-conversation-seen.js","names":[],"sources":["../../src/hooks/use-conversation-seen.ts"],"sourcesContent":["import type { ConversationSeen } from \"@cossistant/types/schemas\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { hydrateConversationSeen, useSeenStore } from \"../realtime/seen-store\";\n\ntype UseConversationSeenOptions = {\n\tinitialData?: ConversationSeen[];\n};\n\nfunction buildSeenId(\n\tconversationId: string,\n\tactorType: string,\n\tactorId: string\n) {\n\treturn `${conversationId}-${actorType}-${actorId}`;\n}\n\n/**\n * Reads the conversation seen store and optionally hydrates it with SSR\n * payloads.\n */\nexport function useConversationSeen(\n\tconversationId: string | null | undefined,\n\toptions: UseConversationSeenOptions = {}\n): ConversationSeen[] {\n\tconst { initialData } = options;\n\tconst hydratedKeyRef = useRef<string | null>(null);\n\n\tuseEffect(() => {\n\t\t// Clear hydration key when conversation changes or is unmounted\n\t\tif (!conversationId) {\n\t\t\thydratedKeyRef.current = null;\n\t\t\treturn;\n\t\t}\n\n\t\t// Skip if no initial data\n\t\tif (!initialData || initialData.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Only hydrate once per conversation\n\t\tconst hydrationKey = conversationId;\n\n\t\tif (hydratedKeyRef.current === hydrationKey) {\n\t\t\treturn; // Already hydrated for this conversation\n\t\t}\n\n\t\thydrateConversationSeen(conversationId, initialData);\n\t\thydratedKeyRef.current = hydrationKey;\n\t}, [conversationId]); // Only depend on conversationId, NOT initialData\n\n\tconst conversationSeen = useSeenStore((state) =>\n\t\tconversationId ? (state.conversations[conversationId] ?? null) : null\n\t);\n\n\treturn useMemo(() => {\n\t\tif (!(conversationId && conversationSeen)) {\n\t\t\treturn [];\n\t\t}\n\n\t\treturn Object.values(conversationSeen).map(\n\t\t\t(entry) =>\n\t\t\t\t({\n\t\t\t\t\tid: buildSeenId(conversationId, entry.actorType, entry.actorId),\n\t\t\t\t\tconversationId,\n\t\t\t\t\tuserId: entry.actorType === \"user\" ? entry.actorId : null,\n\t\t\t\t\tvisitorId: entry.actorType === \"visitor\" ? entry.actorId : null,\n\t\t\t\t\taiAgentId: entry.actorType === \"ai_agent\" ? entry.actorId : null,\n\t\t\t\t\tlastSeenAt: entry.lastSeenAt,\n\t\t\t\t\tcreatedAt: entry.lastSeenAt,\n\t\t\t\t\tupdatedAt: entry.lastSeenAt,\n\t\t\t\t\tdeletedAt: null,\n\t\t\t\t}) satisfies ConversationSeen\n\t\t);\n\t}, [conversationId, conversationSeen]);\n}\n\n/**\n * Debounced version of useConversationSeen that delays updates by 500ms\n * to prevent animation conflicts when messages are sent and immediately seen.\n *\n * Use this in UI components where smooth animations are critical.\n */\nexport function useDebouncedConversationSeen(\n\tconversationId: string | null | undefined,\n\toptions: UseConversationSeenOptions = {},\n\tdelay = 500\n): ConversationSeen[] {\n\tconst seenData = useConversationSeen(conversationId, options);\n\tconst [debouncedSeenData, setDebouncedSeenData] =\n\t\tuseState<ConversationSeen[]>(seenData);\n\tconst timeoutRef = useRef<NodeJS.Timeout | null>(null);\n\n\tuseEffect(() => {\n\t\t// Clear any pending timeout\n\t\tif (timeoutRef.current) {\n\t\t\tclearTimeout(timeoutRef.current);\n\t\t}\n\n\t\t// Set new timeout to update after delay\n\t\ttimeoutRef.current = setTimeout(() => {\n\t\t\tsetDebouncedSeenData(seenData);\n\t\t}, delay);\n\n\t\t// Cleanup on unmount or when seenData changes\n\t\treturn () => {\n\t\t\tif (timeoutRef.current) {\n\t\t\t\tclearTimeout(timeoutRef.current);\n\t\t\t}\n\t\t};\n\t}, [seenData, delay]);\n\n\treturn debouncedSeenData;\n}\n"],"mappings":";;;;AAQA,SAAS,YACR,gBACA,WACA,SACC;AACD,QAAO,GAAG,eAAe,GAAG,UAAU,GAAG;;;;;;AAO1C,SAAgB,oBACf,gBACA,UAAsC,EAAE,EACnB;CACrB,MAAM,EAAE,gBAAgB;CACxB,MAAM,iBAAiB,OAAsB,KAAK;AAElD,iBAAgB;AAEf,MAAI,CAAC,gBAAgB;AACpB,kBAAe,UAAU;AACzB;;AAID,MAAI,CAAC,eAAe,YAAY,WAAW,EAC1C;EAID,MAAM,eAAe;AAErB,MAAI,eAAe,YAAY,aAC9B;AAGD,0BAAwB,gBAAgB,YAAY;AACpD,iBAAe,UAAU;IACvB,CAAC,eAAe,CAAC;CAEpB,MAAM,mBAAmB,cAAc,UACtC,iBAAkB,MAAM,cAAc,mBAAmB,OAAQ,KACjE;AAED,QAAO,cAAc;AACpB,MAAI,EAAE,kBAAkB,kBACvB,QAAO,EAAE;AAGV,SAAO,OAAO,OAAO,iBAAiB,CAAC,KACrC,WACC;GACA,IAAI,YAAY,gBAAgB,MAAM,WAAW,MAAM,QAAQ;GAC/D;GACA,QAAQ,MAAM,cAAc,SAAS,MAAM,UAAU;GACrD,WAAW,MAAM,cAAc,YAAY,MAAM,UAAU;GAC3D,WAAW,MAAM,cAAc,aAAa,MAAM,UAAU;GAC5D,YAAY,MAAM;GAClB,WAAW,MAAM;GACjB,WAAW,MAAM;GACjB,WAAW;GACX,EACF;IACC,CAAC,gBAAgB,iBAAiB,CAAC;;;;;;;;AASvC,SAAgB,6BACf,gBACA,UAAsC,EAAE,EACxC,QAAQ,KACa;CACrB,MAAM,WAAW,oBAAoB,gBAAgB,QAAQ;CAC7D,MAAM,CAAC,mBAAmB,wBACzB,SAA6B,SAAS;CACvC,MAAM,aAAa,OAA8B,KAAK;AAEtD,iBAAgB;AAEf,MAAI,WAAW,QACd,cAAa,WAAW,QAAQ;AAIjC,aAAW,UAAU,iBAAiB;AACrC,wBAAqB,SAAS;KAC5B,MAAM;AAGT,eAAa;AACZ,OAAI,WAAW,QACd,cAAa,WAAW,QAAQ;;IAGhC,CAAC,UAAU,MAAM,CAAC;AAErB,QAAO"}
1
+ {"version":3,"file":"use-conversation-seen.js","names":[],"sources":["../../src/hooks/use-conversation-seen.ts"],"sourcesContent":["import type { ConversationSeen } from \"@cossistant/types/schemas\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { hydrateConversationSeen, useSeenStore } from \"../realtime/seen-store\";\n\ntype UseConversationSeenOptions = {\n\tinitialData?: ConversationSeen[];\n};\n\nfunction buildSeenId(\n\tconversationId: string,\n\tactorType: string,\n\tactorId: string\n) {\n\treturn `${conversationId}-${actorType}-${actorId}`;\n}\n\n/**\n * Reads the conversation seen store and optionally hydrates it with SSR\n * payloads.\n */\nexport function useConversationSeen(\n\tconversationId: string | null | undefined,\n\toptions: UseConversationSeenOptions = {}\n): ConversationSeen[] {\n\tconst { initialData } = options;\n\tconst hydratedKeyRef = useRef<string | null>(null);\n\n\tuseEffect(() => {\n\t\t// Clear hydration key when conversation changes or is unmounted\n\t\tif (!conversationId) {\n\t\t\thydratedKeyRef.current = null;\n\t\t\treturn;\n\t\t}\n\n\t\t// Skip if no initial data\n\t\tif (!initialData || initialData.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Only hydrate once per conversation\n\t\tconst hydrationKey = conversationId;\n\n\t\tif (hydratedKeyRef.current === hydrationKey) {\n\t\t\treturn; // Already hydrated for this conversation\n\t\t}\n\n\t\thydrateConversationSeen(conversationId, initialData);\n\t\thydratedKeyRef.current = hydrationKey;\n\t}, [conversationId]); // Only depend on conversationId, NOT initialData\n\n\tconst conversationSeen = useSeenStore((state) =>\n\t\tconversationId ? (state.conversations[conversationId] ?? null) : null\n\t);\n\n\treturn useMemo(() => {\n\t\tif (!(conversationId && conversationSeen)) {\n\t\t\treturn [];\n\t\t}\n\n\t\treturn Object.values(conversationSeen).map(\n\t\t\t(entry) =>\n\t\t\t\t({\n\t\t\t\t\tid: buildSeenId(conversationId, entry.actorType, entry.actorId),\n\t\t\t\t\tconversationId,\n\t\t\t\t\tuserId: entry.actorType === \"user\" ? entry.actorId : null,\n\t\t\t\t\tvisitorId: entry.actorType === \"visitor\" ? entry.actorId : null,\n\t\t\t\t\taiAgentId: entry.actorType === \"ai_agent\" ? entry.actorId : null,\n\t\t\t\t\tlastSeenAt: entry.lastSeenAt,\n\t\t\t\t\tcreatedAt: entry.lastSeenAt,\n\t\t\t\t\tupdatedAt: entry.lastSeenAt,\n\t\t\t\t\tdeletedAt: null,\n\t\t\t\t}) satisfies ConversationSeen\n\t\t);\n\t}, [conversationId, conversationSeen]);\n}\n\n/**\n * Debounced version of useConversationSeen that delays updates by 500ms\n * to prevent animation conflicts when messages are sent and immediately seen.\n *\n * Use this in UI components where smooth animations are critical.\n */\nexport function useDebouncedConversationSeen(\n\tconversationId: string | null | undefined,\n\toptions: UseConversationSeenOptions = {},\n\tdelay = 500\n): ConversationSeen[] {\n\tconst seenData = useConversationSeen(conversationId, options);\n\t// Use lazy initialization to avoid re-computing initial state on every render\n\tconst [debouncedSeenData, setDebouncedSeenData] = useState<\n\t\tConversationSeen[]\n\t>(() => seenData);\n\tconst timeoutRef = useRef<NodeJS.Timeout | null>(null);\n\n\tuseEffect(() => {\n\t\t// Clear any pending timeout\n\t\tif (timeoutRef.current) {\n\t\t\tclearTimeout(timeoutRef.current);\n\t\t}\n\n\t\t// Set new timeout to update after delay\n\t\ttimeoutRef.current = setTimeout(() => {\n\t\t\tsetDebouncedSeenData(seenData);\n\t\t}, delay);\n\n\t\t// Cleanup on unmount or when seenData changes\n\t\treturn () => {\n\t\t\tif (timeoutRef.current) {\n\t\t\t\tclearTimeout(timeoutRef.current);\n\t\t\t}\n\t\t};\n\t}, [seenData, delay]);\n\n\treturn debouncedSeenData;\n}\n"],"mappings":";;;;AAQA,SAAS,YACR,gBACA,WACA,SACC;AACD,QAAO,GAAG,eAAe,GAAG,UAAU,GAAG;;;;;;AAO1C,SAAgB,oBACf,gBACA,UAAsC,EAAE,EACnB;CACrB,MAAM,EAAE,gBAAgB;CACxB,MAAM,iBAAiB,OAAsB,KAAK;AAElD,iBAAgB;AAEf,MAAI,CAAC,gBAAgB;AACpB,kBAAe,UAAU;AACzB;;AAID,MAAI,CAAC,eAAe,YAAY,WAAW,EAC1C;EAID,MAAM,eAAe;AAErB,MAAI,eAAe,YAAY,aAC9B;AAGD,0BAAwB,gBAAgB,YAAY;AACpD,iBAAe,UAAU;IACvB,CAAC,eAAe,CAAC;CAEpB,MAAM,mBAAmB,cAAc,UACtC,iBAAkB,MAAM,cAAc,mBAAmB,OAAQ,KACjE;AAED,QAAO,cAAc;AACpB,MAAI,EAAE,kBAAkB,kBACvB,QAAO,EAAE;AAGV,SAAO,OAAO,OAAO,iBAAiB,CAAC,KACrC,WACC;GACA,IAAI,YAAY,gBAAgB,MAAM,WAAW,MAAM,QAAQ;GAC/D;GACA,QAAQ,MAAM,cAAc,SAAS,MAAM,UAAU;GACrD,WAAW,MAAM,cAAc,YAAY,MAAM,UAAU;GAC3D,WAAW,MAAM,cAAc,aAAa,MAAM,UAAU;GAC5D,YAAY,MAAM;GAClB,WAAW,MAAM;GACjB,WAAW,MAAM;GACjB,WAAW;GACX,EACF;IACC,CAAC,gBAAgB,iBAAiB,CAAC;;;;;;;;AASvC,SAAgB,6BACf,gBACA,UAAsC,EAAE,EACxC,QAAQ,KACa;CACrB,MAAM,WAAW,oBAAoB,gBAAgB,QAAQ;CAE7D,MAAM,CAAC,mBAAmB,wBAAwB,eAE1C,SAAS;CACjB,MAAM,aAAa,OAA8B,KAAK;AAEtD,iBAAgB;AAEf,MAAI,WAAW,QACd,cAAa,WAAW,QAAQ;AAIjC,aAAW,UAAU,iBAAiB;AACrC,wBAAqB,SAAS;KAC5B,MAAM;AAGT,eAAa;AACZ,OAAI,WAAW,QACd,cAAa,WAAW,QAAQ;;IAGhC,CAAC,UAAU,MAAM,CAAC;AAErB,QAAO"}
@@ -1,4 +1,4 @@
1
- import { GetConversationTimelineItemsRequest, GetConversationTimelineItemsResponse } from "../timeline-item.js";
1
+ import { GetConversationTimelineItemsRequest, GetConversationTimelineItemsResponse } from "../packages/types/src/api/timeline-item.js";
2
2
  import { ConversationTimelineItemsState } from "@cossistant/core";
3
3
 
4
4
  //#region src/hooks/use-conversation-timeline-items.d.ts
@@ -17,10 +17,9 @@ const NO_CONVERSATION_ID = "__no_conversation__";
17
17
  */
18
18
  function useConversationTimelineItems(conversationId, options = {}) {
19
19
  const { client } = useSupport();
20
- const store = client.timelineItemsStore;
21
- if (!store) throw new Error("Timeline items store is not available on the client instance");
20
+ const store = client?.timelineItemsStore ?? null;
22
21
  const stableConversationId = conversationId ?? NO_CONVERSATION_ID;
23
- const selection = useStoreSelector(store, (state) => state.conversations[stableConversationId] ?? EMPTY_STATE);
22
+ const selection = useStoreSelector(store, (state) => state ? state.conversations[stableConversationId] ?? EMPTY_STATE : EMPTY_STATE);
24
23
  const baseArgs = useMemo(() => ({
25
24
  limit: options.limit ?? DEFAULT_LIMIT,
26
25
  cursor: options.cursor ?? void 0
@@ -1 +1 @@
1
- {"version":3,"file":"use-conversation-timeline-items.js","names":["EMPTY_STATE: ConversationTimelineItemsState"],"sources":["../../src/hooks/use-conversation-timeline-items.ts"],"sourcesContent":["import type { ConversationTimelineItemsState } from \"@cossistant/core\";\nimport type {\n\tGetConversationTimelineItemsRequest,\n\tGetConversationTimelineItemsResponse,\n} from \"@cossistant/types/api/timeline-item\";\nimport { useCallback, useMemo } from \"react\";\nimport { useSupport } from \"../provider\";\nimport { useStoreSelector } from \"./private/store/use-store-selector\";\nimport { useClientQuery } from \"./private/use-client-query\";\n\nconst EMPTY_STATE: ConversationTimelineItemsState = {\n\titems: [],\n\thasNextPage: false,\n\tnextCursor: undefined,\n};\n\nconst DEFAULT_LIMIT = 50;\n\nconst NO_CONVERSATION_ID = \"__no_conversation__\" as const;\n\nexport type UseConversationTimelineItemsOptions = {\n\tlimit?: number;\n\tcursor?: string | null;\n\tenabled?: boolean;\n\trefetchInterval?: number | false;\n\trefetchOnWindowFocus?: boolean;\n};\n\nexport type UseConversationTimelineItemsResult =\n\tConversationTimelineItemsState & {\n\t\tisLoading: boolean;\n\t\terror: Error | null;\n\t\trefetch: (\n\t\t\targs?: Pick<GetConversationTimelineItemsRequest, \"cursor\" | \"limit\">\n\t\t) => Promise<GetConversationTimelineItemsResponse | undefined>;\n\t\tfetchNextPage: () => Promise<\n\t\t\tGetConversationTimelineItemsResponse | undefined\n\t\t>;\n\t};\n\n/**\n * Fetches timeline items for a conversation and keeps the local store in sync\n * with pagination helpers.\n */\nexport function useConversationTimelineItems(\n\tconversationId: string | null | undefined,\n\toptions: UseConversationTimelineItemsOptions = {}\n): UseConversationTimelineItemsResult {\n\tconst { client } = useSupport();\n\tconst store = client.timelineItemsStore;\n\n\tif (!store) {\n\t\tthrow new Error(\n\t\t\t\"Timeline items store is not available on the client instance\"\n\t\t);\n\t}\n\n\tconst stableConversationId = conversationId ?? NO_CONVERSATION_ID;\n\n\tconst selection = useStoreSelector(\n\t\tstore,\n\t\t(state) => state.conversations[stableConversationId] ?? EMPTY_STATE\n\t);\n\n\tconst baseArgs = useMemo(\n\t\t() =>\n\t\t\t({\n\t\t\t\tlimit: options.limit ?? DEFAULT_LIMIT,\n\t\t\t\tcursor: options.cursor ?? undefined,\n\t\t\t}) satisfies Pick<\n\t\t\t\tGetConversationTimelineItemsRequest,\n\t\t\t\t\"limit\" | \"cursor\"\n\t\t\t>,\n\t\t[options.cursor, options.limit]\n\t);\n\n\tconst {\n\t\trefetch: queryRefetch,\n\t\tisLoading: queryLoading,\n\t\terror,\n\t} = useClientQuery<\n\t\tGetConversationTimelineItemsResponse,\n\t\tPick<GetConversationTimelineItemsRequest, \"cursor\" | \"limit\">\n\t>({\n\t\tclient,\n\t\tqueryKey: conversationId\n\t\t\t? `timeline:${conversationId}:${baseArgs.limit}:${baseArgs.cursor ?? \"\"}`\n\t\t\t: undefined,\n\t\tqueryFn: (instance, args) => {\n\t\t\tif (!conversationId) {\n\t\t\t\treturn Promise.resolve({\n\t\t\t\t\titems: [],\n\t\t\t\t\thasNextPage: false,\n\t\t\t\t\tnextCursor: null,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn instance.getConversationTimelineItems({\n\t\t\t\tconversationId,\n\t\t\t\tlimit: args?.limit ?? baseArgs.limit,\n\t\t\t\tcursor: args?.cursor ?? baseArgs.cursor ?? undefined,\n\t\t\t});\n\t\t},\n\t\tenabled: Boolean(conversationId) && (options.enabled ?? true),\n\t\trefetchInterval: options.refetchInterval ?? false,\n\t\trefetchOnWindowFocus: options.refetchOnWindowFocus ?? false,\n\t\trefetchOnMount: selection.items.length === 0,\n\t\tinitialArgs: baseArgs,\n\t\tdependencies: [\n\t\t\tstableConversationId,\n\t\t\tbaseArgs.limit,\n\t\t\tbaseArgs.cursor ?? null,\n\t\t],\n\t});\n\n\tconst refetch = useCallback(\n\t\t(args?: Pick<GetConversationTimelineItemsRequest, \"cursor\" | \"limit\">) => {\n\t\t\tif (!conversationId) {\n\t\t\t\treturn Promise.resolve(undefined);\n\t\t\t}\n\n\t\t\treturn queryRefetch({\n\t\t\t\tlimit: baseArgs.limit,\n\t\t\t\tcursor: baseArgs.cursor,\n\t\t\t\t...args,\n\t\t\t});\n\t\t},\n\t\t[queryRefetch, baseArgs, conversationId]\n\t);\n\n\tconst fetchNextPage = useCallback(() => {\n\t\tif (!(selection.hasNextPage && selection.nextCursor)) {\n\t\t\treturn Promise.resolve(undefined);\n\t\t}\n\n\t\treturn refetch({ cursor: selection.nextCursor, limit: baseArgs.limit });\n\t}, [selection.hasNextPage, selection.nextCursor, refetch, baseArgs.limit]);\n\n\tconst isInitialLoad = selection.items.length === 0;\n\tconst isLoading = isInitialLoad ? queryLoading : false;\n\n\treturn {\n\t\titems: selection.items,\n\t\thasNextPage: selection.hasNextPage,\n\t\tnextCursor: selection.nextCursor,\n\t\tisLoading,\n\t\terror,\n\t\trefetch,\n\t\tfetchNextPage,\n\t};\n}\n"],"mappings":";;;;;;AAUA,MAAMA,cAA8C;CACnD,OAAO,EAAE;CACT,aAAa;CACb,YAAY;CACZ;AAED,MAAM,gBAAgB;AAEtB,MAAM,qBAAqB;;;;;AA0B3B,SAAgB,6BACf,gBACA,UAA+C,EAAE,EACZ;CACrC,MAAM,EAAE,WAAW,YAAY;CAC/B,MAAM,QAAQ,OAAO;AAErB,KAAI,CAAC,MACJ,OAAM,IAAI,MACT,+DACA;CAGF,MAAM,uBAAuB,kBAAkB;CAE/C,MAAM,YAAY,iBACjB,QACC,UAAU,MAAM,cAAc,yBAAyB,YACxD;CAED,MAAM,WAAW,eAEd;EACA,OAAO,QAAQ,SAAS;EACxB,QAAQ,QAAQ,UAAU;EAC1B,GAIF,CAAC,QAAQ,QAAQ,QAAQ,MAAM,CAC/B;CAED,MAAM,EACL,SAAS,cACT,WAAW,cACX,UACG,eAGF;EACD;EACA,UAAU,iBACP,YAAY,eAAe,GAAG,SAAS,MAAM,GAAG,SAAS,UAAU,OACnE;EACH,UAAU,UAAU,SAAS;AAC5B,OAAI,CAAC,eACJ,QAAO,QAAQ,QAAQ;IACtB,OAAO,EAAE;IACT,aAAa;IACb,YAAY;IACZ,CAAC;AAGH,UAAO,SAAS,6BAA6B;IAC5C;IACA,OAAO,MAAM,SAAS,SAAS;IAC/B,QAAQ,MAAM,UAAU,SAAS,UAAU;IAC3C,CAAC;;EAEH,SAAS,QAAQ,eAAe,KAAK,QAAQ,WAAW;EACxD,iBAAiB,QAAQ,mBAAmB;EAC5C,sBAAsB,QAAQ,wBAAwB;EACtD,gBAAgB,UAAU,MAAM,WAAW;EAC3C,aAAa;EACb,cAAc;GACb;GACA,SAAS;GACT,SAAS,UAAU;GACnB;EACD,CAAC;CAEF,MAAM,UAAU,aACd,SAAyE;AACzE,MAAI,CAAC,eACJ,QAAO,QAAQ,QAAQ,OAAU;AAGlC,SAAO,aAAa;GACnB,OAAO,SAAS;GAChB,QAAQ,SAAS;GACjB,GAAG;GACH,CAAC;IAEH;EAAC;EAAc;EAAU;EAAe,CACxC;CAED,MAAM,gBAAgB,kBAAkB;AACvC,MAAI,EAAE,UAAU,eAAe,UAAU,YACxC,QAAO,QAAQ,QAAQ,OAAU;AAGlC,SAAO,QAAQ;GAAE,QAAQ,UAAU;GAAY,OAAO,SAAS;GAAO,CAAC;IACrE;EAAC,UAAU;EAAa,UAAU;EAAY;EAAS,SAAS;EAAM,CAAC;CAG1E,MAAM,YADgB,UAAU,MAAM,WAAW,IACf,eAAe;AAEjD,QAAO;EACN,OAAO,UAAU;EACjB,aAAa,UAAU;EACvB,YAAY,UAAU;EACtB;EACA;EACA;EACA;EACA"}
1
+ {"version":3,"file":"use-conversation-timeline-items.js","names":["EMPTY_STATE: ConversationTimelineItemsState"],"sources":["../../src/hooks/use-conversation-timeline-items.ts"],"sourcesContent":["import type { ConversationTimelineItemsState } from \"@cossistant/core\";\nimport type {\n\tGetConversationTimelineItemsRequest,\n\tGetConversationTimelineItemsResponse,\n} from \"@cossistant/types/api/timeline-item\";\nimport { useCallback, useMemo } from \"react\";\nimport { useSupport } from \"../provider\";\nimport { useStoreSelector } from \"./private/store/use-store-selector\";\nimport { useClientQuery } from \"./private/use-client-query\";\n\nconst EMPTY_STATE: ConversationTimelineItemsState = {\n\titems: [],\n\thasNextPage: false,\n\tnextCursor: undefined,\n};\n\nconst DEFAULT_LIMIT = 50;\n\nconst NO_CONVERSATION_ID = \"__no_conversation__\" as const;\n\nexport type UseConversationTimelineItemsOptions = {\n\tlimit?: number;\n\tcursor?: string | null;\n\tenabled?: boolean;\n\trefetchInterval?: number | false;\n\trefetchOnWindowFocus?: boolean;\n};\n\nexport type UseConversationTimelineItemsResult =\n\tConversationTimelineItemsState & {\n\t\tisLoading: boolean;\n\t\terror: Error | null;\n\t\trefetch: (\n\t\t\targs?: Pick<GetConversationTimelineItemsRequest, \"cursor\" | \"limit\">\n\t\t) => Promise<GetConversationTimelineItemsResponse | undefined>;\n\t\tfetchNextPage: () => Promise<\n\t\t\tGetConversationTimelineItemsResponse | undefined\n\t\t>;\n\t};\n\n/**\n * Fetches timeline items for a conversation and keeps the local store in sync\n * with pagination helpers.\n */\nexport function useConversationTimelineItems(\n\tconversationId: string | null | undefined,\n\toptions: UseConversationTimelineItemsOptions = {}\n): UseConversationTimelineItemsResult {\n\tconst { client } = useSupport();\n\tconst store = client?.timelineItemsStore ?? null;\n\n\tconst stableConversationId = conversationId ?? NO_CONVERSATION_ID;\n\n\tconst selection = useStoreSelector(store, (state) =>\n\t\tstate\n\t\t\t? (state.conversations[stableConversationId] ?? EMPTY_STATE)\n\t\t\t: EMPTY_STATE\n\t);\n\n\tconst baseArgs = useMemo(\n\t\t() =>\n\t\t\t({\n\t\t\t\tlimit: options.limit ?? DEFAULT_LIMIT,\n\t\t\t\tcursor: options.cursor ?? undefined,\n\t\t\t}) satisfies Pick<\n\t\t\t\tGetConversationTimelineItemsRequest,\n\t\t\t\t\"limit\" | \"cursor\"\n\t\t\t>,\n\t\t[options.cursor, options.limit]\n\t);\n\n\tconst {\n\t\trefetch: queryRefetch,\n\t\tisLoading: queryLoading,\n\t\terror,\n\t} = useClientQuery<\n\t\tGetConversationTimelineItemsResponse,\n\t\tPick<GetConversationTimelineItemsRequest, \"cursor\" | \"limit\">\n\t>({\n\t\tclient,\n\t\tqueryKey: conversationId\n\t\t\t? `timeline:${conversationId}:${baseArgs.limit}:${baseArgs.cursor ?? \"\"}`\n\t\t\t: undefined,\n\t\tqueryFn: (instance, args) => {\n\t\t\tif (!conversationId) {\n\t\t\t\treturn Promise.resolve({\n\t\t\t\t\titems: [],\n\t\t\t\t\thasNextPage: false,\n\t\t\t\t\tnextCursor: null,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn instance.getConversationTimelineItems({\n\t\t\t\tconversationId,\n\t\t\t\tlimit: args?.limit ?? baseArgs.limit,\n\t\t\t\tcursor: args?.cursor ?? baseArgs.cursor ?? undefined,\n\t\t\t});\n\t\t},\n\t\tenabled: Boolean(conversationId) && (options.enabled ?? true),\n\t\trefetchInterval: options.refetchInterval ?? false,\n\t\trefetchOnWindowFocus: options.refetchOnWindowFocus ?? false,\n\t\trefetchOnMount: selection.items.length === 0,\n\t\tinitialArgs: baseArgs,\n\t\tdependencies: [\n\t\t\tstableConversationId,\n\t\t\tbaseArgs.limit,\n\t\t\tbaseArgs.cursor ?? null,\n\t\t],\n\t});\n\n\tconst refetch = useCallback(\n\t\t(args?: Pick<GetConversationTimelineItemsRequest, \"cursor\" | \"limit\">) => {\n\t\t\tif (!conversationId) {\n\t\t\t\treturn Promise.resolve(undefined);\n\t\t\t}\n\n\t\t\treturn queryRefetch({\n\t\t\t\tlimit: baseArgs.limit,\n\t\t\t\tcursor: baseArgs.cursor,\n\t\t\t\t...args,\n\t\t\t});\n\t\t},\n\t\t[queryRefetch, baseArgs, conversationId]\n\t);\n\n\tconst fetchNextPage = useCallback(() => {\n\t\tif (!(selection.hasNextPage && selection.nextCursor)) {\n\t\t\treturn Promise.resolve(undefined);\n\t\t}\n\n\t\treturn refetch({ cursor: selection.nextCursor, limit: baseArgs.limit });\n\t}, [selection.hasNextPage, selection.nextCursor, refetch, baseArgs.limit]);\n\n\tconst isInitialLoad = selection.items.length === 0;\n\tconst isLoading = isInitialLoad ? queryLoading : false;\n\n\treturn {\n\t\titems: selection.items,\n\t\thasNextPage: selection.hasNextPage,\n\t\tnextCursor: selection.nextCursor,\n\t\tisLoading,\n\t\terror,\n\t\trefetch,\n\t\tfetchNextPage,\n\t};\n}\n"],"mappings":";;;;;;AAUA,MAAMA,cAA8C;CACnD,OAAO,EAAE;CACT,aAAa;CACb,YAAY;CACZ;AAED,MAAM,gBAAgB;AAEtB,MAAM,qBAAqB;;;;;AA0B3B,SAAgB,6BACf,gBACA,UAA+C,EAAE,EACZ;CACrC,MAAM,EAAE,WAAW,YAAY;CAC/B,MAAM,QAAQ,QAAQ,sBAAsB;CAE5C,MAAM,uBAAuB,kBAAkB;CAE/C,MAAM,YAAY,iBAAiB,QAAQ,UAC1C,QACI,MAAM,cAAc,yBAAyB,cAC9C,YACH;CAED,MAAM,WAAW,eAEd;EACA,OAAO,QAAQ,SAAS;EACxB,QAAQ,QAAQ,UAAU;EAC1B,GAIF,CAAC,QAAQ,QAAQ,QAAQ,MAAM,CAC/B;CAED,MAAM,EACL,SAAS,cACT,WAAW,cACX,UACG,eAGF;EACD;EACA,UAAU,iBACP,YAAY,eAAe,GAAG,SAAS,MAAM,GAAG,SAAS,UAAU,OACnE;EACH,UAAU,UAAU,SAAS;AAC5B,OAAI,CAAC,eACJ,QAAO,QAAQ,QAAQ;IACtB,OAAO,EAAE;IACT,aAAa;IACb,YAAY;IACZ,CAAC;AAGH,UAAO,SAAS,6BAA6B;IAC5C;IACA,OAAO,MAAM,SAAS,SAAS;IAC/B,QAAQ,MAAM,UAAU,SAAS,UAAU;IAC3C,CAAC;;EAEH,SAAS,QAAQ,eAAe,KAAK,QAAQ,WAAW;EACxD,iBAAiB,QAAQ,mBAAmB;EAC5C,sBAAsB,QAAQ,wBAAwB;EACtD,gBAAgB,UAAU,MAAM,WAAW;EAC3C,aAAa;EACb,cAAc;GACb;GACA,SAAS;GACT,SAAS,UAAU;GACnB;EACD,CAAC;CAEF,MAAM,UAAU,aACd,SAAyE;AACzE,MAAI,CAAC,eACJ,QAAO,QAAQ,QAAQ,OAAU;AAGlC,SAAO,aAAa;GACnB,OAAO,SAAS;GAChB,QAAQ,SAAS;GACjB,GAAG;GACH,CAAC;IAEH;EAAC;EAAc;EAAU;EAAe,CACxC;CAED,MAAM,gBAAgB,kBAAkB;AACvC,MAAI,EAAE,UAAU,eAAe,UAAU,YACxC,QAAO,QAAQ,QAAQ,OAAU;AAGlC,SAAO,QAAQ;GAAE,QAAQ,UAAU;GAAY,OAAO,SAAS;GAAO,CAAC;IACrE;EAAC,UAAU;EAAa,UAAU;EAAY;EAAS,SAAS;EAAM,CAAC;CAG1E,MAAM,YADgB,UAAU,MAAM,WAAW,IACf,eAAe;AAEjD,QAAO;EACN,OAAO,UAAU;EACjB,aAAa,UAAU;EACvB,YAAY,UAAU;EACtB;EACA;EACA;EACA;EACA"}
@@ -1,4 +1,4 @@
1
- import { TimelineItem } from "../timeline-item.js";
1
+ import { TimelineItem } from "../packages/types/src/api/timeline-item.js";
2
2
  import { useGroupedMessages } from "./private/use-grouped-messages.js";
3
3
  import { TimelineTypingParticipant } from "./private/typing.js";
4
4
  import { useDebouncedConversationSeen } from "./use-conversation-seen.js";
@@ -1 +1 @@
1
- {"version":3,"file":"use-conversation-timeline.d.ts","names":[],"sources":["../../src/hooks/use-conversation-timeline.ts"],"sourcesContent":[],"mappings":";;;;;;;KAYY,qCAAA,GAAwC;KAExC,8BAAA;EAFA,cAAA,EAAA,MAAA;EAEA,KAAA,EAEJ,YAFI,EAAA;EAMA,gBAAA,CAAA,EAAA,MAAA;CACwB;AAAlB,KADN,6BAAA,GACM;EACW,eAAA,EADX,UACW,CAAA,OADO,kBACP,CAAA;EAAlB,QAAA,EAAA,UAAA,CAAA,OAAkB,4BAAlB,CAAA;EACuB,aAAA,EAAlB,UAAkB,CAAA,OAAA,qBAAA,CAAA;EAAlB,kBAAA,EACK,qCADL,EAAA;EACK,4BAAA,EAAA,MAAA;CAAqC;AAQ1D;;;;AAIG,iBAJa,uBAAA,CAIb;EAAA,cAAA;EAAA,KAAA,EAFK,aAEL;EAAA;AAAA,CAAA,EAAA,8BAAA,CAAA,EAAiC,6BAAjC"}
1
+ {"version":3,"file":"use-conversation-timeline.d.ts","names":[],"sources":["../../src/hooks/use-conversation-timeline.ts"],"sourcesContent":[],"mappings":";;;;;;;KAWY,qCAAA,GAAwC;KAExC,8BAAA;EAFA,cAAA,EAAA,MAAA;EAEA,KAAA,EAEJ,YAFI,EAAA;EAMA,gBAAA,CAAA,EAAA,MAAA;CACwB;AAAlB,KADN,6BAAA,GACM;EACW,eAAA,EADX,UACW,CAAA,OADO,kBACP,CAAA;EAAlB,QAAA,EAAA,UAAA,CAAA,OAAkB,4BAAlB,CAAA;EACuB,aAAA,EAAlB,UAAkB,CAAA,OAAA,qBAAA,CAAA;EAAlB,kBAAA,EACK,qCADL,EAAA;EACK,4BAAA,EAAA,MAAA;CAAqC;AAQ1D;;;;AAIG,iBAJa,uBAAA,CAIb;EAAA,cAAA;EAAA,KAAA,EAFK,aAEL;EAAA;AAAA,CAAA,EAAA,8BAAA,CAAA,EAAiC,6BAAjC"}
@@ -3,7 +3,6 @@ import { useGroupedMessages } from "./private/use-grouped-messages.js";
3
3
  import { useDebouncedConversationSeen } from "./use-conversation-seen.js";
4
4
  import { useConversationTyping } from "./use-conversation-typing.js";
5
5
  import { useMemo } from "react";
6
- import { SenderType } from "@cossistant/types";
7
6
 
8
7
  //#region src/hooks/use-conversation-timeline.ts
9
8
  /**
@@ -16,8 +15,7 @@ function useConversationTimeline({ conversationId, items: timelineItems, current
16
15
  const groupedMessages = useGroupedMessages({
17
16
  items: timelineItems,
18
17
  seenData,
19
- currentViewerId: currentVisitorId,
20
- viewerType: SenderType.VISITOR
18
+ currentViewerId: currentVisitorId
21
19
  });
22
20
  const lastVisitorMessageGroupIndex = useMemo(() => {
23
21
  for (let index = groupedMessages.items.length - 1; index >= 0; index--) {