@memori.ai/memori-react 8.39.0 → 8.40.1

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 (49) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/components/MemoriWidget/MemoriWidget.d.ts +2 -1
  3. package/dist/components/MemoriWidget/MemoriWidget.js +450 -146
  4. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  5. package/dist/helpers/credits.d.ts +3 -2
  6. package/dist/helpers/credits.js +4 -3
  7. package/dist/helpers/credits.js.map +1 -1
  8. package/dist/helpers/nats/getNatsConfig.d.ts +5 -0
  9. package/dist/helpers/nats/getNatsConfig.js +29 -0
  10. package/dist/helpers/nats/getNatsConfig.js.map +1 -0
  11. package/dist/helpers/nats/useNats.d.ts +12 -0
  12. package/dist/helpers/nats/useNats.js +72 -0
  13. package/dist/helpers/nats/useNats.js.map +1 -0
  14. package/dist/helpers/nats/useNatsSession.d.ts +27 -0
  15. package/dist/helpers/nats/useNatsSession.js +108 -0
  16. package/dist/helpers/nats/useNatsSession.js.map +1 -0
  17. package/dist/index.js +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/version.d.ts +1 -1
  20. package/dist/version.js +1 -1
  21. package/esm/components/MemoriWidget/MemoriWidget.d.ts +2 -1
  22. package/esm/components/MemoriWidget/MemoriWidget.js +453 -147
  23. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  24. package/esm/helpers/credits.d.ts +3 -2
  25. package/esm/helpers/credits.js +4 -3
  26. package/esm/helpers/credits.js.map +1 -1
  27. package/esm/helpers/nats/getNatsConfig.d.ts +5 -0
  28. package/esm/helpers/nats/getNatsConfig.js +25 -0
  29. package/esm/helpers/nats/getNatsConfig.js.map +1 -0
  30. package/esm/helpers/nats/useNats.d.ts +12 -0
  31. package/esm/helpers/nats/useNats.js +68 -0
  32. package/esm/helpers/nats/useNats.js.map +1 -0
  33. package/esm/helpers/nats/useNatsSession.d.ts +27 -0
  34. package/esm/helpers/nats/useNatsSession.js +103 -0
  35. package/esm/helpers/nats/useNatsSession.js.map +1 -0
  36. package/esm/index.js +1 -1
  37. package/esm/index.js.map +1 -1
  38. package/esm/version.d.ts +1 -1
  39. package/esm/version.js +1 -1
  40. package/package.json +3 -2
  41. package/src/components/MemoriWidget/MemoriWidget.tsx +543 -149
  42. package/src/components/layouts/layouts.stories.tsx +29 -35
  43. package/src/helpers/credits.ts +6 -3
  44. package/src/helpers/nats/getNatsConfig.ts +69 -0
  45. package/src/helpers/nats/useNats.ts +122 -0
  46. package/src/helpers/nats/useNatsSession.ts +210 -0
  47. package/src/index.stories.tsx +19 -3
  48. package/src/index.tsx +1 -0
  49. package/src/version.ts +1 -1
@@ -85,6 +85,12 @@ import { sanitizeText } from '../../helpers/sanitizer';
85
85
  import { TTSConfig, useTTS } from '../../helpers/tts/useTTS';
86
86
  import ChatHistoryDrawer from '../ChatHistoryDrawer/ChatHistory';
87
87
  import { STTConfig, useSTT } from '../../helpers/stt/useSTT';
88
+ import { useNats } from '../../helpers/nats/useNats';
89
+ import {
90
+ NatsProgressEvent,
91
+ NatsDialogResponseEvent,
92
+ NatsErrorEvent,
93
+ } from '../../helpers/nats/useNatsSession';
88
94
 
89
95
  // Widget utilities and helpers
90
96
  const getMemoriState = (integrationId?: string): object | null => {
@@ -115,7 +121,7 @@ const getMemoriState = (integrationId?: string): object | null => {
115
121
  };
116
122
  };
117
123
 
