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

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.
@@ -25,12 +25,18 @@ import type {
25
25
  Message,
26
26
  ServicePermission,
27
27
  } from '@bytexbyte/nxtlinq-ai-agent-core-development';
28
+ import {
29
+ authorizeTextFrontendTool,
30
+ hasRecordedWalletVerification,
31
+ validateWalletSession,
32
+ } from '@bytexbyte/nxtlinq-ai-agent-core-development';
28
33
  import {
29
34
  AIModel,
30
35
  ChatBotContextType,
31
36
  ChatBotProps,
32
37
  PresetMessage
33
38
  } from '../types/ChatBotTypes';
39
+ import { PendingTextToolRetryController } from '../pendingTextToolRetry';
34
40
 
35
41
  const MIC_ENABLED_SESSION_KEY = 'chatbot-mic-enabled';
36
42
 
@@ -212,6 +218,22 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
212
218
  const permissionsRef = React.useRef(permissions);
213
219
  const nxtlinqAITServiceAccessTokenRef = React.useRef(nxtlinqAITServiceAccessToken);
214
220
  const signerRef = React.useRef(signer);
221
+ const pendingTextToolRetryRef = React.useRef(new PendingTextToolRetryController());
222
+ const sendMessageRef = React.useRef<ChatBotContextType['sendMessage']>(async () => {});
223
+ const retryPendingTextTool = React.useCallback(() => {
224
+ const controller = pendingTextToolRetryRef.current;
225
+ return controller.retry(async (request) => {
226
+ // This attempt consumes the old request. A still-blocked preflight will
227
+ // record it again with the next required recovery step.
228
+ controller.clear();
229
+ await sendMessageRef.current(
230
+ request.content,
231
+ 1,
232
+ request.isPresetMessage,
233
+ request.attachments,
234
+ );
235
+ });
236
+ }, []);
215
237
 
216
238
  // Helper function to combine customUsername with Adilas customUserInfo
