@bytexbyte/nxtlinq-ai-agent-sdk 1.0.7 → 1.0.9

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.
@@ -1,1192 +0,0 @@
1
- import * as React from 'react';
2
- import { ethers } from 'ethers';
3
- import { createNxtlinqApi } from '../api/nxtlinq-api';
4
- import stringify from 'fast-json-stable-stringify';
5
- import metakeepClient from '../core/metakeepClient';
6
-
7
- export interface Message {
8
- id: string;
9
- content: string;
10
- role: 'user' | 'assistant';
11
- timestamp: string;
12
- button?: boolean;
13
- error?: string;
14
- }
15
-
16
- export interface PresetMessage {
17
- text: string;
18
- autoSend?: boolean;
19
- }
20
-
21
- export interface ToolUse {
22
- name: string;
23
- input: Record<string, any>;
24
- }
25
-
26
- export interface AITMetadata {
27
- model: string;
28
- permissions: string[];
29
- issuedBy: string;
30
- }
31
-
32
- export interface AIT {
33
- aitId: string;
34
- controller: string;
35
- metadata: AITMetadata;
36
- metadataHash: string;
37
- metadataCid: string;
38
- signature: string;
39
- }
40
-
41
- export interface ChatBotProps {
42
- projectId?: string;
43
- onMessage?: (message: Message) => void;
44
- onError?: (error: Error) => void;
45
- onToolUse?: (toolUse: ToolUse) => Promise<Message | void>;
46
- presetMessages?: PresetMessage[];
47
- placeholder?: string;
48
- className?: string;
49
- maxRetries?: number;
50
- retryDelay?: number;
51
- serviceId: string;
52
- apiKey: string;
53
- apiSecret: string;
54
- onVerifyWallet?: (address: string) => Promise<{
55
- token: string;
56
- }>;
57
- }
58
-
59
- const PermissionForm: React.FC<{
60
- hitAddress: string | null;
61
- permissions: string[];
62
- setPermissions: (permissions: string[]) => void;
63
- setIsDisabled: (disabled: boolean) => void;
64
- onClose: () => void;
65
- onSave: () => void;
66
- onConnectWallet: () => void;
67
- onSignIn: () => void;
68
- isNeedSignInWithWallet: boolean;
69
- walletInfo: any;
70
- onVerifyWallet: () => void;
71
- }> = ({ hitAddress, permissions, setPermissions, setIsDisabled, onClose, onSave, onConnectWallet, onSignIn, isNeedSignInWithWallet, walletInfo, onVerifyWallet }) => {
72
- const availablePermissions = [
73
- { id: 'setUserName', label: 'Set User Name' },
74
- { id: 'navigateToPage', label: 'Navigate To Page' },
75
- { id: 'addMember', label: 'Add Member' }
76
- ];
77
-
78
- const hasAccessToken = !!localStorage.getItem('nxtlinqAITServiceAccessToken');
79
- const isWalletVerified = walletInfo?.id;
80
-
81
- return (
82
- <div style={{
83
- backgroundColor: 'white',
84
- padding: '24px',
85
- borderRadius: '12px',
86
- width: '480px',
87
- maxWidth: '90%',
88
- boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)'
89
- }}>
90
- <div style={{
91
- display: 'flex',
92
- justifyContent: 'space-between',
93
- alignItems: 'center',
94
- marginBottom: '24px'
95
- }}>
96
- <h3 style={{
97
- margin: 0,
98
- fontSize: '20px',
99
- fontWeight: '600',
100
- color: '#1a1a1a'
101
- }}>AIT Settings</h3>
102
- <button
103
- onClick={onClose}
104
- style={{
105
- background: 'none',
106
- border: 'none',
107
- fontSize: '24px',
108
- cursor: 'pointer',
109
- color: '#666',
110
- padding: '4px',
111
- display: 'flex',
112
- alignItems: 'center',
113
- justifyContent: 'center'
114
- }}
115
- >
116
- ×
117
- </button>
118
- </div>
119
-
120
- {!hitAddress ? (
121
- <div style={{ textAlign: 'center', padding: '32px 0' }}>
122
- <div style={{
123
- width: '64px',
124
- height: '64px',
125
- margin: '0 auto 16px',
126
- backgroundColor: '#f5f5f5',
127
- borderRadius: '50%',
128
- display: 'flex',
129
- alignItems: 'center',
130
- justifyContent: 'center'
131
- }}>
132
- <span style={{ fontSize: '32px' }}>👛</span>
133
- </div>
134
- <p style={{
135
- marginBottom: '24px',
136
- fontSize: '16px',
137
- color: '#666'
138
- }}>Please connect your wallet first</p>
139
- <button
140
- onClick={onConnectWallet}
141
- style={{
142
- padding: '12px 24px',
143
- backgroundColor: '#007bff',
144
- color: 'white',
145
- border: 'none',
146
- borderRadius: '8px',
147
- cursor: 'pointer',
148
- fontSize: '16px',
149
- fontWeight: '500',
150
- transition: 'background-color 0.2s'
151
- }}
152
- onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#0056b3'}
153
- onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#007bff'}
154
- >
155
- Connect Wallet
156
- </button>
157
- </div>
158
- ) : isNeedSignInWithWallet ? (
159
- <div style={{ textAlign: 'center', padding: '32px 0' }}>
160
- <div style={{ marginBottom: '24px' }}>
161
- <h4 style={{
162
- marginBottom: '12px',
163
- fontSize: '16px',
164
- color: '#666'
165
- }}>Connected Wallet</h4>
166
- <p style={{
167
- wordBreak: 'break-all',
168
- backgroundColor: '#f8f9fa',
169
- padding: '12px',
170
- borderRadius: '8px',
171
- fontSize: '14px',
172
- color: '#333',
173
- border: '1px solid #e9ecef'
174
- }}>
175
- {hitAddress}
176
- </p>
177
- </div>
178
- <p style={{
179
- marginBottom: '24px',
180
- fontSize: '16px',
181
- color: '#666'
182
- }}>Please sign in to continue</p>
183
- <button
184
- onClick={onSignIn}
185
- style={{
186
- padding: '12px 24px',
187
- backgroundColor: '#007bff',
188
- color: 'white',
189
- border: 'none',
190
- borderRadius: '8px',
191
- cursor: 'pointer',
192
- fontSize: '16px',
193
- fontWeight: '500',
194
- transition: 'background-color 0.2s'
195
- }}
196
- onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#0056b3'}
197
- onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#007bff'}
198
- >
199
- Sign In
200
- </button>
201
- </div>
202
- ) : !isWalletVerified ? (
203
- <div style={{ textAlign: 'center', padding: '32px 0' }}>
204
- <div style={{ marginBottom: '24px' }}>
205
- <h4 style={{
206
- marginBottom: '12px',
207
- fontSize: '16px',
208
- color: '#666'
209
- }}>Connected Wallet</h4>
210
- <p style={{
211
- wordBreak: 'break-all',
212
- backgroundColor: '#f8f9fa',
213
- padding: '12px',
214
- borderRadius: '8px',
215
- fontSize: '14px',
216
- color: '#333',
217
- border: '1px solid #e9ecef'
218
- }}>
219
- {hitAddress}
220
- </p>
221
- </div>
222
- <p style={{
223
- marginBottom: '24px',
224
- fontSize: '16px',
225
- color: '#666'
226
- }}>Please verify your wallet to continue</p>
227
- <button
228
- onClick={onVerifyWallet}
229
- style={{
230
- padding: '12px 24px',
231
- backgroundColor: '#007bff',
232
- color: 'white',
233
- border: 'none',
234
- borderRadius: '8px',
235
- cursor: 'pointer',
236
- fontSize: '16px',
237
- fontWeight: '500',
238
- transition: 'background-color 0.2s'
239
- }}
240
- onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#0056b3'}
241
- onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#007bff'}
242
- >
243
- Verify your wallet
244
- </button>
245
- </div>
246
- ) : (
247
- <>
248
- <div style={{ marginBottom: '24px' }}>
249
- <h4 style={{
250
- marginBottom: '12px',
251
- fontSize: '16px',
252
- color: '#666'
253
- }}>Connected Wallet</h4>
254
- <p style={{
255
- wordBreak: 'break-all',
256
- backgroundColor: '#f8f9fa',
257
- padding: '12px',
258
- borderRadius: '8px',
259
- fontSize: '14px',
260
- color: '#333',
261
- border: '1px solid #e9ecef'
262
- }}>
263
- {hitAddress}
264
- </p>
265
- </div>
266
- <div style={{ marginBottom: '24px' }}>
267
- <h4 style={{
268
- marginBottom: '12px',
269
- fontSize: '16px',
270
- color: '#666'
271
- }}>Permissions</h4>
272
- <div style={{
273
- backgroundColor: '#f8f9fa',
274
- padding: '16px',
275
- borderRadius: '8px',
276
- border: '1px solid #e9ecef'
277
- }}>
278
- {availablePermissions.map((permission) => (
279
- <div key={permission.id} style={{ marginBottom: '12px' }}>
280
- <label style={{
281
- display: 'flex',
282
- alignItems: 'center',
283
- gap: '12px',
284
- cursor: 'pointer',
285
- padding: '8px',
286
- borderRadius: '6px',
287
- transition: 'background-color 0.2s'
288
- }}
289
- onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#e9ecef'}
290
- onMouseOut={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
291
- >
292
- <input
293
- type="checkbox"
294
- checked={permissions.includes(permission.id)}
295
- onChange={() => {
296
- const newPermissions = permissions.includes(permission.id)
297
- ? permissions.filter(p => p !== permission.id)
298
- : [...permissions, permission.id].sort();
299
- setPermissions(newPermissions);
300
- setIsDisabled(false);
301
- }}
302
- style={{
303
- margin: 0,
304
- width: '18px',
305
- height: '18px',
306
- cursor: 'pointer'
307
- }}
308
- />
309
- <span style={{
310
- fontSize: '14px',
311
- color: '#333'
312
- }}>{permission.label}</span>
313
- </label>
314
- </div>
315
- ))}
316
- </div>
317
- </div>
318
- <div style={{
319
- display: 'flex',
320
- justifyContent: 'flex-end',
321
- gap: '12px',
322
- borderTop: '1px solid #e9ecef',
323
- paddingTop: '24px'
324
- }}>
325
- <button
326
- onClick={onClose}
327
- style={{
328
- padding: '10px 20px',
329
- backgroundColor: '#f8f9fa',
330
- color: '#666',
331
- border: '1px solid #dee2e6',
332
- borderRadius: '8px',
333
- cursor: 'pointer',
334
- fontSize: '14px',
335
- fontWeight: '500',
336
- transition: 'all 0.2s'
337
- }}
338
- onMouseOver={(e) => {
339
- e.currentTarget.style.backgroundColor = '#e9ecef';
340
- e.currentTarget.style.borderColor = '#ced4da';
341
- }}
342
- onMouseOut={(e) => {
343
- e.currentTarget.style.backgroundColor = '#f8f9fa';
344
- e.currentTarget.style.borderColor = '#dee2e6';
345
- }}
346
- >
347
- Cancel
348
- </button>
349
- <button
350
- onClick={onSave}
351
- disabled={permissions.length === 0}
352
- style={{
353
- padding: '10px 20px',
354
- backgroundColor: permissions.length === 0 ? '#e9ecef' : '#007bff',
355
- color: 'white',
356
- border: 'none',
357
- borderRadius: '8px',
358
- cursor: permissions.length === 0 ? 'not-allowed' : 'pointer',
359
- fontSize: '14px',
360
- fontWeight: '500',
361
- transition: 'background-color 0.2s'
362
- }}
363
- onMouseOver={(e) => {
364
- if (permissions.length > 0) {
365
- e.currentTarget.style.backgroundColor = '#0056b3';
366
- }
367
- }}
368
- onMouseOut={(e) => {
369
- if (permissions.length > 0) {
370
- e.currentTarget.style.backgroundColor = '#007bff';
371
- }
372
- }}
373
- >
374
- Save
375
- </button>
376
- </div>
377
- </>
378
- )}
379
- </div>
380
- );
381
- };
382
-
383
- export const ChatBot: React.FC<ChatBotProps> = ({
384
- projectId,
385
- onMessage,
386
- onError,
387
- onToolUse,
388
- presetMessages = [],
389
- placeholder = 'Type a message...',
390
- className = '',
391
- maxRetries = 3,
392
- retryDelay = 1000,
393
- serviceId,
394
- apiKey,
395
- apiSecret,
396
- onVerifyWallet
397
- }) => {
398
- const [messages, setMessages] = React.useState<Message[]>([]);
399
- const [inputValue, setInputValue] = React.useState('');
400
- const [isLoading, setIsLoading] = React.useState(false);
401
- const [isOpen, setIsOpen] = React.useState(false);
402
- const [hitAddress, setHitAddress] = React.useState<string | null>(null);
403
- const [ait, setAit] = React.useState<AIT | null>(null);
404
- const [permissions, setPermissions] = React.useState<string[]>([]);
405
- const [showPermissionForm, setShowPermissionForm] = React.useState(false);
406
- const [success, setSuccess] = React.useState(false);
407
- const messagesEndRef = React.useRef<HTMLDivElement>(null);
408
- const [isDisabled, setIsDisabled] = React.useState(true);
409
- const [signer, setSigner] = React.useState<ethers.Signer | null>(null);
410
- const [provider, setProvider] = React.useState<ethers.BrowserProvider | null>(null);
411
- const [accessToken, setAccessToken] = React.useState<string | null>(null);
412
- const [walletInfo, setWalletInfo] = React.useState<any>(null);
413
- const verifyWalletBtnRef = React.useRef<HTMLButtonElement>(null);
414
-
415
- const nxtlinqApi = React.useMemo(() => createNxtlinqApi(apiKey, apiSecret), [apiKey, apiSecret]);
416
-
417
- const isNeedSignInWithWallet = React.useMemo(() => {
418
- if (!hitAddress) {
419
- console.log('isNeedSignInWithWallet: false (no hitAddress)');
420
- return false;
421
- }
422
-
423
- const nxtlinqAITServiceAccessToken = localStorage.getItem('nxtlinqAITServiceAccessToken');
424
- if (!nxtlinqAITServiceAccessToken) {
425
- console.log('isNeedSignInWithWallet: true (no token)');
426
- return true;
427
- }
428
-
429
- try {
430
- // Check if the token is expired
431
- const payload = JSON.parse(atob(nxtlinqAITServiceAccessToken.split('.')[1]));
432
- const exp = payload.exp * 1000; // Convert to milliseconds
433
- const now = Date.now();
434
- if (exp < now) {
435
- console.log('isNeedSignInWithWallet: true (token expired)');
436
- return true;
437
- }
438
-
439
- // Check is the token's payload has the same address as the wallet address
440
- const address = payload.address;
441
- if (address !== hitAddress) {
442
- console.log('isNeedSignInWithWallet: true (address mismatch)');
443
- return true;
444
- }
445
-
446
- console.log('isNeedSignInWithWallet: false (valid token)');
447
- return false;
448
- } catch (error) {
449
- console.error('Error parsing token:', error);
450
- console.log('isNeedSignInWithWallet: true (token parse error)');
451
- return true;
452
- }
453
- }, [hitAddress]);
454
-
455
- const handleVerifySuccess = (address: string) => {
456
- console.log('handleVerifySuccess called with address:', address);
457
- localStorage.setItem(`wallet_verified_${address}`, 'true');
458
- const getWalletInfo = async () => {
459
- try {
460
- const token = JSON.parse(localStorage.getItem('nxtlinqAITServiceAccessToken') || '');
461
- const walletResponse = await nxtlinqApi.wallet.getWallet({ address }, token);
462
- console.log('Wallet response:', walletResponse);
463
- if (!('error' in walletResponse)) {
464
- setWalletInfo(walletResponse);
465
- const aitResponse = await nxtlinqApi.ait.getAITByServiceIdAndController({
466
- serviceId,
467
- controller: address
468
- }, token);
469
- console.log('AIT response:', aitResponse);
470
- if (!('error' in aitResponse)) {
471
- setAit(aitResponse);
472
- }
473
- }
474
- } catch (error) {
475
- console.error('Failed to update wallet info after verification:', error);
476
- }
477
- };
478
- getWalletInfo();
479
- };
480
-
481
- React.useEffect(() => {
482
- const getWalletInfo = async () => {
483
- if (!hitAddress) {
484
- return;
485
- }
486
- if (isNeedSignInWithWallet) {
487
- return;
488
- }
489
- const getWalletResponse = await nxtlinqApi.wallet.getWallet({ address: hitAddress }, accessToken as string);
490
- if ('error' in getWalletResponse) {
491
- console.error(getWalletResponse.error);
492
- return;
493
- }
494
- setWalletInfo(getWalletResponse);
495
- };
496
-
497
- getWalletInfo();
498
- }, [hitAddress, isNeedSignInWithWallet]);
499
-
500
- const handleVerifyWalletClick = async () => {
501
- console.log('handleVerifyWalletClick called');
502
- console.log('Current states:', {
503
- hitAddress,
504
- isNeedSignInWithWallet,
505
- walletInfo,
506
- signer: !!signer
507
- });
508
-
509
- if (!hitAddress) {
510
- alert('Please connect your wallet first.');
511
- return;
512
- }
513
-
514
- try {
515
- if (onVerifyWallet) {
516
- setIsLoading(true);
517
- const result = await onVerifyWallet(hitAddress);
518
- console.log('Verify wallet response:', result);
519
- const { token } = result;
520
- const address = hitAddress;
521
- if (token && address) {
522
- const payload = {
523
- address: hitAddress,
524
- token,
525
- timestamp: Date.now(),
526
- method: 'berifyme'
527
- };
528
- try {
529
- const verifyWalletResponse = await nxtlinqApi.wallet.verifyWallet({ ...payload }, token);
530
- if ('error' in verifyWalletResponse) {
531
- if (verifyWalletResponse.error === 'Wallet already exists') {
532
- // 已存在则直接获取钱包信息
533
- const walletResponse = await nxtlinqApi.wallet.getWallet({ address }, token);
534
- if (!('error' in walletResponse)) {
535
- setWalletInfo(walletResponse);
536
- const aitResponse = await nxtlinqApi.ait.getAITByServiceIdAndController({ serviceId, controller: address }, token);
537
- if (!('error' in aitResponse)) {
538
- setAit(aitResponse);
539
- }
540
- }
541
- // 清理 URL 上的 token
542
- if (typeof window !== 'undefined') {
543
- try {
544
- const url = new URL(window.location.href);
545
- url.searchParams.delete('token');
546
- window.history.replaceState({}, '', url.toString());
547
- } catch (e) {
548
- console.error('Failed to clean URL:', e);
549
- }
550
- }
551
- setIsLoading(false);
552
- return { token, hitAddress: address };
553
- }
554
- alert(verifyWalletResponse.error);
555
- setIsLoading(false);
556
- return;
557
- }
558
- // 验证成功,获取钱包信息
559
- const walletResponse = await nxtlinqApi.wallet.getWallet({ address }, token);
560
- if (!('error' in walletResponse)) {
561
- setWalletInfo(walletResponse);
562
- const aitResponse = await nxtlinqApi.ait.getAITByServiceIdAndController({ serviceId, controller: address }, token);
563
- if (!('error' in aitResponse)) {
564
- setAit(aitResponse);
565
- }
566
- }
567
- // 清理 URL 上的 token
568
- if (typeof window !== 'undefined') {
569
- try {
570
- const url = new URL(window.location.href);
571
- url.searchParams.delete('token');
572
- window.history.replaceState({}, '', url.toString());
573
- } catch (e) {
574
- console.error('Failed to clean URL:', e);
575
- }
576
- }
577
- setIsLoading(false);
578
- return { token, hitAddress: address };
579
- } catch (error) {
580
- let msg = 'Verification failed';
581
- if (typeof error === 'object' && error !== null && 'response' in error) {
582
- // @ts-ignore
583
- msg = error.response?.data?.error || error.message || msg;
584
- } else if (error instanceof Error) {
585
- msg = error.message;
586
- }
587
- console.error('Wallet verification failed:', error);
588
- alert(msg);
589
- setIsLoading(false);
590
- throw error;
591
- }
592
- }
593
- setIsLoading(false);
594
- return { token, hitAddress: address };
595
- }
596
- } catch (error) {
597
- console.error('Failed to verify wallet:', error);
598
- setIsLoading(false);
599
- alert('Failed to verify wallet. Please try again.');
600
- throw error;
601
- }
602
- };
603
-
604
- // 添加一个 useEffect 来处理 URL 参数
605
- React.useEffect(() => {
606
- if (typeof window !== 'undefined') {
607
- try {
608
- const urlParams = new URLSearchParams(window.location.search);
609
- const token = urlParams.get('token');
610
- if (token && hitAddress) {
611
- handleVerifyWalletClick();
612
- }
613
- } catch (e) {
614
- console.error('Failed to get URL params:', e);
615
- }
616
- }
617
- }, [hitAddress]);
618
-
619
- React.useEffect(() => {
620
- console.log('Wallet states changed:', {
621
- hitAddress,
622
- isNeedSignInWithWallet,
623
- walletInfo,
624
- signer: !!signer
625
- });
626
- }, [hitAddress, isNeedSignInWithWallet, walletInfo, signer]);
627
-
628
- const scrollToBottom = () => {
629
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
630
- };
631
-
632
- React.useEffect(() => {
633
- scrollToBottom();
634
- }, [messages]);
635
-
636
- React.useEffect(() => {
637
- if (!hitAddress) {
638
- setAit(null);
639
- }
640
- }, [hitAddress]);
641
-
642
- React.useEffect(() => {
643
- if (!ait) {
644
- return;
645
- }
646
-
647
- const aitPermissions = ait.metadata?.permissions || [];
648
- setPermissions(aitPermissions);
649
- }, [ait]);
650
-
651
- const connectWallet = React.useCallback(async () => {
652
- if (typeof window === 'undefined') {
653
- console.error('Web3 is not available in server-side rendering');
654
- return;
655
- }
656
-
657
- try {
658
- const web3Provider = await metakeepClient.ethereum;
659
- if (!web3Provider) {
660
- throw new Error('Web3 provider not available');
661
- }
662
-
663
- await web3Provider.enable();
664
- const ethersProvider = new ethers.BrowserProvider(web3Provider);
665
-
666
- const userSigner = await ethersProvider.getSigner();
667
- const userAddress = await userSigner.getAddress();
668
-
669
- localStorage.setItem('walletAddress', userAddress);
670
-
671
- setHitAddress(userAddress);
672
- setSigner(userSigner);
673
- setProvider(ethersProvider);
674
-
675
- return userAddress;
676
- } catch (error) {
677
- console.error('Failed to connect wallet:', error);
678
- localStorage.removeItem('walletAddress');
679
- setHitAddress(null);
680
- setSigner(null);
681
- setProvider(null);
682
- throw error;
683
- }
684
- }, []);
685
-
686
- const signInWallet = async () => {
687
- if (!hitAddress) {
688
- alert('Please connect your wallet first.');
689
- return;
690
- }
691
-
692
- if (!signer) {
693
- alert('Please connect your wallet first.');
694
- return;
695
- }
696
-
697
- try {
698
- const nonceResponse = await nxtlinqApi.auth.getNonce({ address: hitAddress });
699
- if ('error' in nonceResponse) {
700
- alert(nonceResponse.error);
701
- return;
702
- }
703
-
704
- const payload = {
705
- address: hitAddress,
706
- code: nonceResponse.code,
707
- timestamp: nonceResponse.timestamp
708
- };
709
-
710
- const stringToSign = stringify(payload);
711
- const signature = await signer.signMessage(stringToSign || '');
712
-
713
- const response = await nxtlinqApi.auth.signIn({
714
- ...payload,
715
- signature
716
- });
717
-
718
- if ('error' in response) {
719
- alert(response.error);
720
- return;
721
- }
722
- const { accessToken } = response;
723
- localStorage.setItem('nxtlinqAITServiceAccessToken', JSON.stringify(accessToken));
724
- setAccessToken(accessToken);
725
-
726
- // 登录成功后,获取 AIT 信息
727
- try {
728
- const token = JSON.parse(localStorage.getItem('nxtlinqAITServiceAccessToken') || '');
729
- const aitResponse = await nxtlinqApi.ait.getAITByServiceIdAndController(
730
- { serviceId, controller: hitAddress },
731
- token
732
- );
733
- if (!('error' in aitResponse)) {
734
- setAit(aitResponse);
735
- }
736
- } catch (error) {
737
- console.error('Failed to get AIT:', error);
738
- }
739
- } catch (error) {
740
- console.error('Failed to sign in:', error);
741
- alert('Failed to sign in. Please try again.');
742
- }
743
- };
744
-
745
- const hasPermission = async (toolName: string) => {
746
- if (!ait) {
747
- setMessages(prev => [...prev, {
748
- id: Date.now().toString(),
749
- content: 'Please connect your HIT wallet and sign in to access permissions.',
750
- role: 'assistant',
751
- timestamp: new Date().toISOString(),
752
- button: true
753
- }]);
754
- return false;
755
- }
756
- return permissions.includes(toolName);
757
- };
758
-
759
- const sendMessage = async (content: string, retryCount = 0): Promise<Message> => {
760
- try {
761
- setIsLoading(true);
762
- const response = await nxtlinqApi.agent.sendMessage({
763
- message: content,
764
- serviceId,
765
- });
766
-
767
- if ('error' in response) {
768
- throw new Error(response.error);
769
- }
770
-
771
- const message: Message = {
772
- id: Date.now().toString(),
773
- content: response.reply,
774
- role: 'assistant',
775
- timestamp: new Date().toISOString()
776
- };
777
-
778
- setMessages(prev => [...prev, message]);
779
- onMessage?.(message);
780
- return message;
781
- } catch (error) {
782
- if (retryCount < maxRetries) {
783
- await new Promise(resolve => setTimeout(resolve, retryDelay));
784
- return sendMessage(content, retryCount + 1);
785
- }
786
- throw error;
787
- } finally {
788
- setIsLoading(false);
789
- }
790
- };
791
-
792
- const handleSubmit = async (e: React.FormEvent) => {
793
- e.preventDefault();
794
- if (!inputValue.trim() || isLoading) return;
795
-
796
- const userMessage: Message = {
797
- id: Date.now().toString(),
798
- content: inputValue,
799
- role: 'user',
800
- timestamp: new Date().toISOString()
801
- };
802
-
803
- setMessages(prev => [...prev, userMessage]);
804
- setInputValue('');
805
- setIsLoading(true);
806
-
807
- try {
808
- const response = await sendMessage(inputValue);
809
- if (response.error) {
810
- throw new Error(response.error);
811
- }
812
- setMessages(prev => [...prev, response]);
813
- } catch (error) {
814
- console.error('发送消息失败:', error);
815
- onError?.(error instanceof Error ? error : new Error('发送消息失败'));
816
- setMessages(prev => [...prev, {
817
- id: Date.now().toString(),
818
- content: 'Sorry, there was an error processing your message. Please try again.',
819
- role: 'assistant',
820
- timestamp: new Date().toISOString()
821
- }]);
822
- } finally {
823
- setIsLoading(false);
824
- }
825
- };
826
-
827
- const handlePresetMessage = (message: PresetMessage) => {
828
- if (message.autoSend) {
829
- setMessages(prev => [...prev, {
830
- id: Date.now().toString(),
831
- content: message.text,
832
- role: 'user',
833
- timestamp: new Date().toISOString()
834
- }]);
835
- sendMessage(message.text);
836
- } else {
837
- setInputValue(message.text);
838
- }
839
- };
840
-
841
- const generateAndRegisterAIT = async () => {
842
- if (!signer || !hitAddress) return;
843
-
844
- const timestamp = Math.floor(Date.now() / 1000);
845
- const aitId = `did:polygon:ike-dashboard:${hitAddress}:${timestamp}`;
846
-
847
- const metadata = {
848
- model: 'gpt-4',
849
- permissions,
850
- issuedBy: hitAddress,
851
- controller: hitAddress
852
- };
853
-
854
- const metadataStr = stringify(metadata);
855
- const metadataHash = ethers.keccak256(ethers.toUtf8Bytes(metadataStr || ''));
856
-
857
- // 1️⃣ Upload metadata to Pinata, get CID
858
- const uploadResponse = await nxtlinqApi.metadata.createMetadata(metadata, accessToken || '');
859
- if ('error' in uploadResponse) {
860
- throw new Error(`Failed to upload metadata: ${uploadResponse.error}`);
861
- }
862
-
863
- const { metadataCid } = uploadResponse;
864
-
865
- // 2️⃣ Sign the message
866
- const messageHash = ethers.solidityPackedKeccak256(
867
- ['string', 'address', 'string', 'bytes32', 'uint256'],
868
- [aitId, hitAddress, serviceId, metadataHash, timestamp]
869
- );
870
-
871
- const signature = await signer.signMessage(ethers.getBytes(messageHash));
872
-
873
- await nxtlinqApi.ait.createAIT({
874
- aitId,
875
- controller: hitAddress,
876
- serviceId,
877
- metadataHash,
878
- metadataCid,
879
- timestamp,
880
- signature
881
- }, accessToken || '');
882
-
883
- const aitInfo = {
884
- aitId,
885
- controller: hitAddress,
886
- metadata,
887
- metadataHash,
888
- metadataCid,
889
- signature
890
- };
891
-
892
- setAit(aitInfo);
893
- };
894
-
895
- const savePermissions = async () => {
896
- setIsLoading(true);
897
- setIsDisabled(true);
898
- try {
899
- await generateAndRegisterAIT();
900
- setIsLoading(false);
901
- setSuccess(true);
902
- setShowPermissionForm(false);
903
- } catch (error) {
904
- console.error('Failed to generate AIT:', error);
905
- setIsLoading(false);
906
- setIsDisabled(false);
907
- if (error instanceof Error) {
908
- alert(error.message);
909
- }
910
- }
911
- };
912
-
913
- // 添加状态变化的监听
914
- React.useEffect(() => {
915
- console.log('States updated:', {
916
- hitAddress,
917
- isNeedSignInWithWallet,
918
- walletInfo,
919
- isOpen
920
- });
921
- }, [hitAddress, isNeedSignInWithWallet, walletInfo, isOpen]);
922
-
923
- return (
924
- <div style={{
925
- position: 'fixed',
926
- bottom: '20px',
927
- right: '20px',
928
- zIndex: 1000,
929
- display: 'flex',
930
- flexDirection: 'column',
931
- alignItems: 'flex-end',
932
- gap: '10px'
933
- }}>
934
- {isOpen && (
935
- <div className={`nxtlinq-chatbot ${className}`} style={{
936
- width: '350px',
937
- height: '500px',
938
- backgroundColor: 'white',
939
- borderRadius: '10px',
940
- boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)',
941
- display: 'flex',
942
- flexDirection: 'column',
943
- overflow: 'hidden'
944
- }}>
945
- <div style={{
946
- padding: '15px',
947
- backgroundColor: '#007bff',
948
- color: 'white',
949
- borderRadius: '10px 10px 0 0',
950
- display: 'flex',
951
- justifyContent: 'space-between',
952
- alignItems: 'center'
953
- }}>
954
- <h3 style={{ margin: 0 }}>AI Agent</h3>
955
- <div style={{ display: 'flex', gap: '10px' }}>
956
- <button
957
- onClick={() => setShowPermissionForm(true)}
958
- style={{
959
- background: 'none',
960
- border: 'none',
961
- color: 'white',
962
- fontSize: '20px',
963
- cursor: 'pointer',
964
- padding: '0 5px'
965
- }}
966
- >
967
- ⚙️
968
- </button>
969
- <button
970
- onClick={() => setIsOpen(false)}
971
- style={{
972
- background: 'none',
973
- border: 'none',
974
- color: 'white',
975
- fontSize: '24px',
976
- cursor: 'pointer',
977
- padding: '0 5px'
978
- }}
979
- >
980
- ×
981
- </button>
982
- </div>
983
- </div>
984
-
985
- <div style={{
986
- flex: 1,
987
- overflowY: 'auto',
988
- padding: '15px',
989
- display: 'flex',
990
- flexDirection: 'column',
991
- gap: '10px'
992
- }}>
993
- {messages.map((message, index) => (
994
- <div
995
- key={index}
996
- style={{
997
- alignSelf: message.role === 'user' ? 'flex-end' : 'flex-start',
998
- backgroundColor: message.role === 'user' ? '#007bff' : '#f0f0f0',
999
- color: message.role === 'user' ? 'white' : 'black',
1000
- padding: '8px 12px',
1001
- borderRadius: '15px',
1002
- maxWidth: '80%',
1003
- wordBreak: 'break-word'
1004
- }}
1005
- >
1006
- {message.content}
1007
- {message.button && (
1008
- <button
1009
- onClick={connectWallet}
1010
- disabled={!!hitAddress}
1011
- style={{
1012
- display: 'block',
1013
- marginTop: '8px',
1014
- padding: '5px 10px',
1015
- backgroundColor: hitAddress ? '#ccc' : '#28a745',
1016
- color: 'white',
1017
- border: 'none',
1018
- borderRadius: '5px',
1019
- cursor: hitAddress ? 'not-allowed' : 'pointer'
1020
- }}
1021
- >
1022
- {hitAddress ? 'Connected' : 'Connect HIT wallet'}
1023
- </button>
1024
- )}
1025
- </div>
1026
- ))}
1027
- {isLoading && (
1028
- <div style={{
1029
- alignSelf: 'flex-start',
1030
- backgroundColor: '#f0f0f0',
1031
- padding: '8px 12px',
1032
- borderRadius: '15px',
1033
- maxWidth: '80%'
1034
- }}>
1035
- Thinking...
1036
- </div>
1037
- )}
1038
- <div ref={messagesEndRef} />
1039
- </div>
1040
-
1041
- <div style={{
1042
- padding: '10px',
1043
- borderTop: '1px solid #eee',
1044
- display: 'flex',
1045
- flexDirection: 'column',
1046
- gap: '10px'
1047
- }}>
1048
- <div style={{
1049
- display: 'flex',
1050
- gap: '5px',
1051
- overflowX: 'auto',
1052
- padding: '5px 0'
1053
- }}>
1054
- {presetMessages.map((message, index) => (
1055
- <button
1056
- key={index}
1057
- onClick={() => handlePresetMessage(message)}
1058
- style={{
1059
- padding: '5px 10px',
1060
- backgroundColor: '#f0f0f0',
1061
- border: 'none',
1062
- borderRadius: '15px',
1063
- cursor: 'pointer',
1064
- whiteSpace: 'nowrap',
1065
- fontSize: '12px'
1066
- }}
1067
- >
1068
- {message.text}
1069
- </button>
1070
- ))}
1071
- </div>
1072
-
1073
- <form onSubmit={handleSubmit} style={{
1074
- display: 'flex',
1075
- gap: '10px'
1076
- }}>
1077
- <input
1078
- type="text"
1079
- value={inputValue}
1080
- onChange={(e) => setInputValue(e.target.value)}
1081
- placeholder={placeholder}
1082
- style={{
1083
- flex: 1,
1084
- padding: '8px 12px',
1085
- border: '1px solid #ddd',
1086
- borderRadius: '20px',
1087
- fontSize: '14px',
1088
- outline: 'none'
1089
- }}
1090
- />
1091
- <button
1092
- type="submit"
1093
- disabled={isLoading}
1094
- style={{
1095
- backgroundColor: '#007bff',
1096
- color: 'white',
1097
- border: 'none',
1098
- borderRadius: '20px',
1099
- padding: '8px 20px',
1100
- cursor: isLoading ? 'not-allowed' : 'pointer',
1101
- fontSize: '14px',
1102
- opacity: isLoading ? 0.7 : 1
1103
- }}
1104
- >
1105
- Send
1106
- </button>
1107
- </form>
1108
- </div>
1109
- </div>
1110
- )}
1111
-
1112
- <button
1113
- onClick={() => setIsOpen(!isOpen)}
1114
- style={{
1115
- width: '120px',
1116
- height: '40px',
1117
- borderRadius: '20px',
1118
- backgroundColor: '#007bff',
1119
- color: '#ffffff',
1120
- border: 'none',
1121
- cursor: 'pointer',
1122
- display: 'flex',
1123
- alignItems: 'center',
1124
- justifyContent: 'center',
1125
- fontSize: '14px',
1126
- fontWeight: 'bold',
1127
- boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
1128
- transition: 'all 0.3s ease-in-out',
1129
- padding: '0 20px'
1130
- }}
1131
- >
1132
- AI Agent
1133
- </button>
1134
-
1135
- {showPermissionForm && (
1136
- <div style={{
1137
- position: 'fixed',
1138
- top: 0,
1139
- left: 0,
1140
- right: 0,
1141
- bottom: 0,
1142
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
1143
- display: 'flex',
1144
- alignItems: 'center',
1145
- justifyContent: 'center',
1146
- zIndex: 2000
1147
- }}>
1148
- <PermissionForm
1149
- hitAddress={hitAddress}
1150
- permissions={permissions}
1151
- setPermissions={setPermissions}
1152
- setIsDisabled={setIsDisabled}
1153
- onClose={() => setShowPermissionForm(false)}
1154
- onConnectWallet={connectWallet}
1155
- onSignIn={signInWallet}
1156
- onSave={savePermissions}
1157
- isNeedSignInWithWallet={isNeedSignInWithWallet}
1158
- walletInfo={walletInfo}
1159
- onVerifyWallet={handleVerifyWalletClick}
1160
- />
1161
- </div>
1162
- )}
1163
-
1164
- {success && (
1165
- <div style={{
1166
- position: 'fixed',
1167
- bottom: 100,
1168
- right: 40,
1169
- background: '#4caf50',
1170
- color: 'white',
1171
- padding: '12px 24px',
1172
- borderRadius: 8,
1173
- zIndex: 2000,
1174
- fontWeight: 600,
1175
- }}>
1176
- Saved successfully!
1177
- <button
1178
- style={{
1179
- background: 'none',
1180
- border: 'none',
1181
- color: 'white',
1182
- marginLeft: 16,
1183
- fontSize: 18,
1184
- cursor: 'pointer'
1185
- }}
1186
- onClick={() => setSuccess(false)}
1187
- >×</button>
1188
- </div>
1189
- )}
1190
- </div>
1191
- );
1192
- };