@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,362 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { Link, useLocation } from 'react-router';
|
|
3
|
+
import { Users, Building2, X, LayoutDashboard } from 'lucide-react';
|
|
4
|
+
import { api } from '../utils/api';
|
|
5
|
+
import { useConfig } from '../contexts/ConfigContext';
|
|
6
|
+
import { useAppPath } from '../hooks/useAppPath';
|
|
7
|
+
import type { AdminStats, FeedbackStats, UsageDataPoint, AdminUsageResponse } from '@chaaskit/shared';
|
|
8
|
+
import UsageChart from '../components/UsageChart';
|
|
9
|
+
|
|
10
|
+
type UsageMetric = 'messages' | 'inputTokens' | 'outputTokens' | 'totalTokens';
|
|
11
|
+
|
|
12
|
+
export default function AdminDashboardPage() {
|
|
13
|
+
const config = useConfig();
|
|
14
|
+
const appPath = useAppPath();
|
|
15
|
+
const [stats, setStats] = useState<AdminStats | null>(null);
|
|
16
|
+
const [feedback, setFeedback] = useState<FeedbackStats | null>(null);
|
|
17
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
18
|
+
const [error, setError] = useState('');
|
|
19
|
+
|
|
20
|
+
// Usage chart state
|
|
21
|
+
const [usage, setUsage] = useState<UsageDataPoint[]>([]);
|
|
22
|
+
const [usagePeriod, setUsagePeriod] = useState(30);
|
|
23
|
+
const [usageMetric, setUsageMetric] = useState<UsageMetric>('messages');
|
|
24
|
+
const [isLoadingUsage, setIsLoadingUsage] = useState(true);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
async function loadData() {
|
|
28
|
+
setIsLoading(true);
|
|
29
|
+
setError('');
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const [statsResponse, feedbackResponse] = await Promise.all([
|
|
33
|
+
api.get<AdminStats>('/api/admin/stats'),
|
|
34
|
+
api.get<FeedbackStats>('/api/admin/feedback?limit=5'),
|
|
35
|
+
]);
|
|
36
|
+
setStats(statsResponse);
|
|
37
|
+
setFeedback(feedbackResponse);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
setError(err instanceof Error ? err.message : 'Failed to load admin data');
|
|
40
|
+
} finally {
|
|
41
|
+
setIsLoading(false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
loadData();
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
// Fetch usage data
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
async function loadUsage() {
|
|
51
|
+
setIsLoadingUsage(true);
|
|
52
|
+
try {
|
|
53
|
+
const response = await api.get<AdminUsageResponse>(`/api/admin/usage?days=${usagePeriod}`);
|
|
54
|
+
setUsage(response.usage);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error('Failed to load usage data:', err);
|
|
57
|
+
setUsage([]);
|
|
58
|
+
} finally {
|
|
59
|
+
setIsLoadingUsage(false);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
loadUsage();
|
|
64
|
+
}, [usagePeriod]);
|
|
65
|
+
|
|
66
|
+
if (isLoading) {
|
|
67
|
+
return (
|
|
68
|
+
<div className="min-h-screen bg-background p-4 sm:p-8">
|
|
69
|
+
<div className="mx-auto max-w-6xl">
|
|
70
|
+
<div className="animate-pulse">
|
|
71
|
+
<div className="h-8 w-48 bg-background-secondary rounded mb-6 sm:mb-8" />
|
|
72
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
73
|
+
{[1, 2, 3, 4].map((i) => (
|
|
74
|
+
<div key={i} className="h-32 bg-background-secondary rounded-lg" />
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (error) {
|
|
84
|
+
return (
|
|
85
|
+
<div className="min-h-screen bg-background p-4 sm:p-8">
|
|
86
|
+
<div className="mx-auto max-w-6xl">
|
|
87
|
+
<div className="rounded-lg bg-error/10 p-4 text-error">
|
|
88
|
+
{error}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!stats) return null;
|
|
96
|
+
|
|
97
|
+
const planColors: Record<string, string> = {
|
|
98
|
+
free: 'bg-gray-400',
|
|
99
|
+
basic: 'bg-blue-400',
|
|
100
|
+
pro: 'bg-purple-400',
|
|
101
|
+
enterprise: 'bg-green-400',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const totalPlanUsers = Object.values(stats.planDistribution).reduce((a, b) => a + b, 0);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="min-h-screen bg-background p-4 sm:p-8">
|
|
108
|
+
<div className="mx-auto max-w-6xl">
|
|
109
|
+
{/* Header */}
|
|
110
|
+
<div className="flex items-center justify-between mb-4">
|
|
111
|
+
<h1 className="text-xl sm:text-2xl font-bold text-text-primary">
|
|
112
|
+
Admin
|
|
113
|
+
</h1>
|
|
114
|
+
<Link
|
|
115
|
+
to={appPath('/')}
|
|
116
|
+
className="flex items-center justify-center rounded-lg p-2 text-text-muted hover:text-text-primary hover:bg-background-secondary"
|
|
117
|
+
aria-label="Close"
|
|
118
|
+
>
|
|
119
|
+
<X size={20} />
|
|
120
|
+
</Link>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Tab Navigation */}
|
|
124
|
+
<div className="flex items-center gap-2 mb-6 sm:mb-8">
|
|
125
|
+
<Link
|
|
126
|
+
to={appPath('/admin')}
|
|
127
|
+
className="flex items-center gap-1.5 rounded-full bg-primary px-4 py-2 text-sm font-medium text-white"
|
|
128
|
+
>
|
|
129
|
+
<LayoutDashboard size={16} />
|
|
130
|
+
Overview
|
|
131
|
+
</Link>
|
|
132
|
+
<Link
|
|
133
|
+
to={appPath('/admin/users')}
|
|
134
|
+
className="flex items-center gap-1.5 rounded-full bg-background-secondary px-4 py-2 text-sm font-medium text-text-secondary hover:bg-background-secondary/80"
|
|
135
|
+
>
|
|
136
|
+
<Users size={16} />
|
|
137
|
+
Users
|
|
138
|
+
</Link>
|
|
139
|
+
{config.teams?.enabled && (
|
|
140
|
+
<Link
|
|
141
|
+
to={appPath('/admin/teams')}
|
|
142
|
+
className="flex items-center gap-1.5 rounded-full bg-background-secondary px-4 py-2 text-sm font-medium text-text-secondary hover:bg-background-secondary/80"
|
|
143
|
+
>
|
|
144
|
+
<Building2 size={16} />
|
|
145
|
+
Teams
|
|
146
|
+
</Link>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{/* Stats Cards */}
|
|
151
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
152
|
+
<StatCard
|
|
153
|
+
title="Total Users"
|
|
154
|
+
value={stats.totalUsers}
|
|
155
|
+
subtitle={`+${stats.newUsersLast30Days} last 30 days`}
|
|
156
|
+
icon={
|
|
157
|
+
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
158
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
159
|
+
</svg>
|
|
160
|
+
}
|
|
161
|
+
/>
|
|
162
|
+
<StatCard
|
|
163
|
+
title="Active Teams"
|
|
164
|
+
value={stats.totalTeams}
|
|
165
|
+
icon={
|
|
166
|
+
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
167
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
|
168
|
+
</svg>
|
|
169
|
+
}
|
|
170
|
+
/>
|
|
171
|
+
<StatCard
|
|
172
|
+
title="Total Threads"
|
|
173
|
+
value={stats.totalThreads}
|
|
174
|
+
icon={
|
|
175
|
+
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
176
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
|
|
177
|
+
</svg>
|
|
178
|
+
}
|
|
179
|
+
/>
|
|
180
|
+
<StatCard
|
|
181
|
+
title="Total Messages"
|
|
182
|
+
value={stats.totalMessages}
|
|
183
|
+
subtitle={`${stats.messagesLast30Days.toLocaleString()} last 30 days`}
|
|
184
|
+
icon={
|
|
185
|
+
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
186
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
187
|
+
</svg>
|
|
188
|
+
}
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{/* Usage Chart */}
|
|
193
|
+
<div className="rounded-lg bg-background-secondary p-6 mb-8">
|
|
194
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
|
195
|
+
<h2 className="text-lg font-semibold text-text-primary">
|
|
196
|
+
Usage Over Time
|
|
197
|
+
</h2>
|
|
198
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
199
|
+
{/* Period selector */}
|
|
200
|
+
<div className="flex rounded-lg bg-background overflow-hidden">
|
|
201
|
+
{[7, 30, 90].map((days) => (
|
|
202
|
+
<button
|
|
203
|
+
key={days}
|
|
204
|
+
onClick={() => setUsagePeriod(days)}
|
|
205
|
+
className={`px-3 py-1.5 text-sm font-medium transition-colors ${
|
|
206
|
+
usagePeriod === days
|
|
207
|
+
? 'bg-primary text-white'
|
|
208
|
+
: 'text-text-muted hover:text-text-primary hover:bg-background-secondary'
|
|
209
|
+
}`}
|
|
210
|
+
>
|
|
211
|
+
{days}d
|
|
212
|
+
</button>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
{/* Metric dropdown */}
|
|
217
|
+
<select
|
|
218
|
+
value={usageMetric}
|
|
219
|
+
onChange={(e) => setUsageMetric(e.target.value as UsageMetric)}
|
|
220
|
+
className="rounded-lg border border-input-border bg-input-background px-3 py-1.5 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-primary/50"
|
|
221
|
+
>
|
|
222
|
+
<option value="messages">Messages</option>
|
|
223
|
+
<option value="inputTokens">Input Tokens</option>
|
|
224
|
+
<option value="outputTokens">Output Tokens</option>
|
|
225
|
+
<option value="totalTokens">Total Tokens</option>
|
|
226
|
+
</select>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<UsageChart data={usage} metric={usageMetric} isLoading={isLoadingUsage} />
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
234
|
+
{/* Plan Distribution */}
|
|
235
|
+
<div className="rounded-lg bg-background-secondary p-6">
|
|
236
|
+
<h2 className="text-lg font-semibold text-text-primary mb-4">
|
|
237
|
+
Plan Distribution
|
|
238
|
+
</h2>
|
|
239
|
+
<div className="space-y-4">
|
|
240
|
+
{Object.entries(stats.planDistribution).map(([plan, count]) => {
|
|
241
|
+
const percentage = totalPlanUsers > 0 ? (count / totalPlanUsers) * 100 : 0;
|
|
242
|
+
return (
|
|
243
|
+
<div key={plan}>
|
|
244
|
+
<div className="flex items-center justify-between mb-1">
|
|
245
|
+
<span className="text-sm text-text-primary capitalize">
|
|
246
|
+
{plan}
|
|
247
|
+
</span>
|
|
248
|
+
<span className="text-sm text-text-muted">
|
|
249
|
+
{count} ({percentage.toFixed(1)}%)
|
|
250
|
+
</span>
|
|
251
|
+
</div>
|
|
252
|
+
<div className="h-2 bg-background rounded-full overflow-hidden">
|
|
253
|
+
<div
|
|
254
|
+
className={`h-full ${planColors[plan] || 'bg-gray-400'}`}
|
|
255
|
+
style={{ width: `${percentage}%` }}
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
})}
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
{/* Feedback Summary */}
|
|
265
|
+
<div className="rounded-lg bg-background-secondary p-6">
|
|
266
|
+
<h2 className="text-lg font-semibold text-text-primary mb-4">
|
|
267
|
+
Feedback Summary
|
|
268
|
+
</h2>
|
|
269
|
+
{feedback && (
|
|
270
|
+
<>
|
|
271
|
+
<div className="flex items-center gap-8 mb-6">
|
|
272
|
+
<div className="flex items-center gap-2">
|
|
273
|
+
<svg className="w-6 h-6 text-success" fill="currentColor" viewBox="0 0 20 20">
|
|
274
|
+
<path d="M2 10.5a1.5 1.5 0 113 0v6a1.5 1.5 0 01-3 0v-6zM6 10.333v5.43a2 2 0 001.106 1.79l.05.025A4 4 0 008.943 18h5.416a2 2 0 001.962-1.608l1.2-6A2 2 0 0015.56 8H12V4a2 2 0 00-2-2 1 1 0 00-1 1v.667a4 4 0 01-.8 2.4L6.8 7.933a4 4 0 00-.8 2.4z" />
|
|
275
|
+
</svg>
|
|
276
|
+
<span className="text-2xl font-bold text-text-primary">
|
|
277
|
+
{feedback.totalUp}
|
|
278
|
+
</span>
|
|
279
|
+
</div>
|
|
280
|
+
<div className="flex items-center gap-2">
|
|
281
|
+
<svg className="w-6 h-6 text-error" fill="currentColor" viewBox="0 0 20 20">
|
|
282
|
+
<path d="M18 9.5a1.5 1.5 0 11-3 0v-6a1.5 1.5 0 013 0v6zM14 9.667v-5.43a2 2 0 00-1.105-1.79l-.05-.025A4 4 0 0011.055 2H5.64a2 2 0 00-1.962 1.608l-1.2 6A2 2 0 004.44 12H8v4a2 2 0 002 2 1 1 0 001-1v-.667a4 4 0 01.8-2.4l1.4-1.866a4 4 0 00.8-2.4z" />
|
|
283
|
+
</svg>
|
|
284
|
+
<span className="text-2xl font-bold text-text-primary">
|
|
285
|
+
{feedback.totalDown}
|
|
286
|
+
</span>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{feedback.recentFeedback.length > 0 && (
|
|
291
|
+
<div>
|
|
292
|
+
<h3 className="text-sm font-medium text-text-muted mb-2">
|
|
293
|
+
Recent Feedback
|
|
294
|
+
</h3>
|
|
295
|
+
<div className="space-y-2">
|
|
296
|
+
{feedback.recentFeedback.map((item) => (
|
|
297
|
+
<div
|
|
298
|
+
key={item.id}
|
|
299
|
+
className="flex items-start gap-3 p-3 rounded-lg bg-background"
|
|
300
|
+
>
|
|
301
|
+
{item.type === 'up' ? (
|
|
302
|
+
<svg className="w-4 h-4 text-success flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
|
303
|
+
<path d="M2 10.5a1.5 1.5 0 113 0v6a1.5 1.5 0 01-3 0v-6zM6 10.333v5.43a2 2 0 001.106 1.79l.05.025A4 4 0 008.943 18h5.416a2 2 0 001.962-1.608l1.2-6A2 2 0 0015.56 8H12V4a2 2 0 00-2-2 1 1 0 00-1 1v.667a4 4 0 01-.8 2.4L6.8 7.933a4 4 0 00-.8 2.4z" />
|
|
304
|
+
</svg>
|
|
305
|
+
) : (
|
|
306
|
+
<svg className="w-4 h-4 text-error flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
|
307
|
+
<path d="M18 9.5a1.5 1.5 0 11-3 0v-6a1.5 1.5 0 013 0v6zM14 9.667v-5.43a2 2 0 00-1.105-1.79l-.05-.025A4 4 0 0011.055 2H5.64a2 2 0 00-1.962 1.608l-1.2 6A2 2 0 004.44 12H8v4a2 2 0 002 2 1 1 0 001-1v-.667a4 4 0 01.8-2.4l1.4-1.866a4 4 0 00.8-2.4z" />
|
|
308
|
+
</svg>
|
|
309
|
+
)}
|
|
310
|
+
<div className="flex-1 min-w-0">
|
|
311
|
+
<p className="text-sm text-text-primary truncate">
|
|
312
|
+
{item.message.content}
|
|
313
|
+
</p>
|
|
314
|
+
{item.comment && (
|
|
315
|
+
<p className="text-xs text-text-muted mt-1">
|
|
316
|
+
"{item.comment}"
|
|
317
|
+
</p>
|
|
318
|
+
)}
|
|
319
|
+
<p className="text-xs text-text-muted mt-1">
|
|
320
|
+
by {item.user.name || item.user.email}
|
|
321
|
+
</p>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
))}
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
</>
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function StatCard({
|
|
338
|
+
title,
|
|
339
|
+
value,
|
|
340
|
+
subtitle,
|
|
341
|
+
icon,
|
|
342
|
+
}: {
|
|
343
|
+
title: string;
|
|
344
|
+
value: number;
|
|
345
|
+
subtitle?: string;
|
|
346
|
+
icon: React.ReactNode;
|
|
347
|
+
}) {
|
|
348
|
+
return (
|
|
349
|
+
<div className="rounded-lg bg-background-secondary p-6">
|
|
350
|
+
<div className="flex items-center justify-between mb-2">
|
|
351
|
+
<span className="text-sm text-text-muted">{title}</span>
|
|
352
|
+
<span className="text-text-muted">{icon}</span>
|
|
353
|
+
</div>
|
|
354
|
+
<div className="text-3xl font-bold text-text-primary">
|
|
355
|
+
{value.toLocaleString()}
|
|
356
|
+
</div>
|
|
357
|
+
{subtitle && (
|
|
358
|
+
<p className="text-sm text-text-muted mt-1">{subtitle}</p>
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { Link, useParams } from 'react-router';
|
|
3
|
+
import { LayoutDashboard, X, Users, Building2 } from 'lucide-react';
|
|
4
|
+
import { useAppPath } from '../hooks/useAppPath';
|
|
5
|
+
import { api } from '../utils/api';
|
|
6
|
+
import type { AdminTeamDetails } from '@chaaskit/shared';
|
|
7
|
+
|
|
8
|
+
export default function AdminTeamPage() {
|
|
9
|
+
const { teamId } = useParams<{ teamId: string }>();
|
|
10
|
+
const appPath = useAppPath();
|
|
11
|
+
const [team, setTeam] = useState<AdminTeamDetails | null>(null);
|
|
12
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
13
|
+
const [error, setError] = useState('');
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
async function loadTeam() {
|
|
17
|
+
if (!teamId) return;
|
|
18
|
+
|
|
19
|
+
setIsLoading(true);
|
|
20
|
+
setError('');
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const response = await api.get<AdminTeamDetails>(`/api/admin/teams/${teamId}`);
|
|
24
|
+
setTeam(response);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
setError(err instanceof Error ? err.message : 'Failed to load team');
|
|
27
|
+
} finally {
|
|
28
|
+
setIsLoading(false);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
loadTeam();
|
|
33
|
+
}, [teamId]);
|
|
34
|
+
|
|
35
|
+
if (isLoading) {
|
|
36
|
+
return (
|
|
37
|
+
<div className="min-h-screen bg-background p-4 sm:p-8">
|
|
38
|
+
<div className="mx-auto max-w-6xl">
|
|
39
|
+
<div className="animate-pulse">
|
|
40
|
+
<div className="h-8 w-48 bg-background-secondary rounded mb-6 sm:mb-8" />
|
|
41
|
+
<div className="h-64 bg-background-secondary rounded" />
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (error || !team) {
|
|
49
|
+
return (
|
|
50
|
+
<div className="min-h-screen bg-background p-4 sm:p-8">
|
|
51
|
+
<div className="mx-auto max-w-6xl">
|
|
52
|
+
{/* Header */}
|
|
53
|
+
<div className="flex items-center justify-between mb-4">
|
|
54
|
+
<h1 className="text-xl sm:text-2xl font-bold text-text-primary">
|
|
55
|
+
Admin
|
|
56
|
+
</h1>
|
|
57
|
+
<Link
|
|
58
|
+
to={appPath('/')}
|
|
59
|
+
className="flex items-center justify-center rounded-lg p-2 text-text-muted hover:text-text-primary hover:bg-background-secondary"
|
|
60
|
+
aria-label="Close"
|
|
61
|
+
>
|
|
62
|
+
<X size={20} />
|
|
63
|
+
</Link>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{/* Tab Navigation */}
|
|
67
|
+
<div className="flex items-center gap-2 mb-6 sm:mb-8">
|
|
68
|
+
<Link
|
|
69
|
+
to={appPath('/admin')}
|
|
70
|
+
className="flex items-center gap-1.5 rounded-full bg-background-secondary px-4 py-2 text-sm font-medium text-text-secondary hover:bg-background-secondary/80"
|
|
71
|
+
>
|
|
72
|
+
<LayoutDashboard size={16} />
|
|
73
|
+
Overview
|
|
74
|
+
</Link>
|
|
75
|
+
<Link
|
|
76
|
+
to={appPath('/admin/users')}
|
|
77
|
+
className="flex items-center gap-1.5 rounded-full bg-background-secondary px-4 py-2 text-sm font-medium text-text-secondary hover:bg-background-secondary/80"
|
|
78
|
+
>
|
|
79
|
+
<Users size={16} />
|
|
80
|
+
Users
|
|
81
|
+
</Link>
|
|
82
|
+
<Link
|
|
83
|
+
to={appPath('/admin/teams')}
|
|
84
|
+
className="flex items-center gap-1.5 rounded-full bg-primary px-4 py-2 text-sm font-medium text-white"
|
|
85
|
+
>
|
|
86
|
+
<Building2 size={16} />
|
|
87
|
+
Teams
|
|
88
|
+
</Link>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="rounded-lg bg-error/10 p-4 text-sm text-error">
|
|
92
|
+
{error || 'Team not found'}
|
|
93
|
+
</div>
|
|
94
|
+
<Link
|
|
95
|
+
to={appPath('/admin/teams')}
|
|
96
|
+
className="mt-4 inline-block text-sm text-primary hover:underline"
|
|
97
|
+
>
|
|
98
|
+
Back to teams
|
|
99
|
+
</Link>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className="min-h-screen bg-background p-4 sm:p-8">
|
|
107
|
+
<div className="mx-auto max-w-6xl">
|
|
108
|
+
{/* Header */}
|
|
109
|
+
<div className="flex items-center justify-between mb-4">
|
|
110
|
+
<h1 className="text-xl sm:text-2xl font-bold text-text-primary">
|
|
111
|
+
Admin
|
|
112
|
+
</h1>
|
|
113
|
+
<Link
|
|
114
|
+
to={appPath('/')}
|
|
115
|
+
className="flex items-center justify-center rounded-lg p-2 text-text-muted hover:text-text-primary hover:bg-background-secondary"
|
|
116
|
+
aria-label="Close"
|
|
117
|
+
>
|
|
118
|
+
<X size={20} />
|
|
119
|
+
</Link>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{/* Tab Navigation */}
|
|
123
|
+
<div className="flex items-center gap-2 mb-6 sm:mb-8">
|
|
124
|
+
<Link
|
|
125
|
+
to={appPath('/admin')}
|
|
126
|
+
className="flex items-center gap-1.5 rounded-full bg-background-secondary px-4 py-2 text-sm font-medium text-text-secondary hover:bg-background-secondary/80"
|
|
127
|
+
>
|
|
128
|
+
<LayoutDashboard size={16} />
|
|
129
|
+
Overview
|
|
130
|
+
</Link>
|
|
131
|
+
<Link
|
|
132
|
+
to={appPath('/admin/users')}
|
|
133
|
+
className="flex items-center gap-1.5 rounded-full bg-background-secondary px-4 py-2 text-sm font-medium text-text-secondary hover:bg-background-secondary/80"
|
|
134
|
+
>
|
|
135
|
+
<Users size={16} />
|
|
136
|
+
Users
|
|
137
|
+
</Link>
|
|
138
|
+
<Link
|
|
139
|
+
to={appPath('/admin/teams')}
|
|
140
|
+
className="flex items-center gap-1.5 rounded-full bg-primary px-4 py-2 text-sm font-medium text-white"
|
|
141
|
+
>
|
|
142
|
+
<Building2 size={16} />
|
|
143
|
+
Teams
|
|
144
|
+
</Link>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{/* Team Header */}
|
|
148
|
+
<div className="flex items-center gap-3 mb-6">
|
|
149
|
+
<div className="w-12 h-12 sm:w-14 sm:h-14 rounded-full bg-primary flex items-center justify-center text-white text-lg sm:text-xl font-medium">
|
|
150
|
+
{team.name[0].toUpperCase()}
|
|
151
|
+
</div>
|
|
152
|
+
<div>
|
|
153
|
+
<h2 className="text-lg sm:text-xl font-bold text-text-primary">
|
|
154
|
+
{team.name}
|
|
155
|
+
</h2>
|
|
156
|
+
{team.archivedAt && (
|
|
157
|
+
<span className="text-xs text-text-muted">(archived)</span>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{/* Team Stats */}
|
|
163
|
+
<div className="grid grid-cols-3 gap-3 sm:gap-4 mb-6">
|
|
164
|
+
<div className="rounded-lg bg-background-secondary p-4 sm:p-6">
|
|
165
|
+
<div className="text-xs sm:text-sm text-text-muted">Members</div>
|
|
166
|
+
<div className="text-xl sm:text-2xl font-bold text-text-primary">
|
|
167
|
+
{team.memberCount}
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
<div className="rounded-lg bg-background-secondary p-4 sm:p-6">
|
|
171
|
+
<div className="text-xs sm:text-sm text-text-muted">Threads</div>
|
|
172
|
+
<div className="text-xl sm:text-2xl font-bold text-text-primary">
|
|
173
|
+
{team.threadCount}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
<div className="rounded-lg bg-background-secondary p-4 sm:p-6">
|
|
177
|
+
<div className="text-xs sm:text-sm text-text-muted">Created</div>
|
|
178
|
+
<div className="text-xl sm:text-2xl font-bold text-text-primary">
|
|
179
|
+
{new Date(team.createdAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}
|
|
180
|
+
</div>
|
|
181
|
+
<div className="text-xs text-text-muted hidden sm:block">
|
|
182
|
+
{new Date(team.createdAt).getFullYear()}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{/* Team Context */}
|
|
188
|
+
{team.context && (
|
|
189
|
+
<div className="mb-6 rounded-lg bg-background-secondary p-4 sm:p-6">
|
|
190
|
+
<h3 className="text-base sm:text-lg font-semibold text-text-primary mb-2">
|
|
191
|
+
Team Context
|
|
192
|
+
</h3>
|
|
193
|
+
<p className="text-sm text-text-secondary whitespace-pre-wrap">
|
|
194
|
+
{team.context}
|
|
195
|
+
</p>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
{/* Members List */}
|
|
200
|
+
<div className="rounded-lg bg-background-secondary overflow-hidden">
|
|
201
|
+
<div className="px-4 sm:px-6 py-3 sm:py-4 bg-background">
|
|
202
|
+
<h3 className="text-base sm:text-lg font-semibold text-text-primary">
|
|
203
|
+
Members ({team.members.length})
|
|
204
|
+
</h3>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div className="divide-y divide-background">
|
|
208
|
+
{team.members.map((member) => (
|
|
209
|
+
<div
|
|
210
|
+
key={member.id}
|
|
211
|
+
className="px-4 sm:px-6 py-3 hover:bg-background/50"
|
|
212
|
+
>
|
|
213
|
+
{/* Desktop View */}
|
|
214
|
+
<div className="hidden sm:flex items-center justify-between">
|
|
215
|
+
<div className="flex items-center gap-3">
|
|
216
|
+
{member.avatarUrl ? (
|
|
217
|
+
<img
|
|
218
|
+
src={member.avatarUrl}
|
|
219
|
+
alt=""
|
|
220
|
+
className="w-10 h-10 rounded-full"
|
|
221
|
+
/>
|
|
222
|
+
) : (
|
|
223
|
+
<div className="w-10 h-10 rounded-full bg-primary flex items-center justify-center text-white font-medium">
|
|
224
|
+
{(member.name || member.email)[0].toUpperCase()}
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
<div>
|
|
228
|
+
<Link
|
|
229
|
+
to={`/admin/users?search=${encodeURIComponent(member.email)}`}
|
|
230
|
+
className="text-sm font-medium text-text-primary hover:text-primary"
|
|
231
|
+
>
|
|
232
|
+
{member.name || member.email}
|
|
233
|
+
</Link>
|
|
234
|
+
{member.name && (
|
|
235
|
+
<div className="text-xs text-text-muted">
|
|
236
|
+
{member.email}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
<div className="flex items-center gap-4">
|
|
242
|
+
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
|
243
|
+
member.role === 'owner'
|
|
244
|
+
? 'bg-primary/10 text-primary'
|
|
245
|
+
: member.role === 'admin'
|
|
246
|
+
? 'bg-warning/10 text-warning'
|
|
247
|
+
: 'bg-background text-text-muted'
|
|
248
|
+
}`}>
|
|
249
|
+
{member.role}
|
|
250
|
+
</span>
|
|
251
|
+
<span className="text-xs text-text-muted">
|
|
252
|
+
Joined {new Date(member.joinedAt).toLocaleDateString()}
|
|
253
|
+
</span>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
{/* Mobile View */}
|
|
258
|
+
<div className="sm:hidden">
|
|
259
|
+
<div className="flex items-center justify-between">
|
|
260
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
261
|
+
{member.avatarUrl ? (
|
|
262
|
+
<img
|
|
263
|
+
src={member.avatarUrl}
|
|
264
|
+
alt=""
|
|
265
|
+
className="w-10 h-10 rounded-full flex-shrink-0"
|
|
266
|
+
/>
|
|
267
|
+
) : (
|
|
268
|
+
<div className="w-10 h-10 rounded-full bg-primary flex items-center justify-center text-white font-medium flex-shrink-0">
|
|
269
|
+
{(member.name || member.email)[0].toUpperCase()}
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
<div className="min-w-0">
|
|
273
|
+
<Link
|
|
274
|
+
to={`/admin/users?search=${encodeURIComponent(member.email)}`}
|
|
275
|
+
className="text-sm font-medium text-text-primary hover:text-primary truncate block"
|
|
276
|
+
>
|
|
277
|
+
{member.name || member.email}
|
|
278
|
+
</Link>
|
|
279
|
+
<div className="flex items-center gap-2 text-xs text-text-muted">
|
|
280
|
+
<span className={`px-1.5 py-0.5 rounded font-medium ${
|
|
281
|
+
member.role === 'owner'
|
|
282
|
+
? 'bg-primary/10 text-primary'
|
|
283
|
+
: member.role === 'admin'
|
|
284
|
+
? 'bg-warning/10 text-warning'
|
|
285
|
+
: 'bg-background text-text-muted'
|
|
286
|
+
}`}>
|
|
287
|
+
{member.role}
|
|
288
|
+
</span>
|
|
289
|
+
<span>
|
|
290
|
+
{new Date(member.joinedAt).toLocaleDateString()}
|
|
291
|
+
</span>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
))}
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
}
|