@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,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
+ }