@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.
@@ -23,6 +23,7 @@ import type {
23
23
  Attachment,
24
24
  ClientTtsVoiceSettings,
25
25
  Message,
26
+ RuntimeAITResult,
26
27
  ServicePermission,
27
28
  } from '@bytexbyte/nxtlinq-ai-agent-core-development';
28
29
  import {
@@ -38,6 +39,24 @@ import {
38
39
  } from '../types/ChatBotTypes';
39
40
  import { AuthorizationLoadingState } from '../authorizationLoadingState';
40
41
  import { PendingTextToolRetryController } from '../pendingTextToolRetry';
42
+ import { completePiiScan } from '../piiMessageState';
43
+ import { filterSuggestions } from '../suggestionState';
44
+ import {
45
+ dispatchRuntimeAITCompleteReplacement,
46
+ removeOneDeniedPermission,
47
+ replaceCurrentMaximumDenyList,
48
+ RuntimeAITMutationQueue,
49
+ RuntimeAITRecomputeCoordinator,
50
+ StaleRuntimeAITResponseError,
51
+ runtimeAITContextKey,
52
+ runtimeAITSubjectKey,
53
+ runtimeAuthorizationMatchesContext,
54
+ runtimeAuthorizationState,
55
+ unavailableRuntimeAuthorization,
56
+ useProviderRuntimeCommit,
57
+ useRuntimeAITContextCommit,
58
+ type RuntimeAITContext,
59
+ } from '../permissionState';
41
60
 
42
61
  const MIC_ENABLED_SESSION_KEY = 'chatbot-mic-enabled';
43
62
 
@@ -68,6 +87,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
68
87
  environment = 'production',
69
88
  onVerifyWallet,
70
89
  permissionGroup,
90
+ roles,
71
91
  children,
72
92
  // AI Model related attributes
73
93
  onModelChange,
@@ -90,9 +110,6 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
90
110
  debugVoiceRms = false,
91
111
  sttGlossary,
92
112
  }) => {
93
- // Set API hosts immediately based on environment (before any API calls)
94
- setApiHosts(environment);
95
-
96
113
  const nxtlinqApi = React.useMemo(
97
114
  () => (serviceToken || getAuthToken
98
115
  ? createNxtlinqApi({ apiKey, apiSecret, serviceToken, getAuthToken })
@@ -152,7 +169,22 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
152
169
  const [hitAddress, setHitAddress] = React.useState<string | null>(null);
153
170
  const [ait, setAit] = React.useState<AIT | null>(null);
154
171
  const [permissions, setPermissions] = React.useState<string[]>([]);
155
- const [availablePermissions, setAvailablePermissions] = React.useState<ServicePermission[]>([]);
172
+ const [maximumPermissions, setMaximumPermissions] = React.useState<string[]>([]);
173
+ const [deniedPermissions, setDeniedPermissions] = React.useState<string[]>([]);
174
+ const [runtimePermissionsUnavailable, setRuntimePermissionsUnavailable] = React.useState(false);
175
+ const [runtimeAuthorizationContextKey, setRuntimeAuthorizationContextKey] =
176
+ React.useState<string | null>(null);
177
+ const [servicePermissionCatalog, setServicePermissionCatalog] = React.useState<ServicePermission[]>([]);
178
+ const availablePermissions = React.useMemo(
179
+ () => maximumPermissions.map((label) =>
180
+ servicePermissionCatalog.find((permission) => permission.label === label) ?? {
181
+ id: label,
182
+ label,
183
+ description: '',
184
+ groups: [],
185
+ }),
186
+ [maximumPermissions, servicePermissionCatalog],
187
+ );
156
188
  const [showPermissionForm, setShowPermissionForm] = React.useState(false);
157
189
  const [isPermissionFormOpen, setIsPermissionFormOpen] = React.useState(false);
158
190
  const [isAITLoading, setIsAITLoading] = React.useState(false);
@@ -161,6 +193,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
161
193
  const [walletInfo, setWalletInfo] = React.useState<any>(null);
162
194
  const [isWalletLoading, setIsWalletLoading] = React.useState(false);
163
195
  const [isAutoConnecting, setIsAutoConnecting] = React.useState(false);
196
+ const runtimeRolesKey = JSON.stringify(roles ?? null);
164
197
 
165
198
  // Persistent data (always use localStorage)
166
199
  const [nxtlinqAITServiceAccessToken, setNxtlinqAITServiceAccessToken] = useLocalStorage<string>('nxtlinqAITServiceAccessToken', '');
@@ -217,6 +250,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
217
250
  const hitAddressRef = React.useRef(hitAddress);
218
251
  const aitRef = React.useRef(ait);
219
252
  const permissionsRef = React.useRef(permissions);
253
+ const deniedPermissionsRef = React.useRef(deniedPermissions);
220
254
  const nxtlinqAITServiceAccessTokenRef = React.useRef(nxtlinqAITServiceAccessToken);
221
255
  const signerRef = React.useRef(signer);
222
256
  const authorizationLoadingStateRef = React.useRef(
@@ -238,6 +272,14 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
238
272
  );
239
273
  });
240
274
  }, []);
