@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,255 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useState,
|
|
5
|
+
useEffect,
|
|
6
|
+
useCallback,
|
|
7
|
+
useRef,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { useSearchParams } from 'react-router';
|
|
11
|
+
import type { TeamWithRole, TeamDetails, TeamInvite, TeamRole } from '@chaaskit/shared';
|
|
12
|
+
import { api } from '../utils/api';
|
|
13
|
+
import { useAuth } from './AuthContext';
|
|
14
|
+
import { useConfig, useConfigLoaded } from './ConfigContext';
|
|
15
|
+
|
|
16
|
+
interface TeamContextType {
|
|
17
|
+
teams: TeamWithRole[];
|
|
18
|
+
currentTeam: TeamDetails | null;
|
|
19
|
+
currentTeamId: string | null;
|
|
20
|
+
isLoadingTeams: boolean;
|
|
21
|
+
isLoadingTeamDetails: boolean;
|
|
22
|
+
loadTeams: () => Promise<void>;
|
|
23
|
+
loadTeamDetails: (teamId: string) => Promise<void>;
|
|
24
|
+
setCurrentTeamId: (teamId: string | null) => void;
|
|
25
|
+
createTeam: (name: string) => Promise<TeamDetails>;
|
|
26
|
+
updateTeam: (teamId: string, updates: { name?: string; context?: string | null }) => Promise<void>;
|
|
27
|
+
archiveTeam: (teamId: string) => Promise<void>;
|
|
28
|
+
inviteMember: (teamId: string, email: string, role: 'admin' | 'member' | 'viewer') => Promise<{ invite: TeamInvite; inviteUrl: string }>;
|
|
29
|
+
removeMember: (teamId: string, userId: string) => Promise<void>;
|
|
30
|
+
updateMemberRole: (teamId: string, userId: string, role: TeamRole) => Promise<void>;
|
|
31
|
+
leaveTeam: (teamId: string) => Promise<void>;
|
|
32
|
+
cancelInvite: (teamId: string, inviteId: string) => Promise<void>;
|
|
33
|
+
getCurrentTeamRole: () => TeamRole | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const TeamContext = createContext<TeamContextType | undefined>(undefined);
|
|
37
|
+
|
|
38
|
+
export function TeamProvider({ children }: { children: ReactNode }) {
|
|
39
|
+
const { user } = useAuth();
|
|
40
|
+
const config = useConfig();
|
|
41
|
+
const configLoaded = useConfigLoaded();
|
|
42
|
+
const teamsEnabled = configLoaded ? (config.teams?.enabled ?? true) : false;
|
|
43
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
44
|
+
const [teams, setTeams] = useState<TeamWithRole[]>([]);
|
|
45
|
+
const [currentTeam, setCurrentTeam] = useState<TeamDetails | null>(null);
|
|
46
|
+
const [currentTeamId, setCurrentTeamId] = useState<string | null>(() => {
|
|
47
|
+
// URL takes priority over localStorage
|
|
48
|
+
const urlTeam = searchParams.get('team');
|
|
49
|
+
if (urlTeam) return urlTeam;
|
|
50
|
+
return localStorage.getItem('currentTeamId');
|
|
51
|
+
});
|
|
52
|
+
const [isLoadingTeams, setIsLoadingTeams] = useState(false);
|
|
53
|
+
const [isLoadingTeamDetails, setIsLoadingTeamDetails] = useState(false);
|
|
54
|
+
|
|
55
|
+
const clearTeamSelection = useCallback(() => {
|
|
56
|
+
setCurrentTeamId(null);
|
|
57
|
+
setCurrentTeam(null);
|
|
58
|
+
localStorage.removeItem('currentTeamId');
|
|
59
|
+
setSearchParams((prev) => {
|
|
60
|
+
prev.delete('team');
|
|
61
|
+
return prev;
|
|
62
|
+
}, { replace: true });
|
|
63
|
+
}, [setSearchParams]);
|
|
64
|
+
|
|
65
|
+
const loadTeams = useCallback(async () => {
|
|
66
|
+
if (!user || !teamsEnabled) {
|
|
67
|
+
setTeams([]);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setIsLoadingTeams(true);
|
|
72
|
+
try {
|
|
73
|
+
const response = await api.get<{ teams: TeamWithRole[] }>('/api/teams');
|
|
74
|
+
setTeams(response.teams);
|
|
75
|
+
|
|
76
|
+
// If current team is no longer available (invalid/archived or user not a member), clear it
|
|
77
|
+
if (currentTeamId && !response.teams.some((t) => t.id === currentTeamId)) {
|
|
78
|
+
clearTeamSelection();
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Failed to load teams:', error);
|
|
82
|
+
setTeams([]);
|
|
83
|
+
} finally {
|
|
84
|
+
setIsLoadingTeams(false);
|
|
85
|
+
}
|
|
86
|
+
}, [user, teamsEnabled, currentTeamId, clearTeamSelection]);
|
|
87
|
+
|
|
88
|
+
const loadTeamDetails = useCallback(async (teamId: string) => {
|
|
89
|
+
if (!teamsEnabled) {
|
|
90
|
+
setCurrentTeam(null);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setIsLoadingTeamDetails(true);
|
|
95
|
+
try {
|
|
96
|
+
const response = await api.get<{ team: TeamDetails }>(`/api/teams/${teamId}`);
|
|
97
|
+
setCurrentTeam(response.team);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('Failed to load team details:', error);
|
|
100
|
+
setCurrentTeam(null);
|
|
101
|
+
} finally {
|
|
102
|
+
setIsLoadingTeamDetails(false);
|
|
103
|
+
}
|
|
104
|
+
}, [teamsEnabled]);
|
|
105
|
+
|
|
106
|
+
const handleSetCurrentTeamId = useCallback((teamId: string | null) => {
|
|
107
|
+
setCurrentTeamId(teamId);
|
|
108
|
+
|
|
109
|
+
// Update URL and localStorage
|
|
110
|
+
if (teamId) {
|
|
111
|
+
localStorage.setItem('currentTeamId', teamId);
|
|
112
|
+
setSearchParams((prev) => {
|
|
113
|
+
prev.set('team', teamId);
|
|
114
|
+
return prev;
|
|
115
|
+
}, { replace: true });
|
|
116
|
+
} else {
|
|
117
|
+
localStorage.removeItem('currentTeamId');
|
|
118
|
+
setCurrentTeam(null);
|
|
119
|
+
setSearchParams((prev) => {
|
|
120
|
+
prev.delete('team');
|
|
121
|
+
return prev;
|
|
122
|
+
}, { replace: true });
|
|
123
|
+
}
|
|
124
|
+
}, [setSearchParams]);
|
|
125
|
+
|
|
126
|
+
const createTeam = useCallback(async (name: string): Promise<TeamDetails> => {
|
|
127
|
+
const response = await api.post<{ team: TeamDetails }>('/api/teams', { name });
|
|
128
|
+
await loadTeams();
|
|
129
|
+
return response.team;
|
|
130
|
+
}, [loadTeams]);
|
|
131
|
+
|
|
132
|
+
const updateTeam = useCallback(async (teamId: string, updates: { name?: string; context?: string | null }) => {
|
|
133
|
+
await api.patch(`/api/teams/${teamId}`, updates);
|
|
134
|
+
await loadTeams();
|
|
135
|
+
if (currentTeamId === teamId) {
|
|
136
|
+
await loadTeamDetails(teamId);
|
|
137
|
+
}
|
|
138
|
+
}, [loadTeams, loadTeamDetails, currentTeamId]);
|
|
139
|
+
|
|
140
|
+
const archiveTeam = useCallback(async (teamId: string) => {
|
|
141
|
+
await api.post(`/api/teams/${teamId}/archive`, {});
|
|
142
|
+
if (currentTeamId === teamId) {
|
|
143
|
+
handleSetCurrentTeamId(null);
|
|
144
|
+
}
|
|
145
|
+
await loadTeams();
|
|
146
|
+
}, [loadTeams, currentTeamId, handleSetCurrentTeamId]);
|
|
147
|
+
|
|
148
|
+
const inviteMember = useCallback(async (
|
|
149
|
+
teamId: string,
|
|
150
|
+
email: string,
|
|
151
|
+
role: 'admin' | 'member' | 'viewer'
|
|
152
|
+
): Promise<{ invite: TeamInvite; inviteUrl: string }> => {
|
|
153
|
+
const response = await api.post<{ invite: TeamInvite; inviteUrl: string }>(
|
|
154
|
+
`/api/teams/${teamId}/invite`,
|
|
155
|
+
{ email, role }
|
|
156
|
+
);
|
|
157
|
+
if (currentTeamId === teamId) {
|
|
158
|
+
await loadTeamDetails(teamId);
|
|
159
|
+
}
|
|
160
|
+
return response;
|
|
161
|
+
}, [loadTeamDetails, currentTeamId]);
|
|
162
|
+
|
|
163
|
+
const removeMember = useCallback(async (teamId: string, userId: string) => {
|
|
164
|
+
await api.delete(`/api/teams/${teamId}/members/${userId}`);
|
|
165
|
+
await loadTeams();
|
|
166
|
+
if (currentTeamId === teamId) {
|
|
167
|
+
await loadTeamDetails(teamId);
|
|
168
|
+
}
|
|
169
|
+
}, [loadTeams, loadTeamDetails, currentTeamId]);
|
|
170
|
+
|
|
171
|
+
const updateMemberRole = useCallback(async (teamId: string, userId: string, role: TeamRole) => {
|
|
172
|
+
await api.patch(`/api/teams/${teamId}/members/${userId}`, { role });
|
|
173
|
+
if (currentTeamId === teamId) {
|
|
174
|
+
await loadTeamDetails(teamId);
|
|
175
|
+
}
|
|
176
|
+
}, [loadTeamDetails, currentTeamId]);
|
|
177
|
+
|
|
178
|
+
const leaveTeam = useCallback(async (teamId: string) => {
|
|
179
|
+
await api.post(`/api/teams/${teamId}/leave`, {});
|
|
180
|
+
if (currentTeamId === teamId) {
|
|
181
|
+
handleSetCurrentTeamId(null);
|
|
182
|
+
}
|
|
183
|
+
await loadTeams();
|
|
184
|
+
}, [loadTeams, currentTeamId, handleSetCurrentTeamId]);
|
|
185
|
+
|
|
186
|
+
const cancelInvite = useCallback(async (teamId: string, inviteId: string) => {
|
|
187
|
+
await api.delete(`/api/teams/${teamId}/invite/${inviteId}`);
|
|
188
|
+
if (currentTeamId === teamId) {
|
|
189
|
+
await loadTeamDetails(teamId);
|
|
190
|
+
}
|
|
191
|
+
}, [loadTeamDetails, currentTeamId]);
|
|
192
|
+
|
|
193
|
+
const getCurrentTeamRole = useCallback((): TeamRole | null => {
|
|
194
|
+
if (!currentTeamId) return null;
|
|
195
|
+
const team = teams.find((t) => t.id === currentTeamId);
|
|
196
|
+
return team?.role || null;
|
|
197
|
+
}, [teams, currentTeamId]);
|
|
198
|
+
|
|
199
|
+
// Track if user was ever logged in to distinguish logout from initial mount
|
|
200
|
+
const wasLoggedIn = useRef(false);
|
|
201
|
+
|
|
202
|
+
// Load teams when user changes and config is loaded
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
if (user && configLoaded) {
|
|
205
|
+
wasLoggedIn.current = true;
|
|
206
|
+
loadTeams();
|
|
207
|
+
} else if (wasLoggedIn.current && !user) {
|
|
208
|
+
// Only clear on actual logout, not on initial mount
|
|
209
|
+
setTeams([]);
|
|
210
|
+
setCurrentTeam(null);
|
|
211
|
+
handleSetCurrentTeamId(null);
|
|
212
|
+
}
|
|
213
|
+
}, [user, configLoaded, loadTeams, handleSetCurrentTeamId]);
|
|
214
|
+
|
|
215
|
+
// Load team details when current team changes
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (currentTeamId && configLoaded && teamsEnabled) {
|
|
218
|
+
loadTeamDetails(currentTeamId);
|
|
219
|
+
}
|
|
220
|
+
}, [currentTeamId, configLoaded, teamsEnabled, loadTeamDetails]);
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<TeamContext.Provider
|
|
224
|
+
value={{
|
|
225
|
+
teams,
|
|
226
|
+
currentTeam,
|
|
227
|
+
currentTeamId,
|
|
228
|
+
isLoadingTeams,
|
|
229
|
+
isLoadingTeamDetails,
|
|
230
|
+
loadTeams,
|
|
231
|
+
loadTeamDetails,
|
|
232
|
+
setCurrentTeamId: handleSetCurrentTeamId,
|
|
233
|
+
createTeam,
|
|
234
|
+
updateTeam,
|
|
235
|
+
archiveTeam,
|
|
236
|
+
inviteMember,
|
|
237
|
+
removeMember,
|
|
238
|
+
updateMemberRole,
|
|
239
|
+
leaveTeam,
|
|
240
|
+
cancelInvite,
|
|
241
|
+
getCurrentTeamRole,
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
{children}
|
|
245
|
+
</TeamContext.Provider>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function useTeam() {
|
|
250
|
+
const context = useContext(TeamContext);
|
|
251
|
+
if (context === undefined) {
|
|
252
|
+
throw new Error('useTeam must be used within a TeamProvider');
|
|
253
|
+
}
|
|
254
|
+
return context;
|
|
255
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useState,
|
|
5
|
+
useEffect,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { useConfig } from './ConfigContext';
|
|
9
|
+
|
|
10
|
+
interface ThemeContextType {
|
|
11
|
+
theme: string;
|
|
12
|
+
setTheme: (theme: string) => void;
|
|
13
|
+
availableThemes: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
|
17
|
+
|
|
18
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
19
|
+
const config = useConfig();
|
|
20
|
+
const [theme, setThemeState] = useState(() => {
|
|
21
|
+
// Check localStorage first, then config default
|
|
22
|
+
const stored = localStorage.getItem('theme');
|
|
23
|
+
if (stored && config.theming.themes[stored]) {
|
|
24
|
+
return stored;
|
|
25
|
+
}
|
|
26
|
+
return config.theming.defaultTheme;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const availableThemes = Object.keys(config.theming.themes);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
// Apply theme to document
|
|
33
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
34
|
+
localStorage.setItem('theme', theme);
|
|
35
|
+
// Set cookie for SSR to read
|
|
36
|
+
document.cookie = `theme=${theme};path=/;max-age=31536000;SameSite=Lax`;
|
|
37
|
+
|
|
38
|
+
// Apply theme colors as CSS variables
|
|
39
|
+
const themeConfig = config.theming.themes[theme];
|
|
40
|
+
if (themeConfig) {
|
|
41
|
+
const root = document.documentElement;
|
|
42
|
+
const colors = themeConfig.colors;
|
|
43
|
+
|
|
44
|
+
// Convert hex colors to RGB values
|
|
45
|
+
Object.entries(colors).forEach(([key, value]) => {
|
|
46
|
+
const cssKey = `--color-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
|
|
47
|
+
const rgb = hexToRgb(value);
|
|
48
|
+
if (rgb) {
|
|
49
|
+
root.style.setProperty(cssKey, `${rgb.r} ${rgb.g} ${rgb.b}`);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Apply fonts
|
|
55
|
+
document.documentElement.style.setProperty(
|
|
56
|
+
'--font-sans',
|
|
57
|
+
config.theming.fonts.sans
|
|
58
|
+
);
|
|
59
|
+
document.documentElement.style.setProperty(
|
|
60
|
+
'--font-mono',
|
|
61
|
+
config.theming.fonts.mono
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Apply border radius
|
|
65
|
+
document.documentElement.style.setProperty(
|
|
66
|
+
'--radius-sm',
|
|
67
|
+
config.theming.borderRadius.sm
|
|
68
|
+
);
|
|
69
|
+
document.documentElement.style.setProperty(
|
|
70
|
+
'--radius-md',
|
|
71
|
+
config.theming.borderRadius.md
|
|
72
|
+
);
|
|
73
|
+
document.documentElement.style.setProperty(
|
|
74
|
+
'--radius-lg',
|
|
75
|
+
config.theming.borderRadius.lg
|
|
76
|
+
);
|
|
77
|
+
document.documentElement.style.setProperty(
|
|
78
|
+
'--radius-full',
|
|
79
|
+
config.theming.borderRadius.full
|
|
80
|
+
);
|
|
81
|
+
}, [theme, config.theming]);
|
|
82
|
+
|
|
83
|
+
function setTheme(newTheme: string) {
|
|
84
|
+
if (config.theming.themes[newTheme]) {
|
|
85
|
+
setThemeState(newTheme);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<ThemeContext.Provider value={{ theme, setTheme, availableThemes }}>
|
|
91
|
+
{children}
|
|
92
|
+
</ThemeContext.Provider>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function useTheme() {
|
|
97
|
+
const context = useContext(ThemeContext);
|
|
98
|
+
if (context === undefined) {
|
|
99
|
+
throw new Error('useTheme must be used within a ThemeProvider');
|
|
100
|
+
}
|
|
101
|
+
return context;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
|
105
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
106
|
+
return result
|
|
107
|
+
? {
|
|
108
|
+
r: parseInt(result[1]!, 16),
|
|
109
|
+
g: parseInt(result[2]!, 16),
|
|
110
|
+
b: parseInt(result[3]!, 16),
|
|
111
|
+
}
|
|
112
|
+
: null;
|
|
113
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Client extension system for @chaaskit/client
|
|
2
|
+
export {
|
|
3
|
+
clientRegistry,
|
|
4
|
+
type PageExtension,
|
|
5
|
+
type ToolExtension,
|
|
6
|
+
type ComponentOverride,
|
|
7
|
+
} from './registry';
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
useExtensionPages,
|
|
11
|
+
useSidebarPages,
|
|
12
|
+
useExtensionTools,
|
|
13
|
+
useToolRenderer,
|
|
14
|
+
useComponentOverride,
|
|
15
|
+
} from './useExtensions';
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Page extension configuration
|
|
5
|
+
*/
|
|
6
|
+
export interface PageExtension {
|
|
7
|
+
/** Unique identifier for the page */
|
|
8
|
+
id: string;
|
|
9
|
+
/** URL path for the page (e.g., "/analytics") */
|
|
10
|
+
path: string;
|
|
11
|
+
/** Display label for navigation */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Optional icon name from lucide-react */
|
|
14
|
+
icon?: string;
|
|
15
|
+
/** React component to render for this page */
|
|
16
|
+
component: ComponentType;
|
|
17
|
+
/** Whether to show in the sidebar navigation */
|
|
18
|
+
showInSidebar?: boolean;
|
|
19
|
+
/** Whether this page requires authentication */
|
|
20
|
+
requiresAuth?: boolean;
|
|
21
|
+
/** Whether this page requires admin access */
|
|
22
|
+
requiresAdmin?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Tool result renderer extension
|
|
27
|
+
*/
|
|
28
|
+
export interface ToolExtension {
|
|
29
|
+
/** Tool name to match */
|
|
30
|
+
name: string;
|
|
31
|
+
/** Description of the tool */
|
|
32
|
+
description: string;
|
|
33
|
+
/** Custom renderer for tool results */
|
|
34
|
+
resultRenderer?: ComponentType<{ result: unknown }>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Component override extension
|
|
39
|
+
*/
|
|
40
|
+
export interface ComponentOverride {
|
|
41
|
+
/** Component slot to override */
|
|
42
|
+
slot: 'header' | 'footer' | 'sidebar-header' | 'sidebar-footer' | 'message-actions';
|
|
43
|
+
/** Component to render in the slot */
|
|
44
|
+
component: ComponentType<Record<string, unknown>>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Client-side extension registry for customizing the ChaasKit UI
|
|
49
|
+
*
|
|
50
|
+
* Note: getPages(), getSidebarPages(), and getTools() cache their results
|
|
51
|
+
* to support React's useSyncExternalStore which requires stable references.
|
|
52
|
+
*/
|
|
53
|
+
class ClientExtensionRegistry {
|
|
54
|
+
private pages: Map<string, PageExtension> = new Map();
|
|
55
|
+
private tools: Map<string, ToolExtension> = new Map();
|
|
56
|
+
private overrides: Map<string, ComponentOverride> = new Map();
|
|
57
|
+
private listeners: Set<() => void> = new Set();
|
|
58
|
+
|
|
59
|
+
// Cached arrays for useSyncExternalStore compatibility
|
|
60
|
+
// These are only updated when the underlying data changes
|
|
61
|
+
private cachedPages: PageExtension[] = [];
|
|
62
|
+
private cachedSidebarPages: PageExtension[] = [];
|
|
63
|
+
private cachedTools: ToolExtension[] = [];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Register a custom page
|
|
67
|
+
*/
|
|
68
|
+
registerPage(page: PageExtension): void {
|
|
69
|
+
this.pages.set(page.id, page);
|
|
70
|
+
this.invalidateCaches();
|
|
71
|
+
this.notifyListeners();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Unregister a custom page
|
|
76
|
+
*/
|
|
77
|
+
unregisterPage(id: string): boolean {
|
|
78
|
+
const result = this.pages.delete(id);
|
|
79
|
+
if (result) {
|
|
80
|
+
this.invalidateCaches();
|
|
81
|
+
this.notifyListeners();
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get all registered pages
|
|
88
|
+
* Returns a cached array for useSyncExternalStore compatibility
|
|
89
|
+
*/
|
|
90
|
+
getPages(): PageExtension[] {
|
|
91
|
+
return this.cachedPages;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get pages that should appear in the sidebar
|
|
96
|
+
* Returns a cached array for useSyncExternalStore compatibility
|
|
97
|
+
*/
|
|
98
|
+
getSidebarPages(): PageExtension[] {
|
|
99
|
+
return this.cachedSidebarPages;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Register a custom tool renderer
|
|
104
|
+
*/
|
|
105
|
+
registerTool(tool: ToolExtension): void {
|
|
106
|
+
this.tools.set(tool.name, tool);
|
|
107
|
+
this.invalidateCaches();
|
|
108
|
+
this.notifyListeners();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Unregister a tool renderer
|
|
113
|
+
*/
|
|
114
|
+
unregisterTool(name: string): boolean {
|
|
115
|
+
const result = this.tools.delete(name);
|
|
116
|
+
if (result) {
|
|
117
|
+
this.invalidateCaches();
|
|
118
|
+
this.notifyListeners();
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get all registered tools
|
|
125
|
+
* Returns a cached array for useSyncExternalStore compatibility
|
|
126
|
+
*/
|
|
127
|
+
getTools(): ToolExtension[] {
|
|
128
|
+
return this.cachedTools;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get a specific tool by name
|
|
133
|
+
*/
|
|
134
|
+
getTool(name: string): ToolExtension | undefined {
|
|
135
|
+
return this.tools.get(name);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Register a component override
|
|
140
|
+
*/
|
|
141
|
+
registerOverride(override: ComponentOverride): void {
|
|
142
|
+
this.overrides.set(override.slot, override);
|
|
143
|
+
this.notifyListeners();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get a component override for a slot
|
|
148
|
+
*/
|
|
149
|
+
getOverride(slot: string): ComponentOverride | undefined {
|
|
150
|
+
return this.overrides.get(slot);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Subscribe to registry changes
|
|
155
|
+
*/
|
|
156
|
+
subscribe(listener: () => void): () => void {
|
|
157
|
+
this.listeners.add(listener);
|
|
158
|
+
return () => this.listeners.delete(listener);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private invalidateCaches(): void {
|
|
162
|
+
this.cachedPages = Array.from(this.pages.values());
|
|
163
|
+
this.cachedSidebarPages = this.cachedPages.filter(p => p.showInSidebar);
|
|
164
|
+
this.cachedTools = Array.from(this.tools.values());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private notifyListeners(): void {
|
|
168
|
+
this.listeners.forEach(listener => listener());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Clear all registered extensions
|
|
173
|
+
*/
|
|
174
|
+
clear(): void {
|
|
175
|
+
this.pages.clear();
|
|
176
|
+
this.tools.clear();
|
|
177
|
+
this.overrides.clear();
|
|
178
|
+
this.invalidateCaches();
|
|
179
|
+
this.notifyListeners();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Singleton instance
|
|
184
|
+
export const clientRegistry = new ClientExtensionRegistry();
|
|
185
|
+
|
|
186
|
+
// Export for use in user extensions
|
|
187
|
+
export default clientRegistry;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useSyncExternalStore, useCallback } from 'react';
|
|
2
|
+
import { clientRegistry, type PageExtension, type ToolExtension, type ComponentOverride } from './registry';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to access extension pages with automatic updates
|
|
6
|
+
*/
|
|
7
|
+
export function useExtensionPages(): PageExtension[] {
|
|
8
|
+
const getSnapshot = useCallback(() => clientRegistry.getPages(), []);
|
|
9
|
+
const subscribe = useCallback((callback: () => void) => clientRegistry.subscribe(callback), []);
|
|
10
|
+
|
|
11
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Hook to access sidebar pages with automatic updates
|
|
16
|
+
*/
|
|
17
|
+
export function useSidebarPages(): PageExtension[] {
|
|
18
|
+
const getSnapshot = useCallback(() => clientRegistry.getSidebarPages(), []);
|
|
19
|
+
const subscribe = useCallback((callback: () => void) => clientRegistry.subscribe(callback), []);
|
|
20
|
+
|
|
21
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Hook to access extension tools with automatic updates
|
|
26
|
+
*/
|
|
27
|
+
export function useExtensionTools(): ToolExtension[] {
|
|
28
|
+
const getSnapshot = useCallback(() => clientRegistry.getTools(), []);
|
|
29
|
+
const subscribe = useCallback((callback: () => void) => clientRegistry.subscribe(callback), []);
|
|
30
|
+
|
|
31
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Hook to get a specific tool renderer
|
|
36
|
+
*/
|
|
37
|
+
export function useToolRenderer(toolName: string): ToolExtension | undefined {
|
|
38
|
+
const getSnapshot = useCallback(() => clientRegistry.getTool(toolName), [toolName]);
|
|
39
|
+
const subscribe = useCallback((callback: () => void) => clientRegistry.subscribe(callback), []);
|
|
40
|
+
|
|
41
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Hook to get a component override
|
|
46
|
+
*/
|
|
47
|
+
export function useComponentOverride(slot: string): ComponentOverride | undefined {
|
|
48
|
+
const getSnapshot = useCallback(() => clientRegistry.getOverride(slot), [slot]);
|
|
49
|
+
const subscribe = useCallback((callback: () => void) => clientRegistry.subscribe(callback), []);
|
|
50
|
+
|
|
51
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
52
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useConfig } from '../contexts/ConfigContext';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns a memoized function to build paths with the configured basePath.
|
|
6
|
+
* Use this for all internal navigation within the authenticated app.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const appPath = useAppPath();
|
|
10
|
+
* navigate(appPath('/')); // Goes to /chat (or whatever basePath is configured)
|
|
11
|
+
* navigate(appPath('/thread/123')); // Goes to /chat/thread/123
|
|
12
|
+
*/
|
|
13
|
+
export function useAppPath(): (path: string) => string {
|
|
14
|
+
const config = useConfig();
|
|
15
|
+
// Default to /chat if basePath is undefined, null, or empty string
|
|
16
|
+
const configBasePath = config?.app?.basePath;
|
|
17
|
+
const basePath = configBasePath && configBasePath.trim() !== '' ? configBasePath : '/chat';
|
|
18
|
+
|
|
19
|
+
return useCallback(
|
|
20
|
+
(path: string) => {
|
|
21
|
+
// Remove leading slash from path if basePath already ends with one
|
|
22
|
+
const normalizedPath = path.startsWith('/') ? path.slice(1) : path;
|
|
23
|
+
const normalizedBase = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
|
24
|
+
|
|
25
|
+
// Handle root path
|
|
26
|
+
if (!normalizedPath || normalizedPath === '') {
|
|
27
|
+
return normalizedBase;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return `${normalizedBase}/${normalizedPath}`;
|
|
31
|
+
},
|
|
32
|
+
[basePath]
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useConfig } from '../contexts/ConfigContext';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the configured basePath for the application.
|
|
5
|
+
* Useful for building absolute URLs or for components that need
|
|
6
|
+
* to know the base path for external navigation.
|
|
7
|
+
*/
|
|
8
|
+
export function useBasePath(): string {
|
|
9
|
+
const config = useConfig();
|
|
10
|
+
// Return basePath if configured and non-empty, otherwise default to /chat
|
|
11
|
+
const configBasePath = config?.app?.basePath;
|
|
12
|
+
return configBasePath && configBasePath.trim() !== '' ? configBasePath : '/chat';
|
|
13
|
+
}
|