@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,1085 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useParams, useNavigate, Link, Navigate, useSearchParams } from 'react-router';
|
|
3
|
+
import { CreditCard, ExternalLink, Loader2, X, MessageSquare } from 'lucide-react';
|
|
4
|
+
import { useTeam } from '../contexts/TeamContext';
|
|
5
|
+
import { useAuth } from '../contexts/AuthContext';
|
|
6
|
+
import { useConfig } from '../contexts/ConfigContext';
|
|
7
|
+
import { useAppPath } from '../hooks/useAppPath';
|
|
8
|
+
import { api } from '../utils/api';
|
|
9
|
+
import type { TeamRole, TeamStats, TeamActivityItem, TeamStatsResponse } from '@chaaskit/shared';
|
|
10
|
+
|
|
11
|
+
interface SlackIntegrationStatus {
|
|
12
|
+
connected: boolean;
|
|
13
|
+
integration?: {
|
|
14
|
+
id: string;
|
|
15
|
+
workspaceId: string;
|
|
16
|
+
workspaceName: string;
|
|
17
|
+
notificationChannel: string | null;
|
|
18
|
+
aiChatEnabled: boolean;
|
|
19
|
+
status: string;
|
|
20
|
+
statusMessage: string | null;
|
|
21
|
+
installedAt: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TeamSubscription {
|
|
26
|
+
plan: string;
|
|
27
|
+
planName: string;
|
|
28
|
+
messagesThisMonth: number;
|
|
29
|
+
monthlyLimit: number;
|
|
30
|
+
credits: number;
|
|
31
|
+
hasStripeCustomer: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function TeamSettingsPage() {
|
|
35
|
+
const { teamId } = useParams<{ teamId: string }>();
|
|
36
|
+
const navigate = useNavigate();
|
|
37
|
+
const appPath = useAppPath();
|
|
38
|
+
const [searchParams] = useSearchParams();
|
|
39
|
+
const { user } = useAuth();
|
|
40
|
+
const config = useConfig();
|
|
41
|
+
const paymentsEnabled = config.payments?.enabled ?? false;
|
|
42
|
+
const {
|
|
43
|
+
teams,
|
|
44
|
+
currentTeam,
|
|
45
|
+
isLoadingTeamDetails,
|
|
46
|
+
loadTeamDetails,
|
|
47
|
+
updateTeam,
|
|
48
|
+
archiveTeam,
|
|
49
|
+
inviteMember,
|
|
50
|
+
removeMember,
|
|
51
|
+
updateMemberRole,
|
|
52
|
+
cancelInvite,
|
|
53
|
+
leaveTeam,
|
|
54
|
+
} = useTeam();
|
|
55
|
+
|
|
56
|
+
const [teamName, setTeamName] = useState('');
|
|
57
|
+
const [teamContext, setTeamContext] = useState('');
|
|
58
|
+
const [inviteEmail, setInviteEmail] = useState('');
|
|
59
|
+
const [inviteRole, setInviteRole] = useState<'admin' | 'member' | 'viewer'>('member');
|
|
60
|
+
const [inviteUrl, setInviteUrl] = useState('');
|
|
61
|
+
const [error, setError] = useState('');
|
|
62
|
+
const [success, setSuccess] = useState('');
|
|
63
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
64
|
+
const [isSavingContext, setIsSavingContext] = useState(false);
|
|
65
|
+
const [stats, setStats] = useState<TeamStats | null>(null);
|
|
66
|
+
const [activity, setActivity] = useState<TeamActivityItem[]>([]);
|
|
67
|
+
const [isLoadingStats, setIsLoadingStats] = useState(true);
|
|
68
|
+
const [subscription, setSubscription] = useState<TeamSubscription | null>(null);
|
|
69
|
+
const [isLoadingSubscription, setIsLoadingSubscription] = useState(true);
|
|
70
|
+
const [isBillingLoading, setIsBillingLoading] = useState(false);
|
|
71
|
+
|
|
72
|
+
// Slack integration state
|
|
73
|
+
const [slackStatus, setSlackStatus] = useState<SlackIntegrationStatus | null>(null);
|
|
74
|
+
const [isLoadingSlack, setIsLoadingSlack] = useState(true);
|
|
75
|
+
const [slackAiChatEnabled, setSlackAiChatEnabled] = useState(true);
|
|
76
|
+
const [slackNotificationChannel, setSlackNotificationChannel] = useState('');
|
|
77
|
+
const [isSavingSlack, setIsSavingSlack] = useState(false);
|
|
78
|
+
const [isDisconnectingSlack, setIsDisconnectingSlack] = useState(false);
|
|
79
|
+
|
|
80
|
+
const currentTeamRole = teams.find((t) => t.id === teamId)?.role;
|
|
81
|
+
const isOwner = currentTeamRole === 'owner';
|
|
82
|
+
const isAdmin = currentTeamRole === 'owner' || currentTeamRole === 'admin';
|
|
83
|
+
const teamsEnabled = config.teams?.enabled ?? false;
|
|
84
|
+
const slackEnabled = (config as unknown as { slack?: { enabled: boolean } }).slack?.enabled ?? false;
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (teamId && teamsEnabled) {
|
|
88
|
+
loadTeamDetails(teamId);
|
|
89
|
+
}
|
|
90
|
+
}, [teamId, teamsEnabled, loadTeamDetails]);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (currentTeam) {
|
|
94
|
+
setTeamName(currentTeam.name);
|
|
95
|
+
setTeamContext(currentTeam.context || '');
|
|
96
|
+
}
|
|
97
|
+
}, [currentTeam]);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (teamId && teamsEnabled) {
|
|
101
|
+
setIsLoadingStats(true);
|
|
102
|
+
api.get<TeamStatsResponse>(`/api/teams/${teamId}/stats`)
|
|
103
|
+
.then(res => {
|
|
104
|
+
setStats(res.stats);
|
|
105
|
+
setActivity(res.recentActivity);
|
|
106
|
+
})
|
|
107
|
+
.catch(() => {
|
|
108
|
+
// Silently fail - stats are not critical
|
|
109
|
+
})
|
|
110
|
+
.finally(() => setIsLoadingStats(false));
|
|
111
|
+
}
|
|
112
|
+
}, [teamId, teamsEnabled]);
|
|
113
|
+
|
|
114
|
+
// Load team subscription data
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (teamId && teamsEnabled && paymentsEnabled) {
|
|
117
|
+
setIsLoadingSubscription(true);
|
|
118
|
+
api.get<TeamSubscription>(`/api/payments/team/${teamId}/subscription`)
|
|
119
|
+
.then(setSubscription)
|
|
120
|
+
.catch(() => {
|
|
121
|
+
// Silently fail - subscription may not exist
|
|
122
|
+
})
|
|
123
|
+
.finally(() => setIsLoadingSubscription(false));
|
|
124
|
+
} else {
|
|
125
|
+
setIsLoadingSubscription(false);
|
|
126
|
+
}
|
|
127
|
+
}, [teamId, teamsEnabled, paymentsEnabled]);
|
|
128
|
+
|
|
129
|
+
// Load Slack integration status
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (teamId && teamsEnabled && slackEnabled) {
|
|
132
|
+
setIsLoadingSlack(true);
|
|
133
|
+
api.get<SlackIntegrationStatus>(`/api/slack/${teamId}/status`)
|
|
134
|
+
.then(data => {
|
|
135
|
+
setSlackStatus(data);
|
|
136
|
+
if (data.integration) {
|
|
137
|
+
setSlackAiChatEnabled(data.integration.aiChatEnabled);
|
|
138
|
+
setSlackNotificationChannel(data.integration.notificationChannel || '');
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
.catch(() => {
|
|
142
|
+
setSlackStatus({ connected: false });
|
|
143
|
+
})
|
|
144
|
+
.finally(() => setIsLoadingSlack(false));
|
|
145
|
+
} else {
|
|
146
|
+
setIsLoadingSlack(false);
|
|
147
|
+
setSlackStatus(null);
|
|
148
|
+
}
|
|
149
|
+
}, [teamId, teamsEnabled, slackEnabled]);
|
|
150
|
+
|
|
151
|
+
// Check for Slack connection status in URL params
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
const slackParam = searchParams.get('slack');
|
|
154
|
+
if (slackParam === 'connected') {
|
|
155
|
+
setSuccess('Slack connected successfully!');
|
|
156
|
+
// Reload slack status
|
|
157
|
+
if (teamId) {
|
|
158
|
+
api.get<SlackIntegrationStatus>(`/api/slack/${teamId}/status`)
|
|
159
|
+
.then(data => {
|
|
160
|
+
setSlackStatus(data);
|
|
161
|
+
if (data.integration) {
|
|
162
|
+
setSlackAiChatEnabled(data.integration.aiChatEnabled);
|
|
163
|
+
setSlackNotificationChannel(data.integration.notificationChannel || '');
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
.catch(() => {});
|
|
167
|
+
}
|
|
168
|
+
} else if (slackParam === 'error') {
|
|
169
|
+
const message = searchParams.get('message');
|
|
170
|
+
setError(`Failed to connect Slack: ${message || 'Unknown error'}`);
|
|
171
|
+
}
|
|
172
|
+
}, [searchParams, teamId]);
|
|
173
|
+
|
|
174
|
+
// Check for payment success/failure in URL params
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
const payment = searchParams.get('payment');
|
|
177
|
+
const credits = searchParams.get('credits');
|
|
178
|
+
|
|
179
|
+
if (payment === 'success') {
|
|
180
|
+
setSuccess('Plan upgraded successfully!');
|
|
181
|
+
// Reload subscription data
|
|
182
|
+
if (teamId) {
|
|
183
|
+
api.get<TeamSubscription>(`/api/payments/team/${teamId}/subscription`)
|
|
184
|
+
.then(setSubscription)
|
|
185
|
+
.catch(() => {});
|
|
186
|
+
}
|
|
187
|
+
} else if (payment === 'cancelled') {
|
|
188
|
+
setError('Payment was cancelled');
|
|
189
|
+
} else if (credits === 'success') {
|
|
190
|
+
setSuccess('Credits purchased successfully!');
|
|
191
|
+
// Reload subscription data
|
|
192
|
+
if (teamId) {
|
|
193
|
+
api.get<TeamSubscription>(`/api/payments/team/${teamId}/subscription`)
|
|
194
|
+
.then(setSubscription)
|
|
195
|
+
.catch(() => {});
|
|
196
|
+
}
|
|
197
|
+
} else if (credits === 'cancelled') {
|
|
198
|
+
setError('Credits purchase was cancelled');
|
|
199
|
+
}
|
|
200
|
+
}, [searchParams, teamId]);
|
|
201
|
+
|
|
202
|
+
// Redirect if teams feature is disabled
|
|
203
|
+
if (!teamsEnabled) {
|
|
204
|
+
return <Navigate to={appPath('/')} replace />;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const handleUpdateTeam = async (e: React.FormEvent) => {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
if (!teamId) return;
|
|
210
|
+
|
|
211
|
+
setError('');
|
|
212
|
+
setSuccess('');
|
|
213
|
+
setIsSubmitting(true);
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
await updateTeam(teamId, { name: teamName });
|
|
217
|
+
setSuccess('Team name updated successfully');
|
|
218
|
+
} catch (err) {
|
|
219
|
+
setError(err instanceof Error ? err.message : 'Failed to update team');
|
|
220
|
+
} finally {
|
|
221
|
+
setIsSubmitting(false);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const handleUpdateContext = async (e: React.FormEvent) => {
|
|
226
|
+
e.preventDefault();
|
|
227
|
+
if (!teamId) return;
|
|
228
|
+
|
|
229
|
+
setError('');
|
|
230
|
+
setSuccess('');
|
|
231
|
+
setIsSavingContext(true);
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
await updateTeam(teamId, { context: teamContext || null });
|
|
235
|
+
setSuccess('Team context updated successfully');
|
|
236
|
+
} catch (err) {
|
|
237
|
+
setError(err instanceof Error ? err.message : 'Failed to update team context');
|
|
238
|
+
} finally {
|
|
239
|
+
setIsSavingContext(false);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const handleInvite = async (e: React.FormEvent) => {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
if (!teamId) return;
|
|
246
|
+
|
|
247
|
+
setError('');
|
|
248
|
+
setSuccess('');
|
|
249
|
+
setInviteUrl('');
|
|
250
|
+
setIsSubmitting(true);
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const result = await inviteMember(teamId, inviteEmail, inviteRole);
|
|
254
|
+
setInviteUrl(result.inviteUrl);
|
|
255
|
+
setInviteEmail('');
|
|
256
|
+
setSuccess('Invite sent successfully');
|
|
257
|
+
} catch (err) {
|
|
258
|
+
setError(err instanceof Error ? err.message : 'Failed to send invite');
|
|
259
|
+
} finally {
|
|
260
|
+
setIsSubmitting(false);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const handleRemoveMember = async (userId: string) => {
|
|
265
|
+
if (!teamId) return;
|
|
266
|
+
if (!confirm('Are you sure you want to remove this member?')) return;
|
|
267
|
+
|
|
268
|
+
setError('');
|
|
269
|
+
setSuccess('');
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
await removeMember(teamId, userId);
|
|
273
|
+
setSuccess('Member removed successfully');
|
|
274
|
+
} catch (err) {
|
|
275
|
+
setError(err instanceof Error ? err.message : 'Failed to remove member');
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const handleUpdateRole = async (userId: string, role: TeamRole) => {
|
|
280
|
+
if (!teamId) return;
|
|
281
|
+
|
|
282
|
+
setError('');
|
|
283
|
+
setSuccess('');
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
await updateMemberRole(teamId, userId, role);
|
|
287
|
+
setSuccess('Role updated successfully');
|
|
288
|
+
} catch (err) {
|
|
289
|
+
setError(err instanceof Error ? err.message : 'Failed to update role');
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const handleCancelInvite = async (inviteId: string) => {
|
|
294
|
+
if (!teamId) return;
|
|
295
|
+
if (!confirm('Are you sure you want to cancel this invite?')) return;
|
|
296
|
+
|
|
297
|
+
setError('');
|
|
298
|
+
setSuccess('');
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
await cancelInvite(teamId, inviteId);
|
|
302
|
+
setSuccess('Invite cancelled');
|
|
303
|
+
} catch (err) {
|
|
304
|
+
setError(err instanceof Error ? err.message : 'Failed to cancel invite');
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const handleArchiveTeam = async () => {
|
|
309
|
+
if (!teamId) return;
|
|
310
|
+
if (!confirm('Are you sure you want to archive this team? Team threads will be preserved but the team will be hidden.')) return;
|
|
311
|
+
|
|
312
|
+
setError('');
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
await archiveTeam(teamId);
|
|
316
|
+
navigate(appPath('/'));
|
|
317
|
+
} catch (err) {
|
|
318
|
+
setError(err instanceof Error ? err.message : 'Failed to archive team');
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const handleLeaveTeam = async () => {
|
|
323
|
+
if (!teamId) return;
|
|
324
|
+
if (!confirm('Are you sure you want to leave this team?')) return;
|
|
325
|
+
|
|
326
|
+
setError('');
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
await leaveTeam(teamId);
|
|
330
|
+
navigate(appPath('/'));
|
|
331
|
+
} catch (err) {
|
|
332
|
+
setError(err instanceof Error ? err.message : 'Failed to leave team');
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const copyInviteUrl = () => {
|
|
337
|
+
navigator.clipboard.writeText(inviteUrl);
|
|
338
|
+
setSuccess('Invite link copied to clipboard');
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const handleConnectSlack = () => {
|
|
342
|
+
// Redirect to Slack OAuth install
|
|
343
|
+
window.location.href = `/api/slack/install/${teamId}`;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const handleDisconnectSlack = async () => {
|
|
347
|
+
if (!teamId) return;
|
|
348
|
+
if (!confirm('Are you sure you want to disconnect Slack? You will need to reconnect to use Slack features.')) return;
|
|
349
|
+
|
|
350
|
+
setError('');
|
|
351
|
+
setIsDisconnectingSlack(true);
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
await api.delete(`/api/slack/${teamId}`);
|
|
355
|
+
setSlackStatus({ connected: false });
|
|
356
|
+
setSuccess('Slack disconnected successfully');
|
|
357
|
+
} catch (err) {
|
|
358
|
+
setError(err instanceof Error ? err.message : 'Failed to disconnect Slack');
|
|
359
|
+
} finally {
|
|
360
|
+
setIsDisconnectingSlack(false);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const handleSaveSlackSettings = async () => {
|
|
365
|
+
if (!teamId) return;
|
|
366
|
+
|
|
367
|
+
setError('');
|
|
368
|
+
setIsSavingSlack(true);
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
await api.patch(`/api/slack/${teamId}/settings`, {
|
|
372
|
+
aiChatEnabled: slackAiChatEnabled,
|
|
373
|
+
notificationChannel: slackNotificationChannel || null,
|
|
374
|
+
});
|
|
375
|
+
setSuccess('Slack settings updated');
|
|
376
|
+
} catch (err) {
|
|
377
|
+
setError(err instanceof Error ? err.message : 'Failed to update Slack settings');
|
|
378
|
+
} finally {
|
|
379
|
+
setIsSavingSlack(false);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const handleUpgradeTeamPlan = () => {
|
|
384
|
+
navigate(appPath(`/pricing?teamId=${teamId}`));
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const handleOpenTeamBillingPortal = async () => {
|
|
388
|
+
if (!teamId) return;
|
|
389
|
+
setIsBillingLoading(true);
|
|
390
|
+
try {
|
|
391
|
+
const response = await fetch(`/api/payments/team/${teamId}/billing-portal`, {
|
|
392
|
+
method: 'POST',
|
|
393
|
+
credentials: 'include',
|
|
394
|
+
});
|
|
395
|
+
if (response.ok) {
|
|
396
|
+
const { url } = await response.json();
|
|
397
|
+
window.location.href = url;
|
|
398
|
+
} else {
|
|
399
|
+
const data = await response.json();
|
|
400
|
+
setError(data.error || 'Failed to open billing portal');
|
|
401
|
+
}
|
|
402
|
+
} catch (err) {
|
|
403
|
+
setError(err instanceof Error ? err.message : 'Failed to open billing portal');
|
|
404
|
+
} finally {
|
|
405
|
+
setIsBillingLoading(false);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const formatRelativeTime = (date: Date | string) => {
|
|
410
|
+
const now = new Date();
|
|
411
|
+
const then = new Date(date);
|
|
412
|
+
const diffMs = now.getTime() - then.getTime();
|
|
413
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
414
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
415
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
416
|
+
|
|
417
|
+
if (diffMins < 1) return 'Just now';
|
|
418
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
419
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
420
|
+
if (diffDays === 1) return 'Yesterday';
|
|
421
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
422
|
+
return then.toLocaleDateString();
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
if (isLoadingTeamDetails || !currentTeam) {
|
|
426
|
+
return (
|
|
427
|
+
<div className="min-h-screen bg-background p-4 sm:p-8">
|
|
428
|
+
<div className="mx-auto max-w-3xl">
|
|
429
|
+
<div className="animate-pulse">
|
|
430
|
+
<div className="h-8 w-48 bg-background-secondary rounded mb-6 sm:mb-8" />
|
|
431
|
+
<div className="h-64 bg-background-secondary rounded" />
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<div className="min-h-screen bg-background p-4 sm:p-8">
|
|
440
|
+
<div className="mx-auto max-w-3xl">
|
|
441
|
+
<div className="flex items-center justify-between mb-6 sm:mb-8">
|
|
442
|
+
<h1 className="text-xl sm:text-2xl font-bold text-text-primary">
|
|
443
|
+
Team Settings
|
|
444
|
+
</h1>
|
|
445
|
+
<Link
|
|
446
|
+
to={appPath('/')}
|
|
447
|
+
className="flex items-center justify-center rounded-lg p-2 text-text-muted hover:text-text-primary hover:bg-background-secondary"
|
|
448
|
+
aria-label="Close"
|
|
449
|
+
>
|
|
450
|
+
<X size={20} />
|
|
451
|
+
</Link>
|
|
452
|
+
</div>
|
|
453
|
+
|
|
454
|
+
{error && (
|
|
455
|
+
<div className="mb-6 rounded-lg bg-error/10 p-4 text-sm text-error">
|
|
456
|
+
{error}
|
|
457
|
+
</div>
|
|
458
|
+
)}
|
|
459
|
+
|
|
460
|
+
{success && (
|
|
461
|
+
<div className="mb-6 rounded-lg bg-success/10 p-4 text-sm text-success">
|
|
462
|
+
{success}
|
|
463
|
+
</div>
|
|
464
|
+
)}
|
|
465
|
+
|
|
466
|
+
{/* Team Stats */}
|
|
467
|
+
<div className="mb-6">
|
|
468
|
+
{isLoadingStats ? (
|
|
469
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
470
|
+
{[...Array(4)].map((_, i) => (
|
|
471
|
+
<div key={i} className="rounded-lg bg-background-secondary p-6 animate-pulse">
|
|
472
|
+
<div className="h-4 w-20 bg-background rounded mb-2" />
|
|
473
|
+
<div className="h-8 w-12 bg-background rounded" />
|
|
474
|
+
</div>
|
|
475
|
+
))}
|
|
476
|
+
</div>
|
|
477
|
+
) : stats && (
|
|
478
|
+
<>
|
|
479
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 mb-6">
|
|
480
|
+
<div className="rounded-lg bg-background-secondary p-4 sm:p-6">
|
|
481
|
+
<div className="text-xs sm:text-sm text-text-muted mb-1">Threads</div>
|
|
482
|
+
<div className="text-xl sm:text-2xl font-semibold text-text-primary">{stats.totalThreads}</div>
|
|
483
|
+
{stats.threadsThisMonth > 0 && (
|
|
484
|
+
<div className="text-xs text-text-muted mt-1">+{stats.threadsThisMonth} this month</div>
|
|
485
|
+
)}
|
|
486
|
+
</div>
|
|
487
|
+
<div className="rounded-lg bg-background-secondary p-4 sm:p-6">
|
|
488
|
+
<div className="text-xs sm:text-sm text-text-muted mb-1">Messages</div>
|
|
489
|
+
<div className="text-xl sm:text-2xl font-semibold text-text-primary">{stats.totalMessages}</div>
|
|
490
|
+
</div>
|
|
491
|
+
<div className="rounded-lg bg-background-secondary p-4 sm:p-6">
|
|
492
|
+
<div className="text-xs sm:text-sm text-text-muted mb-1">This Month</div>
|
|
493
|
+
<div className="text-xl sm:text-2xl font-semibold text-text-primary">{stats.messagesThisMonth}</div>
|
|
494
|
+
<div className="text-xs text-text-muted mt-1">messages</div>
|
|
495
|
+
</div>
|
|
496
|
+
<div className="rounded-lg bg-background-secondary p-4 sm:p-6">
|
|
497
|
+
<div className="text-xs sm:text-sm text-text-muted mb-1">Members</div>
|
|
498
|
+
<div className="text-xl sm:text-2xl font-semibold text-text-primary">{stats.memberCount}</div>
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
502
|
+
{/* Recent Activity */}
|
|
503
|
+
<div className="rounded-lg bg-background-secondary p-4 sm:p-6">
|
|
504
|
+
<h2 className="text-base sm:text-lg font-semibold text-text-primary mb-4">Recent Activity</h2>
|
|
505
|
+
{activity.length === 0 ? (
|
|
506
|
+
<div className="text-text-muted text-sm">No recent activity</div>
|
|
507
|
+
) : (
|
|
508
|
+
<div className="space-y-2">
|
|
509
|
+
{activity.map((item, index) => (
|
|
510
|
+
<div
|
|
511
|
+
key={`${item.type}-${item.timestamp}-${index}`}
|
|
512
|
+
className="flex flex-col sm:flex-row sm:items-center sm:justify-between p-3 rounded-lg bg-background gap-2"
|
|
513
|
+
>
|
|
514
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
515
|
+
{item.type === 'thread_created' ? (
|
|
516
|
+
<svg className="w-5 h-5 flex-shrink-0 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
517
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
|
518
|
+
</svg>
|
|
519
|
+
) : (
|
|
520
|
+
<svg className="w-5 h-5 flex-shrink-0 text-success" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
521
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
|
|
522
|
+
</svg>
|
|
523
|
+
)}
|
|
524
|
+
<div className="text-sm truncate">
|
|
525
|
+
<span className="text-text-primary">{item.user.name || item.user.email}</span>
|
|
526
|
+
<span className="text-text-muted">
|
|
527
|
+
{item.type === 'thread_created'
|
|
528
|
+
? ` created "${item.details}"`
|
|
529
|
+
: ' joined the team'}
|
|
530
|
+
</span>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
<div className="text-xs sm:text-sm text-text-muted flex-shrink-0 pl-8 sm:pl-0">
|
|
534
|
+
{formatRelativeTime(item.timestamp)}
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
))}
|
|
538
|
+
</div>
|
|
539
|
+
)}
|
|
540
|
+
</div>
|
|
541
|
+
</>
|
|
542
|
+
)}
|
|
543
|
+
</div>
|
|
544
|
+
|
|
545
|
+
{/* Team Plan (Billing) */}
|
|
546
|
+
{paymentsEnabled && isAdmin && (
|
|
547
|
+
<div className="mb-6 rounded-lg bg-background-secondary p-4 sm:p-6">
|
|
548
|
+
<div className="flex items-center gap-2 mb-4">
|
|
549
|
+
<CreditCard className="w-5 h-5 text-primary" />
|
|
550
|
+
<h2 className="text-base sm:text-lg font-semibold text-text-primary">Team Plan</h2>
|
|
551
|
+
</div>
|
|
552
|
+
|
|
553
|
+
{isLoadingSubscription ? (
|
|
554
|
+
<div className="flex items-center justify-center py-8">
|
|
555
|
+
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
|
556
|
+
</div>
|
|
557
|
+
) : subscription ? (
|
|
558
|
+
<>
|
|
559
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mb-4">
|
|
560
|
+
<div className="text-sm">
|
|
561
|
+
<span className="text-text-secondary">Current Plan: </span>
|
|
562
|
+
<span className="font-medium text-text-primary">{subscription.planName}</span>
|
|
563
|
+
</div>
|
|
564
|
+
<span className="self-start sm:self-auto rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary">
|
|
565
|
+
{subscription.plan}
|
|
566
|
+
</span>
|
|
567
|
+
</div>
|
|
568
|
+
|
|
569
|
+
{/* Usage Progress */}
|
|
570
|
+
<div className="space-y-2 mb-4">
|
|
571
|
+
<div className="flex justify-between text-sm">
|
|
572
|
+
<span className="text-text-secondary">Messages this month</span>
|
|
573
|
+
<span className="text-text-primary">
|
|
574
|
+
{subscription.messagesThisMonth.toLocaleString()}
|
|
575
|
+
{subscription.monthlyLimit > 0 ? (
|
|
576
|
+
<span className="text-text-muted"> / {subscription.monthlyLimit.toLocaleString()}</span>
|
|
577
|
+
) : (
|
|
578
|
+
<span className="text-text-muted"> / Unlimited</span>
|
|
579
|
+
)}
|
|
580
|
+
</span>
|
|
581
|
+
</div>
|
|
582
|
+
|
|
583
|
+
{subscription.monthlyLimit > 0 && (
|
|
584
|
+
<div className="h-2 overflow-hidden rounded-full bg-background">
|
|
585
|
+
<div
|
|
586
|
+
className={`h-full rounded-full transition-all ${
|
|
587
|
+
subscription.messagesThisMonth / subscription.monthlyLimit > 0.9
|
|
588
|
+
? 'bg-error'
|
|
589
|
+
: subscription.messagesThisMonth / subscription.monthlyLimit > 0.7
|
|
590
|
+
? 'bg-warning'
|
|
591
|
+
: 'bg-primary'
|
|
592
|
+
}`}
|
|
593
|
+
style={{
|
|
594
|
+
width: `${Math.min(100, (subscription.messagesThisMonth / subscription.monthlyLimit) * 100)}%`,
|
|
595
|
+
}}
|
|
596
|
+
/>
|
|
597
|
+
</div>
|
|
598
|
+
)}
|
|
599
|
+
|
|
600
|
+
{subscription.credits > 0 && (
|
|
601
|
+
<div className="flex justify-between text-sm mt-2">
|
|
602
|
+
<span className="text-text-secondary">Credits remaining</span>
|
|
603
|
+
<span className="text-text-primary">{subscription.credits.toLocaleString()}</span>
|
|
604
|
+
</div>
|
|
605
|
+
)}
|
|
606
|
+
</div>
|
|
607
|
+
|
|
608
|
+
{/* Billing Actions */}
|
|
609
|
+
<div className="flex flex-wrap gap-2">
|
|
610
|
+
<button
|
|
611
|
+
onClick={handleUpgradeTeamPlan}
|
|
612
|
+
className="flex items-center gap-1.5 rounded-lg bg-primary px-3 py-2 text-sm font-medium text-white hover:bg-primary-hover"
|
|
613
|
+
>
|
|
614
|
+
<CreditCard size={14} />
|
|
615
|
+
{subscription.plan === 'free' ? 'Upgrade Team Plan' : 'Change Plan'}
|
|
616
|
+
</button>
|
|
617
|
+
{subscription.hasStripeCustomer && (
|
|
618
|
+
<button
|
|
619
|
+
onClick={handleOpenTeamBillingPortal}
|
|
620
|
+
disabled={isBillingLoading}
|
|
621
|
+
className="flex items-center gap-1.5 rounded-lg border border-border bg-background px-3 py-2 text-sm font-medium text-text-secondary hover:text-text-primary hover:bg-background-secondary disabled:opacity-50"
|
|
622
|
+
>
|
|
623
|
+
{isBillingLoading ? (
|
|
624
|
+
<Loader2 size={14} className="animate-spin" />
|
|
625
|
+
) : (
|
|
626
|
+
<ExternalLink size={14} />
|
|
627
|
+
)}
|
|
628
|
+
Manage Subscription
|
|
629
|
+
</button>
|
|
630
|
+
)}
|
|
631
|
+
</div>
|
|
632
|
+
|
|
633
|
+
<p className="mt-4 text-xs text-text-muted">
|
|
634
|
+
Team plans provide a shared message pool for all team members.
|
|
635
|
+
Personal plans still apply to personal chats.
|
|
636
|
+
</p>
|
|
637
|
+
</>
|
|
638
|
+
) : (
|
|
639
|
+
<div className="text-center py-4">
|
|
640
|
+
<p className="text-text-secondary mb-4">
|
|
641
|
+
Your team is on the free plan. Upgrade to get a shared message pool for all members.
|
|
642
|
+
</p>
|
|
643
|
+
<button
|
|
644
|
+
onClick={handleUpgradeTeamPlan}
|
|
645
|
+
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover"
|
|
646
|
+
>
|
|
647
|
+
Upgrade Team Plan
|
|
648
|
+
</button>
|
|
649
|
+
</div>
|
|
650
|
+
)}
|
|
651
|
+
</div>
|
|
652
|
+
)}
|
|
653
|
+
|
|
654
|
+
{/* Slack Integration */}
|
|
655
|
+
{slackEnabled && isAdmin && (
|
|
656
|
+
<div className="mb-6 rounded-lg bg-background-secondary p-4 sm:p-6">
|
|
657
|
+
<div className="flex items-center gap-2 mb-4">
|
|
658
|
+
<MessageSquare className="w-5 h-5 text-primary" />
|
|
659
|
+
<h2 className="text-base sm:text-lg font-semibold text-text-primary">Slack Integration</h2>
|
|
660
|
+
</div>
|
|
661
|
+
|
|
662
|
+
{isLoadingSlack ? (
|
|
663
|
+
<div className="flex items-center justify-center py-8">
|
|
664
|
+
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
|
665
|
+
</div>
|
|
666
|
+
) : slackStatus?.connected ? (
|
|
667
|
+
<>
|
|
668
|
+
{/* Connected state */}
|
|
669
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 mb-4">
|
|
670
|
+
<div className="text-sm">
|
|
671
|
+
<span className="text-text-secondary">Connected to: </span>
|
|
672
|
+
<span className="font-medium text-text-primary">{slackStatus.integration?.workspaceName}</span>
|
|
673
|
+
</div>
|
|
674
|
+
<span className="self-start sm:self-auto rounded-full bg-success/10 px-2.5 py-0.5 text-xs font-medium text-success">
|
|
675
|
+
Active
|
|
676
|
+
</span>
|
|
677
|
+
</div>
|
|
678
|
+
|
|
679
|
+
{/* Settings */}
|
|
680
|
+
<div className="space-y-4 mb-4">
|
|
681
|
+
{/* AI Chat Toggle */}
|
|
682
|
+
<div className="flex items-center justify-between">
|
|
683
|
+
<div>
|
|
684
|
+
<div className="text-sm font-medium text-text-primary">AI Chat</div>
|
|
685
|
+
<div className="text-xs text-text-muted">Respond to @mentions with AI</div>
|
|
686
|
+
</div>
|
|
687
|
+
<button
|
|
688
|
+
onClick={() => setSlackAiChatEnabled(!slackAiChatEnabled)}
|
|
689
|
+
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
690
|
+
slackAiChatEnabled ? 'bg-primary' : 'bg-border'
|
|
691
|
+
}`}
|
|
692
|
+
>
|
|
693
|
+
<span
|
|
694
|
+
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
|
695
|
+
slackAiChatEnabled ? 'translate-x-6' : 'translate-x-1'
|
|
696
|
+
}`}
|
|
697
|
+
/>
|
|
698
|
+
</button>
|
|
699
|
+
</div>
|
|
700
|
+
|
|
701
|
+
{/* Notification Channel */}
|
|
702
|
+
<div>
|
|
703
|
+
<label className="block text-sm font-medium text-text-primary mb-1">
|
|
704
|
+
Notification Channel
|
|
705
|
+
</label>
|
|
706
|
+
<input
|
|
707
|
+
type="text"
|
|
708
|
+
value={slackNotificationChannel}
|
|
709
|
+
onChange={(e) => setSlackNotificationChannel(e.target.value)}
|
|
710
|
+
placeholder="#general"
|
|
711
|
+
className="w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-sm text-text-primary placeholder-text-muted focus:border-primary focus:outline-none"
|
|
712
|
+
/>
|
|
713
|
+
<p className="text-xs text-text-muted mt-1">
|
|
714
|
+
Channel where team notifications will be sent (e.g., thread shared, new members)
|
|
715
|
+
</p>
|
|
716
|
+
</div>
|
|
717
|
+
</div>
|
|
718
|
+
|
|
719
|
+
{/* Save button */}
|
|
720
|
+
<div className="flex flex-wrap gap-2">
|
|
721
|
+
<button
|
|
722
|
+
onClick={handleSaveSlackSettings}
|
|
723
|
+
disabled={isSavingSlack}
|
|
724
|
+
className="flex items-center gap-1.5 rounded-lg bg-primary px-3 py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-50"
|
|
725
|
+
>
|
|
726
|
+
{isSavingSlack ? (
|
|
727
|
+
<Loader2 size={14} className="animate-spin" />
|
|
728
|
+
) : null}
|
|
729
|
+
Save Settings
|
|
730
|
+
</button>
|
|
731
|
+
<button
|
|
732
|
+
onClick={handleDisconnectSlack}
|
|
733
|
+
disabled={isDisconnectingSlack}
|
|
734
|
+
className="flex items-center gap-1.5 rounded-lg border border-error bg-transparent px-3 py-2 text-sm font-medium text-error hover:bg-error/10 disabled:opacity-50"
|
|
735
|
+
>
|
|
736
|
+
{isDisconnectingSlack ? (
|
|
737
|
+
<Loader2 size={14} className="animate-spin" />
|
|
738
|
+
) : null}
|
|
739
|
+
Disconnect
|
|
740
|
+
</button>
|
|
741
|
+
</div>
|
|
742
|
+
|
|
743
|
+
<p className="mt-4 text-xs text-text-muted">
|
|
744
|
+
Connected since {new Date(slackStatus.integration?.installedAt || '').toLocaleDateString()}
|
|
745
|
+
</p>
|
|
746
|
+
</>
|
|
747
|
+
) : (
|
|
748
|
+
<>
|
|
749
|
+
{/* Not connected state */}
|
|
750
|
+
<p className="text-sm text-text-secondary mb-4">
|
|
751
|
+
Connect your team's Slack workspace to chat with your AI assistant directly from Slack.
|
|
752
|
+
Team members can @mention the bot in any channel to get instant responses.
|
|
753
|
+
</p>
|
|
754
|
+
<button
|
|
755
|
+
onClick={handleConnectSlack}
|
|
756
|
+
className="flex items-center gap-2 rounded-lg px-4 py-2 font-medium text-white"
|
|
757
|
+
style={{ backgroundColor: '#4A154B' }}
|
|
758
|
+
>
|
|
759
|
+
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
|
760
|
+
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"/>
|
|
761
|
+
</svg>
|
|
762
|
+
Add to Slack
|
|
763
|
+
</button>
|
|
764
|
+
</>
|
|
765
|
+
)}
|
|
766
|
+
</div>
|
|
767
|
+
)}
|
|
768
|
+
|
|
769
|
+
{/* Team Name */}
|
|
770
|
+
<div className="mb-6 rounded-lg bg-background-secondary p-4 sm:p-6">
|
|
771
|
+
<h2 className="text-base sm:text-lg font-semibold text-text-primary mb-4">
|
|
772
|
+
Team Name
|
|
773
|
+
</h2>
|
|
774
|
+
<form onSubmit={handleUpdateTeam} className="flex flex-col sm:flex-row gap-3 sm:gap-4">
|
|
775
|
+
<input
|
|
776
|
+
type="text"
|
|
777
|
+
value={teamName}
|
|
778
|
+
onChange={(e) => setTeamName(e.target.value)}
|
|
779
|
+
disabled={!isAdmin}
|
|
780
|
+
className="flex-1 rounded-lg border border-input-border bg-input-background px-3 sm:px-4 py-2 text-text-primary focus:border-primary focus:outline-none disabled:opacity-50"
|
|
781
|
+
/>
|
|
782
|
+
{isAdmin && (
|
|
783
|
+
<button
|
|
784
|
+
type="submit"
|
|
785
|
+
disabled={isSubmitting || teamName === currentTeam.name}
|
|
786
|
+
className="rounded-lg bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover disabled:opacity-50"
|
|
787
|
+
>
|
|
788
|
+
Save
|
|
789
|
+
</button>
|
|
790
|
+
)}
|
|
791
|
+
</form>
|
|
792
|
+
</div>
|
|
793
|
+
|
|
794
|
+
{/* Team Context */}
|
|
795
|
+
{isAdmin && (
|
|
796
|
+
<div className="mb-6 rounded-lg bg-background-secondary p-4 sm:p-6">
|
|
797
|
+
<h2 className="text-base sm:text-lg font-semibold text-text-primary mb-2">
|
|
798
|
+
Team Context
|
|
799
|
+
</h2>
|
|
800
|
+
<p className="text-xs sm:text-sm text-text-muted mb-4">
|
|
801
|
+
Additional context that will be provided to the AI assistant for all team conversations.
|
|
802
|
+
This is shared across all team members.
|
|
803
|
+
</p>
|
|
804
|
+
<form onSubmit={handleUpdateContext}>
|
|
805
|
+
<textarea
|
|
806
|
+
value={teamContext}
|
|
807
|
+
onChange={(e) => setTeamContext(e.target.value)}
|
|
808
|
+
placeholder="e.g., Project guidelines, coding standards, team preferences..."
|
|
809
|
+
rows={4}
|
|
810
|
+
className="w-full rounded-lg border border-input-border bg-input-background px-3 sm:px-4 py-3 text-sm sm:text-base text-text-primary placeholder-text-muted focus:border-primary focus:outline-none resize-y"
|
|
811
|
+
/>
|
|
812
|
+
<div className="mt-4 flex justify-end">
|
|
813
|
+
<button
|
|
814
|
+
type="submit"
|
|
815
|
+
disabled={isSavingContext || teamContext === (currentTeam.context || '')}
|
|
816
|
+
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-50"
|
|
817
|
+
>
|
|
818
|
+
{isSavingContext ? 'Saving...' : 'Save Context'}
|
|
819
|
+
</button>
|
|
820
|
+
</div>
|
|
821
|
+
</form>
|
|
822
|
+
</div>
|
|
823
|
+
)}
|
|
824
|
+
|
|
825
|
+
{/* Invite Members */}
|
|
826
|
+
{isAdmin && (
|
|
827
|
+
<div className="mb-6 rounded-lg bg-background-secondary p-4 sm:p-6">
|
|
828
|
+
<h2 className="text-base sm:text-lg font-semibold text-text-primary mb-4">
|
|
829
|
+
Invite Members
|
|
830
|
+
</h2>
|
|
831
|
+
<form onSubmit={handleInvite} className="space-y-3 sm:space-y-0 sm:flex sm:gap-4 mb-4">
|
|
832
|
+
<input
|
|
833
|
+
type="email"
|
|
834
|
+
placeholder="Email address"
|
|
835
|
+
value={inviteEmail}
|
|
836
|
+
onChange={(e) => setInviteEmail(e.target.value)}
|
|
837
|
+
required
|
|
838
|
+
className="w-full sm:flex-1 rounded-lg border border-input-border bg-input-background px-3 sm:px-4 py-2 text-text-primary focus:border-primary focus:outline-none"
|
|
839
|
+
/>
|
|
840
|
+
<div className="flex gap-3 sm:gap-4">
|
|
841
|
+
<select
|
|
842
|
+
value={inviteRole}
|
|
843
|
+
onChange={(e) => setInviteRole(e.target.value as 'admin' | 'member' | 'viewer')}
|
|
844
|
+
className="flex-1 sm:flex-none rounded-lg border border-input-border bg-input-background px-3 sm:px-4 py-2 text-text-primary focus:border-primary focus:outline-none"
|
|
845
|
+
>
|
|
846
|
+
<option value="admin">Admin</option>
|
|
847
|
+
<option value="member">Member</option>
|
|
848
|
+
<option value="viewer">Viewer</option>
|
|
849
|
+
</select>
|
|
850
|
+
<button
|
|
851
|
+
type="submit"
|
|
852
|
+
disabled={isSubmitting}
|
|
853
|
+
className="rounded-lg bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover disabled:opacity-50"
|
|
854
|
+
>
|
|
855
|
+
Invite
|
|
856
|
+
</button>
|
|
857
|
+
</div>
|
|
858
|
+
</form>
|
|
859
|
+
|
|
860
|
+
{inviteUrl && (
|
|
861
|
+
<div className="flex items-center gap-2 p-3 rounded-lg bg-background">
|
|
862
|
+
<input
|
|
863
|
+
type="text"
|
|
864
|
+
value={inviteUrl}
|
|
865
|
+
readOnly
|
|
866
|
+
className="flex-1 bg-transparent text-xs sm:text-sm text-text-secondary min-w-0"
|
|
867
|
+
/>
|
|
868
|
+
<button
|
|
869
|
+
onClick={copyInviteUrl}
|
|
870
|
+
className="flex-shrink-0 text-sm text-primary hover:underline"
|
|
871
|
+
>
|
|
872
|
+
Copy
|
|
873
|
+
</button>
|
|
874
|
+
</div>
|
|
875
|
+
)}
|
|
876
|
+
|
|
877
|
+
{/* Pending Invites */}
|
|
878
|
+
{currentTeam.invites && currentTeam.invites.length > 0 && (
|
|
879
|
+
<div className="mt-4">
|
|
880
|
+
<h3 className="text-sm font-medium text-text-muted mb-2">
|
|
881
|
+
Pending Invites
|
|
882
|
+
</h3>
|
|
883
|
+
<div className="space-y-2">
|
|
884
|
+
{currentTeam.invites.map((invite) => (
|
|
885
|
+
<div
|
|
886
|
+
key={invite.id}
|
|
887
|
+
className="flex items-center justify-between p-3 rounded-lg bg-background"
|
|
888
|
+
>
|
|
889
|
+
<div>
|
|
890
|
+
<span className="text-text-primary">{invite.email}</span>
|
|
891
|
+
<span className="ml-2 text-xs text-text-muted capitalize">
|
|
892
|
+
({invite.role})
|
|
893
|
+
</span>
|
|
894
|
+
</div>
|
|
895
|
+
<button
|
|
896
|
+
onClick={() => handleCancelInvite(invite.id)}
|
|
897
|
+
className="text-sm text-error hover:underline"
|
|
898
|
+
>
|
|
899
|
+
Cancel
|
|
900
|
+
</button>
|
|
901
|
+
</div>
|
|
902
|
+
))}
|
|
903
|
+
</div>
|
|
904
|
+
</div>
|
|
905
|
+
)}
|
|
906
|
+
</div>
|
|
907
|
+
)}
|
|
908
|
+
|
|
909
|
+
{/* Members */}
|
|
910
|
+
<div className="mb-6 rounded-lg bg-background-secondary p-4 sm:p-6">
|
|
911
|
+
<h2 className="text-base sm:text-lg font-semibold text-text-primary mb-4">
|
|
912
|
+
Members ({currentTeam.members.length})
|
|
913
|
+
</h2>
|
|
914
|
+
<div className="space-y-3">
|
|
915
|
+
{currentTeam.members.map((member) => (
|
|
916
|
+
<div
|
|
917
|
+
key={member.id}
|
|
918
|
+
className="p-3 rounded-lg bg-background"
|
|
919
|
+
>
|
|
920
|
+
{/* Desktop View */}
|
|
921
|
+
<div className="hidden sm:flex items-center justify-between">
|
|
922
|
+
<div className="flex items-center gap-3">
|
|
923
|
+
{member.user?.avatarUrl ? (
|
|
924
|
+
<img
|
|
925
|
+
src={member.user.avatarUrl}
|
|
926
|
+
alt=""
|
|
927
|
+
className="w-8 h-8 rounded-full"
|
|
928
|
+
/>
|
|
929
|
+
) : (
|
|
930
|
+
<div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-white text-sm">
|
|
931
|
+
{(member.user?.name || member.user?.email || '?')[0].toUpperCase()}
|
|
932
|
+
</div>
|
|
933
|
+
)}
|
|
934
|
+
<div>
|
|
935
|
+
<div className="text-text-primary">
|
|
936
|
+
{member.user?.name || member.user?.email}
|
|
937
|
+
{member.userId === user?.id && (
|
|
938
|
+
<span className="ml-2 text-xs text-text-muted">(you)</span>
|
|
939
|
+
)}
|
|
940
|
+
</div>
|
|
941
|
+
{member.user?.name && (
|
|
942
|
+
<div className="text-xs text-text-muted">
|
|
943
|
+
{member.user.email}
|
|
944
|
+
</div>
|
|
945
|
+
)}
|
|
946
|
+
</div>
|
|
947
|
+
</div>
|
|
948
|
+
<div className="flex items-center gap-3">
|
|
949
|
+
{isOwner && member.userId !== user?.id && member.role !== 'owner' ? (
|
|
950
|
+
<select
|
|
951
|
+
value={member.role}
|
|
952
|
+
onChange={(e) => handleUpdateRole(member.userId, e.target.value as TeamRole)}
|
|
953
|
+
className="rounded border border-input-border bg-input-background px-2 py-1 text-sm text-text-primary"
|
|
954
|
+
>
|
|
955
|
+
<option value="admin">Admin</option>
|
|
956
|
+
<option value="member">Member</option>
|
|
957
|
+
<option value="viewer">Viewer</option>
|
|
958
|
+
</select>
|
|
959
|
+
) : (
|
|
960
|
+
<span className="text-sm text-text-muted capitalize">
|
|
961
|
+
{member.role}
|
|
962
|
+
</span>
|
|
963
|
+
)}
|
|
964
|
+
{isAdmin && member.userId !== user?.id && member.role !== 'owner' && (
|
|
965
|
+
<button
|
|
966
|
+
onClick={() => handleRemoveMember(member.userId)}
|
|
967
|
+
className="text-sm text-error hover:underline"
|
|
968
|
+
>
|
|
969
|
+
Remove
|
|
970
|
+
</button>
|
|
971
|
+
)}
|
|
972
|
+
</div>
|
|
973
|
+
</div>
|
|
974
|
+
|
|
975
|
+
{/* Mobile View */}
|
|
976
|
+
<div className="sm:hidden">
|
|
977
|
+
<div className="flex items-center justify-between">
|
|
978
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
979
|
+
{member.user?.avatarUrl ? (
|
|
980
|
+
<img
|
|
981
|
+
src={member.user.avatarUrl}
|
|
982
|
+
alt=""
|
|
983
|
+
className="w-10 h-10 rounded-full flex-shrink-0"
|
|
984
|
+
/>
|
|
985
|
+
) : (
|
|
986
|
+
<div className="w-10 h-10 rounded-full bg-primary flex items-center justify-center text-white text-sm flex-shrink-0">
|
|
987
|
+
{(member.user?.name || member.user?.email || '?')[0].toUpperCase()}
|
|
988
|
+
</div>
|
|
989
|
+
)}
|
|
990
|
+
<div className="min-w-0">
|
|
991
|
+
<div className="text-sm font-medium text-text-primary truncate">
|
|
992
|
+
{member.user?.name || member.user?.email}
|
|
993
|
+
{member.userId === user?.id && (
|
|
994
|
+
<span className="ml-1 text-xs text-text-muted">(you)</span>
|
|
995
|
+
)}
|
|
996
|
+
</div>
|
|
997
|
+
{member.user?.name && (
|
|
998
|
+
<div className="text-xs text-text-muted truncate">
|
|
999
|
+
{member.user.email}
|
|
1000
|
+
</div>
|
|
1001
|
+
)}
|
|
1002
|
+
</div>
|
|
1003
|
+
</div>
|
|
1004
|
+
<span className="text-xs text-text-muted capitalize flex-shrink-0 ml-2">
|
|
1005
|
+
{member.role}
|
|
1006
|
+
</span>
|
|
1007
|
+
</div>
|
|
1008
|
+
{isOwner && member.userId !== user?.id && member.role !== 'owner' && (
|
|
1009
|
+
<div className="flex items-center gap-2 mt-2 pt-2 border-t border-border">
|
|
1010
|
+
<select
|
|
1011
|
+
value={member.role}
|
|
1012
|
+
onChange={(e) => handleUpdateRole(member.userId, e.target.value as TeamRole)}
|
|
1013
|
+
className="flex-1 rounded border border-input-border bg-input-background px-2 py-1 text-xs text-text-primary"
|
|
1014
|
+
>
|
|
1015
|
+
<option value="admin">Admin</option>
|
|
1016
|
+
<option value="member">Member</option>
|
|
1017
|
+
<option value="viewer">Viewer</option>
|
|
1018
|
+
</select>
|
|
1019
|
+
<button
|
|
1020
|
+
onClick={() => handleRemoveMember(member.userId)}
|
|
1021
|
+
className="text-xs text-error hover:underline"
|
|
1022
|
+
>
|
|
1023
|
+
Remove
|
|
1024
|
+
</button>
|
|
1025
|
+
</div>
|
|
1026
|
+
)}
|
|
1027
|
+
{isAdmin && !isOwner && member.userId !== user?.id && member.role !== 'owner' && (
|
|
1028
|
+
<div className="flex justify-end mt-2 pt-2 border-t border-border">
|
|
1029
|
+
<button
|
|
1030
|
+
onClick={() => handleRemoveMember(member.userId)}
|
|
1031
|
+
className="text-xs text-error hover:underline"
|
|
1032
|
+
>
|
|
1033
|
+
Remove
|
|
1034
|
+
</button>
|
|
1035
|
+
</div>
|
|
1036
|
+
)}
|
|
1037
|
+
</div>
|
|
1038
|
+
</div>
|
|
1039
|
+
))}
|
|
1040
|
+
</div>
|
|
1041
|
+
</div>
|
|
1042
|
+
|
|
1043
|
+
{/* Danger Zone */}
|
|
1044
|
+
<div className="rounded-lg border border-error bg-error/10 p-4 sm:p-6">
|
|
1045
|
+
<h2 className="text-base sm:text-lg font-semibold text-error mb-4">
|
|
1046
|
+
Danger Zone
|
|
1047
|
+
</h2>
|
|
1048
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
1049
|
+
{isOwner ? (
|
|
1050
|
+
<>
|
|
1051
|
+
<div>
|
|
1052
|
+
<p className="text-sm sm:text-base text-text-primary">Archive this team</p>
|
|
1053
|
+
<p className="text-xs sm:text-sm text-text-muted">
|
|
1054
|
+
Team threads will be preserved but the team will be hidden
|
|
1055
|
+
</p>
|
|
1056
|
+
</div>
|
|
1057
|
+
<button
|
|
1058
|
+
onClick={handleArchiveTeam}
|
|
1059
|
+
className="self-start sm:self-auto rounded-lg bg-error px-4 py-2 text-sm font-medium text-white hover:bg-error/90"
|
|
1060
|
+
>
|
|
1061
|
+
Archive Team
|
|
1062
|
+
</button>
|
|
1063
|
+
</>
|
|
1064
|
+
) : (
|
|
1065
|
+
<>
|
|
1066
|
+
<div>
|
|
1067
|
+
<p className="text-sm sm:text-base text-text-primary">Leave this team</p>
|
|
1068
|
+
<p className="text-xs sm:text-sm text-text-muted">
|
|
1069
|
+
You will lose access to team threads
|
|
1070
|
+
</p>
|
|
1071
|
+
</div>
|
|
1072
|
+
<button
|
|
1073
|
+
onClick={handleLeaveTeam}
|
|
1074
|
+
className="self-start sm:self-auto rounded-lg bg-error px-4 py-2 text-sm font-medium text-white hover:bg-error/90"
|
|
1075
|
+
>
|
|
1076
|
+
Leave Team
|
|
1077
|
+
</button>
|
|
1078
|
+
</>
|
|
1079
|
+
)}
|
|
1080
|
+
</div>
|
|
1081
|
+
</div>
|
|
1082
|
+
</div>
|
|
1083
|
+
</div>
|
|
1084
|
+
);
|
|
1085
|
+
}
|