275
+ const runtimeRecomputeCoordinatorRef = React.useRef(new RuntimeAITRecomputeCoordinator());
276
+ const runtimeMutationQueueRef = React.useRef(new RuntimeAITMutationQueue());
277
+ useProviderRuntimeCommit(
278
+ hitAddressRef,
279
+ hitAddress,
280
+ environment,
281
+ setApiHosts,
282
+ );
241
283
 
242
284
  // Helper function to combine customUsername with Adilas customUserInfo
243
285
  const getFinalCustomUsername = React.useCallback((username: string | undefined): string | undefined => {
@@ -268,6 +310,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
268
310
  const textInputRef = React.useRef<HTMLInputElement | null>(null);
269
311
  const lastPartialRangeRef = React.useRef<{ start: number; end: number } | null>(null);
270
312
  const lastAutoSentTranscriptRef = React.useRef<string>('');
313
+ const presetSendInFlightRef = React.useRef(false);
271
314
  const autoSendTimerRef = React.useRef<NodeJS.Timeout | null>(null);
272
315
  const isCorrectingRef = React.useRef(false);
273
316
  const lastCustomErrorRef = React.useRef<string | undefined>(undefined);
@@ -570,10 +613,6 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
570
613
  }, []);
571
614
 
572
615
  // Update refs when state changes
573
- React.useEffect(() => {
574
- hitAddressRef.current = hitAddress;
575
- }, [hitAddress]);
576
-
577
616
  React.useEffect(() => {
578
617
  aitRef.current = ait;
579
618
  }, [ait]);
@@ -590,6 +629,10 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
590
629
  authorizationLoadingStateRef.current.setAutoConnecting(isAutoConnecting);
591
630
  }, [isAutoConnecting]);
592
631
 
632
+ React.useEffect(() => {
633
+ deniedPermissionsRef.current = deniedPermissions;
634
+ }, [deniedPermissions]);
635
+
593
636
  React.useEffect(() => {
594
637
  nxtlinqAITServiceAccessTokenRef.current = nxtlinqAITServiceAccessToken;
595
638
  }, [nxtlinqAITServiceAccessToken]);
@@ -1020,7 +1063,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1020
1063
  console.error('Failed to fetch permissions:', result.error);
1021
1064
  return undefined;
1022
1065
  }
1023
- setAvailablePermissions(result.permissions);
1066
+ setServicePermissionCatalog(result.permissions);
1024
1067
  return result.permissions;
1025
1068
  } catch (error) {
1026
1069
  console.error('Error fetching permissions:', error);
@@ -1028,7 +1071,109 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1028
1071
  }
1029
1072
  };
1030
1073
 
