@cossistant/react 0.0.26 → 0.0.29

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 (247) hide show
  1. package/README.md +1 -1
  2. package/api.d.ts +1 -1
  3. package/api.d.ts.map +1 -1
  4. package/checks.d.ts +1 -1
  5. package/checks.d.ts.map +1 -1
  6. package/coerce.d.ts +1 -1
  7. package/coerce.d.ts.map +1 -1
  8. package/conversation.d.ts +6 -3
  9. package/conversation.d.ts.map +1 -1
  10. package/core.d.ts +1 -1
  11. package/core.d.ts.map +1 -1
  12. package/errors.d.ts +12 -3
  13. package/errors.d.ts.map +1 -1
  14. package/errors2.d.ts +1 -1
  15. package/errors2.d.ts.map +1 -1
  16. package/hooks/index.d.ts +3 -2
  17. package/hooks/index.js +6 -5
  18. package/hooks/private/store/use-website-store.js +2 -1
  19. package/hooks/private/store/use-website-store.js.map +1 -1
  20. package/hooks/private/use-client-query.d.ts +6 -0
  21. package/hooks/private/use-client-query.d.ts.map +1 -1
  22. package/hooks/private/use-client-query.js +26 -3
  23. package/hooks/private/use-client-query.js.map +1 -1
  24. package/hooks/private/use-grouped-messages.d.ts +8 -5
  25. package/hooks/private/use-grouped-messages.d.ts.map +1 -1
  26. package/hooks/private/use-grouped-messages.js +44 -11
  27. package/hooks/private/use-grouped-messages.js.map +1 -1
  28. package/hooks/private/use-multimodal-input.d.ts.map +1 -1
  29. package/hooks/private/use-multimodal-input.js +7 -5
  30. package/hooks/private/use-multimodal-input.js.map +1 -1
  31. package/hooks/private/use-visitor-typing-reporter.d.ts +18 -1
  32. package/hooks/private/use-visitor-typing-reporter.d.ts.map +1 -1
  33. package/hooks/private/use-visitor-typing-reporter.js +34 -4
  34. package/hooks/private/use-visitor-typing-reporter.js.map +1 -1
  35. package/hooks/use-conversation-page.d.ts +1 -0
  36. package/hooks/use-conversation-page.d.ts.map +1 -1
  37. package/hooks/use-conversation-page.js +6 -1
  38. package/hooks/use-conversation-page.js.map +1 -1
  39. package/hooks/use-conversation-preview.d.ts +2 -1
  40. package/hooks/use-conversation-preview.d.ts.map +1 -1
  41. package/hooks/use-conversation-preview.js +1 -1
  42. package/hooks/use-conversation-preview.js.map +1 -1
  43. package/hooks/use-conversation-seen.js +1 -1
  44. package/hooks/use-conversation-seen.js.map +1 -1
  45. package/hooks/use-conversation-timeline-items.js +2 -1
  46. package/hooks/use-conversation-timeline-items.js.map +1 -1
  47. package/hooks/use-conversation-timeline.d.ts.map +1 -1
  48. package/hooks/use-conversation-timeline.js +1 -3
  49. package/hooks/use-conversation-timeline.js.map +1 -1
  50. package/hooks/use-conversation.js +2 -1
  51. package/hooks/use-conversation.js.map +1 -1
  52. package/hooks/use-conversations.js +1 -0
  53. package/hooks/use-conversations.js.map +1 -1
  54. package/hooks/use-create-conversation.d.ts.map +1 -1
  55. package/hooks/use-file-upload.d.ts +55 -0
  56. package/hooks/use-file-upload.d.ts.map +1 -0
  57. package/hooks/use-file-upload.js +100 -0
  58. package/hooks/use-file-upload.js.map +1 -0
  59. package/hooks/use-message-composer.d.ts +11 -0
  60. package/hooks/use-message-composer.d.ts.map +1 -1
  61. package/hooks/use-message-composer.js +7 -3
  62. package/hooks/use-message-composer.js.map +1 -1
  63. package/hooks/use-send-message.d.ts +1 -0
  64. package/hooks/use-send-message.d.ts.map +1 -1
  65. package/hooks/use-send-message.js +63 -11
  66. package/hooks/use-send-message.js.map +1 -1
  67. package/index.d.ts +7 -4
  68. package/index.js +13 -10
  69. package/json-schema.d.ts +70 -0
  70. package/json-schema.d.ts.map +1 -0
  71. package/package.json +4 -3
  72. package/parse.d.ts +1 -1
  73. package/parse.d.ts.map +1 -1
  74. package/primitives/avatar/fallback.d.ts.map +1 -1
  75. package/primitives/avatar/fallback.js +1 -1
  76. package/primitives/avatar/fallback.js.map +1 -1
  77. package/primitives/conversation-timeline.d.ts.map +1 -1
  78. package/primitives/conversation-timeline.js +10 -5
  79. package/primitives/conversation-timeline.js.map +1 -1
  80. package/primitives/day-separator.d.ts +76 -0
  81. package/primitives/day-separator.d.ts.map +1 -0
  82. package/primitives/day-separator.js +111 -0
  83. package/primitives/day-separator.js.map +1 -0
  84. package/primitives/index.d.ts +5 -3
  85. package/primitives/index.js +17 -5
  86. package/primitives/index.parts.d.ts +4 -2
  87. package/primitives/index.parts.js +5 -3
  88. package/primitives/timeline-item-attachments.d.ts +100 -0
  89. package/primitives/timeline-item-attachments.d.ts.map +1 -0
  90. package/primitives/timeline-item-attachments.js +151 -0
  91. package/primitives/timeline-item-attachments.js.map +1 -0
  92. package/primitives/timeline-item-group.d.ts.map +1 -1
  93. package/primitives/timeline-item-group.js +1 -1
  94. package/primitives/timeline-item-group.js.map +1 -1
  95. package/primitives/timeline-item.js +1 -1
  96. package/primitives/timeline-item.js.map +1 -1
  97. package/primitives/trigger.d.ts +91 -0
  98. package/primitives/trigger.d.ts.map +1 -0
  99. package/primitives/trigger.js +74 -0
  100. package/primitives/trigger.js.map +1 -0
  101. package/primitives/window.d.ts +22 -1
  102. package/primitives/window.d.ts.map +1 -1
  103. package/primitives/window.js +91 -5
  104. package/primitives/window.js.map +1 -1
  105. package/provider.d.ts.map +1 -1
  106. package/provider.js +8 -3
  107. package/provider.js.map +1 -1
  108. package/realtime/index.js +1 -1
  109. package/realtime/provider.js +1 -1
  110. package/realtime/support-provider.js +5 -1
  111. package/realtime/support-provider.js.map +1 -1
  112. package/realtime-events.d.ts +165 -2
  113. package/realtime-events.d.ts.map +1 -1
  114. package/registries.d.ts +1 -1
  115. package/registries.d.ts.map +1 -1
  116. package/schemas.d.ts +305 -7
  117. package/schemas.d.ts.map +1 -1
  118. package/schemas2.d.ts +29 -4
  119. package/schemas2.d.ts.map +1 -1
  120. package/schemas3.d.ts +2 -1
  121. package/schemas3.d.ts.map +1 -1
  122. package/standard-schema.d.ts +83 -21
  123. package/standard-schema.d.ts.map +1 -1
  124. package/support/components/button.d.ts +1 -1
  125. package/support/components/content.d.ts +30 -0
  126. package/support/components/content.d.ts.map +1 -0
  127. package/support/components/content.js +282 -0
  128. package/support/components/content.js.map +1 -0
  129. package/support/components/conversation-button-link.js +1 -1
  130. package/support/components/conversation-timeline.d.ts +5 -0
  131. package/support/components/conversation-timeline.d.ts.map +1 -1
  132. package/support/components/conversation-timeline.js +25 -5
  133. package/support/components/conversation-timeline.js.map +1 -1
  134. package/support/components/header.js +1 -1
  135. package/support/components/image-lightbox.d.ts +49 -0
  136. package/support/components/image-lightbox.d.ts.map +1 -0
  137. package/support/components/image-lightbox.js +142 -0
  138. package/support/components/image-lightbox.js.map +1 -0
  139. package/support/components/index.d.ts +5 -4
  140. package/support/components/index.js +4 -4
  141. package/support/components/multimodal-input.d.ts +4 -1
  142. package/support/components/multimodal-input.d.ts.map +1 -1
  143. package/support/components/multimodal-input.js +71 -45
  144. package/support/components/multimodal-input.js.map +1 -1
  145. package/support/components/navigation-tab.js +1 -1
  146. package/support/components/root.d.ts +23 -0
  147. package/support/components/root.d.ts.map +1 -0
  148. package/support/components/root.js +36 -0
  149. package/support/components/root.js.map +1 -0
  150. package/support/components/timeline-message-item.d.ts.map +1 -1
  151. package/support/components/timeline-message-item.js +82 -18
  152. package/support/components/timeline-message-item.js.map +1 -1
  153. package/support/components/trigger.d.ts +14 -0
  154. package/support/components/trigger.d.ts.map +1 -0
  155. package/support/components/{bubble.js → trigger.js} +16 -12
  156. package/support/components/trigger.js.map +1 -0
  157. package/support/components/typing-indicator.d.ts.map +1 -1
  158. package/support/components/typing-indicator.js +1 -0
  159. package/support/components/typing-indicator.js.map +1 -1
  160. package/support/context/controlled-state.d.ts +46 -0
  161. package/support/context/controlled-state.d.ts.map +1 -0
  162. package/support/context/controlled-state.js +34 -0
  163. package/support/context/controlled-state.js.map +1 -0
  164. package/support/context/events.d.ts +103 -0
  165. package/support/context/events.d.ts.map +1 -0
  166. package/support/context/events.js +139 -0
  167. package/support/context/events.js.map +1 -0
  168. package/support/context/handle.d.ts +90 -0
  169. package/support/context/handle.d.ts.map +1 -0
  170. package/support/context/handle.js +79 -0
  171. package/support/context/handle.js.map +1 -0
  172. package/support/context/positioning.d.ts +17 -0
  173. package/support/context/positioning.d.ts.map +1 -0
  174. package/support/context/positioning.js +26 -0
  175. package/support/context/positioning.js.map +1 -0
  176. package/support/context/slots.d.ts +85 -0
  177. package/support/context/slots.d.ts.map +1 -0
  178. package/support/context/slots.js +115 -0
  179. package/support/context/slots.js.map +1 -0
  180. package/support/context/websocket.d.ts +8 -1
  181. package/support/context/websocket.d.ts.map +1 -1
  182. package/support/context/websocket.js +8 -1
  183. package/support/context/websocket.js.map +1 -1
  184. package/support/index.d.ts +239 -54
  185. package/support/index.d.ts.map +1 -1
  186. package/support/index.js +254 -33
  187. package/support/index.js.map +1 -1
  188. package/support/pages/articles.d.ts.map +1 -1
  189. package/support/pages/articles.js +3 -4
  190. package/support/pages/articles.js.map +1 -1
  191. package/support/pages/conversation-history.js +2 -2
  192. package/support/pages/conversation.js +6 -5
  193. package/support/pages/conversation.js.map +1 -1
  194. package/support/pages/home.js +2 -2
  195. package/support/router.d.ts +52 -12
  196. package/support/router.d.ts.map +1 -1
  197. package/support/router.js +78 -30
  198. package/support/router.js.map +1 -1
  199. package/support/store/index.d.ts +2 -2
  200. package/support/store/support-store.d.ts +26 -20
  201. package/support/store/support-store.d.ts.map +1 -1
  202. package/support/store/support-store.js +47 -6
  203. package/support/store/support-store.js.map +1 -1
  204. package/support/{support-D2EgfIts.css → support-C7Xaw-N6.css} +1 -2
  205. package/support/support-C7Xaw-N6.css.map +1 -0
  206. package/support/text/index.d.ts +1 -1
  207. package/support/text/index.d.ts.map +1 -1
  208. package/support/text/index.js.map +1 -1
  209. package/support/types.d.ts +75 -12
  210. package/support/types.d.ts.map +1 -1
  211. package/support.css +2 -2
  212. package/tailwind.css +0 -1
  213. package/timeline-item.d.ts +68 -2
  214. package/timeline-item.d.ts.map +1 -1
  215. package/to-json-schema.d.ts +96 -0
  216. package/to-json-schema.d.ts.map +1 -0
  217. package/util.d.ts +6 -2
  218. package/util.d.ts.map +1 -1
  219. package/utils/index.d.ts +2 -1
  220. package/utils/index.js +2 -1
  221. package/utils/merge-refs.d.ts +30 -0
  222. package/utils/merge-refs.d.ts.map +1 -0
  223. package/utils/merge-refs.js +46 -0
  224. package/utils/merge-refs.js.map +1 -0
  225. package/utils/use-render-element.d.ts.map +1 -1
  226. package/utils/use-render-element.js +36 -8
  227. package/utils/use-render-element.js.map +1 -1
  228. package/versions.d.ts +2 -2
  229. package/versions.d.ts.map +1 -1
  230. package/zod-extensions.d.ts +1 -1
  231. package/zod-extensions.d.ts.map +1 -1
  232. package/primitives/bubble.d.ts +0 -38
  233. package/primitives/bubble.d.ts.map +0 -1
  234. package/primitives/bubble.js +0 -57
  235. package/primitives/bubble.js.map +0 -1
  236. package/support/components/bubble.d.ts +0 -10
  237. package/support/components/bubble.d.ts.map +0 -1
  238. package/support/components/bubble.js.map +0 -1
  239. package/support/components/container.d.ts +0 -13
  240. package/support/components/container.d.ts.map +0 -1
  241. package/support/components/container.js +0 -109
  242. package/support/components/container.js.map +0 -1
  243. package/support/components/support-content.d.ts +0 -22
  244. package/support/components/support-content.d.ts.map +0 -1
  245. package/support/components/support-content.js +0 -48
  246. package/support/components/support-content.js.map +0 -1
  247. package/support/support-D2EgfIts.css.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"use-multimodal-input.js","names":[],"sources":["../../../src/hooks/private/use-multimodal-input.ts"],"sourcesContent":["import { useCallback, useRef, useState } from \"react\";\n\nexport type UseMultimodalInputOptions = {\n\tonSubmit?: (data: { message: string; files: File[] }) => void | Promise<void>;\n\tonError?: (error: Error) => void;\n\tmaxFileSize?: number; // in bytes\n\tmaxFiles?: number;\n\tallowedFileTypes?: string[]; // MIME types\n};\n\nexport type UseMultimodalInputReturn = {\n\t// State\n\tmessage: string;\n\tfiles: File[];\n\tisSubmitting: boolean;\n\terror: Error | null;\n\n\t// Actions\n\tsetMessage: (message: string) => void;\n\taddFiles: (files: File[]) => void;\n\tremoveFile: (index: number) => void;\n\tclearFiles: () => void;\n\tsubmit: () => Promise<void>;\n\treset: () => void;\n\n\t// Validation\n\tisValid: boolean;\n\tcanSubmit: boolean;\n};\n\n/**\n * Manages message text, file attachments and validation for the multimodal\n * composer component. Provides ergonomic helpers for submit flows and error\n * reporting.\n */\nexport const useMultimodalInput = ({\n\tonSubmit,\n\tonError,\n\tmaxFileSize = 10 * 1024 * 1024, // 10MB default\n\tmaxFiles = 5,\n\tallowedFileTypes = [\"image/*\", \"application/pdf\", \"text/*\"],\n}: UseMultimodalInputOptions = {}): UseMultimodalInputReturn => {\n\tconst [message, setMessage] = useState(\"\");\n\tconst [files, setFiles] = useState<File[]>([]);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\t// Use ref to prevent re-renders when tracking file URLs\n\tconst fileUrlsRef = useRef<string[]>([]);\n\n\t// Validation helpers\n\tconst validateFile = useCallback(\n\t\t(file: File): string | null => {\n\t\t\tif (file.size > maxFileSize) {\n\t\t\t\treturn `File \"${file.name}\" exceeds maximum size of ${maxFileSize / 1024 / 1024}MB`;\n\t\t\t}\n\n\t\t\tif (allowedFileTypes.length > 0) {\n\t\t\t\tconst isAllowed = allowedFileTypes.some((type) => {\n\t\t\t\t\tif (type.endsWith(\"/*\")) {\n\t\t\t\t\t\tconst baseType = type.slice(0, -2);\n\t\t\t\t\t\treturn file.type.startsWith(baseType);\n\t\t\t\t\t}\n\t\t\t\t\treturn file.type === type;\n\t\t\t\t});\n\n\t\t\t\tif (!isAllowed) {\n\t\t\t\t\treturn `File type \"${file.type}\" is not allowed`;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn null;\n\t\t},\n\t\t[maxFileSize, allowedFileTypes]\n\t);\n\n\t// Actions\n\tconst addFiles = useCallback(\n\t\t(newFiles: File[]) => {\n\t\t\tsetError(null);\n\n\t\t\t// Check max files limit\n\t\t\tif (files.length + newFiles.length > maxFiles) {\n\t\t\t\tconst err = new Error(\n\t\t\t\t\t`Cannot add files: maximum ${maxFiles} files allowed`\n\t\t\t\t);\n\t\t\t\tsetError(err);\n\t\t\t\tonError?.(err);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Validate each file\n\t\t\tfor (const file of newFiles) {\n\t\t\t\tconst validationError = validateFile(file);\n\t\t\t\tif (validationError) {\n\t\t\t\t\tconst err = new Error(validationError);\n\t\t\t\t\tsetError(err);\n\t\t\t\t\tonError?.(err);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tsetFiles((prev) => [...prev, ...newFiles]);\n\t\t},\n\t\t[files.length, maxFiles, validateFile, onError]\n\t);\n\n\tconst removeFile = useCallback((index: number) => {\n\t\tsetFiles((prev) => {\n\t\t\tconst newFiles = [...prev];\n\t\t\tnewFiles.splice(index, 1);\n\n\t\t\t// Clean up object URL if it exists\n\t\t\tif (fileUrlsRef.current[index]) {\n\t\t\t\tURL.revokeObjectURL(fileUrlsRef.current[index]);\n\t\t\t\tfileUrlsRef.current.splice(index, 1);\n\t\t\t}\n\n\t\t\treturn newFiles;\n\t\t});\n\t\tsetError(null);\n\t}, []);\n\n\tconst clearFiles = useCallback(() => {\n\t\t// Clean up all object URLs\n\t\tfor (const url of fileUrlsRef.current) {\n\t\t\tURL.revokeObjectURL(url);\n\t\t}\n\t\tfileUrlsRef.current = [];\n\n\t\tsetFiles([]);\n\t\tsetError(null);\n\t}, []);\n\n\tconst reset = useCallback(() => {\n\t\tsetMessage(\"\");\n\t\tclearFiles();\n\t\tsetError(null);\n\t\tsetIsSubmitting(false);\n\t}, [clearFiles]);\n\n\tconst submit = useCallback(async () => {\n\t\tif (!onSubmit) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst trimmedMessage = message.trim();\n\t\tif (!trimmedMessage && files.length === 0) {\n\t\t\tconst err = new Error(\"Please provide a message or attach files\");\n\t\t\tsetError(err);\n\t\t\tonError?.(err);\n\t\t\treturn;\n\t\t}\n\n\t\tconst previousState = {\n\t\t\tmessage,\n\t\t\tfiles,\n\t\t\tfileUrls: [...fileUrlsRef.current],\n\t\t};\n\n\t\tsetIsSubmitting(true);\n\t\tsetError(null);\n\t\tsetMessage(\"\");\n\t\tsetFiles([]);\n\t\tfileUrlsRef.current = [];\n\n\t\ttry {\n\t\t\tawait onSubmit({ message: trimmedMessage, files: previousState.files });\n\n\t\t\tfor (const url of previousState.fileUrls) {\n\t\t\t\tURL.revokeObjectURL(url);\n\t\t\t}\n\t\t\treset();\n\t\t} catch (err) {\n\t\t\tsetMessage(previousState.message);\n\t\t\tsetFiles(previousState.files);\n\t\t\tfileUrlsRef.current = previousState.fileUrls;\n\n\t\t\tconst _error = err instanceof Error ? err : new Error(\"Failed to submit\");\n\t\t\tsetError(_error);\n\t\t\tonError?.(_error);\n\t\t} finally {\n\t\t\tsetIsSubmitting(false);\n\t\t}\n\t}, [message, files, onSubmit, onError, reset]);\n\n\t// Computed values\n\tconst isValid = message.trim().length > 0 || files.length > 0;\n\tconst canSubmit = isValid && !isSubmitting && !error;\n\n\treturn {\n\t\t// State\n\t\tmessage,\n\t\tfiles,\n\t\tisSubmitting,\n\t\terror,\n\n\t\t// Actions\n\t\tsetMessage,\n\t\taddFiles,\n\t\tremoveFile,\n\t\tclearFiles,\n\t\tsubmit,\n\t\treset,\n\n\t\t// Validation\n\t\tisValid,\n\t\tcanSubmit,\n\t};\n};\n"],"mappings":";;;;;;;;AAmCA,MAAa,sBAAsB,EAClC,UACA,SACA,cAAc,KAAK,OAAO,MAC1B,WAAW,GACX,mBAAmB;CAAC;CAAW;CAAmB;CAAS,KAC7B,EAAE,KAA+B;CAC/D,MAAM,CAAC,SAAS,cAAc,SAAS,GAAG;CAC1C,MAAM,CAAC,OAAO,YAAY,SAAiB,EAAE,CAAC;CAC9C,MAAM,CAAC,cAAc,mBAAmB,SAAS,MAAM;CACvD,MAAM,CAAC,OAAO,YAAY,SAAuB,KAAK;CAGtD,MAAM,cAAc,OAAiB,EAAE,CAAC;CAGxC,MAAM,eAAe,aACnB,SAA8B;AAC9B,MAAI,KAAK,OAAO,YACf,QAAO,SAAS,KAAK,KAAK,4BAA4B,cAAc,OAAO,KAAK;AAGjF,MAAI,iBAAiB,SAAS,GAS7B;OAAI,CARc,iBAAiB,MAAM,SAAS;AACjD,QAAI,KAAK,SAAS,KAAK,EAAE;KACxB,MAAM,WAAW,KAAK,MAAM,GAAG,GAAG;AAClC,YAAO,KAAK,KAAK,WAAW,SAAS;;AAEtC,WAAO,KAAK,SAAS;KACpB,CAGD,QAAO,cAAc,KAAK,KAAK;;AAIjC,SAAO;IAER,CAAC,aAAa,iBAAiB,CAC/B;CAGD,MAAM,WAAW,aACf,aAAqB;AACrB,WAAS,KAAK;AAGd,MAAI,MAAM,SAAS,SAAS,SAAS,UAAU;GAC9C,MAAM,sBAAM,IAAI,MACf,6BAA6B,SAAS,gBACtC;AACD,YAAS,IAAI;AACb,aAAU,IAAI;AACd;;AAID,OAAK,MAAM,QAAQ,UAAU;GAC5B,MAAM,kBAAkB,aAAa,KAAK;AAC1C,OAAI,iBAAiB;IACpB,MAAM,MAAM,IAAI,MAAM,gBAAgB;AACtC,aAAS,IAAI;AACb,cAAU,IAAI;AACd;;;AAIF,YAAU,SAAS,CAAC,GAAG,MAAM,GAAG,SAAS,CAAC;IAE3C;EAAC,MAAM;EAAQ;EAAU;EAAc;EAAQ,CAC/C;CAED,MAAM,aAAa,aAAa,UAAkB;AACjD,YAAU,SAAS;GAClB,MAAM,WAAW,CAAC,GAAG,KAAK;AAC1B,YAAS,OAAO,OAAO,EAAE;AAGzB,OAAI,YAAY,QAAQ,QAAQ;AAC/B,QAAI,gBAAgB,YAAY,QAAQ,OAAO;AAC/C,gBAAY,QAAQ,OAAO,OAAO,EAAE;;AAGrC,UAAO;IACN;AACF,WAAS,KAAK;IACZ,EAAE,CAAC;CAEN,MAAM,aAAa,kBAAkB;AAEpC,OAAK,MAAM,OAAO,YAAY,QAC7B,KAAI,gBAAgB,IAAI;AAEzB,cAAY,UAAU,EAAE;AAExB,WAAS,EAAE,CAAC;AACZ,WAAS,KAAK;IACZ,EAAE,CAAC;CAEN,MAAM,QAAQ,kBAAkB;AAC/B,aAAW,GAAG;AACd,cAAY;AACZ,WAAS,KAAK;AACd,kBAAgB,MAAM;IACpB,CAAC,WAAW,CAAC;CAEhB,MAAM,SAAS,YAAY,YAAY;AACtC,MAAI,CAAC,SACJ;EAGD,MAAM,iBAAiB,QAAQ,MAAM;AACrC,MAAI,CAAC,kBAAkB,MAAM,WAAW,GAAG;GAC1C,MAAM,sBAAM,IAAI,MAAM,2CAA2C;AACjE,YAAS,IAAI;AACb,aAAU,IAAI;AACd;;EAGD,MAAM,gBAAgB;GACrB;GACA;GACA,UAAU,CAAC,GAAG,YAAY,QAAQ;GAClC;AAED,kBAAgB,KAAK;AACrB,WAAS,KAAK;AACd,aAAW,GAAG;AACd,WAAS,EAAE,CAAC;AACZ,cAAY,UAAU,EAAE;AAExB,MAAI;AACH,SAAM,SAAS;IAAE,SAAS;IAAgB,OAAO,cAAc;IAAO,CAAC;AAEvE,QAAK,MAAM,OAAO,cAAc,SAC/B,KAAI,gBAAgB,IAAI;AAEzB,UAAO;WACC,KAAK;AACb,cAAW,cAAc,QAAQ;AACjC,YAAS,cAAc,MAAM;AAC7B,eAAY,UAAU,cAAc;GAEpC,MAAM,SAAS,eAAe,QAAQ,sBAAM,IAAI,MAAM,mBAAmB;AACzE,YAAS,OAAO;AAChB,aAAU,OAAO;YACR;AACT,mBAAgB,MAAM;;IAErB;EAAC;EAAS;EAAO;EAAU;EAAS;EAAM,CAAC;CAG9C,MAAM,UAAU,QAAQ,MAAM,CAAC,SAAS,KAAK,MAAM,SAAS;AAG5D,QAAO;EAEN;EACA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA,WAnBiB,WAAW,CAAC,gBAAgB,CAAC;EAoB9C"}
