@chaaskit/client 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/dist/lib/index.js +1023 -160
  3. package/dist/lib/index.js.map +1 -1
  4. package/dist/lib/routes/AcceptInviteRoute.js +1 -1
  5. package/dist/lib/routes/AcceptInviteRoute.js.map +1 -1
  6. package/dist/lib/routes/AdminDashboardRoute.js +1 -1
  7. package/dist/lib/routes/AdminDashboardRoute.js.map +1 -1
  8. package/dist/lib/routes/AdminPromoCodesRoute.js +19 -0
  9. package/dist/lib/routes/AdminPromoCodesRoute.js.map +1 -0
  10. package/dist/lib/routes/AdminTeamRoute.js +1 -1
  11. package/dist/lib/routes/AdminTeamRoute.js.map +1 -1
  12. package/dist/lib/routes/AdminTeamsRoute.js +1 -1
  13. package/dist/lib/routes/AdminTeamsRoute.js.map +1 -1
  14. package/dist/lib/routes/AdminUsersRoute.js +1 -1
  15. package/dist/lib/routes/AdminUsersRoute.js.map +1 -1
  16. package/dist/lib/routes/AdminWaitlistRoute.js +19 -0
  17. package/dist/lib/routes/AdminWaitlistRoute.js.map +1 -0
  18. package/dist/lib/routes/ApiKeysRoute.js +1 -1
  19. package/dist/lib/routes/ApiKeysRoute.js.map +1 -1
  20. package/dist/lib/routes/AutomationsRoute.js +1 -1
  21. package/dist/lib/routes/AutomationsRoute.js.map +1 -1
  22. package/dist/lib/routes/ChatRoute.js +1 -1
  23. package/dist/lib/routes/ChatRoute.js.map +1 -1
  24. package/dist/lib/routes/DocumentsRoute.js +1 -1
  25. package/dist/lib/routes/DocumentsRoute.js.map +1 -1
  26. package/dist/lib/routes/OAuthConsentRoute.js +1 -1
  27. package/dist/lib/routes/OAuthConsentRoute.js.map +1 -1
  28. package/dist/lib/routes/PricingRoute.js +1 -1
  29. package/dist/lib/routes/PricingRoute.js.map +1 -1
  30. package/dist/lib/routes/PrivacyRoute.js +1 -1
  31. package/dist/lib/routes/PrivacyRoute.js.map +1 -1
  32. package/dist/lib/routes/TeamSettingsRoute.js +1 -1
  33. package/dist/lib/routes/TeamSettingsRoute.js.map +1 -1
  34. package/dist/lib/routes/TermsRoute.js +1 -1
  35. package/dist/lib/routes/TermsRoute.js.map +1 -1
  36. package/dist/lib/routes/VerifyEmailRoute.js +1 -1
  37. package/dist/lib/routes/VerifyEmailRoute.js.map +1 -1
  38. package/dist/lib/routes.js +47 -37
  39. package/dist/lib/routes.js.map +1 -1
  40. package/dist/lib/ssr-utils.js +64 -1
  41. package/dist/lib/ssr-utils.js.map +1 -1
  42. package/dist/lib/ssr.js +23 -0
  43. package/dist/lib/ssr.js.map +1 -1
  44. package/dist/lib/styles.css +58 -62
  45. package/dist/lib/useExtensions-B5nX_8XD.js.map +1 -1
  46. package/package.json +25 -12
  47. package/src/components/MessageItem.tsx +35 -4
  48. package/src/components/MessageList.tsx +51 -5
  49. package/src/components/OAuthAppsSection.tsx +1 -1
  50. package/src/components/Sidebar.tsx +1 -3
  51. package/src/components/ToolCallDisplay.tsx +102 -11
  52. package/src/components/tool-renderers/DocumentListRenderer.tsx +44 -0
  53. package/src/components/tool-renderers/DocumentReadRenderer.tsx +33 -0
  54. package/src/components/tool-renderers/DocumentSaveRenderer.tsx +32 -0
  55. package/src/components/tool-renderers/DocumentSearchRenderer.tsx +33 -0
  56. package/src/components/tool-renderers/index.ts +36 -0
  57. package/src/components/tool-renderers/utils.ts +7 -0
  58. package/src/contexts/AuthContext.tsx +16 -6
  59. package/src/contexts/ConfigContext.tsx +60 -28
  60. package/src/contexts/ThemeContext.tsx +39 -68
  61. package/src/extensions/registry.ts +2 -1
  62. package/src/hooks/__tests__/basePath.test.ts +42 -0
  63. package/src/index.tsx +11 -2
  64. package/src/pages/AdminDashboardPage.tsx +15 -1
  65. package/src/pages/AdminPromoCodesPage.tsx +378 -0
  66. package/src/pages/AdminTeamPage.tsx +29 -1
  67. package/src/pages/AdminTeamsPage.tsx +15 -1
  68. package/src/pages/AdminUsersPage.tsx +15 -1
  69. package/src/pages/AdminWaitlistPage.tsx +156 -0
  70. package/src/pages/RegisterPage.tsx +91 -9
  71. package/src/routes/AcceptInviteRoute.tsx +1 -1
  72. package/src/routes/AdminDashboardRoute.tsx +1 -1
  73. package/src/routes/AdminPromoCodesRoute.tsx +24 -0
  74. package/src/routes/AdminTeamRoute.tsx +1 -1
  75. package/src/routes/AdminTeamsRoute.tsx +1 -1
  76. package/src/routes/AdminUsersRoute.tsx +1 -1
  77. package/src/routes/AdminWaitlistRoute.tsx +24 -0
  78. package/src/routes/ApiKeysRoute.tsx +1 -1
  79. package/src/routes/AutomationsRoute.tsx +1 -1
  80. package/src/routes/ChatRoute.tsx +2 -1
  81. package/src/routes/DocumentsRoute.tsx +1 -1
  82. package/src/routes/OAuthConsentRoute.tsx +1 -1
  83. package/src/routes/PricingRoute.tsx +1 -1
  84. package/src/routes/PrivacyRoute.tsx +1 -1
  85. package/src/routes/TeamSettingsRoute.tsx +1 -1
  86. package/src/routes/TermsRoute.tsx +1 -1
  87. package/src/routes/VerifyEmailRoute.tsx +1 -1
  88. package/src/routes/index.ts +2 -0
  89. package/src/ssr-utils.tsx +100 -1
  90. package/src/ssr.ts +59 -0
  91. package/src/stores/chatStore.ts +5 -0
  92. package/src/styles/index.css +16 -63
  93. package/src/tailwind-preset.js +360 -0
  94. package/dist/favicon.svg +0 -11
  95. package/dist/index.html +0 -17
  96. package/dist/logo.svg +0 -12
