@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,577 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { Link, Navigate } from 'react-router';
|
|
3
|
+
import {
|
|
4
|
+
FileText,
|
|
5
|
+
Plus,
|
|
6
|
+
Trash2,
|
|
7
|
+
X,
|
|
8
|
+
Loader2,
|
|
9
|
+
Upload,
|
|
10
|
+
Search,
|
|
11
|
+
Users,
|
|
12
|
+
Folder,
|
|
13
|
+
Edit2,
|
|
14
|
+
Eye,
|
|
15
|
+
} from 'lucide-react';
|
|
16
|
+
import type { MentionableDocument, DocumentScope } from '@chaaskit/shared';
|
|
17
|
+
import { useConfig, useConfigLoaded } from '../contexts/ConfigContext';
|
|
18
|
+
import { useTeam } from '../contexts/TeamContext';
|
|
19
|
+
import { useProject } from '../contexts/ProjectContext';
|
|
20
|
+
import { useAppPath } from '../hooks/useAppPath';
|
|
21
|
+
import { api } from '../utils/api';
|
|
22
|
+
|
|
23
|
+
interface DocumentWithContent extends MentionableDocument {
|
|
24
|
+
content?: string;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
updatedAt: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function DocumentsPage() {
|
|
30
|
+
const config = useConfig();
|
|
31
|
+
const configLoaded = useConfigLoaded();
|
|
32
|
+
const { teams, currentTeamId } = useTeam();
|
|
33
|
+
const { projects, currentProjectId } = useProject();
|
|
34
|
+
const appPath = useAppPath();
|
|
35
|
+
|
|
36
|
+
const [documents, setDocuments] = useState<DocumentWithContent[]>([]);
|
|
37
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
38
|
+
const [error, setError] = useState('');
|
|
39
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
40
|
+
const [scopeFilter, setScopeFilter] = useState<DocumentScope | 'all'>('all');
|
|
41
|
+
|
|
42
|
+
// Create/edit modal
|
|
43
|
+
const [showModal, setShowModal] = useState(false);
|
|
44
|
+
const [editingDoc, setEditingDoc] = useState<DocumentWithContent | null>(null);
|
|
45
|
+
const [docName, setDocName] = useState('');
|
|
46
|
+
const [docContent, setDocContent] = useState('');
|
|
47
|
+
const [docScope, setDocScope] = useState<DocumentScope>('my');
|
|
48
|
+
const [docTeamId, setDocTeamId] = useState('');
|
|
49
|
+
const [docProjectId, setDocProjectId] = useState('');
|
|
50
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
51
|
+
|
|
52
|
+
// View modal
|
|
53
|
+
const [viewingDoc, setViewingDoc] = useState<DocumentWithContent | null>(null);
|
|
54
|
+
const [viewContent, setViewContent] = useState('');
|
|
55
|
+
const [isLoadingContent, setIsLoadingContent] = useState(false);
|
|
56
|
+
|
|
57
|
+
// Upload
|
|
58
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
59
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
60
|
+
|
|
61
|
+
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
62
|
+
|
|
63
|
+
const documentsEnabled = config.documents?.enabled ?? false;
|
|
64
|
+
const teamsEnabled = config.teams?.enabled ?? false;
|
|
65
|
+
const projectsEnabled = config.projects?.enabled ?? false;
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (documentsEnabled) {
|
|
69
|
+
loadDocuments();
|
|
70
|
+
} else {
|
|
71
|
+
setIsLoading(false);
|
|
72
|
+
}
|
|
73
|
+
}, [documentsEnabled, scopeFilter, currentTeamId, currentProjectId]);
|
|
74
|
+
|
|
75
|
+
async function loadDocuments() {
|
|
76
|
+
setIsLoading(true);
|
|
77
|
+
setError('');
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const params = new URLSearchParams();
|
|
81
|
+
if (scopeFilter !== 'all') params.set('scope', scopeFilter);
|
|
82
|
+
if (searchQuery) params.set('query', searchQuery);
|
|
83
|
+
if (currentTeamId) params.set('teamId', currentTeamId);
|
|
84
|
+
if (currentProjectId) params.set('projectId', currentProjectId);
|
|
85
|
+
|
|
86
|
+
const res = await api.get<{ documents: DocumentWithContent[] }>(
|
|
87
|
+
`/api/documents?${params.toString()}`
|
|
88
|
+
);
|
|
89
|
+
setDocuments(res.documents);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
setError(err instanceof Error ? err.message : 'Failed to load documents');
|
|
92
|
+
} finally {
|
|
93
|
+
setIsLoading(false);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function handleSearch(e: React.FormEvent) {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
await loadDocuments();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function openCreateModal() {
|
|
103
|
+
setEditingDoc(null);
|
|
104
|
+
setDocName('');
|
|
105
|
+
setDocContent('');
|
|
106
|
+
setDocScope('my');
|
|
107
|
+
setDocTeamId('');
|
|
108
|
+
setDocProjectId('');
|
|
109
|
+
setShowModal(true);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function openEditModal(doc: DocumentWithContent) {
|
|
113
|
+
setEditingDoc(doc);
|
|
114
|
+
setDocName(doc.name);
|
|
115
|
+
setDocContent(doc.content || '');
|
|
116
|
+
setDocScope(doc.scope);
|
|
117
|
+
setShowModal(true);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function handleSave(e: React.FormEvent) {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
setIsSaving(true);
|
|
123
|
+
setError('');
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
if (editingDoc) {
|
|
127
|
+
// Update existing
|
|
128
|
+
await api.patch(`/api/documents/${editingDoc.id}`, {
|
|
129
|
+
name: docName,
|
|
130
|
+
content: docContent,
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
// Create new
|
|
134
|
+
await api.post('/api/documents', {
|
|
135
|
+
name: docName,
|
|
136
|
+
content: docContent,
|
|
137
|
+
mimeType: 'text/plain',
|
|
138
|
+
teamId: docScope === 'team' ? docTeamId : undefined,
|
|
139
|
+
projectId: docScope === 'project' ? docProjectId : undefined,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
setShowModal(false);
|
|
144
|
+
await loadDocuments();
|
|
145
|
+
} catch (err) {
|
|
146
|
+
setError(err instanceof Error ? err.message : 'Failed to save document');
|
|
147
|
+
} finally {
|
|
148
|
+
setIsSaving(false);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
|
|
153
|
+
const file = e.target.files?.[0];
|
|
154
|
+
if (!file) return;
|
|
155
|
+
|
|
156
|
+
setIsUploading(true);
|
|
157
|
+
setError('');
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const formData = new FormData();
|
|
161
|
+
formData.append('file', file);
|
|
162
|
+
if (currentTeamId) formData.append('teamId', currentTeamId);
|
|
163
|
+
if (currentProjectId) formData.append('projectId', currentProjectId);
|
|
164
|
+
|
|
165
|
+
await fetch('/api/documents/upload', {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
credentials: 'include',
|
|
168
|
+
body: formData,
|
|
169
|
+
}).then(async (res) => {
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
const err = await res.json();
|
|
172
|
+
throw new Error(err.error?.message || 'Upload failed');
|
|
173
|
+
}
|
|
174
|
+
return res.json();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await loadDocuments();
|
|
178
|
+
} catch (err) {
|
|
179
|
+
setError(err instanceof Error ? err.message : 'Failed to upload document');
|
|
180
|
+
} finally {
|
|
181
|
+
setIsUploading(false);
|
|
182
|
+
if (fileInputRef.current) fileInputRef.current.value = '';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function handleDelete(id: string) {
|
|
187
|
+
if (!confirm('Are you sure you want to delete this document?')) return;
|
|
188
|
+
|
|
189
|
+
setDeletingId(id);
|
|
190
|
+
setError('');
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
await api.delete(`/api/documents/${id}`);
|
|
194
|
+
setDocuments((prev) => prev.filter((d) => d.id !== id));
|
|
195
|
+
} catch (err) {
|
|
196
|
+
setError(err instanceof Error ? err.message : 'Failed to delete document');
|
|
197
|
+
} finally {
|
|
198
|
+
setDeletingId(null);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function handleView(doc: DocumentWithContent) {
|
|
203
|
+
setViewingDoc(doc);
|
|
204
|
+
setIsLoadingContent(true);
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const res = await api.get<{ content: string; totalChars: number }>(
|
|
208
|
+
`/api/documents/${doc.id}/content`
|
|
209
|
+
);
|
|
210
|
+
setViewContent(res.content);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
setViewContent('Failed to load content');
|
|
213
|
+
} finally {
|
|
214
|
+
setIsLoadingContent(false);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getScopeIcon(scope: DocumentScope) {
|
|
219
|
+
switch (scope) {
|
|
220
|
+
case 'my':
|
|
221
|
+
return <FileText size={14} className="text-text-muted" />;
|
|
222
|
+
case 'team':
|
|
223
|
+
return <Users size={14} className="text-text-muted" />;
|
|
224
|
+
case 'project':
|
|
225
|
+
return <Folder size={14} className="text-text-muted" />;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function formatDate(dateString: string): string {
|
|
230
|
+
return new Date(dateString).toLocaleDateString(undefined, {
|
|
231
|
+
year: 'numeric',
|
|
232
|
+
month: 'short',
|
|
233
|
+
day: 'numeric',
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function formatCharCount(count: number): string {
|
|
238
|
+
if (count < 1000) return `${count} chars`;
|
|
239
|
+
return `${(count / 1000).toFixed(1)}k chars`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Wait for config to load before checking if documents is enabled
|
|
243
|
+
if (!configLoaded) {
|
|
244
|
+
return (
|
|
245
|
+
<div className="min-h-screen bg-background flex items-center justify-center">
|
|
246
|
+
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!documentsEnabled) {
|
|
252
|
+
return <Navigate to={appPath('/')} replace />;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<div className="min-h-screen bg-background p-4 sm:p-8">
|
|
257
|
+
<div className="mx-auto max-w-4xl">
|
|
258
|
+
{/* Header */}
|
|
259
|
+
<div className="flex items-center justify-between mb-6 sm:mb-8">
|
|
260
|
+
<div className="flex items-center gap-3">
|
|
261
|
+
<FileText size={24} className="text-primary" />
|
|
262
|
+
<h1 className="text-xl sm:text-2xl font-bold text-text-primary">Documents</h1>
|
|
263
|
+
</div>
|
|
264
|
+
<Link
|
|
265
|
+
to={appPath('/')}
|
|
266
|
+
className="flex items-center justify-center rounded-lg p-2 text-text-muted hover:text-text-primary hover:bg-background-secondary"
|
|
267
|
+
aria-label="Close"
|
|
268
|
+
>
|
|
269
|
+
<X size={20} />
|
|
270
|
+
</Link>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
{/* Description */}
|
|
274
|
+
<div className="mb-6 text-sm text-text-secondary">
|
|
275
|
+
Manage your documents that can be referenced in chat using @mentions.
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
{/* Error */}
|
|
279
|
+
{error && (
|
|
280
|
+
<div className="mb-6 rounded-lg bg-error/10 p-4 text-sm text-error">{error}</div>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{/* Actions */}
|
|
284
|
+
<div className="flex flex-wrap items-center gap-3 mb-6">
|
|
285
|
+
<button
|
|
286
|
+
onClick={openCreateModal}
|
|
287
|
+
className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover"
|
|
288
|
+
>
|
|
289
|
+
<Plus size={16} />
|
|
290
|
+
New Document
|
|
291
|
+
</button>
|
|
292
|
+
|
|
293
|
+
<label className="flex items-center gap-2 rounded-lg border border-border px-4 py-2 text-sm font-medium text-text-primary hover:bg-background-secondary cursor-pointer">
|
|
294
|
+
{isUploading ? (
|
|
295
|
+
<Loader2 size={16} className="animate-spin" />
|
|
296
|
+
) : (
|
|
297
|
+
<Upload size={16} />
|
|
298
|
+
)}
|
|
299
|
+
Upload File
|
|
300
|
+
<input
|
|
301
|
+
ref={fileInputRef}
|
|
302
|
+
type="file"
|
|
303
|
+
className="hidden"
|
|
304
|
+
accept=".txt,.md,.csv,.json"
|
|
305
|
+
onChange={handleUpload}
|
|
306
|
+
disabled={isUploading}
|
|
307
|
+
/>
|
|
308
|
+
</label>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
{/* Search & Filter */}
|
|
312
|
+
<form onSubmit={handleSearch} className="flex flex-wrap items-center gap-3 mb-6">
|
|
313
|
+
<div className="relative flex-1 min-w-[200px]">
|
|
314
|
+
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
|
|
315
|
+
<input
|
|
316
|
+
type="text"
|
|
317
|
+
value={searchQuery}
|
|
318
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
319
|
+
placeholder="Search documents..."
|
|
320
|
+
className="w-full rounded-lg border border-input-border bg-input-background pl-9 pr-3 py-2 text-sm text-text-primary placeholder-text-muted focus:border-primary focus:outline-none"
|
|
321
|
+
/>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<select
|
|
325
|
+
value={scopeFilter}
|
|
326
|
+
onChange={(e) => setScopeFilter(e.target.value as DocumentScope | 'all')}
|
|
327
|
+
className="rounded-lg border border-input-border bg-input-background px-3 py-2 text-sm text-text-primary focus:border-primary focus:outline-none"
|
|
328
|
+
>
|
|
329
|
+
<option value="all">All Scopes</option>
|
|
330
|
+
<option value="my">My Documents</option>
|
|
331
|
+
{teamsEnabled && <option value="team">Team</option>}
|
|
332
|
+
{projectsEnabled && <option value="project">Project</option>}
|
|
333
|
+
</select>
|
|
334
|
+
|
|
335
|
+
<button
|
|
336
|
+
type="submit"
|
|
337
|
+
className="rounded-lg border border-border px-4 py-2 text-sm text-text-primary hover:bg-background-secondary"
|
|
338
|
+
>
|
|
339
|
+
Search
|
|
340
|
+
</button>
|
|
341
|
+
</form>
|
|
342
|
+
|
|
343
|
+
{/* Documents List */}
|
|
344
|
+
{isLoading ? (
|
|
345
|
+
<div className="flex items-center justify-center py-12">
|
|
346
|
+
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
|
347
|
+
</div>
|
|
348
|
+
) : documents.length === 0 ? (
|
|
349
|
+
<div className="rounded-lg border border-border bg-background-secondary p-8 text-center">
|
|
350
|
+
<FileText size={48} className="mx-auto mb-4 text-text-muted" />
|
|
351
|
+
<h3 className="text-lg font-medium text-text-primary mb-2">No Documents</h3>
|
|
352
|
+
<p className="text-sm text-text-secondary mb-4">
|
|
353
|
+
Create a document or upload a file to get started.
|
|
354
|
+
</p>
|
|
355
|
+
</div>
|
|
356
|
+
) : (
|
|
357
|
+
<div className="space-y-3">
|
|
358
|
+
{documents.map((doc) => (
|
|
359
|
+
<div
|
|
360
|
+
key={doc.id}
|
|
361
|
+
className="rounded-lg border border-border bg-background-secondary p-4"
|
|
362
|
+
>
|
|
363
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
|
364
|
+
<div className="flex-1 min-w-0">
|
|
365
|
+
<div className="flex items-center gap-2 mb-1">
|
|
366
|
+
{getScopeIcon(doc.scope)}
|
|
367
|
+
<span className="font-medium text-text-primary truncate">{doc.name}</span>
|
|
368
|
+
<span className="text-xs px-2 py-0.5 rounded-full bg-background text-text-muted">
|
|
369
|
+
{doc.mimeType.split('/')[1]}
|
|
370
|
+
</span>
|
|
371
|
+
</div>
|
|
372
|
+
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-muted">
|
|
373
|
+
<span className="font-mono">{doc.path}</span>
|
|
374
|
+
<span>{formatCharCount(doc.charCount)}</span>
|
|
375
|
+
<span>Updated: {formatDate(doc.updatedAt)}</span>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
<div className="flex items-center gap-2">
|
|
379
|
+
<button
|
|
380
|
+
onClick={() => handleView(doc)}
|
|
381
|
+
className="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm text-text-secondary hover:bg-background"
|
|
382
|
+
>
|
|
383
|
+
<Eye size={14} />
|
|
384
|
+
View
|
|
385
|
+
</button>
|
|
386
|
+
<button
|
|
387
|
+
onClick={() => openEditModal(doc)}
|
|
388
|
+
className="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm text-text-secondary hover:bg-background"
|
|
389
|
+
>
|
|
390
|
+
<Edit2 size={14} />
|
|
391
|
+
Edit
|
|
392
|
+
</button>
|
|
393
|
+
<button
|
|
394
|
+
onClick={() => handleDelete(doc.id)}
|
|
395
|
+
disabled={deletingId === doc.id}
|
|
396
|
+
className="flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm text-error hover:bg-error/10 disabled:opacity-50"
|
|
397
|
+
>
|
|
398
|
+
{deletingId === doc.id ? (
|
|
399
|
+
<Loader2 size={14} className="animate-spin" />
|
|
400
|
+
) : (
|
|
401
|
+
<Trash2 size={14} />
|
|
402
|
+
)}
|
|
403
|
+
Delete
|
|
404
|
+
</button>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
))}
|
|
409
|
+
</div>
|
|
410
|
+
)}
|
|
411
|
+
|
|
412
|
+
{/* Create/Edit Modal */}
|
|
413
|
+
{showModal && (
|
|
414
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
415
|
+
<div className="absolute inset-0 bg-black/50" onClick={() => setShowModal(false)} />
|
|
416
|
+
<div className="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto rounded-2xl bg-background p-6">
|
|
417
|
+
<h2 className="text-lg font-semibold text-text-primary mb-4">
|
|
418
|
+
{editingDoc ? 'Edit Document' : 'Create Document'}
|
|
419
|
+
</h2>
|
|
420
|
+
<form onSubmit={handleSave}>
|
|
421
|
+
<div className="space-y-4">
|
|
422
|
+
<div>
|
|
423
|
+
<label className="block text-sm font-medium text-text-primary mb-2">
|
|
424
|
+
Name
|
|
425
|
+
</label>
|
|
426
|
+
<input
|
|
427
|
+
type="text"
|
|
428
|
+
value={docName}
|
|
429
|
+
onChange={(e) => setDocName(e.target.value)}
|
|
430
|
+
placeholder="e.g., api-guidelines"
|
|
431
|
+
required
|
|
432
|
+
className="w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary placeholder-text-muted focus:border-primary focus:outline-none"
|
|
433
|
+
/>
|
|
434
|
+
<p className="mt-1 text-xs text-text-muted">
|
|
435
|
+
Use lowercase with hyphens. This will be part of the @mention path.
|
|
436
|
+
</p>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
{!editingDoc && (
|
|
440
|
+
<div>
|
|
441
|
+
<label className="block text-sm font-medium text-text-primary mb-2">
|
|
442
|
+
Scope
|
|
443
|
+
</label>
|
|
444
|
+
<select
|
|
445
|
+
value={docScope}
|
|
446
|
+
onChange={(e) => setDocScope(e.target.value as DocumentScope)}
|
|
447
|
+
className="w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary focus:border-primary focus:outline-none"
|
|
448
|
+
>
|
|
449
|
+
<option value="my">Personal (@my/...)</option>
|
|
450
|
+
{teamsEnabled && teams.length > 0 && (
|
|
451
|
+
<option value="team">Team (@team/...)</option>
|
|
452
|
+
)}
|
|
453
|
+
{projectsEnabled && projects.length > 0 && (
|
|
454
|
+
<option value="project">Project (@project/...)</option>
|
|
455
|
+
)}
|
|
456
|
+
</select>
|
|
457
|
+
</div>
|
|
458
|
+
)}
|
|
459
|
+
|
|
460
|
+
{!editingDoc && docScope === 'team' && teamsEnabled && (
|
|
461
|
+
<div>
|
|
462
|
+
<label className="block text-sm font-medium text-text-primary mb-2">
|
|
463
|
+
Team
|
|
464
|
+
</label>
|
|
465
|
+
<select
|
|
466
|
+
value={docTeamId}
|
|
467
|
+
onChange={(e) => setDocTeamId(e.target.value)}
|
|
468
|
+
required
|
|
469
|
+
className="w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary focus:border-primary focus:outline-none"
|
|
470
|
+
>
|
|
471
|
+
<option value="">Select a team</option>
|
|
472
|
+
{teams.map((team) => (
|
|
473
|
+
<option key={team.id} value={team.id}>
|
|
474
|
+
{team.name}
|
|
475
|
+
</option>
|
|
476
|
+
))}
|
|
477
|
+
</select>
|
|
478
|
+
</div>
|
|
479
|
+
)}
|
|
480
|
+
|
|
481
|
+
{!editingDoc && docScope === 'project' && projectsEnabled && (
|
|
482
|
+
<div>
|
|
483
|
+
<label className="block text-sm font-medium text-text-primary mb-2">
|
|
484
|
+
Project
|
|
485
|
+
</label>
|
|
486
|
+
<select
|
|
487
|
+
value={docProjectId}
|
|
488
|
+
onChange={(e) => setDocProjectId(e.target.value)}
|
|
489
|
+
required
|
|
490
|
+
className="w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary focus:border-primary focus:outline-none"
|
|
491
|
+
>
|
|
492
|
+
<option value="">Select a project</option>
|
|
493
|
+
{projects.map((project) => (
|
|
494
|
+
<option key={project.id} value={project.id}>
|
|
495
|
+
{project.name}
|
|
496
|
+
</option>
|
|
497
|
+
))}
|
|
498
|
+
</select>
|
|
499
|
+
</div>
|
|
500
|
+
)}
|
|
501
|
+
|
|
502
|
+
<div>
|
|
503
|
+
<label className="block text-sm font-medium text-text-primary mb-2">
|
|
504
|
+
Content
|
|
505
|
+
</label>
|
|
506
|
+
<textarea
|
|
507
|
+
value={docContent}
|
|
508
|
+
onChange={(e) => setDocContent(e.target.value)}
|
|
509
|
+
placeholder="Enter document content..."
|
|
510
|
+
rows={12}
|
|
511
|
+
className="w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary placeholder-text-muted focus:border-primary focus:outline-none font-mono text-sm"
|
|
512
|
+
/>
|
|
513
|
+
<p className="mt-1 text-xs text-text-muted">
|
|
514
|
+
{docContent.length} characters
|
|
515
|
+
</p>
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
|
|
519
|
+
<div className="mt-6 flex justify-end gap-3">
|
|
520
|
+
<button
|
|
521
|
+
type="button"
|
|
522
|
+
onClick={() => setShowModal(false)}
|
|
523
|
+
className="rounded-lg px-4 py-2 text-sm text-text-secondary hover:bg-background-secondary"
|
|
524
|
+
>
|
|
525
|
+
Cancel
|
|
526
|
+
</button>
|
|
527
|
+
<button
|
|
528
|
+
type="submit"
|
|
529
|
+
disabled={isSaving || !docName.trim()}
|
|
530
|
+
className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-50"
|
|
531
|
+
>
|
|
532
|
+
{isSaving && <Loader2 size={14} className="animate-spin" />}
|
|
533
|
+
{editingDoc ? 'Save Changes' : 'Create Document'}
|
|
534
|
+
</button>
|
|
535
|
+
</div>
|
|
536
|
+
</form>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
)}
|
|
540
|
+
|
|
541
|
+
{/* View Modal */}
|
|
542
|
+
{viewingDoc && (
|
|
543
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
544
|
+
<div
|
|
545
|
+
className="absolute inset-0 bg-black/50"
|
|
546
|
+
onClick={() => setViewingDoc(null)}
|
|
547
|
+
/>
|
|
548
|
+
<div className="relative w-full max-w-3xl max-h-[90vh] overflow-y-auto rounded-2xl bg-background p-6">
|
|
549
|
+
<div className="flex items-center justify-between mb-4">
|
|
550
|
+
<div>
|
|
551
|
+
<h2 className="text-lg font-semibold text-text-primary">{viewingDoc.name}</h2>
|
|
552
|
+
<p className="text-sm text-text-muted font-mono">{viewingDoc.path}</p>
|
|
553
|
+
</div>
|
|
554
|
+
<button
|
|
555
|
+
onClick={() => setViewingDoc(null)}
|
|
556
|
+
className="p-2 text-text-muted hover:text-text-primary"
|
|
557
|
+
>
|
|
558
|
+
<X size={20} />
|
|
559
|
+
</button>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
{isLoadingContent ? (
|
|
563
|
+
<div className="flex items-center justify-center py-12">
|
|
564
|
+
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
|
565
|
+
</div>
|
|
566
|
+
) : (
|
|
567
|
+
<pre className="rounded-lg bg-background-secondary p-4 text-sm text-text-primary font-mono whitespace-pre-wrap overflow-x-auto">
|
|
568
|
+
{viewContent}
|
|
569
|
+
</pre>
|
|
570
|
+
)}
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
)}
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
);
|
|
577
|
+
}
|