@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,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading skeleton for chat pages with sidebar.
|
|
3
|
+
* Used as SSR fallback for routes that render the full chat interface.
|
|
4
|
+
*/
|
|
5
|
+
export function ChatLoadingSkeleton() {
|
|
6
|
+
return (
|
|
7
|
+
<div style={{ display: 'flex', height: '100vh' }}>
|
|
8
|
+
{/* Sidebar skeleton */}
|
|
9
|
+
<div
|
|
10
|
+
style={{
|
|
11
|
+
width: '256px',
|
|
12
|
+
backgroundColor: 'rgb(var(--color-sidebar))',
|
|
13
|
+
borderRight: '1px solid rgb(var(--color-border))',
|
|
14
|
+
padding: '1rem',
|
|
15
|
+
}}
|
|
16
|
+
>
|
|
17
|
+
<div
|
|
18
|
+
style={{
|
|
19
|
+
height: '2rem',
|
|
20
|
+
backgroundColor: 'rgb(var(--color-background-secondary))',
|
|
21
|
+
borderRadius: '0.5rem',
|
|
22
|
+
marginBottom: '1rem',
|
|
23
|
+
}}
|
|
24
|
+
/>
|
|
25
|
+
<div
|
|
26
|
+
style={{
|
|
27
|
+
height: '1rem',
|
|
28
|
+
backgroundColor: 'rgb(var(--color-background-secondary))',
|
|
29
|
+
borderRadius: '0.25rem',
|
|
30
|
+
width: '80%',
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
{/* Main content skeleton */}
|
|
35
|
+
<div
|
|
36
|
+
style={{
|
|
37
|
+
flex: 1,
|
|
38
|
+
display: 'flex',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'center',
|
|
41
|
+
backgroundColor: 'rgb(var(--color-background))',
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<div
|
|
45
|
+
style={{
|
|
46
|
+
display: 'flex',
|
|
47
|
+
flexDirection: 'column',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
gap: '1rem',
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<div
|
|
53
|
+
style={{
|
|
54
|
+
width: '2rem',
|
|
55
|
+
height: '2rem',
|
|
56
|
+
border: '2px solid rgb(var(--color-primary))',
|
|
57
|
+
borderTopColor: 'transparent',
|
|
58
|
+
borderRadius: '50%',
|
|
59
|
+
animation: 'chaaskit-spin 1s linear infinite',
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
<p style={{ color: 'rgb(var(--color-text-muted))' }}>Loading...</p>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<style>
|
|
66
|
+
{`
|
|
67
|
+
@keyframes chaaskit-spin {
|
|
68
|
+
to { transform: rotate(360deg); }
|
|
69
|
+
}
|
|
70
|
+
`}
|
|
71
|
+
</style>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Simple centered loading spinner skeleton.
|
|
78
|
+
* Used as SSR fallback for simple pages without sidebar.
|
|
79
|
+
*/
|
|
80
|
+
export function SimpleLoadingSkeleton() {
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
style={{
|
|
84
|
+
display: 'flex',
|
|
85
|
+
height: '100vh',
|
|
86
|
+
alignItems: 'center',
|
|
87
|
+
justifyContent: 'center',
|
|
88
|
+
backgroundColor: 'rgb(var(--color-background))',
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<div
|
|
92
|
+
style={{
|
|
93
|
+
width: '2rem',
|
|
94
|
+
height: '2rem',
|
|
95
|
+
border: '2px solid rgb(var(--color-primary))',
|
|
96
|
+
borderTopColor: 'transparent',
|
|
97
|
+
borderRadius: '50%',
|
|
98
|
+
animation: 'chaaskit-spin 1s linear infinite',
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
<style>
|
|
102
|
+
{`
|
|
103
|
+
@keyframes chaaskit-spin {
|
|
104
|
+
to { transform: rotate(360deg); }
|
|
105
|
+
}
|
|
106
|
+
`}
|
|
107
|
+
</style>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { Key, Link, Unlink, ExternalLink, Loader2, Check, AlertCircle } from 'lucide-react';
|
|
3
|
+
import type { MCPCredentialStatus, MCPAuthMode } from '@chaaskit/shared';
|
|
4
|
+
|
|
5
|
+
interface MCPCredentialsSectionProps {
|
|
6
|
+
onCredentialChange?: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function MCPCredentialsSection({ onCredentialChange }: MCPCredentialsSectionProps) {
|
|
10
|
+
const [credentials, setCredentials] = useState<MCPCredentialStatus[]>([]);
|
|
11
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
12
|
+
const [error, setError] = useState<string | null>(null);
|
|
13
|
+
const [apiKeyInputs, setApiKeyInputs] = useState<Record<string, string>>({});
|
|
14
|
+
const [savingStates, setSavingStates] = useState<Record<string, 'idle' | 'saving' | 'saved' | 'error'>>({});
|
|
15
|
+
const [connectingStates, setConnectingStates] = useState<Record<string, boolean>>({});
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
loadCredentials();
|
|
19
|
+
|
|
20
|
+
// Check for OAuth callback results in URL
|
|
21
|
+
const params = new URLSearchParams(window.location.search);
|
|
22
|
+
const success = params.get('success');
|
|
23
|
+
const errorParam = params.get('error');
|
|
24
|
+
const serverId = params.get('server');
|
|
25
|
+
|
|
26
|
+
if (success === 'oauth_connected' && serverId) {
|
|
27
|
+
// Clear URL params
|
|
28
|
+
window.history.replaceState({}, '', window.location.pathname);
|
|
29
|
+
// Refresh credentials
|
|
30
|
+
loadCredentials();
|
|
31
|
+
} else if (errorParam) {
|
|
32
|
+
setError(`OAuth error: ${errorParam.replace(/_/g, ' ')}`);
|
|
33
|
+
window.history.replaceState({}, '', window.location.pathname);
|
|
34
|
+
}
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
async function loadCredentials() {
|
|
38
|
+
setIsLoading(true);
|
|
39
|
+
setError(null);
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch('/api/mcp/credentials', {
|
|
42
|
+
credentials: 'include',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error('Failed to load credentials');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const data = await response.json();
|
|
50
|
+
console.log('[MCP Credentials] Loaded:', data.credentials);
|
|
51
|
+
setCredentials(data.credentials || []);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error('Failed to load MCP credentials:', err);
|
|
54
|
+
setError('Failed to load credentials');
|
|
55
|
+
} finally {
|
|
56
|
+
setIsLoading(false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function handleSaveApiKey(serverId: string) {
|
|
61
|
+
const apiKey = apiKeyInputs[serverId];
|
|
62
|
+
if (!apiKey?.trim()) return;
|
|
63
|
+
|
|
64
|
+
setSavingStates((prev) => ({ ...prev, [serverId]: 'saving' }));
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(`/api/mcp/credentials/${serverId}/apikey`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
credentials: 'include',
|
|
71
|
+
body: JSON.stringify({ apiKey: apiKey.trim() }),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new Error('Failed to save API key');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setSavingStates((prev) => ({ ...prev, [serverId]: 'saved' }));
|
|
79
|
+
setApiKeyInputs((prev) => ({ ...prev, [serverId]: '' }));
|
|
80
|
+
|
|
81
|
+
// Refresh credentials list
|
|
82
|
+
loadCredentials();
|
|
83
|
+
onCredentialChange?.();
|
|
84
|
+
|
|
85
|
+
// Reset status after delay
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
setSavingStates((prev) => ({ ...prev, [serverId]: 'idle' }));
|
|
88
|
+
}, 2000);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.error('Failed to save API key:', err);
|
|
91
|
+
setSavingStates((prev) => ({ ...prev, [serverId]: 'error' }));
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
setSavingStates((prev) => ({ ...prev, [serverId]: 'idle' }));
|
|
94
|
+
}, 2000);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function handleDisconnect(serverId: string) {
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(`/api/mcp/credentials/${serverId}`, {
|
|
101
|
+
method: 'DELETE',
|
|
102
|
+
credentials: 'include',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error('Failed to disconnect');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
loadCredentials();
|
|
110
|
+
onCredentialChange?.();
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error('Failed to disconnect:', err);
|
|
113
|
+
setError('Failed to disconnect');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function handleOAuthConnect(serverId: string) {
|
|
118
|
+
setConnectingStates((prev) => ({ ...prev, [serverId]: true }));
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const response = await fetch(`/api/mcp/oauth/${serverId}/authorize`, {
|
|
122
|
+
credentials: 'include',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
throw new Error('Failed to start OAuth flow');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const data = await response.json();
|
|
130
|
+
|
|
131
|
+
// Redirect to OAuth provider
|
|
132
|
+
window.location.href = data.authorizationUrl;
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error('Failed to start OAuth:', err);
|
|
135
|
+
setError('Failed to start authentication');
|
|
136
|
+
setConnectingStates((prev) => ({ ...prev, [serverId]: false }));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getAuthModeLabel(authMode: MCPAuthMode): string {
|
|
141
|
+
switch (authMode) {
|
|
142
|
+
case 'user-apikey':
|
|
143
|
+
return 'API Key';
|
|
144
|
+
case 'user-oauth':
|
|
145
|
+
return 'OAuth';
|
|
146
|
+
default:
|
|
147
|
+
return authMode;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (isLoading) {
|
|
152
|
+
return (
|
|
153
|
+
<div className="flex items-center justify-center py-4">
|
|
154
|
+
<Loader2 className="h-5 w-5 animate-spin text-primary" />
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (credentials.length === 0) {
|
|
160
|
+
// No servers configured with user credentials
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div className="pt-4 border-t border-border">
|
|
166
|
+
<h3 className="mb-3 text-sm font-medium text-text-primary">Tool Connections</h3>
|
|
167
|
+
|
|
168
|
+
{error && (
|
|
169
|
+
<div className="mb-3 flex items-center gap-2 rounded-lg bg-error/10 px-3 py-2 text-sm text-error">
|
|
170
|
+
<AlertCircle size={16} />
|
|
171
|
+
{error}
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
<div className="space-y-3">
|
|
176
|
+
{credentials.map((cred) => (
|
|
177
|
+
<div
|
|
178
|
+
key={cred.serverId}
|
|
179
|
+
className="rounded-lg border border-border bg-background-secondary p-3"
|
|
180
|
+
>
|
|
181
|
+
<div className="flex items-center justify-between">
|
|
182
|
+
<div className="flex items-center gap-2">
|
|
183
|
+
{cred.authMode === 'user-apikey' ? (
|
|
184
|
+
<Key size={16} className="text-text-secondary" />
|
|
185
|
+
) : (
|
|
186
|
+
<Link size={16} className="text-text-secondary" />
|
|
187
|
+
)}
|
|
188
|
+
<span className="font-medium text-text-primary">{cred.serverName}</span>
|
|
189
|
+
<span className="rounded-full bg-background px-2 py-0.5 text-xs text-text-muted">
|
|
190
|
+
{getAuthModeLabel(cred.authMode)}
|
|
191
|
+
</span>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{cred.hasCredential && (
|
|
195
|
+
<div className="flex items-center gap-2">
|
|
196
|
+
<span className="flex items-center gap-1 text-xs text-success">
|
|
197
|
+
<Check size={14} />
|
|
198
|
+
Connected
|
|
199
|
+
</span>
|
|
200
|
+
<button
|
|
201
|
+
onClick={() => handleDisconnect(cred.serverId)}
|
|
202
|
+
className="rounded p-1 text-text-secondary hover:bg-background hover:text-error"
|
|
203
|
+
title="Disconnect"
|
|
204
|
+
>
|
|
205
|
+
<Unlink size={14} />
|
|
206
|
+
</button>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
{cred.userInstructions && (
|
|
212
|
+
<p className="mt-2 text-xs text-text-muted">{cred.userInstructions}</p>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{!cred.hasCredential && cred.authMode === 'user-apikey' && (
|
|
216
|
+
<div className="mt-3 flex gap-2">
|
|
217
|
+
<input
|
|
218
|
+
type="password"
|
|
219
|
+
value={apiKeyInputs[cred.serverId] || ''}
|
|
220
|
+
onChange={(e) =>
|
|
221
|
+
setApiKeyInputs((prev) => ({ ...prev, [cred.serverId]: e.target.value }))
|
|
222
|
+
}
|
|
223
|
+
placeholder="Enter API key..."
|
|
224
|
+
className="flex-1 rounded-lg border border-input-border bg-input-background px-3 py-2 text-sm text-text-primary placeholder-text-muted focus:border-primary focus:outline-none"
|
|
225
|
+
/>
|
|
226
|
+
<button
|
|
227
|
+
onClick={() => handleSaveApiKey(cred.serverId)}
|
|
228
|
+
disabled={
|
|
229
|
+
!apiKeyInputs[cred.serverId]?.trim() ||
|
|
230
|
+
savingStates[cred.serverId] === 'saving'
|
|
231
|
+
}
|
|
232
|
+
className="flex items-center gap-1.5 rounded-lg bg-primary px-3 py-2 text-sm text-white hover:bg-primary-hover disabled:opacity-50"
|
|
233
|
+
>
|
|
234
|
+
{savingStates[cred.serverId] === 'saving' ? (
|
|
235
|
+
<Loader2 size={14} className="animate-spin" />
|
|
236
|
+
) : savingStates[cred.serverId] === 'saved' ? (
|
|
237
|
+
<Check size={14} />
|
|
238
|
+
) : (
|
|
239
|
+
<Key size={14} />
|
|
240
|
+
)}
|
|
241
|
+
{savingStates[cred.serverId] === 'saving'
|
|
242
|
+
? 'Saving...'
|
|
243
|
+
: savingStates[cred.serverId] === 'saved'
|
|
244
|
+
? 'Saved'
|
|
245
|
+
: 'Save'}
|
|
246
|
+
</button>
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
|
|
250
|
+
{!cred.hasCredential && cred.authMode === 'user-oauth' && (
|
|
251
|
+
<div className="mt-3">
|
|
252
|
+
<button
|
|
253
|
+
onClick={() => handleOAuthConnect(cred.serverId)}
|
|
254
|
+
disabled={connectingStates[cred.serverId]}
|
|
255
|
+
className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm text-white hover:bg-primary-hover disabled:opacity-50"
|
|
256
|
+
>
|
|
257
|
+
{connectingStates[cred.serverId] ? (
|
|
258
|
+
<Loader2 size={14} className="animate-spin" />
|
|
259
|
+
) : (
|
|
260
|
+
<ExternalLink size={14} />
|
|
261
|
+
)}
|
|
262
|
+
{connectingStates[cred.serverId] ? 'Connecting...' : 'Connect'}
|
|
263
|
+
</button>
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
|
|
267
|
+
{cred.hasCredential && cred.authMode === 'user-apikey' && (
|
|
268
|
+
<div className="mt-3">
|
|
269
|
+
<details className="group">
|
|
270
|
+
<summary className="cursor-pointer text-xs text-text-muted hover:text-text-secondary">
|
|
271
|
+
Update API key
|
|
272
|
+
</summary>
|
|
273
|
+
<div className="mt-2 flex gap-2">
|
|
274
|
+
<input
|
|
275
|
+
type="password"
|
|
276
|
+
value={apiKeyInputs[cred.serverId] || ''}
|
|
277
|
+
onChange={(e) =>
|
|
278
|
+
setApiKeyInputs((prev) => ({ ...prev, [cred.serverId]: e.target.value }))
|
|
279
|
+
}
|
|
280
|
+
placeholder="Enter new API key..."
|
|
281
|
+
className="flex-1 rounded-lg border border-input-border bg-input-background px-3 py-2 text-sm text-text-primary placeholder-text-muted focus:border-primary focus:outline-none"
|
|
282
|
+
/>
|
|
283
|
+
<button
|
|
284
|
+
onClick={() => handleSaveApiKey(cred.serverId)}
|
|
285
|
+
disabled={
|
|
286
|
+
!apiKeyInputs[cred.serverId]?.trim() ||
|
|
287
|
+
savingStates[cred.serverId] === 'saving'
|
|
288
|
+
}
|
|
289
|
+
className="flex items-center gap-1.5 rounded-lg bg-primary px-3 py-2 text-sm text-white hover:bg-primary-hover disabled:opacity-50"
|
|
290
|
+
>
|
|
291
|
+
{savingStates[cred.serverId] === 'saving' ? (
|
|
292
|
+
<Loader2 size={14} className="animate-spin" />
|
|
293
|
+
) : savingStates[cred.serverId] === 'saved' ? (
|
|
294
|
+
<Check size={14} />
|
|
295
|
+
) : (
|
|
296
|
+
<Key size={14} />
|
|
297
|
+
)}
|
|
298
|
+
Update
|
|
299
|
+
</button>
|
|
300
|
+
</div>
|
|
301
|
+
</details>
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
))}
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { User, Users, FolderKanban, FileText, X } from 'lucide-react';
|
|
3
|
+
import type { DocumentScope } from '@chaaskit/shared';
|
|
4
|
+
|
|
5
|
+
interface MentionChipProps {
|
|
6
|
+
path: string;
|
|
7
|
+
onClick?: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Parse path like "@my/doc-name" or "@team/engineering/doc-name"
|
|
11
|
+
function parseMentionPath(path: string): { scope: DocumentScope; scopeSlug?: string; name: string } | null {
|
|
12
|
+
// Remove @ prefix if present
|
|
13
|
+
const cleanPath = path.startsWith('@') ? path.slice(1) : path;
|
|
14
|
+
const parts = cleanPath.split('/');
|
|
15
|
+
|
|
16
|
+
if (parts.length < 2) return null;
|
|
17
|
+
|
|
18
|
+
const scope = parts[0] as DocumentScope;
|
|
19
|
+
if (!['my', 'team', 'project'].includes(scope)) return null;
|
|
20
|
+
|
|
21
|
+
if (scope === 'my') {
|
|
22
|
+
return { scope, name: parts.slice(1).join('/') };
|
|
23
|
+
} else {
|
|
24
|
+
// team/slug/name or project/slug/name
|
|
25
|
+
if (parts.length < 3) return null;
|
|
26
|
+
return { scope, scopeSlug: parts[1], name: parts.slice(2).join('/') };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getScopeIcon(scope: DocumentScope) {
|
|
31
|
+
switch (scope) {
|
|
32
|
+
case 'my':
|
|
33
|
+
return User;
|
|
34
|
+
case 'team':
|
|
35
|
+
return Users;
|
|
36
|
+
case 'project':
|
|
37
|
+
return FolderKanban;
|
|
38
|
+
default:
|
|
39
|
+
return FileText;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getScopeColor(scope: DocumentScope) {
|
|
44
|
+
switch (scope) {
|
|
45
|
+
case 'my':
|
|
46
|
+
return 'bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20';
|
|
47
|
+
case 'team':
|
|
48
|
+
return 'bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/20';
|
|
49
|
+
case 'project':
|
|
50
|
+
return 'bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/20';
|
|
51
|
+
default:
|
|
52
|
+
return 'bg-gray-500/10 text-gray-600 dark:text-gray-400 border-gray-500/20';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default function MentionChip({ path, onClick }: MentionChipProps) {
|
|
57
|
+
const parsed = parseMentionPath(path);
|
|
58
|
+
|
|
59
|
+
if (!parsed) {
|
|
60
|
+
// Fallback: render as plain text if we can't parse
|
|
61
|
+
return <span className="text-primary">{path}</span>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const Icon = getScopeIcon(parsed.scope);
|
|
65
|
+
const colorClass = getScopeColor(parsed.scope);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<span
|
|
69
|
+
onClick={onClick}
|
|
70
|
+
className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium border ${colorClass} ${onClick ? 'cursor-pointer hover:opacity-80' : ''}`}
|
|
71
|
+
title={path}
|
|
72
|
+
>
|
|
73
|
+
<Icon size={12} />
|
|
74
|
+
<span className="max-w-[150px] truncate">{parsed.name}</span>
|
|
75
|
+
</span>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Regex to match @scope/path patterns
|
|
80
|
+
// Matches: @my/name, @team/slug/name, @project/slug/name
|
|
81
|
+
const MENTION_REGEX = /@(my|team|project)\/[\w-]+(\/[\w-]+)*/g;
|
|
82
|
+
|
|
83
|
+
interface ParsedMentionSegment {
|
|
84
|
+
type: 'text' | 'mention';
|
|
85
|
+
content: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parseMentionsFromText(text: string): ParsedMentionSegment[] {
|
|
89
|
+
const segments: ParsedMentionSegment[] = [];
|
|
90
|
+
let lastIndex = 0;
|
|
91
|
+
|
|
92
|
+
// Reset regex state
|
|
93
|
+
MENTION_REGEX.lastIndex = 0;
|
|
94
|
+
|
|
95
|
+
let match;
|
|
96
|
+
while ((match = MENTION_REGEX.exec(text)) !== null) {
|
|
97
|
+
// Add text before the mention
|
|
98
|
+
if (match.index > lastIndex) {
|
|
99
|
+
segments.push({
|
|
100
|
+
type: 'text',
|
|
101
|
+
content: text.slice(lastIndex, match.index),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add the mention
|
|
106
|
+
segments.push({
|
|
107
|
+
type: 'mention',
|
|
108
|
+
content: match[0],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
lastIndex = match.index + match[0].length;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Add remaining text
|
|
115
|
+
if (lastIndex < text.length) {
|
|
116
|
+
segments.push({
|
|
117
|
+
type: 'text',
|
|
118
|
+
content: text.slice(lastIndex),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return segments;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface MessageContentWithMentionsProps {
|
|
126
|
+
content: string;
|
|
127
|
+
onMentionClick?: (path: string) => void;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function MessageContentWithMentions({ content, onMentionClick }: MessageContentWithMentionsProps) {
|
|
131
|
+
const segments = parseMentionsFromText(content);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<>
|
|
135
|
+
{segments.map((segment, index) => {
|
|
136
|
+
if (segment.type === 'mention') {
|
|
137
|
+
return (
|
|
138
|
+
<MentionChip
|
|
139
|
+
key={index}
|
|
140
|
+
path={segment.content}
|
|
141
|
+
onClick={onMentionClick ? () => onMentionClick(segment.content) : undefined}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return <span key={index}>{segment.content}</span>;
|
|
146
|
+
})}
|
|
147
|
+
</>
|
|
148
|
+
);
|
|
149
|
+
}
|