@bytexbyte/nxtlinq-ai-agent-ui-react-development 0.4.3 → 0.4.5

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.
@@ -0,0 +1,16 @@
1
+ export type WaitForAITPermissionsOptions<T> = {
2
+ readAIT: () => Promise<T | undefined>;
3
+ getPermissions: (ait: T) => readonly string[];
4
+ expectedPermissions: readonly string[];
5
+ attempts?: number;
6
+ delayMs?: number;
7
+ sleep?: (delayMs: number) => Promise<void>;
8
+ };
9
+ export declare function includesAITPermissions(actualPermissions: readonly string[], expectedPermissions: readonly string[]): boolean;
10
+ /**
11
+ * Wait until the AIT read path can observe a newly submitted permission set.
12
+ * Stale reads are deliberately discarded so callers never downgrade their
13
+ * optimistic local state while the chain/RPC read path is catching up.
14
+ */
15
+ export declare function waitForAITPermissions<T>({ readAIT, getPermissions, expectedPermissions, attempts, delayMs, sleep, }: WaitForAITPermissionsOptions<T>): Promise<T | undefined>;
16
+ //# sourceMappingURL=aitPermissionPropagation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aitPermissionPropagation.d.ts","sourceRoot":"","sources":["../src/aitPermissionPropagation.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,4BAA4B,CAAC,CAAC,IAAI;IAC5C,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACtC,cAAc,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,SAAS,MAAM,EAAE,CAAC;IAC9C,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C,CAAC;AAEF,wBAAgB,sBAAsB,CACpC,iBAAiB,EAAE,SAAS,MAAM,EAAE,EACpC,mBAAmB,EAAE,SAAS,MAAM,EAAE,GACrC,OAAO,CAGT;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,EAC7C,OAAO,EACP,cAAc,EACd,mBAAmB,EACnB,QAAa,EACb,OAAa,EACb,KAA6E,GAC9E,EAAE,4BAA4B,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAU1D"}
@@ -0,0 +1,21 @@
1
+ export function includesAITPermissions(actualPermissions, expectedPermissions) {
2
+ return actualPermissions.includes('ALL')
3
+ || expectedPermissions.every((permission) => actualPermissions.includes(permission));
4
+ }
5
+ /**
6
+ * Wait until the AIT read path can observe a newly submitted permission set.
7
+ * Stale reads are deliberately discarded so callers never downgrade their
8
+ * optimistic local state while the chain/RPC read path is catching up.
9
+ */
10
+ export async function waitForAITPermissions({ readAIT, getPermissions, expectedPermissions, attempts = 10, delayMs = 750, sleep = (duration) => new Promise((resolve) => setTimeout(resolve, duration)), }) {
11
+ const totalAttempts = Math.max(1, attempts);
12
+ for (let attempt = 0; attempt < totalAttempts; attempt += 1) {
13
+ const ait = await readAIT();
14
+ if (ait && includesAITPermissions(getPermissions(ait), expectedPermissions)) {
15
+ return ait;
16
+ }
17
+ if (attempt + 1 < totalAttempts)
18
+ await sleep(delayMs);
19
+ }
20
+ return undefined;
21
+ }
@@ -0,0 +1,9 @@
1
+ export declare class AuthorizationLoadingState {
2
+ private aitLoading;
3
+ private autoConnecting;
4
+ constructor(aitLoading?: boolean, autoConnecting?: boolean);
5
+ setAITLoading(value: boolean): void;
6
+ setAutoConnecting(value: boolean): void;
7
+ isLoading(): boolean;
8
+ }
9
+ //# sourceMappingURL=authorizationLoadingState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorizationLoadingState.d.ts","sourceRoot":"","sources":["../src/authorizationLoadingState.ts"],"names":[],"mappings":"AAAA,qBAAa,yBAAyB;IACpC,OAAO,CAAC,UAAU,CAAU;IAC5B,OAAO,CAAC,cAAc,CAAU;gBAEpB,UAAU,UAAQ,EAAE,cAAc,UAAQ;IAKtD,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAInC,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAIvC,SAAS,IAAI,OAAO;CAGrB"}
@@ -0,0 +1,15 @@
1
+ export class AuthorizationLoadingState {
2
+ constructor(aitLoading = false, autoConnecting = false) {
3
+ this.aitLoading = aitLoading;
4
+ this.autoConnecting = autoConnecting;
5
+ }
6
+ setAITLoading(value) {
7
+ this.aitLoading = value;
8
+ }
9
+ setAutoConnecting(value) {
10
+ this.autoConnecting = value;
11
+ }
12
+ isLoading() {
13
+ return this.aitLoading || this.autoConnecting;
14
+ }
15
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"ChatBotContext.d.ts","sourceRoot":"","sources":["../../src/context/ChatBotContext.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AA8B/B,OAAO,EAEL,kBAAkB,EAClB,YAAY,EAEb,MAAM,uBAAuB,CAAC;AAO/B,eAAO,MAAM,UAAU,0BAMtB,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,YAAY,CAo8FlD,CAAC"}
1
+ {"version":3,"file":"ChatBotContext.d.ts","sourceRoot":"","sources":["../../src/context/ChatBotContext.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AA8B/B,OAAO,EAEL,kBAAkB,EAClB,YAAY,EAEb,MAAM,uBAAuB,CAAC;AAW/B,eAAO,MAAM,UAAU,0BAMtB,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,YAAY,CAohGlD,CAAC"}
@@ -6,7 +6,11 @@ import { flushSync } from 'react-dom';
6
6
  import { v4 as uuidv4 } from 'uuid';
7
7
  import { createNxtlinqApi, setApiHosts, synthesizeSpeechToBuffer, streamSpeechToAudioContext, useLocalStorage, useSessionStorage, useSpeechToTextFromMic, useVoiceMode, metakeepClient, getEthers, sleep, walletTextUtils, } from '@bytexbyte/nxtlinq-ai-agent-web-development';
8
8
  import { authorizeTextFrontendTool, hasRecordedWalletVerification, validateWalletSession, } from '@bytexbyte/nxtlinq-ai-agent-core-development';
9
+ import { waitForAITPermissions } from '../aitPermissionPropagation';
10
+ import { AuthorizationLoadingState } from '../authorizationLoadingState';
9
11
  import { PendingTextToolRetryController } from '../pendingTextToolRetry';
12
+ import { completePiiScan } from '../piiMessageState';
13
+ import { filterSuggestions } from '../suggestionState';
10
14
  const MIC_ENABLED_SESSION_KEY = 'chatbot-mic-enabled';
11
15
  const ChatBotContext = React.createContext(undefined);
12
16
  export const useChatBot = () => {
@@ -145,6 +149,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
145
149
  const permissionsRef = React.useRef(permissions);
146
150
  const nxtlinqAITServiceAccessTokenRef = React.useRef(nxtlinqAITServiceAccessToken);
147
151
  const signerRef = React.useRef(signer);
152
+ const authorizationLoadingStateRef = React.useRef(new AuthorizationLoadingState(isAITLoading, isAutoConnecting));
148
153
  const pendingTextToolRetryRef = React.useRef(new PendingTextToolRetryController());
149
154
  const sendMessageRef = React.useRef(async () => { });
150
155
  const retryPendingTextTool = React.useCallback(() => {
@@ -185,6 +190,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
185
190
  const textInputRef = React.useRef(null);
186
191
  const lastPartialRangeRef = React.useRef(null);
187
192
  const lastAutoSentTranscriptRef = React.useRef('');
193
+ const presetSendInFlightRef = React.useRef(false);
188
194
  const autoSendTimerRef = React.useRef(null);
189
195
  const isCorrectingRef = React.useRef(false);
190
196
  const lastCustomErrorRef = React.useRef(undefined);
@@ -483,6 +489,12 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
483
489
  React.useEffect(() => {
484
490
  permissionsRef.current = permissions;
485
491
  }, [permissions]);
492
+ React.useEffect(() => {
493
+ authorizationLoadingStateRef.current.setAITLoading(isAITLoading);
494
+ }, [isAITLoading]);
495
+ React.useEffect(() => {
496
+ authorizationLoadingStateRef.current.setAutoConnecting(isAutoConnecting);
497
+ }, [isAutoConnecting]);
486
498
  React.useEffect(() => {
487
499
  nxtlinqAITServiceAccessTokenRef.current = nxtlinqAITServiceAccessToken;
488
500
  }, [nxtlinqAITServiceAccessToken]);
@@ -875,12 +887,16 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
875
887
  const currentHitAddress = hitAddressRef.current || hitAddress;
876
888
  const currentToken = nxtlinqAITServiceAccessTokenRef.current || nxtlinqAITServiceAccessToken;
877
889
  if (!currentHitAddress) {
890
+ aitRef.current = null;
891
+ permissionsRef.current = [];
878
892
  setAit(null);
879
893
  setPermissions([]);
880
894
  setWalletInfo(null);
895
+ authorizationLoadingStateRef.current.setAITLoading(false);
881
896
  setIsAITLoading(false);
882
897
  return;
883
898
  }
899
+ authorizationLoadingStateRef.current.setAITLoading(true);
884
900
  setIsAITLoading(true);
885
901
  try {
886
902
  let walletAllowsAITLookup = !requireWalletIDVVerification;
@@ -910,6 +926,8 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
910
926
  // AIT existence is not identity verification. In strict mode, stop before
911
927
  // the legacy AIT lookup can synthesize a custom wallet record.
912
928
  if (currentToken && !walletAllowsAITLookup) {
929
+ aitRef.current = null;
930
+ permissionsRef.current = [];
913
931
  setAit(null);
914
932
  setPermissions([]);
915
933
  return;
@@ -923,28 +941,81 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
923
941
  }, currentToken);
924
942
  if ('error' in response) {
925
943
  console.error('Failed to fetch AIT:', response.error);
944
+ aitRef.current = null;
945
+ permissionsRef.current = [];
926
946
  setAit(null);
927
947
  setPermissions([]);
928
948
  return;
929
949
  }
950
+ aitRef.current = response;
930
951
  setAit(response);
931
952
  if (!isPermissionFormOpen || forceUpdatePermissions) {
932
953
  const newPermissions = response.metadata?.permissions || [];
954
+ permissionsRef.current = newPermissions;
933
955
  setPermissions(newPermissions);
934
956
  }
935
957
  }
936
958
  else {
937
959
  // No token available, clear AIT data
960
+ aitRef.current = null;
961
+ permissionsRef.current = [];
938
962
  setAit(null);
939
963
  setPermissions([]);
940
964
  }
941
965
  }
942
966
  catch (error) {
943
967
  console.error('Failed to fetch AIT:', error);
968
+ aitRef.current = null;
969
+ permissionsRef.current = [];
944
970
  setAit(null);
945
971
  setPermissions([]);
946
972
  }
947
973
  finally {
974
+ // Keep the authorization snapshot current before refreshAIT resolves.
975
+ // React may not commit setState until the immediate pending-tool retry.
976
+ authorizationLoadingStateRef.current.setAITLoading(false);
977
+ setIsAITLoading(false);
978
+ }
979
+ };
980
+ const waitForPersistedAITPermissions = async (expectedPermissions) => {
981
+ const currentHitAddress = hitAddressRef.current || hitAddress;
982
+ const currentToken = nxtlinqAITServiceAccessTokenRef.current || nxtlinqAITServiceAccessToken;
983
+ if (!currentHitAddress || !currentToken)
984
+ return false;
985
+ authorizationLoadingStateRef.current.setAITLoading(true);
986
+ setIsAITLoading(true);
987
+ try {
988
+ const persistedAIT = await waitForAITPermissions({
989
+ expectedPermissions,
990
+ readAIT: async () => {
991
+ try {
992
+ const response = await nxtlinqApi.ait.getAITByServiceIdAndController({
993
+ serviceId,
994
+ controller: currentHitAddress,
995
+ customUsername: (!requireWalletIDVVerification && customUsername)
996
+ ? getFinalCustomUsername(customUsername)
997
+ : undefined,
998
+ }, currentToken);
999
+ return 'error' in response ? undefined : response;
1000
+ }
1001
+ catch (error) {
1002
+ console.warn('AIT permission propagation check failed:', error);
1003
+ return undefined;
1004
+ }
1005
+ },
1006
+ getPermissions: (currentAIT) => currentAIT.metadata?.permissions || [],
1007
+ });
1008
+ if (!persistedAIT)
1009
+ return false;
1010
+ const persistedPermissions = persistedAIT.metadata?.permissions || [];
1011
+ aitRef.current = persistedAIT;
1012
+ permissionsRef.current = persistedPermissions;
1013
+ setAit(persistedAIT);
1014
+ setPermissions(persistedPermissions);
1015
+ return true;
1016
+ }
1017
+ finally {
1018
+ authorizationLoadingStateRef.current.setAITLoading(false);
948
1019
  setIsAITLoading(false);
949
1020
  }
950
1021
  };
@@ -1158,7 +1229,9 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1158
1229
  // wallet-address subject until issuance/migration supports it.
1159
1230
  externalId: undefined,
1160
1231
  requireWalletIDVVerification,
1161
- loading: isAITLoading || isAutoConnecting,
1232
+ // Use synchronously maintained refs so a retry immediately following
1233
+ // refreshAIT() does not observe the previous React render's loading state.
1234
+ loading: authorizationLoadingStateRef.current.isLoading(),
1162
1235
  },
1163
1236
  ...authFields(),
1164
1237
  customUsername: (!requireWalletIDVVerification && customUsername)
@@ -1225,7 +1298,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1225
1298
  }