118
- /** Place spec with all nulls for postTextEnteredEvent when position is not set or user chose "I don't want to provide my position". */
124
+ /** Place spec with all nulls for postEnterTextAsync when position is not set or user chose "I don't want to provide my position". */
119
125
  const NULL_PLACE_SPEC = {
120
126
  placeName: null,
121
127
  latitude: null,
@@ -123,6 +129,16 @@ const NULL_PLACE_SPEC = {
123
129
  uncertaintyKm: null,
124
130
  } as const;
125
131
 
132
+ const ENTER_TEXT_NATS_TIMEOUT_MS = 120_000;
133
+
134
+ /** Reads correlation id from HTTP async response (supports camelCase / snake_case). */
135
+ function readCorrelationID(response: {
136
+ correlationID?: string;
137
+ }): string | undefined {
138
+ const value = response.correlationID;
139
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
140
+ }
141
+
126
142
  type MemoriTextEnteredEvent = CustomEvent<{
127
143
  text: string;
128
144
  waitForPrevious?: boolean;
@@ -381,7 +397,7 @@ export interface LayoutProps {
381
397
 
382
398
  export interface Props {
383
399
  memori: Memori;
384
- // ownerUserName?: string | null;
400
+ ownerUserName?: string | null;
385
401
  ownerUserID?: string | null;
386
402
  tenantID: string;
387
403
  memoriConfigs?: MemoriConfig[];
@@ -453,7 +469,7 @@ const MemoriWidget = ({
453
469
  memori,
454
470
  memoriConfigs,
455
471
  ownerUserID,
456
- // ownerUserName,
472
+ ownerUserName,
457
473
  tenantID,
458
474
  memoriLang,
459
475
  uiLang,
@@ -521,6 +537,7 @@ const MemoriWidget = ({
521
537
  const {
522
538
  initSession,
523
539
  deleteSession,
540
+ postEnterTextAsync,
524
541
  postTextEnteredEvent,
525
542
  postPlaceChangedEvent,
526
543
  postDateChangedEvent,
@@ -565,6 +582,7 @@ const MemoriWidget = ({
565
582
  const [showLoginDrawer, setShowLoginDrawer] = useState(false);
566
583
 
567
584
  const [clickedStart, setClickedStart] = useState(false);
585
+ const sessionStartingRef = useRef(false);
568
586
 
569
587
  const language =
570
588
  memori.culture?.split('-')?.[0]?.toUpperCase()! ||
@@ -626,6 +644,23 @@ const MemoriWidget = ({
626
644
  const [memoriTyping, setMemoriTyping] = useState<boolean>(false);
627
645
  const [typingText, setTypingText] = useState<string>();
628
646
 
647
+ type PendingEnterText = {
648
+ msg?: string;
649
+ typingText?: string;
650
+ useLoaderTextAsMsg?: boolean;
651
+ hasBatchQueued?: boolean;
652
+ natsTimeoutId?: ReturnType<typeof setTimeout>;
653
+ waitForResponse?: {
654
+ resolve: (event: NatsDialogResponseEvent) => void;
655
+ reject: (error: Error) => void;
656
+ timeoutId: ReturnType<typeof setTimeout>;
657
+ };
658
+ };
659
+ const pendingEnterTextRef = useRef<Map<string, PendingEnterText>>(new Map());
660
+ const bufferedNatsResponsesRef = useRef<Map<string, NatsDialogResponseEvent>>(
661
+ new Map()
662
+ );
663
+
629
664
  // Layout: from prop (string only) or integrationConfig. PII detection is only from integrationConfig (customData.layout as object with piiDetection).
630
665
  const layoutName =
631
666
  typeof layout === 'string'
@@ -783,7 +818,7 @@ const MemoriWidget = ({
783
818
  return Object.keys(place).length > 0 ? place : undefined;
784
819
  }, []);
785
820
 
786
- /** Place to send with postTextEnteredEvent: real place, nulls when no/declined position, or undefined when position not needed. */
821
+ /** Place to send with postEnterTextAsync: real place, nulls when no/declined position, or undefined when position not needed. */
787
822
  const getPlaceSpecForEnterText = useCallback(
788
823
  (venue: Venue | undefined) => {
789
824
  if (!memori.needsPosition) return undefined;
@@ -945,27 +980,18 @@ const MemoriWidget = ({
945
980
  : !!newSessionId,
946
981
  });
947
982
 
948
- // Show typing indicator
949
- setMemoriTyping(true);
950
- setTypingText(typingText);
951
-
983
+ // Show typing indicator after the async enter-text request is accepted (HTTP 200).
952
984
  let gotError = false;
953
985
 
954
986
  try {
955
- // Add chat reference link to the message if it exists
956
- // if (chatLogID) {
957
- // msg =
958
- // msg +
959
- // ' \n\n' +
960
- // '<chat-reference session-id="' +
961
- // sessionID +
962
- // '" event-log-id="' +
963
- // chatLogID +
964
- // '"></chat-reference>';
965
- // }
966
-
967
987
  const placeSpec = getPlaceSpecForEnterText(position);
968
- const { currentState, ...response } = await postTextEnteredEvent({
988
+ console.debug('[EnterText] sendMessage: posting', {
989
+ sessionId: sessionID,
990
+ textLength: msg.length,
991
+ hasBatchQueued,
992
+ typingText,
993
+ });
994
+ const response = await postEnterTextAsync({
969
995
  sessionId: sessionID,
970
996
  text: msg,
971
997
  ...(memori.needsDateTime && {
@@ -973,55 +999,33 @@ const MemoriWidget = ({
973
999
  }),
974
1000
  ...(placeSpec !== undefined && { place: placeSpec }),
975
1001
  });
976
- if (response.resultCode === 0 && currentState) {
977
- setChatLogID(undefined);
978
- const emission =
979
- useLoaderTextAsMsg && typingText
980
- ? typingText
981
- : currentState.emission ?? currentDialogState?.emission;
982
-
983
- if (
984
- userLang.toLowerCase() !== language.toLowerCase() &&
985
- emission &&
986
- isMultilanguageEnabled
987
- ) {
988
- currentState.emission = emission;
989
-
990
- translateDialogState(currentState, userLang, msg).then(ts => {
991
- let text = ts.translatedEmission || ts.emission;
992
- if (text && shouldPlayAudio(text)) {
993
- handleSpeak(text);
994
- }
995
- });
996
- } else {
997
- setCurrentDialogState({
998
- ...currentState,
999
- emission,
1000
- });
1001
-
1002
- if (emission) {
1003
- pushMessage({
1004
- text: emission,
1005
- emitter: currentState.emitter,
1006
- media: currentState.emittedMedia ?? currentState.media,
1007
- llmUsage: (currentState as any).llmUsage,
1008
- fromUser: false,
1009
- questionAnswered: msg,
1010
- generatedByAI: !!currentState.completion,
1011
- contextVars: currentState.contextVars,
1012
- date: currentState.currentDate,
1013
- placeName: currentState.currentPlaceName,
1014
- placeLatitude: currentState.currentLatitude,
1015
- placeLongitude: currentState.currentLongitude,
1016
- placeUncertaintyKm: currentState.currentUncertaintyKm,
1017
- tag: currentState.currentTag,
1018
- memoryTags: currentState.memoryTags,
1019
- } as any);
1020
- if (emission && shouldPlayAudio(emission)) {
1021
- handleSpeak(emission);
1022
- }
1002
+ console.debug('[EnterText] sendMessage: HTTP response', {
1003
+ resultCode: response.resultCode,
1004
+ correlationID: readCorrelationID(response),
1005
+ resultMessage: response.resultMessage,
1006
+ });
1007
+ const correlationID = readCorrelationID(response);
1008
+ if (response.resultCode === 0 && correlationID) {
1009
+ registerPendingEnterText(correlationID, {
1010
+ msg,
1011
+ typingText,
1012
+ useLoaderTextAsMsg,
1013
+ hasBatchQueued,
1014
+ });
1015
+ console.info(
1016
+ '[EnterText] sendMessage: accepted, showing typing indicator',
1017
+ {
1018
+ correlationID: correlationID,
1019
+ typingText,
1023
1020
  }
1024
- }
1021
+ );
1022
+ setMemoriTyping(true);
1023
+ setTypingText(typingText);
1024
+ } else if (response.resultCode === 0) {
1025
+ console.error(
1026
+ '[EnterText] sendMessage: HTTP 200 but missing correlationID — cannot match NATS response',
1027
+ response
1028
+ );
1025
1029
  } else if (response.resultCode === 404) {
1026
1030
  // Handle expired session
1027
1031
  // remove last sent message, will set it as initial
@@ -1070,18 +1074,12 @@ const MemoriWidget = ({
1070
1074
  return Promise.reject(response);
1071
1075
  }
1072
1076
  } catch (error) {
1073
- console.log('error', error);
1074
- console.error(error);
1077
+ console.error('[EnterText] sendMessage: request failed', error);
1075
1078
  gotError = true;
1076
1079
 
1077
1080
  setTypingText(undefined);
1078
1081
  setMemoriTyping(false);
1079
1082
  }
1080
-
1081
- if (!hasBatchQueued) {
1082
- setTypingText(undefined);
1083
- setMemoriTyping(false);
1084
- }
1085
1083
  };
1086
1084
 
1087
1085
  /**
@@ -1388,7 +1386,7 @@ const MemoriWidget = ({
1388
1386
  return;
1389
1387
  }
1390
1388
 
1391
- if (!(await checkCredits({ notify: true, goBack: true }))) {
1389
+ if (!(await checkCredits({ notify: true }))) {
1392
1390
  return;
1393
1391
  }
1394
1392
 
@@ -1562,7 +1560,7 @@ const MemoriWidget = ({
1562
1560
  return;
1563
1561
  }
1564
1562
 
1565
- if (!(await checkCredits({ notify: true, goBack: true }))) {
1563
+ if (!(await checkCredits({ notify: true }))) {
1566
1564
  setLoading(false);
1567
1565
  return null;
1568
1566
  }
@@ -1846,6 +1844,7 @@ const MemoriWidget = ({
1846
1844
  return () => {
1847
1845
  setHasUserActivatedSpeak(false);
1848
1846
  setClickedStart(false);
1847
+ sessionStartingRef.current = false;
1849
1848
  clearInteractionTimeout();
1850
1849
  timeoutRef.current = undefined;
1851
1850
  };
@@ -2046,6 +2045,341 @@ const MemoriWidget = ({
2046
2045
  ]
2047
2046
  );
2048
2047
 
2048
+ const processEnterTextDialogResponse = useCallback(
2049
+ (event: NatsDialogResponseEvent, pending: PendingEnterText) => {
2050
+ console.debug('[EnterText] processDialogResponse', {
2051
+ correlationID: event.correlationID,
2052
+ resultCode: event.resultCode,
2053
+ hasCurrentState: !!event.currentState,
2054
+ hasBatchQueued: pending.hasBatchQueued,
2055
+ });
2056
+ const {
2057
+ msg,
2058
+ typingText: pendingTypingText,
2059
+ useLoaderTextAsMsg,
2060
+ } = pending;
2061
+ const currentState = event.currentState;
2062
+
2063
+ if (event.resultCode !== 0 || !currentState) {
2064
+ if (event.resultCode === 500 && event.resultMessage) {
2065
+ console.warn('[EnterText] processDialogResponse: server error', {
2066
+ correlationID: event.correlationID,
2067
+ resultMessage: event.resultMessage,
2068
+ });
2069
+ setHistory(h => [
2070
+ ...h,
2071
+ {
2072
+ text: 'Error: ' + event.resultMessage,
2073
+ emitter: 'system',
2074
+ fromUser: false,
2075
+ initial: false,
2076
+ contextVars: {},
2077
+ date: new Date().toISOString(),
2078
+ },
2079
+ ]);
2080
+ } else if (event.resultCode !== 0) {
2081
+ console.warn('[SEND_MESSAGE/NATS]', event);
2082
+ }
2083
+ return;
2084
+ }
2085
+
2086
+ if (!msg) {
2087
+ console.debug(
2088
+ '[EnterText] processDialogResponse: no msg in pending, skipping'
2089
+ );
2090
+ return;
2091
+ }
2092
+
2093
+ setChatLogID(undefined);
2094
+ const emission =
2095
+ useLoaderTextAsMsg && pendingTypingText
2096
+ ? pendingTypingText
2097
+ : currentState.emission ?? currentDialogState?.emission;
2098
+
2099
+ console.debug('[EnterText] processDialogResponse: rendering emission', {
2100
+ correlationID: event.correlationID,
2101
+ emissionPreview: emission?.slice(0, 80),
2102
+ state: currentState.state,
2103
+ });
2104
+
2105
+ if (
2106
+ userLang.toLowerCase() !== language.toLowerCase() &&
2107
+ emission &&
2108
+ isMultilanguageEnabled
2109
+ ) {
2110
+ currentState.emission = emission;
2111
+
2112
+ translateDialogState(currentState, userLang, msg).then(ts => {
2113
+ const text = ts.translatedEmission || ts.emission;
2114
+ if (text && shouldPlayAudio(text)) {
2115
+ handleSpeak(text);
2116
+ }
2117
+ });
2118
+ } else {
2119
+ setCurrentDialogState({
2120
+ ...currentState,
2121
+ emission,
2122
+ });
2123
+
2124
+ if (emission) {
2125
+ pushMessage({
2126
+ text: emission,
2127
+ emitter: currentState.emitter,
2128
+ media: currentState.emittedMedia ?? currentState.media,
2129
+ llmUsage: (currentState as any).llmUsage,
2130
+ fromUser: false,
2131
+ questionAnswered: msg,
2132
+ generatedByAI: !!currentState.completion,
2133
+ contextVars: currentState.contextVars,
2134
+ date: currentState.currentDate,
2135
+ placeName: currentState.currentPlaceName,
2136
+ placeLatitude: currentState.currentLatitude,
2137
+ placeLongitude: currentState.currentLongitude,
2138
+ placeUncertaintyKm: currentState.currentUncertaintyKm,
2139
+ tag: currentState.currentTag,
2140
+ memoryTags: currentState.memoryTags,
2141
+ } as any);
2142
+ if (emission && shouldPlayAudio(emission)) {
2143
+ handleSpeak(emission);
2144
+ }
2145
+ }
2146
+ }
2147
+ },
2148
+ [
2149
+ userLang,
2150
+ language,
2151
+ isMultilanguageEnabled,
2152
+ currentDialogState?.emission,
2153
+ translateDialogState,
2154
+ handleSpeak,
2155
+ shouldPlayAudio,
2156
+ ]
2157
+ );
2158
+
2159
+ const clearEnterTextPending = useCallback(
2160
+ (correlationID: string, pending: PendingEnterText) => {
2161
+ if (pending.natsTimeoutId) {
2162
+ clearTimeout(pending.natsTimeoutId);
2163
+ }
2164
+ if (pending.waitForResponse?.timeoutId) {
2165
+ clearTimeout(pending.waitForResponse.timeoutId);
2166
+ }
2167
+ pendingEnterTextRef.current.delete(correlationID);
2168
+ },
2169
+ []
2170
+ );
2171
+
2172
+ const deliverEnterTextNatsError = useCallback(
2173
+ (event: NatsErrorEvent) => {
2174
+ const correlationID = event.correlationID;
2175
+ const errorText = event.errorMessage
2176
+ ? `Error: ${event.errorMessage}`
2177
+ : event.errorCode
2178
+ ? `Error: ${event.errorCode}`
2179
+ : 'Error: An unexpected error occurred';
2180
+
2181
+ console.error('[EnterText] NATS error event', {
2182
+ correlationID,
2183
+ errorCode: event.errorCode,
2184
+ errorMessage: event.errorMessage,
2185
+ });
2186
+
2187
+ pushMessage({
2188
+ text: errorText,
2189
+ emitter: 'system',
2190
+ fromUser: false,
2191
+ initial: false,
2192
+ contextVars: {},
2193
+ date: new Date().toISOString(),
2194
+ });
2195
+
2196
+ if (correlationID) {
2197
+ const pending = pendingEnterTextRef.current.get(correlationID);
2198
+ if (pending) {
2199
+ clearEnterTextPending(correlationID, pending);
2200
+ pending.waitForResponse?.reject(
2201
+ new Error(event.errorMessage ?? String(event.errorCode ?? 'NATS error'))
2202
+ );
2203
+ }
2204
+ }
2205
+
2206
+ setMemoriTyping(false);
2207
+ setTypingText(undefined);
2208
+ },
2209
+ [clearEnterTextPending]
2210
+ );
2211
+
2212
+ const deliverEnterTextNatsResponse = useCallback(
2213
+ (correlationID: string, event: NatsDialogResponseEvent) => {
2214
+ const pending = pendingEnterTextRef.current.get(correlationID);
2215
+ if (!pending) {
2216
+ const pendingCorrelationIDs = [...pendingEnterTextRef.current.keys()];
2217
+ console.warn(
2218
+ '[EnterText] NATS response buffered (no matching pending)',
2219
+ {
2220
+ receivedCorrelationID: correlationID,
2221
+ resultCode: event.resultCode,
2222
+ pendingCorrelationIDs,
2223
+ hint:
2224
+ pendingCorrelationIDs.length > 0
2225
+ ? 'Use one of pendingCorrelationIDs in your nats pub correlation_id'
2226
+ : 'Send a message in the widget first, then copy correlationID from HTTP response logs',
2227
+ }
2228
+ );
2229
+ bufferedNatsResponsesRef.current.set(correlationID, event);
2230
+ return;
2231
+ }
2232
+
2233
+ clearEnterTextPending(correlationID, pending);
2234
+
2235
+ if (pending.waitForResponse) {
2236
+ console.info('[EnterText] NATS response delivered to waiter', {
2237
+ correlationID,
2238
+ resultCode: event.resultCode,
2239
+ });
2240
+ pending.waitForResponse.resolve(event);
2241
+ setMemoriTyping(false);
2242
+ setTypingText(undefined);
2243
+ return;
2244
+ }
2245
+
2246
+ processEnterTextDialogResponse(event, pending);
2247
+
2248
+ if (!pending.hasBatchQueued) {
2249
+ console.info('[EnterText] typing indicator cleared', { correlationID });
2250
+ setMemoriTyping(false);
2251
+ setTypingText(undefined);
2252
+ } else {
2253
+ console.debug('[EnterText] typing kept (batch queued)', {
2254
+ correlationID,
2255
+ });
2256
+ }
2257
+ },
2258
+ [processEnterTextDialogResponse, clearEnterTextPending]
2259
+ );
2260
+
2261
+ const registerPendingEnterText = useCallback(
2262
+ (correlationID: string, pending: PendingEnterText) => {
2263
+ const buffered = bufferedNatsResponsesRef.current.get(correlationID);
2264
+ if (buffered) {
2265
+ console.info('[EnterText] replaying buffered NATS response', {
2266
+ correlationID,
2267
+ waitForResponse: !!pending.waitForResponse,
2268
+ });
2269
+ bufferedNatsResponsesRef.current.delete(correlationID);
2270
+ pendingEnterTextRef.current.set(correlationID, pending);
2271
+ deliverEnterTextNatsResponse(correlationID, buffered);
2272
+ return;
2273
+ }
2274
+
2275
+ if (!pending.waitForResponse && !pending.natsTimeoutId) {
2276
+ pending.natsTimeoutId = setTimeout(() => {
2277
+ const current = pendingEnterTextRef.current.get(correlationID);
2278
+ if (!current) return;
2279
+ clearEnterTextPending(correlationID, current);
2280
+ console.error('[EnterText] NATS response timeout', {
2281
+ correlationID,
2282
+ timeoutMs: ENTER_TEXT_NATS_TIMEOUT_MS,
2283
+ });
2284
+ if (!current.hasBatchQueued) {
2285
+ setMemoriTyping(false);
2286
+ setTypingText(undefined);
2287
+ }
2288
+ current.waitForResponse?.reject(
2289
+ new Error('NATS enter-text response timeout')
2290
+ );
2291
+ }, ENTER_TEXT_NATS_TIMEOUT_MS);
2292
+ }
2293
+
2294
+ console.debug('[EnterText] pending registered', {
2295
+ correlationID,
2296
+ waitForResponse: !!pending.waitForResponse,
2297
+ hasBatchQueued: pending.hasBatchQueued,
2298
+ });
2299
+ pendingEnterTextRef.current.set(correlationID, pending);
2300
+ },
2301
+ [deliverEnterTextNatsResponse, clearEnterTextPending]
2302
+ );
2303
+
2304
+ const waitForEnterTextNatsResponse = useCallback(
2305
+ (correlationID: string, timeoutMs = 120000) =>
2306
+ new Promise<NatsDialogResponseEvent>((resolve, reject) => {
2307
+ console.debug('[EnterText] waiting for NATS response', {
2308
+ correlationID,
2309
+ timeoutMs,
2310
+ });
2311
+ const timeoutId = setTimeout(() => {
2312
+ const current = pendingEnterTextRef.current.get(correlationID);
2313
+ if (current) {
2314
+ clearEnterTextPending(correlationID, current);
2315
+ }
2316
+ console.error('[EnterText] NATS response timeout', {
2317
+ correlationID,
2318
+ timeoutMs,
2319
+ });
2320
+ reject(new Error('NATS enter-text response timeout'));
2321
+ }, timeoutMs);
2322
+
2323
+ registerPendingEnterText(correlationID, {
2324
+ waitForResponse: {
2325
+ resolve: event => {
2326
+ clearTimeout(timeoutId);
2327
+ resolve(event);
2328
+ },
2329
+ reject: error => {
2330
+ clearTimeout(timeoutId);
2331
+ reject(error);
2332
+ },
2333
+ timeoutId,
2334
+ },
2335
+ });
2336
+ }),
2337
+ [registerPendingEnterText, clearEnterTextPending]
2338
+ );
2339
+
2340
+ // NATS subscription: receives progress updates and the async enter-text response.
2341
+ useNats({
2342
+ baseUrl,
2343
+ sessionId,
2344
+ onProgress: useCallback((event: NatsProgressEvent) => {
2345
+ console.debug('[EnterText] NATS progress', {
2346
+ correlationID: event.correlationID,
2347
+ step: event.currentStep,
2348
+ finalStep: event.finalStep,
2349
+ message: event.message,
2350
+ });
2351
+ if (event.message) {
2352
+ setTypingText(event.message);
2353
+ }
2354
+ }, []),
2355
+ onDialogResponse: useCallback(
2356
+ (event: NatsDialogResponseEvent) => {
2357
+ const correlationID = event.correlationID;
2358
+ console.debug(
2359
+ '[EnterText] NATS dialog.text_entered_response received',
2360
+ {
2361
+ correlationID,
2362
+ resultCode: event.resultCode,
2363
+ requestID: event.requestID,
2364
+ }
2365
+ );
2366
+ if (!correlationID) {
2367
+ console.warn(
2368
+ '[EnterText] dialog_text_entered_response without correlationID',
2369
+ event
2370
+ );
2371
+ setMemoriTyping(false);
2372
+ setTypingText(undefined);
2373
+ return;
2374
+ }
2375
+
2376
+ deliverEnterTextNatsResponse(correlationID, event);
2377
+ },
2378
+ [deliverEnterTextNatsResponse]
2379
+ ),
2380
+ onError: deliverEnterTextNatsError,
2381
+ });
2382
+
2049
2383
  const focusChatInput = () => {
2050
2384
  let textarea = document.querySelector(
2051
2385
  '#chat-fieldset textarea'
@@ -2373,7 +2707,7 @@ const MemoriWidget = ({
2373
2707
  return;
2374
2708
  }
2375
2709
 
2376
- if (!(await checkCredits({ notify: true, goBack: true }))) {
2710
+ if (!(await checkCredits({ notify: true }))) {
2377
2711
  setClickedStart(false);
2378
2712
  setLoading(false);
2379
2713
  return;
@@ -2383,6 +2717,7 @@ const MemoriWidget = ({
2383
2717
  if (!sessionID && !!minAge && !birth) {
2384
2718
  setShowAgeVerification(true);
2385
2719
  setClickedStart(false);
2720
+ return;
2386
2721
  }
2387
2722
  // Handle authentication
2388
2723
  else if (
@@ -2398,41 +2733,46 @@ const MemoriWidget = ({
2398
2733
  }
2399
2734
  // Create new session if needed
2400
2735
  else if (!sessionID || initialSessionExpired) {
2401
- setClickedStart(false);
2402
- const session = await fetchSession({
2403
- memoriID: memori.engineMemoriID!,
2404
- password: secret || memoriPwd || memori.secretToken,
2405
- tag: personification?.tag,
2406
- pin: personification?.pin,
2407
- continueFromChatLogID: chatLog?.chatLogID,
2408
- initialContextVars: {
2409
- LANG: userLang,
2410
- PATHNAME: window.location.pathname?.toUpperCase(),
2411
- ROUTE:
2412
- window.location.pathname?.split('/')?.pop()?.toUpperCase() || '',
2413
- ...((!chatLog
2414
- ? initialContextVars
2415
- : chatLog.lines[chatLog.lines.length - 1].contextVars) || {}),
2416
- },
2417
- initialQuestion: chatLog ? undefined : initialQuestion,
2418
- birthDate: birth,
2419
- additionalInfo: {
2420
- ...(additionalInfo || {}),
2421
- loginToken:
2422
- userToken ??
2423
- loginToken ??
2424
- additionalInfo?.loginToken ??
2425
- authToken,
2426
- language: (
2427
- userLang ??
2428
- memori.culture?.split('-')?.[0] ??
2429
- 'IT'
2430
- ).toLowerCase(),
2431
- timeZoneOffset: new Date().getTimezoneOffset().toString(),
2432
- },
2433
- });
2736
+ if (sessionStartingRef.current) {
2737
+ return;
2738
+ }
2739
+ sessionStartingRef.current = true;
2740
+ try {
2741
+ const session = await fetchSession({
2742
+ memoriID: memori.engineMemoriID!,
2743
+ password: secret || memoriPwd || memori.secretToken,
2744
+ tag: personification?.tag,
2745
+ pin: personification?.pin,
2746
+ continueFromChatLogID: chatLog?.chatLogID,
2747
+ initialContextVars: {
2748
+ LANG: userLang,
2749
+ PATHNAME: window.location.pathname?.toUpperCase(),
2750
+ ROUTE:
2751
+ window.location.pathname?.split('/')?.pop()?.toUpperCase() ||
2752
+ '',
2753
+ ...((!chatLog
2754
+ ? initialContextVars
2755
+ : chatLog.lines[chatLog.lines.length - 1].contextVars) || {}),
2756
+ },
2757
+ initialQuestion: chatLog ? undefined : initialQuestion,
2758
+ birthDate: birth,
2759
+ additionalInfo: {
2760
+ ...(additionalInfo || {}),
2761
+ loginToken:
2762
+ userToken ??
2763
+ loginToken ??
2764
+ additionalInfo?.loginToken ??
2765
+ authToken,
2766
+ language: (
2767
+ userLang ??
2768
+ memori.culture?.split('-')?.[0] ??
2769
+ 'IT'
2770
+ ).toLowerCase(),
2771
+ timeZoneOffset: new Date().getTimezoneOffset().toString(),
2772
+ },
2773
+ });
2434
2774
 
2435
- if (session?.dialogState) {
2775
+ if (session?.dialogState) {
2436
2776
  // reset history
2437
2777
  if (!chatLog) {
2438
2778
  setHistory([]);
@@ -2441,6 +2781,7 @@ const MemoriWidget = ({
2441
2781
  await translateAndSpeak(session.dialogState, userLang);
2442
2782
  // No need for additional handleSpeak call since translateAndSpeak already handles it
2443
2783
  setHasUserActivatedSpeak(true);
2784
+ setClickedStart(false);
2444
2785
  } else {
2445
2786
  const messages = chatLog.lines.map(
2446
2787
  (l, i) =>
@@ -2502,12 +2843,18 @@ const MemoriWidget = ({
2502
2843
  true
2503
2844
  ).finally(() => {
2504
2845
  setHasUserActivatedSpeak(true);
2846
+ setClickedStart(false);
2505
2847
  });
2506
2848
  }
2507
- } else if (session?.resultCode === 0) {
2508
- await onClickStart((session as any) || undefined);
2509
- } else {
2510
- setLoading(false);
2849
+ } else if (session?.resultCode === 0) {
2850
+ sessionStartingRef.current = false;
2851
+ await onClickStart((session as any) || undefined);
2852
+ } else {
2853
+ setLoading(false);
2854
+ setClickedStart(false);
2855
+ }
2856
+ } finally {
2857
+ sessionStartingRef.current = false;
2511
2858
  }
2512
2859
 
2513
2860
  return;
@@ -2521,7 +2868,6 @@ const MemoriWidget = ({
2521
2868
  if (response.resultCode !== 0 || !currentState) {
2522
2869
  const { chatLogs } = await getSessionChatLogs(sessionID!, sessionID!);
2523
2870
  setSessionId(undefined);
2524
- setClickedStart(false);
2525
2871
  await onClickStart(undefined, true, chatLogs?.[0]);
2526
2872
  return;
2527
2873
  }
@@ -2547,6 +2893,7 @@ const MemoriWidget = ({
2547
2893
 
2548
2894
  if (session && session.resultCode === 0) {
2549
2895
  await translateAndSpeak(session.currentState, userLang);
2896
+ setClickedStart(false);
2550
2897
  } else {
2551
2898
  throw new Error('No session');
2552
2899
  }
@@ -2570,6 +2917,7 @@ const MemoriWidget = ({
2570
2917
  birth
2571
2918
  ).then(() => {
2572
2919
  setHasUserActivatedSpeak(true);
2920
+ setClickedStart(false);
2573
2921
  });
2574
2922
  }
2575
2923
  }
@@ -2592,6 +2940,7 @@ const MemoriWidget = ({
2592
2940
 
2593
2941
  if (session && session.resultCode === 0) {
2594
2942
  await translateAndSpeak(session.currentState, userLang);
2943
+ setClickedStart(false);
2595
2944
  } else {
2596
2945
  throw new Error('No session');
2597
2946
  }
@@ -2614,6 +2963,7 @@ const MemoriWidget = ({
2614
2963
  birth
2615
2964
  ).then(() => {
2616
2965
  setHasUserActivatedSpeak(true);
2966
+ setClickedStart(false);
2617
2967
  });
2618
2968
  }
2619
2969
  }
@@ -2676,6 +3026,7 @@ const MemoriWidget = ({
2676
3026
  ) {
2677
3027
  // we have a history, don't push message
2678
3028
  setHasUserActivatedSpeak(true);
3029
+ setClickedStart(false);
2679
3030
  await translateAndSpeak(
2680
3031
  currentState,
2681
3032
  userLang,
@@ -2690,11 +3041,15 @@ const MemoriWidget = ({
2690
3041
  translatedMessages = [];
2691
3042
  setHistory([]);
2692
3043
 
2693
- setMemoriTyping(true);
2694
-
2695
3044
  // we have no chat history, we start by initial question
2696
3045
  const placeSpec = getPlaceSpecForEnterText(position);
2697
- const response = await postTextEnteredEvent({
3046
+ console.debug(
3047
+ '[EnterText] onClickStart: posting initial question',
3048
+ {
3049
+ sessionId: sessionID,
3050
+ }
3051
+ );
3052
+ const response = await postEnterTextAsync({
2698
3053
  sessionId: sessionID!,
2699
3054
  text: initialQuestion,
2700
3055
  ...(memori.needsDateTime && {
@@ -2702,8 +3057,12 @@ const MemoriWidget = ({
2702
3057
  }),
2703
3058
  ...(placeSpec !== undefined && { place: placeSpec }),
2704
3059
  });
3060
+ console.debug('[EnterText] onClickStart: HTTP response', {
3061
+ resultCode: response.resultCode,
3062
+ correlationID: readCorrelationID(response),
3063
+ });
2705
3064
 
2706
- // Handle 500 error from TextEnteredEvent
3065
+ // Handle 500 error from EnterTextAsync
2707
3066
  if (response.resultCode === 500 && response.resultMessage) {
2708
3067
  setHistory(h => [
2709
3068
  ...h,
@@ -2716,16 +3075,49 @@ const MemoriWidget = ({
2716
3075
  date: new Date().toISOString(),
2717
3076
  },
2718
3077
  ]);
2719
- setMemoriTyping(false);
2720
3078
  return;
2721
3079
  }
2722
3080
 
2723
- await translateAndSpeak(
2724
- response.currentState ?? currentState,
2725
- userLang,
2726
- undefined,
2727
- false
2728
- );
3081
+ const onClickStartCorrelationID = readCorrelationID(response);
3082
+ if (response.resultCode === 0 && onClickStartCorrelationID) {
3083
+ console.info(
3084
+ '[EnterText] onClickStart: accepted, showing typing indicator',
3085
+ {
3086
+ correlationID: onClickStartCorrelationID,
3087
+ }
3088
+ );
3089
+ setMemoriTyping(true);
3090
+ try {
3091
+ const natsEvent = await waitForEnterTextNatsResponse(
3092
+ onClickStartCorrelationID
3093
+ );
3094
+ console.info(
3095
+ '[EnterText] onClickStart: NATS response received',
3096
+ {
3097
+ correlationID: onClickStartCorrelationID,
3098
+ resultCode: natsEvent.resultCode,
3099
+ }
3100
+ );
3101
+ if (natsEvent.resultCode === 0 && natsEvent.currentState) {
3102
+ await translateAndSpeak(
3103
+ natsEvent.currentState,
3104
+ userLang,
3105
+ undefined,
3106
+ false
3107
+ );
3108
+ setClickedStart(false);
3109
+ }
3110
+ } catch (e) {
3111
+ console.error('[EnterText] onClickStart: NATS wait failed', e);
3112
+ setMemoriTyping(false);
3113
+ setTypingText(undefined);
3114
+ }
3115
+ } else if (response.resultCode === 0) {
3116
+ console.error(
3117
+ '[EnterText] onClickStart: HTTP 200 but missing correlationID',
3118
+ response
3119
+ );
3120
+ }
2729
3121
  }
2730
3122
  }
2731
3123
  }
@@ -2736,16 +3128,23 @@ const MemoriWidget = ({
2736
3128
 
2737
3129
  // everything is fine, just translate dialog state and activate chat
2738
3130
  await translateAndSpeak(dialogState!, userLang);
3131
+ setClickedStart(false);
2739
3132
  }
2740
3133
  },
2741
3134
  [memoriPwd, memori, memoriTokens, birthDate, sessionId, userLang, position]
2742
3135
  );
2743
3136
 
2744
3137
  useEffect(() => {
2745
- if (!clickedStart && autoStart && selectedLayout !== 'HIDDEN_CHAT') {
3138
+ if (
3139
+ !clickedStart &&
3140
+ !sessionStartingRef.current &&
3141
+ !sessionId &&
3142
+ autoStart &&
3143
+ selectedLayout !== 'HIDDEN_CHAT'
3144
+ ) {
2746
3145
  onClickStart();
2747
3146
  }
2748
- }, [clickedStart, autoStart, selectedLayout]);
3147
+ }, [clickedStart, autoStart, selectedLayout, sessionId]);
2749
3148
 
2750
3149
  useEffect(() => {
2751
3150
  const targetNode =
@@ -2822,29 +3221,22 @@ const MemoriWidget = ({
2822
3221
  // check if owner has enough credits
2823
3222
  const needsCredits = tenant?.billingDelegation;
2824
3223
  const [hasEnoughCredits, setHasEnoughCredits] = useState<boolean>(true);
2825
- const handleNotEnoughCredits = useCallback(
2826
- (goBack = false) => {
2827
- setHasEnoughCredits(false);
2828
- setAuthModalState(null);
2829
- toast.error(t('notEnoughCredits'));
2830
-
2831
- if (goBack && window.history.length > 1) {
2832
- window.history.back();
2833
- }
2834
- },
2835
- [t]
2836
- );
3224
+ const handleNotEnoughCredits = useCallback(() => {
3225
+ setHasEnoughCredits(false);
3226
+ setAuthModalState(null);
3227
+ toast.error(t('notEnoughCredits'));
3228
+ }, [t]);
2837
3229
  const checkCredits = useCallback(
2838
- async (options?: { notify?: boolean; goBack?: boolean }) => {
3230
+ async (options?: { notify?: boolean }) => {
2839
3231
  if (!tenant?.billingDelegation) return true;
2840
3232
 
2841
3233
  // Billing delegation is active: credits MUST be verified.
2842
- // Without an ownerUserID we cannot call the API, so we fail closed
3234
+ // Without either owner identifier we cannot call the API, so we fail closed
2843
3235
  // instead of silently letting the session start unverified.
2844
- if (!ownerUserID) {
2845
- console.warn('Cannot verify credits: missing ownerUserID');
3236
+ if (!ownerUserID && !ownerUserName) {
3237
+ console.warn('Cannot verify credits: missing owner identifier');
2846
3238
  if (options?.notify) {
2847
- handleNotEnoughCredits(!!options.goBack);
3239
+ handleNotEnoughCredits();
2848
3240
  } else {
2849
3241
  setHasEnoughCredits(false);
2850
3242
  }
@@ -2858,6 +3250,7 @@ const MemoriWidget = ({
2858
3250
  : 'session_creation',
2859
3251
  baseUrl: baseUrl,
2860
3252
  userID: ownerUserID,
3253
+ userName: ownerUserName,
2861
3254
  tenant: tenantID,
2862
3255
  });
2863
3256
 
@@ -2867,7 +3260,7 @@ const MemoriWidget = ({
2867
3260
  } else {
2868
3261
  console.warn('Not enough credits. Required:', resp.required);
2869
3262
  if (options?.notify) {
2870
- handleNotEnoughCredits(!!options.goBack);
3263
+ handleNotEnoughCredits();
2871
3264
  } else {
2872
3265
  setHasEnoughCredits(false);
2873
3266
  }
@@ -2884,6 +3277,7 @@ const MemoriWidget = ({
2884
3277
  deepThoughtEnabled,
2885
3278
  handleNotEnoughCredits,
2886
3279
  ownerUserID,
3280
+ ownerUserName,
2887
3281
  tenant?.billingDelegation,
2888
3282
  tenantID,
2889
3283
  ]