@chaaskit/client 0.1.0

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 (135) hide show
  1. package/dist/favicon.svg +11 -0
  2. package/dist/index.html +17 -0
  3. package/dist/lib/LoadingSkeletons-IcIC2JPq.js +132 -0
  4. package/dist/lib/LoadingSkeletons-IcIC2JPq.js.map +1 -0
  5. package/dist/lib/ServerThemeProvider-DNF0LAyk.js +42 -0
  6. package/dist/lib/ServerThemeProvider-DNF0LAyk.js.map +1 -0
  7. package/dist/lib/extensions.js +10 -0
  8. package/dist/lib/extensions.js.map +1 -0
  9. package/dist/lib/favicon.svg +11 -0
  10. package/dist/lib/index.js +74126 -0
  11. package/dist/lib/index.js.map +1 -0
  12. package/dist/lib/logo.svg +12 -0
  13. package/dist/lib/routes/AcceptInviteRoute.js +19 -0
  14. package/dist/lib/routes/AcceptInviteRoute.js.map +1 -0
  15. package/dist/lib/routes/AdminDashboardRoute.js +19 -0
  16. package/dist/lib/routes/AdminDashboardRoute.js.map +1 -0
  17. package/dist/lib/routes/AdminTeamRoute.js +19 -0
  18. package/dist/lib/routes/AdminTeamRoute.js.map +1 -0
  19. package/dist/lib/routes/AdminTeamsRoute.js +19 -0
  20. package/dist/lib/routes/AdminTeamsRoute.js.map +1 -0
  21. package/dist/lib/routes/AdminUsersRoute.js +19 -0
  22. package/dist/lib/routes/AdminUsersRoute.js.map +1 -0
  23. package/dist/lib/routes/ApiKeysRoute.js +19 -0
  24. package/dist/lib/routes/ApiKeysRoute.js.map +1 -0
  25. package/dist/lib/routes/AutomationsRoute.js +19 -0
  26. package/dist/lib/routes/AutomationsRoute.js.map +1 -0
  27. package/dist/lib/routes/ChatRoute.js +19 -0
  28. package/dist/lib/routes/ChatRoute.js.map +1 -0
  29. package/dist/lib/routes/DocumentsRoute.js +19 -0
  30. package/dist/lib/routes/DocumentsRoute.js.map +1 -0
  31. package/dist/lib/routes/OAuthConsentRoute.js +19 -0
  32. package/dist/lib/routes/OAuthConsentRoute.js.map +1 -0
  33. package/dist/lib/routes/PricingRoute.js +19 -0
  34. package/dist/lib/routes/PricingRoute.js.map +1 -0
  35. package/dist/lib/routes/PrivacyRoute.js +19 -0
  36. package/dist/lib/routes/PrivacyRoute.js.map +1 -0
  37. package/dist/lib/routes/TeamSettingsRoute.js +19 -0
  38. package/dist/lib/routes/TeamSettingsRoute.js.map +1 -0
  39. package/dist/lib/routes/TermsRoute.js +19 -0
  40. package/dist/lib/routes/TermsRoute.js.map +1 -0
  41. package/dist/lib/routes/VerifyEmailRoute.js +19 -0
  42. package/dist/lib/routes/VerifyEmailRoute.js.map +1 -0
  43. package/dist/lib/routes.js +79 -0
  44. package/dist/lib/routes.js.map +1 -0
  45. package/dist/lib/ssr-utils.js +29 -0
  46. package/dist/lib/ssr-utils.js.map +1 -0
  47. package/dist/lib/ssr.js +60 -0
  48. package/dist/lib/ssr.js.map +1 -0
  49. package/dist/lib/styles.css +2410 -0
  50. package/dist/lib/useExtensions-B5nX_8XD.js +155 -0
  51. package/dist/lib/useExtensions-B5nX_8XD.js.map +1 -0
  52. package/dist/logo.svg +12 -0
  53. package/package.json +84 -0
  54. package/src/components/AgentSelector.tsx +90 -0
  55. package/src/components/BranchModal.tsx +129 -0
  56. package/src/components/ClientOnly.tsx +27 -0
  57. package/src/components/ExportMenu.tsx +122 -0
  58. package/src/components/LoadingSkeletons.tsx +110 -0
  59. package/src/components/MCPCredentialsSection.tsx +309 -0
  60. package/src/components/MentionChip.tsx +149 -0
  61. package/src/components/MentionDropdown.tsx +175 -0
  62. package/src/components/MentionInput.tsx +293 -0
  63. package/src/components/MessageItem.tsx +300 -0
  64. package/src/components/MessageList.tsx +159 -0
  65. package/src/components/OAuthAppsSection.tsx +124 -0
  66. package/src/components/ProjectFolder.tsx +141 -0
  67. package/src/components/ProjectModal.tsx +296 -0
  68. package/src/components/SSRMessageList.tsx +153 -0
  69. package/src/components/SearchModal.tsx +173 -0
  70. package/src/components/SettingsModal.tsx +412 -0
  71. package/src/components/ShareModal.tsx +280 -0
  72. package/src/components/Sidebar.tsx +491 -0
  73. package/src/components/TeamSwitcher.tsx +273 -0
  74. package/src/components/ToolCallDisplay.tsx +473 -0
  75. package/src/components/ToolConfirmationModal.tsx +130 -0
  76. package/src/components/UsageChart.tsx +177 -0
  77. package/src/components/content/CodeBlock.tsx +69 -0
  78. package/src/components/content/MarkdownRenderer.tsx +64 -0
  79. package/src/components/content/SSRMarkdownRenderer.tsx +158 -0
  80. package/src/contexts/AuthContext.tsx +119 -0
  81. package/src/contexts/ConfigContext.tsx +214 -0
  82. package/src/contexts/ProjectContext.tsx +167 -0
  83. package/src/contexts/ServerConfigProvider.tsx +41 -0
  84. package/src/contexts/ServerThemeProvider.tsx +47 -0
  85. package/src/contexts/TeamContext.tsx +255 -0
  86. package/src/contexts/ThemeContext.tsx +113 -0
  87. package/src/extensions/index.ts +15 -0
  88. package/src/extensions/registry.ts +187 -0
  89. package/src/extensions/useExtensions.ts +52 -0
  90. package/src/hooks/useAppPath.ts +34 -0
  91. package/src/hooks/useBasePath.ts +13 -0
  92. package/src/hooks/useKeyboardShortcuts.ts +50 -0
  93. package/src/hooks/useMentionSearch.ts +106 -0
  94. package/src/index.tsx +116 -0
  95. package/src/layouts/MainLayout.tsx +98 -0
  96. package/src/pages/AcceptInvitePage.tsx +175 -0
  97. package/src/pages/AdminDashboardPage.tsx +362 -0
  98. package/src/pages/AdminTeamPage.tsx +304 -0
  99. package/src/pages/AdminTeamsPage.tsx +242 -0
  100. package/src/pages/AdminUsersPage.tsx +385 -0
  101. package/src/pages/ApiKeysPage.tsx +449 -0
  102. package/src/pages/ChatPage.tsx +310 -0
  103. package/src/pages/DocumentsPage.tsx +577 -0
  104. package/src/pages/LoginPage.tsx +232 -0
  105. package/src/pages/OAuthConsentPage.tsx +234 -0
  106. package/src/pages/PricingPage.tsx +314 -0
  107. package/src/pages/PrivacyPage.tsx +65 -0
  108. package/src/pages/RegisterPage.tsx +153 -0
  109. package/src/pages/ScheduledPromptsPage.tsx +702 -0
  110. package/src/pages/SharedThreadPage.tsx +116 -0
  111. package/src/pages/TeamSettingsPage.tsx +1085 -0
  112. package/src/pages/TermsPage.tsx +82 -0
  113. package/src/pages/VerifyEmailPage.tsx +202 -0
  114. package/src/routes/AcceptInviteRoute.tsx +24 -0
  115. package/src/routes/AdminDashboardRoute.tsx +24 -0
  116. package/src/routes/AdminTeamRoute.tsx +24 -0
  117. package/src/routes/AdminTeamsRoute.tsx +24 -0
  118. package/src/routes/AdminUsersRoute.tsx +24 -0
  119. package/src/routes/ApiKeysRoute.tsx +24 -0
  120. package/src/routes/AutomationsRoute.tsx +24 -0
  121. package/src/routes/ChatRoute.tsx +28 -0
  122. package/src/routes/DocumentsRoute.tsx +24 -0
  123. package/src/routes/OAuthConsentRoute.tsx +24 -0
  124. package/src/routes/PricingRoute.tsx +24 -0
  125. package/src/routes/PrivacyRoute.tsx +24 -0
  126. package/src/routes/TeamSettingsRoute.tsx +24 -0
  127. package/src/routes/TermsRoute.tsx +24 -0
  128. package/src/routes/VerifyEmailRoute.tsx +24 -0
  129. package/src/routes/index.ts +57 -0
  130. package/src/ssr-utils.tsx +84 -0
  131. package/src/ssr.ts +123 -0
  132. package/src/stores/chatStore.ts +670 -0
  133. package/src/styles/index.css +254 -0
  134. package/src/utils/api.ts +78 -0
  135. package/src/vite-env.d.ts +13 -0
