@bytexbyte/nxtlinq-ai-agent-ui-react-development 0.4.4 → 0.4.7

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.
@@ -8,6 +8,9 @@ import { createNxtlinqApi, setApiHosts, synthesizeSpeechToBuffer, streamSpeechTo
8
8
  import { authorizeTextFrontendTool, hasRecordedWalletVerification, validateWalletSession, } from '@bytexbyte/nxtlinq-ai-agent-core-development';
9
9
  import { AuthorizationLoadingState } from '../authorizationLoadingState';
10
10
  import { PendingTextToolRetryController } from '../pendingTextToolRetry';
11
+ import { completePiiScan } from '../piiMessageState';
12
+ import { filterSuggestions } from '../suggestionState';
13
+ import { dispatchRuntimeAITCompleteReplacement, removeOneDeniedPermission, replaceCurrentMaximumDenyList, RuntimeAITMutationQueue, RuntimeAITRecomputeCoordinator, StaleRuntimeAITResponseError, runtimeAITContextKey, runtimeAITSubjectKey, runtimeAuthorizationMatchesContext, runtimeAuthorizationState, unavailableRuntimeAuthorization, useProviderRuntimeCommit, useRuntimeAITContextCommit, } from '../permissionState';
11
14
  const MIC_ENABLED_SESSION_KEY = 'chatbot-mic-enabled';
12
15
  const ChatBotContext = React.createContext(undefined);
13
16
  export const useChatBot = () => {
@@ -17,7 +20,7 @@ export const useChatBot = () => {
17
20
  }
18
21
  return context;
19
22
  };
20
- export const ChatBotProvider = ({ onMessage, onError, onToolUse, presetMessages = [], placeholder = 'Type a message...', className = '', maxRetries = 3, retryDelay = 2000, serviceId, apiKey, apiSecret, serviceToken, getAuthToken, environment = 'production', onVerifyWallet, permissionGroup, children,
23
+ export const ChatBotProvider = ({ onMessage, onError, onToolUse, presetMessages = [], placeholder = 'Type a message...', className = '', maxRetries = 3, retryDelay = 2000, serviceId, apiKey, apiSecret, serviceToken, getAuthToken, environment = 'production', onVerifyWallet, permissionGroup, roles, children,
21
24
  // AI Model related attributes
22
25
  onModelChange,
23
26
  // Storage mode configuration
@@ -34,8 +37,6 @@ idvBannerDismissSeconds = 86400,
34
37
  isStopRecordingOnSend = false,
35
38
  // Custom error message to display in chat
36
39
  customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
37
- // Set API hosts immediately based on environment (before any API calls)
38
- setApiHosts(environment);
39
40
  const nxtlinqApi = React.useMemo(() => (serviceToken || getAuthToken
40
41
  ? createNxtlinqApi({ apiKey, apiSecret, serviceToken, getAuthToken })
41
42
  : createNxtlinqApi(apiKey ?? '', apiSecret ?? '')), [apiKey, apiSecret, serviceToken, getAuthToken]);
@@ -87,7 +88,17 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
87
88
  const [hitAddress, setHitAddress] = React.useState(null);
88
89
  const [ait, setAit] = React.useState(null);
89
90
  const [permissions, setPermissions] = React.useState([]);
90
- const [availablePermissions, setAvailablePermissions] = React.useState([]);
91
+ const [maximumPermissions, setMaximumPermissions] = React.useState([]);
92
+ const [deniedPermissions, setDeniedPermissions] = React.useState([]);
93
+ const [runtimePermissionsUnavailable, setRuntimePermissionsUnavailable] = React.useState(false);
94
+ const [runtimeAuthorizationContextKey, setRuntimeAuthorizationContextKey] = React.useState(null);
95
+ const [servicePermissionCatalog, setServicePermissionCatalog] = React.useState([]);
96
+ const availablePermissions = React.useMemo(() => maximumPermissions.map((label) => servicePermissionCatalog.find((permission) => permission.label === label) ?? {
97
+ id: label,
98
+ label,
99
+ description: '',
100
+ groups: [],
101
+ }), [maximumPermissions, servicePermissionCatalog]);
91
102
  const [showPermissionForm, setShowPermissionForm] = React.useState(false);
92
103
  const [isPermissionFormOpen, setIsPermissionFormOpen] = React.useState(false);
93
104
  const [isAITLoading, setIsAITLoading] = React.useState(false);
@@ -96,6 +107,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
96
107
  const [walletInfo, setWalletInfo] = React.useState(null);
97
108
  const [isWalletLoading, setIsWalletLoading] = React.useState(false);
98
109
  const [isAutoConnecting, setIsAutoConnecting] = React.useState(false);
110
+ const runtimeRolesKey = JSON.stringify(roles ?? null);
99
111
  // Persistent data (always use localStorage)
100
112
  const [nxtlinqAITServiceAccessToken, setNxtlinqAITServiceAccessToken] = useLocalStorage('nxtlinqAITServiceAccessToken', '');
101
113
  const [pseudoId, setPseudoId] = useLocalStorage('pseudoId', uuidv4());
@@ -144,6 +156,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
144
156
  const hitAddressRef = React.useRef(hitAddress);
145
157
  const aitRef = React.useRef(ait);
146
158
  const permissionsRef = React.useRef(permissions);
159
+ const deniedPermissionsRef = React.useRef(deniedPermissions);
147
160
  const nxtlinqAITServiceAccessTokenRef = React.useRef(nxtlinqAITServiceAccessToken);
148
161
  const signerRef = React.useRef(signer);
149
162
  const authorizationLoadingStateRef = React.useRef(new AuthorizationLoadingState(isAITLoading, isAutoConnecting));
@@ -158,6 +171,9 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
158
171
  await sendMessageRef.current(request.content, 1, request.isPresetMessage, request.attachments);
159
172
  });
160
173
  }, []);
174
+ const runtimeRecomputeCoordinatorRef = React.useRef(new RuntimeAITRecomputeCoordinator());
175
+ const runtimeMutationQueueRef = React.useRef(new RuntimeAITMutationQueue());
176
+ useProviderRuntimeCommit(hitAddressRef, hitAddress, environment, setApiHosts);
161
177
  // Helper function to combine customUsername with Adilas customUserInfo
162
178
  const getFinalCustomUsername = React.useCallback((username) => {
163
179
  if (!username)
@@ -187,6 +203,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
187
203
  const textInputRef = React.useRef(null);
188
204
  const lastPartialRangeRef = React.useRef(null);
189
205
  const lastAutoSentTranscriptRef = React.useRef('');
206
+ const presetSendInFlightRef = React.useRef(false);
190
207
  const autoSendTimerRef = React.useRef(null);
191
208
  const isCorrectingRef = React.useRef(false);
192
209
  const lastCustomErrorRef = React.useRef(undefined);
@@ -476,9 +493,6 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
476
493
  }
477
494
  }, []);
