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