@@ -19,6 +19,8 @@ const AUTO_APPROVE_LABELS: Record<AutoApproveReason, string> = {
19
19
  thread_allowed: 'Allowed for this chat',
20
20
  };
21
21
 
22
+ const TOOL_UI_MESSAGE_SOURCE = 'chaaskit-tool-ui';
23
+
22
24
  // Generate the window.openai initialization script for OpenAI format resources
23
25
  function generateOpenAiScript(
24
26
  toolInput: Record<string, unknown>,
@@ -68,6 +70,46 @@ function generateOpenAiScript(
68
70
  </style>
69
71
  <script>
70
72
  (function() {
73
+ const MESSAGE_SOURCE = '${TOOL_UI_MESSAGE_SOURCE}';
74
+ let requestId = 0;
75
+ const pending = new Map();
76
+
77
+ function resolvePending(id, data) {
78
+ const entry = pending.get(id);
79
+ if (!entry) return;
80
+ pending.delete(id);
81
+ if (data && data.ok) {
82
+ entry.resolve(data.result);
83
+ return;
84
+ }
85
+ entry.resolve({ error: (data && data.error) || 'Tool UI bridge error' });
86
+ }
87
+
88
+ window.addEventListener('message', (event) => {
89
+ const data = event.data || {};
90
+ if (data.source !== MESSAGE_SOURCE || !data.id) return;
91
+ resolvePending(data.id, data);
92
+ });
93
+
94
+ function sendToParent(type, payload, timeoutMs = 15000) {
95
+ return new Promise((resolve) => {
96
+ const id = String(Date.now()) + '-' + String(requestId++);
97
+ pending.set(id, { resolve });
98
+ try {
99
+ window.parent.postMessage({ source: MESSAGE_SOURCE, type, id, payload }, '*');
100
+ } catch (err) {
101
+ pending.delete(id);
102
+ resolve({ error: 'Unable to reach host window' });
103
+ return;
104
+ }
105
+ setTimeout(() => {
106
+ if (!pending.has(id)) return;
107
+ pending.delete(id);
108
+ resolve({ error: 'Tool UI request timed out' });
109
+ }, timeoutMs);
110
+ });
111
+ }
112
+
71
113
  // Initialize window.openai with the OpenAI Apps SDK spec
72
114
  window.openai = {
73
115
  // Core globals
@@ -92,17 +134,20 @@ function generateOpenAiScript(
92
134
  // API methods
93
135
  callTool: async (name, args) => {
94
136
  console.log('window.openai.callTool called:', { name, args });
95
- // TODO: Implement actual tool calling via parent window messaging
96
- return {
97
- content: [{ type: 'text', text: 'Tool calling not yet implemented' }],
98
- isError: false
99
- };
137
+ const response = await sendToParent('callTool', { name, args });
138
+ if (response && response.error) {
139
+ return {
140
+ content: [{ type: 'text', text: response.error }],
141
+ isError: true
142
+ };
143
+ }
144
+ return response || { content: [{ type: 'text', text: 'No response from host' }], isError: true };
100
145
  },
101
146
 
102
147
  sendFollowUpMessage: async (args) => {
103
148
  console.log('window.openai.sendFollowUpMessage called:', args);
104
- // TODO: Implement via parent window messaging
105
- return {};
149
+ const response = await sendToParent('sendFollowUpMessage', args);
150
+ return response && response.error ? { error: response.error } : (response || {});
106
151
  },
107
152
 
108
153
  openExternal: (payload) => {
@@ -114,27 +159,41 @@ function generateOpenAiScript(
114
159
 
115
160
  requestDisplayMode: async (args) => {
116
161
  console.log('window.openai.requestDisplayMode called:', args);
117
- return { mode: args.mode };
162
+ const response = await sendToParent('requestDisplayMode', args);
163
+ if (response && response.error) {
164
+ return { mode: args.mode };
165
+ }
166
+ return response || { mode: args.mode };
118
167
  },
119
168
 
120
169
  setWidgetState: async (state) => {
121
170
  console.log('window.openai.setWidgetState called:', state);
122
171
  window.openai.widgetState = state;
123
- return {};
172
+ const response = await sendToParent('setWidgetState', state);
173
+ return response && response.error ? {} : (response || {});
124
174
  },
125
175
 
126
176
  requestClose: () => {
127
177
  console.log('window.openai.requestClose called');
178
+ sendToParent('requestClose', {});
128
179
  },
129
180
 
130
181
  getFileDownloadUrl: async ({ fileId }) => {
131
182
  console.log('window.openai.getFileDownloadUrl called:', fileId);
132
- return { url: '' };
183
+ const response = await sendToParent('getFileDownloadUrl', { fileId });
184
+ if (response && response.error) {
185
+ return { url: '' };
186
+ }
187
+ return response || { url: '' };
133
188
  },
134
189
 
135
190
  uploadFile: async (file) => {
136
191
  console.log('window.openai.uploadFile called:', file);
137
- return { fileId: '' };
192
+ const response = await sendToParent('uploadFile', { file });
193
+ if (response && response.error) {
194
+ return { fileId: '' };
195
+ }
196
+ return response || { fileId: '' };
138
197
  }
139
198
  };
140
199
 
@@ -199,6 +258,38 @@ export function UIResourceWidget({ uiResource, theme }: { uiResource: UIResource
199
258
  }
200
259
 
201
260
  export default function ToolCallDisplay({ toolCall, toolResult, isPending, uiResource, hideUiResource, autoApproveReason }: ToolCallDisplayProps) {
261
+ useEffect(() => {
262
+ async function handleMessage(event: MessageEvent) {
263
+ const data = event.data || {};
264
+ if (data.source !== TOOL_UI_MESSAGE_SOURCE || !data.id) return;
265
+
266
+ const targetOrigin = event.origin && event.origin !== 'null' ? event.origin : '*';
267
+
268
+ try {
269
+ const handler = (window as unknown as { chaaskitToolUiHandler?: (payload: unknown) => Promise<unknown> }).chaaskitToolUiHandler;
270
+ const result = typeof handler === 'function'
271
+ ? await handler({ type: data.type, payload: data.payload })
272
+ : { error: 'Tool UI bridge not configured' };
273
+
274
+ const sourceWindow = event.source as Window | null;
275
+ sourceWindow?.postMessage(
276
+ { source: TOOL_UI_MESSAGE_SOURCE, id: data.id, ok: true, result },
277
+ targetOrigin
278
+ );
279
+ } catch (err) {
280
+ const message = err instanceof Error ? err.message : 'Tool UI bridge error';
281
+ const sourceWindow = event.source as Window | null;
282
+ sourceWindow?.postMessage(
283
+ { source: TOOL_UI_MESSAGE_SOURCE, id: data.id, ok: false, error: message },
284
+ targetOrigin
285
+ );
286
+ }
287
+ }
288
+
289
+ window.addEventListener('message', handleMessage);
290
+ return () => window.removeEventListener('message', handleMessage);
291
+ }, []);
292
+
202
293
  // Check if we have HTML content to render
203
294
  const hasHtmlResource = !hideUiResource && uiResource?.text &&
204
295
  (uiResource.mimeType?.includes('html') || uiResource.text.trim().startsWith('<'));
@@ -0,0 +1,44 @@
1
+ import type { ToolCall, ToolResult } from '@chaaskit/shared';
2
+ import { getTextContent } from './utils';
3
+
4
+ interface DocumentSummary {
5
+ id?: string;
6
+ path?: string;
7
+ name?: string;
8
+ mimeType?: string;
9
+ charCount?: number;
10
+ teamId?: string | null;
11
+ projectId?: string | null;
12
+ }
13
+
14
+ export default function DocumentListRenderer({ toolCall, toolResult }: { toolCall: ToolCall; toolResult: ToolResult }) {
15
+ const structured = toolResult.structuredContent as { documents?: DocumentSummary[] } | undefined;
16
+ const documents = structured?.documents ?? [];
17
+ const fallback = getTextContent(toolResult.content);
18
+
19
+ return (
20
+ <div className="space-y-2">
21
+ <div className="text-sm font-semibold text-text-primary">Documents</div>
22
+ {documents.length > 0 ? (
23
+ <ul className="space-y-2">
24
+ {documents.map((doc, index) => (
25
+ <li key={doc.id ?? doc.path ?? index} className="rounded-md border border-border bg-background px-3 py-2">
26
+ <div className="text-sm font-medium text-text-primary">{doc.path ?? doc.name ?? 'Untitled document'}</div>
27
+ <div className="text-xs text-text-muted">
28
+ {doc.mimeType ?? 'text/plain'}
29
+ {typeof doc.charCount === 'number' ? ` • ${doc.charCount} chars` : ''}
30
+ {doc.teamId ? ' • team' : ''}
31
+ {doc.projectId ? ' • project' : ''}
32
+ </div>
33
+ </li>
34
+ ))}
35
+ </ul>
36
+ ) : (
37
+ <div className="text-sm text-text-secondary">
38
+ {fallback ?? 'No documents found.'}
39
+ </div>
40
+ )}
41
+ <div className="text-xs text-text-muted">Tool: {toolCall.toolName}</div>
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,33 @@
1
+ import type { ToolCall, ToolResult } from '@chaaskit/shared';
2
+ import { getTextContent } from './utils';
3
+
4
+ interface ReadSummary {
5
+ path?: string;
6
+ offset?: number;
7
+ linesReturned?: number;
8
+ totalLines?: number;
9
+ truncated?: boolean;
10
+ }
11
+
12
+ export default function DocumentReadRenderer({ toolCall, toolResult }: { toolCall: ToolCall; toolResult: ToolResult }) {
13
+ const structured = toolResult.structuredContent as ReadSummary | undefined;
14
+ const fallback = getTextContent(toolResult.content);
15
+ const rangeText = structured
16
+ ? `Lines ${Number(structured.offset ?? 0) + 1}-${Number(structured.offset ?? 0) + Number(structured.linesReturned ?? 0)} of ${structured.totalLines ?? 'unknown'}`
17
+ : null;
18
+
19
+ return (
20
+ <div className="space-y-2">
21
+ <div className="text-sm font-semibold text-text-primary">Document Preview</div>
22
+ {structured ? (
23
+ <div className="rounded-md border border-border bg-background px-3 py-2 text-sm text-text-secondary">
24
+ <div className="font-medium text-text-primary">{structured.path ?? 'Document'}</div>
25
+ {rangeText && <div className="text-xs text-text-muted">{rangeText}{structured.truncated ? ' (truncated)' : ''}</div>}
26
+ </div>
27
+ ) : (
28
+ <div className="text-sm text-text-secondary">{fallback ?? 'No structured document details.'}</div>
29
+ )}
30
+ <div className="text-xs text-text-muted">Tool: {toolCall.toolName}</div>
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,32 @@
1
+ import type { ToolCall, ToolResult } from '@chaaskit/shared';
2
+ import { getTextContent } from './utils';
3
+
4
+ interface SaveSummary {
5
+ success?: boolean;
6
+ path?: string;
7
+ id?: string;
8
+ charCount?: number;
9
+ }
10
+
11
+ export default function DocumentSaveRenderer({ toolCall, toolResult }: { toolCall: ToolCall; toolResult: ToolResult }) {
12
+ const structured = toolResult.structuredContent as SaveSummary | undefined;
13
+ const fallback = getTextContent(toolResult.content);
14
+ const success = structured?.success !== false && !toolResult.isError;
15
+
16
+ return (
17
+ <div className="space-y-2">
18
+ <div className="text-sm font-semibold text-text-primary">Save Document</div>
19
+ {structured ? (
20
+ <div className={`rounded-md border px-3 py-2 text-sm ${success ? 'border-success/30 bg-success/10 text-success' : 'border-error/30 bg-error/10 text-error'}`}>
21
+ <div className="font-medium">{success ? 'Saved' : 'Failed'}</div>
22
+ {structured.path && <div className="text-xs">Path: {structured.path}</div>}
23
+ {structured.id && <div className="text-xs">ID: {structured.id}</div>}
24
+ {typeof structured.charCount === 'number' && <div className="text-xs">Size: {structured.charCount} chars</div>}
25
+ </div>
26
+ ) : (
27
+ <div className="text-sm text-text-secondary">{fallback ?? 'No structured save data.'}</div>
28
+ )}
29
+ <div className="text-xs text-text-muted">Tool: {toolCall.toolName}</div>
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,33 @@
1
+ import type { ToolCall, ToolResult } from '@chaaskit/shared';
2
+ import { getTextContent } from './utils';
3
+
4
+ interface SearchSummary {
5
+ path?: string;
6
+ query?: string;
7
+ matchCount?: number;
8
+ matchLines?: number[];
9
+ }
10
+
11
+ export default function DocumentSearchRenderer({ toolCall, toolResult }: { toolCall: ToolCall; toolResult: ToolResult }) {
12
+ const structured = toolResult.structuredContent as SearchSummary | undefined;
13
+ const fallback = getTextContent(toolResult.content);
14
+
15
+ return (
16
+ <div className="space-y-2">
17
+ <div className="text-sm font-semibold text-text-primary">Document Search</div>
18
+ {structured ? (
19
+ <div className="rounded-md border border-border bg-background px-3 py-2 text-sm text-text-secondary">
20
+ <div className="font-medium text-text-primary">{structured.path ?? 'Document'}</div>
21
+ <div className="text-xs text-text-muted">Query: {structured.query ?? 'unknown'}</div>
22
+ <div className="text-xs text-text-muted">Matches: {structured.matchCount ?? 0}</div>
23
+ {structured.matchLines && structured.matchLines.length > 0 && (
24
+ <div className="text-xs text-text-muted">Lines: {structured.matchLines.join(', ')}</div>
25
+ )}
26
+ </div>
27
+ ) : (
28
+ <div className="text-sm text-text-secondary">{fallback ?? 'No structured search data.'}</div>
29
+ )}
30
+ <div className="text-xs text-text-muted">Tool: {toolCall.toolName}</div>
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,36 @@
1
+ import { clientRegistry } from '../../extensions/registry';
2
+ import DocumentListRenderer from './DocumentListRenderer';
3
+ import DocumentReadRenderer from './DocumentReadRenderer';
4
+ import DocumentSearchRenderer from './DocumentSearchRenderer';
5
+ import DocumentSaveRenderer from './DocumentSaveRenderer';
6
+
7
+ clientRegistry.registerTool({
8
+ name: 'list_documents',
9
+ description: 'Render document list results',
10
+ resultRenderer: DocumentListRenderer,
11
+ });
12
+
13
+ clientRegistry.registerTool({
14
+ name: 'read_document',
15
+ description: 'Render document read results',
16
+ resultRenderer: DocumentReadRenderer,
17
+ });
18
+
19
+ clientRegistry.registerTool({
20
+ name: 'search_in_document',
21
+ description: 'Render document search results',
22
+ resultRenderer: DocumentSearchRenderer,
23
+ });
24
+
25
+ clientRegistry.registerTool({
26
+ name: 'save_document',
27
+ description: 'Render document save results',
28
+ resultRenderer: DocumentSaveRenderer,
29
+ });
30
+
31
+ export {
32
+ DocumentListRenderer,
33
+ DocumentReadRenderer,
34
+ DocumentSearchRenderer,
35
+ DocumentSaveRenderer,
36
+ };
@@ -0,0 +1,7 @@
1
+ import type { MCPContent } from '@chaaskit/shared';
2
+
3
+ export function getTextContent(content?: MCPContent[]): string | null {
4
+ if (!content) return null;
5
+ const firstText = content.find((item) => item.type === 'text' && typeof item.text === 'string');
6
+ return firstText?.text ?? null;
7
+ }
@@ -20,9 +20,14 @@ interface AuthContextType {
20
20
  user: UserSession | null;
21
21
  isLoading: boolean;
22
22
  login: (email: string, password: string) => Promise<LoginResult>;
23
- register: (email: string, password: string, name?: string) => Promise<RegisterResult>;
23
+ register: (
24
+ email: string,
25
+ password: string,
26
+ name?: string,
27
+ options?: { inviteToken?: string; referralCode?: string }
28
+ ) => Promise<RegisterResult>;
24
29
  logout: () => Promise<void>;
25
- sendMagicLink: (email: string) => Promise<void>;
30
+ sendMagicLink: (email: string, inviteToken?: string) => Promise<void>;
26
31
  verifyEmail: (code: string) => Promise<void>;
27
32
  resendVerification: () => Promise<void>;
28
33
  }
@@ -59,12 +64,17 @@ export function AuthProvider({ children }: { children: ReactNode }) {
59
64
  return { requiresVerification: response.requiresVerification ?? false };
60
65
  }
61
66
 
62
- async function register(email: string, password: string, name?: string): Promise<RegisterResult> {
67
+ async function register(
68
+ email: string,
69
+ password: string,
70
+ name?: string,
71
+ options?: { inviteToken?: string; referralCode?: string }
72
+ ): Promise<RegisterResult> {
63
73
  const response = await api.post<{
64
74
  user: UserSession;
65
75
  token: string;
66
76
  requiresVerification?: boolean;
67
- }>('/api/auth/register', { email, password, name });
77
+ }>('/api/auth/register', { email, password, name, ...options });
68
78
  setUser(response.user);
69
79
  return { requiresVerification: response.requiresVerification ?? false };
70
80
  }
@@ -74,8 +84,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
74
84
  setUser(null);
75
85
  }
76
86
 
77
- async function sendMagicLink(email: string) {
78
- await api.post('/api/auth/magic-link', { email });
87
+ async function sendMagicLink(email: string, inviteToken?: string) {
88
+ await api.post('/api/auth/magic-link', { email, inviteToken });
79
89
  }
80
90
 
81
91
  async function verifyEmail(code: string) {
@@ -1,12 +1,32 @@
1
1
  import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
2
- import type { AppConfig } from '@chaaskit/shared';
2
+ import type { PublicAppConfig } from '@chaaskit/shared';
3
+
4
+ // Declare global window property for SSR-injected config
5
+ declare global {
6
+ interface Window {
7
+ __CHAASKIT_CONFIG__?: PublicAppConfig;
8
+ }
9
+ }
10
+
11
+ /**
12
+ * Get config injected via ConfigScript in the HTML head.
13
+ * This allows the config to be available immediately on page load,
14
+ * avoiding flash of default values.
15
+ */
16
+ function getInjectedConfig(): PublicAppConfig | undefined {
17
+ if (typeof window !== 'undefined' && window.__CHAASKIT_CONFIG__) {
18
+ return window.__CHAASKIT_CONFIG__;
19
+ }
20
+ return undefined;
21
+ }
3
22
 
4
23
  // Default config - used as fallback while loading
5
- const defaultConfig: AppConfig = {
24
+ const defaultConfig: PublicAppConfig = {
6
25
  app: {
7
26
  name: 'AI Chat',
8
27
  description: 'Your AI assistant',
9
28
  url: 'http://localhost:5173',
29
+ basePath: '/chat',
10
30
  },
11
31
  ui: {
12
32
  welcomeTitle: 'Welcome to AI Chat',
@@ -90,27 +110,15 @@ const defaultConfig: AppConfig = {
90
110
  enabled: true,
91
111
  expiresInMinutes: 15,
92
112
  },
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,
113
+ gating: {
114
+ mode: 'open',
115
+ waitlistEnabled: false,
116
+ },
117
+ isAdmin: false,
100
118
  },
101
119
  payments: {
102
120
  enabled: false,
103
121
  provider: 'stripe',
104
- plans: [
105
- {
106
- id: 'free',
107
- name: 'Free',
108
- type: 'free',
109
- params: {
110
- monthlyMessageLimit: 20,
111
- },
112
- },
113
- ],
114
122
  },
115
123
  legal: {
116
124
  privacyPolicyUrl: '/privacy',
@@ -139,7 +147,6 @@ const defaultConfig: AppConfig = {
139
147
  },
140
148
  promptTemplates: {
141
149
  enabled: true,
142
- builtIn: [],
143
150
  allowUserTemplates: true,
144
151
  },
145
152
  teams: {
@@ -147,9 +154,6 @@ const defaultConfig: AppConfig = {
147
154
  },
148
155
  documents: {
149
156
  enabled: false,
150
- storage: {
151
- provider: 'database',
152
- },
153
157
  maxFileSizeMB: 10,
154
158
  hybridThreshold: 1000,
155
159
  acceptedTypes: ['text/plain', 'text/markdown', 'application/json'],
@@ -161,10 +165,19 @@ const defaultConfig: AppConfig = {
161
165
  api: {
162
166
  enabled: false,
163
167
  },
168
+ credits: {
169
+ enabled: false,
170
+ expiryEnabled: false,
171
+ promoEnabled: false,
172
+ },
173
+ metering: {
174
+ enabled: false,
175
+ recordPromptCompletion: true,
176
+ },
164
177
  };
165
178
 
166
179
  interface ConfigContextValue {
167
- config: AppConfig;
180
+ config: PublicAppConfig;
168
181
  configLoaded: boolean;
169
182
  }
170
183
 
@@ -173,11 +186,30 @@ const ConfigContext = createContext<ConfigContextValue>({
173
186
  configLoaded: false,
174
187
  });
175
188
 
176
- export function ConfigProvider({ children }: { children: ReactNode }) {
177
- const [config, setConfig] = useState<AppConfig>(defaultConfig);
178
- const [configLoaded, setConfigLoaded] = useState(false);
189
+ interface ConfigProviderProps {
190
+ children: ReactNode;
191
+ /**
192
+ * Initial config to use immediately, avoiding a flash of default values.
193
+ * If provided, the config will not be fetched from /api/config.
194
+ * Useful when config is available from SSR loaders.
195
+ */
196
+ initialConfig?: PublicAppConfig;
197
+ }
198
+
199
+ export function ConfigProvider({ children, initialConfig }: ConfigProviderProps) {
200
+ // Priority: 1. initialConfig prop, 2. injected window config, 3. defaults + fetch
201
+ const injectedConfig = getInjectedConfig();
202
+ const preloadedConfig = initialConfig || injectedConfig;
203
+
204
+ const [config, setConfig] = useState<PublicAppConfig>(
205
+ preloadedConfig ? { ...defaultConfig, ...preloadedConfig } : defaultConfig
206
+ );
207
+ const [configLoaded, setConfigLoaded] = useState(!!preloadedConfig);
179
208
 
180
209
  useEffect(() => {
210
+ // Skip fetching if we have preloaded config
211
+ if (preloadedConfig) return;
212
+
181
213
  async function loadConfig() {
182
214
  try {
183
215
  const response = await fetch('/api/config');
@@ -196,7 +228,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
196
228
  }
197
229
  }
198
230
  loadConfig();
199
- }, []);
231
+ }, [preloadedConfig]);
200
232
 
201
233
  return (
202
234
  <ConfigContext.Provider value={{ config, configLoaded }}>