478
495
  // Update refs when state changes
479
- React.useEffect(() => {
480
- hitAddressRef.current = hitAddress;
481
- }, [hitAddress]);
482
496
  React.useEffect(() => {
483
497
  aitRef.current = ait;
484
498
  }, [ait]);
@@ -491,6 +505,9 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
491
505
  React.useEffect(() => {
492
506
  authorizationLoadingStateRef.current.setAutoConnecting(isAutoConnecting);
493
507
  }, [isAutoConnecting]);
508
+ React.useEffect(() => {
509
+ deniedPermissionsRef.current = deniedPermissions;
510
+ }, [deniedPermissions]);
494
511
  React.useEffect(() => {
495
512
  nxtlinqAITServiceAccessTokenRef.current = nxtlinqAITServiceAccessToken;
496
513
  }, [nxtlinqAITServiceAccessToken]);
@@ -870,7 +887,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
870
887
  console.error('Failed to fetch permissions:', result.error);
871
888
  return undefined;
872
889
  }
873
- setAvailablePermissions(result.permissions);
890
+ setServicePermissionCatalog(result.permissions);
874
891
  return result.permissions;
875
892
  }
876
893
  catch (error) {
@@ -878,7 +895,74 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
878
895
  return undefined;
879
896
  }
880
897
  };