1031
- // Refresh AIT
1074
+ const clearRuntimeAuthorization = React.useCallback(() => {
1075
+ const cleared = unavailableRuntimeAuthorization(deniedPermissionsRef.current);
1076
+ const activeContext = runtimeRecomputeCoordinatorRef.current.getActiveContext();
1077
+ aitRef.current = null;
1078
+ permissionsRef.current = [];
1079
+ setAit(cleared.ait);
1080
+ setPermissions(cleared.effectivePermissions);
1081
+ setMaximumPermissions(cleared.maximumPermissions);
1082
+ setRuntimePermissionsUnavailable(cleared.unavailable);
1083
+ setRuntimeAuthorizationContextKey(
1084
+ activeContext ? runtimeAITContextKey(activeContext) : null,
1085
+ );
1086
+ }, []);
1087
+
1088
+ const applyRuntimeResult = React.useCallback((result: RuntimeAITResult, context: RuntimeAITContext) => {
1089
+ const next = runtimeAuthorizationState(result, context.controller, context.serviceId);
1090
+ aitRef.current = next.ait;
1091
+ permissionsRef.current = next.effectivePermissions;
1092
+ setAit(next.ait);
1093
+ setPermissions(next.effectivePermissions);
1094
+ setMaximumPermissions(next.maximumPermissions);
1095
+ setDeniedPermissions(next.deniedPermissions);
1096
+ deniedPermissionsRef.current = next.deniedPermissions;
1097
+ setRuntimePermissionsUnavailable(false);
1098
+ setRuntimeAuthorizationContextKey(runtimeAITContextKey(context));
1099
+ return result;
1100
+ }, []);
1101
+
1102
+ const runtimeContext = React.useMemo<RuntimeAITContext | null>(() => hitAddress ? {
1103
+ controller: hitAddress,
1104
+ serviceId,
1105
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
1106
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
1107
+ } : null, [hitAddress, permissionGroup, runtimeRolesKey, serviceId]);
1108
+ const invalidateCommittedRuntimeContext = React.useCallback(() => {
1109
+ const activeContext = runtimeRecomputeCoordinatorRef.current.getActiveContext();
1110
+ aitRef.current = null;
1111
+ permissionsRef.current = [];
1112
+ deniedPermissionsRef.current = [];
1113
+ setAit(null);
1114
+ setPermissions([]);
1115
+ setMaximumPermissions([]);
1116
+ setDeniedPermissions([]);
1117
+ setRuntimePermissionsUnavailable(activeContext !== null);
1118
+ setRuntimeAuthorizationContextKey(
1119
+ activeContext ? runtimeAITContextKey(activeContext) : null,
1120
+ );
1121
+ }, []);
1122
+ useRuntimeAITContextCommit(
1123
+ runtimeRecomputeCoordinatorRef.current,
1124
+ runtimeContext,
1125
+ invalidateCommittedRuntimeContext,
1126
+ );
1127
+
1128
+ const recomputeRuntimeAIT = React.useCallback(async (context: RuntimeAITContext) => {
1129
+ const capturedContext: RuntimeAITContext = {
1130
+ controller: context.controller,
1131
+ serviceId: context.serviceId,
1132
+ ...(context.roles !== undefined ? { roles: [...context.roles] } : {}),
1133
+ ...(context.permissionGroup !== undefined ? { permissionGroup: context.permissionGroup } : {}),
1134
+ };
1135
+ return runtimeRecomputeCoordinatorRef.current.run(
1136
+ capturedContext,
1137
+ () => nxtlinqApi.ait.recomputeAIT(capturedContext),
1138
+ applyRuntimeResult,
1139
+ clearRuntimeAuthorization,
1140
+ );
1141
+ }, [applyRuntimeResult, clearRuntimeAuthorization, nxtlinqApi]);
1142
+
1143
+ const queueRuntimeAITRefresh = React.useCallback(
1144
+ (context: RuntimeAITContext) => runtimeMutationQueueRef.current.run(
1145
+ runtimeAITSubjectKey(context),
1146
+ async () => {
1147
+ runtimeRecomputeCoordinatorRef.current.assertContextActive(context);
1148
+ return recomputeRuntimeAIT(context);
1149
+ },
1150
+ ),
1151
+ [recomputeRuntimeAIT],
1152
+ );
1153
+
1154
+ const convergeRuntimeAITSubject = React.useCallback(async (subjectKey: string) => {
1155
+ const activeContext = runtimeRecomputeCoordinatorRef.current.getActiveContext();
1156
+ if (!activeContext || runtimeAITSubjectKey(activeContext) !== subjectKey) {
1157
+ return;
1158
+ }
1159
+ await recomputeRuntimeAIT(activeContext);
1160
+ }, [recomputeRuntimeAIT]);
1161
+
1162
+ const dispatchRuntimeDenyListReplacement = React.useCallback(async (
1163
+ context: RuntimeAITContext,
1164
+ nextDeniedPermissions: string[],
1165
+ ) => dispatchRuntimeAITCompleteReplacement(
1166
+ runtimeRecomputeCoordinatorRef.current,
1167
+ context,
1168
+ () => nxtlinqApi.ait.updatePermissionDenyList({
1169
+ ...context,
1170
+ deniedPermissions: nextDeniedPermissions,
1171
+ }),
1172
+ convergeRuntimeAITSubject,
1173
+ clearRuntimeAuthorization,
1174
+ ), [clearRuntimeAuthorization, convergeRuntimeAITSubject, nxtlinqApi]);
1175
+
1176
+ // Refresh the authoritative runtime AIT state.
1032
1177
  const refreshAIT = async (forceUpdatePermissions = false) => {
1033
1178
  const currentHitAddress = hitAddressRef.current || hitAddress;
1034
1179
  const currentToken = nxtlinqAITServiceAccessTokenRef.current || nxtlinqAITServiceAccessToken;
@@ -1037,8 +1182,10 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1037
1182
  permissionsRef.current = [];
1038
1183
  setAit(null);
1039
1184
  setPermissions([]);
1185
+ setMaximumPermissions([]);
1040
1186
  setWalletInfo(null);
1041
1187
  authorizationLoadingStateRef.current.setAITLoading(false);
1188
+ setRuntimePermissionsUnavailable(false);
1042
1189
  setIsAITLoading(false);
1043
1190
  return;
1044
1191
  }
@@ -1070,53 +1217,23 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1070
1217
  }
1071
1218
  }
1072
1219
 
1073
- // AIT existence is not identity verification. In strict mode, stop before
1074
- // the legacy AIT lookup can synthesize a custom wallet record.
1075
1220
  if (currentToken && !walletAllowsAITLookup) {
1076
- aitRef.current = null;
1077
- permissionsRef.current = [];
1078
- setAit(null);
1079
- setPermissions([]);
1221
+ clearRuntimeAuthorization();
1080
1222
  return;
1081
1223
  }
1082
1224
 
