@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
@@ -92,6 +92,16 @@ import { sanitizeText } from '../../helpers/sanitizer';
92
92
  import { TTSConfig, useTTS } from '../../helpers/tts/useTTS';
93
93
  import ChatHistoryDrawer from '../ChatHistoryDrawer/ChatHistory';
94
94
  import { STTConfig, useSTT } from '../../helpers/stt/useSTT';
95
+ import { useNats } from '../../helpers/nats/useNats';
96
+ import {
97
+ NatsProgressEvent,
98
+ NatsDialogResponseEvent,
99
+ NatsErrorEvent,
100
+ } from '../../helpers/nats/useNatsSession';
101
+ import {
102
+ isSessionExpiredNatsError,
103
+ isSessionExpiredNatsResponse,
104
+ } from '../../helpers/nats/isSessionExpiredError';
95
105
 
96
106
  // Widget utilities and helpers
97
107
  const getMemoriState = (integrationId?: string): object | null => {
@@ -122,7 +132,21 @@ const getMemoriState = (integrationId?: string): object | null => {
122
132
  };
123
133
  };
124
134
 
125
- /** Place spec with all nulls for postTextEnteredEvent when position is not set or user chose "I don't want to provide my position". */
135
+ /** Unified login token resolution: React state > ref > call-site override > props. */
136
+ const resolveLoginTokenValue = (
137
+ loginToken: string | undefined,
138
+ userTokenRef: string | undefined,
139
+ callAdditionalInfoLoginToken: string | undefined,
140
+ additionalInfoLoginToken: string | undefined,
141
+ authTokenProp: string | undefined
142
+ ): string | undefined =>
143
+ loginToken ??
144
+ userTokenRef ??
145
+ callAdditionalInfoLoginToken ??
146
+ additionalInfoLoginToken ??
147
+ authTokenProp;
148
+
149
+ /** Place spec with all nulls for postEnterTextAsync when position is not set or user chose "I don't want to provide my position". */
126
150
  const NULL_PLACE_SPEC = {
127
151
  placeName: null,
128
152
  latitude: null,
@@ -130,6 +154,18 @@ const NULL_PLACE_SPEC = {
130
154
  uncertaintyKm: null,
131
155
  } as const;
132
156
 
157
+ const logWidgetError = (context: string, detail?: unknown) => {
158
+ console.error(`[MemoriWidget] ${context}`, detail ?? '');
159
+ };
160
+
161
+ /** Reads correlation id from HTTP async response (supports camelCase / snake_case). */
162
+ function readCorrelationID(response: {
163
+ correlationID?: string;
164
+ }): string | undefined {
165
+ const value = response.correlationID;
166
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
167
+ }
168
+
133
169
  type MemoriTextEnteredEvent = CustomEvent<{
134
170
  text: string;
135
171
  waitForPrevious?: boolean;
@@ -363,7 +399,6 @@ window.typeBatchMessages = typeBatchMessages;
363
399
  let audioContext: IAudioContext;
364
400
 
365
401
  let memoriPassword: string | undefined;
366
- let userToken: string | undefined;
367
402
 
368
403
  export interface LayoutProps {
369
404
  Header?: typeof Header;
@@ -580,6 +615,7 @@ const MemoriWidget = ({
580
615
  const {
581
616
  initSession,
582
617
  deleteSession,
618
+ postEnterTextAsync,
583
619
  postTextEnteredEvent,
584
620
  postPlaceChangedEvent,
585
621
  postDateChangedEvent,
@@ -617,36 +653,85 @@ const MemoriWidget = ({
617
653
  const [loginToken, setLoginToken] = useState<string | undefined>(
618
654
  additionalInfo?.loginToken ?? authToken
619
655
  );
656
+ const userTokenRef = useRef<string | undefined>(
657
+ additionalInfo?.loginToken ?? authToken
658
+ );
659
+ const prevPropsLoginTokenRef = useRef<string | undefined>(
660
+ additionalInfo?.loginToken ?? authToken
661
+ );
662
+
620
663
  const [user, setUser] = useState<User | undefined>({
621
664
  avatarURL: typeof userAvatar === 'string' ? userAvatar : undefined,
622
665
  } as User);
666
+
667
+ const resolveLoginToken = useCallback(
668
+ (callAdditionalInfoLoginToken?: string) =>
669
+ resolveLoginTokenValue(
670
+ loginToken,
671
+ userTokenRef.current,
672
+ callAdditionalInfoLoginToken,
673
+ additionalInfo?.loginToken,
674
+ authToken
675
+ ),
676
+ [loginToken, additionalInfo?.loginToken, authToken]
677
+ );
678
+
679
+ // Sync loginToken when props change (including explicit removal from parent)
623
680
  useEffect(() => {
624
- if (
625
- loginToken &&
626
- !user?.userID &&
627
- (showLogin || memori.requireLoginToken)
628
- ) {
629
- client.backend
630
- .pwlGetCurrentUser(loginToken)
631
- .then(({ user, resultCode }) => {
632
- if (user && resultCode === 0) {
633
- setUser(user);
634
- setLocalConfig('loginToken', loginToken);
635
-
636
- if (!birthDate && user.birthDate) {
637
- setBirthDate(user.birthDate);
638
- setLocalConfig('birthDate', user.birthDate);
639
- }
640
- } else {
641
- removeLocalConfig('loginToken');
642
- }
643
- });
681
+ const incomingToken = additionalInfo?.loginToken ?? authToken;
682
+ const prevPropsToken = prevPropsLoginTokenRef.current;
683
+ prevPropsLoginTokenRef.current = incomingToken;
684
+
685
+ if (incomingToken !== prevPropsToken) {
686
+ setLoginToken(incomingToken);
687
+ userTokenRef.current = incomingToken;
688
+ if (!incomingToken) {
689
+ removeLocalConfig('loginToken');
690
+ setUser(undefined);
691
+ }
692
+ }
693
+ }, [additionalInfo?.loginToken, authToken]);
694
+
695
+ useEffect(() => {
696
+ if (!loginToken || !(showLogin || memori.requireLoginToken)) {
697
+ return;
644
698
  }
645
- }, [loginToken, user?.userID]);
699
+
700
+ let cancelled = false;
701
+
702
+ client.backend
703
+ .pwlGetCurrentUser(loginToken)
704
+ .then(({ user: fetchedUser, resultCode }) => {
705
+ if (cancelled) return;
706
+
707
+ if (fetchedUser && resultCode === 0) {
708
+ setUser(fetchedUser);
709
+ setLocalConfig('loginToken', loginToken);
710
+ userTokenRef.current = loginToken;
711
+
712
+ if (!birthDate && fetchedUser.birthDate) {
713
+ setBirthDate(fetchedUser.birthDate);
714
+ setLocalConfig('birthDate', fetchedUser.birthDate);
715
+ }
716
+ } else {
717
+ setLoginToken(undefined);
718
+ userTokenRef.current = undefined;
719
+ setUser(undefined);
720
+ removeLocalConfig('loginToken');
721
+ }
722
+ });
723
+
724
+ return () => {
725
+ cancelled = true;
726
+ };
727
+ }, [loginToken, showLogin, memori.requireLoginToken]);
646
728
  const [showLoginDrawer, setShowLoginDrawer] = useState(false);
647
729
 
648
730
  const [clickedStart, setClickedStart] = useState(false);
649
731
  const [gotErrorInOpening, setGotErrorInOpening] = useState(false);
732
+ const sessionStartingRef = useRef(false);
733
+ const needsCredits = tenant?.billingDelegation;
734
+ const [hasEnoughCredits, setHasEnoughCredits] = useState<boolean>(true);
650
735
 
651
736
  const language =
652
737
  memori.culture?.split('-')?.[0]?.toUpperCase()! ||
@@ -708,7 +793,33 @@ const MemoriWidget = ({
708
793
  const [memoriTyping, setMemoriTyping] = useState<boolean>(false);
709
794
  const [typingText, setTypingText] = useState<string>();
710
795
 
711
- // Layout: prop can be string or object { name, piiDetection } (e.g. from integration customData when PII is enabled).
796
+ type EnterTextRetryParams = {
797
+ text?: string;
798
+ media?: Medium[];
799
+ translate?: boolean;
800
+ translatedText?: string;
801
+ hidden?: boolean;
802
+ typingText?: string;
803
+ useLoaderTextAsMsg?: boolean;
804
+ hasBatchQueued?: boolean;
805
+ expiredSessionID?: string;
806
+ continueFromChatLogID?: string;
807
+ };
808
+
809
+ type PendingEnterText = EnterTextRetryParams & {
810
+ msg?: string;
811
+ waitForResponse?: {
812
+ resolve: (event: NatsDialogResponseEvent) => void;
813
+ reject: (error: Error) => void;
814
+ timeoutId: ReturnType<typeof setTimeout>;
815
+ };
816
+ };
817
+ const pendingEnterTextRef = useRef<Map<string, PendingEnterText>>(new Map());
818
+ const bufferedNatsResponsesRef = useRef<Map<string, NatsDialogResponseEvent>>(
819
+ new Map()
820
+ );
821
+
822
+ // Layout: from prop (string only) or integrationConfig. PII detection is only from integrationConfig (customData.layout as object with piiDetection).
712
823
  const layoutName =
713
824
  typeof layout === 'string'
714
825
  ? layout
@@ -825,7 +936,7 @@ const MemoriWidget = ({
825
936
 
826
937
  if (!additionalInfo?.loginToken && !authToken) {
827
938
  setLoginToken(getLocalConfig<typeof loginToken>('loginToken', undefined));
828
- userToken = getLocalConfig<typeof loginToken>('loginToken', undefined);
939
+ userTokenRef.current = getLocalConfig<typeof loginToken>('loginToken', undefined);
829
940
 
830
941
  setBirthDate(getLocalConfig<string | undefined>('birthDate', undefined));
831
942
  }
@@ -900,7 +1011,7 @@ const MemoriWidget = ({
900
1011
  return Object.keys(place).length > 0 ? place : undefined;
901
1012
  }, []);
902
1013
 
903
- /** Place to send with postTextEnteredEvent: real place, nulls when no/declined position, or undefined when position not needed. */
1014
+ /** Place to send with postEnterTextAsync: real place, nulls when no/declined position, or undefined when position not needed. */
904
1015
  const getPlaceSpecForEnterText = useCallback(
905
1016
  (venue: Venue | undefined) => {
906
1017
  if (!memori.needsPosition) return undefined;
@@ -974,6 +1085,7 @@ const MemoriWidget = ({
974
1085
  * @param typingText Optional custom typing indicator text
975
1086
  * @param useLoaderTextAsMsg Whether to use the loader text as the message (default false)
976
1087
  * @param hasBatchQueued Whether there are more messages queued to be sent (default false)
1088
+ * @param skipHistoryPush Skip adding the user message to history (e.g. session-expired retry)
977
1089
  */
978
1090
  const sendMessage = async (
979
1091
  text: string,
@@ -984,7 +1096,8 @@ const MemoriWidget = ({
984
1096
  hidden: boolean = false,
985
1097
  typingText?: string,
986
1098
  useLoaderTextAsMsg = false,
987
- hasBatchQueued = false
1099
+ hasBatchQueued = false,
1100
+ skipHistoryPush = false
988
1101
  ) => {
989
1102
  // Get the session ID from params or global state
990
1103
  const sessionID =
@@ -1051,7 +1164,7 @@ const MemoriWidget = ({
1051
1164
  }
1052
1165
 
1053
1166
  // Add user message to chat history if not hidden
1054
- if (!hidden)
1167
+ if (!hidden && !skipHistoryPush)
1055
1168
  pushMessage({
1056
1169
  text: text,
1057
1170
  translatedText,
@@ -1062,27 +1175,12 @@ const MemoriWidget = ({
1062
1175
  : !!newSessionId,
1063
1176
  });
1064
1177
 
1065
- // Show typing indicator
1066
- setMemoriTyping(true);
1067
- setTypingText(typingText);
1068
-
1178
+ // Show typing indicator after the async enter-text request is accepted (HTTP 200).
1069
1179
  let gotError = false;
1070
1180
 
1071
1181
  try {
1072
- // Add chat reference link to the message if it exists
1073
- // if (chatLogID) {
1074
- // msg =
1075
- // msg +
1076
- // ' \n\n' +
1077
- // '<chat-reference session-id="' +
1078
- // sessionID +
1079
- // '" event-log-id="' +
1080
- // chatLogID +
1081
- // '"></chat-reference>';
1082
- // }
1083
-
1084
1182
  const placeSpec = getPlaceSpecForEnterText(position);
1085
- const { currentState, ...response } = await postTextEnteredEvent({
1183
+ const response = await postEnterTextAsync({
1086
1184
  sessionId: sessionID,
1087
1185
  text: msg,
1088
1186
  ...(memori.needsDateTime && {
@@ -1090,85 +1188,35 @@ const MemoriWidget = ({
1090
1188
  }),
1091
1189
  ...(placeSpec !== undefined && { place: placeSpec }),
1092
1190
  });
1093
- if (response.resultCode === 0 && currentState) {
1094
- setChatLogID(undefined);
1095
- const emission =
1096
- useLoaderTextAsMsg && typingText
1097
- ? typingText
1098
- : currentState.emission ?? currentDialogState?.emission;
1099
-
1100
- if (
1101
- userLang.toLowerCase() !== language.toLowerCase() &&
1102
- emission &&
1103
- isMultilanguageEnabled
1104
- ) {
1105
- currentState.emission = emission;
1106
-
1107
- translateDialogState(currentState, userLang, msg).then(ts => {
1108
- let text = ts.translatedEmission || ts.emission;
1109
- if (text && shouldPlayAudio(text)) {
1110
- handleSpeak(text);
1111
- }
1112
- });
1113
- } else {
1114
- setCurrentDialogState({
1115
- ...currentState,
1116
- emission,
1117
- });
1118
-
1119
- if (emission) {
1120
- pushMessage({
1121
- text: emission,
1122
- emitter: currentState.emitter,
1123
- media: currentState.emittedMedia ?? currentState.media,
1124
- llmUsage: (currentState as any).llmUsage,
1125
- fromUser: false,
1126
- questionAnswered: msg,
1127
- generatedByAI: !!currentState.completion,
1128
- contextVars: currentState.contextVars,
1129
- date: currentState.currentDate,
1130
- placeName: currentState.currentPlaceName,
1131
- placeLatitude: currentState.currentLatitude,
1132
- placeLongitude: currentState.currentLongitude,
1133
- placeUncertaintyKm: currentState.currentUncertaintyKm,
1134
- tag: currentState.currentTag,
1135
- memoryTags: currentState.memoryTags,
1136
- } as any);
1137
- if (emission && shouldPlayAudio(emission)) {
1138
- handleSpeak(emission);
1139
- }
1140
- }
1141
- }
1191
+ const correlationID = readCorrelationID(response);
1192
+ if (response.resultCode === 0 && correlationID) {
1193
+ registerPendingEnterText(correlationID, {
1194
+ msg,
1195
+ text,
1196
+ media,
1197
+ translate,
1198
+ translatedText,
1199
+ hidden,
1200
+ typingText,
1201
+ useLoaderTextAsMsg,
1202
+ hasBatchQueued,
1203
+ });
1204
+ setMemoriTyping(true);
1205
+ setTypingText(typingText);
1206
+ } else if (response.resultCode === 0) {
1207
+ logWidgetError('enter-text missing correlationID', response);
1142
1208
  } else if (response.resultCode === 404) {
1143
- // Handle expired session
1144
- // remove last sent message, will set it as initial
1145
- setHistory(h => [...h.slice(0, h.length - 1)]);
1146
-
1147
- reopenSession(
1148
- true,
1149
- memoriPwd || memori.secretToken,
1150
- memoriTokens,
1151
- undefined,
1152
- undefined,
1153
- {
1154
- LANG: userLang,
1155
- PATHNAME: window.location.pathname,
1156
- ROUTE: window.location.pathname?.split('/')?.pop() || '',
1157
- ...(initialContextVars || {}),
1158
- },
1159
- initialQuestion,
1160
- undefined,
1161
- undefined,
1162
- undefined,
1163
- undefined,
1164
- true // isSessionExpired
1165
- ).then(state => {
1166
- console.info('session timeout');
1167
- if (state?.sessionID) {
1168
- setTimeout(() => {
1169
- sendMessage(text, media, state?.sessionID);
1170
- }, 500);
1171
- }
1209
+ retryAfterExpiredSessionRef.current({
1210
+ text,
1211
+ media,
1212
+ translate,
1213
+ translatedText,
1214
+ hidden,
1215
+ typingText,
1216
+ useLoaderTextAsMsg,
1217
+ hasBatchQueued,
1218
+ expiredSessionID: sessionID,
1219
+ continueFromChatLogID: chatLogID,
1172
1220
  });
1173
1221
  } else if (response.resultCode === 500 && response.resultMessage) {
1174
1222
  setHistory(h => [
@@ -1183,22 +1231,15 @@ const MemoriWidget = ({
1183
1231
  },
1184
1232
  ]);
1185
1233
  } else {
1186
- console.warn('[SEND_MESSAGE]', response);
1187
1234
  return Promise.reject(response);
1188
1235
  }
1189
1236
  } catch (error) {
1190
- console.log('error', error);
1191
- console.error(error);
1192
1237
  gotError = true;
1238
+ logWidgetError('sendMessage failed', error);
1193
1239
 
1194
1240
  setTypingText(undefined);
1195
1241
  setMemoriTyping(false);
1196
1242
  }
1197
-
1198
- if (!hasBatchQueued) {
1199
- setTypingText(undefined);
1200
- setMemoriTyping(false);
1201
- }
1202
1243
  };
1203
1244
 
1204
1245
  /**
@@ -1211,12 +1252,6 @@ const MemoriWidget = ({
1211
1252
  msg?: string,
1212
1253
  avoidPushingMessage: boolean = false
1213
1254
  ) => {
1214
- // console.log('[TRANSLATE] Starting translation with params:', {
1215
- // state,
1216
- // userLang,
1217
- // msg
1218
- // });
1219
-
1220
1255
  const emission = state?.emission ?? currentDialogState?.emission;
1221
1256
 
1222
1257
  let translatedState = { ...state };
@@ -1229,7 +1264,6 @@ const MemoriWidget = ({
1229
1264
  !isMultilanguageEnabled ||
1230
1265
  avoidPushingMessage
1231
1266
  ) {
1232
- // console.log('[TRANSLATE] Skipping translation - using original emission');
1233
1267
  translatedState = { ...state, emission };
1234
1268
  if (emission) {
1235
1269
  translatedMsg = {
@@ -1251,12 +1285,10 @@ const MemoriWidget = ({
1251
1285
  }
1252
1286
  } else {
1253
1287
  try {
1254
- // console.log('[TRANSLATE] Translating emission');
1255
1288
  const t = await getTranslation(emission, userLang, language, baseUrl);
1256
1289
 
1257
1290
  // Handle hints translation if present
1258
1291
  if (state.hints && state.hints.length > 0) {
1259
- // console.log('[TRANSLATE] Translating hints');
1260
1292
  const translatedHints = await Promise.all(
1261
1293
  (state.hints ?? []).map(async hint => {
1262
1294
  const tHint = await getTranslation(
@@ -1288,7 +1320,6 @@ const MemoriWidget = ({
1288
1320
  }
1289
1321
 
1290
1322
  if (t.text.length > 0) {
1291
- // console.log('[TRANSLATE] Creating translated message');
1292
1323
  translatedMsg = {
1293
1324
  text: emission,
1294
1325
  translatedText: t.text,
@@ -1309,7 +1340,6 @@ const MemoriWidget = ({
1309
1340
  };
1310
1341
  }
1311
1342
  } catch (error) {
1312
- console.error('[TRANSLATE] Error during translation:', error);
1313
1343
  translatedState = { ...state, emission };
1314
1344
  translatedMsg = {
1315
1345
  text: emission,
@@ -1330,10 +1360,8 @@ const MemoriWidget = ({
1330
1360
  }
1331
1361
  }
1332
1362
 
1333
- // console.log('[TRANSLATE] Setting translated state and message');
1334
1363
  setCurrentDialogState(translatedState);
1335
1364
  if (!avoidPushingMessage && translatedMsg) {
1336
- // console.log('[TRANSLATE] Pushing translated message', translatedMsg);
1337
1365
  pushMessage(translatedMsg);
1338
1366
  }
1339
1367
 
@@ -1439,7 +1467,6 @@ const MemoriWidget = ({
1439
1467
  executableSnippets?.forEach(s => {
1440
1468
  try {
1441
1469
  setTimeout(() => {
1442
- console.log('snippet', s);
1443
1470
  // eslint-disable-next-line no-new-func
1444
1471
  new Function(s.content ?? '')();
1445
1472
 
@@ -1453,8 +1480,8 @@ const MemoriWidget = ({
1453
1480
  );
1454
1481
  }, 400);
1455
1482
  }, 1000);
1456
- } catch (e) {
1457
- console.warn(e);
1483
+ } catch {
1484
+ // ignore snippet execution errors
1458
1485
  }
1459
1486
  });
1460
1487
  };
@@ -1505,6 +1532,10 @@ const MemoriWidget = ({
1505
1532
  return;
1506
1533
  }
1507
1534
 
1535
+ if (!(await checkCredits({ notify: true }))) {
1536
+ return;
1537
+ }
1538
+
1508
1539
  setLoading(true);
1509
1540
 
1510
1541
  try {
@@ -1526,8 +1557,8 @@ const MemoriWidget = ({
1526
1557
  referral = (() => {
1527
1558
  return window.location.href;
1528
1559
  })();
1529
- } catch (err) {
1530
- console.debug(err);
1560
+ } catch {
1561
+ // ignore referral URL errors
1531
1562
  }
1532
1563
 
1533
1564
  // Initialize session with parameters
@@ -1538,12 +1569,7 @@ const MemoriWidget = ({
1538
1569
  pin: params.pin ?? personification?.pin,
1539
1570
  additionalInfo: {
1540
1571
  ...(params.additionalInfo || additionalInfo || {}),
1541
- loginToken:
1542
- userToken ??
1543
- loginToken ??
1544
- params.additionalInfo?.loginToken ??
1545
- additionalInfo?.loginToken ??
1546
- authToken,
1572
+ loginToken: resolveLoginToken(params.additionalInfo?.loginToken),
1547
1573
  language: (
1548
1574
  userLang ??
1549
1575
  memori.culture?.split('-')?.[0] ??
@@ -1589,7 +1615,7 @@ const MemoriWidget = ({
1589
1615
  setGotErrorInOpening(true);
1590
1616
  }
1591
1617
  // Handle authentication error
1592
- else if (session?.resultCode === 403) {
1618
+ else if (session?.resultCode === 403 && memori.privacyType !== 'PUBLIC') {
1593
1619
  setMemoriPwd(undefined);
1594
1620
  setAuthModalState('password');
1595
1621
  return session;
@@ -1614,11 +1640,10 @@ const MemoriWidget = ({
1614
1640
  duration: 0,
1615
1641
  })
1616
1642
  );
1617
- setGotErrorInOpening(true);
1618
1643
  return session;
1619
1644
  }
1620
1645
  } catch (err) {
1621
- console.error(err);
1646
+ logWidgetError('fetchSession failed', err);
1622
1647
  }
1623
1648
  };
1624
1649
 
@@ -1646,7 +1671,8 @@ const MemoriWidget = ({
1646
1671
  additionalInfoProp?: { [key: string]: string | undefined },
1647
1672
  continueFromChatLogID?: string,
1648
1673
  continueFromSessionID?: string,
1649
- isSessionExpired?: boolean
1674
+ isSessionExpired?: boolean,
1675
+ suppressHistoryUpdate?: boolean
1650
1676
  ) => {
1651
1677
  // Set loading state while reopening session
1652
1678
  setLoading(true);
@@ -1657,12 +1683,10 @@ const MemoriWidget = ({
1657
1683
  undefined
1658
1684
  );
1659
1685
  let userBirthDate = birthDate ?? storageBirthDate;
1660
- // console.log('[REOPEN_SESSION] Using birth date:', userBirthDate);
1661
1686
 
1662
1687
  try {
1663
1688
  // Show age verification if required and birth date not provided
1664
1689
  if (!userBirthDate && !!minAge) {
1665
- // console.log('[REOPEN_SESSION] Age verification required, showing modal');
1666
1690
  setShowAgeVerification(true);
1667
1691
  return;
1668
1692
  }
@@ -1676,24 +1700,26 @@ const MemoriWidget = ({
1676
1700
  !recoveryTokens &&
1677
1701
  !memoriTokens
1678
1702
  ) {
1679
- // console.log('[REOPEN_SESSION] Authentication required, showing modal');
1680
1703
  setAuthModalState('password');
1681
1704
  return;
1682
1705
  }
1683
1706
 
1707
+ if (!(await checkCredits({ notify: true }))) {
1708
+ setLoading(false);
1709
+ return null;
1710
+ }
1711
+
1684
1712
  // Get current URL as referral
1685
1713
  let referral;
1686
1714
  try {
1687
1715
  referral = (() => {
1688
1716
  return window.location.href;
1689
1717
  })();
1690
- // console.log('[REOPEN_SESSION] Got referral:', referral);
1691
- } catch (err) {
1692
- console.debug('[REOPEN_SESSION] Error getting referral:', err);
1718
+ } catch {
1719
+ // ignore referral URL errors
1693
1720
  }
1694
1721
 
1695
1722
  // Initialize session with provided parameters
1696
- // console.log('[REOPEN_SESSION] Initializing session...');
1697
1723
  const { sessionID, currentState, ...response } = await initSession({
1698
1724
  memoriID: memori.engineMemoriID ?? '',
1699
1725
  password: password || memoriPwd || memori.secretToken,
@@ -1712,12 +1738,7 @@ const MemoriWidget = ({
1712
1738
  birthDate: userBirthDate,
1713
1739
  additionalInfo: {
1714
1740
  ...(additionalInfoProp || additionalInfo || {}),
1715
- loginToken:
1716
- userToken ??
1717
- loginToken ??
1718
- additionalInfoProp?.loginToken ??
1719
- additionalInfo?.loginToken ??
1720
- authToken,
1741
+ loginToken: resolveLoginToken(additionalInfoProp?.loginToken),
1721
1742
  language: (
1722
1743
  userLang ??
1723
1744
  memori.culture?.split('-')?.[0] ??
@@ -1730,27 +1751,34 @@ const MemoriWidget = ({
1730
1751
 
1731
1752
  // Handle successful session initialization
1732
1753
  if (sessionID && currentState && response.resultCode === 0) {
1733
- console.log(
1734
- '[REOPEN_SESSION] Session initialized successfully:',
1735
- sessionID
1736
- );
1737
1754
  setSessionId(sessionID);
1738
1755
 
1739
1756
  // Update dialog state and history if requested
1740
1757
  if (updateDialogState) {
1741
- // console.log('[REOPEN_SESSION] Updating dialog state');
1742
1758
  setCurrentDialogState(currentState);
1743
1759
 
1744
- if (currentState.emission) {
1745
- console.log(
1746
- '[REOPEN_SESSION] Processing emission:',
1747
- currentState.emission
1748
- );
1760
+ const sessionExpiredStatus =
1761
+ isSessionExpired && history.length > 1
1762
+ ? t('sessionExpiredReopening')
1763
+ : null;
1764
+
1765
+ if (sessionExpiredStatus && suppressHistoryUpdate) {
1766
+ pushMessage({
1767
+ text: '',
1768
+ emitter: 'system',
1769
+ fromUser: false,
1770
+ initial: sessionExpiredStatus as any,
1771
+ contextVars: {},
1772
+ date: new Date().toISOString(),
1773
+ });
1774
+ }
1775
+
1776
+ if (currentState.emission && !suppressHistoryUpdate) {
1749
1777
  // Determine initial status message based on context
1750
1778
  // Show status message only if session expired and there's existing history
1751
1779
  const initialStatus =
1752
- isSessionExpired && history.length > 1
1753
- ? 'Session Expired, reopening session'
1780
+ sessionExpiredStatus
1781
+ ? sessionExpiredStatus
1754
1782
  : history.length <= 1
1755
1783
  ? true
1756
1784
  : undefined;
@@ -1816,8 +1844,10 @@ const MemoriWidget = ({
1816
1844
  setGotErrorInOpening(true);
1817
1845
  }
1818
1846
  // Handle authentication error
1819
- else if (response?.resultCode === 403) {
1820
- console.error('[REOPEN_SESSION] Authentication error');
1847
+ else if (
1848
+ response?.resultCode === 403 &&
1849
+ memori.privacyType !== 'PUBLIC'
1850
+ ) {
1821
1851
  setMemoriPwd(undefined);
1822
1852
  setAuthModalState('password');
1823
1853
  }
@@ -1833,7 +1863,7 @@ const MemoriWidget = ({
1833
1863
  setGotErrorInOpening(true);
1834
1864
  }
1835
1865
  } catch (err) {
1836
- console.error('[REOPEN_SESSION] Caught error:', err);
1866
+ logWidgetError('reopenSession failed', err);
1837
1867
  }
1838
1868
  // Reset loading state
1839
1869
  setLoading(false);
@@ -1841,6 +1871,99 @@ const MemoriWidget = ({
1841
1871
  return null;
1842
1872
  };
1843
1873
 
1874
+ const retryAfterExpiredSessionRef = useRef<
1875
+ (params: EnterTextRetryParams) => ReturnType<typeof reopenSession>
1876
+ >(() => Promise.resolve(null));
1877
+
1878
+ retryAfterExpiredSessionRef.current = (params: EnterTextRetryParams) => {
1879
+ const {
1880
+ text,
1881
+ media,
1882
+ translate = true,
1883
+ translatedText,
1884
+ hidden = false,
1885
+ typingText,
1886
+ useLoaderTextAsMsg = false,
1887
+ hasBatchQueued = false,
1888
+ expiredSessionID,
1889
+ continueFromChatLogID: continueFromChatLogIDParam,
1890
+ } = params;
1891
+
1892
+ const continueFromSessionID = expiredSessionID ?? sessionId;
1893
+ const continueFromChatLogID = continueFromChatLogIDParam ?? chatLogID;
1894
+
1895
+ const reopenAfterExpiry = () =>
1896
+ reopenSession(
1897
+ true,
1898
+ memoriPwd || memori.secretToken,
1899
+ memoriTokens,
1900
+ undefined,
1901
+ undefined,
1902
+ {
1903
+ LANG: userLang,
1904
+ PATHNAME: window.location.pathname,
1905
+ ROUTE: window.location.pathname?.split('/')?.pop() || '',
1906
+ ...(initialContextVars || {}),
1907
+ },
1908
+ initialQuestion,
1909
+ undefined,
1910
+ undefined,
1911
+ continueFromChatLogID,
1912
+ continueFromSessionID,
1913
+ true,
1914
+ true
1915
+ );
1916
+
1917
+ const scheduleRetry = (newSessionID: string) => {
1918
+ setTimeout(() => {
1919
+ sendMessage(
1920
+ text!,
1921
+ media,
1922
+ newSessionID,
1923
+ translate,
1924
+ translatedText,
1925
+ hidden,
1926
+ typingText,
1927
+ useLoaderTextAsMsg,
1928
+ hasBatchQueued,
1929
+ true
1930
+ );
1931
+ }, 500);
1932
+ };
1933
+
1934
+ const handleReopenFailure = (state: Awaited<ReturnType<typeof reopenSession>>) => {
1935
+ setMemoriTyping(false);
1936
+ setTypingText(undefined);
1937
+ // `null` = explicit failure; `undefined` = auth/age modal opened (message kept in history)
1938
+ if (state === null && text && !hidden) {
1939
+ add(
1940
+ createAlertOptions({
1941
+ description: t('errors.SESSION_EXPIRED'),
1942
+ severity: 'error',
1943
+ })
1944
+ );
1945
+ }
1946
+ };
1947
+
1948
+ if (!text) {
1949
+ return reopenAfterExpiry().then(state => {
1950
+ if (!state?.sessionID) {
1951
+ handleReopenFailure(state);
1952
+ }
1953
+ return state;
1954
+ });
1955
+ }
1956
+
1957
+ return reopenAfterExpiry().then(state => {
1958
+ if (state?.sessionID) {
1959
+ scheduleRetry(state.sessionID);
1960
+ } else {
1961
+ handleReopenFailure(state);
1962
+ }
1963
+ return state;
1964
+ });
1965
+ };
1966
+
1844
1967
  const changeTag = async (
1845
1968
  memoriId: string,
1846
1969
  sessionId: string,
@@ -1848,7 +1971,6 @@ const MemoriWidget = ({
1848
1971
  pin?: string
1849
1972
  ) => {
1850
1973
  if (!memoriId || !sessionId) {
1851
- console.error('CHANGETAG/Session not found');
1852
1974
  return Promise.reject('Session not found');
1853
1975
  }
1854
1976
 
@@ -1888,7 +2010,6 @@ const MemoriWidget = ({
1888
2010
  };
1889
2011
  }
1890
2012
  } else if ([400, 401, 403, 404, 500].includes(resultCode)) {
1891
- console.warn('[APPCONTEXT/CHANGETAG]', resultCode);
1892
2013
  let storageBirthDate = getLocalConfig<string | undefined>(
1893
2014
  'birthDate',
1894
2015
  undefined
@@ -1899,8 +2020,8 @@ const MemoriWidget = ({
1899
2020
  referral = (() => {
1900
2021
  return window.location.href;
1901
2022
  })();
1902
- } catch (err) {
1903
- console.debug(err);
2023
+ } catch {
2024
+ // ignore referral URL errors
1904
2025
  }
1905
2026
 
1906
2027
  fetchSession({
@@ -1918,11 +2039,7 @@ const MemoriWidget = ({
1918
2039
  birthDate: birthDate || storageBirthDate || undefined,
1919
2040
  additionalInfo: {
1920
2041
  ...(additionalInfo || {}),
1921
- loginToken:
1922
- userToken ??
1923
- loginToken ??
1924
- additionalInfo?.loginToken ??
1925
- authToken,
2042
+ loginToken: resolveLoginToken(),
1926
2043
  language: (
1927
2044
  userLang ??
1928
2045
  memori.culture?.split('-')?.[0] ??
@@ -1942,7 +2059,6 @@ const MemoriWidget = ({
1942
2059
  }
1943
2060
  } catch (_e) {
1944
2061
  let err = _e as Error;
1945
- console.warn('[APPCONTEXT/CHANGETAG]', err);
1946
2062
  return Promise.reject(err);
1947
2063
  }
1948
2064
 
@@ -1969,6 +2085,7 @@ const MemoriWidget = ({
1969
2085
  return () => {
1970
2086
  setHasUserActivatedSpeak(false);
1971
2087
  setClickedStart(false);
2088
+ sessionStartingRef.current = false;
1972
2089
  clearInteractionTimeout();
1973
2090
  timeoutRef.current = undefined;
1974
2091
  };
@@ -1981,8 +2098,6 @@ const MemoriWidget = ({
1981
2098
  const [requestedListening, setRequestedListening] = useState(false);
1982
2099
  const startListeningRef = useRef<(() => Promise<void>) | null>(null);
1983
2100
 
1984
- // console.log('tenantID', tenantID);
1985
-
1986
2101
  // Define TTS configuration
1987
2102
  const ttsConfig = useMemo(
1988
2103
  () => ({
@@ -2037,7 +2152,6 @@ const MemoriWidget = ({
2037
2152
  'muteSpeaker',
2038
2153
  !defaultEnableAudio
2039
2154
  );
2040
- console.log('[MemoriWidget] shouldPlayAudio', currentSpeakerMuted);
2041
2155
  return (
2042
2156
  text &&
2043
2157
  text.trim() &&
@@ -2049,7 +2163,6 @@ const MemoriWidget = ({
2049
2163
 
2050
2164
  // Create a single, centralized function to process and send messages
2051
2165
  const processSpeechAndSendMessage = (text: string) => {
2052
- // console.log('processSpeechAndSendMessage', text);
2053
2166
  // Skip if already processing or no text
2054
2167
  if (!text || text.trim().length === 0) {
2055
2168
  return;
@@ -2058,17 +2171,15 @@ const MemoriWidget = ({
2058
2171
  try {
2059
2172
  // Process the text
2060
2173
  const message = stripDuplicates(text);
2061
- console.debug('Processing speech message:', message);
2062
2174
 
2063
2175
  if (message.length > 0) {
2064
2176
  setUserMessage('');
2065
2177
 
2066
2178
  // Send the message
2067
- console.debug('Sending message:', message);
2068
2179
  sendMessage(message);
2069
2180
  }
2070
- } catch (error) {
2071
- console.error('Error in processSpeechAndSendMessage:', error);
2181
+ } catch {
2182
+ // ignore speech processing errors
2072
2183
  }
2073
2184
  };
2074
2185
 
@@ -2148,8 +2259,7 @@ const MemoriWidget = ({
2148
2259
  }
2149
2260
 
2150
2261
  return translatedState;
2151
- } catch (error) {
2152
- console.error('Error in translateAndSpeak:', error);
2262
+ } catch {
2153
2263
  // Still update activation state even if there's an error
2154
2264
  if (!hasUserActivatedSpeak) {
2155
2265
  setHasUserActivatedSpeak(true);
@@ -2166,6 +2276,277 @@ const MemoriWidget = ({
2166
2276
  ]
2167
2277
  );
2168
2278
 
2279
+ const processEnterTextDialogResponse = useCallback(
2280
+ (event: NatsDialogResponseEvent, pending: PendingEnterText) => {
2281
+ const {
2282
+ msg,
2283
+ typingText: pendingTypingText,
2284
+ useLoaderTextAsMsg,
2285
+ } = pending;
2286
+ const currentState = event.currentState;
2287
+
2288
+ if (event.resultCode !== 0 || !currentState) {
2289
+ if (isSessionExpiredNatsResponse(event) && pending.text) {
2290
+ setMemoriTyping(false);
2291
+ setTypingText(undefined);
2292
+ retryAfterExpiredSessionRef.current({
2293
+ text: pending.text,
2294
+ media: pending.media,
2295
+ translate: pending.translate,
2296
+ translatedText: pending.translatedText,
2297
+ hidden: pending.hidden,
2298
+ typingText: pending.typingText,
2299
+ useLoaderTextAsMsg: pending.useLoaderTextAsMsg,
2300
+ hasBatchQueued: pending.hasBatchQueued,
2301
+ expiredSessionID: sessionId,
2302
+ continueFromChatLogID: chatLogID,
2303
+ });
2304
+ return;
2305
+ }
2306
+
2307
+ if (event.resultCode === 500 && event.resultMessage) {
2308
+ setHistory(h => [
2309
+ ...h,
2310
+ {
2311
+ text: 'Error: ' + event.resultMessage,
2312
+ emitter: 'system',
2313
+ fromUser: false,
2314
+ initial: false,
2315
+ contextVars: {},
2316
+ date: new Date().toISOString(),
2317
+ },
2318
+ ]);
2319
+ }
2320
+ return;
2321
+ }
2322
+
2323
+ if (!msg) {
2324
+ return;
2325
+ }
2326
+
2327
+ setChatLogID(undefined);
2328
+ const emission =
2329
+ useLoaderTextAsMsg && pendingTypingText
2330
+ ? pendingTypingText
2331
+ : currentState.emission ?? currentDialogState?.emission;
2332
+
2333
+ if (
2334
+ userLang.toLowerCase() !== language.toLowerCase() &&
2335
+ emission &&
2336
+ isMultilanguageEnabled
2337
+ ) {
2338
+ currentState.emission = emission;
2339
+
2340
+ translateDialogState(currentState, userLang, msg).then(ts => {
2341
+ const text = ts.translatedEmission || ts.emission;
2342
+ if (text && shouldPlayAudio(text)) {
2343
+ handleSpeak(text);
2344
+ }
2345
+ });
2346
+ } else {
2347
+ setCurrentDialogState({
2348
+ ...currentState,
2349
+ emission,
2350
+ });
2351
+
2352
+ if (emission) {
2353
+ pushMessage({
2354
+ text: emission,
2355
+ emitter: currentState.emitter,
2356
+ media: currentState.emittedMedia ?? currentState.media,
2357
+ llmUsage: (currentState as any).llmUsage,
2358
+ fromUser: false,
2359
+ questionAnswered: msg,
2360
+ generatedByAI: !!currentState.completion,
2361
+ contextVars: currentState.contextVars,
2362
+ date: currentState.currentDate,
2363
+ placeName: currentState.currentPlaceName,
2364
+ placeLatitude: currentState.currentLatitude,
2365
+ placeLongitude: currentState.currentLongitude,
2366
+ placeUncertaintyKm: currentState.currentUncertaintyKm,
2367
+ tag: currentState.currentTag,
2368
+ memoryTags: currentState.memoryTags,
2369
+ } as any);
2370
+ if (emission && shouldPlayAudio(emission)) {
2371
+ handleSpeak(emission);
2372
+ }
2373
+ }
2374
+ }
2375
+ },
2376
+ [
2377
+ userLang,
2378
+ language,
2379
+ isMultilanguageEnabled,
2380
+ currentDialogState?.emission,
2381
+ translateDialogState,
2382
+ handleSpeak,
2383
+ shouldPlayAudio,
2384
+ ]
2385
+ );
2386
+
2387
+ const clearEnterTextPending = useCallback(
2388
+ (correlationID: string, pending: PendingEnterText) => {
2389
+ if (pending.waitForResponse?.timeoutId) {
2390
+ clearTimeout(pending.waitForResponse.timeoutId);
2391
+ }
2392
+ pendingEnterTextRef.current.delete(correlationID);
2393
+ },
2394
+ []
2395
+ );
2396
+
2397
+ const deliverEnterTextNatsError = useCallback(
2398
+ (event: NatsErrorEvent) => {
2399
+ const correlationID = event.correlationID;
2400
+ let pending: PendingEnterText | undefined;
2401
+
2402
+ if (correlationID) {
2403
+ pending = pendingEnterTextRef.current.get(correlationID);
2404
+ if (pending) {
2405
+ clearEnterTextPending(correlationID, pending);
2406
+ pending.waitForResponse?.reject(
2407
+ new Error(
2408
+ event.errorMessage ?? String(event.errorCode ?? 'NATS error')
2409
+ )
2410
+ );
2411
+ }
2412
+ }
2413
+
2414
+ if (isSessionExpiredNatsError(event) && pending?.text) {
2415
+ setMemoriTyping(false);
2416
+ setTypingText(undefined);
2417
+ retryAfterExpiredSessionRef.current({
2418
+ text: pending.text,
2419
+ media: pending.media,
2420
+ translate: pending.translate,
2421
+ translatedText: pending.translatedText,
2422
+ hidden: pending.hidden,
2423
+ typingText: pending.typingText,
2424
+ useLoaderTextAsMsg: pending.useLoaderTextAsMsg,
2425
+ hasBatchQueued: pending.hasBatchQueued,
2426
+ expiredSessionID: sessionId,
2427
+ continueFromChatLogID: chatLogID,
2428
+ });
2429
+ return;
2430
+ }
2431
+
2432
+ const errorText = event.errorMessage
2433
+ ? `Error: ${event.errorMessage}`
2434
+ : event.errorCode
2435
+ ? `Error: ${event.errorCode}`
2436
+ : 'Error: An unexpected error occurred';
2437
+
2438
+ pushMessage({
2439
+ text: errorText,
2440
+ emitter: 'system',
2441
+ fromUser: false,
2442
+ initial: false,
2443
+ contextVars: {},
2444
+ date: new Date().toISOString(),
2445
+ });
2446
+
2447
+ setMemoriTyping(false);
2448
+ setTypingText(undefined);
2449
+ },
2450
+ [clearEnterTextPending]
2451
+ );
2452
+
2453
+ const deliverEnterTextNatsResponse = useCallback(
2454
+ (correlationID: string, event: NatsDialogResponseEvent) => {
2455
+ const pending = pendingEnterTextRef.current.get(correlationID);
2456
+ if (!pending) {
2457
+ bufferedNatsResponsesRef.current.set(correlationID, event);
2458
+ return;
2459
+ }
2460
+
2461
+ clearEnterTextPending(correlationID, pending);
2462
+
2463
+ if (pending.waitForResponse) {
2464
+ pending.waitForResponse.resolve(event);
2465
+ setMemoriTyping(false);
2466
+ setTypingText(undefined);
2467
+ return;
2468
+ }
2469
+
2470
+ processEnterTextDialogResponse(event, pending);
2471
+
2472
+ if (!pending.hasBatchQueued) {
2473
+ setMemoriTyping(false);
2474
+ setTypingText(undefined);
2475
+ }
2476
+ },
2477
+ [processEnterTextDialogResponse, clearEnterTextPending]
2478
+ );
2479
+
2480
+ const registerPendingEnterText = useCallback(
2481
+ (correlationID: string, pending: PendingEnterText) => {
2482
+ const buffered = bufferedNatsResponsesRef.current.get(correlationID);
2483
+ if (buffered) {
2484
+ bufferedNatsResponsesRef.current.delete(correlationID);
2485
+ pendingEnterTextRef.current.set(correlationID, pending);
2486
+ deliverEnterTextNatsResponse(correlationID, buffered);
2487
+ return;
2488
+ }
2489
+
2490
+ pendingEnterTextRef.current.set(correlationID, pending);
2491
+ },
2492
+ [deliverEnterTextNatsResponse]
2493
+ );
2494
+
2495
+ const waitForEnterTextNatsResponse = useCallback(
2496
+ (correlationID: string, timeoutMs = 120000) =>
2497
+ new Promise<NatsDialogResponseEvent>((resolve, reject) => {
2498
+ const timeoutId = setTimeout(() => {
2499
+ const current = pendingEnterTextRef.current.get(correlationID);
2500
+ if (current) {
2501
+ clearEnterTextPending(correlationID, current);
2502
+ }
2503
+ logWidgetError('NATS timeout', { correlationID, timeoutMs });
2504
+ reject(new Error('NATS enter-text response timeout'));
2505
+ }, timeoutMs);
2506
+
2507
+ registerPendingEnterText(correlationID, {
2508
+ waitForResponse: {
2509
+ resolve: event => {
2510
+ clearTimeout(timeoutId);
2511
+ resolve(event);
2512
+ },
2513
+ reject: error => {
2514
+ clearTimeout(timeoutId);
2515
+ reject(error);
2516
+ },
2517
+ timeoutId,
2518
+ },
2519
+ });
2520
+ }),
2521
+ [registerPendingEnterText, clearEnterTextPending]
2522
+ );
2523
+
2524
+ // NATS subscription: receives progress updates and the async enter-text response.
2525
+ useNats({
2526
+ baseUrl,
2527
+ sessionId,
2528
+ onProgress: useCallback((event: NatsProgressEvent) => {
2529
+ if (event.message) {
2530
+ setTypingText(event.message);
2531
+ }
2532
+ }, []),
2533
+ onDialogResponse: useCallback(
2534
+ (event: NatsDialogResponseEvent) => {
2535
+ const correlationID = event.correlationID;
2536
+ if (!correlationID) {
2537
+ logWidgetError('NATS dialog response missing correlationID', event);
2538
+ setMemoriTyping(false);
2539
+ setTypingText(undefined);
2540
+ return;
2541
+ }
2542
+
2543
+ deliverEnterTextNatsResponse(correlationID, event);
2544
+ },
2545
+ [deliverEnterTextNatsResponse]
2546
+ ),
2547
+ onError: deliverEnterTextNatsError,
2548
+ });
2549
+
2169
2550
  const focusChatInput = () => {
2170
2551
  let textarea = document.querySelector(
2171
2552
  '#chat-fieldset textarea'
@@ -2194,8 +2575,8 @@ const MemoriWidget = ({
2194
2575
  setClickedStart(false);
2195
2576
  timeoutRef.current = undefined;
2196
2577
  ttsStop();
2197
- } catch (e) {
2198
- // console.log('Error: resetUIEffects', e);
2578
+ } catch {
2579
+ // ignore reset errors
2199
2580
  }
2200
2581
  };
2201
2582
  useEffect(() => {
@@ -2451,12 +2832,6 @@ const MemoriWidget = ({
2451
2832
  chatLog?: ChatLog,
2452
2833
  targetSessionID?: string
2453
2834
  ) => {
2454
- // console.log('[onClickStart] Starting with params:', {
2455
- // session,
2456
- // initialSessionExpired,
2457
- // chatLog
2458
- // });
2459
-
2460
2835
  const sessionID = chatLog ? undefined : session?.sessionID || sessionId;
2461
2836
  const dialogState = chatLog
2462
2837
  ? undefined
@@ -2485,10 +2860,17 @@ const MemoriWidget = ({
2485
2860
  return;
2486
2861
  }
2487
2862
 
2863
+ if (!(await checkCredits({ notify: true }))) {
2864
+ setClickedStart(false);
2865
+ setLoading(false);
2866
+ return;
2867
+ }
2868
+
2488
2869
  // Handle age verification
2489
2870
  if (!sessionID && !!minAge && !birth) {
2490
2871
  setShowAgeVerification(true);
2491
2872
  setClickedStart(false);
2873
+ return;
2492
2874
  }
2493
2875
  // Handle authentication
2494
2876
  else if (
@@ -2505,42 +2887,43 @@ const MemoriWidget = ({
2505
2887
  }
2506
2888
  // Create new session if needed
2507
2889
  else if (!sessionID || initialSessionExpired) {
2508
- setClickedStart(false);
2509
2890
  setGotErrorInOpening(false);
2510
- const session = await fetchSession({
2511
- memoriID: memori.engineMemoriID!,
2512
- password: secret || memoriPwd || memori.secretToken,
2513
- tag: personification?.tag,
2514
- pin: personification?.pin,
2515
- continueFromChatLogID: chatLog?.chatLogID,
2516
- initialContextVars: {
2517
- LANG: userLang,
2518
- PATHNAME: window.location.pathname?.toUpperCase(),
2519
- ROUTE:
2520
- window.location.pathname?.split('/')?.pop()?.toUpperCase() || '',
2521
- ...((!chatLog
2522
- ? initialContextVars
2523
- : chatLog.lines[chatLog.lines.length - 1].contextVars) || {}),
2524
- },
2525
- initialQuestion: chatLog ? undefined : initialQuestion,
2526
- birthDate: birth,
2527
- additionalInfo: {
2528
- ...(additionalInfo || {}),
2529
- loginToken:
2530
- userToken ??
2531
- loginToken ??
2532
- additionalInfo?.loginToken ??
2533
- authToken,
2534
- language: (
2535
- userLang ??
2536
- memori.culture?.split('-')?.[0] ??
2537
- 'IT'
2538
- ).toLowerCase(),
2539
- timeZoneOffset: new Date().getTimezoneOffset().toString(),
2540
- },
2541
- });
2891
+ if (sessionStartingRef.current) {
2892
+ return;
2893
+ }
2894
+ sessionStartingRef.current = true;
2895
+ try {
2896
+ const session = await fetchSession({
2897
+ memoriID: memori.engineMemoriID!,
2898
+ password: secret || memoriPwd || memori.secretToken,
2899
+ tag: personification?.tag,
2900
+ pin: personification?.pin,
2901
+ continueFromChatLogID: chatLog?.chatLogID,
2902
+ initialContextVars: {
2903
+ LANG: userLang,
2904
+ PATHNAME: window.location.pathname?.toUpperCase(),
2905
+ ROUTE:
2906
+ window.location.pathname?.split('/')?.pop()?.toUpperCase() ||
2907
+ '',
2908
+ ...((!chatLog
2909
+ ? initialContextVars
2910
+ : chatLog.lines[chatLog.lines.length - 1].contextVars) || {}),
2911
+ },
2912
+ initialQuestion: chatLog ? undefined : initialQuestion,
2913
+ birthDate: birth,
2914
+ additionalInfo: {
2915
+ ...(additionalInfo || {}),
2916
+ loginToken: resolveLoginToken(),
2917
+ language: (
2918
+ userLang ??
2919
+ memori.culture?.split('-')?.[0] ??
2920
+ 'IT'
2921
+ ).toLowerCase(),
2922
+ timeZoneOffset: new Date().getTimezoneOffset().toString(),
2923
+ },
2924
+ });
2542
2925
 
2543
- if (session?.dialogState) {
2926
+ if (session?.dialogState) {
2544
2927
  // reset history
2545
2928
  if (!chatLog) {
2546
2929
  setHistory([]);
@@ -2549,6 +2932,7 @@ const MemoriWidget = ({
2549
2932
  await translateAndSpeak(session.dialogState, userLang);
2550
2933
  // No need for additional handleSpeak call since translateAndSpeak already handles it
2551
2934
  setHasUserActivatedSpeak(true);
2935
+ setClickedStart(false);
2552
2936
  } else {
2553
2937
  const messages = chatLog.lines.map(
2554
2938
  (l, i) =>
@@ -2596,8 +2980,8 @@ const MemoriWidget = ({
2596
2980
  };
2597
2981
  })
2598
2982
  );
2599
- } catch (e) {
2600
- console.error('[onClickStart] Error translating messages:', e);
2983
+ } catch {
2984
+ // ignore translation errors
2601
2985
  }
2602
2986
  }
2603
2987
 
@@ -2610,12 +2994,18 @@ const MemoriWidget = ({
2610
2994
  true
2611
2995
  ).finally(() => {
2612
2996
  setHasUserActivatedSpeak(true);
2997
+ setClickedStart(false);
2613
2998
  });
2614
2999
  }
2615
- } else if (session?.resultCode === 0) {
2616
- await onClickStart((session as any) || undefined);
2617
- } else {
2618
- setLoading(false);
3000
+ } else if (session?.resultCode === 0) {
3001
+ sessionStartingRef.current = false;
3002
+ await onClickStart((session as any) || undefined);
3003
+ } else {
3004
+ setLoading(false);
3005
+ setClickedStart(false);
3006
+ }
3007
+ } finally {
3008
+ sessionStartingRef.current = false;
2619
3009
  }
2620
3010
 
2621
3011
  return;
@@ -2628,9 +3018,7 @@ const MemoriWidget = ({
2628
3018
 
2629
3019
  if (response.resultCode !== 0 || !currentState) {
2630
3020
  const { chatLogs } = await getSessionChatLogs(sessionID!, sessionID!);
2631
- setGotErrorInOpening(true);
2632
3021
  setSessionId(undefined);
2633
- setClickedStart(false);
2634
3022
  await onClickStart(undefined, true, chatLogs?.[0]);
2635
3023
  return;
2636
3024
  }
@@ -2656,11 +3044,11 @@ const MemoriWidget = ({
2656
3044
 
2657
3045
  if (session && session.resultCode === 0) {
2658
3046
  await translateAndSpeak(session.currentState, userLang);
3047
+ setClickedStart(false);
2659
3048
  } else {
2660
3049
  throw new Error('No session');
2661
3050
  }
2662
- } catch (e) {
2663
- console.error('[onClickStart] Error changing tag:', e);
3051
+ } catch {
2664
3052
  reopenSession(
2665
3053
  true,
2666
3054
  memori?.secretToken,
@@ -2679,6 +3067,7 @@ const MemoriWidget = ({
2679
3067
  birth
2680
3068
  ).then(() => {
2681
3069
  setHasUserActivatedSpeak(true);
3070
+ setClickedStart(false);
2682
3071
  });
2683
3072
  }
2684
3073
  }
@@ -2701,6 +3090,7 @@ const MemoriWidget = ({
2701
3090
 
2702
3091
  if (session && session.resultCode === 0) {
2703
3092
  await translateAndSpeak(session.currentState, userLang);
3093
+ setClickedStart(false);
2704
3094
  } else {
2705
3095
  throw new Error('No session');
2706
3096
  }
@@ -2723,6 +3113,7 @@ const MemoriWidget = ({
2723
3113
  birth
2724
3114
  ).then(() => {
2725
3115
  setHasUserActivatedSpeak(true);
3116
+ setClickedStart(false);
2726
3117
  });
2727
3118
  }
2728
3119
  }
@@ -2769,14 +3160,14 @@ const MemoriWidget = ({
2769
3160
  ).text,
2770
3161
  }))
2771
3162
  );
2772
- } catch (e) {
2773
- console.error('[onClickStart] Error translating messages:', e);
3163
+ } catch {
3164
+ // ignore translation errors
2774
3165
  }
2775
3166
  }
2776
3167
 
2777
3168
  setHistory(translatedMessages);
2778
- } catch (e) {
2779
- console.error('[onClickStart] Error retrieving chat logs:', e);
3169
+ } catch {
3170
+ // ignore chat log retrieval errors
2780
3171
  }
2781
3172
 
2782
3173
  if (
@@ -2785,6 +3176,7 @@ const MemoriWidget = ({
2785
3176
  ) {
2786
3177
  // we have a history, don't push message
2787
3178
  setHasUserActivatedSpeak(true);
3179
+ setClickedStart(false);
2788
3180
  await translateAndSpeak(
2789
3181
  currentState,
2790
3182
  userLang,
@@ -2794,16 +3186,13 @@ const MemoriWidget = ({
2794
3186
  !!translatedMessages?.length
2795
3187
  );
2796
3188
  } else {
2797
- console.log('[onClickStart] Starting with initial question');
2798
3189
  // remove default initial message
2799
3190
  translatedMessages = [];
2800
3191
  setHistory([]);
2801
3192
 
2802
- setMemoriTyping(true);
2803
-
2804
3193
  // we have no chat history, we start by initial question
2805
3194
  const placeSpec = getPlaceSpecForEnterText(position);
2806
- const response = await postTextEnteredEvent({
3195
+ const response = await postEnterTextAsync({
2807
3196
  sessionId: sessionID!,
2808
3197
  text: initialQuestion,
2809
3198
  ...(memori.needsDateTime && {
@@ -2811,8 +3200,7 @@ const MemoriWidget = ({
2811
3200
  }),
2812
3201
  ...(placeSpec !== undefined && { place: placeSpec }),
2813
3202
  });
2814
-
2815
- // Handle 500 error from TextEnteredEvent
3203
+ // Handle 500 error from EnterTextAsync
2816
3204
  if (response.resultCode === 500 && response.resultMessage) {
2817
3205
  setHistory(h => [
2818
3206
  ...h,
@@ -2825,16 +3213,33 @@ const MemoriWidget = ({
2825
3213
  date: new Date().toISOString(),
2826
3214
  },
2827
3215
  ]);
2828
- setMemoriTyping(false);
2829
3216
  return;
2830
3217
  }
2831
3218
 
2832
- await translateAndSpeak(
2833
- response.currentState ?? currentState,
2834
- userLang,
2835
- undefined,
2836
- false
2837
- );
3219
+ const onClickStartCorrelationID = readCorrelationID(response);
3220
+ if (response.resultCode === 0 && onClickStartCorrelationID) {
3221
+ setMemoriTyping(true);
3222
+ try {
3223
+ const natsEvent = await waitForEnterTextNatsResponse(
3224
+ onClickStartCorrelationID
3225
+ );
3226
+ if (natsEvent.resultCode === 0 && natsEvent.currentState) {
3227
+ await translateAndSpeak(
3228
+ natsEvent.currentState,
3229
+ userLang,
3230
+ undefined,
3231
+ false
3232
+ );
3233
+ setClickedStart(false);
3234
+ }
3235
+ } catch (err) {
3236
+ logWidgetError('onClickStart NATS wait failed', err);
3237
+ setMemoriTyping(false);
3238
+ setTypingText(undefined);
3239
+ }
3240
+ } else if (response.resultCode === 0) {
3241
+ logWidgetError('onClickStart enter-text missing correlationID', response);
3242
+ }
2838
3243
  }
2839
3244
  }
2840
3245
  }
@@ -2845,6 +3250,7 @@ const MemoriWidget = ({
2845
3250
 
2846
3251
  // everything is fine, just translate dialog state and activate chat
2847
3252
  await translateAndSpeak(dialogState!, userLang);
3253
+ setClickedStart(false);
2848
3254
  }
2849
3255
  },
2850
3256
  [memoriPwd, memori, memoriTokens, birthDate, sessionId, userLang, position]
@@ -2861,10 +3267,24 @@ const MemoriWidget = ({
2861
3267
  );
2862
3268
 
2863
3269
  useEffect(() => {
2864
- if (!clickedStart && autoStart && selectedLayout !== 'HIDDEN_CHAT') {
3270
+ if (
3271
+ !clickedStart &&
3272
+ !sessionStartingRef.current &&
3273
+ !sessionId &&
3274
+ autoStart &&
3275
+ selectedLayout !== 'HIDDEN_CHAT' &&
3276
+ (!needsCredits || hasEnoughCredits)
3277
+ ) {
2865
3278
  onClickStart();
2866
3279
  }
2867
- }, [clickedStart, autoStart, selectedLayout]);
3280
+ }, [
3281
+ clickedStart,
3282
+ autoStart,
3283
+ selectedLayout,
3284
+ sessionId,
3285
+ needsCredits,
3286
+ hasEnoughCredits,
3287
+ ]);
2868
3288
 
2869
3289
  useEffect(() => {
2870
3290
  const targetNode =
@@ -2875,29 +3295,31 @@ const MemoriWidget = ({
2875
3295
  return;
2876
3296
  }
2877
3297
 
3298
+ const propsToken = additionalInfo?.loginToken ?? authToken;
3299
+ const initialDomToken = targetNode.getAttribute('authtoken') || undefined;
3300
+ if (initialDomToken && !propsToken) {
3301
+ setLoginToken(initialDomToken);
3302
+ userTokenRef.current = initialDomToken;
3303
+ }
3304
+
3305
+ const applyAuthTokenFromElement = (element: Element) => {
3306
+ const clientNode =
3307
+ element.nodeName === 'MEMORI-CLIENT'
3308
+ ? element
3309
+ : element.closest('memori-client') ?? element.parentElement;
3310
+ const token = clientNode?.getAttribute('authtoken') || undefined;
3311
+ setLoginToken(token);
3312
+ userTokenRef.current = token;
3313
+ };
3314
+
2878
3315
  const config = { attributes: true, childList: false, subtree: false };
2879
- const callback: MutationCallback = (mutationList, _observer) => {
3316
+ const callback: MutationCallback = mutationList => {
2880
3317
  for (const mutation of mutationList) {
2881
3318
  if (
2882
3319
  mutation.type === 'attributes' &&
2883
3320
  mutation.attributeName?.toLowerCase() === 'authtoken'
2884
3321
  ) {
2885
- if (mutation.target.nodeName === 'MEMORI-CLIENT') {
2886
- setLoginToken(
2887
- // @ts-ignore
2888
- mutation.target.getAttribute('authtoken') || undefined
2889
- );
2890
- // @ts-ignore
2891
- userToken = mutation.target.getAttribute('authtoken') || undefined;
2892
- } else {
2893
- // @ts-ignore
2894
- setLoginToken(
2895
- mutation.target?.parentElement?.getAttribute('authtoken') ||
2896
- undefined
2897
- );
2898
- // @ts-ignore
2899
- userToken = mutation.target.getAttribute('authtoken') || undefined;
2900
- }
3322
+ applyAuthTokenFromElement(mutation.target as Element);
2901
3323
  }
2902
3324
  }
2903
3325
  };
@@ -2907,7 +3329,7 @@ const MemoriWidget = ({
2907
3329
  return () => {
2908
3330
  observer.disconnect();
2909
3331
  };
2910
- }, []);
3332
+ }, [memori.name, memori.memoriID, additionalInfo?.loginToken, authToken]);
2911
3333
 
2912
3334
  /**
2913
3335
  * Experts references
@@ -2921,11 +3343,9 @@ const MemoriWidget = ({
2921
3343
 
2922
3344
  if (resp.resultCode === 0) {
2923
3345
  setExperts(experts);
2924
- } else {
2925
- console.warn('Error fetching experts', resp);
2926
3346
  }
2927
- } catch (err) {
2928
- console.warn(err);
3347
+ } catch {
3348
+ // ignore expert fetch errors
2929
3349
  }
2930
3350
  }, [sessionId, memori?.enableBoardOfExperts]);
2931
3351
  useEffect(() => {
@@ -2938,39 +3358,74 @@ const MemoriWidget = ({
2938
3358
  !!user?.userID &&
2939
3359
  user?.pAndCUAccepted;
2940
3360
 
2941
- // check if owner has enough credits
2942
- const needsCredits = tenant?.billingDelegation;
2943
- const [hasEnoughCredits, setHasEnoughCredits] = useState<boolean>(true);
2944
- const checkCredits = useCallback(async () => {
2945
- if (!tenant?.billingDelegation) return;
3361
+ const handleNotEnoughCredits = useCallback(() => {
3362
+ setHasEnoughCredits(false);
3363
+ setAuthModalState(null);
3364
+ add(
3365
+ createAlertOptions({
3366
+ description: t('notEnoughCredits'),
3367
+ severity: 'error',
3368
+ })
3369
+ );
3370
+ }, [add, t]);
3371
+ const checkCredits = useCallback(
3372
+ async (options?: { notify?: boolean }) => {
3373
+ if (!tenant?.billingDelegation) return true;
3374
+
3375
+ // Billing delegation is active: credits MUST be verified.
3376
+ // Without either owner identifier we cannot call the API, so we fail closed
3377
+ // instead of silently letting the session start unverified.
3378
+ if (!ownerUserID && !ownerUserName) {
3379
+ if (options?.notify) {
3380
+ handleNotEnoughCredits();
3381
+ } else {
3382
+ setHasEnoughCredits(false);
3383
+ }
3384
+ return false;
3385
+ }
2946
3386
 
2947
- try {
2948
- const resp = await getCredits({
2949
- operation: deepThoughtEnabled
2950
- ? 'dt_session_creation'
2951
- : 'session_creation',
2952
- baseUrl: baseUrl,
2953
- userID: ownerUserID,
2954
- userName: ownerUserName,
2955
- tenant: tenantID,
2956
- });
3387
+ try {
3388
+ const resp = await getCredits({
3389
+ operation: deepThoughtEnabled
3390
+ ? 'dt_session_creation'
3391
+ : 'session_creation',
3392
+ baseUrl: baseUrl,
3393
+ userID: ownerUserID,
3394
+ userName: ownerUserName,
3395
+ tenant: tenantID,
3396
+ });
2957
3397
 
2958
- if (resp.enough) {
2959
- setHasEnoughCredits(true);
2960
- } else {
2961
- setHasEnoughCredits(false);
2962
- console.warn('Not enough credits. Required:', resp.required);
3398
+ if (resp.enough) {
3399
+ setHasEnoughCredits(true);
3400
+ return true;
3401
+ } else {
3402
+ if (options?.notify) {
3403
+ handleNotEnoughCredits();
3404
+ } else {
3405
+ setHasEnoughCredits(false);
3406
+ }
3407
+ return false;
3408
+ }
3409
+ } catch (err) {
3410
+ logWidgetError('checkCredits failed', err);
3411
+ return true;
2963
3412
  }
2964
- } catch (e) {
2965
- let err = e as Error;
2966
- console.debug(err);
2967
- }
2968
- }, [tenant?.billingDelegation, deepThoughtEnabled]);
3413
+ },
3414
+ [
3415
+ baseUrl,
3416
+ deepThoughtEnabled,
3417
+ handleNotEnoughCredits,
3418
+ ownerUserID,
3419
+ ownerUserName,
3420
+ tenant?.billingDelegation,
3421
+ tenantID,
3422
+ ]
3423
+ );
2969
3424
  useEffect(() => {
2970
3425
  if (tenant?.billingDelegation) {
2971
3426
  checkCredits();
2972
3427
  }
2973
- }, [tenant?.billingDelegation, deepThoughtEnabled]);
3428
+ }, [tenant?.billingDelegation, deepThoughtEnabled, checkCredits]);
2974
3429
 
2975
3430
  useEffect(() => {
2976
3431
  if (__WEBCOMPONENT__) return;
@@ -3043,7 +3498,7 @@ const MemoriWidget = ({
3043
3498
  setShowLoginDrawer(false);
3044
3499
  setUser(undefined);
3045
3500
  setLoginToken(undefined);
3046
- userToken = undefined;
3501
+ userTokenRef.current = undefined;
3047
3502
  removeLocalConfig('loginToken');
3048
3503
  });
3049
3504
  },
@@ -3120,8 +3575,7 @@ const MemoriWidget = ({
3120
3575
  showTypingText:
3121
3576
  showTypingText ?? integrationConfig?.showTypingText ?? false,
3122
3577
  history: showFullHistory ? history : history.slice(-2),
3123
- authToken:
3124
- loginToken ?? userToken ?? additionalInfo?.loginToken ?? authToken,
3578
+ authToken: resolveLoginToken(),
3125
3579
  dialogState: currentDialogState,
3126
3580
  pushMessage,
3127
3581
  simulateUserPrompt,
@@ -3520,7 +3974,7 @@ const MemoriWidget = ({
3520
3974
  setShowLoginDrawer(false);
3521
3975
  setUser(user);
3522
3976
  setLoginToken(token);
3523
- userToken = token;
3977
+ userTokenRef.current = token;
3524
3978
  setLocalConfig('loginToken', token);
3525
3979
  // Push a message with initial status to show status message when a new session is created after login
3526
3980
  if (
@@ -3563,7 +4017,7 @@ const MemoriWidget = ({
3563
4017
  setShowLoginDrawer(false);
3564
4018
  setUser(undefined);
3565
4019
  setLoginToken(undefined);
3566
- userToken = undefined;
4020
+ userTokenRef.current = undefined;
3567
4021
  removeLocalConfig('loginToken');
3568
4022
  });
3569
4023
  }}