@memori.ai/memori-react 8.39.0 → 8.40.0

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 +13 -0
  2. package/dist/components/MemoriWidget/MemoriWidget.d.ts +2 -1
  3. package/dist/components/MemoriWidget/MemoriWidget.js +357 -73
  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 +357 -73
  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 +475 -108
  42. package/src/components/layouts/layouts.stories.tsx +28 -34
  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,
@@ -626,6 +643,23 @@ const MemoriWidget = ({
626
643
  const [memoriTyping, setMemoriTyping] = useState<boolean>(false);
627
644
  const [typingText, setTypingText] = useState<string>();
628
645
 
646
+ type PendingEnterText = {
647
+ msg?: string;
648
+ typingText?: string;
649
+ useLoaderTextAsMsg?: boolean;
650
+ hasBatchQueued?: boolean;
651
+ natsTimeoutId?: ReturnType<typeof setTimeout>;
652
+ waitForResponse?: {
653
+ resolve: (event: NatsDialogResponseEvent) => void;
654
+ reject: (error: Error) => void;
655
+ timeoutId: ReturnType<typeof setTimeout>;
656
+ };
657
+ };
658
+ const pendingEnterTextRef = useRef<Map<string, PendingEnterText>>(new Map());
659
+ const bufferedNatsResponsesRef = useRef<Map<string, NatsDialogResponseEvent>>(
660
+ new Map()
661
+ );
662
+
629
663
  // Layout: from prop (string only) or integrationConfig. PII detection is only from integrationConfig (customData.layout as object with piiDetection).
630
664
  const layoutName =
631
665
  typeof layout === 'string'
@@ -783,7 +817,7 @@ const MemoriWidget = ({
783
817
  return Object.keys(place).length > 0 ? place : undefined;
784
818
  }, []);
785
819
 
786
- /** Place to send with postTextEnteredEvent: real place, nulls when no/declined position, or undefined when position not needed. */
820
+ /** Place to send with postEnterTextAsync: real place, nulls when no/declined position, or undefined when position not needed. */
787
821
  const getPlaceSpecForEnterText = useCallback(
788
822
  (venue: Venue | undefined) => {
789
823
  if (!memori.needsPosition) return undefined;
@@ -945,27 +979,18 @@ const MemoriWidget = ({
945
979
  : !!newSessionId,
946
980
  });
947
981
 
948
- // Show typing indicator
949
- setMemoriTyping(true);
950
- setTypingText(typingText);
951
-
982
+ // Show typing indicator after the async enter-text request is accepted (HTTP 200).
952
983
  let gotError = false;
953
984
 
954
985
  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
986
  const placeSpec = getPlaceSpecForEnterText(position);
968
- const { currentState, ...response } = await postTextEnteredEvent({
987
+ console.debug('[EnterText] sendMessage: posting', {
988
+ sessionId: sessionID,
989
+ textLength: msg.length,
990
+ hasBatchQueued,
991
+ typingText,
992
+ });
993
+ const response = await postEnterTextAsync({
969
994
  sessionId: sessionID,
970
995
  text: msg,
971
996
  ...(memori.needsDateTime && {
@@ -973,55 +998,33 @@ const MemoriWidget = ({
973
998
  }),
974
999
  ...(placeSpec !== undefined && { place: placeSpec }),
975
1000
  });
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
- }
1001
+ console.debug('[EnterText] sendMessage: HTTP response', {
1002
+ resultCode: response.resultCode,
1003
+ correlationID: readCorrelationID(response),
1004
+ resultMessage: response.resultMessage,
1005
+ });
1006
+ const correlationID = readCorrelationID(response);
1007
+ if (response.resultCode === 0 && correlationID) {
1008
+ registerPendingEnterText(correlationID, {
1009
+ msg,
1010
+ typingText,
1011
+ useLoaderTextAsMsg,
1012
+ hasBatchQueued,
1013
+ });
1014
+ console.info(
1015
+ '[EnterText] sendMessage: accepted, showing typing indicator',
1016
+ {
1017
+ correlationID: correlationID,
1018
+ typingText,
1023
1019
  }
1024
- }
1020
+ );
1021
+ setMemoriTyping(true);
1022
+ setTypingText(typingText);
1023
+ } else if (response.resultCode === 0) {
1024
+ console.error(
1025
+ '[EnterText] sendMessage: HTTP 200 but missing correlationID — cannot match NATS response',
1026
+ response
1027
+ );
1025
1028
  } else if (response.resultCode === 404) {
1026
1029
  // Handle expired session
1027
1030
  // remove last sent message, will set it as initial
@@ -1070,18 +1073,12 @@ const MemoriWidget = ({
1070
1073
  return Promise.reject(response);
1071
1074
  }
1072
1075
  } catch (error) {
1073
- console.log('error', error);
1074
- console.error(error);
1076
+ console.error('[EnterText] sendMessage: request failed', error);
1075
1077
  gotError = true;
1076
1078
 
1077
1079
  setTypingText(undefined);
1078
1080
  setMemoriTyping(false);
1079
1081
  }
1080
-
1081
- if (!hasBatchQueued) {
1082
- setTypingText(undefined);
1083
- setMemoriTyping(false);
1084
- }
1085
1082
  };
1086
1083
 
1087
1084
  /**
@@ -1388,7 +1385,7 @@ const MemoriWidget = ({
1388
1385
  return;
1389
1386
  }
1390
1387
 
1391
- if (!(await checkCredits({ notify: true, goBack: true }))) {
1388
+ if (!(await checkCredits({ notify: true }))) {
1392
1389
  return;
1393
1390
  }
1394
1391
 
@@ -1562,7 +1559,7 @@ const MemoriWidget = ({
1562
1559
  return;
1563
1560
  }
1564
1561
 
1565
- if (!(await checkCredits({ notify: true, goBack: true }))) {
1562
+ if (!(await checkCredits({ notify: true }))) {
1566
1563
  setLoading(false);
1567
1564
  return null;
1568
1565
  }
@@ -2046,6 +2043,341 @@ const MemoriWidget = ({
2046
2043
  ]
2047
2044
  );
2048
2045
 
2046
+ const processEnterTextDialogResponse = useCallback(
2047
+ (event: NatsDialogResponseEvent, pending: PendingEnterText) => {
2048
+ console.debug('[EnterText] processDialogResponse', {
2049
+ correlationID: event.correlationID,
2050
+ resultCode: event.resultCode,
2051
+ hasCurrentState: !!event.currentState,
2052
+ hasBatchQueued: pending.hasBatchQueued,
2053
+ });
2054
+ const {
2055
+ msg,
2056
+ typingText: pendingTypingText,
2057
+ useLoaderTextAsMsg,
2058
+ } = pending;
2059
+ const currentState = event.currentState;
2060
+
2061
+ if (event.resultCode !== 0 || !currentState) {
2062
+ if (event.resultCode === 500 && event.resultMessage) {
2063
+ console.warn('[EnterText] processDialogResponse: server error', {
2064
+ correlationID: event.correlationID,
2065
+ resultMessage: event.resultMessage,
2066
+ });
2067
+ setHistory(h => [
2068
+ ...h,
2069
+ {
2070
+ text: 'Error: ' + event.resultMessage,
2071
+ emitter: 'system',
2072
+ fromUser: false,
2073
+ initial: false,
2074
+ contextVars: {},
2075
+ date: new Date().toISOString(),
2076
+ },
2077
+ ]);
2078
+ } else if (event.resultCode !== 0) {
2079
+ console.warn('[SEND_MESSAGE/NATS]', event);
2080
+ }
2081
+ return;
2082
+ }
2083
+
2084
+ if (!msg) {
2085
+ console.debug(
2086
+ '[EnterText] processDialogResponse: no msg in pending, skipping'
2087
+ );
2088
+ return;
2089
+ }
2090
+
2091
+ setChatLogID(undefined);
2092
+ const emission =
2093
+ useLoaderTextAsMsg && pendingTypingText
2094
+ ? pendingTypingText
2095
+ : currentState.emission ?? currentDialogState?.emission;
2096
+
2097
+ console.debug('[EnterText] processDialogResponse: rendering emission', {
2098
+ correlationID: event.correlationID,
2099
+ emissionPreview: emission?.slice(0, 80),
2100
+ state: currentState.state,
2101
+ });
2102
+
2103
+ if (
2104
+ userLang.toLowerCase() !== language.toLowerCase() &&
2105
+ emission &&
2106
+ isMultilanguageEnabled
2107
+ ) {
2108
+ currentState.emission = emission;
2109
+
2110
+ translateDialogState(currentState, userLang, msg).then(ts => {
2111
+ const text = ts.translatedEmission || ts.emission;
2112
+ if (text && shouldPlayAudio(text)) {
2113
+ handleSpeak(text);
2114
+ }
2115
+ });
2116
+ } else {
2117
+ setCurrentDialogState({
2118
+ ...currentState,
2119
+ emission,
2120
+ });
2121
+
2122
+ if (emission) {
2123
+ pushMessage({
2124
+ text: emission,
2125
+ emitter: currentState.emitter,
2126
+ media: currentState.emittedMedia ?? currentState.media,
2127
+ llmUsage: (currentState as any).llmUsage,
2128
+ fromUser: false,
2129
+ questionAnswered: msg,
2130
+ generatedByAI: !!currentState.completion,
2131
+ contextVars: currentState.contextVars,
2132
+ date: currentState.currentDate,
2133
+ placeName: currentState.currentPlaceName,
2134
+ placeLatitude: currentState.currentLatitude,
2135
+ placeLongitude: currentState.currentLongitude,
2136
+ placeUncertaintyKm: currentState.currentUncertaintyKm,
2137
+ tag: currentState.currentTag,
2138
+ memoryTags: currentState.memoryTags,
2139
+ } as any);
2140
+ if (emission && shouldPlayAudio(emission)) {
2141
+ handleSpeak(emission);
2142
+ }
2143
+ }
2144
+ }
2145
+ },
2146
+ [
2147
+ userLang,
2148
+ language,
2149
+ isMultilanguageEnabled,
2150
+ currentDialogState?.emission,
2151
+ translateDialogState,
2152
+ handleSpeak,
2153
+ shouldPlayAudio,
2154
+ ]
2155
+ );
2156
+
2157
+ const clearEnterTextPending = useCallback(
2158
+ (correlationID: string, pending: PendingEnterText) => {
2159
+ if (pending.natsTimeoutId) {
2160
+ clearTimeout(pending.natsTimeoutId);
2161
+ }
2162
+ if (pending.waitForResponse?.timeoutId) {
2163
+ clearTimeout(pending.waitForResponse.timeoutId);
2164
+ }
2165
+ pendingEnterTextRef.current.delete(correlationID);
2166
+ },
2167
+ []
2168
+ );
2169
+
2170
+ const deliverEnterTextNatsError = useCallback(
2171
+ (event: NatsErrorEvent) => {
2172
+ const correlationID = event.correlationID;
2173
+ const errorText = event.errorMessage
2174
+ ? `Error: ${event.errorMessage}`
2175
+ : event.errorCode
2176
+ ? `Error: ${event.errorCode}`
2177
+ : 'Error: An unexpected error occurred';
2178
+
2179
+ console.error('[EnterText] NATS error event', {
2180
+ correlationID,
2181
+ errorCode: event.errorCode,
2182
+ errorMessage: event.errorMessage,
2183
+ });
2184
+
2185
+ pushMessage({
2186
+ text: errorText,
2187
+ emitter: 'system',
2188
+ fromUser: false,
2189
+ initial: false,
2190
+ contextVars: {},
2191
+ date: new Date().toISOString(),
2192
+ });
2193
+
2194
+ if (correlationID) {
2195
+ const pending = pendingEnterTextRef.current.get(correlationID);
2196
+ if (pending) {
2197
+ clearEnterTextPending(correlationID, pending);
2198
+ pending.waitForResponse?.reject(
2199
+ new Error(event.errorMessage ?? String(event.errorCode ?? 'NATS error'))
2200
+ );
2201
+ }
2202
+ }
2203
+
2204
+ setMemoriTyping(false);
2205
+ setTypingText(undefined);
2206
+ },
2207
+ [clearEnterTextPending]
2208
+ );
2209
+
2210
+ const deliverEnterTextNatsResponse = useCallback(
2211
+ (correlationID: string, event: NatsDialogResponseEvent) => {
2212
+ const pending = pendingEnterTextRef.current.get(correlationID);
2213
+ if (!pending) {
2214
+ const pendingCorrelationIDs = [...pendingEnterTextRef.current.keys()];
2215
+ console.warn(
2216
+ '[EnterText] NATS response buffered (no matching pending)',
2217
+ {
2218
+ receivedCorrelationID: correlationID,
2219
+ resultCode: event.resultCode,
2220
+ pendingCorrelationIDs,
2221
+ hint:
2222
+ pendingCorrelationIDs.length > 0
2223
+ ? 'Use one of pendingCorrelationIDs in your nats pub correlation_id'
2224
+ : 'Send a message in the widget first, then copy correlationID from HTTP response logs',
2225
+ }
2226
+ );
2227
+ bufferedNatsResponsesRef.current.set(correlationID, event);
2228
+ return;
2229
+ }
2230
+
2231
+ clearEnterTextPending(correlationID, pending);
2232
+
2233
+ if (pending.waitForResponse) {
2234
+ console.info('[EnterText] NATS response delivered to waiter', {
2235
+ correlationID,
2236
+ resultCode: event.resultCode,
2237
+ });
2238
+ pending.waitForResponse.resolve(event);
2239
+ setMemoriTyping(false);
2240
+ setTypingText(undefined);
2241
+ return;
2242
+ }
2243
+
2244
+ processEnterTextDialogResponse(event, pending);
2245
+
2246
+ if (!pending.hasBatchQueued) {
2247
+ console.info('[EnterText] typing indicator cleared', { correlationID });
2248
+ setMemoriTyping(false);
2249
+ setTypingText(undefined);
2250
+ } else {
2251
+ console.debug('[EnterText] typing kept (batch queued)', {
2252
+ correlationID,
2253
+ });
2254
+ }
2255
+ },
2256
+ [processEnterTextDialogResponse, clearEnterTextPending]
2257
+ );
2258
+
2259
+ const registerPendingEnterText = useCallback(
2260
+ (correlationID: string, pending: PendingEnterText) => {
2261
+ const buffered = bufferedNatsResponsesRef.current.get(correlationID);
2262
+ if (buffered) {
2263
+ console.info('[EnterText] replaying buffered NATS response', {
2264
+ correlationID,
2265
+ waitForResponse: !!pending.waitForResponse,
2266
+ });
2267
+ bufferedNatsResponsesRef.current.delete(correlationID);
2268
+ pendingEnterTextRef.current.set(correlationID, pending);
2269
+ deliverEnterTextNatsResponse(correlationID, buffered);
2270
+ return;
2271
+ }
2272
+
2273
+ if (!pending.waitForResponse && !pending.natsTimeoutId) {
2274
+ pending.natsTimeoutId = setTimeout(() => {
2275
+ const current = pendingEnterTextRef.current.get(correlationID);
2276
+ if (!current) return;
2277
+ clearEnterTextPending(correlationID, current);
2278
+ console.error('[EnterText] NATS response timeout', {
2279
+ correlationID,
2280
+ timeoutMs: ENTER_TEXT_NATS_TIMEOUT_MS,
2281
+ });
2282
+ if (!current.hasBatchQueued) {
2283
+ setMemoriTyping(false);
2284
+ setTypingText(undefined);
2285
+ }
2286
+ current.waitForResponse?.reject(
2287
+ new Error('NATS enter-text response timeout')
2288
+ );
2289
+ }, ENTER_TEXT_NATS_TIMEOUT_MS);
2290
+ }
2291
+
2292
+ console.debug('[EnterText] pending registered', {
2293
+ correlationID,
2294
+ waitForResponse: !!pending.waitForResponse,
2295
+ hasBatchQueued: pending.hasBatchQueued,
2296
+ });
2297
+ pendingEnterTextRef.current.set(correlationID, pending);
2298
+ },
2299
+ [deliverEnterTextNatsResponse, clearEnterTextPending]
2300
+ );
2301
+
2302
+ const waitForEnterTextNatsResponse = useCallback(
2303
+ (correlationID: string, timeoutMs = 120000) =>
2304
+ new Promise<NatsDialogResponseEvent>((resolve, reject) => {
2305
+ console.debug('[EnterText] waiting for NATS response', {
2306
+ correlationID,
2307
+ timeoutMs,
2308
+ });
2309
+ const timeoutId = setTimeout(() => {
2310
+ const current = pendingEnterTextRef.current.get(correlationID);
2311
+ if (current) {
2312
+ clearEnterTextPending(correlationID, current);
2313
+ }
2314
+ console.error('[EnterText] NATS response timeout', {
2315
+ correlationID,
2316
+ timeoutMs,
2317
+ });
2318
+ reject(new Error('NATS enter-text response timeout'));
2319
+ }, timeoutMs);
2320
+
2321
+ registerPendingEnterText(correlationID, {
2322
+ waitForResponse: {
2323
+ resolve: event => {
2324
+ clearTimeout(timeoutId);
2325
+ resolve(event);
2326
+ },
2327
+ reject: error => {
2328
+ clearTimeout(timeoutId);
2329
+ reject(error);
2330
+ },
2331
+ timeoutId,
2332
+ },
2333
+ });
2334
+ }),
2335
+ [registerPendingEnterText, clearEnterTextPending]
2336
+ );
2337
+
2338
+ // NATS subscription: receives progress updates and the async enter-text response.
2339
+ useNats({
2340
+ baseUrl,
2341
+ sessionId,
2342
+ onProgress: useCallback((event: NatsProgressEvent) => {
2343
+ console.debug('[EnterText] NATS progress', {
2344
+ correlationID: event.correlationID,
2345
+ step: event.currentStep,
2346
+ finalStep: event.finalStep,
2347
+ message: event.message,
2348
+ });
2349
+ if (event.message) {
2350
+ setTypingText(event.message);
2351
+ }
2352
+ }, []),
2353
+ onDialogResponse: useCallback(
2354
+ (event: NatsDialogResponseEvent) => {
2355
+ const correlationID = event.correlationID;
2356
+ console.debug(
2357
+ '[EnterText] NATS dialog.text_entered_response received',
2358
+ {
2359
+ correlationID,
2360
+ resultCode: event.resultCode,
2361
+ requestID: event.requestID,
2362
+ }
2363
+ );
2364
+ if (!correlationID) {
2365
+ console.warn(
2366
+ '[EnterText] dialog_text_entered_response without correlationID',
2367
+ event
2368
+ );
2369
+ setMemoriTyping(false);
2370
+ setTypingText(undefined);
2371
+ return;
2372
+ }
2373
+
2374
+ deliverEnterTextNatsResponse(correlationID, event);
2375
+ },
2376
+ [deliverEnterTextNatsResponse]
2377
+ ),
2378
+ onError: deliverEnterTextNatsError,
2379
+ });
2380
+
2049
2381
  const focusChatInput = () => {
2050
2382
  let textarea = document.querySelector(
2051
2383
  '#chat-fieldset textarea'
@@ -2373,7 +2705,7 @@ const MemoriWidget = ({
2373
2705
  return;
2374
2706
  }
2375
2707
 
2376
- if (!(await checkCredits({ notify: true, goBack: true }))) {
2708
+ if (!(await checkCredits({ notify: true }))) {
2377
2709
  setClickedStart(false);
2378
2710
  setLoading(false);
2379
2711
  return;
@@ -2690,11 +3022,15 @@ const MemoriWidget = ({
2690
3022
  translatedMessages = [];
2691
3023
  setHistory([]);
2692
3024
 
2693
- setMemoriTyping(true);
2694
-
2695
3025
  // we have no chat history, we start by initial question
2696
3026
  const placeSpec = getPlaceSpecForEnterText(position);
2697
- const response = await postTextEnteredEvent({
3027
+ console.debug(
3028
+ '[EnterText] onClickStart: posting initial question',
3029
+ {
3030
+ sessionId: sessionID,
3031
+ }
3032
+ );
3033
+ const response = await postEnterTextAsync({
2698
3034
  sessionId: sessionID!,
2699
3035
  text: initialQuestion,
2700
3036
  ...(memori.needsDateTime && {
@@ -2702,8 +3038,12 @@ const MemoriWidget = ({
2702
3038
  }),
2703
3039
  ...(placeSpec !== undefined && { place: placeSpec }),
2704
3040
  });
3041
+ console.debug('[EnterText] onClickStart: HTTP response', {
3042
+ resultCode: response.resultCode,
3043
+ correlationID: readCorrelationID(response),
3044
+ });
2705
3045
 
2706
- // Handle 500 error from TextEnteredEvent
3046
+ // Handle 500 error from EnterTextAsync
2707
3047
  if (response.resultCode === 500 && response.resultMessage) {
2708
3048
  setHistory(h => [
2709
3049
  ...h,
@@ -2716,16 +3056,48 @@ const MemoriWidget = ({
2716
3056
  date: new Date().toISOString(),
2717
3057
  },
2718
3058
  ]);
2719
- setMemoriTyping(false);
2720
3059
  return;
2721
3060
  }
2722
3061
 
2723
- await translateAndSpeak(
2724
- response.currentState ?? currentState,
2725
- userLang,
2726
- undefined,
2727
- false
2728
- );
3062
+ const onClickStartCorrelationID = readCorrelationID(response);
3063
+ if (response.resultCode === 0 && onClickStartCorrelationID) {
3064
+ console.info(
3065
+ '[EnterText] onClickStart: accepted, showing typing indicator',
3066
+ {
3067
+ correlationID: onClickStartCorrelationID,
3068
+ }
3069
+ );
3070
+ setMemoriTyping(true);
3071
+ try {
3072
+ const natsEvent = await waitForEnterTextNatsResponse(
3073
+ onClickStartCorrelationID
3074
+ );
3075
+ console.info(
3076
+ '[EnterText] onClickStart: NATS response received',
3077
+ {
3078
+ correlationID: onClickStartCorrelationID,
3079
+ resultCode: natsEvent.resultCode,
3080
+ }
3081
+ );
3082
+ if (natsEvent.resultCode === 0 && natsEvent.currentState) {
3083
+ await translateAndSpeak(
3084
+ natsEvent.currentState,
3085
+ userLang,
3086
+ undefined,
3087
+ false
3088
+ );
3089
+ }
3090
+ } catch (e) {
3091
+ console.error('[EnterText] onClickStart: NATS wait failed', e);
3092
+ setMemoriTyping(false);
3093
+ setTypingText(undefined);
3094
+ }
3095
+ } else if (response.resultCode === 0) {
3096
+ console.error(
3097
+ '[EnterText] onClickStart: HTTP 200 but missing correlationID',
3098
+ response
3099
+ );
3100
+ }
2729
3101
  }
2730
3102
  }
2731
3103
  }
@@ -2822,29 +3194,22 @@ const MemoriWidget = ({
2822
3194
  // check if owner has enough credits
2823
3195
  const needsCredits = tenant?.billingDelegation;
2824
3196
  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
- );
3197
+ const handleNotEnoughCredits = useCallback(() => {
3198
+ setHasEnoughCredits(false);
3199
+ setAuthModalState(null);
3200
+ toast.error(t('notEnoughCredits'));
3201
+ }, [t]);
2837
3202
  const checkCredits = useCallback(
2838
- async (options?: { notify?: boolean; goBack?: boolean }) => {
3203
+ async (options?: { notify?: boolean }) => {
2839
3204
  if (!tenant?.billingDelegation) return true;
2840
3205
 
2841
3206
  // Billing delegation is active: credits MUST be verified.
2842
- // Without an ownerUserID we cannot call the API, so we fail closed
3207
+ // Without either owner identifier we cannot call the API, so we fail closed
2843
3208
  // instead of silently letting the session start unverified.
2844
- if (!ownerUserID) {
2845
- console.warn('Cannot verify credits: missing ownerUserID');
3209
+ if (!ownerUserID && !ownerUserName) {
3210
+ console.warn('Cannot verify credits: missing owner identifier');
2846
3211
  if (options?.notify) {
2847
- handleNotEnoughCredits(!!options.goBack);
3212
+ handleNotEnoughCredits();
2848
3213
  } else {
2849
3214
  setHasEnoughCredits(false);
2850
3215
  }
@@ -2858,6 +3223,7 @@ const MemoriWidget = ({
2858
3223
  : 'session_creation',
2859
3224
  baseUrl: baseUrl,
2860
3225
  userID: ownerUserID,
3226
+ userName: ownerUserName,
2861
3227
  tenant: tenantID,
2862
3228
  });
2863
3229
 
@@ -2867,7 +3233,7 @@ const MemoriWidget = ({
2867
3233
  } else {
2868
3234
  console.warn('Not enough credits. Required:', resp.required);
2869
3235
  if (options?.notify) {
2870
- handleNotEnoughCredits(!!options.goBack);
3236
+ handleNotEnoughCredits();
2871
3237
  } else {
2872
3238
  setHasEnoughCredits(false);
2873
3239
  }
@@ -2884,6 +3250,7 @@ const MemoriWidget = ({
2884
3250
  deepThoughtEnabled,
2885
3251
  handleNotEnoughCredits,
2886
3252
  ownerUserID,
3253
+ ownerUserName,
2887
3254
  tenant?.billingDelegation,
2888
3255
  tenantID,
2889
3256
  ]