@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,173 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { useNavigate } from 'react-router';
4
+ import { Search, X, Loader2, MessageSquare } from 'lucide-react';
5
+ import { formatShortcut } from '../hooks/useKeyboardShortcuts';
6
+ import { useAppPath } from '../hooks/useAppPath';
7
+
8
+ interface SearchResult {
9
+ id: string;
10
+ threadId: string;
11
+ threadTitle: string;
12
+ role: string;
13
+ content: string;
14
+ highlight: string;
15
+ createdAt: string;
16
+ }
17
+
18
+ interface SearchModalProps {
19
+ isOpen: boolean;
20
+ onClose: () => void;
21
+ }
22
+
23
+ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
24
+ const navigate = useNavigate();
25
+ const appPath = useAppPath();
26
+ const inputRef = useRef<HTMLInputElement>(null);
27
+ const [query, setQuery] = useState('');
28
+ const [results, setResults] = useState<SearchResult[]>([]);
29
+ const [isLoading, setIsLoading] = useState(false);
30
+ const [hasSearched, setHasSearched] = useState(false);
31
+
32
+ // Focus input when modal opens
33
+ useEffect(() => {
34
+ if (isOpen) {
35
+ setQuery('');
36
+ setResults([]);
37
+ setHasSearched(false);
38
+ setTimeout(() => inputRef.current?.focus(), 50);
39
+ }
40
+ }, [isOpen]);
41
+
42
+ // Debounced search
43
+ useEffect(() => {
44
+ if (!query.trim()) {
45
+ setResults([]);
46
+ setHasSearched(false);
47
+ return;
48
+ }
49
+
50
+ const timer = setTimeout(async () => {
51
+ setIsLoading(true);
52
+ setHasSearched(true);
53
+ try {
54
+ const response = await fetch(
55
+ `/api/search?q=${encodeURIComponent(query.trim())}&limit=20`,
56
+ { credentials: 'include' }
57
+ );
58
+ if (response.ok) {
59
+ const data = await response.json();
60
+ setResults(data.results);
61
+ }
62
+ } catch (error) {
63
+ console.error('Search failed:', error);
64
+ } finally {
65
+ setIsLoading(false);
66
+ }
67
+ }, 300);
68
+
69
+ return () => clearTimeout(timer);
70
+ }, [query]);
71
+
72
+ function handleResultClick(result: SearchResult) {
73
+ navigate(appPath(`/thread/${result.threadId}`));
74
+ onClose();
75
+ }
76
+
77
+ function handleKeyDown(e: React.KeyboardEvent) {
78
+ if (e.key === 'Escape') {
79
+ onClose();
80
+ }
81
+ }
82
+
83
+ if (!isOpen) return null;
84
+
85
+ return createPortal(
86
+ <div className="fixed inset-0 z-50 flex items-start justify-center pt-[15vh]">
87
+ {/* Backdrop */}
88
+ <div className="absolute inset-0 bg-black/50" onClick={onClose} />
89
+
90
+ {/* Modal */}
91
+ <div
92
+ className="relative w-full max-w-2xl rounded-xl bg-background shadow-2xl"
93
+ style={{ maxHeight: '70vh' }}
94
+ >
95
+ {/* Search Input */}
96
+ <div className="flex items-center gap-3 border-b border-border px-4 py-3">
97
+ <Search size={20} className="flex-shrink-0 text-text-muted" />
98
+ <input
99
+ ref={inputRef}
100
+ type="text"
101
+ value={query}
102
+ onChange={(e) => setQuery(e.target.value)}
103
+ onKeyDown={handleKeyDown}
104
+ placeholder="Search conversations..."
105
+ className="flex-1 bg-transparent text-base text-text-primary placeholder-text-muted focus:outline-none"
106
+ />
107
+ {isLoading && <Loader2 size={20} className="animate-spin text-text-muted" />}
108
+ <button
109
+ onClick={onClose}
110
+ className="flex-shrink-0 rounded p-1 text-text-muted hover:bg-background-secondary hover:text-text-primary"
111
+ >
112
+ <X size={20} />
113
+ </button>
114
+ </div>
115
+
116
+ {/* Results */}
117
+ <div className="overflow-y-auto" style={{ maxHeight: 'calc(70vh - 60px)' }}>
118
+ {!hasSearched && !query && (
119
+ <div className="px-4 py-8 text-center text-text-muted">
120
+ <p className="mb-2">Search your conversations</p>
121
+ <p className="text-sm">
122
+ Press <kbd className="rounded bg-background-secondary px-1.5 py-0.5 text-xs">{formatShortcut('K')}</kbd> anytime to open search
123
+ </p>
124
+ </div>
125
+ )}
126
+
127
+ {hasSearched && results.length === 0 && !isLoading && (
128
+ <div className="px-4 py-8 text-center text-text-muted">
129
+ No results found for "{query}"
130
+ </div>
131
+ )}
132
+
133
+ {results.length > 0 && (
134
+ <div className="py-2">
135
+ {results.map((result) => (
136
+ <button
137
+ key={result.id}
138
+ onClick={() => handleResultClick(result)}
139
+ className="w-full px-4 py-3 text-left hover:bg-background-secondary"
140
+ >
141
+ <div className="mb-1 flex items-center gap-2">
142
+ <MessageSquare size={14} className="text-text-muted" />
143
+ <span className="text-sm font-medium text-text-primary">
144
+ {result.threadTitle}
145
+ </span>
146
+ <span className="text-xs text-text-muted">
147
+ {result.role === 'user' ? 'You' : 'Assistant'}
148
+ </span>
149
+ </div>
150
+ <p
151
+ className="line-clamp-2 text-sm text-text-secondary"
152
+ dangerouslySetInnerHTML={{ __html: result.highlight }}
153
+ />
154
+ </button>
155
+ ))}
156
+ </div>
157
+ )}
158
+ </div>
159
+
160
+ {/* Footer hint */}
161
+ <div className="border-t border-border px-4 py-2 text-xs text-text-muted">
162
+ <span className="mr-4">
163
+ <kbd className="rounded bg-background-secondary px-1.5 py-0.5">↵</kbd> to select
164
+ </span>
165
+ <span>
166
+ <kbd className="rounded bg-background-secondary px-1.5 py-0.5">esc</kbd> to close
167
+ </span>
168
+ </div>
169
+ </div>
170
+ </div>,
171
+ document.body
172
+ );
173
+ }
@@ -0,0 +1,412 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { useNavigate, Link } from 'react-router';
4
+ import { X, Loader2, Check, BarChart3, CreditCard, ExternalLink, Key } from 'lucide-react';
5
+ import { useConfig } from '../contexts/ConfigContext';
6
+ import { useTheme } from '../contexts/ThemeContext';
7
+ import { useAuth } from '../contexts/AuthContext';
8
+ import { useAppPath } from '../hooks/useAppPath';
9
+ import MCPCredentialsSection from './MCPCredentialsSection';
10
+ import OAuthAppsSection from './OAuthAppsSection';
11
+ import { api } from '../utils/api';
12
+
13
+ interface UsageData {
14
+ messagesThisMonth: number;
15
+ monthlyLimit: number;
16
+ credits: number;
17
+ plan: string;
18
+ }
19
+
20
+ interface SettingsModalProps {
21
+ isOpen: boolean;
22
+ onClose: () => void;
23
+ }
24
+
25
+ export default function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
26
+ const config = useConfig();
27
+ const { theme, setTheme, availableThemes } = useTheme();
28
+ const { user } = useAuth();
29
+ const navigate = useNavigate();
30
+ const appPath = useAppPath();
31
+
32
+ const [settings, setSettings] = useState<Record<string, string>>({});
33
+ const [usageData, setUsageData] = useState<UsageData | null>(null);
34
+ const [isLoading, setIsLoading] = useState(false);
35
+ const [isSaving, setIsSaving] = useState(false);
36
+ const [saveStatus, setSaveStatus] = useState<'idle' | 'saved' | 'error'>('idle');
37
+ const [isBillingLoading, setIsBillingLoading] = useState(false);
38
+ const [canAccessApiKeys, setCanAccessApiKeys] = useState(false);
39
+
40
+ // Load settings and usage when modal opens
41
+ useEffect(() => {
42
+ if (isOpen && user) {
43
+ loadSettings();
44
+ loadUsage();
45
+ // Check API keys access
46
+ if (config.api?.enabled) {
47
+ api.get<{ canAccess: boolean }>('/api/api-keys/access')
48
+ .then((res) => setCanAccessApiKeys(res.canAccess))
49
+ .catch(() => setCanAccessApiKeys(false));
50
+ }
51
+ }
52
+ }, [isOpen, user, config.api?.enabled]);
53
+
54
+ // Reset save status after showing
55
+ useEffect(() => {
56
+ if (saveStatus !== 'idle') {
57
+ const timer = setTimeout(() => setSaveStatus('idle'), 2000);
58
+ return () => clearTimeout(timer);
59
+ }
60
+ }, [saveStatus]);
61
+
62
+ async function loadSettings() {
63
+ setIsLoading(true);
64
+ try {
65
+ const response = await fetch('/api/user/settings', {
66
+ credentials: 'include',
67
+ });
68
+ if (response.ok) {
69
+ const data = await response.json();
70
+ setSettings(data.settings || {});
71
+ }
72
+ } catch (error) {
73
+ console.error('Failed to load settings:', error);
74
+ } finally {
75
+ setIsLoading(false);
76
+ }
77
+ }
78
+
79
+ async function loadUsage() {
80
+ try {
81
+ const response = await fetch('/api/user/usage', {
82
+ credentials: 'include',
83
+ });
84
+ if (response.ok) {
85
+ const data = await response.json();
86
+ setUsageData(data);
87
+ }
88
+ } catch (error) {
89
+ console.error('Failed to load usage:', error);
90
+ }
91
+ }
92
+
93
+ async function handleSave() {
94
+ setIsSaving(true);
95
+ setSaveStatus('idle');
96
+ try {
97
+ const response = await fetch('/api/user/settings', {
98
+ method: 'PATCH',
99
+ headers: { 'Content-Type': 'application/json' },
100
+ credentials: 'include',
101
+ body: JSON.stringify({
102
+ ...settings,
103
+ themePreference: theme,
104
+ }),
105
+ });
106
+
107
+ if (response.ok) {
108
+ setSaveStatus('saved');
109
+ } else {
110
+ setSaveStatus('error');
111
+ }
112
+ } catch (error) {
113
+ console.error('Failed to save settings:', error);
114
+ setSaveStatus('error');
115
+ } finally {
116
+ setIsSaving(false);
117
+ }
118
+ }
119
+
120
+ function handleFieldChange(key: string, value: string) {
121
+ setSettings((prev) => ({ ...prev, [key]: value }));
122
+ }
123
+
124
+ function handleThemeChange(newTheme: string) {
125
+ setTheme(newTheme);
126
+ }
127
+
128
+ async function handleOpenBillingPortal() {
129
+ setIsBillingLoading(true);
130
+ try {
131
+ const response = await fetch('/api/payments/billing-portal', {
132
+ method: 'POST',
133
+ credentials: 'include',
134
+ });
135
+ if (response.ok) {
136
+ const { url } = await response.json();
137
+ window.location.href = url;
138
+ }
139
+ } catch (error) {
140
+ console.error('Failed to open billing portal:', error);
141
+ } finally {
142
+ setIsBillingLoading(false);
143
+ }
144
+ }
145
+
146
+ function handleUpgrade() {
147
+ onClose();
148
+ navigate(appPath('/pricing'));
149
+ }
150
+
151
+ if (!isOpen) return null;
152
+
153
+ return createPortal(
154
+ <div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center">
155
+ {/* Backdrop */}
156
+ <div
157
+ className="absolute inset-0 bg-black/50"
158
+ onClick={onClose}
159
+ />
160
+
161
+ {/* Modal */}
162
+ <div
163
+ className="relative w-full max-w-lg rounded-t-2xl bg-background sm:rounded-2xl"
164
+ style={{
165
+ maxHeight: 'calc(100vh - env(safe-area-inset-top) - 1rem)',
166
+ paddingBottom: 'env(safe-area-inset-bottom)',
167
+ }}
168
+ >
169
+ {/* Header */}
170
+ <div className="flex items-center justify-between border-b border-border px-4 py-3 sm:px-6 sm:py-4">
171
+ <h2 className="text-lg font-semibold text-text-primary">Settings</h2>
172
+ <button
173
+ onClick={onClose}
174
+ className="rounded-lg p-2 text-text-secondary hover:bg-background-secondary hover:text-text-primary active:bg-background-secondary"
175
+ aria-label="Close settings"
176
+ >
177
+ <X size={20} />
178
+ </button>
179
+ </div>
180
+
181
+ {/* Content */}
182
+ <div className="overflow-y-auto px-4 py-4 sm:px-6" style={{ maxHeight: 'calc(100vh - 200px)' }}>
183
+ {isLoading ? (
184
+ <div className="flex items-center justify-center py-8">
185
+ <Loader2 className="h-6 w-6 animate-spin text-primary" />
186
+ </div>
187
+ ) : (
188
+ <div className="space-y-6">
189
+ {/* Account Info */}
190
+ {user && (
191
+ <div className="rounded-lg border border-border bg-background-secondary p-4">
192
+ <h3 className="mb-2 font-medium text-text-primary">Account</h3>
193
+ <div className="text-sm text-text-secondary">
194
+ <span className="text-text-muted">Email: </span>
195
+ <span className="text-text-primary">{user.email}</span>
196
+ </div>
197
+ </div>
198
+ )}
199
+
200
+ {/* Usage Metrics */}
201
+ {usageData && (
202
+ <div className="rounded-lg border border-border bg-background-secondary p-4">
203
+ <div className="mb-3 flex items-center gap-2">
204
+ <BarChart3 size={18} className="text-primary" />
205
+ <h3 className="font-medium text-text-primary">Usage</h3>
206
+ <span className="ml-auto rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
207
+ {usageData.plan}
208
+ </span>
209
+ </div>
210
+
211
+ {/* Messages Progress */}
212
+ <div className="space-y-2">
213
+ <div className="flex justify-between text-sm">
214
+ <span className="text-text-secondary">Messages this month</span>
215
+ <span className="text-text-primary">
216
+ {usageData.messagesThisMonth.toLocaleString()}
217
+ {usageData.monthlyLimit > 0 ? (
218
+ <span className="text-text-muted"> / {usageData.monthlyLimit.toLocaleString()}</span>
219
+ ) : (
220
+ <span className="text-text-muted"> / Unlimited</span>
221
+ )}
222
+ </span>
223
+ </div>
224
+
225
+ {usageData.monthlyLimit > 0 && (
226
+ <div className="h-2 overflow-hidden rounded-full bg-background">
227
+ <div
228
+ className={`h-full rounded-full transition-all ${
229
+ usageData.messagesThisMonth / usageData.monthlyLimit > 0.9
230
+ ? 'bg-error'
231
+ : usageData.messagesThisMonth / usageData.monthlyLimit > 0.7
232
+ ? 'bg-warning'
233
+ : 'bg-primary'
234
+ }`}
235
+ style={{
236
+ width: `${Math.min(100, (usageData.messagesThisMonth / usageData.monthlyLimit) * 100)}%`,
237
+ }}
238
+ />
239
+ </div>
240
+ )}
241
+
242
+ {usageData.credits > 0 && (
243
+ <div className="mt-3 flex justify-between text-sm">
244
+ <span className="text-text-secondary">Credits remaining</span>
245
+ <span className="text-text-primary">{usageData.credits.toLocaleString()}</span>
246
+ </div>
247
+ )}
248
+
249
+ {/* Billing Actions */}
250
+ <div className="mt-4 flex flex-wrap gap-2">
251
+ <button
252
+ onClick={handleUpgrade}
253
+ className="flex items-center gap-1.5 rounded-lg bg-primary px-3 py-1.5 text-sm font-medium text-white hover:bg-primary-hover"
254
+ >
255
+ <CreditCard size={14} />
256
+ {usageData.plan === 'free' ? 'Upgrade Plan' : 'Change Plan'}
257
+ </button>
258
+ {usageData.plan !== 'free' && (
259
+ <button
260
+ onClick={handleOpenBillingPortal}
261
+ disabled={isBillingLoading}
262
+ className="flex items-center gap-1.5 rounded-lg border border-border bg-background px-3 py-1.5 text-sm font-medium text-text-secondary hover:text-text-primary hover:bg-background-secondary disabled:opacity-50"
263
+ >
264
+ {isBillingLoading ? (
265
+ <Loader2 size={14} className="animate-spin" />
266
+ ) : (
267
+ <ExternalLink size={14} />
268
+ )}
269
+ Manage Subscription
270
+ </button>
271
+ )}
272
+ </div>
273
+ </div>
274
+ </div>
275
+ )}
276
+
277
+ {/* Theme Selection */}
278
+ {config.theming.allowUserThemeSwitch && (
279
+ <div>
280
+ <label className="mb-2 block text-sm font-medium text-text-primary">
281
+ Theme
282
+ </label>
283
+ <div className="flex flex-wrap gap-2">
284
+ {availableThemes.map((t) => (
285
+ <button
286
+ key={t}
287
+ onClick={() => handleThemeChange(t)}
288
+ className={`rounded-lg px-4 py-2 text-sm capitalize ${
289
+ theme === t
290
+ ? 'bg-primary text-white'
291
+ : 'bg-background-secondary text-text-secondary hover:text-text-primary active:bg-primary/10'
292
+ }`}
293
+ >
294
+ {config.theming.themes[t]?.name || t}
295
+ </button>
296
+ ))}
297
+ </div>
298
+ </div>
299
+ )}
300
+
301
+ {/* User Settings Fields */}
302
+ {config.userSettings.fields.map((field) => (
303
+ <div key={field.key}>
304
+ <label
305
+ htmlFor={field.key}
306
+ className="mb-2 block text-sm font-medium text-text-primary"
307
+ >
308
+ {field.label}
309
+ </label>
310
+
311
+ {field.type === 'text' && (
312
+ <input
313
+ id={field.key}
314
+ type="text"
315
+ value={settings[field.key] || ''}
316
+ onChange={(e) => handleFieldChange(field.key, e.target.value)}
317
+ placeholder={field.placeholder}
318
+ className="w-full rounded-lg border border-input-border bg-input-background px-3 py-2.5 text-base text-text-primary placeholder-text-muted focus:border-primary focus:outline-none sm:text-sm"
319
+ />
320
+ )}
321
+
322
+ {field.type === 'textarea' && (
323
+ <textarea
324
+ id={field.key}
325
+ value={settings[field.key] || ''}
326
+ onChange={(e) => handleFieldChange(field.key, e.target.value)}
327
+ placeholder={field.placeholder}
328
+ rows={3}
329
+ className="w-full rounded-lg border border-input-border bg-input-background px-3 py-2.5 text-base text-text-primary placeholder-text-muted focus:border-primary focus:outline-none sm:text-sm"
330
+ />
331
+ )}
332
+
333
+ {field.type === 'select' && field.options && (
334
+ <select
335
+ id={field.key}
336
+ value={settings[field.key] || ''}
337
+ onChange={(e) => handleFieldChange(field.key, e.target.value)}
338
+ className="w-full rounded-lg border border-input-border bg-input-background px-3 py-2.5 text-base text-text-primary focus:border-primary focus:outline-none sm:text-sm"
339
+ >
340
+ <option value="">Select...</option>
341
+ {field.options.map((option) => (
342
+ <option key={option} value={option}>
343
+ {option}
344
+ </option>
345
+ ))}
346
+ </select>
347
+ )}
348
+ </div>
349
+ ))}
350
+
351
+ {/* MCP Credentials Section */}
352
+ <MCPCredentialsSection />
353
+
354
+ {/* OAuth Connected Applications */}
355
+ <OAuthAppsSection />
356
+
357
+ {/* API Keys Link */}
358
+ {config.api?.enabled && canAccessApiKeys && (
359
+ <div className="rounded-lg border border-border bg-background-secondary p-4">
360
+ <div className="flex items-center justify-between">
361
+ <div className="flex items-center gap-3">
362
+ <Key size={18} className="text-primary" />
363
+ <div>
364
+ <h3 className="font-medium text-text-primary">API Keys</h3>
365
+ <p className="text-xs text-text-muted">Manage keys for programmatic API access</p>
366
+ </div>
367
+ </div>
368
+ <Link
369
+ to={appPath('/api-keys')}
370
+ onClick={onClose}
371
+ className="text-sm text-primary hover:underline"
372
+ >
373
+ Manage
374
+ </Link>
375
+ </div>
376
+ </div>
377
+ )}
378
+ </div>
379
+ )}
380
+ </div>
381
+
382
+ {/* Footer */}
383
+ <div className="flex items-center justify-end gap-3 border-t border-border px-4 py-3 sm:px-6 sm:py-4">
384
+ {saveStatus === 'saved' && (
385
+ <span className="flex items-center gap-1 text-sm text-success">
386
+ <Check size={16} />
387
+ Saved
388
+ </span>
389
+ )}
390
+ {saveStatus === 'error' && (
391
+ <span className="text-sm text-error">Failed to save</span>
392
+ )}
393
+ <button
394
+ onClick={onClose}
395
+ className="rounded-lg px-4 py-2 text-sm text-text-secondary hover:bg-background-secondary hover:text-text-primary active:bg-background-secondary"
396
+ >
397
+ Cancel
398
+ </button>
399
+ <button
400
+ onClick={handleSave}
401
+ disabled={isSaving || isLoading}
402
+ className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm text-white hover:bg-primary-hover active:bg-primary-hover disabled:opacity-50"
403
+ >
404
+ {isSaving && <Loader2 size={16} className="animate-spin" />}
405
+ Save
406
+ </button>
407
+ </div>
408
+ </div>
409
+ </div>,
410
+ document.body
411
+ );
412
+ }