1083
- // Only try to fetch AIT if we have a token
1225
+ // Recompute is the sole mint/reuse path and the only permission authority.
1084
1226
  if (currentToken) {
1085
- const response = await nxtlinqApi.ait.getAITByServiceIdAndController({
1086
- serviceId,
1087
- controller: currentHitAddress,
1088
- customUsername: (!requireWalletIDVVerification && customUsername) ? getFinalCustomUsername(customUsername) : undefined
1089
- }, currentToken);
1090
-
1091
- if ('error' in response) {
1092
- console.error('Failed to fetch AIT:', response.error);
1093
- aitRef.current = null;
1094
- permissionsRef.current = [];
1095
- setAit(null);
1096
- setPermissions([]);
1097
- return;
1098
- }
1099
-
1100
- aitRef.current = response;
1101
- setAit(response);
1102
- if (!isPermissionFormOpen || forceUpdatePermissions) {
1103
- const newPermissions = response.metadata?.permissions || [];
1104
- permissionsRef.current = newPermissions;
1105
- setPermissions(newPermissions);
1227
+ if (runtimeContext) {
1228
+ await queueRuntimeAITRefresh(runtimeContext);
1106
1229
  }
1107
1230
  } else {
1108
- // No token available, clear AIT data
1109
- aitRef.current = null;
1110
- permissionsRef.current = [];
1111
- setAit(null);
1112
- setPermissions([]);
1231
+ clearRuntimeAuthorization();
1113
1232
  }
1114
1233
  } catch (error) {
1115
- console.error('Failed to fetch AIT:', error);
1116
- aitRef.current = null;
1117
- permissionsRef.current = [];
1118
- setAit(null);
1119
- setPermissions([]);
1234
+ if (!(error instanceof StaleRuntimeAITResponseError)) {
1235
+ console.error('Failed to recompute AIT:', error);
1236
+ }
1120
1237
  } finally {
1121
1238
  // Keep the authorization snapshot current before refreshAIT resolves.
1122
1239
  // React may not commit setState until the immediate pending-tool retry.
@@ -1321,10 +1438,13 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1321
1438
  const { accessToken } = response;
1322
1439
  setNxtlinqAITServiceAccessToken(accessToken);
1323
1440
  nxtlinqAITServiceAccessTokenRef.current = accessToken;
1324
-
1325
- // Wait for state to update before refreshing AIT
1326
- await new Promise(resolve => setTimeout(resolve, 100));
1327
- await refreshAIT();
1441
+ localStorage.setItem('nxtlinqAITServiceAccessToken', JSON.stringify(accessToken));
1442
+ await queueRuntimeAITRefresh({
1443
+ controller: addressToUse,
1444
+ serviceId,
1445
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
1446
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
1447
+ });
1328
1448
 
1329
1449
  if (autoShowSuccessMessage) {
1330
1450
  showSuccess(walletTextUtils.getWalletText('Successfully signed in with your HIT wallet. You can now use the AI agent.', serviceId));
@@ -1363,6 +1483,8 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1363
1483
  loading: authorizationLoadingStateRef.current.isLoading(),
1364
1484
  },
1365
1485
  ...authFields(),
1486
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
1487
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
1366
1488
  customUsername: (!requireWalletIDVVerification && customUsername)
1367
1489
  ? getFinalCustomUsername(customUsername)
1368
1490
  : undefined,
@@ -1433,7 +1555,11 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1433
1555
  return effectiveAvailableModels[safeIndex];
1434
1556
  }, [effectiveAvailableModels, selectedModelIndex]);
1435
1557
 
1436
- const updateSuggestions = React.useCallback(async (pseudoId: string, externalId?: string) => {
1558
+ const updateSuggestions = React.useCallback(async (
1559
+ pseudoId: string,
1560
+ externalId?: string,
1561
+ lastUserMessage?: string,
1562
+ ) => {
1437
1563
  const result = await nxtlinqApi.agent.generateSuggestions({
1438
1564
  ...authFields(),
1439
1565
  pseudoId,
@@ -1446,11 +1572,11 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1446
1572
  return;
1447
1573
  }
1448
1574
 
1449
- setSuggestions(result.suggestions.map((sug: string) => ({
1450
- text: sug,
1451
- autoSend: true
1452
- })));
1453
- }, [])
1575
+ setSuggestions(filterSuggestions(
1576
+ result.suggestions.map((sug: string) => ({ text: sug, autoSend: true })),
1577
+ lastUserMessage,
1578
+ ));
1579
+ }, [nxtlinqApi, authFields, setSuggestions])
1454
1580
 
1455
1581
  // Updated sendMessage function to support different AI models and attachments
1456
1582
  const sendMessage = async (
@@ -1586,15 +1712,14 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1586
1712
  const updated = [...prev];
1587
1713
  for (let i = updated.length - 1; i >= 0; i--) {
1588
1714
  if (updated[i].role === 'user' && updated[i].piiStatus === 'scanning') {
1589
- const patch: Partial<typeof updated[0]> = { piiStep };
1590
- // scan_complete carries anonymized data set piiProtection early for immediate redact
1591
- if (step === 'scan_complete' && data?.anonymizedUserMessage) {
1592
- patch.piiProtection = {
1593
- anonymizedContent: data.anonymizedUserMessage,
1594
- mapping: data.mapping || {},
1595
- };
1715
+ if (step === 'scan_complete') {
1716
+ // PII work is complete independently of any later tool
1717
+ // authorization. Never leave the bubble in Sending while
1718
+ // waiting for wallet recovery or permission elevation.
1719
+ updated[i] = completePiiScan(updated[i], data);
1720
+ } else {
1721
+ updated[i] = { ...updated[i], piiStep };
1596
1722
  }
1597
- updated[i] = { ...updated[i], ...patch };
1598
1723
  break;
1599
1724
  }
1600
1725
  }
@@ -1603,6 +1728,27 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1603
1728
  } : undefined,
1604
1729
  });
1605
1730
 