1
+ {"version":3,"file":"use-multimodal-input.js","names":[],"sources":["../../../src/hooks/private/use-multimodal-input.ts"],"sourcesContent":["import { ALLOWED_MIME_TYPES } from \"@cossistant/core\";\nimport { useCallback, useRef, useState } from \"react\";\n\n// Convert ALLOWED_MIME_TYPES to validation-friendly format with wildcards for image/* and text/*\nconst DEFAULT_ALLOWED_FILE_TYPES = [\n\t...ALLOWED_MIME_TYPES.filter(\n\t\t(type) => !(type.startsWith(\"image/\") || type.startsWith(\"text/\"))\n\t),\n\t\"image/*\",\n\t\"text/*\",\n];\n\nexport type UseMultimodalInputOptions = {\n\tonSubmit?: (data: { message: string; files: File[] }) => void | Promise<void>;\n\tonError?: (error: Error) => void;\n\tmaxFileSize?: number; // in bytes\n\tmaxFiles?: number;\n\tallowedFileTypes?: string[]; // MIME types\n};\n\nexport type UseMultimodalInputReturn = {\n\t// State\n\tmessage: string;\n\tfiles: File[];\n\tisSubmitting: boolean;\n\terror: Error | null;\n\n\t// Actions\n\tsetMessage: (message: string) => void;\n\taddFiles: (files: File[]) => void;\n\tremoveFile: (index: number) => void;\n\tclearFiles: () => void;\n\tsubmit: () => Promise<void>;\n\treset: () => void;\n\n\t// Validation\n\tisValid: boolean;\n\tcanSubmit: boolean;\n};\n\n/**\n * Manages message text, file attachments and validation for the multimodal\n * composer component. Provides ergonomic helpers for submit flows and error\n * reporting.\n */\nexport const useMultimodalInput = ({\n\tonSubmit,\n\tonError,\n\tmaxFileSize = 10 * 1024 * 1024, // 10MB default\n\tmaxFiles = 5,\n\tallowedFileTypes = DEFAULT_ALLOWED_FILE_TYPES,\n}: UseMultimodalInputOptions = {}): UseMultimodalInputReturn => {\n\tconst [message, setMessage] = useState(\"\");\n\tconst [files, setFiles] = useState<File[]>([]);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\t// Use ref to prevent re-renders when tracking file URLs\n\tconst fileUrlsRef = useRef<string[]>([]);\n\n\t// Validation helpers\n\tconst validateFile = useCallback(\n\t\t(file: File): string | null => {\n\t\t\tif (file.size > maxFileSize) {\n\t\t\t\treturn `File \"${file.name}\" exceeds maximum size of ${maxFileSize / 1024 / 1024}MB`;\n\t\t\t}\n\n\t\t\tif (allowedFileTypes.length > 0) {\n\t\t\t\tconst isAllowed = allowedFileTypes.some((type) => {\n\t\t\t\t\tif (type.endsWith(\"/*\")) {\n\t\t\t\t\t\tconst baseType = type.slice(0, -2);\n\t\t\t\t\t\treturn file.type.startsWith(baseType);\n\t\t\t\t\t}\n\t\t\t\t\treturn file.type === type;\n\t\t\t\t});\n\n\t\t\t\tif (!isAllowed) {\n\t\t\t\t\treturn `File type \"${file.type}\" is not allowed`;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn null;\n\t\t},\n\t\t[maxFileSize, allowedFileTypes]\n\t);\n\n\t// Actions\n\tconst addFiles = useCallback(\n\t\t(newFiles: File[]) => {\n\t\t\tsetError(null);\n\n\t\t\t// Check max files limit\n\t\t\tif (files.length + newFiles.length > maxFiles) {\n\t\t\t\tconst err = new Error(\n\t\t\t\t\t`Cannot add files: maximum ${maxFiles} files allowed`\n\t\t\t\t);\n\t\t\t\tsetError(err);\n\t\t\t\tonError?.(err);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Validate each file\n\t\t\tfor (const file of newFiles) {\n\t\t\t\tconst validationError = validateFile(file);\n\t\t\t\tif (validationError) {\n\t\t\t\t\tconst err = new Error(validationError);\n\t\t\t\t\tsetError(err);\n\t\t\t\t\tonError?.(err);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tsetFiles((prev) => [...prev, ...newFiles]);\n\t\t},\n\t\t[files.length, maxFiles, validateFile, onError]\n\t);\n\n\tconst removeFile = useCallback((index: number) => {\n\t\tsetFiles((prev) => {\n\t\t\tconst newFiles = [...prev];\n\t\t\tnewFiles.splice(index, 1);\n\n\t\t\t// Clean up object URL if it exists\n\t\t\tif (fileUrlsRef.current[index]) {\n\t\t\t\tURL.revokeObjectURL(fileUrlsRef.current[index]);\n\t\t\t\tfileUrlsRef.current.splice(index, 1);\n\t\t\t}\n\n\t\t\treturn newFiles;\n\t\t});\n\t\tsetError(null);\n\t}, []);\n\n\tconst clearFiles = useCallback(() => {\n\t\t// Clean up all object URLs\n\t\tfor (const url of fileUrlsRef.current) {\n\t\t\tURL.revokeObjectURL(url);\n\t\t}\n\t\tfileUrlsRef.current = [];\n\n\t\tsetFiles([]);\n\t\tsetError(null);\n\t}, []);\n\n\tconst reset = useCallback(() => {\n\t\tsetMessage(\"\");\n\t\tclearFiles();\n\t\tsetError(null);\n\t\tsetIsSubmitting(false);\n\t}, [clearFiles]);\n\n\tconst submit = useCallback(async () => {\n\t\tif (!onSubmit) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst trimmedMessage = message.trim();\n\t\tif (!trimmedMessage && files.length === 0) {\n\t\t\tconst err = new Error(\"Please provide a message or attach files\");\n\t\t\tsetError(err);\n\t\t\tonError?.(err);\n\t\t\treturn;\n\t\t}\n\n\t\tconst previousState = {\n\t\t\tmessage,\n\t\t\tfiles,\n\t\t\tfileUrls: [...fileUrlsRef.current],\n\t\t};\n\n\t\tsetIsSubmitting(true);\n\t\tsetError(null);\n\t\tsetMessage(\"\");\n\t\tsetFiles([]);\n\t\tfileUrlsRef.current = [];\n\n\t\ttry {\n\t\t\tawait onSubmit({ message: trimmedMessage, files: previousState.files });\n\n\t\t\tfor (const url of previousState.fileUrls) {\n\t\t\t\tURL.revokeObjectURL(url);\n\t\t\t}\n\t\t\treset();\n\t\t} catch (err) {\n\t\t\tsetMessage(previousState.message);\n\t\t\tsetFiles(previousState.files);\n\t\t\tfileUrlsRef.current = previousState.fileUrls;\n\n\t\t\tconst _error = err instanceof Error ? err : new Error(\"Failed to submit\");\n\t\t\tsetError(_error);\n\t\t\tonError?.(_error);\n\t\t} finally {\n\t\t\tsetIsSubmitting(false);\n\t\t}\n\t}, [message, files, onSubmit, onError, reset]);\n\n\t// Computed values\n\tconst isValid = message.trim().length > 0 || files.length > 0;\n\tconst canSubmit = isValid && !isSubmitting && !error;\n\n\treturn {\n\t\t// State\n\t\tmessage,\n\t\tfiles,\n\t\tisSubmitting,\n\t\terror,\n\n\t\t// Actions\n\t\tsetMessage,\n\t\taddFiles,\n\t\tremoveFile,\n\t\tclearFiles,\n\t\tsubmit,\n\t\treset,\n\n\t\t// Validation\n\t\tisValid,\n\t\tcanSubmit,\n\t};\n};\n"],"mappings":"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"}
@@ -1,9 +1,21 @@
1
+ import { AnyRealtimeEvent } from "../../realtime-events.js";
1
2
  import { CossistantClient } from "@cossistant/core";
