@memori.ai/memori-react 8.38.8 → 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 (45) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/components/MemoriWidget/MemoriWidget.js +401 -87
  3. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  4. package/dist/helpers/credits.d.ts +5 -2
  5. package/dist/helpers/credits.js +5 -1
  6. package/dist/helpers/credits.js.map +1 -1
  7. package/dist/helpers/nats/getNatsConfig.d.ts +5 -0
  8. package/dist/helpers/nats/getNatsConfig.js +29 -0
  9. package/dist/helpers/nats/getNatsConfig.js.map +1 -0
  10. package/dist/helpers/nats/useNats.d.ts +12 -0
  11. package/dist/helpers/nats/useNats.js +72 -0
  12. package/dist/helpers/nats/useNats.js.map +1 -0
  13. package/dist/helpers/nats/useNatsSession.d.ts +27 -0
  14. package/dist/helpers/nats/useNatsSession.js +108 -0
  15. package/dist/helpers/nats/useNatsSession.js.map +1 -0
  16. package/dist/version.d.ts +1 -1
  17. package/dist/version.js +1 -1
  18. package/esm/components/MemoriWidget/MemoriWidget.js +401 -87
  19. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  20. package/esm/helpers/credits.d.ts +5 -2
  21. package/esm/helpers/credits.js +5 -1
  22. package/esm/helpers/credits.js.map +1 -1
  23. package/esm/helpers/nats/getNatsConfig.d.ts +5 -0
  24. package/esm/helpers/nats/getNatsConfig.js +25 -0
  25. package/esm/helpers/nats/getNatsConfig.js.map +1 -0
  26. package/esm/helpers/nats/useNats.d.ts +12 -0
  27. package/esm/helpers/nats/useNats.js +68 -0
  28. package/esm/helpers/nats/useNats.js.map +1 -0
  29. package/esm/helpers/nats/useNatsSession.d.ts +27 -0
  30. package/esm/helpers/nats/useNatsSession.js +103 -0
  31. package/esm/helpers/nats/useNatsSession.js.map +1 -0
  32. package/esm/version.d.ts +1 -1
  33. package/esm/version.js +1 -1
  34. package/package.json +3 -2
  35. package/src/components/MemoriWidget/MemoriWidget.tsx +546 -140
  36. package/src/components/StartPanel/StartPanel.stories.tsx +21 -0
  37. package/src/components/StartPanel/StartPanel.test.tsx +66 -1
  38. package/src/components/StartPanel/__snapshots__/StartPanel.test.tsx.snap +156 -0
  39. package/src/components/layouts/layouts.stories.tsx +28 -34
  40. package/src/helpers/credits.ts +16 -1
  41. package/src/helpers/nats/getNatsConfig.ts +69 -0
  42. package/src/helpers/nats/useNats.ts +122 -0
  43. package/src/helpers/nats/useNatsSession.ts +210 -0
  44. package/src/index.stories.tsx +19 -3
  45. 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;
