@memori.ai/memori-react 9.0.0-alpha.4 → 9.0.0-alpha.6

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 (268) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/components/AgeVerificationModal/AgeVerificationModal.js +5 -2
  3. package/dist/components/AgeVerificationModal/AgeVerificationModal.js.map +1 -1
  4. package/dist/components/Avatar/Avatar.js +5 -10
  5. package/dist/components/Avatar/Avatar.js.map +1 -1
  6. package/dist/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js +8 -1
  7. package/dist/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js.map +1 -1
  8. package/dist/components/Avatar/AvatarView/AvatarComponent/components/controllers/MorphTargetController.js +1 -1
  9. package/dist/components/Avatar/AvatarView/AvatarComponent/components/controllers/MorphTargetController.js.map +1 -1
  10. package/dist/components/Avatar/AvatarView/AvatarComponent/constants.d.ts +2 -0
  11. package/dist/components/Avatar/AvatarView/AvatarComponent/constants.js +14 -5
  12. package/dist/components/Avatar/AvatarView/AvatarComponent/constants.js.map +1 -1
  13. package/dist/components/ChatBubble/ChatBubble.js +22 -16
  14. package/dist/components/ChatBubble/ChatBubble.js.map +1 -1
  15. package/dist/components/CompletionProviderStatus/CompletionProviderStatus.js +6 -3
  16. package/dist/components/CompletionProviderStatus/CompletionProviderStatus.js.map +1 -1
  17. package/dist/components/DateSelector/DateSelector.d.ts +2 -1
  18. package/dist/components/DateSelector/DateSelector.js +17 -7
  19. package/dist/components/DateSelector/DateSelector.js.map +1 -1
  20. package/dist/components/FilePreview/FilePreview.js +22 -5
  21. package/dist/components/FilePreview/FilePreview.js.map +1 -1
  22. package/dist/components/Header/ChatConsumptionDropdown.js +1 -1
  23. package/dist/components/Header/ChatConsumptionDropdown.js.map +1 -1
  24. package/dist/components/Header/Header.css +2 -1
  25. package/dist/components/Header/Header.js +1 -1
  26. package/dist/components/Header/Header.js.map +1 -1
  27. package/dist/components/MediaWidget/MediaItemWidget.js +14 -12
  28. package/dist/components/MediaWidget/MediaItemWidget.js.map +1 -1
  29. package/dist/components/MediaWidget/MediaItemWidget.utils.d.ts +3 -2
  30. package/dist/components/MediaWidget/MediaItemWidget.utils.js +28 -6
  31. package/dist/components/MediaWidget/MediaItemWidget.utils.js.map +1 -1
  32. package/dist/components/MediaWidget/MediaPreviewModal.js +6 -7
  33. package/dist/components/MediaWidget/MediaPreviewModal.js.map +1 -1
  34. package/dist/components/MemoriArtifactSystem/components/ArtifactActions/styles.css +5 -4
  35. package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.css +90 -38
  36. package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.d.ts +1 -0
  37. package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js +14 -9
  38. package/dist/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js.map +1 -1
  39. package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js +2 -2
  40. package/dist/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
  41. package/dist/components/MemoriWidget/MemoriWidget.js +632 -280
  42. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  43. package/dist/components/StartPanel/StartPanel.js +10 -5
  44. package/dist/components/StartPanel/StartPanel.js.map +1 -1
  45. package/dist/components/UploadButton/UploadButton.js +21 -5
  46. package/dist/components/UploadButton/UploadButton.js.map +1 -1
  47. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +123 -59
  48. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  49. package/dist/components/layouts/Chat.js +25 -30
  50. package/dist/components/layouts/Chat.js.map +1 -1
  51. package/dist/components/layouts/FullPage.js +5 -8
  52. package/dist/components/layouts/FullPage.js.map +1 -1
  53. package/dist/components/layouts/HiddenChat.js +2 -1
  54. package/dist/components/layouts/HiddenChat.js.map +1 -1
  55. package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js +5 -1
  56. package/dist/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
  57. package/dist/components/layouts/WebsiteAssistant/website-assistant.css +3 -4
  58. package/dist/components/layouts/ZoomedFullBody.js +4 -7
  59. package/dist/components/layouts/ZoomedFullBody.js.map +1 -1
  60. package/dist/components/layouts/chat.css +19 -31
  61. package/dist/components/layouts/fullpage.css +52 -19
  62. package/dist/components/layouts/zoomed-full-body.css +40 -1
  63. package/dist/helpers/constants.d.ts +3 -0
  64. package/dist/helpers/constants.js +27 -1
  65. package/dist/helpers/constants.js.map +1 -1
  66. package/dist/helpers/credits.d.ts +5 -2
  67. package/dist/helpers/credits.js +5 -1
  68. package/dist/helpers/credits.js.map +1 -1
  69. package/dist/helpers/message.d.ts +1 -1
  70. package/dist/helpers/message.js +4 -4
  71. package/dist/helpers/message.js.map +1 -1
  72. package/dist/helpers/nats/getNatsConfig.d.ts +9 -0
  73. package/dist/helpers/nats/getNatsConfig.js +65 -0
  74. package/dist/helpers/nats/getNatsConfig.js.map +1 -0
  75. package/dist/helpers/nats/isSessionExpiredError.d.ts +3 -0
  76. package/dist/helpers/nats/isSessionExpiredError.js +36 -0
  77. package/dist/helpers/nats/isSessionExpiredError.js.map +1 -0
  78. package/dist/helpers/nats/useNats.d.ts +12 -0
  79. package/dist/helpers/nats/useNats.js +75 -0
  80. package/dist/helpers/nats/useNats.js.map +1 -0
  81. package/dist/helpers/nats/useNatsSession.d.ts +28 -0
  82. package/dist/helpers/nats/useNatsSession.js +212 -0
  83. package/dist/helpers/nats/useNatsSession.js.map +1 -0
  84. package/dist/helpers/usePressTooltip.d.ts +13 -0
  85. package/dist/helpers/usePressTooltip.js +23 -0
  86. package/dist/helpers/usePressTooltip.js.map +1 -0
  87. package/dist/helpers/utils.d.ts +21 -0
  88. package/dist/helpers/utils.js +75 -4
  89. package/dist/helpers/utils.js.map +1 -1
  90. package/dist/locales/de.json +1 -0
  91. package/dist/locales/en.json +1 -0
  92. package/dist/locales/es.json +1 -0
  93. package/dist/locales/fr.json +1 -0
  94. package/dist/locales/it.json +1 -0
  95. package/dist/styles.css +1 -1
  96. package/dist/types/integration.d.ts +3 -0
  97. package/dist/types/integration.js +16 -0
  98. package/dist/types/integration.js.map +1 -0
  99. package/dist/version.d.ts +1 -1
  100. package/dist/version.js +1 -1
  101. package/esm/components/AgeVerificationModal/AgeVerificationModal.js +5 -2
  102. package/esm/components/AgeVerificationModal/AgeVerificationModal.js.map +1 -1
  103. package/esm/components/Avatar/Avatar.js +5 -10
  104. package/esm/components/Avatar/Avatar.js.map +1 -1
  105. package/esm/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js +9 -2
  106. package/esm/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.js.map +1 -1
  107. package/esm/components/Avatar/AvatarView/AvatarComponent/components/controllers/MorphTargetController.js +2 -2
  108. package/esm/components/Avatar/AvatarView/AvatarComponent/components/controllers/MorphTargetController.js.map +1 -1
  109. package/esm/components/Avatar/AvatarView/AvatarComponent/constants.d.ts +2 -0
  110. package/esm/components/Avatar/AvatarView/AvatarComponent/constants.js +13 -4
  111. package/esm/components/Avatar/AvatarView/AvatarComponent/constants.js.map +1 -1
  112. package/esm/components/ChatBubble/ChatBubble.js +22 -16
  113. package/esm/components/ChatBubble/ChatBubble.js.map +1 -1
  114. package/esm/components/CompletionProviderStatus/CompletionProviderStatus.js +6 -3
  115. package/esm/components/CompletionProviderStatus/CompletionProviderStatus.js.map +1 -1
  116. package/esm/components/DateSelector/DateSelector.d.ts +2 -1
  117. package/esm/components/DateSelector/DateSelector.js +17 -7
  118. package/esm/components/DateSelector/DateSelector.js.map +1 -1
  119. package/esm/components/FilePreview/FilePreview.js +24 -7
  120. package/esm/components/FilePreview/FilePreview.js.map +1 -1
  121. package/esm/components/Header/ChatConsumptionDropdown.js +1 -1
  122. package/esm/components/Header/ChatConsumptionDropdown.js.map +1 -1
  123. package/esm/components/Header/Header.css +2 -1
  124. package/esm/components/Header/Header.js +1 -1
  125. package/esm/components/Header/Header.js.map +1 -1
  126. package/esm/components/MediaWidget/MediaItemWidget.js +16 -14
  127. package/esm/components/MediaWidget/MediaItemWidget.js.map +1 -1
  128. package/esm/components/MediaWidget/MediaItemWidget.utils.d.ts +3 -2
  129. package/esm/components/MediaWidget/MediaItemWidget.utils.js +27 -6
  130. package/esm/components/MediaWidget/MediaItemWidget.utils.js.map +1 -1
  131. package/esm/components/MediaWidget/MediaPreviewModal.js +6 -7
  132. package/esm/components/MediaWidget/MediaPreviewModal.js.map +1 -1
  133. package/esm/components/MemoriArtifactSystem/components/ArtifactActions/styles.css +5 -4
  134. package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.css +90 -38
  135. package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.d.ts +1 -0
  136. package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js +14 -9
  137. package/esm/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.js.map +1 -1
  138. package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js +2 -2
  139. package/esm/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.js.map +1 -1
  140. package/esm/components/MemoriWidget/MemoriWidget.js +632 -280
  141. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  142. package/esm/components/StartPanel/StartPanel.js +10 -5
  143. package/esm/components/StartPanel/StartPanel.js.map +1 -1
  144. package/esm/components/UploadButton/UploadButton.js +21 -5
  145. package/esm/components/UploadButton/UploadButton.js.map +1 -1
  146. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +123 -59
  147. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  148. package/esm/components/layouts/Chat.js +25 -30
  149. package/esm/components/layouts/Chat.js.map +1 -1
  150. package/esm/components/layouts/FullPage.js +5 -8
  151. package/esm/components/layouts/FullPage.js.map +1 -1
  152. package/esm/components/layouts/HiddenChat.js +2 -1
  153. package/esm/components/layouts/HiddenChat.js.map +1 -1
  154. package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js +5 -1
  155. package/esm/components/layouts/WebsiteAssistant/WebsiteAssistant.js.map +1 -1
  156. package/esm/components/layouts/WebsiteAssistant/website-assistant.css +3 -4
  157. package/esm/components/layouts/ZoomedFullBody.js +4 -7
  158. package/esm/components/layouts/ZoomedFullBody.js.map +1 -1
  159. package/esm/components/layouts/chat.css +19 -31
  160. package/esm/components/layouts/fullpage.css +52 -19
  161. package/esm/components/layouts/zoomed-full-body.css +40 -1
  162. package/esm/helpers/constants.d.ts +3 -0
  163. package/esm/helpers/constants.js +26 -0
  164. package/esm/helpers/constants.js.map +1 -1
  165. package/esm/helpers/credits.d.ts +5 -2
  166. package/esm/helpers/credits.js +5 -1
  167. package/esm/helpers/credits.js.map +1 -1
  168. package/esm/helpers/message.d.ts +1 -1
  169. package/esm/helpers/message.js +2 -2
  170. package/esm/helpers/message.js.map +1 -1
  171. package/esm/helpers/nats/getNatsConfig.d.ts +9 -0
  172. package/esm/helpers/nats/getNatsConfig.js +61 -0
  173. package/esm/helpers/nats/getNatsConfig.js.map +1 -0
  174. package/esm/helpers/nats/isSessionExpiredError.d.ts +3 -0
  175. package/esm/helpers/nats/isSessionExpiredError.js +32 -0
  176. package/esm/helpers/nats/isSessionExpiredError.js.map +1 -0
  177. package/esm/helpers/nats/useNats.d.ts +12 -0
  178. package/esm/helpers/nats/useNats.js +72 -0
  179. package/esm/helpers/nats/useNats.js.map +1 -0
  180. package/esm/helpers/nats/useNatsSession.d.ts +28 -0
  181. package/esm/helpers/nats/useNatsSession.js +208 -0
  182. package/esm/helpers/nats/useNatsSession.js.map +1 -0
  183. package/esm/helpers/usePressTooltip.d.ts +13 -0
  184. package/esm/helpers/usePressTooltip.js +20 -0
  185. package/esm/helpers/usePressTooltip.js.map +1 -0
  186. package/esm/helpers/utils.d.ts +21 -0
  187. package/esm/helpers/utils.js +68 -3
  188. package/esm/helpers/utils.js.map +1 -1
  189. package/esm/locales/de.json +1 -0
  190. package/esm/locales/en.json +1 -0
  191. package/esm/locales/es.json +1 -0
  192. package/esm/locales/fr.json +1 -0
  193. package/esm/locales/it.json +1 -0
  194. package/esm/styles.css +1 -1
  195. package/esm/types/integration.d.ts +3 -0
  196. package/esm/types/integration.js +11 -0
  197. package/esm/types/integration.js.map +1 -0
  198. package/esm/version.d.ts +1 -1
  199. package/esm/version.js +1 -1
  200. package/package.json +6 -4
  201. package/src/components/AgeVerificationModal/AgeVerificationModal.tsx +6 -1
  202. package/src/components/Avatar/Avatar.stories.tsx +45 -28
  203. package/src/components/Avatar/Avatar.test.tsx +54 -0
  204. package/src/components/Avatar/Avatar.tsx +8 -12
  205. package/src/components/Avatar/AvatarView/AvatarComponent/components/FullbodyAvatar/fullbodyAvatar.tsx +12 -5
  206. package/src/components/Avatar/AvatarView/AvatarComponent/components/controllers/MorphTargetController.ts +2 -1
  207. package/src/components/Avatar/AvatarView/AvatarComponent/constants.ts +16 -4
  208. package/src/components/Avatar/__snapshots__/Avatar.test.tsx.snap +164 -0
  209. package/src/components/ChatBubble/ChatBubble.tsx +41 -12
  210. package/src/components/CompletionProviderStatus/CompletionProviderStatus.tsx +8 -4
  211. package/src/components/DateSelector/DateSelector.stories.tsx +11 -2
  212. package/src/components/DateSelector/DateSelector.tsx +28 -9
  213. package/src/components/FilePreview/FilePreview.tsx +28 -7
  214. package/src/components/Header/ChatConsumptionDropdown.tsx +5 -1
  215. package/src/components/Header/Header.css +2 -1
  216. package/src/components/Header/Header.tsx +1 -0
  217. package/src/components/MediaWidget/MediaItemWidget.tsx +26 -13
  218. package/src/components/MediaWidget/MediaItemWidget.utils.test.ts +45 -2
  219. package/src/components/MediaWidget/MediaItemWidget.utils.ts +39 -6
  220. package/src/components/MediaWidget/MediaPreviewModal.tsx +9 -7
  221. package/src/components/MemoriArtifactSystem/ArtifactDrawer.stories.tsx +218 -2252
  222. package/src/components/MemoriArtifactSystem/components/ArtifactActions/styles.css +5 -4
  223. package/src/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.css +90 -38
  224. package/src/components/MemoriArtifactSystem/components/ArtifactDrawer/ArtifactDrawer.tsx +31 -14
  225. package/src/components/MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler.tsx +4 -3
  226. package/src/components/MemoriWidget/MemoriWidget.tsx +785 -331
  227. package/src/components/StartPanel/StartPanel.stories.tsx +22 -0
  228. package/src/components/StartPanel/StartPanel.test.tsx +64 -1
  229. package/src/components/StartPanel/StartPanel.tsx +26 -3
  230. package/src/components/StartPanel/__snapshots__/StartPanel.test.tsx.snap +261 -0
  231. package/src/components/UploadButton/UploadButton.tsx +30 -11
  232. package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +148 -61
  233. package/src/components/UploadButton/__snapshots__/UploadButton.test.tsx.snap +2 -2
  234. package/src/components/layouts/Chat.tsx +16 -19
  235. package/src/components/layouts/FullPage.tsx +32 -25
  236. package/src/components/layouts/HiddenChat.tsx +4 -0
  237. package/src/components/layouts/WebsiteAssistant/WebsiteAssistant.tsx +8 -0
  238. package/src/components/layouts/WebsiteAssistant/website-assistant.css +3 -4
  239. package/src/components/layouts/ZoomedFullBody.tsx +30 -24
  240. package/src/components/layouts/__snapshots__/Chat.test.tsx.snap +6 -3
  241. package/src/components/layouts/__snapshots__/FullPage.test.tsx.snap +824 -810
  242. package/src/components/layouts/__snapshots__/ZoomedFullBody.test.tsx.snap +280 -273
  243. package/src/components/layouts/chat.css +19 -31
  244. package/src/components/layouts/fullpage.css +52 -19
  245. package/src/components/layouts/layouts.stories.tsx +131 -12
  246. package/src/components/layouts/zoomed-full-body.css +40 -1
  247. package/src/helpers/constants.ts +32 -1
  248. package/src/helpers/credits.ts +16 -1
  249. package/src/helpers/message.test.ts +28 -1
  250. package/src/helpers/message.ts +4 -4
  251. package/src/helpers/nats/getNatsConfig.ts +145 -0
  252. package/src/helpers/nats/isSessionExpiredError.test.ts +38 -0
  253. package/src/helpers/nats/isSessionExpiredError.ts +51 -0
  254. package/src/helpers/nats/useNats.ts +124 -0
  255. package/src/helpers/nats/useNatsSession.ts +394 -0
  256. package/src/helpers/usePressTooltip.ts +29 -0
  257. package/src/helpers/utils.test.ts +162 -0
  258. package/src/helpers/utils.ts +108 -3
  259. package/src/index.stories.tsx +32 -0
  260. package/src/locales/de.json +1 -0
  261. package/src/locales/en.json +1 -0
  262. package/src/locales/es.json +1 -0
  263. package/src/locales/fr.json +1 -0
  264. package/src/locales/it.json +1 -0
  265. package/src/styles.css +4 -7
  266. package/src/types/integration.test.ts +32 -0
  267. package/src/types/integration.ts +31 -0
  268. package/src/version.ts +1 -1