1731
+ // PII scanning is complete when the Agent response arrives, regardless
1732
+ // of whether a later frontend-tool authorization step can continue.
1733
+ if (piiDisplayMode === 'redacted') {
1734
+ const anonymizedUserMsg = response.piiProtection?.anonymizedUserMessage;
1735
+ const piiMappingData = response.piiProtection?.mapping ?? undefined;
1736
+ setMessages(prev => {
1737
+ const updated = [...prev];
1738
+ for (let i = updated.length - 1; i >= 0; i--) {
1739
+ if (updated[i].role === 'user' && updated[i].piiStatus === 'scanning') {
1740
+ updated[i] = completePiiScan(updated[i], {
1741
+ entityCount: piiMappingData ? Object.keys(piiMappingData).length : 0,
1742
+ anonymizedUserMessage: anonymizedUserMsg,
1743
+ mapping: piiMappingData,
1744
+ });
1745
+ break;
1746
+ }
1747
+ }
1748
+ return updated;
1749
+ });
1750
+ }
1751
+
1606
1752
  if (!('error' in response && (response as { error?: string }).error)) {
1607
1753
  const tr = response as AgentResponse;
1608
1754
  if (
@@ -1732,7 +1878,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1732
1878
  replyText = 'Sorry, I cannot understand your question';
1733
1879
  }
1734
1880
 
1735
- updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined);
1881
+ updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined, content);
1736
1882
 
1737
1883
  setMessages(prev => prev.map(m =>
1738
1884
  m.id === streamAssistantId
@@ -1893,7 +2039,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1893
2039
  }
1894
2040
  }
1895
2041
 
1896
- updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined);
2042
+ updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined, content);
1897
2043
 
1898
2044
  // Skip creating a new botResponse since we already updated the streaming message
1899
2045
  } else {
@@ -1933,7 +2079,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1933
2079
  }
1934
2080
  }
1935
2081
 
1936
- updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined);
2082
+ updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined, content);
1937
2083
  const newBotResponse: Message = {
1938
2084
  id: (Date.now() + 1).toString(),
1939
2085
  content: mergedContent,
@@ -1959,7 +2105,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1959
2105
  .join(' ') || 'Sorry, I cannot understand your question'
1960
2106
  : response.reply || 'Sorry, I cannot understand your question';
1961
2107
 
1962
- updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined);
2108
+ updateSuggestions(pseudoId, localStorage.getItem('walletAddress') || undefined, content);
1963
2109
 
1964
2110
  const newBotResponse: Message = {
1965
2111
  id: (Date.now() + 1).toString(),
@@ -1999,38 +2145,6 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1999
2145
  botResponse = newBotResponse;
2000
2146
  }
2001
2147
 
2002
- // ===== PII Protection: Update user message with anonymized version =====
2003
- const anonymizedUserMsg = response.piiProtection?.anonymizedUserMessage;
2004
- const piiMappingData = response.piiProtection?.mapping ?? undefined;
2005
- if (anonymizedUserMsg) {
2006
- setMessages(prev => {
2007
- const updated = [...prev];
2008
- for (let i = updated.length - 1; i >= 0; i--) {
2009
- if (updated[i].role === 'user') {
2010
- updated[i] = {
2011
- ...updated[i],
2012
- piiProtection: { anonymizedContent: anonymizedUserMsg, mapping: piiMappingData },
2013
- piiStatus: 'complete',
2014
- };
2015
- break;
2016
- }
2017
- }
2018
- return updated;
2019
- });
2020
- } else if (piiDisplayMode === 'redacted') {
2021
- // No PII detected — set piiStatus to 'none' to trigger "No sensitive data" indicator
2022
- setMessages(prev => {
2023
- const updated = [...prev];
2024
- for (let i = updated.length - 1; i >= 0; i--) {
2025
- if (updated[i].role === 'user' && updated[i].piiStatus === 'scanning') {
2026
- updated[i] = { ...updated[i], piiStatus: 'none' };
2027
- break;
2028
- }
2029
- }
2030
- return updated;
2031
- });
2032
- }
2033
-
2034
2148
  // Execute redirect after all message processing is complete
2035
2149
  if (redirectUrl) {
2036
2150
  // Use setTimeout to ensure the message is displayed before redirect
@@ -2400,21 +2514,19 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2400
2514
  }, [nxtlinqApi, authFields, pseudoId]);
2401
2515
 
2402
2516
  // Handle preset message
