@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,214 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
|
|
2
|
+
import type { AppConfig } from '@chaaskit/shared';
|
|
3
|
+
|
|
4
|
+
// Default config - used as fallback while loading
|
|
5
|
+
const defaultConfig: AppConfig = {
|
|
6
|
+
app: {
|
|
7
|
+
name: 'AI Chat',
|
|
8
|
+
description: 'Your AI assistant',
|
|
9
|
+
url: 'http://localhost:5173',
|
|
10
|
+
},
|
|
11
|
+
ui: {
|
|
12
|
+
welcomeTitle: 'Welcome to AI Chat',
|
|
13
|
+
welcomeSubtitle: 'How can I help you today?',
|
|
14
|
+
inputPlaceholder: 'Type your message...',
|
|
15
|
+
samplePrompts: [
|
|
16
|
+
{ label: 'Explain a concept', prompt: 'Explain quantum computing in simple terms' },
|
|
17
|
+
{ label: 'Write code', prompt: 'Write a function to sort an array' },
|
|
18
|
+
{ label: 'Help me brainstorm', prompt: 'Help me brainstorm ideas for a mobile app' },
|
|
19
|
+
],
|
|
20
|
+
logo: '/logo.svg',
|
|
21
|
+
},
|
|
22
|
+
theming: {
|
|
23
|
+
defaultTheme: 'light',
|
|
24
|
+
allowUserThemeSwitch: true,
|
|
25
|
+
themes: {
|
|
26
|
+
light: {
|
|
27
|
+
name: 'Light',
|
|
28
|
+
colors: {
|
|
29
|
+
primary: '#6366f1',
|
|
30
|
+
primaryHover: '#4f46e5',
|
|
31
|
+
secondary: '#8b5cf6',
|
|
32
|
+
background: '#ffffff',
|
|
33
|
+
backgroundSecondary: '#f9fafb',
|
|
34
|
+
sidebar: '#f3f4f6',
|
|
35
|
+
textPrimary: '#111827',
|
|
36
|
+
textSecondary: '#6b7280',
|
|
37
|
+
textMuted: '#9ca3af',
|
|
38
|
+
border: '#e5e7eb',
|
|
39
|
+
inputBackground: '#ffffff',
|
|
40
|
+
inputBorder: '#d1d5db',
|
|
41
|
+
userMessageBg: '#6366f1',
|
|
42
|
+
userMessageText: '#ffffff',
|
|
43
|
+
assistantMessageBg: '#f3f4f6',
|
|
44
|
+
assistantMessageText: '#111827',
|
|
45
|
+
success: '#10b981',
|
|
46
|
+
warning: '#f59e0b',
|
|
47
|
+
error: '#ef4444',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
dark: {
|
|
51
|
+
name: 'Dark',
|
|
52
|
+
colors: {
|
|
53
|
+
primary: '#818cf8',
|
|
54
|
+
primaryHover: '#a5b4fc',
|
|
55
|
+
secondary: '#a78bfa',
|
|
56
|
+
background: '#111827',
|
|
57
|
+
backgroundSecondary: '#1f2937',
|
|
58
|
+
sidebar: '#0f172a',
|
|
59
|
+
textPrimary: '#f9fafb',
|
|
60
|
+
textSecondary: '#d1d5db',
|
|
61
|
+
textMuted: '#6b7280',
|
|
62
|
+
border: '#374151',
|
|
63
|
+
inputBackground: '#1f2937',
|
|
64
|
+
inputBorder: '#4b5563',
|
|
65
|
+
userMessageBg: '#4f46e5',
|
|
66
|
+
userMessageText: '#ffffff',
|
|
67
|
+
assistantMessageBg: '#1f2937',
|
|
68
|
+
assistantMessageText: '#f9fafb',
|
|
69
|
+
success: '#34d399',
|
|
70
|
+
warning: '#fbbf24',
|
|
71
|
+
error: '#f87171',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
fonts: {
|
|
76
|
+
sans: 'Inter, system-ui, sans-serif',
|
|
77
|
+
mono: 'JetBrains Mono, Menlo, monospace',
|
|
78
|
+
},
|
|
79
|
+
borderRadius: {
|
|
80
|
+
sm: '0.25rem',
|
|
81
|
+
md: '0.5rem',
|
|
82
|
+
lg: '0.75rem',
|
|
83
|
+
full: '9999px',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
auth: {
|
|
87
|
+
methods: ['email-password'],
|
|
88
|
+
allowUnauthenticated: false,
|
|
89
|
+
magicLink: {
|
|
90
|
+
enabled: true,
|
|
91
|
+
expiresInMinutes: 15,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
agent: {
|
|
95
|
+
type: 'built-in',
|
|
96
|
+
provider: 'anthropic',
|
|
97
|
+
model: 'claude-sonnet-4-20250514',
|
|
98
|
+
systemPrompt: 'You are a helpful AI assistant.',
|
|
99
|
+
maxTokens: 4096,
|
|
100
|
+
},
|
|
101
|
+
payments: {
|
|
102
|
+
enabled: false,
|
|
103
|
+
provider: 'stripe',
|
|
104
|
+
plans: [
|
|
105
|
+
{
|
|
106
|
+
id: 'free',
|
|
107
|
+
name: 'Free',
|
|
108
|
+
type: 'free',
|
|
109
|
+
params: {
|
|
110
|
+
monthlyMessageLimit: 20,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
legal: {
|
|
116
|
+
privacyPolicyUrl: '/privacy',
|
|
117
|
+
termsOfServiceUrl: '/terms',
|
|
118
|
+
},
|
|
119
|
+
userSettings: {
|
|
120
|
+
fields: [
|
|
121
|
+
{
|
|
122
|
+
key: 'name',
|
|
123
|
+
label: 'Your Name',
|
|
124
|
+
type: 'text',
|
|
125
|
+
placeholder: 'Enter your name',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
key: 'context',
|
|
129
|
+
label: 'Additional Context',
|
|
130
|
+
type: 'textarea',
|
|
131
|
+
placeholder: 'Any context the AI should know about you...',
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
sharing: {
|
|
136
|
+
enabled: false,
|
|
137
|
+
scope: 'public',
|
|
138
|
+
expirationOptions: ['1h', '24h', '7d', '30d', 'never'],
|
|
139
|
+
},
|
|
140
|
+
promptTemplates: {
|
|
141
|
+
enabled: true,
|
|
142
|
+
builtIn: [],
|
|
143
|
+
allowUserTemplates: true,
|
|
144
|
+
},
|
|
145
|
+
teams: {
|
|
146
|
+
enabled: true,
|
|
147
|
+
},
|
|
148
|
+
documents: {
|
|
149
|
+
enabled: false,
|
|
150
|
+
storage: {
|
|
151
|
+
provider: 'database',
|
|
152
|
+
},
|
|
153
|
+
maxFileSizeMB: 10,
|
|
154
|
+
hybridThreshold: 1000,
|
|
155
|
+
acceptedTypes: ['text/plain', 'text/markdown', 'application/json'],
|
|
156
|
+
},
|
|
157
|
+
projects: {
|
|
158
|
+
enabled: false,
|
|
159
|
+
colors: ['#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#f97316', '#eab308', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6'],
|
|
160
|
+
},
|
|
161
|
+
api: {
|
|
162
|
+
enabled: false,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
interface ConfigContextValue {
|
|
167
|
+
config: AppConfig;
|
|
168
|
+
configLoaded: boolean;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const ConfigContext = createContext<ConfigContextValue>({
|
|
172
|
+
config: defaultConfig,
|
|
173
|
+
configLoaded: false,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
export function ConfigProvider({ children }: { children: ReactNode }) {
|
|
177
|
+
const [config, setConfig] = useState<AppConfig>(defaultConfig);
|
|
178
|
+
const [configLoaded, setConfigLoaded] = useState(false);
|
|
179
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
async function loadConfig() {
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetch('/api/config');
|
|
184
|
+
if (response.ok) {
|
|
185
|
+
const serverConfig = await response.json();
|
|
186
|
+
// Merge with defaults to ensure all fields exist
|
|
187
|
+
setConfig({
|
|
188
|
+
...defaultConfig,
|
|
189
|
+
...serverConfig,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.warn('[Config] Failed to load config from server:', error);
|
|
194
|
+
} finally {
|
|
195
|
+
setConfigLoaded(true);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
loadConfig();
|
|
199
|
+
}, []);
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<ConfigContext.Provider value={{ config, configLoaded }}>
|
|
203
|
+
{children}
|
|
204
|
+
</ConfigContext.Provider>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function useConfig() {
|
|
209
|
+
return useContext(ConfigContext).config;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function useConfigLoaded() {
|
|
213
|
+
return useContext(ConfigContext).configLoaded;
|
|
214
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useState,
|
|
5
|
+
useEffect,
|
|
6
|
+
useCallback,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import type { Project, ProjectWithThreadCount, ProjectSharing, CreateProjectInput, UpdateProjectInput } from '@chaaskit/shared';
|
|
10
|
+
import { api } from '../utils/api';
|
|
11
|
+
import { useAuth } from './AuthContext';
|
|
12
|
+
import { useTeam } from './TeamContext';
|
|
13
|
+
import { useConfig } from './ConfigContext';
|
|
14
|
+
|
|
15
|
+
interface ProjectContextType {
|
|
16
|
+
projects: ProjectWithThreadCount[];
|
|
17
|
+
currentProject: Project | null;
|
|
18
|
+
currentProjectId: string | null;
|
|
19
|
+
isLoadingProjects: boolean;
|
|
20
|
+
projectsEnabled: boolean;
|
|
21
|
+
projectColors: string[];
|
|
22
|
+
loadProjects: () => Promise<void>;
|
|
23
|
+
setCurrentProjectId: (projectId: string | null) => void;
|
|
24
|
+
createProject: (data: CreateProjectInput) => Promise<Project>;
|
|
25
|
+
updateProject: (projectId: string, updates: UpdateProjectInput) => Promise<void>;
|
|
26
|
+
archiveProject: (projectId: string) => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ProjectContext = createContext<ProjectContextType | undefined>(undefined);
|
|
30
|
+
|
|
31
|
+
export function ProjectProvider({ children }: { children: ReactNode }) {
|
|
32
|
+
const { user } = useAuth();
|
|
33
|
+
const { currentTeamId } = useTeam();
|
|
34
|
+
const config = useConfig();
|
|
35
|
+
const [projects, setProjects] = useState<ProjectWithThreadCount[]>([]);
|
|
36
|
+
const [currentProject, setCurrentProject] = useState<Project | null>(null);
|
|
37
|
+
const [currentProjectId, setCurrentProjectId] = useState<string | null>(null);
|
|
38
|
+
const [isLoadingProjects, setIsLoadingProjects] = useState(false);
|
|
39
|
+
|
|
40
|
+
const projectsEnabled = config?.projects?.enabled ?? false;
|
|
41
|
+
const projectColors = config?.projects?.colors ?? [];
|
|
42
|
+
|
|
43
|
+
const loadProjects = useCallback(async () => {
|
|
44
|
+
if (!user || !projectsEnabled) {
|
|
45
|
+
setProjects([]);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setIsLoadingProjects(true);
|
|
50
|
+
try {
|
|
51
|
+
const params = new URLSearchParams();
|
|
52
|
+
if (currentTeamId) {
|
|
53
|
+
params.set('teamId', currentTeamId);
|
|
54
|
+
}
|
|
55
|
+
const url = `/api/projects${params.toString() ? `?${params.toString()}` : ''}`;
|
|
56
|
+
const response = await api.get<{ projects: ProjectWithThreadCount[] }>(url);
|
|
57
|
+
setProjects(response.projects);
|
|
58
|
+
|
|
59
|
+
// If current project is no longer available, clear it
|
|
60
|
+
if (currentProjectId && !response.projects.some((p) => p.id === currentProjectId)) {
|
|
61
|
+
setCurrentProjectId(null);
|
|
62
|
+
setCurrentProject(null);
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('Failed to load projects:', error);
|
|
66
|
+
setProjects([]);
|
|
67
|
+
} finally {
|
|
68
|
+
setIsLoadingProjects(false);
|
|
69
|
+
}
|
|
70
|
+
}, [user, projectsEnabled, currentTeamId, currentProjectId]);
|
|
71
|
+
|
|
72
|
+
const loadProjectDetails = useCallback(async (projectId: string) => {
|
|
73
|
+
try {
|
|
74
|
+
const response = await api.get<{ project: Project }>(`/api/projects/${projectId}`);
|
|
75
|
+
setCurrentProject(response.project);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Failed to load project details:', error);
|
|
78
|
+
setCurrentProject(null);
|
|
79
|
+
}
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const handleSetCurrentProjectId = useCallback((projectId: string | null) => {
|
|
83
|
+
setCurrentProjectId(projectId);
|
|
84
|
+
if (!projectId) {
|
|
85
|
+
setCurrentProject(null);
|
|
86
|
+
}
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
const createProject = useCallback(async (data: CreateProjectInput): Promise<Project> => {
|
|
90
|
+
// If we're in a team context, add the teamId
|
|
91
|
+
const projectData = {
|
|
92
|
+
...data,
|
|
93
|
+
teamId: currentTeamId || undefined,
|
|
94
|
+
};
|
|
95
|
+
const response = await api.post<{ project: Project }>('/api/projects', projectData);
|
|
96
|
+
await loadProjects();
|
|
97
|
+
return response.project;
|
|
98
|
+
}, [loadProjects, currentTeamId]);
|
|
99
|
+
|
|
100
|
+
const updateProject = useCallback(async (projectId: string, updates: UpdateProjectInput) => {
|
|
101
|
+
await api.patch(`/api/projects/${projectId}`, updates);
|
|
102
|
+
await loadProjects();
|
|
103
|
+
if (currentProjectId === projectId) {
|
|
104
|
+
await loadProjectDetails(projectId);
|
|
105
|
+
}
|
|
106
|
+
}, [loadProjects, loadProjectDetails, currentProjectId]);
|
|
107
|
+
|
|
108
|
+
const archiveProject = useCallback(async (projectId: string) => {
|
|
109
|
+
await api.post(`/api/projects/${projectId}/archive`, {});
|
|
110
|
+
if (currentProjectId === projectId) {
|
|
111
|
+
handleSetCurrentProjectId(null);
|
|
112
|
+
}
|
|
113
|
+
await loadProjects();
|
|
114
|
+
}, [loadProjects, currentProjectId, handleSetCurrentProjectId]);
|
|
115
|
+
|
|
116
|
+
// Load projects when user or team changes
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (user && projectsEnabled) {
|
|
119
|
+
loadProjects();
|
|
120
|
+
} else {
|
|
121
|
+
setProjects([]);
|
|
122
|
+
setCurrentProject(null);
|
|
123
|
+
setCurrentProjectId(null);
|
|
124
|
+
}
|
|
125
|
+
}, [user, projectsEnabled, currentTeamId, loadProjects]);
|
|
126
|
+
|
|
127
|
+
// Load project details when current project changes
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (currentProjectId) {
|
|
130
|
+
loadProjectDetails(currentProjectId);
|
|
131
|
+
}
|
|
132
|
+
}, [currentProjectId, loadProjectDetails]);
|
|
133
|
+
|
|
134
|
+
// Clear project selection when team changes
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
setCurrentProjectId(null);
|
|
137
|
+
setCurrentProject(null);
|
|
138
|
+
}, [currentTeamId]);
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<ProjectContext.Provider
|
|
142
|
+
value={{
|
|
143
|
+
projects,
|
|
144
|
+
currentProject,
|
|
145
|
+
currentProjectId,
|
|
146
|
+
isLoadingProjects,
|
|
147
|
+
projectsEnabled,
|
|
148
|
+
projectColors,
|
|
149
|
+
loadProjects,
|
|
150
|
+
setCurrentProjectId: handleSetCurrentProjectId,
|
|
151
|
+
createProject,
|
|
152
|
+
updateProject,
|
|
153
|
+
archiveProject,
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
{children}
|
|
157
|
+
</ProjectContext.Provider>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function useProject() {
|
|
162
|
+
const context = useContext(ProjectContext);
|
|
163
|
+
if (context === undefined) {
|
|
164
|
+
throw new Error('useProject must be used within a ProjectProvider');
|
|
165
|
+
}
|
|
166
|
+
return context;
|
|
167
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from 'react';
|
|
2
|
+
import type { AppConfig } from '@chaaskit/shared';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Server-safe config provider that accepts pre-loaded config data.
|
|
6
|
+
* Used for SSR routes where config is loaded in the Remix loader.
|
|
7
|
+
* Does not make any API calls or use browser APIs.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface ServerConfigContextValue {
|
|
11
|
+
config: Partial<AppConfig>;
|
|
12
|
+
configLoaded: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ServerConfigContext = createContext<ServerConfigContextValue | null>(null);
|
|
16
|
+
|
|
17
|
+
export interface ServerConfigProviderProps {
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
config: Partial<AppConfig>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ServerConfigProvider({ children, config }: ServerConfigProviderProps) {
|
|
23
|
+
return (
|
|
24
|
+
<ServerConfigContext.Provider value={{ config, configLoaded: true }}>
|
|
25
|
+
{children}
|
|
26
|
+
</ServerConfigContext.Provider>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useServerConfig(): Partial<AppConfig> {
|
|
31
|
+
const context = useContext(ServerConfigContext);
|
|
32
|
+
if (!context) {
|
|
33
|
+
throw new Error('useServerConfig must be used within a ServerConfigProvider');
|
|
34
|
+
}
|
|
35
|
+
return context.config;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useServerConfigLoaded(): boolean {
|
|
39
|
+
const context = useContext(ServerConfigContext);
|
|
40
|
+
return context?.configLoaded ?? false;
|
|
41
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Server-safe theme provider that accepts a pre-determined theme.
|
|
5
|
+
* Used for SSR routes where theme is determined on the server.
|
|
6
|
+
* Does not access localStorage or document.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface ServerThemeContextValue {
|
|
10
|
+
theme: string;
|
|
11
|
+
availableThemes: string[];
|
|
12
|
+
// setTheme is a no-op on server, actual theme switching happens client-side
|
|
13
|
+
setTheme: (theme: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ServerThemeContext = createContext<ServerThemeContextValue | null>(null);
|
|
17
|
+
|
|
18
|
+
export interface ServerThemeProviderProps {
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
theme: string;
|
|
21
|
+
availableThemes: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ServerThemeProvider({
|
|
25
|
+
children,
|
|
26
|
+
theme,
|
|
27
|
+
availableThemes,
|
|
28
|
+
}: ServerThemeProviderProps) {
|
|
29
|
+
// Server-side setTheme is a no-op - theme changes require client-side hydration
|
|
30
|
+
const setTheme = () => {
|
|
31
|
+
// No-op on server
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<ServerThemeContext.Provider value={{ theme, availableThemes, setTheme }}>
|
|
36
|
+
{children}
|
|
37
|
+
</ServerThemeContext.Provider>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function useServerTheme() {
|
|
42
|
+
const context = useContext(ServerThemeContext);
|
|
43
|
+
if (!context) {
|
|
44
|
+
throw new Error('useServerTheme must be used within a ServerThemeProvider');
|
|
45
|
+
}
|
|
46
|
+
return context;
|
|
47
|
+
}
|