2
3
 
3
4
  //#region src/hooks/private/use-visitor-typing-reporter.d.ts
5
+ type RealtimeSendFn = (event: AnyRealtimeEvent) => void;
4
6
  type UseVisitorTypingReporterOptions = {
5
7
  client: CossistantClient | null;
6
8
  conversationId: string | null;
9
+ /**
10
+ * Optional WebSocket send function. When provided and the connection is available,
11
+ * typing events will be sent via WebSocket instead of HTTP for better performance.
12
+ */
13
+ realtimeSend?: RealtimeSendFn | null;
14
+ /**
15
+ * Whether the WebSocket connection is currently established.
16
+ * Required when using realtimeSend to determine when to use HTTP fallback.
17
+ */
18
+ isRealtimeConnected?: boolean;
7
19
  };
8
20
  type UseVisitorTypingReporterResult = {
9
21
  handleInputChange: (value: string) => void;
@@ -15,10 +27,15 @@ type UseVisitorTypingReporterResult = {
15
27
  *
16
28
  * Handles throttling, keep-alive pings, inactivity fallbacks and ensures a
17
29
  * `stop` event is emitted when the component unmounts.
30
+ *
31
+ * When `realtimeSend` is provided and connected, typing events are sent via WebSocket
32
+ * for reduced latency. Falls back to HTTP when WebSocket is unavailable.
18
33
  */
19
34
  declare function useVisitorTypingReporter({
20
35
  client,
21
- conversationId
36
+ conversationId,
37
+ realtimeSend,
38
+ isRealtimeConnected
22
39
  }: UseVisitorTypingReporterOptions): UseVisitorTypingReporterResult;
23
40
  //#endregion
24
41
  export { useVisitorTypingReporter };
@@ -1 +1 @@
1
- {"version":3,"file":"use-visitor-typing-reporter.d.ts","names":[],"sources":["../../../src/hooks/private/use-visitor-typing-reporter.ts"],"sourcesContent":[],"mappings":";;;KAQK,+BAAA;UACI;EADJ,cAAA,EAAA,MAAA,GAAA,IAAA;AACoB,CAAA;AAgBzB,KAZK,8BAAA,GAYmC;EACvC,iBAAA,EAAA,CAAA,KAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EACA,YAAA,EAAA,GAAA,GAAA,IAAA;EACE,IAAA,EAAA,GAAA,GAAA,IAAA;CAAkC;;;;;;;iBAHrB,wBAAA;;;GAGb,kCAAkC"}
1
+ {"version":3,"file":"use-visitor-typing-reporter.d.ts","names":[],"sources":["../../../src/hooks/private/use-visitor-typing-reporter.ts"],"sourcesContent":[],"mappings":";;;;KASK,cAAA,WAAyB;KAEzB,+BAAA;EAFA,MAAA,EAGI,gBAHU,GAAA,IAAW;EAEzB,cAAA,EAAA,MAAA,GAAA,IAAA;EAeA;AAeL;;;EAGC,YAAA,CAAA,EA1Be,cA0Bf,GAAA,IAAA;EACA;;;;;;KAnBI,8BAAA;;;;;;;;;;;;;;iBAeW,wBAAA;;;;;GAKb,kCAAkC"}
@@ -10,8 +10,11 @@ const STOP_TYPING_DELAY_MS = 2e3;
10
10
  *
11
11
  * Handles throttling, keep-alive pings, inactivity fallbacks and ensures a
12
12
  * `stop` event is emitted when the component unmounts.
13
+ *
14
+ * When `realtimeSend` is provided and connected, typing events are sent via WebSocket
15
+ * for reduced latency. Falls back to HTTP when WebSocket is unavailable.
13
16
  */
14
- function useVisitorTypingReporter({ client, conversationId }) {
17
+ function useVisitorTypingReporter({ client, conversationId, realtimeSend, isRealtimeConnected = false }) {
15
18
  const typingActiveRef = useRef(false);
16
19
  const lastSentAtRef = useRef(0);
17
20
  const latestPreviewRef = useRef("");
@@ -30,7 +33,26 @@ function useVisitorTypingReporter({ client, conversationId }) {
30
33
  }
31
34
  }, []);
32
35
  const sendTyping = useCallback(async (isTyping, preview) => {
33
- if (!(client && conversationId)) return;
36
+ if (!conversationId) return;
37
+ if (realtimeSend && isRealtimeConnected) try {
38
+ realtimeSend({
39
+ type: "conversationTyping",
40
+ payload: {
41
+ conversationId,
42
+ isTyping,
43
+ visitorPreview: isTyping && preview ? preview.slice(0, PREVIEW_MAX_LENGTH) : null,
44
+ websiteId: "",
45
+ organizationId: "",
46
+ visitorId: null,
47
+ userId: null,
48
+ aiAgentId: null
49
+ }
50
+ });
51
+ return;
52
+ } catch (error) {
53
+ console.warn("[Support] WebSocket typing send failed, falling back to HTTP", error);
54
+ }
55
+ if (!client) return;
34
56
  try {
35
57
  await client.setVisitorTyping({
36
58
  conversationId,
@@ -40,7 +62,12 @@ function useVisitorTypingReporter({ client, conversationId }) {
40
62
  } catch (error) {
41
63
  console.error("[Support] Failed to send typing event", error);
42
64
  }
43
- }, [client, conversationId]);
65
+ }, [
66
+ client,
67
+ conversationId,
68
+ realtimeSend,
69
+ isRealtimeConnected
70
+ ]);
44
71
  const scheduleKeepAlive = useCallback(() => {
45
72
  clearKeepAlive();
46
73
  keepAliveTimeoutRef.current = setTimeout(() => {
@@ -65,7 +92,8 @@ function useVisitorTypingReporter({ client, conversationId }) {
65
92
  sendTyping
66
93
  ]);
67
94
  const handleInputChange = useCallback((value) => {
68
- if (!(client && conversationId)) return;
95
+ if (!conversationId) return;
96
+ if (!(realtimeSend && isRealtimeConnected || Boolean(client))) return;
69
97
  const trimmed = value.trim();
70
98
  latestPreviewRef.current = trimmed.slice(0, PREVIEW_MAX_LENGTH);
71
99
  const now = typeof window !== "undefined" ? Date.now() : 0;
@@ -95,6 +123,8 @@ function useVisitorTypingReporter({ client, conversationId }) {
95
123
  }, [
96
124
  client,
97
125
  conversationId,
126
+ realtimeSend,
127
+ isRealtimeConnected,
98
128
  sendTyping,
99
129
  scheduleKeepAlive,
100
130
  scheduleStopTyping,
@@ -1 +1 @@
1
- {"version":3,"file":"use-visitor-typing-reporter.js","names":[],"sources":["../../../src/hooks/private/use-visitor-typing-reporter.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport { useCallback, useEffect, useRef } from \"react\";\n\nconst PREVIEW_MAX_LENGTH = 2000;\nconst SEND_INTERVAL_MS = 800;\nconst KEEP_ALIVE_MS = 4000;\nconst STOP_TYPING_DELAY_MS = 2000; // Send isTyping: false after 2 seconds of inactivity\n\ntype UseVisitorTypingReporterOptions = {\n\tclient: CossistantClient | null;\n\tconversationId: string | null;\n};\n\ntype UseVisitorTypingReporterResult = {\n\thandleInputChange: (value: string) => void;\n\thandleSubmit: () => void;\n\tstop: () => void;\n};\n\n/**\n * Tracks visitor composer activity and reports typing previews to the backend.\n *\n * Handles throttling, keep-alive pings, inactivity fallbacks and ensures a\n * `stop` event is emitted when the component unmounts.\n */\nexport function useVisitorTypingReporter({\n\tclient,\n\tconversationId,\n}: UseVisitorTypingReporterOptions): UseVisitorTypingReporterResult {\n\tconst typingActiveRef = useRef(false);\n\tconst lastSentAtRef = useRef(0);\n\tconst latestPreviewRef = useRef<string>(\"\");\n\tconst keepAliveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n\t\tnull\n\t);\n\tconst stopTypingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n\t\tnull\n\t);\n\n\tconst clearKeepAlive = useCallback(() => {\n\t\tif (keepAliveTimeoutRef.current) {\n\t\t\tclearTimeout(keepAliveTimeoutRef.current);\n\t\t\tkeepAliveTimeoutRef.current = null;\n\t\t}\n\t}, []);\n\n\tconst clearStopTypingTimeout = useCallback(() => {\n\t\tif (stopTypingTimeoutRef.current) {\n\t\t\tclearTimeout(stopTypingTimeoutRef.current);\n\t\t\tstopTypingTimeoutRef.current = null;\n\t\t}\n\t}, []);\n\n\tconst sendTyping = useCallback(\n\t\tasync (isTyping: boolean, preview?: string | null) => {\n\t\t\tif (!(client && conversationId)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tawait client.setVisitorTyping({\n\t\t\t\t\tconversationId,\n\t\t\t\t\tisTyping,\n\t\t\t\t\tvisitorPreview: preview ?? undefined,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"[Support] Failed to send typing event\", error);\n\t\t\t}\n\t\t},\n\t\t[client, conversationId]\n\t);\n\n\tconst scheduleKeepAlive = useCallback(() => {\n\t\tclearKeepAlive();\n\t\tkeepAliveTimeoutRef.current = setTimeout(() => {\n\t\t\tif (typingActiveRef.current) {\n\t\t\t\tvoid sendTyping(true, latestPreviewRef.current);\n\t\t\t\tscheduleKeepAlive();\n\t\t\t}\n\t\t}, KEEP_ALIVE_MS);\n\t}, [clearKeepAlive, sendTyping]);\n\n\tconst scheduleStopTyping = useCallback(() => {\n\t\tclearStopTypingTimeout();\n\t\tstopTypingTimeoutRef.current = setTimeout(() => {\n\t\t\tif (typingActiveRef.current) {\n\t\t\t\ttypingActiveRef.current = false;\n\t\t\t\tclearKeepAlive();\n\t\t\t\tvoid sendTyping(false);\n\t\t\t}\n\t\t}, STOP_TYPING_DELAY_MS);\n\t}, [clearStopTypingTimeout, clearKeepAlive, sendTyping]);\n\n\tconst handleInputChange = useCallback(\n\t\t(value: string) => {\n\t\t\tif (!(client && conversationId)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst trimmed = value.trim();\n\t\t\tlatestPreviewRef.current = trimmed.slice(0, PREVIEW_MAX_LENGTH);\n\t\t\tconst now = typeof window !== \"undefined\" ? Date.now() : 0;\n\n\t\t\tif (trimmed.length === 0) {\n\t\t\t\tif (typingActiveRef.current) {\n\t\t\t\t\ttypingActiveRef.current = false;\n\t\t\t\t\tlastSentAtRef.current = now;\n\t\t\t\t\tclearKeepAlive();\n\t\t\t\t\tclearStopTypingTimeout();\n\t\t\t\t\tvoid sendTyping(false);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Schedule auto-stop after inactivity\n\t\t\tscheduleStopTyping();\n\n\t\t\tif (!typingActiveRef.current) {\n\t\t\t\ttypingActiveRef.current = true;\n\t\t\t\tlastSentAtRef.current = now;\n\t\t\t\tvoid sendTyping(true, latestPreviewRef.current);\n\t\t\t\tscheduleKeepAlive();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (now - lastSentAtRef.current >= SEND_INTERVAL_MS) {\n\t\t\t\tlastSentAtRef.current = now;\n\t\t\t\tvoid sendTyping(true, latestPreviewRef.current);\n\t\t\t\tscheduleKeepAlive();\n\t\t\t}\n\t\t},\n\t\t[\n\t\t\tclient,\n\t\t\tconversationId,\n\t\t\tsendTyping,\n\t\t\tscheduleKeepAlive,\n\t\t\tscheduleStopTyping,\n\t\t\tclearKeepAlive,\n\t\t\tclearStopTypingTimeout,\n\t\t]\n\t);\n\n\tconst handleSubmit = useCallback(() => {\n\t\tif (!typingActiveRef.current) {\n\t\t\treturn;\n\t\t}\n\n\t\ttypingActiveRef.current = false;\n\t\tlastSentAtRef.current = typeof window !== \"undefined\" ? Date.now() : 0;\n\t\tclearKeepAlive();\n\t\tclearStopTypingTimeout();\n\t\tvoid sendTyping(false);\n\t}, [clearKeepAlive, clearStopTypingTimeout, sendTyping]);\n\n\tconst stop = useCallback(() => {\n\t\tif (!typingActiveRef.current) {\n\t\t\treturn;\n\t\t}\n\n\t\ttypingActiveRef.current = false;\n\t\tlastSentAtRef.current = typeof window !== \"undefined\" ? Date.now() : 0;\n\t\tclearKeepAlive();\n\t\tclearStopTypingTimeout();\n\t\tvoid sendTyping(false);\n\t}, [clearKeepAlive, clearStopTypingTimeout, sendTyping]);\n\n\tuseEffect(\n\t\t() => () => {\n\t\t\tif (typingActiveRef.current) {\n\t\t\t\tvoid sendTyping(false);\n\t\t\t}\n\t\t\tclearKeepAlive();\n\t\t\tclearStopTypingTimeout();\n\t\t},\n\t\t[clearKeepAlive, clearStopTypingTimeout, sendTyping]\n\t);\n\n\treturn {\n\t\thandleInputChange,\n\t\thandleSubmit,\n\t\tstop,\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-visitor-typing-reporter.js","names":[],"sources":["../../../src/hooks/private/use-visitor-typing-reporter.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport type { AnyRealtimeEvent } from \"@cossistant/types/realtime-events\";\nimport { useCallback, useEffect, useRef } from \"react\";\n\nconst PREVIEW_MAX_LENGTH = 2000;\nconst SEND_INTERVAL_MS = 800;\nconst KEEP_ALIVE_MS = 4000;\nconst STOP_TYPING_DELAY_MS = 2000; // Send isTyping: false after 2 seconds of inactivity\n\ntype RealtimeSendFn = (event: AnyRealtimeEvent) => void;\n\ntype UseVisitorTypingReporterOptions = {\n\tclient: CossistantClient | null;\n\tconversationId: string | null;\n\t/**\n\t * Optional WebSocket send function. When provided and the connection is available,\n\t * typing events will be sent via WebSocket instead of HTTP for better performance.\n\t */\n\trealtimeSend?: RealtimeSendFn | null;\n\t/**\n\t * Whether the WebSocket connection is currently established.\n\t * Required when using realtimeSend to determine when to use HTTP fallback.\n\t */\n\tisRealtimeConnected?: boolean;\n};\n\ntype UseVisitorTypingReporterResult = {\n\thandleInputChange: (value: string) => void;\n\thandleSubmit: () => void;\n\tstop: () => void;\n};\n\n/**\n * Tracks visitor composer activity and reports typing previews to the backend.\n *\n * Handles throttling, keep-alive pings, inactivity fallbacks and ensures a\n * `stop` event is emitted when the component unmounts.\n *\n * When `realtimeSend` is provided and connected, typing events are sent via WebSocket\n * for reduced latency. Falls back to HTTP when WebSocket is unavailable.\n */\nexport function useVisitorTypingReporter({\n\tclient,\n\tconversationId,\n\trealtimeSend,\n\tisRealtimeConnected = false,\n}: UseVisitorTypingReporterOptions): UseVisitorTypingReporterResult {\n\tconst typingActiveRef = useRef(false);\n\tconst lastSentAtRef = useRef(0);\n\tconst latestPreviewRef = useRef<string>(\"\");\n\tconst keepAliveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n\t\tnull\n\t);\n\tconst stopTypingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(\n\t\tnull\n\t);\n\n\tconst clearKeepAlive = useCallback(() => {\n\t\tif (keepAliveTimeoutRef.current) {\n\t\t\tclearTimeout(keepAliveTimeoutRef.current);\n\t\t\tkeepAliveTimeoutRef.current = null;\n\t\t}\n\t}, []);\n\n\tconst clearStopTypingTimeout = useCallback(() => {\n\t\tif (stopTypingTimeoutRef.current) {\n\t\t\tclearTimeout(stopTypingTimeoutRef.current);\n\t\t\tstopTypingTimeoutRef.current = null;\n\t\t}\n\t}, []);\n\n\tconst sendTyping = useCallback(\n\t\tasync (isTyping: boolean, preview?: string | null) => {\n\t\t\tif (!conversationId) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Try WebSocket first if available and connected\n\t\t\tif (realtimeSend && isRealtimeConnected) {\n\t\t\t\ttry {\n\t\t\t\t\tconst event: AnyRealtimeEvent = {\n\t\t\t\t\t\ttype: \"conversationTyping\",\n\t\t\t\t\t\tpayload: {\n\t\t\t\t\t\t\tconversationId,\n\t\t\t\t\t\t\tisTyping,\n\t\t\t\t\t\t\tvisitorPreview:\n\t\t\t\t\t\t\t\tisTyping && preview\n\t\t\t\t\t\t\t\t\t? preview.slice(0, PREVIEW_MAX_LENGTH)\n\t\t\t\t\t\t\t\t\t: null,\n\t\t\t\t\t\t\t// These will be enriched by the server with the actual values\n\t\t\t\t\t\t\twebsiteId: \"\",\n\t\t\t\t\t\t\torganizationId: \"\",\n\t\t\t\t\t\t\tvisitorId: null,\n\t\t\t\t\t\t\tuserId: null,\n\t\t\t\t\t\t\taiAgentId: null,\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t\trealtimeSend(event);\n\t\t\t\t\treturn;\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// WebSocket send failed, fall through to HTTP\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\"[Support] WebSocket typing send failed, falling back to HTTP\",\n\t\t\t\t\t\terror\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to HTTP via client\n\t\t\tif (!client) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tawait client.setVisitorTyping({\n\t\t\t\t\tconversationId,\n\t\t\t\t\tisTyping,\n\t\t\t\t\tvisitorPreview: preview ?? undefined,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"[Support] Failed to send typing event\", error);\n\t\t\t}\n\t\t},\n\t\t[client, conversationId, realtimeSend, isRealtimeConnected]\n\t);\n\n\tconst scheduleKeepAlive = useCallback(() => {\n\t\tclearKeepAlive();\n\t\tkeepAliveTimeoutRef.current = setTimeout(() => {\n\t\t\tif (typingActiveRef.current) {\n\t\t\t\tvoid sendTyping(true, latestPreviewRef.current);\n\t\t\t\tscheduleKeepAlive();\n\t\t\t}\n\t\t}, KEEP_ALIVE_MS);\n\t}, [clearKeepAlive, sendTyping]);\n\n\tconst scheduleStopTyping = useCallback(() => {\n\t\tclearStopTypingTimeout();\n\t\tstopTypingTimeoutRef.current = setTimeout(() => {\n\t\t\tif (typingActiveRef.current) {\n\t\t\t\ttypingActiveRef.current = false;\n\t\t\t\tclearKeepAlive();\n\t\t\t\tvoid sendTyping(false);\n\t\t\t}\n\t\t}, STOP_TYPING_DELAY_MS);\n\t}, [clearStopTypingTimeout, clearKeepAlive, sendTyping]);\n\n\tconst handleInputChange = useCallback(\n\t\t(value: string) => {\n\t\t\tif (!conversationId) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Need either WebSocket or HTTP client to be available\n\t\t\tconst hasWebSocket = realtimeSend && isRealtimeConnected;\n\t\t\tconst hasHttpClient = Boolean(client);\n\t\t\tif (!(hasWebSocket || hasHttpClient)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst trimmed = value.trim();\n\t\t\tlatestPreviewRef.current = trimmed.slice(0, PREVIEW_MAX_LENGTH);\n\t\t\tconst now = typeof window !== \"undefined\" ? Date.now() : 0;\n\n\t\t\tif (trimmed.length === 0) {\n\t\t\t\tif (typingActiveRef.current) {\n\t\t\t\t\ttypingActiveRef.current = false;\n\t\t\t\t\tlastSentAtRef.current = now;\n\t\t\t\t\tclearKeepAlive();\n\t\t\t\t\tclearStopTypingTimeout();\n\t\t\t\t\tvoid sendTyping(false);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Schedule auto-stop after inactivity\n\t\t\tscheduleStopTyping();\n\n\t\t\tif (!typingActiveRef.current) {\n\t\t\t\ttypingActiveRef.current = true;\n\t\t\t\tlastSentAtRef.current = now;\n\t\t\t\tvoid sendTyping(true, latestPreviewRef.current);\n\t\t\t\tscheduleKeepAlive();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (now - lastSentAtRef.current >= SEND_INTERVAL_MS) {\n\t\t\t\tlastSentAtRef.current = now;\n\t\t\t\tvoid sendTyping(true, latestPreviewRef.current);\n\t\t\t\tscheduleKeepAlive();\n\t\t\t}\n\t\t},\n\t\t[\n\t\t\tclient,\n\t\t\tconversationId,\n\t\t\trealtimeSend,\n\t\t\tisRealtimeConnected,\n\t\t\tsendTyping,\n\t\t\tscheduleKeepAlive,\n\t\t\tscheduleStopTyping,\n\t\t\tclearKeepAlive,\n\t\t\tclearStopTypingTimeout,\n\t\t]\n\t);\n\n\tconst handleSubmit = useCallback(() => {\n\t\tif (!typingActiveRef.current) {\n\t\t\treturn;\n\t\t}\n\n\t\ttypingActiveRef.current = false;\n\t\tlastSentAtRef.current = typeof window !== \"undefined\" ? Date.now() : 0;\n\t\tclearKeepAlive();\n\t\tclearStopTypingTimeout();\n\t\tvoid sendTyping(false);\n\t}, [clearKeepAlive, clearStopTypingTimeout, sendTyping]);\n\n\tconst stop = useCallback(() => {\n\t\tif (!typingActiveRef.current) {\n\t\t\treturn;\n\t\t}\n\n\t\ttypingActiveRef.current = false;\n\t\tlastSentAtRef.current = typeof window !== \"undefined\" ? Date.now() : 0;\n\t\tclearKeepAlive();\n\t\tclearStopTypingTimeout();\n\t\tvoid sendTyping(false);\n\t}, [clearKeepAlive, clearStopTypingTimeout, sendTyping]);\n\n\tuseEffect(\n\t\t() => () => {\n\t\t\tif (typingActiveRef.current) {\n\t\t\t\tvoid sendTyping(false);\n\t\t\t}\n\t\t\tclearKeepAlive();\n\t\t\tclearStopTypingTimeout();\n\t\t},\n\t\t[clearKeepAlive, clearStopTypingTimeout, sendTyping]\n\t);\n\n\treturn {\n\t\thandleInputChange,\n\t\thandleSubmit,\n\t\tstop,\n\t};\n}\n"],"mappings":"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"}
@@ -37,6 +37,7 @@ type UseConversationPageReturn = {
37
37
  message: string;
38
38
  files: File[];
39
39
  isSubmitting: boolean;
40
+ isUploading: boolean;
40
41
  canSubmit: boolean;
41
42
  setMessage: (message: string) => void;
42
43
  addFiles: (files: File[]) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"use-conversation-page.d.ts","names":[],"sources":["../../src/hooks/use-conversation-page.ts"],"sourcesContent":[],"mappings":";;;KAaY,0BAAA;;AAAZ;AA+BA;;EAMQ,cAAA,EAAA,MAAA;EAKC;;;EAWsB,cAAA,CAAA,EAAA,MAAA;EAuCf;;;;;;;;UAvEP;;;;;;;;KAUG,yBAAA;;;SAIJ;;SAEA;;;WAKC;;;;sBAIW;;;;;oBAOD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuCH,mBAAA,UACN,6BACP"}
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,3 +1,4 @@
1
+ import { useWebSocketSafe } from "../support/context/websocket.js";
1
2
  import { useConversationAutoSeen } from "./use-conversation-auto-seen.js";
2
3
  import { useConversationLifecycle } from "./use-conversation-lifecycle.js";
3
4
  import { useConversationTimelineItems } from "./use-conversation-timeline-items.js";
@@ -47,6 +48,7 @@ import { ConversationTimelineType, TimelineItemVisibility } from "@cossistant/ty
47
48
  function useConversationPage(options) {
48
49
  const { conversationId: initialConversationId, initialMessage, onConversationIdChange, items: passedItems = [], autoSeenEnabled = true } = options;
49
50
  const { client, visitor } = useSupport();
51
+ const websocket = useWebSocketSafe();
50
52
  const trimmedInitialMessage = initialMessage?.trim() ?? "";
51
53
  const hasInitialMessage = trimmedInitialMessage.length > 0;
52
54
  const lifecycle = useConversationLifecycle({
@@ -110,7 +112,9 @@ function useConversationPage(options) {
110
112
  visitorId: visitor?.id,
111
113
  onMessageSent: (newConversationId) => {
112
114
  if (lifecycle.isPending) lifecycle.setConversationId(newConversationId);
113
- }
115
+ },
116
+ realtimeSend: websocket?.send ?? null,
117
+ isRealtimeConnected: websocket?.isConnected ?? false
114
118
  });
115
119
  const initialMessageSubmittedRef = useRef(false);
116
120
  const lastInitialMessageRef = useRef(null);
@@ -160,6 +164,7 @@ function useConversationPage(options) {
160
164
  message: composer.message,
161
165
  files: composer.files,
162
166
  isSubmitting: composer.isSubmitting,
167
+ isUploading: composer.isUploading,
163
168
  canSubmit: composer.canSubmit,
164
169
  setMessage: composer.setMessage,
165
170
  addFiles: composer.addFiles,
@@ -1 +1 @@
1
- {"version":3,"file":"use-conversation-page.js","names":["identificationItem: TimelineItem"],"sources":["../../src/hooks/use-conversation-page.ts"],"sourcesContent":["import type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport {\n\tConversationTimelineType,\n\tTimelineItemVisibility,\n} from \"@cossistant/types/enums\";\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { useSupport } from \"../provider\";\nimport { useDefaultMessages } from \"./private/use-default-messages\";\nimport { useConversationAutoSeen } from \"./use-conversation-auto-seen\";\nimport { useConversationLifecycle } from \"./use-conversation-lifecycle\";\nimport { useConversationTimelineItems } from \"./use-conversation-timeline-items\";\nimport { useMessageComposer } from \"./use-message-composer\";\n\nexport type UseConversationPageOptions = {\n\t/**\n\t * Initial conversation ID (from URL params, navigation state, etc.)\n\t * Can be PENDING_CONVERSATION_ID or a real ID.\n\t */\n\tconversationId: string;\n\n\t/**\n\t * Optional initial message to send when the conversation opens.\n\t */\n\tinitialMessage?: string;\n\n\t/**\n\t * Callback when conversation ID changes (e.g., after creation).\n\t * Use this to update navigation state or URL.\n\t */\n\tonConversationIdChange?: (conversationId: string) => void;\n\n\t/**\n\t * Optional timeline items to pass through (e.g., optimistic updates).\n\t */\n\titems?: TimelineItem[];\n\n\t/**\n\t * Whether automatic \"seen\" tracking should be enabled.\n\t * Set to false when the conversation isn't visible (e.g. widget closed).\n\t * Default: true\n\t */\n\tautoSeenEnabled?: boolean;\n};\n\nexport type UseConversationPageReturn = {\n\t// Conversation state\n\tconversationId: string;\n\tisPending: boolean;\n\titems: TimelineItem[];\n\tisLoading: boolean;\n\terror: Error | null;\n\n\t// Message composer\n\tcomposer: {\n\t\tmessage: string;\n\t\tfiles: File[];\n\t\tisSubmitting: boolean;\n\t\tcanSubmit: boolean;\n\t\tsetMessage: (message: string) => void;\n\t\taddFiles: (files: File[]) => void;\n\t\tremoveFile: (index: number) => void;\n\t\tsubmit: () => void;\n\t};\n\n\t// UI helpers\n\thasItems: boolean;\n\tlastTimelineItem: TimelineItem | null;\n};\n\n/**\n * Main orchestrator hook for the conversation page.\n *\n * This hook combines all conversation-related logic:\n * - Lifecycle management (pending → real conversation)\n * - Message fetching and display\n * - Message composition and sending\n * - Automatic seen tracking\n * - Default/welcome messages before conversation is created\n *\n * It provides a clean, simple API for building conversation UIs.\n *\n * @example\n * ```tsx\n * export function ConversationPage({ conversationId: initialId }) {\n * const conversation = useConversationPage({\n * conversationId: initialId,\n * onConversationIdChange: (newId) => {\n * // Update URL or navigation state\n * navigate(`/conversation/${newId}`);\n * },\n * });\n *\n * return (\n * <>\n * <MessageList messages={conversation.messages} />\n * <MessageInput\n * value={conversation.composer.message}\n * onChange={conversation.composer.setMessage}\n * onSubmit={conversation.composer.submit}\n * />\n * </>\n * );\n * }\n * ```\n */\nexport function useConversationPage(\n\toptions: UseConversationPageOptions\n): UseConversationPageReturn {\n\tconst {\n\t\tconversationId: initialConversationId,\n\t\tinitialMessage,\n\t\tonConversationIdChange,\n\t\titems: passedItems = [],\n\t\tautoSeenEnabled = true,\n\t} = options;\n\n\tconst { client, visitor } = useSupport();\n\n\tconst trimmedInitialMessage = initialMessage?.trim() ?? \"\";\n\tconst hasInitialMessage = trimmedInitialMessage.length > 0;\n\n\t// 1. Manage conversation lifecycle (pending vs real)\n\tconst lifecycle = useConversationLifecycle({\n\t\tinitialConversationId,\n\t\tonConversationCreated: onConversationIdChange,\n\t});\n\n\t// 2. Get default timeline items for pending state\n\tconst defaultTimelineItems = useDefaultMessages({\n\t\tconversationId: lifecycle.conversationId,\n\t});\n\n\tconst effectiveDefaultTimelineItems = hasInitialMessage\n\t\t? []\n\t\t: defaultTimelineItems;\n\n\t// 3. Fetch timeline items from backend if real conversation exists\n\tconst timelineQuery = useConversationTimelineItems(lifecycle.conversationId, {\n\t\tenabled: !lifecycle.isPending,\n\t});\n\n\t// 4. Determine which items to display\n\tconst baseItems = useMemo(() => {\n\t\t// If we have fetched timeline items, use them\n\t\tif (timelineQuery.items.length > 0) {\n\t\t\treturn timelineQuery.items;\n\t\t}\n\n\t\t// If pending, use default timeline items\n\t\tif (lifecycle.isPending && effectiveDefaultTimelineItems.length > 0) {\n\t\t\treturn effectiveDefaultTimelineItems;\n\t\t}\n\n\t\t// Use passed items as fallback\n\t\tif (passedItems.length > 0) {\n\t\t\treturn passedItems;\n\t\t}\n\n\t\treturn [];\n\t}, [\n\t\ttimelineQuery.items,\n\t\tlifecycle.isPending,\n\t\teffectiveDefaultTimelineItems,\n\t\tpassedItems,\n\t]);\n\n\tconst shouldShowIdentificationTool = useMemo(() => {\n\t\tif (lifecycle.isPending) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (visitor?.contact) {\n\t\t\treturn false;\n\t\t}\n\n\t\treturn !baseItems.some(\n\t\t\t(item) => item.type === ConversationTimelineType.IDENTIFICATION\n\t\t);\n\t}, [baseItems, lifecycle.isPending, visitor?.contact]);\n\n\tconst displayItems = useMemo(() => {\n\t\tif (!shouldShowIdentificationTool) {\n\t\t\treturn baseItems;\n\t\t}\n\n\t\tconst organizationId =\n\t\t\tbaseItems.at(-1)?.organizationId ??\n\t\t\tclient.getConfiguration().organizationId ??\n\t\t\t\"\";\n\n\t\tconst identificationItem: TimelineItem = {\n\t\t\tid: `identification-${lifecycle.conversationId}`,\n\t\t\tconversationId: lifecycle.conversationId,\n\t\t\torganizationId,\n\t\t\tvisibility: TimelineItemVisibility.PUBLIC,\n\t\t\ttype: ConversationTimelineType.IDENTIFICATION,\n\t\t\ttext: null,\n\t\t\ttool: \"identification\",\n\t\t\tparts: [],\n\t\t\tuserId: null,\n\t\t\tvisitorId: visitor?.id ?? null,\n\t\t\taiAgentId: null,\n\t\t\tcreatedAt: typeof window !== \"undefined\" ? new Date().toISOString() : \"\",\n\t\t\tdeletedAt: null,\n\t\t};\n\n\t\treturn [...baseItems, identificationItem];\n\t}, [\n\t\tbaseItems,\n\t\tclient,\n\t\tlifecycle.conversationId,\n\t\tshouldShowIdentificationTool,\n\t\tvisitor?.id,\n\t]);\n\n\tconst lastTimelineItem = useMemo(\n\t\t() => displayItems.at(-1) ?? null,\n\t\t[displayItems]\n\t);\n\n\t// 5. Set up message composer\n\tconst composer = useMessageComposer({\n\t\tclient,\n\t\tconversationId: lifecycle.realConversationId,\n\t\tdefaultTimelineItems: effectiveDefaultTimelineItems,\n\t\tvisitorId: visitor?.id,\n\t\tonMessageSent: (newConversationId) => {\n\t\t\t// Transition from pending to real conversation\n\t\t\tif (lifecycle.isPending) {\n\t\t\t\tlifecycle.setConversationId(newConversationId);\n\t\t\t}\n\t\t},\n\t});\n\n\tconst initialMessageSubmittedRef = useRef(false);\n\tconst lastInitialMessageRef = useRef<string | null>(null);\n\n\tuseEffect(() => {\n\t\tif (!hasInitialMessage) {\n\t\t\tinitialMessageSubmittedRef.current = false;\n\t\t\tlastInitialMessageRef.current = null;\n\t\t\treturn;\n\t\t}\n\n\t\tif (lastInitialMessageRef.current !== trimmedInitialMessage) {\n\t\t\tinitialMessageSubmittedRef.current = false;\n\t\t\tlastInitialMessageRef.current = trimmedInitialMessage;\n\t\t}\n\n\t\tif (!lifecycle.isPending) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (composer.message !== trimmedInitialMessage) {\n\t\t\tcomposer.setMessage(trimmedInitialMessage);\n\t\t\treturn;\n\t\t}\n\n\t\tif (\n\t\t\tinitialMessageSubmittedRef.current ||\n\t\t\tcomposer.isSubmitting ||\n\t\t\t!composer.canSubmit\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tinitialMessageSubmittedRef.current = true;\n\t\tcomposer.submit();\n\t}, [\n\t\thasInitialMessage,\n\t\tlifecycle.isPending,\n\t\tcomposer.message,\n\t\tcomposer.setMessage,\n\t\tcomposer.isSubmitting,\n\t\tcomposer.canSubmit,\n\t\tcomposer.submit,\n\t\ttrimmedInitialMessage,\n\t]);\n\n\t// 6. Auto-mark messages as seen\n\tuseConversationAutoSeen({\n\t\tclient,\n\t\tconversationId: lifecycle.realConversationId,\n\t\tvisitorId: visitor?.id,\n\t\tlastTimelineItem,\n\t\tenabled: autoSeenEnabled,\n\t\tisWidgetOpen: autoSeenEnabled,\n\t});\n\n\treturn {\n\t\tconversationId: lifecycle.conversationId,\n\t\tisPending: lifecycle.isPending,\n\t\titems: displayItems,\n\t\tisLoading: timelineQuery.isLoading,\n\t\terror: timelineQuery.error || composer.error,\n\t\tcomposer: {\n\t\t\tmessage: composer.message,\n\t\t\tfiles: composer.files,\n\t\t\tisSubmitting: composer.isSubmitting,\n\t\t\tcanSubmit: composer.canSubmit,\n\t\t\tsetMessage: composer.setMessage,\n\t\t\taddFiles: composer.addFiles,\n\t\t\tremoveFile: composer.removeFile,\n\t\t\tsubmit: composer.submit,\n\t\t},\n\t\thasItems: displayItems.length > 0,\n\t\tlastTimelineItem,\n\t};\n}\n"],"mappings":"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"}
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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"}
@@ -27,7 +27,8 @@ type UseConversationPreviewOptions = {
27
27
  conversation: Conversation;
28
28
  /**
29
29
  * Whether the hook should fetch timeline items for the conversation.
30
- * Enabled by default.
30
+ * Disabled by default to reduce API calls - conversation.lastTimelineItem
31
+ * is typically sufficient for previews.
31
32
  */
32
33
  includeTimelineItems?: boolean;
33
34
  /**
@@ -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;AAqBY,KApCA,gCAAA,GAoC4B;EACzB,IAAA,EAAA,MAAA;EAED,KAAA,EAAA,MAAA,GAAA,IAAA;EACE,IAAA,EAAA,OAAA,GAAA,IAAA,GAAA,UAAA;CACP;AACoB,KApCjB,oCAAA,GAAuC,wBAoCtB;AAAlB,KAlCC,8BAAA,GAkCD;EAAU,YAAA,EAjCN,oCAiCM,EAAA;EA0BL,kBAAA,EA1DK,oCA2DX,GAAA,IAAA;;;;KAtDE,6BAAA;gBACG;;;;;;;;;yBASS;;;;;;;;;;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;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"}
@@ -20,7 +20,7 @@ function resolveLastTimelineMessage(items, fallback) {
20
20
  * snippets and typing state for use in lists.
21
21
  */
22
22
  function useConversationPreview(options) {
23
- const { conversation, includeTimelineItems = true, initialTimelineItems = [], typing } = options;
23
+ const { conversation, includeTimelineItems = false, initialTimelineItems = [], typing } = options;
24
24
  const { availableHumanAgents, availableAIAgents, visitor } = useSupport();
25
25
  const text = useSupportText();
26
26
  const timeline = useConversationTimelineItems(conversation.id, { enabled: includeTimelineItems });
@@ -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 * Enabled by default.\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 = true,\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};\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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"}
@@ -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"}
@@ -27,6 +27,7 @@ function useConversationTimelineItems(conversationId, options = {}) {
27
27
  }), [options.cursor, options.limit]);
28
28
  const { refetch: queryRefetch, isLoading: queryLoading, error } = useClientQuery({
29
29
  client,
30
+ queryKey: conversationId ? `timeline:${conversationId}:${baseArgs.limit}:${baseArgs.cursor ?? ""}` : void 0,
30
31
  queryFn: (instance, args) => {
31
32
  if (!conversationId) return Promise.resolve({
32
33
  items: [],
@@ -41,7 +42,7 @@ function useConversationTimelineItems(conversationId, options = {}) {
41
42
  },
42
43
  enabled: Boolean(conversationId) && (options.enabled ?? true),
43
44
  refetchInterval: options.refetchInterval ?? false,
44
- refetchOnWindowFocus: options.refetchOnWindowFocus ?? true,
45
+ refetchOnWindowFocus: options.refetchOnWindowFocus ?? false,
45
46
  refetchOnMount: selection.items.length === 0,
46
47
  initialArgs: baseArgs,
47
48
  dependencies: [
@@ -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\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 ?? true,\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,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;\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 +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--) {
@@ -1 +1 @@
1
- {"version":3,"file":"use-conversation-timeline.js","names":[],"sources":["../../src/hooks/use-conversation-timeline.ts"],"sourcesContent":["import { SenderType } from \"@cossistant/types\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport { useMemo } from \"react\";\n\nimport {\n\tmapTypingEntriesToParticipants,\n\ttype TimelineTypingParticipant,\n} from \"./private/typing\";\nimport { useGroupedMessages } from \"./private/use-grouped-messages\";\nimport { useDebouncedConversationSeen } from \"./use-conversation-seen\";\nimport { useConversationTyping } from \"./use-conversation-typing\";\n\nexport type ConversationTimelineTypingParticipant = TimelineTypingParticipant;\n\nexport type UseConversationTimelineOptions = {\n\tconversationId: string;\n\titems: TimelineItem[];\n\tcurrentVisitorId?: string;\n};\n\nexport type UseConversationTimelineReturn = {\n\tgroupedMessages: ReturnType<typeof useGroupedMessages>;\n\tseenData: ReturnType<typeof useDebouncedConversationSeen>;\n\ttypingEntries: ReturnType<typeof useConversationTyping>;\n\ttypingParticipants: ConversationTimelineTypingParticipant[];\n\tlastVisitorMessageGroupIndex: number;\n};\n\n/**\n * Produces grouped timeline items, seen data and typing state suitable for the\n * conversation detail view.\n */\nexport function useConversationTimeline({\n\tconversationId,\n\titems: timelineItems,\n\tcurrentVisitorId,\n}: UseConversationTimelineOptions): UseConversationTimelineReturn {\n\tconst seenData = useDebouncedConversationSeen(conversationId);\n\tconst typingEntries = useConversationTyping(conversationId, {\n\t\texcludeVisitorId: currentVisitorId ?? null,\n\t});\n\n\tconst groupedMessages = useGroupedMessages({\n\t\titems: timelineItems,\n\t\tseenData,\n\t\tcurrentViewerId: currentVisitorId,\n\t\tviewerType: SenderType.VISITOR,\n\t});\n\n\tconst lastVisitorMessageGroupIndex = useMemo(() => {\n\t\tfor (let index = groupedMessages.items.length - 1; index >= 0; index--) {\n\t\t\tconst item = groupedMessages.items[index];\n\n\t\t\tif (!item || item.type !== \"message_group\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst firstMessage = item.items?.[0];\n\t\t\tif (firstMessage?.visitorId === currentVisitorId) {\n\t\t\t\treturn index;\n\t\t\t}\n\t\t}\n\n\t\treturn -1;\n\t}, [groupedMessages.items, currentVisitorId]);\n\n\tconst typingParticipants = useMemo(\n\t\t() => mapTypingEntriesToParticipants(typingEntries),\n\t\t[typingEntries]\n\t);\n\n\treturn {\n\t\tgroupedMessages,\n\t\tseenData,\n\t\ttypingEntries,\n\t\ttypingParticipants,\n\t\tlastVisitorMessageGroupIndex,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;AAgCA,SAAgB,wBAAwB,EACvC,gBACA,OAAO,eACP,oBACiE;CACjE,MAAM,WAAW,6BAA6B,eAAe;CAC7D,MAAM,gBAAgB,sBAAsB,gBAAgB,EAC3D,kBAAkB,oBAAoB,MACtC,CAAC;CAEF,MAAM,kBAAkB,mBAAmB;EAC1C,OAAO;EACP;EACA,iBAAiB;EACjB,YAAY,WAAW;EACvB,CAAC;CAEF,MAAM,+BAA+B,cAAc;AAClD,OAAK,IAAI,QAAQ,gBAAgB,MAAM,SAAS,GAAG,SAAS,GAAG,SAAS;GACvE,MAAM,OAAO,gBAAgB,MAAM;AAEnC,OAAI,CAAC,QAAQ,KAAK,SAAS,gBAC1B;AAID,QADqB,KAAK,QAAQ,KAChB,cAAc,iBAC/B,QAAO;;AAIT,SAAO;IACL,CAAC,gBAAgB,OAAO,iBAAiB,CAAC;AAO7C,QAAO;EACN;EACA;EACA;EACA,oBAT0B,cACpB,+BAA+B,cAAc,EACnD,CAAC,cAAc,CACf;EAOA;EACA"}
1
+ {"version":3,"file":"use-conversation-timeline.js","names":[],"sources":["../../src/hooks/use-conversation-timeline.ts"],"sourcesContent":["import type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport { useMemo } from \"react\";\n\nimport {\n\tmapTypingEntriesToParticipants,\n\ttype TimelineTypingParticipant,\n} from \"./private/typing\";\nimport { useGroupedMessages } from \"./private/use-grouped-messages\";\nimport { useDebouncedConversationSeen } from \"./use-conversation-seen\";\nimport { useConversationTyping } from \"./use-conversation-typing\";\n\nexport type ConversationTimelineTypingParticipant = TimelineTypingParticipant;\n\nexport type UseConversationTimelineOptions = {\n\tconversationId: string;\n\titems: TimelineItem[];\n\tcurrentVisitorId?: string;\n};\n\nexport type UseConversationTimelineReturn = {\n\tgroupedMessages: ReturnType<typeof useGroupedMessages>;\n\tseenData: ReturnType<typeof useDebouncedConversationSeen>;\n\ttypingEntries: ReturnType<typeof useConversationTyping>;\n\ttypingParticipants: ConversationTimelineTypingParticipant[];\n\tlastVisitorMessageGroupIndex: number;\n};\n\n/**\n * Produces grouped timeline items, seen data and typing state suitable for the\n * conversation detail view.\n */\nexport function useConversationTimeline({\n\tconversationId,\n\titems: timelineItems,\n\tcurrentVisitorId,\n}: UseConversationTimelineOptions): UseConversationTimelineReturn {\n\tconst seenData = useDebouncedConversationSeen(conversationId);\n\tconst typingEntries = useConversationTyping(conversationId, {\n\t\texcludeVisitorId: currentVisitorId ?? null,\n\t});\n\n\tconst groupedMessages = useGroupedMessages({\n\t\titems: timelineItems,\n\t\tseenData,\n\t\tcurrentViewerId: currentVisitorId,\n\t});\n\n\tconst lastVisitorMessageGroupIndex = useMemo(() => {\n\t\tfor (let index = groupedMessages.items.length - 1; index >= 0; index--) {\n\t\t\tconst item = groupedMessages.items[index];\n\n\t\t\tif (!item || item.type !== \"message_group\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst firstMessage = item.items?.[0];\n\t\t\tif (firstMessage?.visitorId === currentVisitorId) {\n\t\t\t\treturn index;\n\t\t\t}\n\t\t}\n\n\t\treturn -1;\n\t}, [groupedMessages.items, currentVisitorId]);\n\n\tconst typingParticipants = useMemo(\n\t\t() => mapTypingEntriesToParticipants(typingEntries),\n\t\t[typingEntries]\n\t);\n\n\treturn {\n\t\tgroupedMessages,\n\t\tseenData,\n\t\ttypingEntries,\n\t\ttypingParticipants,\n\t\tlastVisitorMessageGroupIndex,\n\t};\n}\n"],"mappings":";;;;;;;;;;;AA+BA,SAAgB,wBAAwB,EACvC,gBACA,OAAO,eACP,oBACiE;CACjE,MAAM,WAAW,6BAA6B,eAAe;CAC7D,MAAM,gBAAgB,sBAAsB,gBAAgB,EAC3D,kBAAkB,oBAAoB,MACtC,CAAC;CAEF,MAAM,kBAAkB,mBAAmB;EAC1C,OAAO;EACP;EACA,iBAAiB;EACjB,CAAC;CAEF,MAAM,+BAA+B,cAAc;AAClD,OAAK,IAAI,QAAQ,gBAAgB,MAAM,SAAS,GAAG,SAAS,GAAG,SAAS;GACvE,MAAM,OAAO,gBAAgB,MAAM;AAEnC,OAAI,CAAC,QAAQ,KAAK,SAAS,gBAC1B;AAID,QADqB,KAAK,QAAQ,KAChB,cAAc,iBAC/B,QAAO;;AAIT,SAAO;IACL,CAAC,gBAAgB,OAAO,iBAAiB,CAAC;AAO7C,QAAO;EACN;EACA;EACA;EACA,oBAT0B,cACpB,+BAA+B,cAAc,EACnD,CAAC,cAAc,CACf;EAOA;EACA"}
@@ -24,13 +24,14 @@ function useConversation(conversationId, options = {}) {
24
24
  const request = conversationId ? { conversationId } : void 0;
25
25
  const { refetch: queryRefetch, isLoading: queryLoading, error } = useClientQuery({
26
26
  client,
27
+ queryKey: conversationId ? `conversation:${conversationId}` : void 0,
27
28
  queryFn: (instance) => {
28
29
  if (!request) throw new Error("Conversation ID is required");
29
30
  return instance.getConversation(request);
30
31
  },
31
32
  enabled: Boolean(conversationId && (options.enabled ?? true)),
32
33
  refetchInterval: options.refetchInterval ?? false,
33
- refetchOnWindowFocus: options.refetchOnWindowFocus ?? true,
34
+ refetchOnWindowFocus: options.refetchOnWindowFocus ?? false,
34
35
  refetchOnMount: !conversation,
35
36
  initialArgs: request,
36
37
  dependencies: [conversationId ?? "null"]
@@ -1 +1 @@
1
- {"version":3,"file":"use-conversation.js","names":["request: GetConversationRequest | undefined"],"sources":["../../src/hooks/use-conversation.ts"],"sourcesContent":["import type {\n\tGetConversationRequest,\n\tGetConversationResponse,\n} from \"@cossistant/types/api/conversation\";\nimport { useSupport } from \"../provider\";\nimport { useStoreSelector } from \"./private/store/use-store-selector\";\nimport { useClientQuery } from \"./private/use-client-query\";\n\nexport type UseConversationOptions = {\n\tenabled?: boolean;\n\trefetchInterval?: number | false;\n\trefetchOnWindowFocus?: boolean;\n};\n\nexport type UseConversationResult = {\n\tconversation: GetConversationResponse[\"conversation\"] | null;\n\tisLoading: boolean;\n\terror: Error | null;\n\trefetch: (\n\t\targs?: GetConversationRequest\n\t) => Promise<GetConversationResponse | undefined>;\n};\n\n/**\n * Loads and caches a single conversation identified by `conversationId`.\n *\n * The hook keeps the conversations store hydrated, exposes a derived loading\n * state that respects cached data and provides a `refetch` helper to manually\n * refresh the thread.\n *\n * @param conversationId The conversation to retrieve; when `null` the hook\n * skips requests and returns `null` data.\n * @param options Additional react-query style controls for the request.\n */\nexport function useConversation(\n\tconversationId: string | null,\n\toptions: UseConversationOptions = {}\n): UseConversationResult {\n\tconst { client } = useSupport();\n\tconst store = client.conversationsStore;\n\n\tconst conversation = useStoreSelector(store, (state) => {\n\t\tif (!conversationId) {\n\t\t\treturn null;\n\t\t}\n\t\treturn state.byId[conversationId] ?? null;\n\t});\n\n\tconst request: GetConversationRequest | undefined = conversationId\n\t\t? { conversationId }\n\t\t: undefined;\n\n\tconst {\n\t\trefetch: queryRefetch,\n\t\tisLoading: queryLoading,\n\t\terror,\n\t} = useClientQuery<GetConversationResponse, GetConversationRequest>({\n\t\tclient,\n\t\tqueryFn: (instance) => {\n\t\t\tif (!request) {\n\t\t\t\tthrow new Error(\"Conversation ID is required\");\n\t\t\t}\n\t\t\treturn instance.getConversation(request);\n\t\t},\n\t\tenabled: Boolean(conversationId && (options.enabled ?? true)),\n\t\trefetchInterval: options.refetchInterval ?? false,\n\t\trefetchOnWindowFocus: options.refetchOnWindowFocus ?? true,\n\t\trefetchOnMount: !conversation,\n\t\tinitialArgs: request,\n\t\tdependencies: [conversationId ?? \"null\"],\n\t});\n\n\tconst refetch = (args?: GetConversationRequest) => {\n\t\tif (!conversationId) {\n\t\t\treturn Promise.resolve(undefined);\n\t\t}\n\n\t\treturn queryRefetch({\n\t\t\tconversationId,\n\t\t\t...args,\n\t\t});\n\t};\n\n\tconst isLoading = conversation ? false : queryLoading;\n\n\treturn {\n\t\tconversation,\n\t\tisLoading,\n\t\terror,\n\t\trefetch,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkCA,SAAgB,gBACf,gBACA,UAAkC,EAAE,EACZ;CACxB,MAAM,EAAE,WAAW,YAAY;CAC/B,MAAM,QAAQ,OAAO;CAErB,MAAM,eAAe,iBAAiB,QAAQ,UAAU;AACvD,MAAI,CAAC,eACJ,QAAO;AAER,SAAO,MAAM,KAAK,mBAAmB;GACpC;CAEF,MAAMA,UAA8C,iBACjD,EAAE,gBAAgB,GAClB;CAEH,MAAM,EACL,SAAS,cACT,WAAW,cACX,UACG,eAAgE;EACnE;EACA,UAAU,aAAa;AACtB,OAAI,CAAC,QACJ,OAAM,IAAI,MAAM,8BAA8B;AAE/C,UAAO,SAAS,gBAAgB,QAAQ;;EAEzC,SAAS,QAAQ,mBAAmB,QAAQ,WAAW,MAAM;EAC7D,iBAAiB,QAAQ,mBAAmB;EAC5C,sBAAsB,QAAQ,wBAAwB;EACtD,gBAAgB,CAAC;EACjB,aAAa;EACb,cAAc,CAAC,kBAAkB,OAAO;EACxC,CAAC;CAEF,MAAM,WAAW,SAAkC;AAClD,MAAI,CAAC,eACJ,QAAO,QAAQ,QAAQ,OAAU;AAGlC,SAAO,aAAa;GACnB;GACA,GAAG;GACH,CAAC;;AAKH,QAAO;EACN;EACA,WAJiB,eAAe,QAAQ;EAKxC;EACA;EACA"}
1
+ {"version":3,"file":"use-conversation.js","names":["request: GetConversationRequest | undefined"],"sources":["../../src/hooks/use-conversation.ts"],"sourcesContent":["import type {\n\tGetConversationRequest,\n\tGetConversationResponse,\n} from \"@cossistant/types/api/conversation\";\nimport { useSupport } from \"../provider\";\nimport { useStoreSelector } from \"./private/store/use-store-selector\";\nimport { useClientQuery } from \"./private/use-client-query\";\n\nexport type UseConversationOptions = {\n\tenabled?: boolean;\n\trefetchInterval?: number | false;\n\trefetchOnWindowFocus?: boolean;\n};\n\nexport type UseConversationResult = {\n\tconversation: GetConversationResponse[\"conversation\"] | null;\n\tisLoading: boolean;\n\terror: Error | null;\n\trefetch: (\n\t\targs?: GetConversationRequest\n\t) => Promise<GetConversationResponse | undefined>;\n};\n\n/**\n * Loads and caches a single conversation identified by `conversationId`.\n *\n * The hook keeps the conversations store hydrated, exposes a derived loading\n * state that respects cached data and provides a `refetch` helper to manually\n * refresh the thread.\n *\n * @param conversationId The conversation to retrieve; when `null` the hook\n * skips requests and returns `null` data.\n * @param options Additional react-query style controls for the request.\n */\nexport function useConversation(\n\tconversationId: string | null,\n\toptions: UseConversationOptions = {}\n): UseConversationResult {\n\tconst { client } = useSupport();\n\tconst store = client.conversationsStore;\n\n\tconst conversation = useStoreSelector(store, (state) => {\n\t\tif (!conversationId) {\n\t\t\treturn null;\n\t\t}\n\t\treturn state.byId[conversationId] ?? null;\n\t});\n\n\tconst request: GetConversationRequest | undefined = conversationId\n\t\t? { conversationId }\n\t\t: undefined;\n\n\tconst {\n\t\trefetch: queryRefetch,\n\t\tisLoading: queryLoading,\n\t\terror,\n\t} = useClientQuery<GetConversationResponse, GetConversationRequest>({\n\t\tclient,\n\t\tqueryKey: conversationId ? `conversation:${conversationId}` : undefined,\n\t\tqueryFn: (instance) => {\n\t\t\tif (!request) {\n\t\t\t\tthrow new Error(\"Conversation ID is required\");\n\t\t\t}\n\t\t\treturn instance.getConversation(request);\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: !conversation,\n\t\tinitialArgs: request,\n\t\tdependencies: [conversationId ?? \"null\"],\n\t});\n\n\tconst refetch = (args?: GetConversationRequest) => {\n\t\tif (!conversationId) {\n\t\t\treturn Promise.resolve(undefined);\n\t\t}\n\n\t\treturn queryRefetch({\n\t\t\tconversationId,\n\t\t\t...args,\n\t\t});\n\t};\n\n\tconst isLoading = conversation ? false : queryLoading;\n\n\treturn {\n\t\tconversation,\n\t\tisLoading,\n\t\terror,\n\t\trefetch,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkCA,SAAgB,gBACf,gBACA,UAAkC,EAAE,EACZ;CACxB,MAAM,EAAE,WAAW,YAAY;CAC/B,MAAM,QAAQ,OAAO;CAErB,MAAM,eAAe,iBAAiB,QAAQ,UAAU;AACvD,MAAI,CAAC,eACJ,QAAO;AAER,SAAO,MAAM,KAAK,mBAAmB;GACpC;CAEF,MAAMA,UAA8C,iBACjD,EAAE,gBAAgB,GAClB;CAEH,MAAM,EACL,SAAS,cACT,WAAW,cACX,UACG,eAAgE;EACnE;EACA,UAAU,iBAAiB,gBAAgB,mBAAmB;EAC9D,UAAU,aAAa;AACtB,OAAI,CAAC,QACJ,OAAM,IAAI,MAAM,8BAA8B;AAE/C,UAAO,SAAS,gBAAgB,QAAQ;;EAEzC,SAAS,QAAQ,mBAAmB,QAAQ,WAAW,MAAM;EAC7D,iBAAiB,QAAQ,mBAAmB;EAC5C,sBAAsB,QAAQ,wBAAwB;EACtD,gBAAgB,CAAC;EACjB,aAAa;EACb,cAAc,CAAC,kBAAkB,OAAO;EACxC,CAAC;CAEF,MAAM,WAAW,SAAkC;AAClD,MAAI,CAAC,eACJ,QAAO,QAAQ,QAAQ,OAAU;AAGlC,SAAO,aAAa;GACnB;GACA,GAAG;GACH,CAAC;;AAKH,QAAO;EACN;EACA,WAJiB,eAAe,QAAQ;EAKxC;EACA;EACA"}
@@ -44,6 +44,7 @@ function useConversations(options = {}) {
44
44
  }), areSelectionsEqual);
45
45
  const { refetch: queryRefetch, isLoading: queryLoading, error } = useClientQuery({
46
46
  client,
47
+ queryKey: `conversations:${limit ?? ""}:${page ?? ""}:${status ?? ""}:${orderBy ?? ""}:${order ?? ""}`,
47
48
  queryFn: (instance, args) => instance.listConversations({
48
49
  ...requestDefaults,
49
50
  ...args