881
- // Refresh AIT
898
+ const clearRuntimeAuthorization = React.useCallback(() => {
899
+ const cleared = unavailableRuntimeAuthorization(deniedPermissionsRef.current);
900
+ const activeContext = runtimeRecomputeCoordinatorRef.current.getActiveContext();
901
+ aitRef.current = null;
902
+ permissionsRef.current = [];
903
+ setAit(cleared.ait);
904
+ setPermissions(cleared.effectivePermissions);
905
+ setMaximumPermissions(cleared.maximumPermissions);
906
+ setRuntimePermissionsUnavailable(cleared.unavailable);
907
+ setRuntimeAuthorizationContextKey(activeContext ? runtimeAITContextKey(activeContext) : null);
908
+ }, []);
909
+ const applyRuntimeResult = React.useCallback((result, context) => {
910
+ const next = runtimeAuthorizationState(result, context.controller, context.serviceId);
911
+ aitRef.current = next.ait;
912
+ permissionsRef.current = next.effectivePermissions;
913
+ setAit(next.ait);
914
+ setPermissions(next.effectivePermissions);
915
+ setMaximumPermissions(next.maximumPermissions);
916
+ setDeniedPermissions(next.deniedPermissions);
917
+ deniedPermissionsRef.current = next.deniedPermissions;
918
+ setRuntimePermissionsUnavailable(false);
919
+ setRuntimeAuthorizationContextKey(runtimeAITContextKey(context));
920
+ return result;
921
+ }, []);
922
+ const runtimeContext = React.useMemo(() => hitAddress ? {
923
+ controller: hitAddress,
924
+ serviceId,
925
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
926
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
927
+ } : null, [hitAddress, permissionGroup, runtimeRolesKey, serviceId]);
928
+ const invalidateCommittedRuntimeContext = React.useCallback(() => {
929
+ const activeContext = runtimeRecomputeCoordinatorRef.current.getActiveContext();
930
+ aitRef.current = null;
931
+ permissionsRef.current = [];
932
+ deniedPermissionsRef.current = [];
933
+ setAit(null);
934
+ setPermissions([]);
935
+ setMaximumPermissions([]);
936
+ setDeniedPermissions([]);
937
+ setRuntimePermissionsUnavailable(activeContext !== null);
938
+ setRuntimeAuthorizationContextKey(activeContext ? runtimeAITContextKey(activeContext) : null);
939
+ }, []);
940
+ useRuntimeAITContextCommit(runtimeRecomputeCoordinatorRef.current, runtimeContext, invalidateCommittedRuntimeContext);
941
+ const recomputeRuntimeAIT = React.useCallback(async (context) => {
942
+ const capturedContext = {
943
+ controller: context.controller,
944
+ serviceId: context.serviceId,
945
+ ...(context.roles !== undefined ? { roles: [...context.roles] } : {}),
946
+ ...(context.permissionGroup !== undefined ? { permissionGroup: context.permissionGroup } : {}),
947
+ };
948
+ return runtimeRecomputeCoordinatorRef.current.run(capturedContext, () => nxtlinqApi.ait.recomputeAIT(capturedContext), applyRuntimeResult, clearRuntimeAuthorization);
949
+ }, [applyRuntimeResult, clearRuntimeAuthorization, nxtlinqApi]);
950
+ const queueRuntimeAITRefresh = React.useCallback((context) => runtimeMutationQueueRef.current.run(runtimeAITSubjectKey(context), async () => {
951
+ runtimeRecomputeCoordinatorRef.current.assertContextActive(context);
952
+ return recomputeRuntimeAIT(context);
953
+ }), [recomputeRuntimeAIT]);
954
+ const convergeRuntimeAITSubject = React.useCallback(async (subjectKey) => {
955
+ const activeContext = runtimeRecomputeCoordinatorRef.current.getActiveContext();
956
+ if (!activeContext || runtimeAITSubjectKey(activeContext) !== subjectKey) {
957
+ return;
958
+ }
959
+ await recomputeRuntimeAIT(activeContext);
960
+ }, [recomputeRuntimeAIT]);
961
+ const dispatchRuntimeDenyListReplacement = React.useCallback(async (context, nextDeniedPermissions) => dispatchRuntimeAITCompleteReplacement(runtimeRecomputeCoordinatorRef.current, context, () => nxtlinqApi.ait.updatePermissionDenyList({
962
+ ...context,
963
+ deniedPermissions: nextDeniedPermissions,
964
+ }), convergeRuntimeAITSubject, clearRuntimeAuthorization), [clearRuntimeAuthorization, convergeRuntimeAITSubject, nxtlinqApi]);
965
+ // Refresh the authoritative runtime AIT state.
882
966
  const refreshAIT = async (forceUpdatePermissions = false) => {
883
967
  const currentHitAddress = hitAddressRef.current || hitAddress;
884
968
  const currentToken = nxtlinqAITServiceAccessTokenRef.current || nxtlinqAITServiceAccessToken;
@@ -887,8 +971,10 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
887
971
  permissionsRef.current = [];
888
972
  setAit(null);
889
973
  setPermissions([]);
974
+ setMaximumPermissions([]);
890
975
  setWalletInfo(null);
891
976
  authorizationLoadingStateRef.current.setAITLoading(false);
977
+ setRuntimePermissionsUnavailable(false);
892
978
  setIsAITLoading(false);
893
979
  return;
894
980
  }
@@ -919,52 +1005,24 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
919
1005
  console.error('Failed to fetch wallet info:', error);
920
1006
  }
921
1007
  }
922
- // AIT existence is not identity verification. In strict mode, stop before
923
- // the legacy AIT lookup can synthesize a custom wallet record.
924
1008
  if (currentToken && !walletAllowsAITLookup) {
925
- aitRef.current = null;
926
- permissionsRef.current = [];
927
- setAit(null);
928
- setPermissions([]);
1009
+ clearRuntimeAuthorization();
929
1010
  return;
930
1011
  }
931
- // Only try to fetch AIT if we have a token
1012
+ // Recompute is the sole mint/reuse path and the only permission authority.
932
1013
  if (currentToken) {
933
- const response = await nxtlinqApi.ait.getAITByServiceIdAndController({
934
- serviceId,
935
- controller: currentHitAddress,
936
- customUsername: (!requireWalletIDVVerification && customUsername) ? getFinalCustomUsername(customUsername) : undefined
937
- }, currentToken);
938
- if ('error' in response) {
939
- console.error('Failed to fetch AIT:', response.error);
940
- aitRef.current = null;
941
- permissionsRef.current = [];
942
- setAit(null);
943
- setPermissions([]);
944
- return;
945
- }
946
- aitRef.current = response;
947
- setAit(response);
948
- if (!isPermissionFormOpen || forceUpdatePermissions) {
949
- const newPermissions = response.metadata?.permissions || [];
950
- permissionsRef.current = newPermissions;
951
- setPermissions(newPermissions);
1014
+ if (runtimeContext) {
1015
+ await queueRuntimeAITRefresh(runtimeContext);
952
1016
  }
953
1017
  }
954
1018
  else {
955
- // No token available, clear AIT data
956
- aitRef.current = null;
957
- permissionsRef.current = [];
958
- setAit(null);
959
- setPermissions([]);
1019
+ clearRuntimeAuthorization();
960
1020
  }
961
1021
  }
