nitrostack 1.0.70 → 1.0.72

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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/src/studio/app/api/chat/route.ts +33 -15
  3. package/src/studio/app/auth/callback/page.tsx +6 -6
  4. package/src/studio/app/chat/page.tsx +1124 -415
  5. package/src/studio/app/chat/page.tsx.backup +1046 -187
  6. package/src/studio/app/globals.css +361 -191
  7. package/src/studio/app/health/page.tsx +72 -76
  8. package/src/studio/app/layout.tsx +9 -11
  9. package/src/studio/app/logs/page.tsx +29 -30
  10. package/src/studio/app/page.tsx +134 -230
  11. package/src/studio/app/prompts/page.tsx +115 -97
  12. package/src/studio/app/resources/page.tsx +115 -124
  13. package/src/studio/app/settings/page.tsx +1080 -125
  14. package/src/studio/app/tools/page.tsx +343 -0
  15. package/src/studio/components/EnlargeModal.tsx +76 -65
  16. package/src/studio/components/LogMessage.tsx +5 -5
  17. package/src/studio/components/MarkdownRenderer.tsx +4 -4
  18. package/src/studio/components/Sidebar.tsx +150 -210
  19. package/src/studio/components/SplashScreen.tsx +109 -0
  20. package/src/studio/components/ToolCard.tsx +50 -41
  21. package/src/studio/components/VoiceOrbOverlay.tsx +469 -0
  22. package/src/studio/components/WidgetRenderer.tsx +8 -3
  23. package/src/studio/components/tools/ToolsCanvas.tsx +327 -0
  24. package/src/studio/lib/llm-service.ts +104 -1
  25. package/src/studio/lib/store.ts +36 -21
  26. package/src/studio/lib/types.ts +1 -1
  27. package/src/studio/package-lock.json +3303 -0
  28. package/src/studio/package.json +3 -1
  29. package/src/studio/public/NitroStudio Isotype Color.png +0 -0
  30. package/src/studio/tailwind.config.ts +63 -17
  31. package/templates/typescript-starter/package-lock.json +4112 -0
  32. package/templates/typescript-starter/package.json +2 -3
  33. package/templates/typescript-starter/src/modules/calculator/calculator.tools.ts +100 -5
  34. package/src/studio/app/auth/page.tsx +0 -560
  35. package/src/studio/app/ping/page.tsx +0 -209
@@ -1,14 +1,291 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useEffect } from 'react';
4
- import { Settings as SettingsIcon, Wifi, CheckCircle, AlertCircle } from 'lucide-react';
4
+ import { api } from '@/lib/api';
5
+ import { useStudioStore } from '@/lib/store';
6
+ import {
7
+ Cog6ToothIcon as SettingsIcon,
8
+ WifiIcon,
9
+ CheckCircleIcon,
10
+ ExclamationCircleIcon,
11
+ MicrophoneIcon,
12
+ SpeakerWaveIcon,
13
+ BookmarkIcon,
14
+ ArrowTopRightOnSquareIcon,
15
+ InformationCircleIcon,
16
+ ShieldCheckIcon,
17
+ KeyIcon,
18
+ XCircleIcon,
19
+ LockClosedIcon,
20
+ ClockIcon,
21
+ HeartIcon,
22
+ ArrowTrendingUpIcon,
23
+ ChatBubbleLeftIcon,
24
+ } from '@heroicons/react/24/outline';
5
25
 
