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