1226
1299
  return effectiveAvailableModels[safeIndex];
1227
1300
  }, [effectiveAvailableModels, selectedModelIndex]);
1228
- const updateSuggestions = React.useCallback(async (pseudoId, externalId) => {
1301
+ const updateSuggestions = React.useCallback(async (pseudoId, externalId, lastUserMessage) => {
1229
1302
  const result = await nxtlinqApi.agent.generateSuggestions({
1230
1303
  ...authFields(),
1231
1304
  pseudoId,
@@ -1236,11 +1309,8 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1236
1309
  setSuggestions([]);
1237
1310
  return;
1238
1311
  }
1239
- setSuggestions(result.suggestions.map((sug) => ({
1240
- text: sug,
1241
- autoSend: true
1242
- })));
1243
- }, []);
1312
+ setSuggestions(filterSuggestions(result.suggestions.map((sug) => ({ text: sug, autoSend: true })), lastUserMessage));
1313
+ }, [nxtlinqApi, authFields, setSuggestions]);
1244
1314
  // Updated sendMessage function to support different AI models and attachments
1245
1315
  const sendMessage = async (content, retryCount = 0, isPresetMessage = false, attachments, clientPipelineOverride) => {
1246
1316
  const hasContent = content.trim() || (attachments && attachments.length > 0);
@@ -1360,15 +1430,15 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1360
1430
  const updated = [...prev];
1361
1431
  for (let i = updated.length - 1; i >= 0; i--) {
1362
1432
  if (updated[i].role === 'user' && updated[i].piiStatus === 'scanning') {
1363
- const patch = { piiStep };
1364
- // scan_complete carries anonymized data set piiProtection early for immediate redact
1365
- if (step === 'scan_complete' && data?.anonymizedUserMessage) {
1366
- patch.piiProtection = {
1367
- anonymizedContent: data.anonymizedUserMessage,
1368
- mapping: data.mapping || {},
1369
- };
1433
+ if (step === 'scan_complete') {
1434
+ // PII work is complete independently of any later tool
1435
+ // authorization. Never leave the bubble in Sending while
1436
+ // waiting for wallet recovery or permission elevation.
1437
+ updated[i] = completePiiScan(updated[i], data);
1438
+ }
1439
+ else {
1440
+ updated[i] = { ...updated[i], piiStep };
1370
1441
  }
1371
- updated[i] = { ...updated[i], ...patch };
1372
1442
  break;
1373
1443
  }
1374
1444
  }
@@ -1376,6 +1446,26 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1376
1446
  });
1377
1447
  } : undefined,