@@ -0,0 +1,232 @@
1
+ import { useState } from 'react';
2
+ import { Link, useNavigate } from 'react-router';
3
+ import { useAuth } from '../contexts/AuthContext';
4
+ import { useConfig } from '../contexts/ConfigContext';
5
+ import { useTheme } from '../contexts/ThemeContext';
6
+ import { useAppPath } from '../hooks/useAppPath';
7
+
8
+ export default function LoginPage() {
9
+ const navigate = useNavigate();
10
+ const appPath = useAppPath();
11
+ const { login, sendMagicLink } = useAuth();
12
+ const config = useConfig();
13
+ const { theme } = useTheme();
14
+
15
+ const [email, setEmail] = useState('');
16
+ const [password, setPassword] = useState('');
17
+ const [error, setError] = useState('');
18
+ const [isLoading, setIsLoading] = useState(false);
19
+ const [magicLinkSent, setMagicLinkSent] = useState(false);
20
+
21
+ const hasEmailPassword = config.auth.methods.includes('email-password');
22
+ const hasMagicLink = config.auth.magicLink.enabled;
23
+ const hasGoogle = config.auth.methods.includes('google');
24
+ const hasGitHub = config.auth.methods.includes('github');
25
+
26
+ async function handleSubmit(e: React.FormEvent) {
27
+ e.preventDefault();
28
+ setError('');
29
+ setIsLoading(true);
30
+
31
+ try {
32
+ const { requiresVerification } = await login(email, password);
33
+ if (requiresVerification) {
34
+ navigate('/verify-email');
35
+ } else {
36
+ navigate(appPath('/'));
37
+ }
38
+ } catch (err) {
39
+ setError(err instanceof Error ? err.message : 'Login failed');
40
+ } finally {
41
+ setIsLoading(false);
42
+ }
43
+ }
44
+
45
+ async function handleMagicLink() {
46
+ if (!email) {
47
+ setError('Please enter your email');
48
+ return;
49
+ }
50
+
51
+ setError('');
52
+ setIsLoading(true);
53
+
54
+ try {
55
+ await sendMagicLink(email);
56
+ setMagicLinkSent(true);
57
+ } catch (err) {
58
+ setError(err instanceof Error ? err.message : 'Failed to send magic link');
59
+ } finally {
60
+ setIsLoading(false);
61
+ }
62
+ }
63
+
64
+ if (magicLinkSent) {
65
+ return (
66
+ <div className="flex min-h-screen items-center justify-center bg-background p-4">
67
+ <div className="w-full max-w-md text-center">
68
+ <h1 className="mb-4 text-2xl font-bold text-text-primary">
69
+ Check your email
70
+ </h1>
71
+ <p className="mb-6 text-text-secondary">
72
+ We've sent a magic link to <strong>{email}</strong>. Click the link
73
+ in the email to sign in.
74
+ </p>
75
+ <button
76
+ onClick={() => setMagicLinkSent(false)}
77
+ className="text-primary hover:underline"
78
+ >
79
+ Use a different email
80
+ </button>
81
+ </div>
82
+ </div>
83
+ );
84
+ }
85
+
86
+ return (
87
+ <div className="flex min-h-screen items-center justify-center bg-background p-4">
88
+ <div className="w-full max-w-md">
89
+ <div className="mb-8 text-center">
90
+ {config.ui.logo && (
91
+ <img
92
+ src={typeof config.ui.logo === 'string' ? config.ui.logo : (theme === 'dark' ? config.ui.logo.dark : config.ui.logo.light)}
93
+ alt={config.app.name}
94
+ className="mx-auto mb-4 h-16 w-16 rounded-lg object-contain"
95
+ />
96
+ )}
97
+ <h1 className="text-3xl font-bold text-text-primary">Welcome back</h1>
98
+ <p className="mt-2 text-text-secondary">Sign in to {config.app.name}</p>
99
+ </div>
100
+
101
+ {error && (
102
+ <div className="mb-4 rounded-lg bg-error/10 p-3 text-sm text-error">
103
+ {error}
104
+ </div>
105
+ )}
106
+
107
+ {/* OAuth buttons */}
108
+ {(hasGoogle || hasGitHub) && (
109
+ <div className="mb-6 space-y-3">
110
+ {hasGoogle && (
111
+ <a
112
+ href="/api/auth/oauth/google"
113
+ className="flex w-full items-center justify-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-text-primary hover:bg-background-secondary"
114
+ >
115
+ <svg className="h-5 w-5" viewBox="0 0 24 24">
116
+ <path
117
+ fill="currentColor"
118
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
119
+ />
120
+ <path
121
+ fill="currentColor"
122
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
123
+ />
124
+ <path
125
+ fill="currentColor"
126
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
127
+ />
128
+ <path
129
+ fill="currentColor"
130
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
131
+ />
132
+ </svg>
133
+ Continue with Google
134
+ </a>
135
+ )}
136
+
137
+ {hasGitHub && (
138
+ <a
139
+ href="/api/auth/oauth/github"
140
+ className="flex w-full items-center justify-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-text-primary hover:bg-background-secondary"
141
+ >
142
+ <svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
143
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
144
+ </svg>
145
+ Continue with GitHub
146
+ </a>
147
+ )}
148
+ </div>
149
+ )}
150
+
151
+ {(hasGoogle || hasGitHub) && hasEmailPassword && (
152
+ <div className="relative mb-6">
153
+ <div className="absolute inset-0 flex items-center">
154
+ <div className="w-full border-t border-border" />
155
+ </div>
156
+ <div className="relative flex justify-center text-sm">
157
+ <span className="bg-background px-2 text-text-muted">
158
+ Or continue with email
159
+ </span>
160
+ </div>
161
+ </div>
162
+ )}
163
+
164
+ {/* Email/Password form */}
165
+ {hasEmailPassword && (
166
+ <form onSubmit={handleSubmit} className="space-y-4">
167
+ <div>
168
+ <label
169
+ htmlFor="email"
170
+ className="block text-sm font-medium text-text-primary"
171
+ >
172
+ Email
173
+ </label>
174
+ <input
175
+ type="email"
176
+ id="email"
177
+ value={email}
178
+ onChange={(e) => setEmail(e.target.value)}
179
+ required
180
+ className="mt-1 w-full rounded-lg border border-input-border bg-input-background px-4 py-2 text-text-primary focus:border-primary focus:outline-none"
181
+ />
182
+ </div>
183
+
184
+ <div>
185
+ <label
186
+ htmlFor="password"
187
+ className="block text-sm font-medium text-text-primary"
188
+ >
189
+ Password
190
+ </label>
191
+ <input
192
+ type="password"
193
+ id="password"
194
+ value={password}
195
+ onChange={(e) => setPassword(e.target.value)}
196
+ required
197
+ className="mt-1 w-full rounded-lg border border-input-border bg-input-background px-4 py-2 text-text-primary focus:border-primary focus:outline-none"
198
+ />
199
+ </div>
200
+
201
+ <button
202
+ type="submit"
203
+ disabled={isLoading}
204
+ className="w-full rounded-lg bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover disabled:opacity-50"
205
+ >
206
+ {isLoading ? 'Signing in...' : 'Sign in'}
207
+ </button>
208
+ </form>
209
+ )}
210
+
211
+ {/* Magic Link */}
212
+ {hasMagicLink && hasEmailPassword && (
213
+ <button
214
+ onClick={handleMagicLink}
215
+ disabled={isLoading}
216
+ className="mt-4 w-full text-center text-sm text-primary hover:underline disabled:opacity-50"
217
+ >
218
+ Sign in with magic link instead
219
+ </button>
220
+ )}
221
+
222
+ {/* Register link */}
223
+ <p className="mt-6 text-center text-sm text-text-secondary">
224
+ Don't have an account?{' '}
225
+ <Link to="/register" className="text-primary hover:underline">
226
+ Sign up
227
+ </Link>
228
+ </p>
229
+ </div>
230
+ </div>
231
+ );
232
+ }
@@ -0,0 +1,234 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useSearchParams, useNavigate, Navigate } from 'react-router';
3
+ import { Shield, Check, X, Loader2, ExternalLink } from 'lucide-react';
4
+ import { useAuth } from '../contexts/AuthContext';
5
+ import { useConfig } from '../contexts/ConfigContext';
6
+ import { useTheme } from '../contexts/ThemeContext';
7
+ import { useAppPath } from '../hooks/useAppPath';
8
+
9
+ interface ClientInfo {
10
+ clientId: string;
11
+ clientName: string;
12
+ clientUri?: string;
13
+ }
14
+
15
+ export default function OAuthConsentPage() {
16
+ const [searchParams] = useSearchParams();
17
+ const navigate = useNavigate();
18
+ const appPath = useAppPath();
19
+ const { user, isLoading: authLoading } = useAuth();
20
+ const config = useConfig();
21
+ const { theme } = useTheme();
22
+
23
+ const [clientInfo, setClientInfo] = useState<ClientInfo | null>(null);
24
+ const [isLoading, setIsLoading] = useState(true);
25
+ const [isSubmitting, setIsSubmitting] = useState(false);
26
+ const [error, setError] = useState<string | null>(null);
27
+
28
+ // Get params from URL
29
+ const clientId = searchParams.get('client_id');
30
+ const redirectUri = searchParams.get('redirect_uri');
31
+ const scope = searchParams.get('scope');
32
+ const state = searchParams.get('state');
33
+ const codeChallenge = searchParams.get('code_challenge');
34
+ const codeChallengeMethod = searchParams.get('code_challenge_method');
35
+
36
+ // Get logo
37
+ const logo = typeof config.ui.logo === 'string'
38
+ ? config.ui.logo
39
+ : (theme === 'dark' ? config.ui.logo.dark : config.ui.logo.light);
40
+
41
+ // Load client info
42
+ useEffect(() => {
43
+ async function loadClientInfo() {
44
+ if (!clientId) {
45
+ setError('Missing client_id parameter');
46
+ setIsLoading(false);
47
+ return;
48
+ }
49
+
50
+ try {
51
+ // For now, we'll just display the client_id
52
+ // In a full implementation, we'd fetch client details from the server
53
+ setClientInfo({
54
+ clientId,
55
+ clientName: clientId.startsWith('mcp_') ? 'MCP Client' : clientId,
56
+ });
57
+ setIsLoading(false);
58
+ } catch (err) {
59
+ setError('Failed to load client information');
60
+ setIsLoading(false);
61
+ }
62
+ }
63
+
64
+ loadClientInfo();
65
+ }, [clientId]);
66
+
67
+ // Handle consent decision
68
+ async function handleConsent(approved: boolean) {
69
+ setIsSubmitting(true);
70
+ setError(null);
71
+
72
+ try {
73
+ // OAuth endpoints are at the API root level, not under /api
74
+ // In dev mode with separate servers, detect localhost:5173 and route to localhost:3000
75
+ // In production, same-origin requests work since API and client are served together
76
+ let apiUrl = import.meta.env.VITE_API_URL || '';
77
+ if (!apiUrl && window.location.hostname === 'localhost' && window.location.port === '5173') {
78
+ apiUrl = 'http://localhost:3000';
79
+ }
80
+ const response = await fetch(`${apiUrl}/oauth/authorize`, {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ credentials: 'include',
84
+ body: JSON.stringify({
85
+ client_id: clientId,
86
+ redirect_uri: redirectUri,
87
+ scope,
88
+ state,
89
+ code_challenge: codeChallenge,
90
+ code_challenge_method: codeChallengeMethod || 'S256',
91
+ consent: approved ? 'approve' : 'deny',
92
+ }),
93
+ });
94
+
95
+ if (!response.ok) {
96
+ const error = await response.json().catch(() => ({ message: 'Request failed' }));
97
+ throw new Error(error.error?.message || error.message || 'Authorization failed');
98
+ }
99
+
100
+ const data = await response.json() as { redirect: string };
101
+ // Redirect to the client's redirect URI
102
+ window.location.href = data.redirect;
103
+ } catch (err) {
104
+ setError(err instanceof Error ? err.message : 'Authorization failed');
105
+ setIsSubmitting(false);
106
+ }
107
+ }
108
+
109
+ // Redirect to login if not authenticated
110
+ if (!authLoading && !user) {
111
+ const returnUrl = `/oauth/consent?${searchParams.toString()}`;
112
+ return <Navigate to={`/login?returnTo=${encodeURIComponent(returnUrl)}`} replace />;
113
+ }
114
+
115
+ // Parse scopes for display
116
+ const scopeList = scope ? scope.split(' ') : [];
117
+
118
+ return (
119
+ <div className="flex min-h-screen flex-col items-center justify-center bg-background p-4">
120
+ <div className="w-full max-w-md">
121
+ {/* Logo */}
122
+ <div className="mb-8 text-center">
123
+ <img
124
+ src={logo}
125
+ alt={config.app.name}
126
+ className="mx-auto h-12 w-auto"
127
+ />
128
+ </div>
129
+
130
+ {/* Consent Card */}
131
+ <div className="rounded-2xl border border-border bg-background-secondary p-6 shadow-lg">
132
+ {isLoading || authLoading ? (
133
+ <div className="flex items-center justify-center py-8">
134
+ <Loader2 className="h-8 w-8 animate-spin text-primary" />
135
+ </div>
136
+ ) : error ? (
137
+ <div className="text-center">
138
+ <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-error/10">
139
+ <X className="h-6 w-6 text-error" />
140
+ </div>
141
+ <h2 className="text-lg font-semibold text-text-primary">Authorization Error</h2>
142
+ <p className="mt-2 text-sm text-text-secondary">{error}</p>
143
+ <button
144
+ onClick={() => navigate(appPath('/'))}
145
+ className="mt-4 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover"
146
+ >
147
+ Return Home
148
+ </button>
149
+ </div>
150
+ ) : (
151
+ <>
152
+ {/* Header */}
153
+ <div className="mb-6 text-center">
154
+ <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
155
+ <Shield className="h-6 w-6 text-primary" />
156
+ </div>
157
+ <h2 className="text-lg font-semibold text-text-primary">
158
+ Authorize Application
159
+ </h2>
160
+ <p className="mt-1 text-sm text-text-secondary">
161
+ <span className="font-medium text-text-primary">{clientInfo?.clientName}</span>
162
+ {' '}wants to access your account
163
+ </p>
164
+ </div>
165
+
166
+ {/* Client Info */}
167
+ {clientInfo?.clientUri && (
168
+ <a
169
+ href={clientInfo.clientUri}
170
+ target="_blank"
171
+ rel="noopener noreferrer"
172
+ className="mb-4 flex items-center justify-center gap-1 text-sm text-primary hover:underline"
173
+ >
174
+ Visit application website
175
+ <ExternalLink size={14} />
176
+ </a>
177
+ )}
178
+
179
+ {/* Scopes */}
180
+ {scopeList.length > 0 && (
181
+ <div className="mb-6 rounded-lg border border-border bg-background p-4">
182
+ <p className="mb-3 text-sm font-medium text-text-primary">
183
+ This application will be able to:
184
+ </p>
185
+ <ul className="space-y-2">
186
+ {scopeList.map((s) => (
187
+ <li key={s} className="flex items-center gap-2 text-sm text-text-secondary">
188
+ <Check size={16} className="text-success" />
189
+ {s === 'mcp:tools' && 'Use available tools on your behalf'}
190
+ {s === 'mcp:resources' && 'Read resources on your behalf'}
191
+ {!['mcp:tools', 'mcp:resources'].includes(s) && s}
192
+ </li>
193
+ ))}
194
+ </ul>
195
+ </div>
196
+ )}
197
+
198
+ {/* User Info */}
199
+ <div className="mb-6 rounded-lg border border-border bg-background p-3">
200
+ <p className="text-xs text-text-muted">Authorizing as</p>
201
+ <p className="text-sm font-medium text-text-primary">{user?.email}</p>
202
+ </div>
203
+
204
+ {/* Actions */}
205
+ <div className="flex gap-3">
206
+ <button
207
+ onClick={() => handleConsent(false)}
208
+ disabled={isSubmitting}
209
+ className="flex-1 rounded-lg border border-border bg-background px-4 py-2.5 text-sm font-medium text-text-secondary hover:bg-background-secondary hover:text-text-primary disabled:opacity-50"
210
+ >
211
+ Deny
212
+ </button>
213
+ <button
214
+ onClick={() => handleConsent(true)}
215
+ disabled={isSubmitting}
216
+ className="flex flex-1 items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2.5 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-50"
217
+ >
218
+ {isSubmitting && <Loader2 size={16} className="animate-spin" />}
219
+ Authorize
220
+ </button>
221
+ </div>
222
+ </>
223
+ )}
224
+ </div>
225
+
226
+ {/* Footer */}
227
+ <p className="mt-6 text-center text-xs text-text-muted">
228
+ By authorizing, you allow this application to access your account
229
+ according to its terms of service and privacy policy.
230
+ </p>
231
+ </div>
232
+ </div>
233
+ );
234
+ }