2403
- const handlePresetMessage = (message: PresetMessage) => {
2517
+ const handlePresetMessage = async (message: PresetMessage): Promise<void> => {
2404
2518
  // If preset is configured as auto-send, avoid duplicate sends when user clicks repeatedly
2405
2519
  if (message.autoSend) {
2406
2520
  const trimmedText = (message.text || '').trim();
2407
2521
  if (!trimmedText) return;
2408
2522
 
2409
2523
  // Prevent sending messages while AI Agent is processing
2410
- if (isLoading) {
2524
+ if (isLoading || presetSendInFlightRef.current) {
2411
2525
  return;
2412
2526
  }
2413
2527
 
2414
- // If this exact preset text was just sent (by auto-send / manual / preset), skip to prevent duplicates
2415
- if (lastAutoSentTranscriptRef.current === trimmedText) {
2416
- return;
2417
- }
2528
+ presetSendInFlightRef.current = true;
2529
+ setSuggestions(previous => filterSuggestions(previous, trimmedText));
2418
2530
 
2419
2531
  // For preset messages, we need to add the user message first since sendMessage won't add it on retries
2420
2532
  const userMessage: Message = {
@@ -2434,114 +2546,16 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2434
2546
  lastAutoSentTranscriptRef.current = trimmedText;
2435
2547
 
2436
2548
  // Pass a flag to indicate this is a preset message so sendMessage won't add user message again
2437
- sendMessage(trimmedText, 0, true);
2549
+ try {
2550
+ await sendMessage(trimmedText, 0, true);
2551
+ } finally {
2552
+ presetSendInFlightRef.current = false;
2553
+ }
2438
2554
  } else {
2439
2555
  setInputValue(message.text);
2440
2556
  }
2441
2557
  };
2442
2558
 
2443
- // Generate and register AIT
2444
- const generateAndRegisterAIT = async (newPermissions?: string[], isFromAIAgent = false) => {
2445
- let currentSigner = signer;
2446
- let currentAddress = hitAddress;
2447
-
2448
- // If we have an address but no signer, try to auto-connect wallet
2449
- if (currentAddress && !currentSigner) {
2450
- const storedWalletAddress = localStorage.getItem('walletAddress');
2451
- if (storedWalletAddress === currentAddress) {
2452
- try {
2453
- const autoConnectResult = await connectWallet(false); // Don't show sign-in message yet
2454
- if (autoConnectResult) {
2455
- // Wait for state to update
2456
- await new Promise(resolve => setTimeout(resolve, 1000));
2457
- // Get the updated signer and address
2458
- currentSigner = signerRef.current || signer;
2459
- currentAddress = hitAddressRef.current || hitAddress;
2460
- }
2461
- } catch (error) {
2462
- console.error('Auto-connect failed during AIT generation:', error);
2463
- }
2464
- }
2465
- }
2466
-
2467
- if (!currentSigner || !currentAddress) {
2468
- throw new Error('Missing signer or wallet address');
2469
- }
2470
-
2471
- return generateAndRegisterAITWithSigner(newPermissions, isFromAIAgent, currentSigner, currentAddress);
2472
- };
2473
-
2474
- // Generate and register AIT with explicit signer and address
2475
- const generateAndRegisterAITWithSigner = async (
2476
- newPermissions?: string[],
2477
- isFromAIAgent = false,
2478
- explicitSigner?: ethers.JsonRpcSigner,
2479
- explicitAddress?: string
2480
- ) => {
2481
- const currentSigner = explicitSigner || signer;
2482
- const currentAddress = explicitAddress || hitAddress;
2483
-
2484
- if (!currentSigner || !currentAddress) {
2485
- throw new Error('Missing signer or wallet address');
2486
- }
2487
-
2488
- // If AI Agent is creating, must check if user has existing AIT
2489
- if (isFromAIAgent && !aitRef.current) {
2490
- throw new Error('You must have an existing AIT before AI Agent can create new AITs');
2491
- }
2492
-
2493
- const timestamp = Math.floor(Date.now() / 1000);
2494
- const aitId = `did:polygon:nxtlinq:${currentAddress}:${timestamp}`;
2495
-
2496
- const metadata = {
2497
- permissions: newPermissions || permissions,
2498
- issuedBy: currentAddress,
2499
- };
2500
-
2501
- const metadataStr = stringify(metadata);
2502
- // ethers v6: utils.keccak256 -> keccak256, utils.toUtf8Bytes -> toUtf8Bytes
2503
- const metadataHash = ethers.keccak256(ethers.toUtf8Bytes(metadataStr));
2504
-
2505
- const uploadResponse = await nxtlinqApi.metadata.createMetadata(metadata, nxtlinqAITServiceAccessToken || '');
2506
- if ('error' in uploadResponse) {
2507
- throw new Error(`Failed to upload metadata: ${uploadResponse.error}`);
2508
- }
2509
-
2510
- const { metadataCid } = uploadResponse;
2511
-
2512
- const createAITParams = {
2513
- aitId,
2514
- controller: currentAddress,
2515
- serviceId,
2516
- metadataHash,
2517
- metadataCid,
2518
- isFromAIAgent: isFromAIAgent,
2519
- parentAITId: isFromAIAgent ? aitRef.current?.aitId : undefined,
2520
- };
2521
-
2522
- const createAITResponse = await nxtlinqApi.ait.createAIT(createAITParams, nxtlinqAITServiceAccessToken || '');
2523
-
2524
- if ('error' in createAITResponse) {
2525
- throw new Error(`Failed to create AIT: ${createAITResponse.error}`);
2526
- }
2527
-
2528
- const aitInfo = {
2529
- aitId,
2530
- controller: currentAddress,
2531
- metadata,
2532
- metadataHash,
2533
- metadataCid,
2534
- };
2535
-
2536
- const nextPermissions = newPermissions || permissions;
2537
- aitRef.current = aitInfo;
2538
- permissionsRef.current = nextPermissions;
2539
- setAit(aitInfo);
2540
- setPermissions(nextPermissions);
2541
- };
2542
-
2543
-
2544
-
2545
2559
  // Auto enable AIT permission
2546
2560
  const enableAIT = async (toolName: string) => {
2547
2561
  if (isAITEnabling) return false; // Prevent duplicate
@@ -2608,40 +2622,45 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2608
2622
  }
2609
2623
 
2610
2624
  try {
2611
- // Get available permissions to find the tool
2612
- const availablePermissionLabels = availablePermissions.map(p => p.label);
2613
- if (!availablePermissionLabels.includes(toolName)) {
2625
+ const context: RuntimeAITContext = {
2626
+ controller: currentHitAddress,
2627
+ serviceId,
2628
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
2629
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
2630
+ };
2631
+ const enabled = await runtimeMutationQueueRef.current.run(
2632
+ runtimeAITSubjectKey(context),
2633
+ async () => {
2634
+ runtimeRecomputeCoordinatorRef.current.assertContextActive(context);
2635
+ const current = await recomputeRuntimeAIT(context);
2636
+ if (!current.maximumPermissions.includes(toolName)) {
2637
+ return false;
2638
+ }
2639
+ const nextDeniedPermissions = removeOneDeniedPermission(
2640
+ current.deniedPermissions,
2641
+ toolName,
2642
+ );
2643
+ runtimeRecomputeCoordinatorRef.current.assertContextActive(context);
2644
+ await dispatchRuntimeDenyListReplacement(context, nextDeniedPermissions);
2645
+ return true;
2646
+ },
2647
+ );
2648
+ if (!enabled) {
2614
2649
  showError(`Tool ${toolName} is not available for your current identity provider`);
2615
2650
  setIsAITEnabling(false);
2616
2651
  return false;
2617
2652
  }
2618
-
2619
- // Get current permissions from AIT metadata instead of React state
2620
- // This ensures we don't lose existing permissions when enabling new ones
2621
- const currentAITPermissions = aitRef.current?.metadata?.permissions || permissions;
2622
- const newPermissions = [...currentAITPermissions];
2623
- if (!newPermissions.includes(toolName)) {
2624
- newPermissions.push(toolName);
2625
- }
2626
-
2627
- // Filter out deleted permissions before saving
2628
- const validPermissions = newPermissions.filter(permission =>
2629
- availablePermissionLabels.includes(permission)
2630
- );
2631
-
2632
- // Generate and register AIT with new permissions
2633
- // For auto-enable, we should create a regular AIT (not AI Agent AIT) if user doesn't have existing AIT
2634
- const shouldCreateAsAIAgent = !!aitRef.current; // Only create as AI Agent if user already has an AIT
2635
-
2636
- await generateAndRegisterAITWithSigner(validPermissions, shouldCreateAsAIAgent, currentSigner, currentHitAddress);
2637
2653
  showSuccess('AIT permission enabled successfully! You can now use the AI agent.');
2638
- await refreshAIT(true);
2639
2654
  setIsAITEnabling(false);
2640
2655
  return true;
2641
2656
  } catch (error) {
2642
- console.error('Failed to auto-enable AIT:', error);
2657
+ if (!(error instanceof StaleRuntimeAITResponseError)) {
2658
+ console.error('Failed to auto-enable AIT:', error);
2659
+ }
2643
2660
  setIsAITEnabling(false);
2644
- if (error instanceof Error) {
2661
+ if (error instanceof StaleRuntimeAITResponseError) {
2662
+ return false;
2663
+ } else if (error instanceof Error) {
2645
2664
  showError(error.message);
2646
2665
  } else {
2647
2666
  showError('Failed to enable AIT permission. Please try again.');
@@ -2654,23 +2673,43 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2654
2673
  const savePermissions = async (newPermissions?: string[]) => {
2655
2674
  setIsDisabled(true);
2656
2675
  try {
2657
- // Filter out deleted permissions before saving
2658
- const permissionsToSave = newPermissions || permissions;
2659
- const availablePermissionLabels = availablePermissions.map(p => p.label);
2660
- const validPermissions = permissionsToSave.filter(permission =>
2661
- availablePermissionLabels.includes(permission)
2676
+ if (!hitAddress) {
2677
+ throw new Error('Please connect your wallet first');
2678
+ }
2679
+ const context: RuntimeAITContext = {
2680
+ controller: hitAddress,
2681
+ serviceId,
2682
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
2683
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
2684
+ };
2685
+ const checkedPermissions = [...(newPermissions ?? permissions)];
2686
+ await runtimeMutationQueueRef.current.run(
2687
+ runtimeAITSubjectKey(context),
2688
+ async () => {
2689
+ runtimeRecomputeCoordinatorRef.current.assertContextActive(context);
2690
+ const fresh = await recomputeRuntimeAIT(context);
2691
+ const nextDeniedPermissions = replaceCurrentMaximumDenyList(
2692
+ fresh.deniedPermissions,
2693
+ fresh.maximumPermissions,
2694
+ checkedPermissions,
2695
+ );
2696
+ runtimeRecomputeCoordinatorRef.current.assertContextActive(context);
2697
+ await dispatchRuntimeDenyListReplacement(context, nextDeniedPermissions);
2698
+ },
2662
2699
  );
2663
-
2664
- await generateAndRegisterAIT(validPermissions, false);
2665
2700
  showSuccess('AIT permissions saved successfully! You can now use the AI agent with your configured permissions.');
2666
2701
  setShowPermissionForm(false);
2667
2702
  setIsPermissionFormOpen(false);
2668
2703
  await refreshAIT(true);
2669
2704
  await retryPendingTextTool();
2670
2705
  } catch (error) {
2671
- console.error('Failed to generate AIT:', error);
2706
+ if (!(error instanceof StaleRuntimeAITResponseError)) {
2707
+ console.error('Failed to update AIT deny-list:', error);
2708
+ }
2672
2709
  setIsDisabled(false);
2673
- if (error instanceof Error) {
2710
+ if (error instanceof StaleRuntimeAITResponseError) {
2711
+ return;
2712
+ } else if (error instanceof Error) {
2674
2713
  showError(error.message);
2675
2714
  } else {
2676
2715
  showError('Failed to save permissions. Please try again.');
@@ -2719,15 +2758,12 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2719
2758
  const walletResponse = await nxtlinqApi.wallet.getWallet({ address }, token);
2720
2759
  if (!('error' in walletResponse)) {
2721
2760
  setWalletInfo(walletResponse);
2722
- const aitResponse = await nxtlinqApi.ait.getAITByServiceIdAndController({
2723
- serviceId,
2761
+ await queueRuntimeAITRefresh({
2724
2762
  controller: address,
2725
- customUsername: (!requireWalletIDVVerification && customUsername) ? getFinalCustomUsername(customUsername) : undefined
2726
- }, token);
2727
- if (!('error' in aitResponse)) {
2728
- aitRef.current = aitResponse;
2729
- setAit(aitResponse);
2730
- }
2763
+ serviceId,
2764
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
2765
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
2766
+ });
2731
2767
  }
2732
2768
  } finally {
2733
2769
  setIsWalletLoading(false);
@@ -2759,15 +2795,12 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2759
2795
  const walletResponse = await nxtlinqApi.wallet.getWallet({ address }, token);
2760
2796
  if (!('error' in walletResponse)) {
2761
2797
  setWalletInfo(walletResponse);
2762
- const aitResponse = await nxtlinqApi.ait.getAITByServiceIdAndController({
2763
- serviceId,
2798
+ await queueRuntimeAITRefresh({
2764
2799
  controller: address,
2765
- customUsername: (!requireWalletIDVVerification && customUsername) ? getFinalCustomUsername(customUsername) : undefined
2766
- }, token);
2767
- if (!('error' in aitResponse)) {
2768
- aitRef.current = aitResponse;
2769
- setAit(aitResponse);
2770
- }
2800
+ serviceId,
2801
+ ...(roles !== undefined ? { roles: [...roles] } : {}),
2802
+ ...(permissionGroup !== undefined ? { permissionGroup } : {}),
2803
+ });
2771
2804
  }
2772
2805
  } finally {
2773
2806
  setIsWalletLoading(false);
@@ -2877,7 +2910,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2877
2910
  if (hitAddress && nxtlinqAITServiceAccessToken) {
2878
2911
  refreshAIT();
2879
2912
  }
2880
- }, [hitAddress, nxtlinqAITServiceAccessToken]);
2913
+ }, [hitAddress, nxtlinqAITServiceAccessToken, permissionGroup, runtimeRolesKey, serviceId]);
2881
2914
 
2882
2915
  // Set loading state when permission form opens
2883
2916
  React.useEffect(() => {
@@ -2966,6 +2999,8 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2966
2999
  updateExternalId()
2967
3000
  }, [hitAddress, nxtlinqApi, authFields, pseudoId]);
2968
3001
 
3002
+ const runtimeAuthorizationMatchesRenderedContext =
3003
+ runtimeAuthorizationMatchesContext(runtimeAuthorizationContextKey, runtimeContext);
2969
3004
  const contextValue: ChatBotContextType = {
2970
3005
  // State
2971
3006
  messages,
@@ -2973,9 +3008,16 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2973
3008
  isLoading,
2974
3009
  isOpen,
2975
3010
  hitAddress,
2976
- ait,
2977
- permissions,
2978
- availablePermissions,
3011
+ ait: runtimeAuthorizationMatchesRenderedContext ? ait : null,
3012
+ permissions: runtimeAuthorizationMatchesRenderedContext ? permissions : [],
3013
+ maximumPermissions: runtimeAuthorizationMatchesRenderedContext ? maximumPermissions : [],
3014
+ deniedPermissions: runtimeAuthorizationMatchesRenderedContext ? deniedPermissions : [],
3015
+ runtimePermissionsUnavailable: runtimeAuthorizationMatchesRenderedContext
3016
+ ? runtimePermissionsUnavailable
3017
+ : runtimeContext !== null,
3018
+ availablePermissions: runtimeAuthorizationMatchesRenderedContext
3019
+ ? availablePermissions
3020
+ : [],
2979
3021
  showPermissionForm,
2980
3022
  isPermissionFormOpen,
2981
3023
  isAITLoading,
@@ -3012,7 +3054,6 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
3012
3054
  setIsOpen,
3013
3055
  setShowPermissionForm,
3014
3056
  setIsPermissionFormOpen,
3015
- setPermissions,
3016
3057
  setIsDisabled,
3017
3058
  setIsWalletLoading,
3018
3059
  setNotification,
@@ -3063,6 +3104,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
3063
3104
  onVerifyWallet: (method: 'berifyme' | 'custom') => handleVerifyWalletClick(method),
3064
3105
  serviceId,
3065
3106
  permissionGroup,
3107
+ roles,
3066
3108
 
3067
3109
  // Props
3068
3110
  props: {
@@ -3081,6 +3123,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
3081
3123
  getAuthToken,
3082
3124
  onVerifyWallet,
3083
3125
  permissionGroup,
3126
+ roles,
3084
3127
  onModelChange,
3085
3128
  storageMode,
3086
3129
  requireWalletIDVVerification,