1378
1448
  });
1449
+ // PII scanning is complete when the Agent response arrives, regardless
1450
+ // of whether a later frontend-tool authorization step can continue.
1451
+ if (piiDisplayMode === 'redacted') {
1452
+ const anonymizedUserMsg = response.piiProtection?.anonymizedUserMessage;
1453
+ const piiMappingData = response.piiProtection?.mapping ?? undefined;
1454
+ setMessages(prev => {
1455
+ const updated = [...prev];
1456
+ for (let i = updated.length - 1; i >= 0; i--) {
1457
+ if (updated[i].role === 'user' && updated[i].piiStatus === 'scanning') {
1458
+ updated[i] = completePiiScan(updated[i], {
1459
+ entityCount: piiMappingData ? Object.keys(piiMappingData).length : 0,
1460
+ anonymizedUserMessage: anonymizedUserMsg,
1461
+ mapping: piiMappingData,
1462
+ });
1463
+ break;
1464
+ }
1465
+ }
1466
+ return updated;
1467
+ });
1468
+ }
1379
1469
  if (!('error' in response && response.error)) {
1380
1470
  const tr = response;
1381
1471
  if (tr.ttsVoice &&
@@ -1500,7 +1590,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1500
1590
  if (!replyText.trim()) {
1501
1591
  replyText = 'Sorry, I cannot understand your question';
1502
1592
  }
1503
- updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined);
1593
+ updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined, content);
1504
1594
  setMessages(prev => prev.map(m => m.id === streamAssistantId
1505
1595
  ? {
1506
1596
  ...m,
@@ -1639,7 +1729,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1639
1729
  // Don't block the UI if update fails
1640
1730
  }
1641
1731
  }
1642
- updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined);
1732
+ updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined, content);
1643
1733
  // Skip creating a new botResponse since we already updated the streaming message
1644
1734
  }
1645
1735
  else {
@@ -1678,7 +1768,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1678
1768
  // Don't block the UI if update fails
1679
1769
  }
1680
1770
  }
1681
- updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined);
1771
+ updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined, content);
1682
1772
  const newBotResponse = {
1683
1773
  id: (Date.now() + 1).toString(),
1684
1774
  content: mergedContent,
@@ -1704,7 +1794,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1704
1794
  .map((item) => item.text)
1705
1795
  .join(' ') || 'Sorry, I cannot understand your question'
1706
1796
  : response.reply || 'Sorry, I cannot understand your question';
1707
- updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined);
1797
+ updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined, content);
1708
1798
  const newBotResponse = {
1709
1799
  id: (Date.now() + 1).toString(),
1710
1800
  content: replyText,
@@ -1742,38 +1832,6 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1742
1832
  setMessages(prev => [...prev, newBotResponse]);
1743
1833
  botResponse = newBotResponse;
1744
1834
  }
1745
- // ===== PII Protection: Update user message with anonymized version =====
1746
- const anonymizedUserMsg = response.piiProtection?.anonymizedUserMessage;
1747
- const piiMappingData = response.piiProtection?.mapping ?? undefined;
1748
- if (anonymizedUserMsg) {
1749
- setMessages(prev => {
1750
- const updated = [...prev];
1751
- for (let i = updated.length - 1; i >= 0; i--) {
1752
- if (updated[i].role === 'user') {
1753
- updated[i] = {
1754
- ...updated[i],
1755
- piiProtection: { anonymizedContent: anonymizedUserMsg, mapping: piiMappingData },
1756
- piiStatus: 'complete',
1757
- };
1758
- break;
1759
- }
1760
- }
1761
- return updated;
1762
- });
1763
- }
1764
- else if (piiDisplayMode === 'redacted') {
1765
- // No PII detected — set piiStatus to 'none' to trigger "No sensitive data" indicator
1766
- setMessages(prev => {
1767
- const updated = [...prev];
1768
- for (let i = updated.length - 1; i >= 0; i--) {
1769
- if (updated[i].role === 'user' && updated[i].piiStatus === 'scanning') {
1770
- updated[i] = { ...updated[i], piiStatus: 'none' };
1771
- break;
1772
- }
1773
- }
1774
- return updated;
1775
- });
1776
- }
1777
1835
  // Execute redirect after all message processing is complete
1778
1836
  if (redirectUrl) {
1779
1837
  // Use setTimeout to ensure the message is displayed before redirect
@@ -2114,20 +2172,18 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2114
2172
  return { url: result.url };
2115
2173
  }, [nxtlinqApi, authFields, pseudoId]);
2116
2174
  // Handle preset message