962
1022
  catch (error) {
963
- console.error('Failed to fetch AIT:', error);
964
- aitRef.current = null;
965
- permissionsRef.current = [];
966
- setAit(null);
967
- setPermissions([]);
1023
+ if (!(error instanceof StaleRuntimeAITResponseError)) {
1024
+ console.error('Failed to recompute AIT:', error);
1025
+ }
968
1026
  }
969
1027
  finally {
970
1028
  // Keep the authorization snapshot current before refreshAIT resolves.
@@ -1151,9 +1209,13 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1151
1209
  const { accessToken } = response;
1152
1210
  setNxtlinqAITServiceAccessToken(accessToken);
1153
1211
  nxtlinqAITServiceAccessTokenRef.current = accessToken;
1154
- // Wait for state to update before refreshing AIT
1155
- await new Promise(resolve => setTimeout(resolve, 100));
1156
- await refreshAIT();
1212
+ localStorage.setItem('nxtlinqAITServiceAccessToken', JSON.stringify(accessToken));
1213
+ await queueRuntimeAITRefresh({
1214
+ controller: addressToUse,
1215
+ serviceId,
1216
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
1217
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
1218
+ });
1157
1219
  if (autoShowSuccessMessage) {
1158
1220
  showSuccess(walletTextUtils.getWalletText('Successfully signed in with your HIT wallet. You can now use the AI agent.', serviceId));
1159
1221
  }
@@ -1188,6 +1250,8 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1188
1250
  loading: authorizationLoadingStateRef.current.isLoading(),
1189
1251
  },
1190
1252
  ...authFields(),
1253
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
1254
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
1191
1255
  customUsername: (!requireWalletIDVVerification && customUsername)
1192
1256
  ? getFinalCustomUsername(customUsername)
1193
1257
  : undefined,
@@ -1252,7 +1316,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1252
1316
  }
1253
1317
  return effectiveAvailableModels[safeIndex];
1254
1318
  }, [effectiveAvailableModels, selectedModelIndex]);
1255
- const updateSuggestions = React.useCallback(async (pseudoId, externalId) => {
1319
+ const updateSuggestions = React.useCallback(async (pseudoId, externalId, lastUserMessage) => {
1256
1320
  const result = await nxtlinqApi.agent.generateSuggestions({
1257
1321
  ...authFields(),
1258
1322
  pseudoId,
@@ -1263,11 +1327,8 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1263
1327
  setSuggestions([]);
1264
1328
  return;
1265
1329
  }
1266
- setSuggestions(result.suggestions.map((sug) => ({
1267
- text: sug,
1268
- autoSend: true
1269
- })));
1270
- }, []);
1330
+ setSuggestions(filterSuggestions(result.suggestions.map((sug) => ({ text: sug, autoSend: true })), lastUserMessage));
1331
+ }, [nxtlinqApi, authFields, setSuggestions]);
1271
1332
  // Updated sendMessage function to support different AI models and attachments
1272
1333
  const sendMessage = async (content, retryCount = 0, isPresetMessage = false, attachments, clientPipelineOverride) => {
1273
1334
  const hasContent = content.trim() || (attachments && attachments.length > 0);
@@ -1387,15 +1448,15 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1387
1448
  const updated = [...prev];
1388
1449
  for (let i = updated.length - 1; i >= 0; i--) {
1389
1450
  if (updated[i].role === 'user' && updated[i].piiStatus === 'scanning') {
1390
- const patch = { piiStep };
1391
- // scan_complete carries anonymized data set piiProtection early for immediate redact
1392
- if (step === 'scan_complete' && data?.anonymizedUserMessage) {
1393
- patch.piiProtection = {
1394
- anonymizedContent: data.anonymizedUserMessage,
1395
- mapping: data.mapping || {},
1396
- };
1451
+ if (step === 'scan_complete') {
1452
+ // PII work is complete independently of any later tool
1453
+ // authorization. Never leave the bubble in Sending while
1454
+ // waiting for wallet recovery or permission elevation.
1455
+ updated[i] = completePiiScan(updated[i], data);
1456
+ }
1457
+ else {
1458
+ updated[i] = { ...updated[i], piiStep };
1397
1459
  }
1398
- updated[i] = { ...updated[i], ...patch };
1399
1460
  break;
1400
1461
  }
1401
1462
  }
@@ -1403,6 +1464,26 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1403
1464
  });
1404
1465
  } : undefined,
1405
1466
  });
