@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,155 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
5
|
+
class ClientExtensionRegistry {
|
|
6
|
+
constructor() {
|
|
7
|
+
__publicField(this, "pages", /* @__PURE__ */ new Map());
|
|
8
|
+
__publicField(this, "tools", /* @__PURE__ */ new Map());
|
|
9
|
+
__publicField(this, "overrides", /* @__PURE__ */ new Map());
|
|
10
|
+
__publicField(this, "listeners", /* @__PURE__ */ new Set());
|
|
11
|
+
// Cached arrays for useSyncExternalStore compatibility
|
|
12
|
+
// These are only updated when the underlying data changes
|
|
13
|
+
__publicField(this, "cachedPages", []);
|
|
14
|
+
__publicField(this, "cachedSidebarPages", []);
|
|
15
|
+
__publicField(this, "cachedTools", []);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Register a custom page
|
|
19
|
+
*/
|
|
20
|
+
registerPage(page) {
|
|
21
|
+
this.pages.set(page.id, page);
|
|
22
|
+
this.invalidateCaches();
|
|
23
|
+
this.notifyListeners();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Unregister a custom page
|
|
27
|
+
*/
|
|
28
|
+
unregisterPage(id) {
|
|
29
|
+
const result = this.pages.delete(id);
|
|
30
|
+
if (result) {
|
|
31
|
+
this.invalidateCaches();
|
|
32
|
+
this.notifyListeners();
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get all registered pages
|
|
38
|
+
* Returns a cached array for useSyncExternalStore compatibility
|
|
39
|
+
*/
|
|
40
|
+
getPages() {
|
|
41
|
+
return this.cachedPages;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get pages that should appear in the sidebar
|
|
45
|
+
* Returns a cached array for useSyncExternalStore compatibility
|
|
46
|
+
*/
|
|
47
|
+
getSidebarPages() {
|
|
48
|
+
return this.cachedSidebarPages;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Register a custom tool renderer
|
|
52
|
+
*/
|
|
53
|
+
registerTool(tool) {
|
|
54
|
+
this.tools.set(tool.name, tool);
|
|
55
|
+
this.invalidateCaches();
|
|
56
|
+
this.notifyListeners();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Unregister a tool renderer
|
|
60
|
+
*/
|
|
61
|
+
unregisterTool(name) {
|
|
62
|
+
const result = this.tools.delete(name);
|
|
63
|
+
if (result) {
|
|
64
|
+
this.invalidateCaches();
|
|
65
|
+
this.notifyListeners();
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get all registered tools
|
|
71
|
+
* Returns a cached array for useSyncExternalStore compatibility
|
|
72
|
+
*/
|
|
73
|
+
getTools() {
|
|
74
|
+
return this.cachedTools;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get a specific tool by name
|
|
78
|
+
*/
|
|
79
|
+
getTool(name) {
|
|
80
|
+
return this.tools.get(name);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Register a component override
|
|
84
|
+
*/
|
|
85
|
+
registerOverride(override) {
|
|
86
|
+
this.overrides.set(override.slot, override);
|
|
87
|
+
this.notifyListeners();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get a component override for a slot
|
|
91
|
+
*/
|
|
92
|
+
getOverride(slot) {
|
|
93
|
+
return this.overrides.get(slot);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Subscribe to registry changes
|
|
97
|
+
*/
|
|
98
|
+
subscribe(listener) {
|
|
99
|
+
this.listeners.add(listener);
|
|
100
|
+
return () => this.listeners.delete(listener);
|
|
101
|
+
}
|
|
102
|
+
invalidateCaches() {
|
|
103
|
+
this.cachedPages = Array.from(this.pages.values());
|
|
104
|
+
this.cachedSidebarPages = this.cachedPages.filter((p) => p.showInSidebar);
|
|
105
|
+
this.cachedTools = Array.from(this.tools.values());
|
|
106
|
+
}
|
|
107
|
+
notifyListeners() {
|
|
108
|
+
this.listeners.forEach((listener) => listener());
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Clear all registered extensions
|
|
112
|
+
*/
|
|
113
|
+
clear() {
|
|
114
|
+
this.pages.clear();
|
|
115
|
+
this.tools.clear();
|
|
116
|
+
this.overrides.clear();
|
|
117
|
+
this.invalidateCaches();
|
|
118
|
+
this.notifyListeners();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const clientRegistry = new ClientExtensionRegistry();
|
|
122
|
+
function useExtensionPages() {
|
|
123
|
+
const getSnapshot = useCallback(() => clientRegistry.getPages(), []);
|
|
124
|
+
const subscribe = useCallback((callback) => clientRegistry.subscribe(callback), []);
|
|
125
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
126
|
+
}
|
|
127
|
+
function useSidebarPages() {
|
|
128
|
+
const getSnapshot = useCallback(() => clientRegistry.getSidebarPages(), []);
|
|
129
|
+
const subscribe = useCallback((callback) => clientRegistry.subscribe(callback), []);
|
|
130
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
131
|
+
}
|
|
132
|
+
function useExtensionTools() {
|
|
133
|
+
const getSnapshot = useCallback(() => clientRegistry.getTools(), []);
|
|
134
|
+
const subscribe = useCallback((callback) => clientRegistry.subscribe(callback), []);
|
|
135
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
136
|
+
}
|
|
137
|
+
function useToolRenderer(toolName) {
|
|
138
|
+
const getSnapshot = useCallback(() => clientRegistry.getTool(toolName), [toolName]);
|
|
139
|
+
const subscribe = useCallback((callback) => clientRegistry.subscribe(callback), []);
|
|
140
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
141
|
+
}
|
|
142
|
+
function useComponentOverride(slot) {
|
|
143
|
+
const getSnapshot = useCallback(() => clientRegistry.getOverride(slot), [slot]);
|
|
144
|
+
const subscribe = useCallback((callback) => clientRegistry.subscribe(callback), []);
|
|
145
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
146
|
+
}
|
|
147
|
+
export {
|
|
148
|
+
useSidebarPages as a,
|
|
149
|
+
useExtensionTools as b,
|
|
150
|
+
clientRegistry as c,
|
|
151
|
+
useToolRenderer as d,
|
|
152
|
+
useComponentOverride as e,
|
|
153
|
+
useExtensionPages as u
|
|
154
|
+
};
|
|
155
|
+
//# sourceMappingURL=useExtensions-B5nX_8XD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useExtensions-B5nX_8XD.js","sources":["../../src/extensions/registry.ts","../../src/extensions/useExtensions.ts"],"sourcesContent":["import type { ComponentType } from 'react';\n\n/**\n * Page extension configuration\n */\nexport interface PageExtension {\n /** Unique identifier for the page */\n id: string;\n /** URL path for the page (e.g., \"/analytics\") */\n path: string;\n /** Display label for navigation */\n label: string;\n /** Optional icon name from lucide-react */\n icon?: string;\n /** React component to render for this page */\n component: ComponentType;\n /** Whether to show in the sidebar navigation */\n showInSidebar?: boolean;\n /** Whether this page requires authentication */\n requiresAuth?: boolean;\n /** Whether this page requires admin access */\n requiresAdmin?: boolean;\n}\n\n/**\n * Tool result renderer extension\n */\nexport interface ToolExtension {\n /** Tool name to match */\n name: string;\n /** Description of the tool */\n description: string;\n /** Custom renderer for tool results */\n resultRenderer?: ComponentType<{ result: unknown }>;\n}\n\n/**\n * Component override extension\n */\nexport interface ComponentOverride {\n /** Component slot to override */\n slot: 'header' | 'footer' | 'sidebar-header' | 'sidebar-footer' | 'message-actions';\n /** Component to render in the slot */\n component: ComponentType<Record<string, unknown>>;\n}\n\n/**\n * Client-side extension registry for customizing the ChaasKit UI\n *\n * Note: getPages(), getSidebarPages(), and getTools() cache their results\n * to support React's useSyncExternalStore which requires stable references.\n */\nclass ClientExtensionRegistry {\n private pages: Map<string, PageExtension> = new Map();\n private tools: Map<string, ToolExtension> = new Map();\n private overrides: Map<string, ComponentOverride> = new Map();\n private listeners: Set<() => void> = new Set();\n\n // Cached arrays for useSyncExternalStore compatibility\n // These are only updated when the underlying data changes\n private cachedPages: PageExtension[] = [];\n private cachedSidebarPages: PageExtension[] = [];\n private cachedTools: ToolExtension[] = [];\n\n /**\n * Register a custom page\n */\n registerPage(page: PageExtension): void {\n this.pages.set(page.id, page);\n this.invalidateCaches();\n this.notifyListeners();\n }\n\n /**\n * Unregister a custom page\n */\n unregisterPage(id: string): boolean {\n const result = this.pages.delete(id);\n if (result) {\n this.invalidateCaches();\n this.notifyListeners();\n }\n return result;\n }\n\n /**\n * Get all registered pages\n * Returns a cached array for useSyncExternalStore compatibility\n */\n getPages(): PageExtension[] {\n return this.cachedPages;\n }\n\n /**\n * Get pages that should appear in the sidebar\n * Returns a cached array for useSyncExternalStore compatibility\n */\n getSidebarPages(): PageExtension[] {\n return this.cachedSidebarPages;\n }\n\n /**\n * Register a custom tool renderer\n */\n registerTool(tool: ToolExtension): void {\n this.tools.set(tool.name, tool);\n this.invalidateCaches();\n this.notifyListeners();\n }\n\n /**\n * Unregister a tool renderer\n */\n unregisterTool(name: string): boolean {\n const result = this.tools.delete(name);\n if (result) {\n this.invalidateCaches();\n this.notifyListeners();\n }\n return result;\n }\n\n /**\n * Get all registered tools\n * Returns a cached array for useSyncExternalStore compatibility\n */\n getTools(): ToolExtension[] {\n return this.cachedTools;\n }\n\n /**\n * Get a specific tool by name\n */\n getTool(name: string): ToolExtension | undefined {\n return this.tools.get(name);\n }\n\n /**\n * Register a component override\n */\n registerOverride(override: ComponentOverride): void {\n this.overrides.set(override.slot, override);\n this.notifyListeners();\n }\n\n /**\n * Get a component override for a slot\n */\n getOverride(slot: string): ComponentOverride | undefined {\n return this.overrides.get(slot);\n }\n\n /**\n * Subscribe to registry changes\n */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n private invalidateCaches(): void {\n this.cachedPages = Array.from(this.pages.values());\n this.cachedSidebarPages = this.cachedPages.filter(p => p.showInSidebar);\n this.cachedTools = Array.from(this.tools.values());\n }\n\n private notifyListeners(): void {\n this.listeners.forEach(listener => listener());\n }\n\n /**\n * Clear all registered extensions\n */\n clear(): void {\n this.pages.clear();\n this.tools.clear();\n this.overrides.clear();\n this.invalidateCaches();\n this.notifyListeners();\n }\n}\n\n// Singleton instance\nexport const clientRegistry = new ClientExtensionRegistry();\n\n// Export for use in user extensions\nexport default clientRegistry;\n","import { useSyncExternalStore, useCallback } from 'react';\nimport { clientRegistry, type PageExtension, type ToolExtension, type ComponentOverride } from './registry';\n\n/**\n * Hook to access extension pages with automatic updates\n */\nexport function useExtensionPages(): PageExtension[] {\n const getSnapshot = useCallback(() => clientRegistry.getPages(), []);\n const subscribe = useCallback((callback: () => void) => clientRegistry.subscribe(callback), []);\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\n/**\n * Hook to access sidebar pages with automatic updates\n */\nexport function useSidebarPages(): PageExtension[] {\n const getSnapshot = useCallback(() => clientRegistry.getSidebarPages(), []);\n const subscribe = useCallback((callback: () => void) => clientRegistry.subscribe(callback), []);\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\n/**\n * Hook to access extension tools with automatic updates\n */\nexport function useExtensionTools(): ToolExtension[] {\n const getSnapshot = useCallback(() => clientRegistry.getTools(), []);\n const subscribe = useCallback((callback: () => void) => clientRegistry.subscribe(callback), []);\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\n/**\n * Hook to get a specific tool renderer\n */\nexport function useToolRenderer(toolName: string): ToolExtension | undefined {\n const getSnapshot = useCallback(() => clientRegistry.getTool(toolName), [toolName]);\n const subscribe = useCallback((callback: () => void) => clientRegistry.subscribe(callback), []);\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\n/**\n * Hook to get a component override\n */\nexport function useComponentOverride(slot: string): ComponentOverride | undefined {\n const getSnapshot = useCallback(() => clientRegistry.getOverride(slot), [slot]);\n const subscribe = useCallback((callback: () => void) => clientRegistry.subscribe(callback), []);\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n"],"names":[],"mappings":"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"}
|
package/dist/logo.svg
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" style="stop-color:#6366f1"/>
|
|
5
|
+
<stop offset="100%" style="stop-color:#8b5cf6"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="100" height="100" rx="20" fill="url(#gradient)"/>
|
|
9
|
+
<path d="M30 35 L50 25 L70 35 L70 55 L50 65 L30 55 Z" fill="none" stroke="white" stroke-width="3" stroke-linejoin="round"/>
|
|
10
|
+
<circle cx="50" cy="45" r="8" fill="white"/>
|
|
11
|
+
<path d="M35 70 Q50 78 65 70" fill="none" stroke="white" stroke-width="3" stroke-linecap="round"/>
|
|
12
|
+
</svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chaaskit/client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React frontend client for ChaasKit AI chat applications",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Matt Ferrante <@ferrants>",
|
|
7
|
+
"homepage": "https://chaaskit.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ferrants/chaaskit.git",
|
|
11
|
+
"directory": "packages/client"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ferrants/chaaskit/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": ["chaaskit", "ai", "chat", "saas", "client", "react", "vite"],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./src/index.tsx",
|
|
21
|
+
"import": "./dist/lib/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./extensions": {
|
|
24
|
+
"types": "./src/extensions/index.ts",
|
|
25
|
+
"import": "./dist/lib/extensions.js"
|
|
26
|
+
},
|
|
27
|
+
"./ssr": {
|
|
28
|
+
"types": "./src/ssr.ts",
|
|
29
|
+
"import": "./dist/lib/ssr.js"
|
|
30
|
+
},
|
|
31
|
+
"./ssr-utils": {
|
|
32
|
+
"types": "./src/ssr-utils.tsx",
|
|
33
|
+
"import": "./dist/lib/ssr-utils.js"
|
|
34
|
+
},
|
|
35
|
+
"./routes": {
|
|
36
|
+
"types": "./src/routes/index.ts",
|
|
37
|
+
"import": "./dist/lib/routes.js"
|
|
38
|
+
},
|
|
39
|
+
"./routes/*": {
|
|
40
|
+
"types": "./src/routes/*.tsx",
|
|
41
|
+
"import": "./dist/lib/routes/*.js"
|
|
42
|
+
},
|
|
43
|
+
"./styles": "./dist/lib/styles.css",
|
|
44
|
+
"./src/*": "./src/*"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"dist",
|
|
48
|
+
"src"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"dev": "vite --host",
|
|
52
|
+
"build": "tsc && vite build --config vite.config.lib.ts",
|
|
53
|
+
"build:lib": "vite build --config vite.config.lib.ts",
|
|
54
|
+
"preview": "vite preview",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"clean": "rm -rf dist"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@chaaskit/shared": "^0.1.0",
|
|
60
|
+
"lucide-react": "^0.321.0",
|
|
61
|
+
"react": "^18.2.0",
|
|
62
|
+
"react-dom": "^18.2.0",
|
|
63
|
+
"react-markdown": "^9.0.1",
|
|
64
|
+
"react-syntax-highlighter": "^15.5.0",
|
|
65
|
+
"remark-gfm": "^4.0.0",
|
|
66
|
+
"zustand": "^4.5.0"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/react": "^18.2.55",
|
|
70
|
+
"@types/react-dom": "^18.2.19",
|
|
71
|
+
"@types/react-syntax-highlighter": "^15.5.11",
|
|
72
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
73
|
+
"autoprefixer": "^10.4.17",
|
|
74
|
+
"postcss": "^8.4.35",
|
|
75
|
+
"tailwindcss": "^3.4.1",
|
|
76
|
+
"typescript": "^5.3.3",
|
|
77
|
+
"vite": "^5.1.0"
|
|
78
|
+
},
|
|
79
|
+
"peerDependencies": {
|
|
80
|
+
"react": "^18.2.0",
|
|
81
|
+
"react-dom": "^18.2.0",
|
|
82
|
+
"react-router": "^7.0.0"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ChevronDown, Bot } from 'lucide-react';
|
|
2
|
+
import { useState, useRef, useEffect } from 'react';
|
|
3
|
+
import { useChatStore, type AgentInfo } from '../stores/chatStore';
|
|
4
|
+
|
|
5
|
+
interface AgentSelectorProps {
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function AgentSelector({ className = '' }: AgentSelectorProps) {
|
|
10
|
+
const { availableAgents, selectedAgentId, setSelectedAgentId, isLoadingAgents } = useChatStore();
|
|
11
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
12
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
13
|
+
|
|
14
|
+
// Close dropdown when clicking outside
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
function handleClickOutside(event: MouseEvent) {
|
|
17
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
18
|
+
setIsOpen(false);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
23
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
// Don't show if only one agent or no agents
|
|
27
|
+
if (availableAgents.length <= 1) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const selectedAgent = availableAgents.find((a) => a.id === selectedAgentId) || availableAgents[0];
|
|
32
|
+
|
|
33
|
+
function handleSelect(agent: AgentInfo) {
|
|
34
|
+
setSelectedAgentId(agent.id);
|
|
35
|
+
setIsOpen(false);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isLoadingAgents) {
|
|
39
|
+
return (
|
|
40
|
+
<div className={`flex items-center gap-2 text-sm text-text-muted ${className}`}>
|
|
41
|
+
<Bot size={16} />
|
|
42
|
+
<span>Loading agents...</span>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className={`relative ${className}`} ref={dropdownRef}>
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
52
|
+
className="flex items-center gap-2 rounded-lg border border-border bg-background-secondary px-3 py-2 text-sm text-text-primary hover:border-primary hover:bg-background-secondary/80 focus:outline-none focus:ring-2 focus:ring-primary/20"
|
|
53
|
+
>
|
|
54
|
+
<Bot size={16} className="text-text-secondary" />
|
|
55
|
+
<span className="max-w-[150px] truncate">{selectedAgent?.name || 'Select agent'}</span>
|
|
56
|
+
<ChevronDown
|
|
57
|
+
size={16}
|
|
58
|
+
className={`text-text-secondary transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
|
59
|
+
/>
|
|
60
|
+
</button>
|
|
61
|
+
|
|
62
|
+
{isOpen && (
|
|
63
|
+
<div className="absolute left-0 top-full z-50 mt-1 min-w-[200px] rounded-lg border border-border bg-background shadow-lg">
|
|
64
|
+
<div className="max-h-[300px] overflow-y-auto py-1">
|
|
65
|
+
{availableAgents.map((agent) => (
|
|
66
|
+
<button
|
|
67
|
+
key={agent.id}
|
|
68
|
+
type="button"
|
|
69
|
+
onClick={() => handleSelect(agent)}
|
|
70
|
+
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-background-secondary ${
|
|
71
|
+
agent.id === selectedAgentId
|
|
72
|
+
? 'bg-primary/10 text-primary'
|
|
73
|
+
: 'text-text-primary'
|
|
74
|
+
}`}
|
|
75
|
+
>
|
|
76
|
+
<Bot size={16} className={agent.id === selectedAgentId ? 'text-primary' : 'text-text-secondary'} />
|
|
77
|
+
<span className="flex-1">{agent.name}</span>
|
|
78
|
+
{agent.isDefault && (
|
|
79
|
+
<span className="rounded bg-background-secondary px-1.5 py-0.5 text-xs text-text-muted">
|
|
80
|
+
Default
|
|
81
|
+
</span>
|
|
82
|
+
)}
|
|
83
|
+
</button>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { X, GitBranch, Loader2 } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
interface BranchModalProps {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
onBranch: (content?: string) => Promise<void>;
|
|
8
|
+
messagePreview: string;
|
|
9
|
+
initialContent?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function BranchModal({ isOpen, onClose, onBranch, messagePreview, initialContent = '' }: BranchModalProps) {
|
|
13
|
+
const [content, setContent] = useState(initialContent);
|
|
14
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
15
|
+
|
|
16
|
+
// Update content when modal opens with new initialContent
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (isOpen) {
|
|
19
|
+
setContent(initialContent);
|
|
20
|
+
}
|
|
21
|
+
}, [isOpen, initialContent]);
|
|
22
|
+
|
|
23
|
+
if (!isOpen) return null;
|
|
24
|
+
|
|
25
|
+
async function handleBranch() {
|
|
26
|
+
setIsLoading(true);
|
|
27
|
+
try {
|
|
28
|
+
await onBranch(content || undefined);
|
|
29
|
+
setContent('');
|
|
30
|
+
onClose();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Failed to create branch:', error);
|
|
33
|
+
} finally {
|
|
34
|
+
setIsLoading(false);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function handleKeyDown(e: React.KeyboardEvent) {
|
|
39
|
+
if (e.key === 'Enter' && e.metaKey) {
|
|
40
|
+
handleBranch();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
|
46
|
+
<div className="w-full max-w-lg rounded-lg bg-background border border-border shadow-xl">
|
|
47
|
+
{/* Header */}
|
|
48
|
+
<div className="flex items-center justify-between border-b border-border p-4">
|
|
49
|
+
<div className="flex items-center gap-2">
|
|
50
|
+
<GitBranch size={20} className="text-primary" />
|
|
51
|
+
<h2 className="text-lg font-semibold text-text-primary">Branch Conversation</h2>
|
|
52
|
+
</div>
|
|
53
|
+
<button
|
|
54
|
+
onClick={onClose}
|
|
55
|
+
className="rounded p-1 text-text-muted hover:bg-background-secondary hover:text-text-primary"
|
|
56
|
+
>
|
|
57
|
+
<X size={20} />
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{/* Content */}
|
|
62
|
+
<div className="p-4 space-y-4">
|
|
63
|
+
{/* Message preview */}
|
|
64
|
+
<div>
|
|
65
|
+
<label className="block text-sm font-medium text-text-secondary mb-2">
|
|
66
|
+
Branch from this response:
|
|
67
|
+
</label>
|
|
68
|
+
<div className="rounded-lg bg-background-secondary p-3 text-sm text-text-secondary max-h-24 overflow-y-auto">
|
|
69
|
+
{messagePreview}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Explanation */}
|
|
74
|
+
<p className="text-sm text-text-muted">
|
|
75
|
+
{initialContent
|
|
76
|
+
? 'Edit your message below and send it to create a new branch exploring a different direction.'
|
|
77
|
+
: 'This will create a new conversation branch from this point. Optionally add a new message to continue in a different direction.'}
|
|
78
|
+
</p>
|
|
79
|
+
|
|
80
|
+
{/* New message input */}
|
|
81
|
+
<div>
|
|
82
|
+
<label className="block text-sm font-medium text-text-secondary mb-2">
|
|
83
|
+
{initialContent ? 'Your message:' : 'Add a new message (optional):'}
|
|
84
|
+
</label>
|
|
85
|
+
<textarea
|
|
86
|
+
value={content}
|
|
87
|
+
onChange={(e) => setContent(e.target.value)}
|
|
88
|
+
onKeyDown={handleKeyDown}
|
|
89
|
+
placeholder={initialContent ? 'Edit your message...' : 'Ask a different question or take the conversation in a new direction...'}
|
|
90
|
+
className="w-full rounded-lg border border-border bg-background p-3 text-sm text-text-primary placeholder-text-muted focus:border-primary focus:outline-none resize-none"
|
|
91
|
+
rows={3}
|
|
92
|
+
autoFocus={Boolean(initialContent)}
|
|
93
|
+
/>
|
|
94
|
+
<p className="mt-1 text-xs text-text-muted">
|
|
95
|
+
Press <kbd className="px-1 py-0.5 bg-background-secondary rounded text-text-secondary">⌘</kbd> + <kbd className="px-1 py-0.5 bg-background-secondary rounded text-text-secondary">Enter</kbd> to branch
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Footer */}
|
|
101
|
+
<div className="flex justify-end gap-2 border-t border-border p-4">
|
|
102
|
+
<button
|
|
103
|
+
onClick={onClose}
|
|
104
|
+
className="rounded-lg px-4 py-2 text-sm font-medium text-text-secondary hover:bg-background-secondary"
|
|
105
|
+
>
|
|
106
|
+
Cancel
|
|
107
|
+
</button>
|
|
108
|
+
<button
|
|
109
|
+
onClick={handleBranch}
|
|
110
|
+
disabled={isLoading}
|
|
111
|
+
className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-50"
|
|
112
|
+
>
|
|
113
|
+
{isLoading ? (
|
|
114
|
+
<>
|
|
115
|
+
<Loader2 size={16} className="animate-spin" />
|
|
116
|
+
Creating...
|
|
117
|
+
</>
|
|
118
|
+
) : (
|
|
119
|
+
<>
|
|
120
|
+
<GitBranch size={16} />
|
|
121
|
+
Create Branch
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useState, useEffect, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ClientOnlyProps {
|
|
4
|
+
children: () => ReactNode;
|
|
5
|
+
fallback?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Renders children only on the client after hydration.
|
|
10
|
+
* Use this for components that rely on browser APIs or client-side state.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <ClientOnly fallback={<LoadingSkeleton />}>
|
|
15
|
+
* {() => <ChatApp />}
|
|
16
|
+
* </ClientOnly>
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function ClientOnly({ children, fallback = null }: ClientOnlyProps) {
|
|
20
|
+
const [mounted, setMounted] = useState(false);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setMounted(true);
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
return mounted ? <>{children()}</> : <>{fallback}</>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { Download, FileText, FileJson, FileType, X } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface ExportMenuProps {
|
|
6
|
+
threadId: string;
|
|
7
|
+
threadTitle: string;
|
|
8
|
+
isOpen: boolean;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function ExportMenu({ threadId, threadTitle, isOpen, onClose }: ExportMenuProps) {
|
|
13
|
+
const [isExporting, setIsExporting] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
async function handleExport(format: 'markdown' | 'json' | 'pdf') {
|
|
16
|
+
setIsExporting(format);
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(`/api/export/${threadId}?format=${format}`, {
|
|
19
|
+
credentials: 'include',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error('Export failed');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Get the blob and create download
|
|
27
|
+
const blob = await response.blob();
|
|
28
|
+
const url = URL.createObjectURL(blob);
|
|
29
|
+
const a = document.createElement('a');
|
|
30
|
+
a.href = url;
|
|
31
|
+
|
|
32
|
+
// Set filename based on format
|
|
33
|
+
const ext = format === 'pdf' ? 'html' : format === 'markdown' ? 'md' : 'json';
|
|
34
|
+
a.download = `${threadTitle}.${ext}`;
|
|
35
|
+
|
|
36
|
+
document.body.appendChild(a);
|
|
37
|
+
a.click();
|
|
38
|
+
document.body.removeChild(a);
|
|
39
|
+
URL.revokeObjectURL(url);
|
|
40
|
+
|
|
41
|
+
onClose();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('Export failed:', error);
|
|
44
|
+
} finally {
|
|
45
|
+
setIsExporting(null);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!isOpen) return null;
|
|
50
|
+
|
|
51
|
+
return createPortal(
|
|
52
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
53
|
+
{/* Backdrop */}
|
|
54
|
+
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
|
|
55
|
+
|
|
56
|
+
{/* Modal */}
|
|
57
|
+
<div className="relative w-full max-w-sm rounded-xl bg-background p-4 shadow-2xl">
|
|
58
|
+
<div className="mb-4 flex items-center justify-between">
|
|
59
|
+
<h3 className="text-lg font-semibold text-text-primary">Export Conversation</h3>
|
|
60
|
+
<button
|
|
61
|
+
onClick={onClose}
|
|
62
|
+
className="rounded p-1 text-text-muted hover:bg-background-secondary hover:text-text-primary"
|
|
63
|
+
>
|
|
64
|
+
<X size={20} />
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<p className="mb-4 text-sm text-text-secondary">
|
|
69
|
+
Choose a format to export "{threadTitle}"
|
|
70
|
+
</p>
|
|
71
|
+
|
|
72
|
+
<div className="space-y-2">
|
|
73
|
+
<button
|
|
74
|
+
onClick={() => handleExport('markdown')}
|
|
75
|
+
disabled={isExporting !== null}
|
|
76
|
+
className="flex w-full items-center gap-3 rounded-lg border border-border px-4 py-3 text-left hover:bg-background-secondary disabled:opacity-50"
|
|
77
|
+
>
|
|
78
|
+
<FileText size={20} className="text-text-muted" />
|
|
79
|
+
<div className="flex-1">
|
|
80
|
+
<p className="font-medium text-text-primary">Markdown</p>
|
|
81
|
+
<p className="text-xs text-text-muted">Best for documentation and notes</p>
|
|
82
|
+
</div>
|
|
83
|
+
{isExporting === 'markdown' && (
|
|
84
|
+
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
|
85
|
+
)}
|
|
86
|
+
</button>
|
|
87
|
+
|
|
88
|
+
<button
|
|
89
|
+
onClick={() => handleExport('json')}
|
|
90
|
+
disabled={isExporting !== null}
|
|
91
|
+
className="flex w-full items-center gap-3 rounded-lg border border-border px-4 py-3 text-left hover:bg-background-secondary disabled:opacity-50"
|
|
92
|
+
>
|
|
93
|
+
<FileJson size={20} className="text-text-muted" />
|
|
94
|
+
<div className="flex-1">
|
|
95
|
+
<p className="font-medium text-text-primary">JSON</p>
|
|
96
|
+
<p className="text-xs text-text-muted">Machine-readable, includes metadata</p>
|
|
97
|
+
</div>
|
|
98
|
+
{isExporting === 'json' && (
|
|
99
|
+
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
|
100
|
+
)}
|
|
101
|
+
</button>
|
|
102
|
+
|
|
103
|
+
<button
|
|
104
|
+
onClick={() => handleExport('pdf')}
|
|
105
|
+
disabled={isExporting !== null}
|
|
106
|
+
className="flex w-full items-center gap-3 rounded-lg border border-border px-4 py-3 text-left hover:bg-background-secondary disabled:opacity-50"
|
|
107
|
+
>
|
|
108
|
+
<FileType size={20} className="text-text-muted" />
|
|
109
|
+
<div className="flex-1">
|
|
110
|
+
<p className="font-medium text-text-primary">HTML (Print to PDF)</p>
|
|
111
|
+
<p className="text-xs text-text-muted">Formatted for printing</p>
|
|
112
|
+
</div>
|
|
113
|
+
{isExporting === 'pdf' && (
|
|
114
|
+
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
|
115
|
+
)}
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>,
|
|
120
|
+
document.body
|
|
121
|
+
);
|
|
122
|
+
}
|