2117
- const handlePresetMessage = (message) => {
2175
+ const handlePresetMessage = async (message) => {
2118
2176
  // If preset is configured as auto-send, avoid duplicate sends when user clicks repeatedly
2119
2177
  if (message.autoSend) {
2120
2178
  const trimmedText = (message.text || '').trim();
2121
2179
  if (!trimmedText)
2122
2180
  return;
2123
2181
  // Prevent sending messages while AI Agent is processing
2124
- if (isLoading) {
2125
- return;
2126
- }
2127
- // If this exact preset text was just sent (by auto-send / manual / preset), skip to prevent duplicates
2128
- if (lastAutoSentTranscriptRef.current === trimmedText) {
2182
+ if (isLoading || presetSendInFlightRef.current) {
2129
2183
  return;
2130
2184
  }
2185
+ presetSendInFlightRef.current = true;
2186
+ setSuggestions(previous => filterSuggestions(previous, trimmedText));
2131
2187
  // For preset messages, we need to add the user message first since sendMessage won't add it on retries
2132
2188
  const userMessage = {
2133
2189
  id: Date.now().toString(),
@@ -2144,7 +2200,12 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2144
2200
  // Mark as last sent to guard against rapid re-clicks and other duplicate flows
2145
2201
  lastAutoSentTranscriptRef.current = trimmedText;
2146
2202
  // Pass a flag to indicate this is a preset message so sendMessage won't add user message again
2147
- sendMessage(trimmedText, 0, true);
2203
+ try {
2204
+ await sendMessage(trimmedText, 0, true);
2205
+ }
2206
+ finally {
2207
+ presetSendInFlightRef.current = false;
2208
+ }
2148
2209
  }
2149
2210
  else {
2150
2211
  setInputValue(message.text);
@@ -2223,8 +2284,11 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2223
2284
  metadataHash,
2224
2285
  metadataCid,
2225
2286
  };
2287
+ const nextPermissions = newPermissions || permissions;
2288
+ aitRef.current = aitInfo;
2289
+ permissionsRef.current = nextPermissions;
2226
2290
  setAit(aitInfo);
2227
- setPermissions(newPermissions || permissions);
2291
+ setPermissions(nextPermissions);
2228
2292
  };
2229
2293
  // Auto enable AIT permission
2230
2294
  const enableAIT = async (toolName) => {
@@ -2307,8 +2371,13 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2307
2371
  // For auto-enable, we should create a regular AIT (not AI Agent AIT) if user doesn't have existing AIT
2308
2372
  const shouldCreateAsAIAgent = !!aitRef.current; // Only create as AI Agent if user already has an AIT
2309
2373
  await generateAndRegisterAITWithSigner(validPermissions, shouldCreateAsAIAgent, currentSigner, currentHitAddress);
2374
+ const permissionsPersisted = await waitForPersistedAITPermissions(validPermissions);
2375
+ if (!permissionsPersisted) {
2376
+ showWarning('AIT permission was submitted but is still synchronizing. Please try again shortly.');
2377
+ setIsAITEnabling(false);
2378
+ return false;
2379
+ }
2310
2380
  showSuccess('AIT permission enabled successfully! You can now use the AI agent.');
2311
- await refreshAIT(true);
2312
2381
  setIsAITEnabling(false);
2313
2382
  return true;
2314
2383
  }
@@ -2394,6 +2463,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2394
2463
  customUsername: (!requireWalletIDVVerification && customUsername) ? getFinalCustomUsername(customUsername) : undefined
2395
2464
  }, token);
2396
2465
  if (!('error' in aitResponse)) {
2466
+ aitRef.current = aitResponse;
2397
2467
  setAit(aitResponse);
2398
2468
  }
2399
2469
  }
@@ -2435,6 +2505,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2435
2505
  customUsername: (!requireWalletIDVVerification && customUsername) ? getFinalCustomUsername(customUsername) : undefined
2436
2506
  }, token);
2437
2507
  if (!('error' in aitResponse)) {
2508
+ aitRef.current = aitResponse;
2438
2509
  setAit(aitResponse);
2439
2510
  }
2440
2511
  }
@@ -2552,6 +2623,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2552
2623
  // Set loading state when permission form opens
2553
2624
  React.useEffect(() => {
2554
2625
  if (isPermissionFormOpen && hitAddress) {
2626
+ authorizationLoadingStateRef.current.setAITLoading(true);
2555
2627
  setIsAITLoading(true);
2556
2628
  }
2557
2629
  }, [isPermissionFormOpen, hitAddress]);