6
26
  export default function SettingsPage() {
27
+ // Auth State
28
+ const { oauthState, setOAuthState } = useStudioStore();
29
+ const initialServerUrl =
30
+ typeof process !== 'undefined' && process.env.MCP_TRANSPORT_TYPE === 'stdio'
31
+ ? `http://localhost:${process.env.MCP_SERVER_PORT || 3005}`
32
+ : '';
33
+ const [serverUrl, setServerUrl] = useState(initialServerUrl);
34
+ const [clientName, setClientName] = useState('NitroStack Studio');
35
+ const [redirectUri, setRedirectUri] = useState('http://localhost:3000/auth/callback');
36
+ const [manualToken, setManualToken] = useState('');
37
+ const [manualApiKey, setManualApiKey] = useState('');
38
+ const [discovering, setDiscovering] = useState(false);
39
+ const [registering, setRegistering] = useState(false);
40
+ const [manualClientId, setManualClientId] = useState('');
41
+ const [manualClientSecret, setManualClientSecret] = useState('');
42
+
43
+ // Ping State
44
+ const { pingHistory, addPingResult } = useStudioStore();
45
+ const [pinging, setPinging] = useState(false);
46
+ const [lastLatency, setLastLatency] = useState<number | null>(null);
47
+
48
+ // Ping Helpers
49
+ const handlePing = async () => {
50
+ setPinging(true);
51
+ const startTime = Date.now();
52
+
53
+ try {
54
+ await api.ping();
55
+ const latency = Date.now() - startTime;
56
+ setLastLatency(latency);
57
+ addPingResult({ time: new Date(), latency });
58
+ } catch (error) {
59
+ console.error('Ping failed:', error);
60
+ } finally {
61
+ setPinging(false);
62
+ }
63
+ };
64
+
65
+ const getLatencyColor = (latency: number) => {
66
+ if (latency < 100) return 'text-emerald-500';
67
+ if (latency < 500) return 'text-yellow-500';
68
+ return 'text-rose-500';
69
+ };
70
+
71
+ const getLatencyDotColor = (latency: number) => {
72
+ if (latency < 100) return 'bg-emerald-500';
73
+ if (latency < 500) return 'bg-yellow-500';
74
+ return 'bg-rose-500';
75
+ };
76
+
77
+ // Calculate statistics
78
+ const averageLatency = pingHistory.length > 0
79
+ ? Math.round(pingHistory.reduce((acc, curr) => acc + curr.latency, 0) / pingHistory.length)
80
+ : 0;
81
+
82
+ const minLatency = pingHistory.length > 0
83
+ ? Math.min(...pingHistory.map(p => p.latency))
84
+ : 0;
85
+
86
+ const maxLatency = pingHistory.length > 0
87
+ ? Math.max(...pingHistory.map(p => p.latency))
88
+ : 0;
89
+
90
+ // PKCE Helpers
91
+ const generateCodeVerifier = () => {
92
+ const array = new Uint8Array(32);
93
+ crypto.getRandomValues(array);
94
+ return base64UrlEncode(array);
95
+ };
96
+
97
+ const generateCodeChallenge = async (verifier: string) => {
98
+ const encoder = new TextEncoder();
99
+ const data = encoder.encode(verifier);
100
+ const hash = await crypto.subtle.digest('SHA-256', data);
101
+ return base64UrlEncode(new Uint8Array(hash));
102
+ };
103
+
104
+ const base64UrlEncode = (buffer: Uint8Array) => {
105
+ const base64 = btoa(String.fromCharCode(...buffer));
106
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
107
+ };
108
+
109
+ const handleDiscover = async () => {
110
+ setDiscovering(true);
111
+ try {
112
+ const resourceMetadataUrl = new URL('/.well-known/oauth-protected-resource', serverUrl).toString();
113
+ const resourceMetadata = await api.discoverAuth(resourceMetadataUrl, 'resource');
114
+ const authServerUrl = resourceMetadata.authorization_servers[0];
115
+ const authServerMetadataUrl = new URL('/.well-known/oauth-authorization-server', authServerUrl).toString();
116
+ const authServerMetadata = await api.discoverAuth(authServerMetadataUrl, 'auth-server');
117
+
118
+ setOAuthState({
119
+ authServerUrl: serverUrl,
120
+ resourceMetadata,
121
+ authServerMetadata,
122
+ });
123
+
124
+ alert('Discovery successful!');
125
+ } catch (error) {
126
+ console.error('Discovery failed:', error);
127
+ alert('Discovery failed. See console for details.');
128
+ } finally {
129
+ setDiscovering(false);
130
+ }
131
+ };
132
+
133
+ const handleRegister = async () => {
134
+ if (!oauthState.authServerMetadata?.registration_endpoint) {
135
+ alert('Dynamic registration not supported');
136
+ return;
137
+ }
138
+
139
+ setRegistering(true);
140
+ try {
141
+ const registration = await api.registerClient(
142
+ oauthState.authServerMetadata.registration_endpoint,
143
+ {
144
+ client_name: clientName,
145
+ redirect_uris: [redirectUri],
146
+ grant_types: ['authorization_code', 'refresh_token'],
147
+ response_types: ['code'],
148
+ scope: oauthState.selectedScopes.join(' '),
149
+ }
150
+ );
151
+
152
+ setOAuthState({ clientRegistration: registration });
153
+ alert('Client registered successfully!');
154
+ } catch (error) {
155
+ console.error('Registration failed:', error);
156
+ alert('Registration failed. See console for details.');
157
+ } finally {
158
+ setRegistering(false);
159
+ }
160
+ };
161
+
162
+ const handleManualCredentials = () => {
163
+ if (!manualClientId.trim()) {
164
+ alert('Please enter Client ID');
165
+ return;
166
+ }
167
+
168
+ setOAuthState({
169
+ ...oauthState,
170
+ clientRegistration: {
171
+ client_id: manualClientId.trim(),
172
+ client_secret: manualClientSecret.trim() || undefined,
173
+ },
174
+ });
175
+
176
+ alert('Client credentials saved!');
177
+ };
178
+
179
+ const handleUseManualToken = () => {
180
+ if (!manualToken.trim()) {
181
+ alert('Please enter a token');
182
+ return;
183
+ }
184
+
185
+ setJwtToken(manualToken);
186
+ setManualToken('');
187
+ alert('Token set successfully!');
188
+ };
189
+
190
+ const handleUseApiKey = () => {
191
+ if (!manualApiKey.trim()) {
192
+ alert('Please enter an API key');
193
+ return;
194
+ }
195
+
196
+ setApiKey(manualApiKey);
197
+ setManualApiKey('');
198
+ alert('API key set successfully!');
199
+ };
200
+
201
+ const handleStartOAuthFlow = async () => {
202
+ if (!oauthState.clientRegistration?.client_id) {
203
+ alert('Please register a client or enter manual credentials first');
204
+ return;
205
+ }
206
+
207
+ if (!oauthState.authServerMetadata?.authorization_endpoint) {
208
+ alert('Authorization endpoint not found in server metadata');
209
+ return;
210
+ }
211
+
212
+ try {
213
+ const codeVerifier = generateCodeVerifier();
214
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
215
+
216
+ sessionStorage.setItem('oauth_code_verifier', codeVerifier);
217
+ sessionStorage.setItem('oauth_state', Math.random().toString(36).substring(7));
218
+
219
+ const authUrl = new URL(oauthState.authServerMetadata.authorization_endpoint);
220
+ authUrl.searchParams.set('client_id', oauthState.clientRegistration.client_id);
221
+ authUrl.searchParams.set('response_type', 'code');
222
+ authUrl.searchParams.set('redirect_uri', redirectUri);
223
+ authUrl.searchParams.set('scope', oauthState.resourceMetadata?.scopes_supported?.join(' ') || 'openid profile');
224
+ authUrl.searchParams.set('state', sessionStorage.getItem('oauth_state')!);
225
+ authUrl.searchParams.set('code_challenge', codeChallenge);
226
+ authUrl.searchParams.set('code_challenge_method', 'S256');
227
+
228
+ if (oauthState.resourceMetadata?.resource) {
229
+ authUrl.searchParams.set('audience', oauthState.resourceMetadata.resource);
230
+ }
231
+
232
+ window.location.href = authUrl.toString();
233
+ } catch (error) {
234
+ console.error('Failed to start OAuth flow:', error);
235
+ alert('Failed to start OAuth flow. See console for details.');
236
+ }
237
+ };
238
+
239
+ const [activeTab, setActiveTab] = useState<'general' | 'chat' | 'auth' | 'ping'>('general');
7
240
  const [transport, setTransport] = useState<'stdio' | 'http'>('stdio');
241
+ const {
242
+ connection,
243
+ jwtToken, setJwtToken,
244
+ apiKey, setApiKey,
245
+ elevenLabsApiKey, setElevenLabsApiKey
246
+ } = useStudioStore();
247
+
248
+ // Chat Settings State
249
+ const [currentProvider, setCurrentProvider] = useState<'openai' | 'gemini'>('gemini');
250
+ const [voiceModel, setVoiceModel] = useState('eleven_multilingual_v2');
251
+ const [voiceId, setVoiceId] = useState('21m00Tcm4TlvDq8ikWAM'); // Rachel
252
+ const [availableVoices, setAvailableVoices] = useState<any[]>([]);
253
+
8
254
  const [connecting, setConnecting] = useState(false);
9
255
  const [connectionStatus, setConnectionStatus] = useState<'idle' | 'success' | 'error'>('idle');
10
256
  const [errorMessage, setErrorMessage] = useState('');
11
257
 
258
+ // Fetch ElevenLabs voices when API key is set
259
+ useEffect(() => {
260
+ if (elevenLabsApiKey) {
261
+ fetch('https://api.elevenlabs.io/v1/voices', {
262
+ headers: { 'xi-api-key': elevenLabsApiKey }
263
+ })
264
+ .then(res => res.json())
265
+ .then(data => {
266
+ if (data.voices) {
267
+ setAvailableVoices(data.voices);
268
+ }
269
+ })
270
+ .catch(err => console.error('Failed to fetch voices:', err));
271
+ }
272
+ }, [elevenLabsApiKey]);
273
+
274
+ const saveApiKey = (provider: 'openai' | 'gemini') => {
275
+ const input = document.getElementById(`${provider}-api-key`) as HTMLInputElement;
276
+ const key = input?.value.trim();
277
+
278
+ if (!key || key === '••••••••') {
279
+ alert('Please enter a valid API key');
280
+ return;
281
+ }
282
+
283
+ localStorage.setItem(`${provider}_api_key`, key);
284
+ input.value = '••••••••';
285
+ alert(`${provider === 'openai' ? 'OpenAI' : 'Gemini'} API key saved`);
286
+ };
287
+
288
+ // ... (existing useEffect and handleSaveSettings) ...
12
289
  useEffect(() => {
13
290
  // Load saved settings from localStorage
14
291
  const savedTransport = localStorage.getItem('mcp_transport');
@@ -51,149 +328,827 @@ export default function SettingsPage() {
51
328
  }
52
329
  };
53
330
 
331
+
54
332
  return (
55
333
  <div className="fixed inset-0 flex flex-col bg-background" style={{ left: 'var(--sidebar-width, 15rem)' }}>
56
- {/* Sticky Header */}
57
- <div className="sticky top-0 z-10 border-b border-border/50 px-6 py-3 flex items-center justify-between bg-card/80 backdrop-blur-md shadow-sm">
58
- <div className="flex items-center gap-3">
59
- <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-amber-500 flex items-center justify-center shadow-md">
60
- <SettingsIcon className="w-5 h-5 text-white" strokeWidth={2.5} />
61
- </div>
62
- <div>
63
- <h1 className="text-lg font-bold text-foreground">Settings</h1>
64
- </div>
65
- </div>
334
+ {/* Header */}
335
+ <div className="sticky top-0 z-10 border-b border-border/50 px-6 py-4 flex items-center justify-between bg-card/50 backdrop-blur-sm">
336
+ <h1 className="text-lg font-semibold text-foreground flex items-center gap-2">
337
+ <SettingsIcon className="w-5 h-5 text-muted-foreground" />
338
+ Settings
339
+ </h1>
66
340
  </div>
67
341
 
68
- {/* Content - ONLY this scrolls */}
69
- <div className="flex-1 overflow-y-auto overflow-x-hidden">
70
- <div className="max-w-4xl mx-auto px-6 py-6">
71
-
72
- {/* Settings Card */}
73
- <div className="max-w-2xl">
74
- <div className="card p-6">
75
- <h2 className="text-xl font-semibold text-foreground mb-6">Transport Configuration</h2>
76
-
77
- {/* Transport Type Selection */}
78
- <div className="mb-6">
79
- <label className="block text-sm font-medium text-foreground mb-3">
80
- Transport Type
81
- </label>
82
- <div className="grid grid-cols-2 gap-4">
83
- <button
84
- disabled
85
- className={`p-4 rounded-lg border-2 transition-all text-left border-primary bg-primary/10`}
86
- >
87
- <div className="flex items-center gap-3 mb-2">
88
- <div className={`w-3 h-3 rounded-full bg-primary`} />
89
- <h3 className="font-semibold text-foreground">STDIO</h3>
90
- </div>
91
- <p className="text-sm text-muted-foreground">
92
- Direct process communication (default)
93
- </p>
94
- </button>
95
-
96
- <button
97
- disabled
98
- className={`p-4 rounded-lg border-2 transition-all text-left border-border hover:border-primary/50`}
99
- >
100
- <div className="flex items-center gap-3 mb-2">
101
- <div className={`w-3 h-3 rounded-full bg-muted`} />
102
- <h3 className="font-semibold text-foreground">HTTP</h3>
342
+ <div className="flex flex-1 overflow-hidden">
343
+ {/* Settings Sidebar */}
344
+ <div className="w-64 border-r border-border/50 bg-muted/10 p-4 space-y-2 overflow-y-auto">
345
+ <button
346
+ onClick={() => setActiveTab('general')}
347
+ className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'general' ? 'bg-primary/10 text-primary' : 'text-muted-foreground hover:bg-muted hover:text-foreground'
348
+ }`}
349
+ >
350
+ <SettingsIcon className="w-5 h-5" />
351
+ General
352
+ </button>
353
+ <button
354
+ onClick={() => setActiveTab('chat')}
355
+ className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'chat' ? 'bg-primary/10 text-primary' : 'text-muted-foreground hover:bg-muted hover:text-foreground'
356
+ }`}
357
+ >
358
+ <ChatBubbleLeftIcon className="w-5 h-5" />
359
+ Chat Configuration
360
+ </button>
361
+ <button
362
+ onClick={() => setActiveTab('auth')}
363
+ className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'auth' ? 'bg-primary/10 text-primary' : 'text-muted-foreground hover:bg-muted hover:text-foreground'
364
+ }`}
365
+ >
366
+ <ShieldCheckIcon className="w-5 h-5" />
367
+ Authentication
368
+ </button>
369
+ <button
370
+ onClick={() => setActiveTab('ping')}
371
+ className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors ${activeTab === 'ping' ? 'bg-primary/10 text-primary' : 'text-muted-foreground hover:bg-muted hover:text-foreground'
372
+ }`}
373
+ >
374
+ <WifiIcon className="w-5 h-5" />
375
+ Connectivity Check
376
+ </button>
377
+ </div>
378
+
379
+ {/* Content Area */}
380
+ <div className="flex-1 overflow-y-auto p-8">
381
+ <div className="max-w-3xl mx-auto animate-fade-in">
382
+ {activeTab === 'general' && (
383
+ <div className="space-y-6">
384
+ <div>
385
+ <h2 className="text-xl font-semibold text-foreground mb-1">General Settings</h2>
386
+ <p className="text-sm text-muted-foreground">Configure global application settings and transport</p>
103
387
  </div>
104
- <p className="text-sm text-muted-foreground">
105
- HTTP/SSE transport for remote servers
106
- </p>
107
- </button>
108
- </div>
109
- </div>
110
388
 
111
- {/* Port Allocation Info */}
112
- <div className="mb-6 p-4 bg-blue-500/10 rounded-lg border border-blue-500/20">
113
- <h4 className="font-semibold text-foreground mb-2">Port Allocation</h4>
114
- <div className="space-y-1 text-sm">
115
- <div className="flex items-center gap-2">
116
- <span className="text-muted-foreground">Studio UI:</span>
117
- <code className="px-2 py-0.5 bg-muted rounded text-foreground">Port 3000</code>
118
- </div>
119
- <div className="flex items-center gap-2">
120
- <span className="text-muted-foreground">Widget Server:</span>
121
- <code className="px-2 py-0.5 bg-muted rounded text-foreground">Port 3001</code>
122
- </div>
123
- <div className="flex items-center gap-2">
124
- <span className="text-muted-foreground">MCP HTTP Server:</span>
125
- <code className="px-2 py-0.5 bg-primary/20 rounded text-primary font-semibold">Port 3002</code>
389
+ {/* Transport Configuration (Existing) */}
390
+ <div className="card p-6">
391
+ <h2 className="text-lg font-semibold text-foreground mb-6">Transport Configuration</h2>
392
+
393
+ {/* Transport Type Selection */}
394
+ <div className="mb-6">
395
+ <label className="block text-sm font-medium text-foreground mb-3">
396
+ Transport Type
397
+ </label>
398
+ <div className="grid grid-cols-2 gap-4">
399
+ <button
400
+ disabled
401
+ className={`p-4 rounded-lg border-2 transition-all text-left border-primary bg-primary/10`}
402
+ >
403
+ <div className="flex items-center gap-3 mb-2">
404
+ <div className={`w-3 h-3 rounded-full bg-primary`} />
405
+ <h3 className="font-semibold text-foreground">STDIO</h3>
406
+ </div>
407
+ <p className="text-sm text-muted-foreground">
408
+ Direct process communication (default)
409
+ </p>
410
+ </button>
411
+
412
+ <button
413
+ disabled
414
+ className={`p-4 rounded-lg border-2 transition-all text-left border-border hover:border-primary/50`}
415
+ >
416
+ <div className="flex items-center gap-3 mb-2">
417
+ <div className={`w-3 h-3 rounded-full bg-muted`} />
418
+ <h3 className="font-semibold text-foreground">HTTP</h3>
419
+ </div>
420
+ <p className="text-sm text-muted-foreground">
421
+ HTTP/SSE transport for remote servers
422
+ </p>
423
+ </button>
424
+ </div>
425
+ </div>
426
+
427
+ {/* Port Allocation Info */}
428
+ <div className="mb-6 p-4 bg-blue-500/10 rounded-lg border border-blue-500/20">
429
+ <h4 className="font-semibold text-foreground mb-2">Port Allocation</h4>
430
+ <div className="space-y-1 text-sm">
431
+ <div className="flex items-center gap-2">
432
+ <span className="text-muted-foreground">Studio UI:</span>
433
+ <code className="px-2 py-0.5 bg-muted rounded text-foreground">Port 3000</code>
434
+ </div>
435
+ <div className="flex items-center gap-2">
436
+ <span className="text-muted-foreground">Widget Server:</span>
437
+ <code className="px-2 py-0.5 bg-muted rounded text-foreground">Port 3001</code>
438
+ </div>
439
+ <div className="flex items-center gap-2">
440
+ <span className="text-muted-foreground">MCP HTTP Server:</span>
441
+ <code className="px-2 py-0.5 bg-primary/20 rounded text-primary font-semibold">Port 3002</code>
442
+ </div>
443
+ </div>
444
+ </div>
445
+
446
+ {/* Connection Status & Save Button */}
447
+ {connectionStatus !== 'idle' && (
448
+ <div className={`mb-6 p-4 rounded-lg border ${connectionStatus === 'success'
449
+ ? 'bg-emerald-500/10 border-emerald-500/20'
450
+ : 'bg-rose-500/10 border-rose-500/20'
451
+ }`}>
452
+ <div className="flex gap-3">
453
+ {connectionStatus === 'success' ? (
454
+ <>
455
+ <CheckCircleIcon className="w-5 h-5 text-emerald-500 flex-shrink-0 mt-0.5" />
456
+ <div>
457
+ <h4 className="font-semibold text-emerald-500 mb-1">Connected</h4>
458
+ <p className="text-sm text-emerald-600">
459
+ Settings saved and connection established successfully.
460
+ </p>
461
+ </div>
462
+ </>
463
+ ) : (
464
+ <>
465
+ <ExclamationCircleIcon className="w-5 h-5 text-rose-500 flex-shrink-0 mt-0.5" />
466
+ <div>
467
+ <h4 className="font-semibold text-rose-500 mb-1">Connection Failed</h4>
468
+ <p className="text-sm text-rose-600">
469
+ {errorMessage || 'Failed to establish connection with the selected transport.'}
470
+ </p>
471
+ </div>
472
+ </>
473
+ )}
474
+ </div>
475
+ </div>
476
+ )}
477
+
478
+ <button
479
+ onClick={handleSaveSettings}
480
+ disabled={connecting}
481
+ className="btn btn-primary w-full"
482
+ >
483
+ {connecting ? (
484
+ <>
485
+ <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
486
+ Connecting...
487
+ </>
488
+ ) : (
489
+ 'Save & Connect'
490
+ )}
491
+ </button>
492
+ </div>
126
493
  </div>
127
- </div>
128
- </div>
494
+ )}
495
+
496
+ {activeTab === 'chat' && (
497
+ <div className="space-y-8 animate-fade-in">
498
+ <div>
499
+ <h2 className="text-xl font-semibold text-foreground mb-1">Chat Configuration</h2>
500
+ <p className="text-sm text-muted-foreground">Manage AI providers, API keys, and voice settings</p>
501
+ </div>
502
+
503
+ {/* Section: AI Provider */}
504
+ <section className="space-y-4">
505
+ <div className="flex items-center justify-between">
506
+ <label className="text-xs font-bold text-muted-foreground uppercase tracking-wider">AI Provider</label>
507
+ <span className="text-[10px] font-medium px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20">Active</span>
508
+ </div>
509
+
510
+ <div className="grid grid-cols-2 gap-3">
511
+ <button
512
+ onClick={() => setCurrentProvider('gemini')}
513
+ className={`relative p-4 rounded-xl border-2 text-left transition-all duration-200 group ${currentProvider === 'gemini'
514
+ ? 'border-blue-500 bg-blue-50/50 dark:bg-blue-500/10 shadow-sm'
515
+ : 'border-border hover:border-blue-500/50 hover:bg-muted/30'
516
+ }`}
517
+ >
518
+ {currentProvider === 'gemini' && (
519
+ <div className="absolute top-3 right-3 w-2 h-2 rounded-full bg-blue-500 shadow-[0_0_8px_rgba(59,130,246,0.6)]" />
520
+ )}
521
+ <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center mb-3 shadow-inner">
522
+ <span className="text-white font-bold text-sm">G</span>
523
+ </div>
524
+ <div className="font-semibold text-sm text-foreground mb-0.5">Gemini</div>
525
+ <div className="text-xs text-muted-foreground">Google AI</div>
526
+ </button>
527
+
528
+ <button
529
+ onClick={() => setCurrentProvider('openai')}
530
+ className={`relative p-4 rounded-xl border-2 text-left transition-all duration-200 group ${currentProvider === 'openai'
531
+ ? 'border-green-500 bg-green-50/50 dark:bg-green-500/10 shadow-sm'
532
+ : 'border-border hover:border-green-500/50 hover:bg-muted/30'
533
+ }`}
534
+ >
535
+ {currentProvider === 'openai' && (
536
+ <div className="absolute top-3 right-3 w-2 h-2 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)]" />
537
+ )}
538
+ <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center mb-3 shadow-inner">
539
+ <span className="text-white font-bold text-xs tracking-tighter">AI</span>
540
+ </div>
541
+ <div className="font-semibold text-sm text-foreground mb-0.5">OpenAI</div>
542
+ <div className="text-xs text-muted-foreground">GPT-4</div>
543
+ </button>
544
+ </div>
545
+ </section>
546
+
547
+ {/* Section: API Keys */}
548
+ <section className="space-y-4">
549
+ <label className="text-xs font-bold text-muted-foreground uppercase tracking-wider">Configuration</label>
550
+
551
+ <div className="space-y-4">
552
+ {currentProvider === 'gemini' && (
553
+ <div className="animate-fade-in">
554
+ <label className="block text-sm font-medium text-foreground mb-2">Gemini API Key</label>
555
+ <div className="relative flex items-center">
556
+ <input
557
+ type="password"
558
+ className="input w-full pr-20 font-mono text-sm bg-muted/30 focus:bg-background transition-colors"
559
+ placeholder={localStorage.getItem('gemini_api_key') ? "••••••••" : "Paste API Key"}
560
+ id="gemini-api-key"
561
+ />
562
+ <button
563
+ onClick={() => saveApiKey('gemini')}
564
+ className="absolute right-1 top-1 bottom-1 px-3 bg-white dark:bg-zinc-800 hover:bg-gray-50 dark:hover:bg-zinc-700 text-xs font-medium rounded border border-border transition-colors shadow-sm"
565
+ >
566
+ Save
567
+ </button>
568
+ </div>
569
+ <p className="text-[10px] text-muted-foreground mt-2 flex items-center gap-1.5">
570
+ <InformationCircleIcon className="w-3 h-3" />
571
+ <span>Get your key from <a href="https://aistudio.google.com/app/apikey" target="_blank" className="underline hover:text-foreground">Google AI Studio</a></span>
572
+ </p>
573
+ </div>
574
+ )}
575
+
576
+ {currentProvider === 'openai' && (
577
+ <div className="animate-fade-in">
578
+ <label className="block text-sm font-medium text-foreground mb-2">OpenAI API Key</label>
579
+ <div className="relative flex items-center">
580
+ <input
581
+ type="password"
582
+ className="input w-full pr-20 font-mono text-sm bg-muted/30 focus:bg-background transition-colors"
583
+ placeholder={localStorage.getItem('openai_api_key') ? "••••••••" : "Paste API Key"}
584
+ id="openai-api-key"
585
+ />
586
+ <button
587
+ onClick={() => saveApiKey('openai')}
588
+ className="absolute right-1 top-1 bottom-1 px-3 bg-white dark:bg-zinc-800 hover:bg-gray-50 dark:hover:bg-zinc-700 text-xs font-medium rounded border border-border transition-colors shadow-sm"
589
+ >
590
+ Save
591
+ </button>
592
+ </div>
593
+ <p className="text-[10px] text-muted-foreground mt-2 flex items-center gap-1.5">
594
+ <InformationCircleIcon className="w-3 h-3" />
595
+ <span>Get your key from <a href="https://platform.openai.com/api-keys" target="_blank" className="underline hover:text-foreground">OpenAI Platform</a></span>
596
+ </p>
597
+ </div>
598
+ )}
599
+ </div>
600
+ </section>
129
601
 
130
- {/* Info Box */}
131
- <div className="mb-6 p-4 bg-primary/5 rounded-lg border border-primary/20">
132
- <div className="flex gap-3">
133
- <Wifi className="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
134
- <div>
135
- <h4 className="font-semibold text-foreground mb-1">Transport Information</h4>
136
- <p className="text-sm text-muted-foreground mb-2">
137
- <strong>STDIO</strong> (Default): Studio spawns the MCP server as a child process and communicates
138
- via standard input/output. The MCP server still runs dual transport (STDIO + HTTP on port 3002).
139
- </p>
602
+ {/* Section: Voice (ElevenLabs) */}
603
+ <section className="space-y-4 pt-4 border-t border-border">
604
+ <div className="flex items-center justify-between">
605
+ <label className="text-xs font-bold text-muted-foreground uppercase tracking-wider flex items-center gap-2">
606
+ <MicrophoneIcon className="w-3 h-3" /> Voice Integration
607
+ </label>
608
+ {elevenLabsApiKey && <span className="text-[10px] bg-purple-500/10 text-purple-600 px-2 py-0.5 rounded-full font-medium border border-purple-500/20">Enabled</span>}
609
+ </div>
610
+
611
+ <div className="bg-card rounded-xl border border-border overflow-hidden">
612
+ <div className="p-4 bg-muted/10 border-b border-border">
613
+ <div className="flex items-center gap-3">
614
+ <div className="w-10 h-10 rounded-lg bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center text-white shadow-md">
615
+ <SpeakerWaveIcon className="w-5 h-5" />
616
+ </div>
617
+ <div>
618
+ <h3 className="text-sm font-semibold text-foreground">ElevenLabs</h3>
619
+ <p className="text-xs text-muted-foreground">Text-to-Speech Engine</p>
620
+ </div>
621
+ </div>
622
+ </div>
623
+
624
+ <div className="p-4 space-y-4">
625
+ <div>
626
+ <label className="block text-xs font-medium text-foreground mb-1.5">API Configuration</label>
627
+ <div className="relative">
628
+ <input
629
+ type="password"
630
+ value={elevenLabsApiKey || ''}
631
+ onChange={(e) => setElevenLabsApiKey(e.target.value || null)}
632
+ className="input w-full font-mono text-xs"
633
+ placeholder={elevenLabsApiKey ? "••••••••••••••••" : "Paste your xi-api-key here"}
634
+ />
635
+ </div>
636
+ </div>
637
+
638
+ {elevenLabsApiKey ? (
639
+ <div className="space-y-3 animate-fade-in">
640
+ <div>
641
+ <label className="block text-xs font-medium text-foreground mb-1.5">Voice Model</label>
642
+ <select
643
+ value={voiceModel}
644
+ onChange={(e) => setVoiceModel(e.target.value)}
645
+ className="input w-full text-xs"
646
+ >
647
+ <option value="eleven_multilingual_v2">Multilingual v2 (Best)</option>
648
+ <option value="eleven_turbo_v2_5">Turbo v2.5 (Fastest)</option>
649
+ <option value="eleven_monolingual_v1">Monolingual v1</option>
650
+ </select>
651
+ </div>
652
+
653
+ <div>
654
+ <label className="block text-xs font-medium text-foreground mb-1.5">Voice ID</label>
655
+ <select
656
+ value={voiceId}
657
+ onChange={(e) => setVoiceId(e.target.value)}
658
+ className="input w-full text-xs"
659
+ >
660
+ {availableVoices.map(v => (
661
+ <option key={v.voice_id} value={v.voice_id}>{v.name} ({v.labels?.accent || 'Default'})</option>
662
+ ))}
663
+ </select>
664
+ </div>
665
+ </div>
666
+ ) : (
667
+ <div className="p-3 bg-muted/30 rounded-lg border border-dashed border-border text-center">
668
+ <p className="text-xs text-muted-foreground">Add API key to unlock premium voice capabilities.</p>
669
+ <a href="https://elevenlabs.io" target="_blank" className="text-[10px] text-primary hover:underline mt-1 block">Get a key →</a>
670
+ </div>
671
+ )}
672
+ </div>
673
+ </div>
674
+ </section>
140
675
  </div>
141
- </div>
142
- </div>
676
+ )}
677
+
678
+ {activeTab === 'auth' && (
679
+ <div className="space-y-8 animate-fade-in">
680
+ <div>
681
+ <h2 className="text-xl font-semibold text-foreground mb-1">Authentication</h2>
682
+ <p className="text-sm text-muted-foreground">Configure OAuth 2.1, JWT tokens, and API Keys</p>
683
+ </div>
684
+
685
+ {/* JWT Token Management */}
686
+ <div className="card card-hover p-6">
687
+ <h2 className="text-xl font-semibold text-foreground mb-4 flex items-center gap-2">
688
+ <KeyIcon className="w-5 h-5 text-primary" />
689
+ JWT Token
690
+ </h2>
143
691
 
144
- {/* Connection Status */}
145
- {connectionStatus !== 'idle' && (
146
- <div className={`mb-6 p-4 rounded-lg border ${
147
- connectionStatus === 'success'
148
- ? 'bg-emerald-500/10 border-emerald-500/20'
149
- : 'bg-rose-500/10 border-rose-500/20'
150
- }`}>
151
- <div className="flex gap-3">
152
- {connectionStatus === 'success' ? (
153
- <>
154
- <CheckCircle className="w-5 h-5 text-emerald-500 flex-shrink-0 mt-0.5" />
155
- <div>
156
- <h4 className="font-semibold text-emerald-500 mb-1">Connected</h4>
157
- <p className="text-sm text-emerald-600">
158
- Settings saved and connection established successfully.
692
+ {/* Status Badge */}
693
+ <div className="flex items-center gap-4 mb-6">
694
+ <div className={`w-14 h-14 rounded flex items-center justify-center ${jwtToken ? 'bg-emerald-500/10' : 'bg-rose-500/10'}`}>
695
+ {jwtToken ? (
696
+ <CheckCircleIcon className="w-8 h-8 text-emerald-500" />
697
+ ) : (
698
+ <XCircleIcon className="w-8 h-8 text-rose-500" />
699
+ )}
700
+ </div>
701
+ <div className="flex-1">
702
+ <p className="font-semibold text-foreground">
703
+ {jwtToken ? 'Token Active' : 'No Token Set'}
159
704
  </p>
705
+ <p className="text-sm text-muted-foreground mt-1">
706
+ {jwtToken
707
+ ? 'Token is automatically included in all tool calls and chat requests'
708
+ : 'Set a token manually or login via tools to authenticate'}
709
+ </p>
710
+ </div>
711
+ </div>
712
+
713
+ {/* Token Input/Display */}
714
+ <div className="space-y-3">
715
+ <label className="block text-sm font-medium text-foreground">
716
+ Token Value
717
+ {jwtToken && <span className="ml-2 text-xs text-emerald-500">(Currently Active)</span>}
718
+ </label>
719
+ <div className="flex gap-2">
720
+ <input
721
+ type="text"
722
+ value={manualToken || jwtToken || ''}
723
+ onChange={(e) => setManualToken(e.target.value)}
724
+ placeholder="Paste or edit JWT token here..."
725
+ className="input flex-1 font-mono text-sm"
726
+ />
727
+ <button
728
+ onClick={handleUseManualToken}
729
+ className="btn btn-primary gap-2"
730
+ disabled={!manualToken.trim()}
731
+ >
732
+ <KeyIcon className="w-4 h-4" />
733
+ {jwtToken ? 'Update' : 'Set'} Token
734
+ </button>
735
+ </div>
736
+
737
+ {jwtToken && (
738
+ <div className="flex gap-2">
739
+ <button
740
+ onClick={() => {
741
+ setManualToken(jwtToken);
742
+ alert('Token copied to input for editing');
743
+ }}
744
+ className="btn btn-secondary btn-sm gap-2"
745
+ >
746
+ <LockClosedIcon className="w-3 h-3" />
747
+ Edit Current Token
748
+ </button>
749
+ <button
750
+ onClick={() => {
751
+ setJwtToken(null);
752
+ setManualToken('');
753
+ alert('Token cleared');
754
+ }}
755
+ className="btn btn-secondary btn-sm gap-2"
756
+ >
757
+ <XCircleIcon className="w-3 h-3" />
758
+ Clear Token
759
+ </button>
760
+ </div>
761
+ )}
762
+ </div>
763
+
764
+ {/* Info Box */}
765
+ <div className="mt-4 p-4 bg-blue-500/10 border border-blue-500/20 rounded-lg">
766
+ <div className="flex items-start gap-2">
767
+ <ExclamationCircleIcon className="w-5 h-5 text-blue-400 mt-0.5 flex-shrink-0" />
768
+ <div className="text-sm text-blue-200">
769
+ <p className="font-medium mb-1">How Token Management Works:</p>
770
+ <ul className="list-disc list-inside space-y-1 text-blue-300/80">
771
+ <li>Login via tool execution automatically saves token here</li>
772
+ <li>Login via chat automatically saves token here</li>
773
+ <li>Manually paste token here to use it globally</li>
774
+ <li>Token persists across page refresh</li>
775
+ <li>Token is sent with all subsequent requests</li>
776
+ </ul>
777
+ </div>
160
778
  </div>
161
- </>
162
- ) : (
163
- <>
164
- <AlertCircle className="w-5 h-5 text-rose-500 flex-shrink-0 mt-0.5" />
165
- <div>
166
- <h4 className="font-semibold text-rose-500 mb-1">Connection Failed</h4>
167
- <p className="text-sm text-rose-600">
168
- {errorMessage || 'Failed to establish connection with the selected transport.'}
779
+ </div>
780
+ </div>
781
+
782
+ {/* API Key Management */}
783
+ <div className="card card-hover p-6">
784
+ <h2 className="text-xl font-semibold text-foreground mb-4 flex items-center gap-2">
785
+ <KeyIcon className="w-5 h-5 text-primary" />
786
+ API Key
787
+ </h2>
788
+
789
+ {/* Status Badge */}
790
+ <div className="flex items-center gap-4 mb-6">
791
+ <div className={`w-14 h-14 rounded flex items-center justify-center ${apiKey ? 'bg-emerald-500/10' : 'bg-rose-500/10'}`}>
792
+ {apiKey ? (
793
+ <CheckCircleIcon className="w-8 h-8 text-emerald-500" />
794
+ ) : (
795
+ <XCircleIcon className="w-8 h-8 text-rose-500" />
796
+ )}
797
+ </div>
798
+ <div className="flex-1">
799
+ <p className="font-semibold text-foreground">
800
+ {apiKey ? 'API Key Active' : 'No API Key Set'}
801
+ </p>
802
+ <p className="text-sm text-muted-foreground mt-1">
803
+ {apiKey
804
+ ? 'API key is automatically included in all tool calls and chat requests'
805
+ : 'Set an API key to access protected tools'}
169
806
  </p>
170
807
  </div>
171
- </>
172
- )}
808
+ </div>
809
+
810
+ {/* API Key Input/Display */}
811
+ <div className="space-y-3">
812
+ <label className="block text-sm font-medium text-foreground">
813
+ API Key Value
814
+ {apiKey && <span className="ml-2 text-xs text-emerald-500">(Currently Active)</span>}
815
+ </label>
816
+ <div className="flex gap-2">
817
+ <input
818
+ type="password"
819
+ value={manualApiKey || apiKey || ''}
820
+ onChange={(e) => setManualApiKey(e.target.value)}
821
+ placeholder="Enter your API key here (e.g., sk_...)..."
822
+ className="input flex-1 font-mono text-sm"
823
+ />
824
+ <button
825
+ onClick={handleUseApiKey}
826
+ className="btn btn-primary gap-2"
827
+ disabled={!manualApiKey.trim()}
828
+ >
829
+ <KeyIcon className="w-4 h-4" />
830
+ {apiKey ? 'Update' : 'Set'} Key
831
+ </button>
832
+ </div>
833
+
834
+ {apiKey && (
835
+ <div className="flex gap-2">
836
+ <button
837
+ onClick={() => {
838
+ setManualApiKey(apiKey);
839
+ alert('API key copied to input for editing');
840
+ }}
841
+ className="btn btn-secondary btn-sm gap-2"
842
+ >
843
+ <LockClosedIcon className="w-3 h-3" />
844
+ Edit Current Key
845
+ </button>
846
+ <button
847
+ onClick={() => {
848
+ setApiKey(null);
849
+ setManualApiKey('');
850
+ alert('API key cleared');
851
+ }}
852
+ className="btn btn-secondary btn-sm gap-2"
853
+ >
854
+ <XCircleIcon className="w-3 h-3" />
855
+ Clear Key
856
+ </button>
857
+ </div>
858
+ )}
859
+ </div>
860
+
861
+ {/* Info Box */}
862
+ <div className="mt-4 p-4 bg-purple-500/10 border border-purple-500/20 rounded-lg">
863
+ <div className="flex items-start gap-2">
864
+ <ExclamationCircleIcon className="w-5 h-5 text-purple-400 mt-0.5 flex-shrink-0" />
865
+ <div className="text-sm text-purple-200">
866
+ <p className="font-medium mb-1">API Key Authentication:</p>
867
+ <ul className="list-disc list-inside space-y-1 text-purple-300/80">
868
+ <li>API keys are simpler than JWT tokens</li>
869
+ <li>Common format: sk_xxx (secret key) or pk_xxx (public key)</li>
870
+ <li>Keys are sent in X-API-Key header or _meta.apiKey field</li>
871
+ <li>Ideal for service-to-service authentication</li>
872
+ <li>Can be used together with JWT tokens for multi-auth</li>
873
+ </ul>
874
+ </div>
875
+ </div>
876
+ </div>
877
+ </div>
878
+
879
+ {/* OAuth 2.1 Flow */}
880
+ <div className="card card-hover p-6">
881
+ <h2 className="text-xl font-semibold text-foreground mb-4 flex items-center gap-2">
882
+ <ShieldCheckIcon className="w-5 h-5 text-primary" />
883
+ OAuth 2.1 Flow
884
+ </h2>
885
+ <p className="text-sm text-muted-foreground mb-6">
886
+ For OpenAI Apps SDK and OAuth 2.1 compliant servers
887
+ </p>
888
+
889
+ {/* Step 1: Discovery */}
890
+ <div className="mb-6">
891
+ <h3 className="font-medium text-foreground mb-3">1. Discover Server Auth</h3>
892
+ <input
893
+ type="url"
894
+ value={serverUrl}
895
+ onChange={(e) => setServerUrl(e.target.value)}
896
+ placeholder="https://mcp.example.com"
897
+ className="input mb-3"
898
+ />
899
+ <button
900
+ onClick={handleDiscover}
901
+ className="btn btn-primary gap-2"
902
+ disabled={discovering || !serverUrl}
903
+ >
904
+ <ArrowTopRightOnSquareIcon className="w-4 h-4" />
905
+ {discovering ? 'Discovering...' : 'Discover Auth Config'}
906
+ </button>
907
+
908
+ {oauthState.resourceMetadata && (
909
+ <div className="mt-4 p-4 bg-emerald-500/10 border border-emerald-500/20 rounded-lg">
910
+ <div className="flex items-center gap-2 mb-2">
911
+ <CheckCircleIcon className="w-4 h-4 text-emerald-500" />
912
+ <p className="text-sm font-semibold text-emerald-600 dark:text-emerald-400">Discovery Successful</p>
913
+ </div>
914
+ <details className="text-sm text-muted-foreground">
915
+ <summary className="cursor-pointer hover:text-foreground font-medium">
916
+ View Metadata
917
+ </summary>
918
+ <pre className="mt-3 p-3 bg-background rounded-lg overflow-auto max-h-40 font-mono text-xs text-foreground border border-border">
919
+ {JSON.stringify(oauthState.resourceMetadata, null, 2)}
920
+ </pre>
921
+ </details>
922
+ </div>
923
+ )}
924
+ </div>
925
+
926
+ {/* Step 2a: Manual Client Credentials (Alternative to Registration) */}
927
+ {oauthState.resourceMetadata && !oauthState.clientRegistration && (
928
+ <div className="mb-6 border-t border-border pt-6">
929
+ <h3 className="font-medium text-foreground mb-3">2a. Use Existing Client (Optional)</h3>
930
+ <p className="text-sm text-muted-foreground mb-4">
931
+ If you already have a Client ID and Secret from your OAuth provider, enter them here instead of dynamic registration.
932
+ </p>
933
+ <div className="space-y-3 mb-4">
934
+ <input
935
+ type="text"
936
+ value={manualClientId}
937
+ onChange={(e) => setManualClientId(e.target.value)}
938
+ placeholder="Client ID"
939
+ className="input font-mono"
940
+ />
941
+ <input
942
+ type="password"
943
+ value={manualClientSecret}
944
+ onChange={(e) => setManualClientSecret(e.target.value)}
945
+ placeholder="Client Secret (optional for public clients)"
946
+ className="input font-mono"
947
+ />
948
+ </div>
949
+ <button
950
+ onClick={handleManualCredentials}
951
+ className="btn btn-primary gap-2"
952
+ disabled={!manualClientId.trim()}
953
+ >
954
+ <KeyIcon className="w-4 h-4" />
955
+ Save Client Credentials
956
+ </button>
957
+ </div>
958
+ )}
959
+
960
+ {/* Step 2b: Registration */}
961
+ {oauthState.authServerMetadata && !oauthState.clientRegistration && (
962
+ <div className="mb-6 border-t border-border pt-6">
963
+ <h3 className="font-medium text-foreground mb-3">2b. Register New Client (Dynamic)</h3>
964
+ <div className="space-y-3 mb-4">
965
+ <input
966
+ type="text"
967
+ value={clientName}
968
+ onChange={(e) => setClientName(e.target.value)}
969
+ placeholder="Client Name"
970
+ className="input"
971
+ />
972
+ <input
973
+ type="url"
974
+ value={redirectUri}
975
+ onChange={(e) => setRedirectUri(e.target.value)}
976
+ placeholder="Redirect URI"
977
+ className="input"
978
+ />
979
+ </div>
980
+ <button
981
+ onClick={handleRegister}
982
+ className="btn btn-primary gap-2"
983
+ disabled={registering}
984
+ >
985
+ <ShieldCheckIcon className="w-4 h-4" />
986
+ {registering ? 'Registering...' : 'Register Client'}
987
+ </button>
988
+
989
+ {oauthState.clientRegistration && (
990
+ <div className="mt-4 p-4 bg-emerald-500/10 border border-emerald-500/20 rounded-lg">
991
+ <div className="flex items-center gap-2 mb-2">
992
+ <CheckCircleIcon className="w-4 h-4 text-emerald-500" />
993
+ <p className="text-sm font-semibold text-emerald-600 dark:text-emerald-400">Registration Successful</p>
994
+ </div>
995
+ <p className="text-sm text-muted-foreground font-mono">
996
+ Client ID: {oauthState.clientRegistration.client_id}
997
+ </p>
998
+ </div>
999
+ )}
1000
+ </div>
1001
+ )}
1002
+
1003
+ {/* Step 3: Start Flow */}
1004
+ {oauthState.clientRegistration && (
1005
+ <div className="border-t border-border pt-6">
1006
+ <h3 className="font-medium text-foreground mb-3">3. Start OAuth Flow</h3>
1007
+ <button
1008
+ onClick={handleStartOAuthFlow}
1009
+ className="btn btn-primary gap-2"
1010
+ >
1011
+ <ArrowTopRightOnSquareIcon className="w-4 h-4" />
1012
+ Start Authorization Flow
1013
+ </button>
1014
+ <p className="text-sm text-muted-foreground mt-2 flex items-center gap-1">
1015
+ <ExclamationCircleIcon className="w-4 h-4" />
1016
+ This will redirect you to the authorization server for login
1017
+ </p>
1018
+ </div>
1019
+ )}
1020
+ </div>
173
1021
  </div>
174
- </div>
175
- )}
1022
+ )}
176
1023
 