1467
+ // PII scanning is complete when the Agent response arrives, regardless
1468
+ // of whether a later frontend-tool authorization step can continue.
1469
+ if (piiDisplayMode === 'redacted') {
1470
+ const anonymizedUserMsg = response.piiProtection?.anonymizedUserMessage;
1471
+ const piiMappingData = response.piiProtection?.mapping ?? undefined;
1472
+ setMessages(prev => {
1473
+ const updated = [...prev];
1474
+ for (let i = updated.length - 1; i >= 0; i--) {
1475
+ if (updated[i].role === 'user' && updated[i].piiStatus === 'scanning') {
1476
+ updated[i] = completePiiScan(updated[i], {
1477
+ entityCount: piiMappingData ? Object.keys(piiMappingData).length : 0,
1478
+ anonymizedUserMessage: anonymizedUserMsg,
1479
+ mapping: piiMappingData,
1480
+ });
1481
+ break;
1482
+ }
1483
+ }
1484
+ return updated;
1485
+ });
1486
+ }
1406
1487
  if (!('error' in response && response.error)) {
1407
1488
  const tr = response;
1408
1489
  if (tr.ttsVoice &&
@@ -1527,7 +1608,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1527
1608
  if (!replyText.trim()) {
1528
1609
  replyText = 'Sorry, I cannot understand your question';
1529
1610
  }
1530
- updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined);
1611
+ updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined, content);
1531
1612
  setMessages(prev => prev.map(m => m.id === streamAssistantId
1532
1613
  ? {
1533
1614
  ...m,
@@ -1666,7 +1747,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1666
1747
  // Don't block the UI if update fails
1667
1748
  }
1668
1749
  }
1669
- updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined);
1750
+ updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined, content);
1670
1751
  // Skip creating a new botResponse since we already updated the streaming message
1671
1752
  }
1672
1753
  else {
@@ -1705,7 +1786,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1705
1786
  // Don't block the UI if update fails
1706
1787
  }
1707
1788
  }
1708
- updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined);
1789
+ updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined, content);
1709
1790
  const newBotResponse = {
1710
1791
  id: (Date.now() + 1).toString(),
1711
1792
  content: mergedContent,
@@ -1731,7 +1812,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1731
1812
  .map((item) => item.text)
1732
1813
  .join(' ') || 'Sorry, I cannot understand your question'
1733
1814
  : response.reply || 'Sorry, I cannot understand your question';
1734
- updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined);
1815
+ updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined, content);
1735
1816
  const newBotResponse = {
1736
1817
  id: (Date.now() + 1).toString(),
1737
1818
  content: replyText,
@@ -1769,38 +1850,6 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
1769
1850
  setMessages(prev => [...prev, newBotResponse]);
1770
1851
  botResponse = newBotResponse;
1771
1852
  }
1772
- // ===== PII Protection: Update user message with anonymized version =====
1773
- const anonymizedUserMsg = response.piiProtection?.anonymizedUserMessage;
1774
- const piiMappingData = response.piiProtection?.mapping ?? undefined;
1775
- if (anonymizedUserMsg) {
1776
- setMessages(prev => {
1777
- const updated = [...prev];
1778
- for (let i = updated.length - 1; i >= 0; i--) {
1779
- if (updated[i].role === 'user') {
1780
- updated[i] = {
1781
- ...updated[i],
1782
- piiProtection: { anonymizedContent: anonymizedUserMsg, mapping: piiMappingData },
1783
- piiStatus: 'complete',
1784
- };
1785
- break;
1786
- }
1787
- }
1788
- return updated;
1789
- });
1790
- }
1791
- else if (piiDisplayMode === 'redacted') {
1792
- // No PII detected — set piiStatus to 'none' to trigger "No sensitive data" indicator
1793
- setMessages(prev => {
1794
- const updated = [...prev];
1795
- for (let i = updated.length - 1; i >= 0; i--) {
1796
- if (updated[i].role === 'user' && updated[i].piiStatus === 'scanning') {
1797
- updated[i] = { ...updated[i], piiStatus: 'none' };
1798
- break;
1799
- }
1800
- }
1801
- return updated;
1802
- });
1803
- }
1804
1853
  // Execute redirect after all message processing is complete
1805
1854
  if (redirectUrl) {
1806
1855
  // Use setTimeout to ensure the message is displayed before redirect
@@ -2141,20 +2190,18 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2141
2190
  return { url: result.url };
2142
2191
  }, [nxtlinqApi, authFields, pseudoId]);
2143
2192
  // Handle preset message