@@ -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,7 +582,6 @@ const MemoriWidget = ({
565
582
  const [showLoginDrawer, setShowLoginDrawer] = useState(false);
566
583
 
567
584
  const [clickedStart, setClickedStart] = useState(false);
568
- const [gotErrorInOpening, setGotErrorInOpening] = useState(false);
569
585
 
570
586
  const language =
571
587
  memori.culture?.split('-')?.[0]?.toUpperCase()! ||
@@ -627,6 +643,23 @@ const MemoriWidget = ({
627
643
  const [memoriTyping, setMemoriTyping] = useState<boolean>(false);
628
644
  const [typingText, setTypingText] = useState<string>();
629
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
+
630
663
  // Layout: from prop (string only) or integrationConfig. PII detection is only from integrationConfig (customData.layout as object with piiDetection).
631
664
  const layoutName =
632
665
  typeof layout === 'string'
@@ -707,7 +740,9 @@ const MemoriWidget = ({
707
740
  setRuntimeShowMessageConsumption(
708
741
  getLocalConfig(
709
742
  'showMessageConsumption',
710
- showMessageConsumption ?? integrationConfig?.showMessageConsumption ?? false
743
+ showMessageConsumption ??
744
+ integrationConfig?.showMessageConsumption ??
745
+ false
711
746
  )
712
747
  );
713
748
 
@@ -770,17 +805,11 @@ const MemoriWidget = ({
770
805
  longitude?: number;
771
806
  uncertaintyKm?: number;
772
807
  } = {};
773
- if (
774
- venue.latitude != null &&
775
- venue.longitude != null
776
- ) {
808
+ if (venue.latitude != null && venue.longitude != null) {
777
809
  place.latitude = venue.latitude;
778
810
  place.longitude = venue.longitude;
779
811
  if (venue.placeName) place.placeName = venue.placeName;
780
- if (
781
- venue.uncertainty != null &&
782
- venue.uncertainty > 0
783
- )
812
+ if (venue.uncertainty != null && venue.uncertainty > 0)
784
813
  place.uncertaintyKm = venue.uncertainty;
785
814
  } else if (venue.placeName) {
786
815
  place.placeName = venue.placeName;
@@ -788,7 +817,7 @@ const MemoriWidget = ({
788
817
  return Object.keys(place).length > 0 ? place : undefined;
789
818
  }, []);
790
819
 
791
- /** 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. */
792
821
  const getPlaceSpecForEnterText = useCallback(
793
822
  (venue: Venue | undefined) => {
794
823
  if (!memori.needsPosition) return undefined;
@@ -950,27 +979,18 @@ const MemoriWidget = ({
950
979
  : !!newSessionId,
951
980
  });
952
981
 
953
- // Show typing indicator
954
- setMemoriTyping(true);
955
- setTypingText(typingText);
956
-
982
+ // Show typing indicator after the async enter-text request is accepted (HTTP 200).
957
983
  let gotError = false;
958
984
 
959
985
  try {
960
- // Add chat reference link to the message if it exists
961
- // if (chatLogID) {
962
- // msg =
963
- // msg +
964
- // ' \n\n' +
965
- // '<chat-reference session-id="' +
966
- // sessionID +
967
- // '" event-log-id="' +
968
- // chatLogID +
969
- // '"></chat-reference>';
970
- // }
971
-
972
986
  const placeSpec = getPlaceSpecForEnterText(position);
973
- 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({
974
994
  sessionId: sessionID,
975
995
  text: msg,
976
996
  ...(memori.needsDateTime && {
@@ -978,55 +998,33 @@ const MemoriWidget = ({
978
998
  }),
979
999
  ...(placeSpec !== undefined && { place: placeSpec }),
980
1000
  });
981
- if (response.resultCode === 0 && currentState) {
982
- setChatLogID(undefined);
983
- const emission =
984
- useLoaderTextAsMsg && typingText
985
- ? typingText
986
- : currentState.emission ?? currentDialogState?.emission;
987
-
988
- if (
989
- userLang.toLowerCase() !== language.toLowerCase() &&
990
- emission &&
991
- isMultilanguageEnabled
992
- ) {
993
- currentState.emission = emission;
994
-
995
- translateDialogState(currentState, userLang, msg).then(ts => {
996
- let text = ts.translatedEmission || ts.emission;
997
- if (text && shouldPlayAudio(text)) {
998
- handleSpeak(text);
999
- }
1000
- });
1001
- } else {
1002
- setCurrentDialogState({
1003
- ...currentState,
1004
- emission,
1005
- });
1006
-
1007
- if (emission) {
1008
- pushMessage({
1009
- text: emission,
1010
- emitter: currentState.emitter,
1011
- media: currentState.emittedMedia ?? currentState.media,
1012
- llmUsage: (currentState as any).llmUsage,
1013
- fromUser: false,
1014
- questionAnswered: msg,
1015
- generatedByAI: !!currentState.completion,
1016
- contextVars: currentState.contextVars,
1017
- date: currentState.currentDate,
1018
- placeName: currentState.currentPlaceName,
1019
- placeLatitude: currentState.currentLatitude,
1020
- placeLongitude: currentState.currentLongitude,
1021
- placeUncertaintyKm: currentState.currentUncertaintyKm,
1022
- tag: currentState.currentTag,
1023
- memoryTags: currentState.memoryTags,
1024
- } as any);
1025
- if (emission && shouldPlayAudio(emission)) {
1026
- handleSpeak(emission);
1027
- }
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,
1028
1019
  }
1029
- }
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
+ );
1030
1028
  } else if (response.resultCode === 404) {
1031
1029
  // Handle expired session
1032
1030
  // remove last sent message, will set it as initial
@@ -1075,18 +1073,12 @@ const MemoriWidget = ({
1075
1073
  return Promise.reject(response);
1076
1074
  }
1077
1075
  } catch (error) {
1078
- console.log('error', error);
1079
- console.error(error);
1076
+ console.error('[EnterText] sendMessage: request failed', error);
1080
1077
  gotError = true;
1081
1078
 
1082
1079
  setTypingText(undefined);
1083
1080
  setMemoriTyping(false);
1084
1081
  }
1085
-
1086
- if (!hasBatchQueued) {
1087
- setTypingText(undefined);
1088
- setMemoriTyping(false);
1089
- }
1090
1082
  };
1091
1083
 
1092
1084
  /**
@@ -1393,6 +1385,10 @@ const MemoriWidget = ({
1393
1385
  return;
1394
1386
  }
1395
1387
 
1388
+ if (!(await checkCredits({ notify: true }))) {
1389
+ return;
1390
+ }
1391
+
1396
1392
  setLoading(true);
1397
1393
 
1398
1394
  try {
@@ -1469,10 +1465,9 @@ const MemoriWidget = ({
1469
1465
  ) {
1470
1466
  console.warn(session);
1471
1467
  toast.error(t('underageTwinSession', { age: minAge }));
1472
- setGotErrorInOpening(true);
1473
1468
  }
1474
1469
  // Handle authentication error
1475
- else if (session?.resultCode === 403) {
1470
+ else if (session?.resultCode === 403 && memori.privacyType !== 'PUBLIC') {
1476
1471
  setMemoriPwd(undefined);
1477
1472
  setAuthModalState('password');
1478
1473
  return session;
@@ -1498,7 +1493,6 @@ const MemoriWidget = ({
1498
1493
  duration: Infinity,
1499
1494
  }
1500
1495
  );
1501
- setGotErrorInOpening(true);
1502
1496
  return session;
1503
1497
  }
1504
1498
  } catch (err) {
@@ -1565,6 +1559,11 @@ const MemoriWidget = ({
1565
1559
  return;
1566
1560
  }
1567
1561
 
1562
+ if (!(await checkCredits({ notify: true }))) {
1563
+ setLoading(false);
1564
+ return null;
1565
+ }
1566
+
1568
1567
  // Get current URL as referral
1569
1568
  let referral;
1570
1569
  try {
@@ -1692,10 +1691,12 @@ const MemoriWidget = ({
1692
1691
  ) {
1693
1692
  console.error('[REOPEN_SESSION] Age restriction error:', response);
1694
1693
  toast.error(t('underageTwinSession', { age: minAge }));
1695
- setGotErrorInOpening(true);
1696
1694
  }
1697
1695
  // Handle authentication error
1698
- else if (response?.resultCode === 403) {
1696
+ else if (
1697
+ response?.resultCode === 403 &&
1698
+ memori.privacyType !== 'PUBLIC'
1699
+ ) {
1699
1700
  console.error('[REOPEN_SESSION] Authentication error');
1700
1701
  setMemoriPwd(undefined);
1701
1702
  setAuthModalState('password');
@@ -1704,7 +1705,6 @@ const MemoriWidget = ({
1704
1705
  else {
1705
1706
  console.error('[REOPEN_SESSION] Other error:', response);
1706
1707
  toast.error(t(getErrori18nKey(response.resultCode)));
1707
- setGotErrorInOpening(true);
1708
1708
  }
1709
1709
  } catch (err) {
1710
1710
  console.error('[REOPEN_SESSION] Caught error:', err);
@@ -2043,6 +2043,341 @@ const MemoriWidget = ({
2043
2043
  ]
2044
2044
  );
2045
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
+
2046
2381
  const focusChatInput = () => {
2047
2382
  let textarea = document.querySelector(
2048
2383
  '#chat-fieldset textarea'
@@ -2370,6 +2705,12 @@ const MemoriWidget = ({
2370
2705
  return;
2371
2706
  }
2372
2707
 
2708
+ if (!(await checkCredits({ notify: true }))) {
2709
+ setClickedStart(false);
2710
+ setLoading(false);
2711
+ return;
2712
+ }
2713
+
2373
2714
  // Handle age verification
2374
2715
  if (!sessionID && !!minAge && !birth) {
2375
2716
  setShowAgeVerification(true);
@@ -2377,12 +2718,11 @@ const MemoriWidget = ({
2377
2718
  }
2378
2719
  // Handle authentication
2379
2720
  else if (
2380
- (!sessionID &&
2381
- memori.privacyType !== 'PUBLIC' &&
2382
- !memori.secretToken &&
2383
- !memoriPwd &&
2384
- !memoriTokens) ||
2385
- (!sessionID && gotErrorInOpening)
2721
+ !sessionID &&
2722
+ memori.privacyType !== 'PUBLIC' &&
2723
+ !memori.secretToken &&
2724
+ !memoriPwd &&
2725
+ !memoriTokens
2386
2726
  ) {
2387
2727
  setAuthModalState('password');
2388
2728
  setClickedStart(false);
@@ -2391,7 +2731,6 @@ const MemoriWidget = ({
2391
2731
  // Create new session if needed
2392
2732
  else if (!sessionID || initialSessionExpired) {
2393
2733
  setClickedStart(false);
2394
- setGotErrorInOpening(false);
2395
2734
  const session = await fetchSession({
2396
2735
  memoriID: memori.engineMemoriID!,
2397
2736
  password: secret || memoriPwd || memori.secretToken,
@@ -2513,7 +2852,6 @@ const MemoriWidget = ({
2513
2852
 
2514
2853
  if (response.resultCode !== 0 || !currentState) {
2515
2854
  const { chatLogs } = await getSessionChatLogs(sessionID!, sessionID!);
2516
- setGotErrorInOpening(true);
2517
2855
  setSessionId(undefined);
2518
2856
  setClickedStart(false);
2519
2857
  await onClickStart(undefined, true, chatLogs?.[0]);
@@ -2684,11 +3022,15 @@ const MemoriWidget = ({
2684
3022
  translatedMessages = [];
2685
3023
  setHistory([]);
2686
3024
 
2687
- setMemoriTyping(true);
2688
-
2689
3025
  // we have no chat history, we start by initial question
2690
3026
  const placeSpec = getPlaceSpecForEnterText(position);
2691
- const response = await postTextEnteredEvent({
3027
+ console.debug(
3028
+ '[EnterText] onClickStart: posting initial question',
3029
+ {
3030
+ sessionId: sessionID,
3031
+ }
3032
+ );
3033
+ const response = await postEnterTextAsync({
2692
3034
  sessionId: sessionID!,
2693
3035
  text: initialQuestion,
2694
3036
  ...(memori.needsDateTime && {
@@ -2696,8 +3038,12 @@ const MemoriWidget = ({
2696
3038
  }),
2697
3039
  ...(placeSpec !== undefined && { place: placeSpec }),
2698
3040
  });
3041
+ console.debug('[EnterText] onClickStart: HTTP response', {
3042
+ resultCode: response.resultCode,
3043
+ correlationID: readCorrelationID(response),
3044
+ });
2699
3045
 
2700
- // Handle 500 error from TextEnteredEvent
3046
+ // Handle 500 error from EnterTextAsync
2701
3047
  if (response.resultCode === 500 && response.resultMessage) {
2702
3048
  setHistory(h => [
2703
3049
  ...h,
@@ -2710,19 +3056,50 @@ const MemoriWidget = ({
2710
3056
  date: new Date().toISOString(),
2711
3057
  },
2712
3058
  ]);
2713
- setMemoriTyping(false);
2714
3059
  return;
2715
3060
  }
2716
3061
 
2717
- await translateAndSpeak(
2718
- response.currentState ?? currentState,
2719
- userLang,
2720
- undefined,
2721
- false
2722
- );
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
+ }
2723
3101
  }
2724
3102
  }
2725
-
2726
3103
  }
2727
3104
  // Default case - just translate and activate
2728
3105
  else {
@@ -2817,36 +3194,72 @@ const MemoriWidget = ({
2817
3194
  // check if owner has enough credits
2818
3195
  const needsCredits = tenant?.billingDelegation;
2819
3196
  const [hasEnoughCredits, setHasEnoughCredits] = useState<boolean>(true);
2820
- const checkCredits = useCallback(async () => {
2821
- if (!tenant?.billingDelegation) return;
3197
+ const handleNotEnoughCredits = useCallback(() => {
3198
+ setHasEnoughCredits(false);
3199
+ setAuthModalState(null);
3200
+ toast.error(t('notEnoughCredits'));
3201
+ }, [t]);
3202
+ const checkCredits = useCallback(
3203
+ async (options?: { notify?: boolean }) => {
3204
+ if (!tenant?.billingDelegation) return true;
3205
+
3206
+ // Billing delegation is active: credits MUST be verified.
3207
+ // Without either owner identifier we cannot call the API, so we fail closed
3208
+ // instead of silently letting the session start unverified.
3209
+ if (!ownerUserID && !ownerUserName) {
3210
+ console.warn('Cannot verify credits: missing owner identifier');
3211
+ if (options?.notify) {
3212
+ handleNotEnoughCredits();
3213
+ } else {
3214
+ setHasEnoughCredits(false);
3215
+ }
3216
+ return false;
3217
+ }
2822
3218
 
2823
- try {
2824
- const resp = await getCredits({
2825
- operation: deepThoughtEnabled
2826
- ? 'dt_session_creation'
2827
- : 'session_creation',
2828
- baseUrl: baseUrl,
2829
- userID: ownerUserID,
2830
- userName: ownerUserName,
2831
- tenant: tenantID,
2832
- });
3219
+ try {
3220
+ const resp = await getCredits({
3221
+ operation: deepThoughtEnabled
3222
+ ? 'dt_session_creation'
3223
+ : 'session_creation',
3224
+ baseUrl: baseUrl,
3225
+ userID: ownerUserID,
3226
+ userName: ownerUserName,
3227
+ tenant: tenantID,
3228
+ });
2833
3229
 
2834
- if (resp.enough) {
2835
- setHasEnoughCredits(true);
2836
- } else {
2837
- setHasEnoughCredits(false);
2838
- console.warn('Not enough credits. Required:', resp.required);
3230
+ if (resp.enough) {
3231
+ setHasEnoughCredits(true);
3232
+ return true;
3233
+ } else {
3234
+ console.warn('Not enough credits. Required:', resp.required);
3235
+ if (options?.notify) {
3236
+ handleNotEnoughCredits();
3237
+ } else {
3238
+ setHasEnoughCredits(false);
3239
+ }
3240
+ return false;
3241
+ }
3242
+ } catch (e) {
3243
+ let err = e as Error;
3244
+ console.debug(err);
3245
+ return true;
2839
3246
  }
2840
- } catch (e) {
2841
- let err = e as Error;
2842
- console.debug(err);
2843
- }
2844
- }, [tenant?.billingDelegation, deepThoughtEnabled]);
3247
+ },
3248
+ [
3249
+ baseUrl,
3250
+ deepThoughtEnabled,
3251
+ handleNotEnoughCredits,
3252
+ ownerUserID,
3253
+ ownerUserName,
3254
+ tenant?.billingDelegation,
3255
+ tenantID,
3256
+ ]
3257
+ );
2845
3258
  useEffect(() => {
2846
3259
  if (tenant?.billingDelegation) {
2847
3260
  checkCredits();
2848
3261
  }
2849
- }, [tenant?.billingDelegation, deepThoughtEnabled]);
3262
+ }, [tenant?.billingDelegation, deepThoughtEnabled, checkCredits]);
2850
3263
 
2851
3264
  useEffect(() => {
2852
3265
  if (__WEBCOMPONENT__) return;
@@ -3203,12 +3616,6 @@ const MemoriWidget = ({
3203
3616
  }
3204
3617
  })
3205
3618
  .catch(error => {
3206
- if (
3207
- !(error instanceof Error) ||
3208
- error.message !== 'AUTH_FAILED'
3209
- ) {
3210
- setGotErrorInOpening(true);
3211
- }
3212
3619
  throw error;
3213
3620
  });
3214
3621
  }}
@@ -3254,7 +3661,6 @@ const MemoriWidget = ({
3254
3661
  })
3255
3662
  .catch(() => {
3256
3663
  setShowAgeVerification(false);
3257
- setGotErrorInOpening(true);
3258
3664
  });
3259
3665
  } else {
3260
3666
  setShowAgeVerification(false);