@@ -0,0 +1,394 @@
1
+ // helpers/nats/useNatsSession.ts - Subscribe to a session's NATS channel.
2
+ //
3
+ // Opens a W3C WebSocket connection (via `wsconnect`), then either:
4
+ // - core NATS: subscribes to the session subject directly, or
5
+ // - JetStream: consumes from the configured stream (ordered consumer by default).
6
+ //
7
+ // When JetStream is enabled, reconnects on tab visibility / network resume and
8
+ // replays the last message per subject to recover events missed while Safari
9
+ // (or other browsers) suspend the WebSocket in background.
10
+ //
11
+ // The cleanup (`consumerMessages.stop()` + `nc.close()`) is essential: without
12
+ // it, every `sessionId` change would leave orphan connections / iterators.
13
+ import { useEffect, useRef, useState, type MutableRefObject } from 'react';
14
+ import { wsconnect, NatsConnection } from '@nats-io/nats-core';
15
+ import {
16
+ jetstream,
17
+ DeliverPolicy,
18
+ type ConsumerMessages,
19
+ } from '@nats-io/jetstream';
20
+ import type { NatsConfig } from './getNatsConfig';
21
+
22
+ /**
23
+ * `progress` event: job advancement updates.
24
+ */
25
+ export interface NatsProgressEvent {
26
+ eventType: 'progress';
27
+ jobId?: string;
28
+ currentStep?: number;
29
+ finalStep?: number;
30
+ message?: string;
31
+ startingTime?: string;
32
+ correlationID?: string;
33
+ }
34
+
35
+ /**
36
+ * `dialog.text_entered_response` event: same body returned by
37
+ * `POST /memori/v2/EnterTextAsync/{sessionId}` (via NATS).
38
+ */
39
+ export interface NatsDialogResponseEvent {
40
+ eventType: 'dialog_text_entered_response';
41
+ requestID?: string;
42
+ resultCode?: number;
43
+ resultMessage?: string;
44
+ currentState?: any;
45
+ correlationID?: string;
46
+ }
47
+
48
+ /**
49
+ * `error` event.
50
+ */
51
+ export interface NatsErrorEvent {
52
+ eventType: 'error';
53
+ errorCode?: string | number;
54
+ errorMessage?: string;
55
+ backtrace?: string;
56
+ correlationID?: string;
57
+ }
58
+
59
+ /**
60
+ * Discriminated union of all events received on the session subject.
61
+ */
62
+ export type NatsSessionEvent =
63
+ | NatsProgressEvent
64
+ | NatsDialogResponseEvent
65
+ | NatsErrorEvent;
66
+
67
+ type JetStreamConsumeMode = 'live' | 'catch-up';
68
+
69
+ function readString(
70
+ raw: Record<string, unknown>,
71
+ camelKey: string,
72
+ snakeKey: string
73
+ ): string | undefined {
74
+ const value = raw[camelKey] ?? raw[snakeKey];
75
+ return typeof value === 'string' ? value : undefined;
76
+ }
77
+
78
+ function readNumber(
79
+ raw: Record<string, unknown>,
80
+ camelKey: string,
81
+ snakeKey: string
82
+ ): number | undefined {
83
+ const value = raw[camelKey] ?? raw[snakeKey];
84
+ return typeof value === 'number' ? value : undefined;
85
+ }
86
+
87
+ /**
88
+ * Unwraps the `currentState` of a `dialog_text_entered_response`.
89
+ *
90
+ * The async (NATS) response wraps the dialog state one level deeper than the
91
+ * synchronous HTTP response: `currentState` is a `ResponseSpec` that *contains*
92
+ * the actual `DialogState` under a nested `currentState` key. We unwrap it so
93
+ * downstream consumers always receive the `DialogState` directly, while still
94
+ * accepting the already-flat shape for backward compatibility.
95
+ */
96
+ function unwrapDialogState(value: unknown): unknown {
97
+ if (
98
+ value &&
99
+ typeof value === 'object' &&
100
+ 'currentState' in (value as Record<string, unknown>)
101
+ ) {
102
+ return (value as Record<string, unknown>).currentState;
103
+ }
104
+ return value;
105
+ }
106
+
107
+ /**
108
+ * Normalizes raw NATS payloads (snake_case or camelCase) into typed events.
109
+ */
110
+ export function normalizeNatsEvent(raw: Record<string, unknown>): NatsSessionEvent {
111
+ const eventType = (readString(raw, 'eventType', 'event_type') ??
112
+ 'unknown') as NatsSessionEvent['eventType'];
113
+
114
+ const correlationID =
115
+ readString(raw, 'correlationID', 'correlation_id') ??
116
+ readString(raw, 'correlationId', 'correlation_id');
117
+
118
+ if (eventType === 'progress') {
119
+ return {
120
+ eventType: 'progress',
121
+ jobId: readString(raw, 'jobId', 'job_id'),
122
+ currentStep: readNumber(raw, 'currentStep', 'current_step'),
123
+ finalStep: readNumber(raw, 'finalStep', 'final_step'),
124
+ message: typeof raw.message === 'string' ? raw.message : undefined,
125
+ startingTime: readString(raw, 'startingTime', 'starting_time'),
126
+ correlationID,
127
+ };
128
+ }
129
+
130
+ if (eventType === 'dialog_text_entered_response') {
131
+ return {
132
+ eventType: 'dialog_text_entered_response',
133
+ requestID:
134
+ typeof raw.requestID === 'string' ? raw.requestID : undefined,
135
+ resultCode:
136
+ typeof raw.resultCode === 'number' ? raw.resultCode : undefined,
137
+ resultMessage:
138
+ typeof raw.resultMessage === 'string' ? raw.resultMessage : undefined,
139
+ currentState: unwrapDialogState(raw.currentState),
140
+ correlationID,
141
+ };
142
+ }
143
+
144
+ if (eventType === 'error') {
145
+ return {
146
+ eventType: 'error',
147
+ errorCode: (raw.errorCode ?? raw.error_code) as string | number | undefined,
148
+ errorMessage: readString(raw, 'errorMessage', 'error_message'),
149
+ backtrace: typeof raw.backtrace === 'string' ? raw.backtrace : undefined,
150
+ correlationID,
151
+ };
152
+ }
153
+
154
+ return {
155
+ eventType,
156
+ correlationID,
157
+ } as NatsSessionEvent;
158
+ }
159
+
160
+ function decodeMessage(
161
+ subject: string,
162
+ received: number,
163
+ raw: Record<string, unknown>,
164
+ onMessage: (event: NatsSessionEvent) => void
165
+ ) {
166
+ const payload = normalizeNatsEvent(raw);
167
+ console.debug(
168
+ `[NATS] message #${received} on ${subject} (eventType=${payload.eventType})`,
169
+ payload
170
+ );
171
+ onMessage(payload);
172
+ }
173
+
174
+ async function consumeCoreSubscription(
175
+ nc: NatsConnection,
176
+ subject: string,
177
+ onMessage: (event: NatsSessionEvent) => void
178
+ ) {
179
+ const sub = nc.subscribe(subject);
180
+ console.info('[NATS] subscribed to subject', subject);
181
+ let received = 0;
182
+ for await (const msg of sub) {
183
+ received += 1;
184
+ try {
185
+ decodeMessage(
186
+ subject,
187
+ received,
188
+ msg.json<Record<string, unknown>>(),
189
+ onMessage
190
+ );
191
+ } catch (err) {
192
+ console.error('[NATS] message decode error', err);
193
+ }
194
+ }
195
+ console.info(
196
+ `[NATS] subscription on ${subject} closed (received ${received} messages)`
197
+ );
198
+ }
199
+
200
+ async function consumeJetStream(
201
+ nc: NatsConnection,
202
+ config: Pick<NatsConfig, 'stream' | 'consumer'>,
203
+ subject: string,
204
+ mode: JetStreamConsumeMode,
205
+ onMessage: (event: NatsSessionEvent) => void,
206
+ setConsumerMessages: (messages: ConsumerMessages) => void
207
+ ) {
208
+ const js = jetstream(nc);
209
+ const consumer = config.consumer
210
+ ? await js.consumers.get(config.stream!, config.consumer)
211
+ : await js.consumers.get(config.stream!, {
212
+ filter_subjects: [subject],
213
+ deliver_policy:
214
+ mode === 'catch-up'
215
+ ? DeliverPolicy.LastPerSubject
216
+ : DeliverPolicy.New,
217
+ });
218
+
219
+ console.info(
220
+ '[NATS] JetStream consumer ready',
221
+ config.consumer
222
+ ? { stream: config.stream, consumer: config.consumer, subject, mode }
223
+ : { stream: config.stream, subject, mode }
224
+ );
225
+
226
+ const messages = await consumer.consume();
227
+ setConsumerMessages(messages);
228
+ let received = 0;
229
+ for await (const msg of messages) {
230
+ received += 1;
231
+ try {
232
+ decodeMessage(subject, received, msg.json(), onMessage);
233
+ } catch (err) {
234
+ console.error('[NATS] message decode error', err);
235
+ }
236
+ }
237
+ console.info(
238
+ `[NATS] JetStream consumer on ${subject} closed (received ${received} messages)`
239
+ );
240
+ }
241
+
242
+ function closeNatsSession(
243
+ consumerMessagesRef: MutableRefObject<ConsumerMessages | null>,
244
+ connRef: MutableRefObject<NatsConnection | null>
245
+ ) {
246
+ consumerMessagesRef.current?.stop();
247
+ consumerMessagesRef.current = null;
248
+ connRef.current?.close();
249
+ connRef.current = null;
250
+ }
251
+
252
+ /**
253
+ * Open a NATS WebSocket connection, subscribe to the session subject and
254
+ * forward decoded events to `onMessage`.
255
+ *
256
+ * `onMessage` is kept in a ref so callers do not need to memoize it: the
257
+ * connection is only (re)opened when `sessionId`, `config` or `resumeGeneration`
258
+ * change.
259
+ *
260
+ * @param sessionId Current session UUID (subscription is skipped when falsy).
261
+ * @param config Connection config from `/api/nats` (url + token required).
262
+ * @param onMessage Callback invoked for each decoded event.
263
+ */
264
+ export function useNatsSession(
265
+ sessionId: string | undefined,
266
+ config: NatsConfig | undefined,
267
+ onMessage: (event: NatsSessionEvent) => void
268
+ ) {
269
+ const connRef = useRef<NatsConnection | null>(null);
270
+ const consumerMessagesRef = useRef<ConsumerMessages | null>(null);
271
+ const onMessageRef = useRef(onMessage);
272
+ const wasHiddenRef = useRef(false);
273
+ const [resumeGeneration, setResumeGeneration] = useState(0);
274
+
275
+ // Keep the latest callback without retriggering the connection effect.
276
+ useEffect(() => {
277
+ onMessageRef.current = onMessage;
278
+ }, [onMessage]);
279
+
280
+ // Reconnect when the tab returns from background or the network comes back.
281
+ useEffect(() => {
282
+ if (!sessionId || !config?.stream) {
283
+ return;
284
+ }
285
+
286
+ const requestResume = () => {
287
+ if (document.visibilityState !== 'visible') {
288
+ return;
289
+ }
290
+ console.info('[NATS] resuming JetStream after visibility/network change');
291
+ setResumeGeneration(generation => generation + 1);
292
+ };
293
+
294
+ const onVisibilityChange = () => {
295
+ if (document.visibilityState === 'hidden') {
296
+ wasHiddenRef.current = true;
297
+ return;
298
+ }
299
+ if (wasHiddenRef.current) {
300
+ wasHiddenRef.current = false;
301
+ requestResume();
302
+ }
303
+ };
304
+
305
+ const onPageShow = (event: PageTransitionEvent) => {
306
+ if (event.persisted) {
307
+ requestResume();
308
+ }
309
+ };
310
+
311
+ document.addEventListener('visibilitychange', onVisibilityChange);
312
+ window.addEventListener('online', requestResume);
313
+ window.addEventListener('pageshow', onPageShow);
314
+
315
+ return () => {
316
+ document.removeEventListener('visibilitychange', onVisibilityChange);
317
+ window.removeEventListener('online', requestResume);
318
+ window.removeEventListener('pageshow', onPageShow);
319
+ };
320
+ }, [sessionId, config?.stream]);
321
+
322
+ useEffect(() => {
323
+ if (!sessionId || !config?.url || !config.token) {
324
+ console.debug(
325
+ '[NATS] subscription skipped (missing sessionId/url/token)',
326
+ {
327
+ sessionId,
328
+ hasUrl: !!config?.url,
329
+ hasToken: !!config?.token,
330
+ }
331
+ );
332
+ return;
333
+ }
334
+
335
+ let closed = false;
336
+ const subject = config.subject ?? `sessions.${sessionId}`;
337
+ const useJetStream = !!config.stream;
338
+ const jetStreamMode: JetStreamConsumeMode =
339
+ useJetStream && resumeGeneration > 0 ? 'catch-up' : 'live';
340
+
341
+ (async () => {
342
+ console.info(
343
+ '[NATS] connecting to',
344
+ config.url,
345
+ 'for session',
346
+ sessionId,
347
+ useJetStream ? `(JetStream/${jetStreamMode})` : '(core)'
348
+ );
349
+ const nc = await wsconnect({
350
+ servers: [config.url],
351
+ token: config.token,
352
+ });
353
+ if (closed) {
354
+ console.debug(
355
+ '[NATS] connection established after cleanup, closing immediately'
356
+ );
357
+ await nc.close();
358
+ return;
359
+ }
360
+ connRef.current = nc;
361
+ console.info('[NATS] connected to', nc.getServer());
362
+
363
+ const dispatch = (event: NatsSessionEvent) => {
364
+ onMessageRef.current(event);
365
+ };
366
+
367
+ if (useJetStream) {
368
+ await consumeJetStream(
369
+ nc,
370
+ config,
371
+ subject,
372
+ jetStreamMode,
373
+ dispatch,
374
+ messages => {
375
+ consumerMessagesRef.current = messages;
376
+ }
377
+ );
378
+ } else {
379
+ await consumeCoreSubscription(nc, subject, dispatch);
380
+ }
381
+ })().catch(err => console.error('[NATS] connection error', err));
382
+
383
+ return () => {
384
+ console.info('[NATS] cleanup: closing connection for subject', subject);
385
+ closed = true;
386
+ closeNatsSession(consumerMessagesRef, connRef);
387
+ };
388
+ }, [sessionId, config, resumeGeneration]);
389
+
390
+ // Reset catch-up generation when the active session changes.
391
+ useEffect(() => {
392
+ setResumeGeneration(0);
393
+ }, [sessionId]);
394
+ }
@@ -0,0 +1,29 @@
1
+ import {
2
+ useCallback,
3
+ useState,
4
+ type MouseEvent,
5
+ type PointerEvent,
6
+ } from 'react';
7
+
8
+ export function usePressTooltip(defaultOpen = false) {
9
+ const [open, setOpen] = useState(defaultOpen);
10
+
11
+ const toggleOpen = useCallback((e: MouseEvent | PointerEvent) => {
12
+ e.stopPropagation();
13
+ setOpen(prev => !prev);
14
+ }, []);
15
+
16
+ const pressTriggerProps = {
17
+ onPointerDown: (e: PointerEvent) => {
18
+ e.stopPropagation();
19
+ },
20
+ onClick: toggleOpen,
21
+ } as const;
22
+
23
+ const tooltipProps = {
24
+ open,
25
+ onOpenChange: setOpen,
26
+ } as const;
27
+
28
+ return { open, setOpen, pressTriggerProps, tooltipProps };
29
+ }
@@ -1,9 +1,15 @@
1
1
  import {
2
2
  difference,
3
3
  stripEmojis,
4
+ stripHTML,
4
5
  stripMarkdown,
5
6
  stripOutputTags,
6
7
  escapeHTML,
8
+ extractAttachmentLinks,
9
+ extractAttachmentLink,
10
+ isAssetOnlyDocumentAttachment,
11
+ isOfficeNativeFilename,
12
+ parseDocumentAttachmentsFromMessage,
7
13
  } from './utils';