@@ -0,0 +1,16 @@
1
+ type PiiMessage = {
2
+ piiStatus?: 'scanning' | 'complete' | 'none';
3
+ piiStep?: 0 | 1 | 2;
4
+ piiProtection?: {
5
+ anonymizedContent?: string;
6
+ mapping?: Record<string, string>;
7
+ };
8
+ };
9
+ export type PiiScanCompleteData = {
10
+ entityCount?: number;
11
+ anonymizedUserMessage?: string | null;
12
+ mapping?: Record<string, string> | null;
13
+ };
14
+ export declare function completePiiScan<T extends PiiMessage>(message: T, data?: PiiScanCompleteData): T;
15
+ export {};
16
+ //# sourceMappingURL=piiMessageState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"piiMessageState.d.ts","sourceRoot":"","sources":["../src/piiMessageState.ts"],"names":[],"mappings":"AAAA,KAAK,UAAU,GAAG;IAChB,SAAS,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,MAAM,CAAC;IAC7C,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpB,aAAa,CAAC,EAAE;QACd,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KAClC,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CACzC,CAAC;AAEF,wBAAgB,eAAe,CAAC,CAAC,SAAS,UAAU,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,mBAAmB,GAAG,CAAC,CAmB/F"}
@@ -0,0 +1,18 @@
1
+ export function completePiiScan(message, data) {
2
+ const mapping = data?.mapping || {};
3
+ const entityCount = typeof data?.entityCount === 'number'
4
+ ? data.entityCount
5
+ : Object.keys(mapping).length;
6
+ if (entityCount <= 0 || !data?.anonymizedUserMessage) {
7
+ return { ...message, piiStatus: 'none', piiStep: 1 };
8
+ }
9
+ return {
10
+ ...message,
11
+ piiStatus: 'complete',
12
+ piiStep: 1,
13
+ piiProtection: {
14
+ anonymizedContent: data.anonymizedUserMessage,
15
+ mapping,
16
+ },
17
+ };
18
+ }
@@ -0,0 +1,4 @@
1
+ import type { PresetMessage } from './types/ChatBotTypes';
2
+ export declare function normalizeSuggestionText(value: string): string;
3
+ export declare function filterSuggestions(suggestions: readonly PresetMessage[], lastUserMessage?: string): PresetMessage[];
4
+ //# sourceMappingURL=suggestionState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suggestionState.d.ts","sourceRoot":"","sources":["../src/suggestionState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,SAAS,aAAa,EAAE,EACrC,eAAe,CAAC,EAAE,MAAM,GACvB,aAAa,EAAE,CAUjB"}
@@ -0,0 +1,14 @@
1
+ export function normalizeSuggestionText(value) {
2
+ return value.trim().replace(/\s+/g, ' ').toLocaleLowerCase();
3
+ }
4
+ export function filterSuggestions(suggestions, lastUserMessage) {
5
+ const lastUserText = normalizeSuggestionText(lastUserMessage || '');
6
+ const seen = new Set();
7
+ return suggestions.filter((suggestion) => {
8
+ const normalized = normalizeSuggestionText(suggestion.text || '');
9
+ if (!normalized || normalized === lastUserText || seen.has(normalized))
10
+ return false;
11
+ seen.add(normalized);
12
+ return true;
13
+ });
14
+ }
@@ -138,7 +138,7 @@ export interface ChatBotContextType {
138
138
  retryPendingTextTool: () => Promise<boolean>;
139
139
  sendMessage: (content: string, retryCount?: number, isPresetMessage?: boolean, attachments?: Attachment[]) => Promise<void>;
140
140
  handleSubmit: (e: React.FormEvent, attachments?: Attachment[]) => Promise<void>;
141
- handlePresetMessage: (message: PresetMessage) => void;
141
+ handlePresetMessage: (message: PresetMessage) => Promise<void>;
142
142
  savePermissions: (newPermissions?: string[]) => Promise<void>;
143
143
  enableAIT: (toolName: string) => Promise<boolean>;
144
144
  handleVerifyWalletClick: (method: 'berifyme' | 'custom') => Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"ChatBotTypes.d.ts","sourceRoot":"","sources":["../../src/types/ChatBotTypes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ChatBotTypes.d.ts","sourceRoot":"","sources":["../../src/types/ChatBotTypes.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"PresetMessages.d.ts","sourceRoot":"","sources":["../../src/ui/PresetMessages.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAM/B,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EA0ClC,CAAC"}
1
+ {"version":3,"file":"PresetMessages.d.ts","sourceRoot":"","sources":["../../src/ui/PresetMessages.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAO/B,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EA4ClC,CAAC"}
@@ -2,9 +2,12 @@ import { jsx as _jsx } from "@emotion/react/jsx-runtime";
2
2
  import { css } from '@emotion/react';
3
3
  import { useChatBot } from '../context/ChatBotContext';
4
4
  import { actionButton } from './styles/isolatedStyles';
5
+ import { filterSuggestions, normalizeSuggestionText } from '../suggestionState';
5
6
  export const PresetMessages = () => {
6
- const { suggestions, handlePresetMessage } = useChatBot();
7
- if (!suggestions || suggestions.length === 0) {
7
+ const { suggestions, messages, handlePresetMessage } = useChatBot();
8
+ const lastUserMessage = [...messages].reverse().find(message => message.role === 'user')?.content;
9
+ const visibleSuggestions = filterSuggestions(suggestions || [], lastUserMessage);
10
+ if (visibleSuggestions.length === 0) {
8
11
  return null;
9
12
  }
10
13
  return (_jsx("div", { css: css `
@@ -15,7 +18,7 @@ export const PresetMessages = () => {
15
18
  display: flex !important;
16
19
  gap: 10px !important;
17
20
  align-items: center !important;
18
- `, children: suggestions.map((preset, index) => (_jsx("button", { onClick: () => handlePresetMessage(preset), css: css `
21
+ `, children: visibleSuggestions.map((preset) => (_jsx("button", { onClick: () => { void handlePresetMessage(preset); }, css: css `
19
22
  ${actionButton}
20
23
  padding: 5px 10px !important;
21
24
  font-size: 12px !important;
@@ -29,5 +32,5 @@ export const PresetMessages = () => {
29
32
  &:active {
30
33
  background-color: #004085 !important;
31
34
  }
32
- `, children: preset.text }, index))) }));
35
+ `, children: preset.text }, normalizeSuggestionText(preset.text)))) }));
33
36
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytexbyte/nxtlinq-ai-agent-ui-react-development",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Official React Web UI for nxtlinq AI Agent — drop-in chat widget",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,40 @@
1
+ export type WaitForAITPermissionsOptions<T> = {
2
+ readAIT: () => Promise<T | undefined>;
3
+ getPermissions: (ait: T) => readonly string[];
4
+ expectedPermissions: readonly string[];
5
+ attempts?: number;
6
+ delayMs?: number;
7
+ sleep?: (delayMs: number) => Promise<void>;
8
+ };
9
+
10
+ export function includesAITPermissions(
11
+ actualPermissions: readonly string[],
12
+ expectedPermissions: readonly string[],
13
+ ): boolean {
14
+ return actualPermissions.includes('ALL')
15
+ || expectedPermissions.every((permission) => actualPermissions.includes(permission));
16
+ }
17
+
18
+ /**
19
+ * Wait until the AIT read path can observe a newly submitted permission set.
20
+ * Stale reads are deliberately discarded so callers never downgrade their
21
+ * optimistic local state while the chain/RPC read path is catching up.
22
+ */
23
+ export async function waitForAITPermissions<T>({
24
+ readAIT,
25
+ getPermissions,
26
+ expectedPermissions,
27
+ attempts = 10,
28
+ delayMs = 750,
29
+ sleep = (duration) => new Promise((resolve) => setTimeout(resolve, duration)),
30
+ }: WaitForAITPermissionsOptions<T>): Promise<T | undefined> {
31
+ const totalAttempts = Math.max(1, attempts);
32
+ for (let attempt = 0; attempt < totalAttempts; attempt += 1) {
33
+ const ait = await readAIT();
34
+ if (ait && includesAITPermissions(getPermissions(ait), expectedPermissions)) {
35
+ return ait;
36
+ }
37
+ if (attempt + 1 < totalAttempts) await sleep(delayMs);
38
+ }
39
+ return undefined;
40
+ }
@@ -0,0 +1,21 @@
1
+ export class AuthorizationLoadingState {
2
+ private aitLoading: boolean;
3
+ private autoConnecting: boolean;
4
+
5
+ constructor(aitLoading = false, autoConnecting = false) {
6
+ this.aitLoading = aitLoading;
7
+ this.autoConnecting = autoConnecting;
8
+ }
9
+
10
+ setAITLoading(value: boolean): void {
11
+ this.aitLoading = value;
12
+ }
13
+
14
+ setAutoConnecting(value: boolean): void {
15
+ this.autoConnecting = value;
16
+ }
17
+
18
+ isLoading(): boolean {
19
+ return this.aitLoading || this.autoConnecting;
20
+ }
21
+ }
@@ -36,7 +36,11 @@ import {
36
36
  ChatBotProps,
37
37
  PresetMessage
38
38
  } from '../types/ChatBotTypes';
39
+ import { waitForAITPermissions } from '../aitPermissionPropagation';
40
+ import { AuthorizationLoadingState } from '../authorizationLoadingState';
39
41
  import { PendingTextToolRetryController } from '../pendingTextToolRetry';
42
+ import { completePiiScan } from '../piiMessageState';
43
+ import { filterSuggestions } from '../suggestionState';
40
44
 
41
45
  const MIC_ENABLED_SESSION_KEY = 'chatbot-mic-enabled';
42
46
 
@@ -218,6 +222,9 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
218
222
  const permissionsRef = React.useRef(permissions);
219
223
  const nxtlinqAITServiceAccessTokenRef = React.useRef(nxtlinqAITServiceAccessToken);
220
224
  const signerRef = React.useRef(signer);
225
+ const authorizationLoadingStateRef = React.useRef(
226
+ new AuthorizationLoadingState(isAITLoading, isAutoConnecting),
227
+ );
221
228
  const pendingTextToolRetryRef = React.useRef(new PendingTextToolRetryController());
222
229
  const sendMessageRef = React.useRef<ChatBotContextType['sendMessage']>(async () => {});
223
230
  const retryPendingTextTool = React.useCallback(() => {
@@ -264,6 +271,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
264
271
  const textInputRef = React.useRef<HTMLInputElement | null>(null);
265
272
  const lastPartialRangeRef = React.useRef<{ start: number; end: number } | null>(null);
266
273
  const lastAutoSentTranscriptRef = React.useRef<string>('');
274
+ const presetSendInFlightRef = React.useRef(false);
267
275
  const autoSendTimerRef = React.useRef<NodeJS.Timeout | null>(null);
268
276
  const isCorrectingRef = React.useRef(false);
269
277
  const lastCustomErrorRef = React.useRef<string | undefined>(undefined);
@@ -578,6 +586,14 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
578
586
  permissionsRef.current = permissions;
579
587
  }, [permissions]);
580
588
 
589
+ React.useEffect(() => {
590
+ authorizationLoadingStateRef.current.setAITLoading(isAITLoading);
591
+ }, [isAITLoading]);
592
+
593
+ React.useEffect(() => {
594
+ authorizationLoadingStateRef.current.setAutoConnecting(isAutoConnecting);
595
+ }, [isAutoConnecting]);
596
+
581
597
  React.useEffect(() => {
582
598
  nxtlinqAITServiceAccessTokenRef.current = nxtlinqAITServiceAccessToken;
583
599
  }, [nxtlinqAITServiceAccessToken]);
@@ -1021,13 +1037,17 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1021
1037
  const currentHitAddress = hitAddressRef.current || hitAddress;
1022
1038
  const currentToken = nxtlinqAITServiceAccessTokenRef.current || nxtlinqAITServiceAccessToken;
1023
1039
  if (!currentHitAddress) {
1040
+ aitRef.current = null;
1041
+ permissionsRef.current = [];
1024
1042
  setAit(null);
1025
1043
  setPermissions([]);
1026
1044
  setWalletInfo(null);
1045
+ authorizationLoadingStateRef.current.setAITLoading(false);
1027
1046
  setIsAITLoading(false);
1028
1047
  return;
1029
1048
  }
1030
1049
 
1050
+ authorizationLoadingStateRef.current.setAITLoading(true);
1031
1051
  setIsAITLoading(true);
1032
1052
  try {
1033
1053
  let walletAllowsAITLookup = !requireWalletIDVVerification;
@@ -1057,6 +1077,8 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1057
1077
  // AIT existence is not identity verification. In strict mode, stop before
1058
1078
  // the legacy AIT lookup can synthesize a custom wallet record.
1059
1079
  if (currentToken && !walletAllowsAITLookup) {
1080
+ aitRef.current = null;
1081
+ permissionsRef.current = [];
1060
1082
  setAit(null);
1061
1083
  setPermissions([]);
1062
1084
  return;
@@ -1072,26 +1094,81 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1072
1094
 
1073
1095
  if ('error' in response) {
1074
1096
  console.error('Failed to fetch AIT:', response.error);
1097
+ aitRef.current = null;
1098
+ permissionsRef.current = [];
1075
1099
  setAit(null);
1076
1100
  setPermissions([]);
1077
1101
  return;
1078
1102
  }
1079
1103
 
1104
+ aitRef.current = response;
1080
1105
  setAit(response);
1081
1106
  if (!isPermissionFormOpen || forceUpdatePermissions) {
1082
1107
  const newPermissions = response.metadata?.permissions || [];
1108
+ permissionsRef.current = newPermissions;
1083
1109
  setPermissions(newPermissions);
1084
1110
  }
1085
1111
  } else {
1086
1112
  // No token available, clear AIT data
1113
+ aitRef.current = null;
1114
+ permissionsRef.current = [];
1087
1115
  setAit(null);
1088
1116
  setPermissions([]);
1089
1117
  }
1090
1118
  } catch (error) {
1091
1119
  console.error('Failed to fetch AIT:', error);
1120
+ aitRef.current = null;
1121
+ permissionsRef.current = [];
1092
1122
  setAit(null);
1093
1123
  setPermissions([]);
1094
1124
  } finally {
1125
+ // Keep the authorization snapshot current before refreshAIT resolves.
1126
+ // React may not commit setState until the immediate pending-tool retry.
1127
+ authorizationLoadingStateRef.current.setAITLoading(false);
1128
+ setIsAITLoading(false);
1129
+ }
1130
+ };
1131
+
1132
+ const waitForPersistedAITPermissions = async (
1133
+ expectedPermissions: string[],
1134
+ ): Promise<boolean> => {
1135
+ const currentHitAddress = hitAddressRef.current || hitAddress;
1136
+ const currentToken = nxtlinqAITServiceAccessTokenRef.current || nxtlinqAITServiceAccessToken;
1137
+ if (!currentHitAddress || !currentToken) return false;
1138
+
1139
+ authorizationLoadingStateRef.current.setAITLoading(true);
1140
+ setIsAITLoading(true);
1141
+ try {
1142
+ const persistedAIT = await waitForAITPermissions({
1143
+ expectedPermissions,
1144
+ readAIT: async () => {
1145
+ try {
1146
+ const response = await nxtlinqApi.ait.getAITByServiceIdAndController({
1147
+ serviceId,
1148
+ controller: currentHitAddress,
1149
+ customUsername: (!requireWalletIDVVerification && customUsername)
1150
+ ? getFinalCustomUsername(customUsername)
1151
+ : undefined,
1152
+ }, currentToken);
1153
+ return 'error' in response ? undefined : response;
1154
+ } catch (error) {
1155
+ console.warn('AIT permission propagation check failed:', error);
1156
+ return undefined;
1157
+ }
1158
+ },
1159
+ getPermissions: (currentAIT) => currentAIT.metadata?.permissions || [],
1160
+ });
1161
+
1162
+ if (!persistedAIT) return false;
1163
+
1164
+ const persistedPermissions = persistedAIT.metadata?.permissions || [];
1165
+ aitRef.current = persistedAIT;
1166
+ permissionsRef.current = persistedPermissions;
1167
+ setAit(persistedAIT);
1168
+ setPermissions(persistedPermissions);
1169
+ return true;
1170
+ } finally {
1171
+ authorizationLoadingStateRef.current.setAITLoading(false);
1095
1172
  setIsAITLoading(false);
1096
1173
  }
1097
1174
  };
@@ -1329,7 +1406,9 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1329
1406
  // wallet-address subject until issuance/migration supports it.
1330
1407
  externalId: undefined,
1331
1408
  requireWalletIDVVerification,
1332
- loading: isAITLoading || isAutoConnecting,
1409
+ // Use synchronously maintained refs so a retry immediately following
1410
+ // refreshAIT() does not observe the previous React render's loading state.
1411
+ loading: authorizationLoadingStateRef.current.isLoading(),
1333
1412
  },
1334
1413
  ...authFields(),
1335
1414
  customUsername: (!requireWalletIDVVerification && customUsername)
@@ -1402,7 +1481,11 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1402
1481
  return effectiveAvailableModels[safeIndex];
1403
1482
  }, [effectiveAvailableModels, selectedModelIndex]);
1404
1483
 
1405
- const updateSuggestions = React.useCallback(async (pseudoId: string, externalId?: string) => {
1484
+ const updateSuggestions = React.useCallback(async (
1485
+ pseudoId: string,
1486
+ externalId?: string,
1487
+ lastUserMessage?: string,
1488
+ ) => {
1406
1489
  const result = await nxtlinqApi.agent.generateSuggestions({
1407
1490
  ...authFields(),
1408
1491
  pseudoId,
@@ -1415,11 +1498,11 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1415
1498
  return;
1416
1499
  }
1417
1500
 
1418
- setSuggestions(result.suggestions.map((sug: string) => ({
1419
- text: sug,
1420
- autoSend: true
1421
- })));
1422
- }, [])
1501
+ setSuggestions(filterSuggestions(
1502
+ result.suggestions.map((sug: string) => ({ text: sug, autoSend: true })),
1503
+ lastUserMessage,
1504
+ ));
1505
+ }, [nxtlinqApi, authFields, setSuggestions])
1423
1506
 
1424
1507
  // Updated sendMessage function to support different AI models and attachments
1425
1508
  const sendMessage = async (
@@ -1555,15 +1638,14 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1555
1638
  const updated = [...prev];
1556
1639
  for (let i = updated.length - 1; i >= 0; i--) {
1557
1640
  if (updated[i].role === 'user' && updated[i].piiStatus === 'scanning') {
1558
- const patch: Partial<typeof updated[0]> = { piiStep };
1559
- // scan_complete carries anonymized data set piiProtection early for immediate redact
1560
- if (step === 'scan_complete' && data?.anonymizedUserMessage) {
1561
- patch.piiProtection = {
1562
- anonymizedContent: data.anonymizedUserMessage,
1563
- mapping: data.mapping || {},
1564
- };
1641
+ if (step === 'scan_complete') {
1642
+ // PII work is complete independently of any later tool
1643
+ // authorization. Never leave the bubble in Sending while
1644
+ // waiting for wallet recovery or permission elevation.
1645
+ updated[i] = completePiiScan(updated[i], data);
1646
+ } else {
1647
+ updated[i] = { ...updated[i], piiStep };
1565
1648
  }
1566
- updated[i] = { ...updated[i], ...patch };
1567
1649
  break;
1568
1650
  }
1569
1651
  }
@@ -1572,6 +1654,27 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1572
1654
  } : undefined,
1573
1655
  });
1574
1656
 
1657
+ // PII scanning is complete when the Agent response arrives, regardless
1658
+ // of whether a later frontend-tool authorization step can continue.
1659
+ if (piiDisplayMode === 'redacted') {
1660
+ const anonymizedUserMsg = response.piiProtection?.anonymizedUserMessage;
1661
+ const piiMappingData = response.piiProtection?.mapping ?? undefined;
1662
+ setMessages(prev => {
1663
+ const updated = [...prev];
1664
+ for (let i = updated.length - 1; i >= 0; i--) {
1665
+ if (updated[i].role === 'user' && updated[i].piiStatus === 'scanning') {
1666
+ updated[i] = completePiiScan(updated[i], {
1667
+ entityCount: piiMappingData ? Object.keys(piiMappingData).length : 0,
1668
+ anonymizedUserMessage: anonymizedUserMsg,
1669
+ mapping: piiMappingData,
1670
+ });
1671
+ break;
1672
+ }
1673
+ }
1674
+ return updated;
1675
+ });
1676
+ }
1677
+
1575
1678
  if (!('error' in response && (response as { error?: string }).error)) {
1576
1679
  const tr = response as AgentResponse;
1577
1680
  if (
@@ -1701,7 +1804,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1701
1804
  replyText = 'Sorry, I cannot understand your question';
1702
1805
  }
1703
1806
 
1704
- updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined);
1807
+ updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined, content);
1705
1808
 
1706
1809
  setMessages(prev => prev.map(m =>
1707
1810
  m.id === streamAssistantId
@@ -1862,7 +1965,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1862
1965
  }
1863
1966
  }
1864
1967
 
1865
- updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined);
1968
+ updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined, content);
1866
1969
 
1867
1970
  // Skip creating a new botResponse since we already updated the streaming message
1868
1971
  } else {
@@ -1902,7 +2005,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1902
2005
  }
1903
2006
  }
1904
2007
 
1905
- updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined);
2008
+ updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined, content);
1906
2009
  const newBotResponse: Message = {
1907
2010
  id: (Date.now() + 1).toString(),
1908
2011
  content: mergedContent,
@@ -1928,7 +2031,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1928
2031
  .join(' ') || 'Sorry, I cannot understand your question'
1929
2032
  : response.reply || 'Sorry, I cannot understand your question';
1930
2033
 
1931
- updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined);
2034
+ updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined, content);
1932
2035
 
1933
2036
  const newBotResponse: Message = {
1934
2037
  id: (Date.now() + 1).toString(),
@@ -1968,38 +2071,6 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1968
2071
  botResponse = newBotResponse;
1969
2072
  }
1970
2073
 
1971
- // ===== PII Protection: Update user message with anonymized version =====
1972
- const anonymizedUserMsg = response.piiProtection?.anonymizedUserMessage;
1973
- const piiMappingData = response.piiProtection?.mapping ?? undefined;
1974
- if (anonymizedUserMsg) {
1975
- setMessages(prev => {
1976
- const updated = [...prev];
1977
- for (let i = updated.length - 1; i >= 0; i--) {
1978
- if (updated[i].role === 'user') {
1979
- updated[i] = {
1980
- ...updated[i],
1981
- piiProtection: { anonymizedContent: anonymizedUserMsg, mapping: piiMappingData },
1982
- piiStatus: 'complete',
1983
- };
1984
- break;
1985
- }
1986
- }
1987
- return updated;
1988
- });
1989
- } else if (piiDisplayMode === 'redacted') {
1990
- // No PII detected — set piiStatus to 'none' to trigger "No sensitive data" indicator
1991
- setMessages(prev => {
1992
- const updated = [...prev];
1993
- for (let i = updated.length - 1; i >= 0; i--) {
1994
- if (updated[i].role === 'user' && updated[i].piiStatus === 'scanning') {
1995
- updated[i] = { ...updated[i], piiStatus: 'none' };
1996
- break;
1997
- }
1998
- }
1999
- return updated;
2000
- });
2001
- }
2002
-
2003
2074
  // Execute redirect after all message processing is complete
2004
2075
  if (redirectUrl) {
2005
2076
  // Use setTimeout to ensure the message is displayed before redirect
@@ -2369,21 +2440,19 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2369
2440
  }, [nxtlinqApi, authFields, pseudoId]);
2370
2441
 
2371
2442
  // Handle preset message
2372
- const handlePresetMessage = (message: PresetMessage) => {
2443
+ const handlePresetMessage = async (message: PresetMessage): Promise<void> => {
2373
2444
  // If preset is configured as auto-send, avoid duplicate sends when user clicks repeatedly
2374
2445
  if (message.autoSend) {
2375
2446
  const trimmedText = (message.text || '').trim();
2376
2447
  if (!trimmedText) return;
2377
2448
 
2378
2449
  // Prevent sending messages while AI Agent is processing
2379
- if (isLoading) {
2450
+ if (isLoading || presetSendInFlightRef.current) {
2380
2451
  return;
2381
2452
  }
2382
2453
 
2383
- // If this exact preset text was just sent (by auto-send / manual / preset), skip to prevent duplicates
2384
- if (lastAutoSentTranscriptRef.current === trimmedText) {
2385
- return;
2386
- }
2454
+ presetSendInFlightRef.current = true;
2455
+ setSuggestions(previous => filterSuggestions(previous, trimmedText));
2387
2456
 
2388
2457
  // For preset messages, we need to add the user message first since sendMessage won't add it on retries
2389
2458
  const userMessage: Message = {
@@ -2403,7 +2472,11 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2403
2472
  lastAutoSentTranscriptRef.current = trimmedText;
2404
2473
 
2405
2474
  // Pass a flag to indicate this is a preset message so sendMessage won't add user message again
2406
- sendMessage(trimmedText, 0, true);
2475
+ try {
2476
+ await sendMessage(trimmedText, 0, true);
2477
+ } finally {
2478
+ presetSendInFlightRef.current = false;
2479
+ }
2407
2480
  } else {
2408
2481
  setInputValue(message.text);
2409
2482
  }
@@ -2502,8 +2575,11 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2502
2575
  metadataCid,
2503
2576
  };
2504
2577
 
2578
+ const nextPermissions = newPermissions || permissions;
2579
+ aitRef.current = aitInfo;
2580
+ permissionsRef.current = nextPermissions;
2505
2581
  setAit(aitInfo);
2506
- setPermissions(newPermissions || permissions);
2582
+ setPermissions(nextPermissions);
2507
2583
  };
2508
2584
 
2509
2585
 
@@ -2600,8 +2676,13 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2600
2676
  const shouldCreateAsAIAgent = !!aitRef.current; // Only create as AI Agent if user already has an AIT
2601
2677
 
2602
2678
  await generateAndRegisterAITWithSigner(validPermissions, shouldCreateAsAIAgent, currentSigner, currentHitAddress);
2679
+ const permissionsPersisted = await waitForPersistedAITPermissions(validPermissions);
2680
+ if (!permissionsPersisted) {
2681
+ showWarning('AIT permission was submitted but is still synchronizing. Please try again shortly.');
2682
+ setIsAITEnabling(false);
2683
+ return false;
2684
+ }
2603
2685
  showSuccess('AIT permission enabled successfully! You can now use the AI agent.');
2604
- await refreshAIT(true);
2605
2686
  setIsAITEnabling(false);
2606
2687
  return true;
2607
2688
  } catch (error) {
@@ -2691,6 +2772,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2691
2772
  customUsername: (!requireWalletIDVVerification && customUsername) ? getFinalCustomUsername(customUsername) : undefined
2692
2773
  }, token);
2693
2774
  if (!('error' in aitResponse)) {
2775
+ aitRef.current = aitResponse;
2694
2776
  setAit(aitResponse);
2695
2777
  }
2696
2778
  }
@@ -2730,6 +2812,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2730
2812
  customUsername: (!requireWalletIDVVerification && customUsername) ? getFinalCustomUsername(customUsername) : undefined
2731
2813
  }, token);
2732
2814
  if (!('error' in aitResponse)) {
2815
+ aitRef.current = aitResponse;
2733
2816
  setAit(aitResponse);
2734
2817
  }
2735
2818
  }
@@ -2846,6 +2929,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2846
2929
  // Set loading state when permission form opens
2847
2930
  React.useEffect(() => {
2848
2931
  if (isPermissionFormOpen && hitAddress) {
2932
+ authorizationLoadingStateRef.current.setAITLoading(true);
2849
2933
  setIsAITLoading(true);
2850
2934
  }
2851
2935
  }, [isPermissionFormOpen, hitAddress]);
@@ -0,0 +1,35 @@
1
+ type PiiMessage = {
2
+ piiStatus?: 'scanning' | 'complete' | 'none';
3
+ piiStep?: 0 | 1 | 2;
4
+ piiProtection?: {
5
+ anonymizedContent?: string;
6
+ mapping?: Record<string, string>;
7
+ };
8
+ };
9
+
10
+ export type PiiScanCompleteData = {
11
+ entityCount?: number;
12
+ anonymizedUserMessage?: string | null;
13
+ mapping?: Record<string, string> | null;
14
+ };
15
+
16
+ export function completePiiScan<T extends PiiMessage>(message: T, data?: PiiScanCompleteData): T {
17
+ const mapping = data?.mapping || {};
18
+ const entityCount = typeof data?.entityCount === 'number'
19
+ ? data.entityCount
20
+ : Object.keys(mapping).length;
21
+
22
+ if (entityCount <= 0 || !data?.anonymizedUserMessage) {
23
+ return { ...message, piiStatus: 'none', piiStep: 1 } as T;
24
+ }
25
+
26
+ return {
27
+ ...message,
28
+ piiStatus: 'complete',
29
+ piiStep: 1,
30
+ piiProtection: {
31
+ anonymizedContent: data.anonymizedUserMessage,
32
+ mapping,
33
+ },
34
+ } as T;
35
+ }
@@ -0,0 +1,20 @@
1
+ import type { PresetMessage } from './types/ChatBotTypes';
2
+
3
+ export function normalizeSuggestionText(value: string): string {
4
+ return value.trim().replace(/\s+/g, ' ').toLocaleLowerCase();
5
+ }
6
+
7
+ export function filterSuggestions(
8
+ suggestions: readonly PresetMessage[],
9
+ lastUserMessage?: string,
10
+ ): PresetMessage[] {
11
+ const lastUserText = normalizeSuggestionText(lastUserMessage || '');
12
+ const seen = new Set<string>();
13
+
14
+ return suggestions.filter((suggestion) => {
15
+ const normalized = normalizeSuggestionText(suggestion.text || '');
16
+ if (!normalized || normalized === lastUserText || seen.has(normalized)) return false;
17
+ seen.add(normalized);
18
+ return true;
19
+ });
20
+ }
@@ -167,7 +167,7 @@ export interface ChatBotContextType {
167
167
  retryPendingTextTool: () => Promise<boolean>;
168
168
  sendMessage: (content: string, retryCount?: number, isPresetMessage?: boolean, attachments?: Attachment[]) => Promise<void>;
169
169
  handleSubmit: (e: React.FormEvent, attachments?: Attachment[]) => Promise<void>;
170
- handlePresetMessage: (message: PresetMessage) => void;
170
+ handlePresetMessage: (message: PresetMessage) => Promise<void>;
171
171
  savePermissions: (newPermissions?: string[]) => Promise<void>;
172
172
  enableAIT: (toolName: string) => Promise<boolean>;
173
173
  handleVerifyWalletClick: (method: 'berifyme' | 'custom') => Promise<void>;
@@ -4,11 +4,14 @@ import { css } from '@emotion/react';
4
4
  import { useChatBot } from '../context/ChatBotContext';
5
5
  import { PresetMessage } from '../types/ChatBotTypes';
6
6
  import { actionButton } from './styles/isolatedStyles';
7
+ import { filterSuggestions, normalizeSuggestionText } from '../suggestionState';
7
8
 
8
9
  export const PresetMessages: React.FC = () => {
9
- const { suggestions, handlePresetMessage } = useChatBot();
10
+ const { suggestions, messages, handlePresetMessage } = useChatBot();
11
+ const lastUserMessage = [...messages].reverse().find(message => message.role === 'user')?.content;
12
+ const visibleSuggestions = filterSuggestions(suggestions || [], lastUserMessage);
10
13
 
11
- if (!suggestions || suggestions.length === 0) {
14
+ if (visibleSuggestions.length === 0) {
12
15
  return null;
13
16
  }
14
17
 
@@ -22,10 +25,10 @@ export const PresetMessages: React.FC = () => {
22
25
  gap: 10px !important;
23
26
  align-items: center !important;
24
27
  `}>
25
- {suggestions.map((preset: PresetMessage, index: number) => (
28
+ {visibleSuggestions.map((preset: PresetMessage) => (
26
29
  <button
27
- key={index}
28
- onClick={() => handlePresetMessage(preset)}
30
+ key={normalizeSuggestionText(preset.text)}
31
+ onClick={() => { void handlePresetMessage(preset); }}
29
32
  css={css`
30
33
  ${actionButton}
31
34
  padding: 5px 10px !important;
@@ -47,4 +50,4 @@ export const PresetMessages: React.FC = () => {
47
50
  ))}
48
51
  </div>
49
52
  );
50
- };
53
+ };