@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.
- package/dist/favicon.svg +11 -0
- package/dist/index.html +17 -0
- package/dist/lib/LoadingSkeletons-IcIC2JPq.js +132 -0
- package/dist/lib/LoadingSkeletons-IcIC2JPq.js.map +1 -0
- package/dist/lib/ServerThemeProvider-DNF0LAyk.js +42 -0
- package/dist/lib/ServerThemeProvider-DNF0LAyk.js.map +1 -0
- package/dist/lib/extensions.js +10 -0
- package/dist/lib/extensions.js.map +1 -0
- package/dist/lib/favicon.svg +11 -0
- package/dist/lib/index.js +74126 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logo.svg +12 -0
- package/dist/lib/routes/AcceptInviteRoute.js +19 -0
- package/dist/lib/routes/AcceptInviteRoute.js.map +1 -0
- package/dist/lib/routes/AdminDashboardRoute.js +19 -0
- package/dist/lib/routes/AdminDashboardRoute.js.map +1 -0
- package/dist/lib/routes/AdminTeamRoute.js +19 -0
- package/dist/lib/routes/AdminTeamRoute.js.map +1 -0
- package/dist/lib/routes/AdminTeamsRoute.js +19 -0
- package/dist/lib/routes/AdminTeamsRoute.js.map +1 -0
- package/dist/lib/routes/AdminUsersRoute.js +19 -0
- package/dist/lib/routes/AdminUsersRoute.js.map +1 -0
- package/dist/lib/routes/ApiKeysRoute.js +19 -0
- package/dist/lib/routes/ApiKeysRoute.js.map +1 -0
- package/dist/lib/routes/AutomationsRoute.js +19 -0
- package/dist/lib/routes/AutomationsRoute.js.map +1 -0
- package/dist/lib/routes/ChatRoute.js +19 -0
- package/dist/lib/routes/ChatRoute.js.map +1 -0
- package/dist/lib/routes/DocumentsRoute.js +19 -0
- package/dist/lib/routes/DocumentsRoute.js.map +1 -0
- package/dist/lib/routes/OAuthConsentRoute.js +19 -0
- package/dist/lib/routes/OAuthConsentRoute.js.map +1 -0
- package/dist/lib/routes/PricingRoute.js +19 -0
- package/dist/lib/routes/PricingRoute.js.map +1 -0
- package/dist/lib/routes/PrivacyRoute.js +19 -0
- package/dist/lib/routes/PrivacyRoute.js.map +1 -0
- package/dist/lib/routes/TeamSettingsRoute.js +19 -0
- package/dist/lib/routes/TeamSettingsRoute.js.map +1 -0
- package/dist/lib/routes/TermsRoute.js +19 -0
- package/dist/lib/routes/TermsRoute.js.map +1 -0
- package/dist/lib/routes/VerifyEmailRoute.js +19 -0
- package/dist/lib/routes/VerifyEmailRoute.js.map +1 -0
- package/dist/lib/routes.js +79 -0
- package/dist/lib/routes.js.map +1 -0
- package/dist/lib/ssr-utils.js +29 -0
- package/dist/lib/ssr-utils.js.map +1 -0
- package/dist/lib/ssr.js +60 -0
- package/dist/lib/ssr.js.map +1 -0
- package/dist/lib/styles.css +2410 -0
- package/dist/lib/useExtensions-B5nX_8XD.js +155 -0
- package/dist/lib/useExtensions-B5nX_8XD.js.map +1 -0
- package/dist/logo.svg +12 -0
- package/package.json +84 -0
- package/src/components/AgentSelector.tsx +90 -0
- package/src/components/BranchModal.tsx +129 -0
- package/src/components/ClientOnly.tsx +27 -0
- package/src/components/ExportMenu.tsx +122 -0
- package/src/components/LoadingSkeletons.tsx +110 -0
- package/src/components/MCPCredentialsSection.tsx +309 -0
- package/src/components/MentionChip.tsx +149 -0
- package/src/components/MentionDropdown.tsx +175 -0
- package/src/components/MentionInput.tsx +293 -0
- package/src/components/MessageItem.tsx +300 -0
- package/src/components/MessageList.tsx +159 -0
- package/src/components/OAuthAppsSection.tsx +124 -0
- package/src/components/ProjectFolder.tsx +141 -0
- package/src/components/ProjectModal.tsx +296 -0
- package/src/components/SSRMessageList.tsx +153 -0
- package/src/components/SearchModal.tsx +173 -0
- package/src/components/SettingsModal.tsx +412 -0
- package/src/components/ShareModal.tsx +280 -0
- package/src/components/Sidebar.tsx +491 -0
- package/src/components/TeamSwitcher.tsx +273 -0
- package/src/components/ToolCallDisplay.tsx +473 -0
- package/src/components/ToolConfirmationModal.tsx +130 -0
- package/src/components/UsageChart.tsx +177 -0
- package/src/components/content/CodeBlock.tsx +69 -0
- package/src/components/content/MarkdownRenderer.tsx +64 -0
- package/src/components/content/SSRMarkdownRenderer.tsx +158 -0
- package/src/contexts/AuthContext.tsx +119 -0
- package/src/contexts/ConfigContext.tsx +214 -0
- package/src/contexts/ProjectContext.tsx +167 -0
- package/src/contexts/ServerConfigProvider.tsx +41 -0
- package/src/contexts/ServerThemeProvider.tsx +47 -0
- package/src/contexts/TeamContext.tsx +255 -0
- package/src/contexts/ThemeContext.tsx +113 -0
- package/src/extensions/index.ts +15 -0
- package/src/extensions/registry.ts +187 -0
- package/src/extensions/useExtensions.ts +52 -0
- package/src/hooks/useAppPath.ts +34 -0
- package/src/hooks/useBasePath.ts +13 -0
- package/src/hooks/useKeyboardShortcuts.ts +50 -0
- package/src/hooks/useMentionSearch.ts +106 -0
- package/src/index.tsx +116 -0
- package/src/layouts/MainLayout.tsx +98 -0
- package/src/pages/AcceptInvitePage.tsx +175 -0
- package/src/pages/AdminDashboardPage.tsx +362 -0
- package/src/pages/AdminTeamPage.tsx +304 -0
- package/src/pages/AdminTeamsPage.tsx +242 -0
- package/src/pages/AdminUsersPage.tsx +385 -0
- package/src/pages/ApiKeysPage.tsx +449 -0
- package/src/pages/ChatPage.tsx +310 -0
- package/src/pages/DocumentsPage.tsx +577 -0
- package/src/pages/LoginPage.tsx +232 -0
- package/src/pages/OAuthConsentPage.tsx +234 -0
- package/src/pages/PricingPage.tsx +314 -0
- package/src/pages/PrivacyPage.tsx +65 -0
- package/src/pages/RegisterPage.tsx +153 -0
- package/src/pages/ScheduledPromptsPage.tsx +702 -0
- package/src/pages/SharedThreadPage.tsx +116 -0
- package/src/pages/TeamSettingsPage.tsx +1085 -0
- package/src/pages/TermsPage.tsx +82 -0
- package/src/pages/VerifyEmailPage.tsx +202 -0
- package/src/routes/AcceptInviteRoute.tsx +24 -0
- package/src/routes/AdminDashboardRoute.tsx +24 -0
- package/src/routes/AdminTeamRoute.tsx +24 -0
- package/src/routes/AdminTeamsRoute.tsx +24 -0
- package/src/routes/AdminUsersRoute.tsx +24 -0
- package/src/routes/ApiKeysRoute.tsx +24 -0
- package/src/routes/AutomationsRoute.tsx +24 -0
- package/src/routes/ChatRoute.tsx +28 -0
- package/src/routes/DocumentsRoute.tsx +24 -0
- package/src/routes/OAuthConsentRoute.tsx +24 -0
- package/src/routes/PricingRoute.tsx +24 -0
- package/src/routes/PrivacyRoute.tsx +24 -0
- package/src/routes/TeamSettingsRoute.tsx +24 -0
- package/src/routes/TermsRoute.tsx +24 -0
- package/src/routes/VerifyEmailRoute.tsx +24 -0
- package/src/routes/index.ts +57 -0
- package/src/ssr-utils.tsx +84 -0
- package/src/ssr.ts +123 -0
- package/src/stores/chatStore.ts +670 -0
- package/src/styles/index.css +254 -0
- package/src/utils/api.ts +78 -0
- 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
|
+
}
|