177
- {/* Save Button */}
178
- <button
179
- onClick={handleSaveSettings}
180
- disabled={connecting}
181
- className="btn btn-primary w-full"
182
- >
183
- {connecting ? (
184
- <>
185
- <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
186
- Connecting...
187
- </>
188
- ) : (
189
- 'Save & Connect'
1024
+ {activeTab === 'ping' && (
1025
+ <div className="space-y-8 animate-fade-in">
1026
+ <div>
1027
+ <h2 className="text-xl font-semibold text-foreground mb-1">Connectivity Check</h2>
1028
+ <p className="text-sm text-muted-foreground">Test connection latency to your MCP server</p>
1029
+ </div>
1030
+
1031
+ {/* Ping Action Area */}
1032
+ <div className="card p-8 border-2 border-border/50">
1033
+ <div className="flex flex-col items-center justify-center text-center">
1034
+ <div className="relative mb-6 group">
1035
+ <div className={`absolute inset-0 bg-primary/20 rounded-full blur-xl transition-all duration-500 ${pinging ? 'scale-150 opacity-100' : 'scale-100 opacity-0 group-hover:opacity-50'}`} />
1036
+ <button
1037
+ onClick={handlePing}
1038
+ disabled={pinging}
1039
+ className={`relative w-24 h-24 rounded-full flex items-center justify-center transition-all duration-300 ${pinging
1040
+ ? 'bg-primary scale-95 shadow-inner'
1041
+ : 'bg-gradient-to-br from-primary to-primary/80 hover:scale-105 shadow-xl hover:shadow-primary/25'
1042
+ }`}
1043
+ >
1044
+ <WifiIcon className={`w-10 h-10 text-primary-foreground transition-transform duration-700 ${pinging ? 'animate-pulse' : ''}`} />
1045
+ </button>
1046
+ </div>
1047
+
1048
+ <h2 className="text-2xl font-bold text-foreground mb-2">
1049
+ {pinging ? 'Pinging Server...' : 'Test Connection'}
1050
+ </h2>
1051
+ <p className="text-muted-foreground max-w-md mx-auto mb-8">
1052
+ Send a ping to your MCP server to verify connectivity and measure response latency.
1053
+ </p>
1054
+
1055
+ {lastLatency !== null && (
1056
+ <div className="animate-fade-in flex flex-col items-center">
1057
+ <div className={`text-5xl font-mono font-bold tracking-tight mb-2 ${getLatencyColor(lastLatency)}`}>
1058
+ {lastLatency}ms
1059
+ </div>
1060
+ <div className="text-muted-foreground flex items-center gap-2 text-sm bg-muted/50 px-3 py-1 rounded-full border border-border/50">
1061
+ <ClockIcon className="w-4 h-4" />
1062
+ Last ping latency
1063
+ </div>
1064
+ </div>
1065
+ )}
1066
+ </div>
1067
+ </div>
1068
+
1069
+ {/* Statistics Grid */}
1070
+ {pingHistory.length > 0 && (
1071
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
1072
+ {/* Average */}
1073
+ <div className="card card-hover p-5">
1074
+ <div className="flex items-center gap-3 mb-3">
1075
+ <div className="w-8 h-8 rounded-lg bg-blue-500/10 flex items-center justify-center">
1076
+ <ArrowTrendingUpIcon className="w-4 h-4 text-blue-500" />
1077
+ </div>
1078
+ <h3 className="font-medium text-foreground text-sm">Average</h3>
1079
+ </div>
1080
+ <div className="text-2xl font-bold text-foreground">{averageLatency}ms</div>
1081
+ <p className="text-xs text-muted-foreground mt-1">{pingHistory.length} total pings</p>
1082
+ </div>
1083
+
1084
+ {/* Min */}
1085
+ <div className="card card-hover p-5">
1086
+ <div className="flex items-center gap-3 mb-3">
1087
+ <div className="w-8 h-8 rounded-lg bg-emerald-500/10 flex items-center justify-center">
1088
+ <HeartIcon className="w-4 h-4 text-emerald-500" />
1089
+ </div>
1090
+ <h3 className="font-medium text-foreground text-sm">Best</h3>
1091
+ </div>
1092
+ <div className="text-2xl font-bold text-emerald-500">{minLatency}ms</div>
1093
+ <p className="text-xs text-muted-foreground mt-1">Fastest response</p>
1094
+ </div>
1095
+
1096
+ {/* Max */}
1097
+ <div className="card card-hover p-5">
1098
+ <div className="flex items-center gap-3 mb-3">
1099
+ <div className="w-8 h-8 rounded bg-orange-500/10 flex items-center justify-center">
1100
+ <ClockIcon className="w-4 h-4 text-orange-500" />
1101
+ </div>
1102
+ <h3 className="font-medium text-foreground text-sm">Worst</h3>
1103
+ </div>
1104
+ <div className="text-2xl font-bold text-orange-500">{maxLatency}ms</div>
1105
+ <p className="text-xs text-muted-foreground mt-1">Slowest response</p>
1106
+ </div>
1107
+ </div>
1108
+ )}
1109
+
1110
+ {/* Ping History */}
1111
+ {pingHistory.length > 0 && (
1112
+ <div>
1113
+ <h2 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
1114
+ <HeartIcon className="w-5 h-5 text-primary" />
1115
+ Recent Activity
1116
+ </h2>
1117
+ <div className="space-y-2 max-h-96 overflow-y-auto pr-1 custom-scrollbar">
1118
+ {pingHistory.slice().reverse().map((ping, idx) => (
1119
+ <div
1120
+ key={idx}
1121
+ className="card card-hover p-4 flex items-center justify-between animate-fade-in group"
1122
+ >
1123
+ <div className="flex items-center gap-3">
1124
+ <div className={`w-2.5 h-2.5 rounded-full ${getLatencyDotColor(ping.latency)} shadow-lg`} />
1125
+ <span className="text-sm text-foreground font-mono">
1126
+ {new Date(ping.time).toLocaleTimeString()}
1127
+ </span>
1128
+ </div>
1129
+ <div className="flex items-center gap-3">
1130
+ <span className={`text-sm font-bold ${getLatencyColor(ping.latency)}`}>
1131
+ {ping.latency}ms
1132
+ </span>
1133
+ <span className={`text-[10px] px-2 py-0.5 rounded-full font-medium border ${ping.latency < 100
1134
+ ? 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20'
1135
+ : ping.latency < 500
1136
+ ? 'bg-yellow-500/10 text-yellow-600 border-yellow-500/20'
1137
+ : 'bg-rose-500/10 text-rose-600 border-rose-500/20'
1138
+ }`}>
1139
+ {ping.latency < 100 ? 'Excellent' : ping.latency < 500 ? 'Good' : 'Slow'}
1140
+ </span>
1141
+ </div>
1142
+ </div>
1143
+ ))}
1144
+ </div>
1145
+ </div>
1146
+ )}
1147
+ </div>
190
1148
  )}
191
- </button>
192
- </div>
193
- </div>
1149
+ </div>
194
1150
  </div>
195
1151
  </div>
196
1152
  </div>
197
1153
  );
198
1154
  }
199
-