217
239
  const getFinalCustomUsername = React.useCallback((username: string | undefined): string | undefined => {
@@ -996,7 +1018,9 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
996
1018
 
997
1019
  // Refresh AIT
998
1020
  const refreshAIT = async (forceUpdatePermissions = false) => {
999
- if (!hitAddress) {
1021
+ const currentHitAddress = hitAddressRef.current || hitAddress;
1022
+ const currentToken = nxtlinqAITServiceAccessTokenRef.current || nxtlinqAITServiceAccessToken;
1023
+ if (!currentHitAddress) {
1000
1024
  setAit(null);
1001
1025
  setPermissions([]);
1002
1026
  setWalletInfo(null);
@@ -1006,13 +1030,18 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1006
1030
 
1007
1031
  setIsAITLoading(true);
1008
1032
  try {
1033
+ let walletAllowsAITLookup = !requireWalletIDVVerification;
1034
+
1009
1035
  // Get wallet info first - always try to get wallet info if we have a token
1010
- if (nxtlinqAITServiceAccessToken) {
1036
+ if (currentToken) {
1011
1037
  try {
1012
- const walletResponse = await nxtlinqApi.wallet.getWallet({ address: hitAddress }, nxtlinqAITServiceAccessToken);
1038
+ const walletResponse = await nxtlinqApi.wallet.getWallet({ address: currentHitAddress }, currentToken);
1013
1039
  if (!('error' in walletResponse)) {
1014
1040
  setWalletInfo(walletResponse);
1041
+ walletAllowsAITLookup = !requireWalletIDVVerification
1042
+ || hasRecordedWalletVerification(walletResponse);
1015
1043
  } else {
1044
+ setWalletInfo(null);
1016
1045
  // Check if the error is due to invalid/expired token
1017
1046
  if (walletResponse.error.includes('Invalid or expired token')) {
1018
1047
  console.log('Token appears to be invalid during wallet info fetch, clearing it');
@@ -1020,17 +1049,26 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1020
1049
  }
1021
1050
  }
1022
1051
  } catch (error) {
1052
+ setWalletInfo(null);
1023
1053
  console.error('Failed to fetch wallet info:', error);
1024
1054
  }
1025
1055
  }
1026
1056
 
1057
+ // AIT existence is not identity verification. In strict mode, stop before
1058
+ // the legacy AIT lookup can synthesize a custom wallet record.
1059
+ if (currentToken && !walletAllowsAITLookup) {
1060
+ setAit(null);
1061
+ setPermissions([]);
1062
+ return;
1063
+ }
1064
+
1027
1065
  // Only try to fetch AIT if we have a token
1028
- if (nxtlinqAITServiceAccessToken) {
1066
+ if (currentToken) {
1029
1067
  const response = await nxtlinqApi.ait.getAITByServiceIdAndController({
1030
1068
  serviceId,
1031
- controller: hitAddress,
1069
+ controller: currentHitAddress,
1032
1070
  customUsername: (!requireWalletIDVVerification && customUsername) ? getFinalCustomUsername(customUsername) : undefined
1033
- }, nxtlinqAITServiceAccessToken);
1071
+ }, currentToken);
1034
1072
 
1035
1073
  if ('error' in response) {
1036
1074
  console.error('Failed to fetch AIT:', response.error);
@@ -1062,17 +1100,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1062
1100
  const isNeedSignInWithWallet = React.useMemo(() => {
1063
1101
  if (!hitAddress) return false;
1064
1102
  if (!nxtlinqAITServiceAccessToken) return true;
1065
-
1066
- try {
1067
- const payload = JSON.parse(atob(nxtlinqAITServiceAccessToken.split('.')[1]));
1068
- const address = payload.address;
1069
- if (address !== hitAddress) return true;
1070
-
1071
- return false;
1072
- } catch (error) {
1073
- console.error('Error parsing token:', error);
1074
- return true;
1075
- }
1103
+ return !validateWalletSession(nxtlinqAITServiceAccessToken, hitAddress).valid;
1076
1104
  }, [hitAddress, nxtlinqAITServiceAccessToken]);
1077
1105
 
1078
1106
  // Connect wallet
@@ -1182,7 +1210,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1182
1210
  }
1183
1211
  }, [nxtlinqAITServiceAccessToken, refreshAIT, isNeedSignInWithWallet, requireWalletIDVVerification, customUsername, nxtlinqApi]);
1184
1212
 
1185
- const signInWallet = async (autoShowSuccessMessage = true) => {
1213
+ const signInWallet = async (autoShowSuccessMessage = true): Promise<boolean> => {
1186
1214
  // Use refs to get latest state values for consistency with hasPermission
1187
1215
  const currentHitAddress = hitAddressRef.current;
1188
1216
  const currentSigner = signerRef.current;
@@ -1215,7 +1243,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1215
1243
  if (autoShowSuccessMessage) {
1216
1244
  showError(walletTextUtils.getWalletText('Please connect your wallet first.', serviceId));
1217
1245
  }
1218
- return;
1246
+ return false;
1219
1247
  }
1220
1248
 
1221
1249
  if (!signerToUse) {
@@ -1223,7 +1251,13 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1223
1251
  if (autoShowSuccessMessage) {
1224
1252
  showError(walletTextUtils.getWalletText('Please connect your wallet first.', serviceId));
1225
1253
  }
1226
- return;
1254
+ return false;
1255
+ }
1256
+
1257
+ const currentToken = nxtlinqAITServiceAccessTokenRef.current;
1258
+ if (currentToken && validateWalletSession(currentToken, addressToUse).valid) {
1259
+ await refreshAIT();
1260
+ return true;
1227
1261
  }
1228
1262
 
1229
1263
  try {
@@ -1232,7 +1266,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1232
1266
  if (autoShowSuccessMessage) {
1233
1267
  showError(normalizeErrorForUser(nonceResponse.error));
1234
1268
  }
1235
- return;
1269
+ return false;
1236
1270
  }
1237
1271
 
1238
1272
  const payload = {
@@ -1253,10 +1287,11 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1253
1287
  if (autoShowSuccessMessage) {
1254
1288
  showError(normalizeErrorForUser(response.error));
1255
1289
  }
1256
- return;
1290
+ return false;
1257
1291
  }
1258
1292
  const { accessToken } = response;
1259
1293
  setNxtlinqAITServiceAccessToken(accessToken);
1294
+ nxtlinqAITServiceAccessTokenRef.current = accessToken;
1260
1295
 
1261
1296
  // Wait for state to update before refreshing AIT
1262
1297
  await new Promise(resolve => setTimeout(resolve, 100));
@@ -1265,372 +1300,83 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1265
1300
  if (autoShowSuccessMessage) {
1266
1301
  showSuccess(walletTextUtils.getWalletText('Successfully signed in with your HIT wallet. You can now use the AI agent.', serviceId));
1267
1302
  }
1303
+ return true;
1268
1304
  } catch (error) {
1269
1305
  console.error('Failed to sign in:', error);
1270
1306
  if (autoShowSuccessMessage) {
1271
1307
  showError('Failed to sign in. Please try again.');
1272
1308
  }
1309
+ return false;
1273
1310
  }
1274
1311
  };
1275
1312
 
1276
1313
  // Check permissions
1277
- const hasPermission = async (requiredPermission: string, autoRetry = true, onAutoConnect?: () => void, onAutoSignIn?: () => void): Promise<boolean> => {
1314
+ const hasPermission = async (
1315
+ requiredPermission: string,
1316
+ ): Promise<boolean> => {
1278
1317
  // Use refs to get latest state values
1279
1318
  const currentHitAddress = hitAddressRef.current;
1280
- const currentAit = aitRef.current;
1281
- const currentPermissions = permissionsRef.current;
1282
1319
  const currentToken = nxtlinqAITServiceAccessTokenRef.current;
1283
1320
 
1284
- if (!currentHitAddress) {
1285
- if (autoRetry) {
1286
- setIsLoading(false); // Stop thinking before showing message
1287
- setMessages(prev => [...prev, {
1288
- id: Date.now().toString(),
1289
- content: walletTextUtils.getWalletText('Please connect your HIT wallet to continue.', serviceId),
1290
- role: 'assistant',
1291
- timestamp: new Date().toISOString(),
1292
- button: 'connectWallet'
1293
- }]);
1294
-
1295
- try {
1296
- setIsAutoConnecting(true); // Mark as auto-connecting
1297
- await connectWallet(false); // Don't show sign-in message yet
1298
- setIsAutoConnecting(false); // Clear auto-connecting state
1299
-
1300
- // Show brief success message for auto-connect
1301
- showSuccess(walletTextUtils.getWalletText('Auto wallet connection successful', serviceId));
1302
-
1303
-
1304
- onAutoConnect?.(); // Call callback if provided
1305
- await new Promise(resolve => setTimeout(resolve, 2000));
1306
-
1307
- // After auto connect, if not signed in, then auto sign-in
1308
- const tokenAfterConnect = nxtlinqAITServiceAccessTokenRef.current;
1309
- if (!tokenAfterConnect) {
1310
- setIsAutoConnecting(true);
1311
- await signInWallet(false);
1312
- onAutoSignIn?.(); // Call callback if provided
1313
- setIsAutoConnecting(false);
1314
- showSuccess(walletTextUtils.getWalletText('Auto sign-in successful after wallet connect', serviceId));
1315
- await refreshAIT();
1316
- // Wait for AIT to be fully loaded with polling
1317
- let attempts = 0;
1318
- const maxAttempts = 5;
1319
- while (!aitRef.current && attempts < maxAttempts) {
1320
- await new Promise(resolve => setTimeout(resolve, 2000));
1321
- attempts++;
1322
- }
1323
- }
1324
-
1325
- // If connection (and sign-in if needed) successful, continue with permission check
1326
- const result = await hasPermission(requiredPermission, false);
1327
- return result;
1328
- } catch (error) {
1329
- console.error('Failed to auto-connect wallet:', error);
1330
- setIsAutoConnecting(false); // Clear auto-connecting state on error
1331
- return false;
1332
- }
1333
- }
1334
- // If autoRetry is false, don't show message again, just return false
1335
- return false;
1336
- }
1337
-
1338
- if (!currentToken) {
1339
- if (autoRetry) {
1340
- setIsLoading(false); // Stop thinking before showing message
1341
- setMessages(prev => [...prev, {
1342
- id: Date.now().toString(),
1343
- content: walletTextUtils.getWalletText('Please sign in with your HIT wallet to continue.', serviceId),
1344
- role: 'assistant',
1345
- timestamp: new Date().toISOString(),
1346
- button: 'signIn'
1347
- }]);
1348
-
1349
- try {
1350
- setIsAutoConnecting(true); // Mark as auto-signing
1351
- await signInWallet(false); // Don't show success message yet
1352
- onAutoSignIn?.(); // Call callback if provided
1353
- setIsAutoConnecting(false); // Clear auto-signing state
1354
-
1355
- // Show brief success message for auto-sign-in
1356
- showSuccess('Auto sign-in successful');
1357
-
1358
- // Ensure AIT is refreshed after sign-in
1359
- await refreshAIT();
1360
-
1361
- // Wait for AIT to be fully loaded with polling
1362
- let attempts = 0;
1363
- const maxAttempts = 5; // Wait up to 10 seconds (5 * 2000ms)
1364
- while (!aitRef.current && attempts < maxAttempts) {
1365
- await new Promise(resolve => setTimeout(resolve, 2000));
1366
- attempts++;
1367
- }
1368
-
1369
- // Only continue if AIT is actually loaded
1370
- if (aitRef.current) {
1371
- // Wait a bit more to ensure permissions are also loaded
1372
- await new Promise(resolve => setTimeout(resolve, 2000));
1373
- // If sign-in successful, continue with permission check
1374
- const result = await hasPermission(requiredPermission, false);
1375
- return result;
1376
- } else {
1377
- return false;
1378
- }
1379
- } catch (error) {
1380
- console.error('Failed to auto-sign-in wallet:', error);
1381
- setIsAutoConnecting(false); // Clear auto-signing state on error
1382
- return false;
1383
- }
1384
- }
1385
- // If autoRetry is false, don't show message again, just return false
1386
- return false;
1387
- }
1388
-
1389
- try {
1390
- const payload = JSON.parse(atob(currentToken.split('.')[1]));
1391
- const address = payload.address;
1392
- if (address !== currentHitAddress) {
1393
- setNxtlinqAITServiceAccessToken('');
1394
- if (autoRetry) {
1395
- setIsLoading(false); // Stop thinking before showing message
1396
- setMessages(prev => [...prev, {
1397
- id: Date.now().toString(),
1398
- content: walletTextUtils.getWalletText('Wallet address mismatch. Please sign in with the correct wallet.', serviceId),
1399
- role: 'assistant',
1400
- timestamp: new Date().toISOString(),
1401
- button: 'signIn'
1402
- }]);
1403
-
1404
- try {
1405
- setIsAutoConnecting(true); // Mark as auto-signing
1406
- await signInWallet(false); // Don't show success message yet
1407
- onAutoSignIn?.(); // Call callback if provided
1408
- setIsAutoConnecting(false); // Clear auto-signing state
1409
-
1410
- // Show brief success message for auto-sign-in after address mismatch
1411
- showSuccess('Auto sign-in successful after address mismatch');
1412
-
1413
- // Ensure AIT is refreshed after sign-in
1414
- await refreshAIT();
1415
-
1416
- // Wait for AIT to be fully loaded with polling
1417
- let attempts = 0;
1418
- const maxAttempts = 5; // Wait up to 10 seconds (5 * 2000ms)
1419
- while (!aitRef.current && attempts < maxAttempts) {
1420
- await new Promise(resolve => setTimeout(resolve, 2000));
1421
- attempts++;
1422
- }
1423
-
1424
- // Only continue if AIT is actually loaded
1425
- if (aitRef.current) {
1426
- // Wait a bit more to ensure permissions are also loaded
1427
- await new Promise(resolve => setTimeout(resolve, 2000));
1428
- // If sign-in successful, continue with permission check
1429
- const result = await hasPermission(requiredPermission, false);
1430
- return result;
1431
- } else {
1432
- return false;
1433
- }
1434
- } catch (error) {
1435
- console.error('Failed to auto-sign-in after address mismatch:', error);
1436
- setIsAutoConnecting(false); // Clear auto-signing state on error
1437
- return false;
1438
- }
1439
- }
1440
- // If autoRetry is false, don't show message again, just return false
1441
- return false;
1442
- }
1443
- } catch (error) {
1444
- console.error('Error parsing token:', error);
1445
- setNxtlinqAITServiceAccessToken('');
1446
- if (autoRetry) {
1447
- setIsLoading(false); // Stop thinking before showing message
1448
- setMessages(prev => [...prev, {
1449
- id: Date.now().toString(),
1450
- content: walletTextUtils.getWalletText('Invalid wallet session. Please sign in again.', serviceId),
1451
- role: 'assistant',
1452
- timestamp: new Date().toISOString(),
1453
- button: 'signIn'
1454
- }]);
1455
-
1456
- try {
1457
- setIsAutoConnecting(true); // Mark as auto-signing
1458
- await signInWallet(false); // Don't show success message yet
1459
- onAutoSignIn?.(); // Call callback if provided
1460
- setIsAutoConnecting(false); // Clear auto-signing state
1461
-
1462
- // Show brief success message for auto-sign-in after token parse error
1463
- showSuccess('Auto sign-in successful after token error');
1464
-
1465
- // Ensure AIT is refreshed after sign-in
1466
- await refreshAIT();
1467
-
1468
- // Wait for AIT to be fully loaded with polling
1469
- let attempts = 0;
1470
- const maxAttempts = 5; // Wait up to 10 seconds (5 * 2000ms)
1471
- while (!aitRef.current && attempts < maxAttempts) {
1472
- await new Promise(resolve => setTimeout(resolve, 2000));
1473
- attempts++;
1474
- }
1475
-
1476
- // Only continue if AIT is actually loaded
1477
- if (aitRef.current) {
1478
- // Wait a bit more to ensure permissions are also loaded
1479
- await new Promise(resolve => setTimeout(resolve, 2000));
1480
- // If sign-in successful, continue with permission check
1481
- const result = await hasPermission(requiredPermission, false);
1482
- return result;
1483
- } else {
1484
- return false;
1485
- }
1486
- } catch (signInError) {
1487
- console.error('Failed to auto-sign-in after token parse error:', signInError);
1488
- setIsAutoConnecting(false); // Clear auto-signing state on error
1489
- return false;
1490
- }
1491
- }
1492
- // If autoRetry is false, don't show message again, just return false
1493
- return false;
1494
- }
1495
-
1496
- if (!currentAit) {
1497
- // Show loading message if AIT is still loading
1498
- if (isAITLoading) {
1499
- setIsLoading(false); // Stop thinking before showing message
1500
- setMessages(prev => [...prev, {
1501
- id: Date.now().toString(),
1502
- content: walletTextUtils.getWalletText('Loading your wallet configuration... Please wait a moment.', serviceId),
1503
- role: 'assistant',
1504
- timestamp: new Date().toISOString()
1505
- }]);
1506
- return false;
1507
- }
1508
-
1509
- // If AIT is not loaded but we have a token, try to refresh it once
1510
- if (currentToken && !isAITLoading) {
1511
- try {
1512
- await refreshAIT();
1513
- // Wait for AIT to be loaded with polling
1514
- let attempts = 0;
1515
- const maxAttempts = 5; // Wait up to 10 seconds (5 * 2000ms)
1516
- while (!aitRef.current && attempts < maxAttempts) {
1517
- await new Promise(resolve => setTimeout(resolve, 2000));
1518
- attempts++;
1519
- }
1520
- // Check again after refresh
1521
- if (!aitRef.current) {
1522
- setIsLoading(false); // Stop thinking before showing message
1523
- setMessages(prev => [...prev, {
1524
- id: Date.now().toString(),
1525
- content: walletTextUtils.getWalletText('No AIT found for your wallet. Please click the settings button (⚙️) to configure your AIT permissions.', serviceId),
1526
- role: 'assistant',
1527
- timestamp: new Date().toISOString()
1528
- }]);
1529
- return false;
1530
- }
1531
- } catch (error) {
1532
- console.error('Failed to refresh AIT during permission check:', error);
1533
- setIsLoading(false); // Stop thinking before showing message
1534
- setMessages(prev => [...prev, {
1535
- id: Date.now().toString(),
1536
- content: 'No AIT found for your wallet. Please click the settings button (⚙️) to configure your AIT permissions.',
1537
- role: 'assistant',
1538
- timestamp: new Date().toISOString()
1539
- }]);
1540
- return false;
1541
- }
1542
- } else {
1543
- setIsLoading(false); // Stop thinking before showing message
1544
- setMessages(prev => [...prev, {
1545
- id: Date.now().toString(),
1546
- content: 'No AIT found for your wallet. Please click the settings button (⚙️) to configure your AIT permissions.',
1547
- role: 'assistant',
1548
- timestamp: new Date().toISOString()
1549
- }]);
1550
- return false;
1551
- }
1552
- }
1553
-
1554
- if (availablePermissions.length === 0) {
1555
- setIsLoading(false); // Stop thinking before showing message
1556
- setMessages(prev => [...prev, {
1557
- id: Date.now().toString(),
1558
- content: `No permissions available for your current identity provider. Please check your service configuration or contact support. Service ID: ${serviceId}, Permission Group: ${permissionGroup || 'None'}`,
1559
- role: 'assistant',
1560
- timestamp: new Date().toISOString()
1561
- }]);
1562
- return false;
1563
- }
1564
-
1565
- const checkToolPermissionResult = await nxtlinqApi.agent.checkToolPermission({
1321
+ const authorization = await authorizeTextFrontendTool({
1322
+ api: nxtlinqApi,
1323
+ serviceId,
1324
+ toolName: requiredPermission,
1325
+ snapshot: {
1326
+ walletAddress: currentHitAddress,
1327
+ walletToken: currentToken,
1328
+ // Legacy AIT issuance is unscoped (`externalId=''`). Do not impose a
1329
+ // wallet-address subject until issuance/migration supports it.
1330
+ externalId: undefined,
1331
+ requireWalletIDVVerification,
1332
+ loading: isAITLoading || isAutoConnecting,
1333
+ },
1566
1334
  ...authFields(),
1567
- aitToken: currentToken!,
1568
- controller: currentHitAddress!,
1569
- toolName: requiredPermission
1570
- })
1571
-
1572
- if ('error' in checkToolPermissionResult) {
1573
- // Check if AIT is still loading or if we just auto-connected
1574
- if (isAITLoading || isAutoConnecting) {
1575
- setIsLoading(false); // Stop thinking before showing message
1576
- setMessages(prev => [...prev, {
1577
- id: Date.now().toString(),
1578
- content: walletTextUtils.getWalletText('Loading your wallet configuration... Please wait a moment.', serviceId),
1579
- role: 'assistant',
1580
- timestamp: new Date().toISOString()
1581
- }]);
1582
- return false;
1583
- }
1584
-
1585
- // Refresh service permissions before judging; use latest result to decide message
1586
- const freshAvailablePermissions = await fetchAvailablePermissions();
1587
- const latestAvailablePermissions = freshAvailablePermissions ?? availablePermissions;
1588
-
1589
- const requiredPermission = checkToolPermissionResult.requiredPermission;
1590
- const isInServiceList = latestAvailablePermissions.some((p) => p.label === requiredPermission);
1591
-
1592
- // Permission not in service's available list → "not available for your current identity provider"
1593
- if (!isInServiceList) {
1594
- setIsLoading(false); // Stop thinking before showing message
1595
- setMessages(prev => [...prev, {
1596
- id: Date.now().toString(),
1597
- content: `This permission (${requiredPermission}) is not available for your current identity provider.`,
1598
- role: 'assistant',
1599
- timestamp: new Date().toISOString()
1600
- }]);
1601
- return false;
1602
- }
1603
-
1604
- // AIT loaded but permissions empty (no AIT found)
1605
- if (currentAit && currentPermissions.length === 0) {
1606
- setIsLoading(false); // Stop thinking before showing message
1607
- setMessages(prev => [...prev, {
1608
- id: Date.now().toString(),
1609
- content: 'No AIT found for your wallet. Please click the settings button (⚙️) to configure your AIT permissions.',
1610
- role: 'assistant',
1611
- timestamp: new Date().toISOString()
1612
- }]);
1613
- return false;
1614
- }
1615
-
1616
- // User has AIT but hasn't enabled this permission → prompt to enable
1617
- setIsLoading(false); // Stop thinking before showing message
1618
- const permissionMsg = `You don't have the required AIT permission: ${requiredPermission}. Would you like to enable AIT permission?`;
1619
- setMessages(prev => [
1620
- ...prev,
1621
- {
1622
- id: Date.now().toString(),
1623
- content: permissionMsg,
1624
- role: 'assistant',
1625
- timestamp: new Date().toISOString(),
1626
- button: 'enableAIT',
1627
- metadata: { requiredPermission }
1628
- }
1629
- ]);
1630
- return false;
1631
- }
1632
-
1633
- return true;
1335
+ customUsername: (!requireWalletIDVVerification && customUsername)
1336
+ ? getFinalCustomUsername(customUsername)
1337
+ : undefined,
1338
+ });
1339
+ if (authorization.status === 'allowed') return true;
1340
+
1341
+ setIsLoading(false);
1342
+ const recovery: Message = {
1343
+ id: Date.now().toString(),
1344
+ content: 'Authorization is temporarily unavailable. Please try again.',
1345
+ role: 'assistant',
1346
+ timestamp: new Date().toISOString(),
1347
+ };
1348
+ switch (authorization.status) {
1349
+ case 'wallet_disconnected':
1350
+ recovery.content = walletTextUtils.getWalletText('Please connect your HIT wallet to continue.', serviceId);
1351
+ recovery.button = 'connectWallet';
1352
+ break;
1353
+ case 'wallet_signed_out':
1354
+ recovery.content = walletTextUtils.getWalletText('Please sign in with your HIT wallet to continue.', serviceId);
1355
+ recovery.button = 'signIn';
1356
+ break;
1357
+ case 'wallet_session_invalid':
1358
+ recovery.content = walletTextUtils.getWalletText('Invalid wallet session. Please sign in again.', serviceId);
1359
+ recovery.button = 'signIn';
1360
+ break;
1361
+ case 'wallet_verification_required':
1362
+ recovery.content = 'Please verify your wallet identity before using this tool.';
1363
+ recovery.button = 'verifyWallet';
1364
+ setIsPermissionFormOpen(true);
1365
+ setShowPermissionForm(true);
1366
+ break;
1367
+ case 'ait_not_found':
1368
+ recovery.content = 'No AIT found for your wallet. Please open settings to configure your AIT.';
1369
+ break;
1370
+ case 'permission_denied':
1371
+ recovery.content = `You don't have the required AIT permission: ${authorization.requiredPermission}. Would you like to enable AIT permission?`;
1372
+ recovery.button = 'enableAIT';
1373
+ recovery.metadata = { requiredPermission: authorization.requiredPermission };
1374
+ break;
1375
+ case 'authorization_unavailable':
1376
+ break;
1377
+ }
1378
+ setMessages(prev => [...prev, recovery]);
1379
+ return false;
1634
1380
  };
1635
1381
 
1636
1382
  // AI Model related functions
@@ -1684,8 +1430,17 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1684
1430
  clientPipelineOverride?: Array<{ name: string; durationMs: number }>
1685
1431
  ): Promise<void> => {
1686
1432
  const hasContent = content.trim() || (attachments && attachments.length > 0);
1687
- if (!hasContent || isLoading) return;
1433
+ // Recovery retries may run immediately after a completion handler calls
1434
+ // setIsLoading(false), before React has committed that state update.
1435
+ if (!hasContent || (retryCount === 0 && isLoading)) return;
1436
+ if (retryCount === 0) {
1437
+ // A new user turn supersedes any older interrupted tool request.
1438
+ pendingTextToolRetryRef.current.clear();
1439
+ }
1688
1440
  setPendingAutoTts(null);
1441
+ const turnExternalId = typeof window !== 'undefined'
1442
+ ? localStorage.getItem('walletAddress') || undefined
1443
+ : hitAddressRef.current || undefined;
1689
1444
 
1690
1445
  const currentModel = getCurrentModel();
1691
1446
  // Initialize with current model, will be updated with actual model from backend response
@@ -1764,7 +1519,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1764
1519
  model: currentModel.value,
1765
1520
  ...authFields(),
1766
1521
  pseudoId: pseudoId,
1767
- externalId: localStorage.getItem('walletAddress') || undefined,
1522
+ externalId: turnExternalId,
1768
1523
  customUserInfo,
1769
1524
  customUsername,
1770
1525
  message: content || (attachments && attachments.length > 0 ? `Uploaded ${attachments.length} file(s)` : ''),
@@ -1994,96 +1749,25 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
1994
1749
 
1995
1750
  let toolMsg = '';
1996
1751
  if (onToolUse) {
1997
- let wasAutoConnected = false;
1998
- let wasAutoSignedIn = false;
1999
- // Added: Mark if permission denied due to missing AIT permission
2000
- let permissionDenied = false;
2001
- // Use requiredPermission from response if available, otherwise fall back to toolUse.name
2002
- const permissionToCheck = (response as any)?.requiredPermission || toolUse.name;
2003
-
2004
1752
  const isToolAllowed = await hasPermission(
2005
- permissionToCheck,
2006
- true,
2007
- () => { wasAutoConnected = true },
2008
- () => { wasAutoSignedIn = true }
1753
+ toolUse.name,
2009
1754
  );
2010
1755
 
2011
- // If currentPermissions does not include permissionToCheck and availablePermissionLabels includes permissionToCheck, it means AIT permission is missing
2012
- if (!isToolAllowed && !permissions.includes(permissionToCheck) && availablePermissions.map(p => p.label).includes(permissionToCheck)) {
2013
- permissionDenied = true;
2014
- }
1756
+ // Typed preflight already emitted the only permitted recovery UI.
2015
1757
  if (!isToolAllowed) {
2016
- // If permission denied due to missing AIT permission return
2017
- if (permissionDenied) {
2018
- setIsLoading(false);
2019
- return;
2020
- }
2021
-
2022
- if (isSemiAutomaticMode) {
2023
- setIsLoading(false);
2024
- setMessages(prev => [...prev, {
2025
- id: Date.now().toString(),
2026
- content: 'Click button to continue using tool',
2027
- role: 'assistant',
2028
- timestamp: new Date().toISOString(),
2029
- button: 'continue'
2030
- }]);
2031
- return;
2032
- } else {
2033
- // Only retry for auto-connect/auto-sign-in scenarios
2034
- if (wasAutoConnected && retryCount < 1) {
2035
- // Clear loading state and retry immediately
2036
- setIsLoading(false);
2037
-
2038
- // Check if wallet is already signed in
2039
- const currentToken = nxtlinqAITServiceAccessTokenRef.current;
2040
-
2041
- if (!currentToken) {
2042
- // If not signed in, directly retry the message without waiting for AIT
2043
- setTimeout(() => {
2044
- sendMessage(content, retryCount + 1, isPresetMessage);
2045
- }, 2000);
2046
- } else {
2047
- // If already signed in, wait for AIT to be fully loaded before retrying
2048
- setTimeout(async () => {
2049
- // Wait for AIT to be loaded if needed
2050
- if (!aitRef.current && nxtlinqAITServiceAccessTokenRef.current) {
2051
- await refreshAIT();
2052
- }
2053
-
2054
- // Wait for AIT to be fully loaded with polling
2055
- let attempts = 0;
2056
- const maxAttempts = 5; // Wait up to 10 seconds (5 * 2000ms)
2057
- while (!aitRef.current && attempts < maxAttempts) {
2058
- await new Promise(resolve => setTimeout(resolve, 2000));
2059
- attempts++;
2060
- }
2061
-
2062
- // Only retry if AIT is actually loaded
2063
- if (aitRef.current) {
2064
- // Wait a bit more to ensure permissions are also loaded
2065
- await new Promise(resolve => setTimeout(resolve, 3000));
2066
- sendMessage(content, retryCount + 1, isPresetMessage);
2067
- }
2068
- }, 2000);
2069
- }
2070
- }
2071
- return;
2072
- }
2073
- }
2074
-
2075
- if (isSemiAutomaticMode && wasAutoSignedIn) {
1758
+ pendingTextToolRetryRef.current.set({
1759
+ content,
1760
+ isPresetMessage,
1761
+ attachments,
1762
+ });
2076
1763
  setIsLoading(false);
2077
- setMessages(prev => [...prev, {
2078
- id: Date.now().toString(),
2079
- content: 'Click button to continue using tool',
2080
- role: 'assistant',
2081
- timestamp: new Date().toISOString(),
2082
- button: 'continue'
2083
- }]);
2084
1764
  return;
2085
1765
  }
2086
1766
 
1767
+ // Authorization passed. Clear before invoking the host callback so
1768
+ // concurrent completion events cannot execute this tool twice.
1769
+ pendingTextToolRetryRef.current.clear();
1770
+
2087
1771
  // Create streaming message for tool execution
2088
1772
  const streamingMessageId = `streaming-${Date.now()}`;
2089
1773
  const streamingMessage: Message = {
@@ -2393,6 +2077,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2393
2077
  setIsLoading(false);
2394
2078
  }
2395
2079
  };
2080
+ sendMessageRef.current = sendMessage;
2396
2081
 
2397
2082
  // Handle submit
2398
2083
  const handleSubmit = async (e: React.FormEvent, attachments?: Attachment[]) => {
@@ -2947,6 +2632,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
2947
2632
  setShowPermissionForm(false);
2948
2633
  setIsPermissionFormOpen(false);
2949
2634
  await refreshAIT(true);
2635
+ await retryPendingTextTool();
2950
2636
  } catch (error) {
2951
2637
  console.error('Failed to generate AIT:', error);
2952
2638
  setIsDisabled(false);
@@ -3025,7 +2711,8 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
3025
2711
  }
3026
2712
  setIsLoading(false);
3027
2713
  showSuccess(walletTextUtils.getWalletText('Wallet verification completed successfully! Your wallet is now verified and ready to use.', serviceId));
3028
- refreshAIT();
2714
+ await refreshAIT();
2715
+ await retryPendingTextTool();
3029
2716
  return;
3030
2717
  }
3031
2718
  showError(verifyWalletResponse.error);
@@ -3063,7 +2750,8 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
3063
2750
  }
3064
2751
  setIsLoading(false);
3065
2752
  showSuccess('Wallet verification completed successfully! Your wallet is now verified and ready to use.');
3066
- refreshAIT();
2753
+ await refreshAIT();
2754
+ await retryPendingTextTool();
3067
2755
  return;
3068
2756
  } catch (error) {
3069
2757
  let msg = 'Verification failed';
@@ -3103,6 +2791,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
3103
2791
  await refreshAIT();
3104
2792
  setIsLoading(false);
3105
2793
  showSuccess(walletTextUtils.getWalletText('Wallet verification completed successfully! Your wallet is now verified and ready to use.', serviceId));
2794
+ await retryPendingTextTool();
3106
2795
  return;
3107
2796
  }
3108
2797
  // Handle specific error messages
@@ -3118,6 +2807,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
3118
2807
  await refreshAIT();
3119
2808
  setIsLoading(false);
3120
2809
  showSuccess(walletTextUtils.getWalletText('Wallet verification completed successfully! Your wallet is now verified and ready to use.', serviceId));
2810
+ await retryPendingTextTool();
3121
2811
  return;
3122
2812
  } catch (error) {
3123
2813
  console.error('Custom wallet verification failed:', error);
@@ -3298,6 +2988,7 @@ export const ChatBotProvider: React.FC<ChatBotProps> = ({
3298
2988
  // Functions
3299
2989
  connectWallet,
3300
2990
  signInWallet,
2991
+ retryPendingTextTool,
3301
2992
  sendMessage,
3302
2993
  handleSubmit,
3303
2994
  handlePresetMessage,