2144
- const handlePresetMessage = (message) => {
2193
+ const handlePresetMessage = async (message) => {
2145
2194
  // If preset is configured as auto-send, avoid duplicate sends when user clicks repeatedly
2146
2195
  if (message.autoSend) {
2147
2196
  const trimmedText = (message.text || '').trim();
2148
2197
  if (!trimmedText)
2149
2198
  return;
2150
2199
  // Prevent sending messages while AI Agent is processing
2151
- if (isLoading) {
2152
- return;
2153
- }
2154
- // If this exact preset text was just sent (by auto-send / manual / preset), skip to prevent duplicates
2155
- if (lastAutoSentTranscriptRef.current === trimmedText) {
2200
+ if (isLoading || presetSendInFlightRef.current) {
2156
2201
  return;
2157
2202
  }
2203
+ presetSendInFlightRef.current = true;
2204
+ setSuggestions(previous => filterSuggestions(previous, trimmedText));
2158
2205
  // For preset messages, we need to add the user message first since sendMessage won't add it on retries
2159
2206
  const userMessage = {
2160
2207
  id: Date.now().toString(),
@@ -2171,91 +2218,17 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2171
2218
  // Mark as last sent to guard against rapid re-clicks and other duplicate flows
2172
2219
  lastAutoSentTranscriptRef.current = trimmedText;
2173
2220
  // Pass a flag to indicate this is a preset message so sendMessage won't add user message again
2174
- sendMessage(trimmedText, 0, true);
2221
+ try {
2222
+ await sendMessage(trimmedText, 0, true);
2223
+ }
2224
+ finally {
2225
+ presetSendInFlightRef.current = false;
2226
+ }
2175
2227
  }
2176
2228
  else {
2177
2229
  setInputValue(message.text);
2178
2230
  }
2179
2231
  };
2180
- // Generate and register AIT
2181
- const generateAndRegisterAIT = async (newPermissions, isFromAIAgent = false) => {
2182
- let currentSigner = signer;
2183
- let currentAddress = hitAddress;
2184
- // If we have an address but no signer, try to auto-connect wallet
2185
- if (currentAddress && !currentSigner) {
2186
- const storedWalletAddress = localStorage.getItem('walletAddress');
2187
- if (storedWalletAddress === currentAddress) {
2188
- try {
2189
- const autoConnectResult = await connectWallet(false); // Don't show sign-in message yet
2190
- if (autoConnectResult) {
2191
- // Wait for state to update
2192
- await new Promise(resolve => setTimeout(resolve, 1000));
2193
- // Get the updated signer and address
2194
- currentSigner = signerRef.current || signer;
2195
- currentAddress = hitAddressRef.current || hitAddress;
2196
- }
2197
- }
2198
- catch (error) {
2199
- console.error('Auto-connect failed during AIT generation:', error);
2200
- }
2201
- }
2202
- }
2203
- if (!currentSigner || !currentAddress) {
2204
- throw new Error('Missing signer or wallet address');
2205
- }
2206
- return generateAndRegisterAITWithSigner(newPermissions, isFromAIAgent, currentSigner, currentAddress);
2207
- };
2208
- // Generate and register AIT with explicit signer and address
2209
- const generateAndRegisterAITWithSigner = async (newPermissions, isFromAIAgent = false, explicitSigner, explicitAddress) => {
2210
- const currentSigner = explicitSigner || signer;
2211
- const currentAddress = explicitAddress || hitAddress;
2212
- if (!currentSigner || !currentAddress) {
2213
- throw new Error('Missing signer or wallet address');
2214
- }
2215
- // If AI Agent is creating, must check if user has existing AIT
2216
- if (isFromAIAgent && !aitRef.current) {
2217
- throw new Error('You must have an existing AIT before AI Agent can create new AITs');
2218
- }
2219
- const timestamp = Math.floor(Date.now() / 1000);
2220
- const aitId = `did:polygon:nxtlinq:${currentAddress}:${timestamp}`;
2221
- const metadata = {
2222
- permissions: newPermissions || permissions,
2223
- issuedBy: currentAddress,
2224
- };
2225
- const metadataStr = stringify(metadata);
2226
- // ethers v6: utils.keccak256 -> keccak256, utils.toUtf8Bytes -> toUtf8Bytes
2227
- const metadataHash = ethers.keccak256(ethers.toUtf8Bytes(metadataStr));
2228
- const uploadResponse = await nxtlinqApi.metadata.createMetadata(metadata, nxtlinqAITServiceAccessToken || '');
2229
- if ('error' in uploadResponse) {
2230
- throw new Error(`Failed to upload metadata: ${uploadResponse.error}`);
2231
- }
2232
- const { metadataCid } = uploadResponse;
2233
- const createAITParams = {
2234
- aitId,
2235
- controller: currentAddress,
2236
- serviceId,
2237
- metadataHash,
2238
- metadataCid,
2239
- isFromAIAgent: isFromAIAgent,
2240
- parentAITId: isFromAIAgent ? aitRef.current?.aitId : undefined,
2241
- };
2242
- const createAITResponse = await nxtlinqApi.ait.createAIT(createAITParams, nxtlinqAITServiceAccessToken || '');
2243
- if ('error' in createAITResponse) {
2244
- throw new Error(`Failed to create AIT: ${createAITResponse.error}`);
2245
- }
2246
- const aitInfo = {
2247
- aitId,
2248
- controller: currentAddress,
2249
- metadata,
2250
- metadataHash,
2251
- metadataCid,
2252
- };
2253
- const nextPermissions = newPermissions || permissions;
2254
- aitRef.current = aitInfo;
2255
- permissionsRef.current = nextPermissions;
2256
- setAit(aitInfo);
2257
- setPermissions(nextPermissions);
2258
- };
2259
2232
  // Auto enable AIT permission
2260
2233
  const enableAIT = async (toolName) => {
2261
2234
  if (isAITEnabling)
@@ -2317,35 +2290,41 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2317
2290
  }
2318
2291
  }
2319
2292
  try {
2320
- // Get available permissions to find the tool
2321
- const availablePermissionLabels = availablePermissions.map(p => p.label);
2322
- if (!availablePermissionLabels.includes(toolName)) {
2293
+ const context = {
2294
+ controller: currentHitAddress,
2295
+ serviceId,
2296
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
2297
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
2298
+ };
2299
+ const enabled = await runtimeMutationQueueRef.current.run(runtimeAITSubjectKey(context), async () => {
2300
+ runtimeRecomputeCoordinatorRef.current.assertContextActive(context);
2301
+ const current = await recomputeRuntimeAIT(context);
2302
+ if (!current.maximumPermissions.includes(toolName)) {
2303
+ return false;
2304
+ }
2305
+ const nextDeniedPermissions = removeOneDeniedPermission(current.deniedPermissions, toolName);
2306
+ runtimeRecomputeCoordinatorRef.current.assertContextActive(context);
2307
+ await dispatchRuntimeDenyListReplacement(context, nextDeniedPermissions);
2308
+ return true;
2309
+ });
2310
+ if (!enabled) {
2323
2311
  showError(`Tool ${toolName} is not available for your current identity provider`);
2324
2312
  setIsAITEnabling(false);
2325
2313
  return false;
2326
2314
  }
2327
- // Get current permissions from AIT metadata instead of React state
2328
- // This ensures we don't lose existing permissions when enabling new ones
2329
- const currentAITPermissions = aitRef.current?.metadata?.permissions || permissions;
2330
- const newPermissions = [...currentAITPermissions];
2331
- if (!newPermissions.includes(toolName)) {
2332
- newPermissions.push(toolName);
2333
- }
2334
- // Filter out deleted permissions before saving
2335
- const validPermissions = newPermissions.filter(permission => availablePermissionLabels.includes(permission));
2336
- // Generate and register AIT with new permissions
2337
- // For auto-enable, we should create a regular AIT (not AI Agent AIT) if user doesn't have existing AIT
2338
- const shouldCreateAsAIAgent = !!aitRef.current; // Only create as AI Agent if user already has an AIT
2339
- await generateAndRegisterAITWithSigner(validPermissions, shouldCreateAsAIAgent, currentSigner, currentHitAddress);
2340
2315
  showSuccess('AIT permission enabled successfully! You can now use the AI agent.');
2341
- await refreshAIT(true);
2342
2316
  setIsAITEnabling(false);
2343
2317
  return true;
2344
2318
  }
2345
2319
  catch (error) {
2346
- console.error('Failed to auto-enable AIT:', error);
2320
+ if (!(error instanceof StaleRuntimeAITResponseError)) {
2321
+ console.error('Failed to auto-enable AIT:', error);
2322
+ }
2347
2323
  setIsAITEnabling(false);
2348
- if (error instanceof Error) {
2324
+ if (error instanceof StaleRuntimeAITResponseError) {
2325
+ return false;
2326
+ }
2327
+ else if (error instanceof Error) {
2349
2328
  showError(error.message);
2350
2329
  }
2351
2330
  else {
@@ -2358,11 +2337,23 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2358
2337
  const savePermissions = async (newPermissions) => {
2359
2338
  setIsDisabled(true);
2360
2339
  try {
2361
- // Filter out deleted permissions before saving
2362
- const permissionsToSave = newPermissions || permissions;
2363
- const availablePermissionLabels = availablePermissions.map(p => p.label);
2364
- const validPermissions = permissionsToSave.filter(permission => availablePermissionLabels.includes(permission));
2365
- await generateAndRegisterAIT(validPermissions, false);
2340
+ if (!hitAddress) {
2341
+ throw new Error('Please connect your wallet first');
2342
+ }
2343
+ const context = {
2344
+ controller: hitAddress,
2345
+ serviceId,
2346
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
2347
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
2348
+ };
2349
+ const checkedPermissions = [...(newPermissions ?? permissions)];
2350
+ await runtimeMutationQueueRef.current.run(runtimeAITSubjectKey(context), async () => {
2351
+ runtimeRecomputeCoordinatorRef.current.assertContextActive(context);
2352
+ const fresh = await recomputeRuntimeAIT(context);
2353
+ const nextDeniedPermissions = replaceCurrentMaximumDenyList(fresh.deniedPermissions, fresh.maximumPermissions, checkedPermissions);
2354
+ runtimeRecomputeCoordinatorRef.current.assertContextActive(context);
2355
+ await dispatchRuntimeDenyListReplacement(context, nextDeniedPermissions);
2356
+ });
2366
2357
  showSuccess('AIT permissions saved successfully! You can now use the AI agent with your configured permissions.');
2367
2358
  setShowPermissionForm(false);
2368
2359
  setIsPermissionFormOpen(false);
@@ -2370,9 +2361,14 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2370
2361
  await retryPendingTextTool();
2371
2362
  }
2372
2363
  catch (error) {
2373
- console.error('Failed to generate AIT:', error);
2364
+ if (!(error instanceof StaleRuntimeAITResponseError)) {
2365
+ console.error('Failed to update AIT deny-list:', error);
2366
+ }
2374
2367
  setIsDisabled(false);
2375
- if (error instanceof Error) {
2368
+ if (error instanceof StaleRuntimeAITResponseError) {
2369
+ return;
2370
+ }
2371
+ else if (error instanceof Error) {
2376
2372
  showError(error.message);
2377
2373
  }
2378
2374
  else {
@@ -2418,15 +2414,12 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2418
2414
  const walletResponse = await nxtlinqApi.wallet.getWallet({ address }, token);
2419
2415
  if (!('error' in walletResponse)) {
2420
2416
  setWalletInfo(walletResponse);
2421
- const aitResponse = await nxtlinqApi.ait.getAITByServiceIdAndController({
2422
- serviceId,
2417
+ await queueRuntimeAITRefresh({
2423
2418
  controller: address,
2424
- customUsername: (!requireWalletIDVVerification && customUsername) ? getFinalCustomUsername(customUsername) : undefined
2425
- }, token);
2426
- if (!('error' in aitResponse)) {
2427
- aitRef.current = aitResponse;
2428
- setAit(aitResponse);
2429
- }
2419
+ serviceId,
2420
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
2421
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
2422
+ });
2430
2423
  }
2431
2424
  }
2432
2425
  finally {
@@ -2460,15 +2453,12 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2460
2453
  const walletResponse = await nxtlinqApi.wallet.getWallet({ address }, token);
2461
2454
  if (!('error' in walletResponse)) {
2462
2455
  setWalletInfo(walletResponse);
2463
- const aitResponse = await nxtlinqApi.ait.getAITByServiceIdAndController({
2464
- serviceId,
2456
+ await queueRuntimeAITRefresh({
2465
2457
  controller: address,
2466
- customUsername: (!requireWalletIDVVerification && customUsername) ? getFinalCustomUsername(customUsername) : undefined
2467
- }, token);
2468
- if (!('error' in aitResponse)) {
2469
- aitRef.current = aitResponse;
2470
- setAit(aitResponse);
2471
- }
2458
+ serviceId,
2459
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
2460
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
2461
+ });
2472
2462
  }
2473
2463
  }
2474
2464
  finally {
@@ -2580,7 +2570,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2580
2570
  if (hitAddress && nxtlinqAITServiceAccessToken) {
2581
2571
  refreshAIT();
2582
2572
  }
2583
- }, [hitAddress, nxtlinqAITServiceAccessToken]);
2573
+ }, [hitAddress, nxtlinqAITServiceAccessToken, permissionGroup, runtimeRolesKey, serviceId]);
2584
2574
  // Set loading state when permission form opens
2585
2575
  React.useEffect(() => {
2586
2576
  if (isPermissionFormOpen && hitAddress) {
@@ -2660,6 +2650,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2660
2650
  };
2661
2651
  updateExternalId();
2662
2652
  }, [hitAddress, nxtlinqApi, authFields, pseudoId]);
2653
+ const runtimeAuthorizationMatchesRenderedContext = runtimeAuthorizationMatchesContext(runtimeAuthorizationContextKey, runtimeContext);
2663
2654
  const contextValue = {
2664
2655
  // State
2665
2656
  messages,
@@ -2667,9 +2658,16 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2667
2658
  isLoading,
2668
2659
  isOpen,
2669
2660
  hitAddress,
2670
- ait,
2671
- permissions,
2672
- availablePermissions,
2661
+ ait: runtimeAuthorizationMatchesRenderedContext ? ait : null,
2662
+ permissions: runtimeAuthorizationMatchesRenderedContext ? permissions : [],
2663
+ maximumPermissions: runtimeAuthorizationMatchesRenderedContext ? maximumPermissions : [],
2664
+ deniedPermissions: runtimeAuthorizationMatchesRenderedContext ? deniedPermissions : [],
2665
+ runtimePermissionsUnavailable: runtimeAuthorizationMatchesRenderedContext
2666
+ ? runtimePermissionsUnavailable
2667
+ : runtimeContext !== null,
2668
+ availablePermissions: runtimeAuthorizationMatchesRenderedContext
2669
+ ? availablePermissions
2670
+ : [],
2673
2671
  showPermissionForm,
2674
2672
  isPermissionFormOpen,
2675
2673
  isAITLoading,
@@ -2705,7 +2703,6 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2705
2703
  setIsOpen,
2706
2704
  setShowPermissionForm,
2707
2705
  setIsPermissionFormOpen,
2708
- setPermissions,
2709
2706
  setIsDisabled,
2710
2707
  setIsWalletLoading,
2711
2708
  setNotification,
@@ -2754,6 +2751,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2754
2751
  onVerifyWallet: (method) => handleVerifyWalletClick(method),
2755
2752
  serviceId,
2756
2753
  permissionGroup,
2754
+ roles,
2757
2755
  // Props
2758
2756
  props: {
2759
2757
  onMessage,
@@ -2771,6 +2769,7 @@ customError, voiceThresholds, debugVoiceRms = false, sttGlossary, }) => {
2771
2769
  getAuthToken,
2772
2770
  onVerifyWallet,
2773
2771
  permissionGroup,
2772
+ roles,
2774
2773
  onModelChange,
2775
2774
  storageMode,
2776
2775
  requireWalletIDVVerification,