8
14
 
9
15
  describe('Utils/difference', () => {
@@ -141,6 +147,162 @@ describe('utils/stripOutputTags', () => {
141
147
  });
142
148
  });
143
149
 
150
+ describe('utils/attachment helpers', () => {
151
+ it('extracts multiple attachment links in order', () => {
152
+ const input = [
153
+ '<document_attachment filename="a.docx" type="application/vnd.openxmlformats-officedocument.wordprocessingml.document">',
154
+ '</document_attachment>',
155
+ '<attachment_link>',
156
+ 'https://assets.example.com/a.docx',
157
+ '</attachment_link>',
158
+ '<document_attachment filename="b.pdf" type="application/pdf">',
159
+ 'pdf text',
160
+ '</document_attachment>',
161
+ '<attachment_link>https://assets.example.com/b.txt</attachment_link>',
162
+ ].join('\n');
163
+
164
+ expect(extractAttachmentLinks(input)).toEqual([
165
+ 'https://assets.example.com/a.docx',
166
+ 'https://assets.example.com/b.txt',
167
+ ]);
168
+ });
169
+
170
+ it('extracts a single attachment link', () => {
171
+ const input =
172
+ '<attachment_link>\nhttps://assets.example.com/file.docx\n</attachment_link>';
173
+ expect(extractAttachmentLink(input)).toBe(
174
+ 'https://assets.example.com/file.docx'
175
+ );
176
+ });
177
+
178
+ it('detects office native filenames', () => {
179
+ expect(isOfficeNativeFilename('report.doc')).toBe(true);
180
+ expect(isOfficeNativeFilename('report.docx')).toBe(true);
181
+ expect(isOfficeNativeFilename('budget.xls')).toBe(false);
182
+ expect(isOfficeNativeFilename('template.XLTX')).toBe(true);
183
+ expect(isOfficeNativeFilename('slides.potx')).toBe(true);
184
+ expect(isOfficeNativeFilename('notes.pdf')).toBe(false);
185
+ expect(isOfficeNativeFilename('data.xlsx')).toBe(false);
186
+ });
187
+
188
+ it('detects asset-only document attachments', () => {
189
+ expect(
190
+ isAssetOnlyDocumentAttachment({
191
+ title: 'report.docx',
192
+ content: '',
193
+ url: 'https://assets.example.com/file.docx',
194
+ })
195
+ ).toBe(true);
196
+ expect(
197
+ isAssetOnlyDocumentAttachment({
198
+ title: 'report.docx',
199
+ content: [
200
+ '<document_attachment filename="report.docx" type="application/vnd.openxmlformats-officedocument.wordprocessingml.document">',
201
+ '</document_attachment>',
202
+ '<attachment_link>https://assets.example.com/file.docx</attachment_link>',
203
+ ].join('\n'),
204
+ url: '',
205
+ })
206
+ ).toBe(true);
207
+ expect(
208
+ isAssetOnlyDocumentAttachment({
209
+ content: 'extracted text',
210
+ url: 'https://assets.example.com/file.txt',
211
+ })
212
+ ).toBe(false);
213
+ });
214
+
215
+ it('pairs each document attachment with its adjacent link', () => {
216
+ const input = [
217
+ '<attachment_link>https://assets.example.com/orphan.txt</attachment_link>',
218
+ '<document_attachment filename="a.docx" type="application/vnd.openxmlformats-officedocument.wordprocessingml.document">',
219
+ '</document_attachment>',
220
+ '<attachment_link>https://assets.example.com/a.docx</attachment_link>',
221
+ '<document_attachment filename="b.pdf" type="application/pdf">',
222
+ 'pdf text',
223
+ '</document_attachment>',
224
+ '<attachment_link>https://assets.example.com/b.txt</attachment_link>',
225
+ ].join('\n');
226
+
227
+ expect(parseDocumentAttachmentsFromMessage(input)).toEqual([
228
+ {
229
+ filename: 'a.docx',
230
+ type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
231
+ content: '',
232
+ url: 'https://assets.example.com/a.docx',
233
+ },
234
+ {
235
+ filename: 'b.pdf',
236
+ type: 'application/pdf',
237
+ content: 'pdf text',
238
+ url: 'https://assets.example.com/b.txt',
239
+ },
240
+ ]);
241
+ });
242
+
243
+ it('returns an empty url when no adjacent attachment link exists', () => {
244
+ const input =
245
+ '<document_attachment filename="a.docx" type="application/vnd.openxmlformats-officedocument.wordprocessingml.document"></document_attachment>';
246
+
247
+ expect(parseDocumentAttachmentsFromMessage(input)).toEqual([
248
+ {
249
+ filename: 'a.docx',
250
+ type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
251
+ content: '',
252
+ url: '',
253
+ },
254
+ ]);
255
+ });
256
+ });
257
+
258
+ describe('utils/stripHTML', () => {
259
+ it('strips tags and returns text content', () => {
260
+ expect(stripHTML('<p>Hello <strong>world</strong></p>')).toBe(
261
+ 'Hello world'
262
+ );
263
+ });
264
+
265
+ it('does not retain onerror from partner XSS payload', () => {
266
+ const payload =
267
+ '<img src="immagine-inesistente.jpg" onerror="alert(\'XSS Eseguito!\')">';
268
+ const result = stripHTML(payload);
269
+ expect(result).toBe('');
270
+ expect(result).not.toMatch(/onerror/i);
271
+ // Re-parse result: must not introduce an executable handler attribute
272
+ const reparsed = new DOMParser().parseFromString(result, 'text/html');
273
+ expect(reparsed.querySelector('[onerror]')).toBeNull();
274
+ expect(reparsed.querySelector('img')).toBeNull();
275
+ });
276
+
277
+ it('strips svg onload payloads without leaving handlers', () => {
278
+ const payload = '<svg onload="alert(1)"><text>x</text></svg>';
279
+ const result = stripHTML(payload);
280
+ expect(result).toBe('x');
281
+ expect(result).not.toMatch(/onload/i);
282
+ });
283
+
284
+ it('decodes HTML entities without executing nested markup', () => {
285
+ const encoded =
286
+ '&lt;img src="x" onerror="alert(1)"&gt;hello&lt;/img&gt;';
287
+ const result = stripHTML(encoded);
288
+ expect(result).toContain('hello');
289
+ // Entity-decoded text may contain the string "onerror" as text; ensure no live element
290
+ const reparsed = new DOMParser().parseFromString(
291
+ `<div>${result.replace(/</g, '&lt;')}</div>`,
292
+ 'text/html'
293
+ );
294
+ expect(reparsed.querySelector('[onerror]')).toBeNull();
295
+ });
296
+
297
+ it('handles nested tags', () => {
298
+ expect(stripHTML('<div><span>a</span><b>b</b></div>')).toBe('ab');
299
+ });
300
+
301
+ it('returns empty string for empty input', () => {
302
+ expect(stripHTML('')).toBe('');
303
+ });
304
+ });
305
+
144
306
  describe('utils/parsing combined', () => {
145
307
  it('should remove output tag from real message', () => {
146
308
  const result = escapeHTML(