@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,50 @@
|
|
|
1
|
+
import { useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ShortcutHandlers {
|
|
4
|
+
onSearch?: () => void;
|
|
5
|
+
onNewThread?: () => void;
|
|
6
|
+
onEscape?: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function useKeyboardShortcuts(handlers: ShortcutHandlers) {
|
|
10
|
+
const handleKeyDown = useCallback(
|
|
11
|
+
(e: KeyboardEvent) => {
|
|
12
|
+
const isMod = e.metaKey || e.ctrlKey;
|
|
13
|
+
|
|
14
|
+
// Cmd/Ctrl + K - Open search
|
|
15
|
+
if (isMod && e.key === 'k') {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
handlers.onSearch?.();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Cmd/Ctrl + N - New thread
|
|
22
|
+
if (isMod && e.key === 'n') {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
handlers.onNewThread?.();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Escape - Close modal/cancel
|
|
29
|
+
if (e.key === 'Escape') {
|
|
30
|
+
handlers.onEscape?.();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
[handlers]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
39
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
40
|
+
}, [handleKeyDown]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Display helpers for keyboard shortcut hints
|
|
44
|
+
export function getModifierKey(): string {
|
|
45
|
+
return navigator.platform.includes('Mac') ? '⌘' : 'Ctrl';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function formatShortcut(key: string): string {
|
|
49
|
+
return `${getModifierKey()}+${key.toUpperCase()}`;
|
|
50
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
import type { MentionableDocument, DocumentScope } from '@chaaskit/shared';
|
|
3
|
+
import { api } from '../utils/api';
|
|
4
|
+
|
|
5
|
+
interface MentionSearchResult {
|
|
6
|
+
documents: MentionableDocument[];
|
|
7
|
+
grouped: {
|
|
8
|
+
my: MentionableDocument[];
|
|
9
|
+
team: MentionableDocument[];
|
|
10
|
+
project: MentionableDocument[];
|
|
11
|
+
};
|
|
12
|
+
hasMore: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UseMentionSearchOptions {
|
|
16
|
+
debounceMs?: number;
|
|
17
|
+
limit?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useMentionSearch(options: UseMentionSearchOptions = {}) {
|
|
21
|
+
const { debounceMs = 200, limit = 20 } = options;
|
|
22
|
+
|
|
23
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
24
|
+
const [results, setResults] = useState<MentionSearchResult | null>(null);
|
|
25
|
+
const [error, setError] = useState<string | null>(null);
|
|
26
|
+
|
|
27
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
28
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
29
|
+
|
|
30
|
+
const search = useCallback(
|
|
31
|
+
async (query: string, scope?: DocumentScope, teamId?: string, projectId?: string) => {
|
|
32
|
+
// Clear any pending debounce
|
|
33
|
+
if (debounceRef.current) {
|
|
34
|
+
clearTimeout(debounceRef.current);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Cancel any in-flight request
|
|
38
|
+
if (abortControllerRef.current) {
|
|
39
|
+
abortControllerRef.current.abort();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Debounce the search (allow empty query to show all documents)
|
|
43
|
+
return new Promise<void>((resolve) => {
|
|
44
|
+
debounceRef.current = setTimeout(async () => {
|
|
45
|
+
setIsSearching(true);
|
|
46
|
+
setError(null);
|
|
47
|
+
|
|
48
|
+
abortControllerRef.current = new AbortController();
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const params = new URLSearchParams();
|
|
52
|
+
if (query) params.set('q', query);
|
|
53
|
+
if (scope) params.set('scope', scope);
|
|
54
|
+
if (teamId) params.set('teamId', teamId);
|
|
55
|
+
if (projectId) params.set('projectId', projectId);
|
|
56
|
+
params.set('limit', limit.toString());
|
|
57
|
+
|
|
58
|
+
const result = await api.get<MentionSearchResult>(
|
|
59
|
+
`/api/mentions/search?${params.toString()}`
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
setResults(result);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
65
|
+
// Ignore aborted requests
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
console.error('Mention search error:', err);
|
|
69
|
+
setError(err instanceof Error ? err.message : 'Search failed');
|
|
70
|
+
setResults(null);
|
|
71
|
+
} finally {
|
|
72
|
+
setIsSearching(false);
|
|
73
|
+
resolve();
|
|
74
|
+
}
|
|
75
|
+
}, debounceMs);
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
[debounceMs, limit]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const clearResults = useCallback(() => {
|
|
82
|
+
setResults(null);
|
|
83
|
+
setError(null);
|
|
84
|
+
setIsSearching(false);
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
// Cleanup on unmount
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
return () => {
|
|
90
|
+
if (debounceRef.current) {
|
|
91
|
+
clearTimeout(debounceRef.current);
|
|
92
|
+
}
|
|
93
|
+
if (abortControllerRef.current) {
|
|
94
|
+
abortControllerRef.current.abort();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
search,
|
|
101
|
+
clearResults,
|
|
102
|
+
results,
|
|
103
|
+
isSearching,
|
|
104
|
+
error,
|
|
105
|
+
};
|
|
106
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Import styles for CSS extraction during build
|
|
2
|
+
import './styles/index.css';
|
|
3
|
+
|
|
4
|
+
// ClientOnly and Loading Skeletons for SSR-safe rendering
|
|
5
|
+
export { ClientOnly } from './components/ClientOnly';
|
|
6
|
+
export { ChatLoadingSkeleton, SimpleLoadingSkeleton } from './components/LoadingSkeletons';
|
|
7
|
+
|
|
8
|
+
// Context providers
|
|
9
|
+
export { AuthProvider, useAuth } from './contexts/AuthContext';
|
|
10
|
+
export { ThemeProvider, useTheme } from './contexts/ThemeContext';
|
|
11
|
+
export { ConfigProvider, useConfig, useConfigLoaded } from './contexts/ConfigContext';
|
|
12
|
+
export { TeamProvider, useTeam } from './contexts/TeamContext';
|
|
13
|
+
export { ProjectProvider, useProject } from './contexts/ProjectContext';
|
|
14
|
+
|
|
15
|
+
// SSR-safe context providers
|
|
16
|
+
export {
|
|
17
|
+
ServerConfigProvider,
|
|
18
|
+
useServerConfig,
|
|
19
|
+
useServerConfigLoaded,
|
|
20
|
+
} from './contexts/ServerConfigProvider';
|
|
21
|
+
export {
|
|
22
|
+
ServerThemeProvider,
|
|
23
|
+
useServerTheme,
|
|
24
|
+
} from './contexts/ServerThemeProvider';
|
|
25
|
+
|
|
26
|
+
// Stores
|
|
27
|
+
export { useChatStore } from './stores/chatStore';
|
|
28
|
+
|
|
29
|
+
// Hooks
|
|
30
|
+
export { useBasePath } from './hooks/useBasePath';
|
|
31
|
+
export { useAppPath } from './hooks/useAppPath';
|
|
32
|
+
|
|
33
|
+
// Extension registry
|
|
34
|
+
export { clientRegistry } from './extensions/registry';
|
|
35
|
+
|
|
36
|
+
// Layout components for implementing projects
|
|
37
|
+
export { default as MainLayout } from './layouts/MainLayout';
|
|
38
|
+
export { default as Sidebar } from './components/Sidebar';
|
|
39
|
+
|
|
40
|
+
// SSR-safe components for server rendering
|
|
41
|
+
export { SSRMessageList } from './components/SSRMessageList';
|
|
42
|
+
export { SSRMarkdownRenderer } from './components/content/SSRMarkdownRenderer';
|
|
43
|
+
|
|
44
|
+
// Re-export styles path for consumers
|
|
45
|
+
export const styles = '@chaaskit/client/src/styles/index.css';
|
|
46
|
+
|
|
47
|
+
// ============================================
|
|
48
|
+
// Page Components (for React Router v7 routes)
|
|
49
|
+
// ============================================
|
|
50
|
+
export { default as ChatPage } from './pages/ChatPage';
|
|
51
|
+
export { default as LoginPage } from './pages/LoginPage';
|
|
52
|
+
export { default as RegisterPage } from './pages/RegisterPage';
|
|
53
|
+
export { default as VerifyEmailPage } from './pages/VerifyEmailPage';
|
|
54
|
+
export { default as SharedThreadPage } from './pages/SharedThreadPage';
|
|
55
|
+
export { default as PricingPage } from './pages/PricingPage';
|
|
56
|
+
export { default as PrivacyPage } from './pages/PrivacyPage';
|
|
57
|
+
export { default as TermsPage } from './pages/TermsPage';
|
|
58
|
+
export { default as ApiKeysPage } from './pages/ApiKeysPage';
|
|
59
|
+
export { default as DocumentsPage } from './pages/DocumentsPage';
|
|
60
|
+
export { default as ScheduledPromptsPage } from './pages/ScheduledPromptsPage';
|
|
61
|
+
export { default as TeamSettingsPage } from './pages/TeamSettingsPage';
|
|
62
|
+
export { default as AcceptInvitePage } from './pages/AcceptInvitePage';
|
|
63
|
+
export { default as OAuthConsentPage } from './pages/OAuthConsentPage';
|
|
64
|
+
export { default as AdminDashboardPage } from './pages/AdminDashboardPage';
|
|
65
|
+
export { default as AdminUsersPage } from './pages/AdminUsersPage';
|
|
66
|
+
export { default as AdminTeamsPage } from './pages/AdminTeamsPage';
|
|
67
|
+
export { default as AdminTeamPage } from './pages/AdminTeamPage';
|
|
68
|
+
|
|
69
|
+
// ============================================
|
|
70
|
+
// ChatProviders - Wraps the chat app with all required providers
|
|
71
|
+
// Use in React Router routes that render the chat interface
|
|
72
|
+
// ============================================
|
|
73
|
+
import React from 'react';
|
|
74
|
+
import { AuthProvider as Auth } from './contexts/AuthContext';
|
|
75
|
+
import { ThemeProvider as Theme } from './contexts/ThemeContext';
|
|
76
|
+
import { ConfigProvider as Config } from './contexts/ConfigContext';
|
|
77
|
+
import { TeamProvider as Team } from './contexts/TeamContext';
|
|
78
|
+
import { ProjectProvider as Project } from './contexts/ProjectContext';
|
|
79
|
+
|
|
80
|
+
export interface ChatProvidersProps {
|
|
81
|
+
children: React.ReactNode;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Wraps children with all context providers needed for the chat application.
|
|
86
|
+
* Use this in React Router routes that render chat-related pages.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```tsx
|
|
90
|
+
* // app/routes/chat.tsx
|
|
91
|
+
* import { ChatProviders, ChatPage } from '@chaaskit/client';
|
|
92
|
+
*
|
|
93
|
+
* export default function Chat() {
|
|
94
|
+
* return (
|
|
95
|
+
* <ChatProviders>
|
|
96
|
+
* <ChatPage />
|
|
97
|
+
* </ChatProviders>
|
|
98
|
+
* );
|
|
99
|
+
* }
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export function ChatProviders({ children }: ChatProvidersProps) {
|
|
103
|
+
return (
|
|
104
|
+
<Config>
|
|
105
|
+
<Theme>
|
|
106
|
+
<Auth>
|
|
107
|
+
<Team>
|
|
108
|
+
<Project>
|
|
109
|
+
{children}
|
|
110
|
+
</Project>
|
|
111
|
+
</Team>
|
|
112
|
+
</Auth>
|
|
113
|
+
</Theme>
|
|
114
|
+
</Config>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useState, useCallback, type ReactNode } from 'react';
|
|
2
|
+
import { Menu, Search } from 'lucide-react';
|
|
3
|
+
import { useParams, useNavigate } from 'react-router';
|
|
4
|
+
import Sidebar from '../components/Sidebar';
|
|
5
|
+
import SearchModal from '../components/SearchModal';
|
|
6
|
+
import { useConfig } from '../contexts/ConfigContext';
|
|
7
|
+
import { useChatStore } from '../stores/chatStore';
|
|
8
|
+
import { useKeyboardShortcuts, formatShortcut } from '../hooks/useKeyboardShortcuts';
|
|
9
|
+
import { useAppPath } from '../hooks/useAppPath';
|
|
10
|
+
|
|
11
|
+
interface MainLayoutProps {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function MainLayout({ children }: MainLayoutProps) {
|
|
16
|
+
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
17
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
18
|
+
const { threadId } = useParams();
|
|
19
|
+
const navigate = useNavigate();
|
|
20
|
+
const appPath = useAppPath();
|
|
21
|
+
const config = useConfig();
|
|
22
|
+
const { currentThread, clearCurrentThread } = useChatStore();
|
|
23
|
+
|
|
24
|
+
// Get display title for mobile header
|
|
25
|
+
const headerTitle = currentThread?.title || config.app.name;
|
|
26
|
+
|
|
27
|
+
// Keyboard shortcuts
|
|
28
|
+
const handleNewThread = useCallback(() => {
|
|
29
|
+
// Clear current thread and navigate to welcome screen
|
|
30
|
+
// Thread will be created when user sends first message with selected agent
|
|
31
|
+
clearCurrentThread();
|
|
32
|
+
navigate(appPath('/'));
|
|
33
|
+
setSidebarOpen(false);
|
|
34
|
+
}, [clearCurrentThread, navigate, appPath]);
|
|
35
|
+
|
|
36
|
+
useKeyboardShortcuts({
|
|
37
|
+
onSearch: () => setSearchOpen(true),
|
|
38
|
+
onNewThread: handleNewThread,
|
|
39
|
+
onEscape: () => {
|
|
40
|
+
if (searchOpen) setSearchOpen(false);
|
|
41
|
+
else if (sidebarOpen) setSidebarOpen(false);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex h-screen-safe bg-background" style={{ paddingTop: 'env(safe-area-inset-top)' }}>
|
|
47
|
+
{/* Mobile sidebar backdrop */}
|
|
48
|
+
{sidebarOpen && (
|
|
49
|
+
<div
|
|
50
|
+
className="fixed inset-0 z-20 bg-black/50 lg:hidden"
|
|
51
|
+
onClick={() => setSidebarOpen(false)}
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
54
|
+
|
|
55
|
+
{/* Sidebar */}
|
|
56
|
+
<aside
|
|
57
|
+
className={`
|
|
58
|
+
fixed inset-y-0 left-0 z-30 w-64 transform bg-sidebar transition-transform duration-200 ease-in-out
|
|
59
|
+
lg:relative lg:translate-x-0
|
|
60
|
+
${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
|
|
61
|
+
`}
|
|
62
|
+
style={{ paddingTop: 'env(safe-area-inset-top)' }}
|
|
63
|
+
>
|
|
64
|
+
<Sidebar onClose={() => setSidebarOpen(false)} onOpenSearch={() => setSearchOpen(true)} />
|
|
65
|
+
</aside>
|
|
66
|
+
|
|
67
|
+
{/* Main content */}
|
|
68
|
+
<main className="flex flex-1 flex-col overflow-hidden">
|
|
69
|
+
{/* Mobile header - sticky at top */}
|
|
70
|
+
<header className="sticky top-0 z-10 flex items-center gap-3 border-b border-border bg-background px-4 py-3 lg:hidden">
|
|
71
|
+
<button
|
|
72
|
+
onClick={() => setSidebarOpen(true)}
|
|
73
|
+
className="flex-shrink-0 rounded-lg p-2 text-text-secondary hover:bg-background-secondary hover:text-text-primary active:bg-background-secondary"
|
|
74
|
+
aria-label="Open menu"
|
|
75
|
+
>
|
|
76
|
+
<Menu size={20} />
|
|
77
|
+
</button>
|
|
78
|
+
<h1 className="flex-1 truncate text-base font-medium text-text-primary">
|
|
79
|
+
{headerTitle}
|
|
80
|
+
</h1>
|
|
81
|
+
<button
|
|
82
|
+
onClick={() => setSearchOpen(true)}
|
|
83
|
+
className="flex-shrink-0 rounded-lg p-2 text-text-secondary hover:bg-background-secondary hover:text-text-primary active:bg-background-secondary"
|
|
84
|
+
aria-label="Search"
|
|
85
|
+
>
|
|
86
|
+
<Search size={20} />
|
|
87
|
+
</button>
|
|
88
|
+
</header>
|
|
89
|
+
|
|
90
|
+
{/* Page content */}
|
|
91
|
+
<div className="flex-1 overflow-hidden">{children}</div>
|
|
92
|
+
</main>
|
|
93
|
+
|
|
94
|
+
{/* Search Modal */}
|
|
95
|
+
<SearchModal isOpen={searchOpen} onClose={() => setSearchOpen(false)} />
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useParams, useNavigate, Link } from 'react-router';
|
|
3
|
+
import { useAuth } from '../contexts/AuthContext';
|
|
4
|
+
import { useTeam } from '../contexts/TeamContext';
|
|
5
|
+
import { useAppPath } from '../hooks/useAppPath';
|
|
6
|
+
import { api } from '../utils/api';
|
|
7
|
+
|
|
8
|
+
interface InviteDetails {
|
|
9
|
+
email: string;
|
|
10
|
+
role: string;
|
|
11
|
+
teamName: string;
|
|
12
|
+
expiresAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function AcceptInvitePage() {
|
|
16
|
+
const { token } = useParams<{ token: string }>();
|
|
17
|
+
const navigate = useNavigate();
|
|
18
|
+
const appPath = useAppPath();
|
|
19
|
+
const { user, isLoading: authLoading } = useAuth();
|
|
20
|
+
const { loadTeams, setCurrentTeamId } = useTeam();
|
|
21
|
+
|
|
22
|
+
const [invite, setInvite] = useState<InviteDetails | null>(null);
|
|
23
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
24
|
+
const [isAccepting, setIsAccepting] = useState(false);
|
|
25
|
+
const [error, setError] = useState('');
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
async function loadInvite() {
|
|
29
|
+
if (!token) return;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const response = await api.get<{ invite: InviteDetails }>(`/api/teams/invite/${token}`);
|
|
33
|
+
setInvite(response.invite);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
setError(err instanceof Error ? err.message : 'Failed to load invite');
|
|
36
|
+
} finally {
|
|
37
|
+
setIsLoading(false);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
loadInvite();
|
|
42
|
+
}, [token]);
|
|
43
|
+
|
|
44
|
+
const handleAccept = async () => {
|
|
45
|
+
if (!token) return;
|
|
46
|
+
|
|
47
|
+
setIsAccepting(true);
|
|
48
|
+
setError('');
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const response = await api.post<{ team: { id: string } }>(`/api/teams/invite/${token}/accept`, {});
|
|
52
|
+
await loadTeams();
|
|
53
|
+
setCurrentTeamId(response.team.id);
|
|
54
|
+
navigate(appPath('/'));
|
|
55
|
+
} catch (err) {
|
|
56
|
+
setError(err instanceof Error ? err.message : 'Failed to accept invite');
|
|
57
|
+
setIsAccepting(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (authLoading || isLoading) {
|
|
62
|
+
return (
|
|
63
|
+
<div className="flex min-h-screen items-center justify-center bg-[var(--color-background)]">
|
|
64
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--color-primary)]" />
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (error) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex min-h-screen items-center justify-center bg-[var(--color-background)] p-4">
|
|
72
|
+
<div className="w-full max-w-md text-center">
|
|
73
|
+
<div className="mb-4 rounded-lg bg-[var(--color-error)]/10 p-4 text-[var(--color-error)]">
|
|
74
|
+
{error}
|
|
75
|
+
</div>
|
|
76
|
+
<Link
|
|
77
|
+
to="/"
|
|
78
|
+
className="text-[var(--color-primary)] hover:underline"
|
|
79
|
+
>
|
|
80
|
+
Go to homepage
|
|
81
|
+
</Link>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!user) {
|
|
88
|
+
return (
|
|
89
|
+
<div className="flex min-h-screen items-center justify-center bg-[var(--color-background)] p-4">
|
|
90
|
+
<div className="w-full max-w-md text-center">
|
|
91
|
+
<h1 className="text-2xl font-bold text-[var(--color-text-primary)] mb-4">
|
|
92
|
+
Team Invitation
|
|
93
|
+
</h1>
|
|
94
|
+
{invite && (
|
|
95
|
+
<div className="mb-6 rounded-lg border border-[var(--color-border)] bg-[var(--color-background-secondary)] p-6">
|
|
96
|
+
<p className="text-[var(--color-text-primary)] mb-2">
|
|
97
|
+
You've been invited to join
|
|
98
|
+
</p>
|
|
99
|
+
<p className="text-2xl font-bold text-[var(--color-text-primary)] mb-4">
|
|
100
|
+
{invite.teamName}
|
|
101
|
+
</p>
|
|
102
|
+
<p className="text-sm text-[var(--color-text-muted)]">
|
|
103
|
+
as {invite.role}
|
|
104
|
+
</p>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
<p className="text-[var(--color-text-secondary)] mb-6">
|
|
108
|
+
Please sign in or create an account to accept this invitation.
|
|
109
|
+
</p>
|
|
110
|
+
<div className="flex gap-4 justify-center">
|
|
111
|
+
<Link
|
|
112
|
+
to="/login"
|
|
113
|
+
className="rounded-lg bg-[var(--color-primary)] px-6 py-2 font-medium text-white hover:bg-[var(--color-primary-hover)]"
|
|
114
|
+
>
|
|
115
|
+
Sign In
|
|
116
|
+
</Link>
|
|
117
|
+
<Link
|
|
118
|
+
to="/register"
|
|
119
|
+
className="rounded-lg border border-[var(--color-border)] px-6 py-2 font-medium text-[var(--color-text-primary)] hover:bg-[var(--color-background-secondary)]"
|
|
120
|
+
>
|
|
121
|
+
Create Account
|
|
122
|
+
</Link>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div className="flex min-h-screen items-center justify-center bg-[var(--color-background)] p-4">
|
|
131
|
+
<div className="w-full max-w-md text-center">
|
|
132
|
+
<h1 className="text-2xl font-bold text-[var(--color-text-primary)] mb-4">
|
|
133
|
+
Team Invitation
|
|
134
|
+
</h1>
|
|
135
|
+
{invite && (
|
|
136
|
+
<>
|
|
137
|
+
<div className="mb-6 rounded-lg border border-[var(--color-border)] bg-[var(--color-background-secondary)] p-6">
|
|
138
|
+
<p className="text-[var(--color-text-primary)] mb-2">
|
|
139
|
+
You've been invited to join
|
|
140
|
+
</p>
|
|
141
|
+
<p className="text-2xl font-bold text-[var(--color-text-primary)] mb-4">
|
|
142
|
+
{invite.teamName}
|
|
143
|
+
</p>
|
|
144
|
+
<p className="text-sm text-[var(--color-text-muted)]">
|
|
145
|
+
as {invite.role}
|
|
146
|
+
</p>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{user.email.toLowerCase() !== invite.email.toLowerCase() && (
|
|
150
|
+
<div className="mb-6 rounded-lg bg-[var(--color-warning)]/10 p-4 text-sm text-[var(--color-warning)]">
|
|
151
|
+
This invite was sent to {invite.email}. You are signed in as {user.email}.
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
<div className="flex gap-4 justify-center">
|
|
156
|
+
<button
|
|
157
|
+
onClick={handleAccept}
|
|
158
|
+
disabled={isAccepting}
|
|
159
|
+
className="rounded-lg bg-[var(--color-primary)] px-6 py-2 font-medium text-white hover:bg-[var(--color-primary-hover)] disabled:opacity-50"
|
|
160
|
+
>
|
|
161
|
+
{isAccepting ? 'Accepting...' : 'Accept Invitation'}
|
|
162
|
+
</button>
|
|
163
|
+
<Link
|
|
164
|
+
to="/"
|
|
165
|
+
className="rounded-lg border border-[var(--color-border)] px-6 py-2 font-medium text-[var(--color-text-primary)] hover:bg-[var(--color-background-secondary)]"
|
|
166
|
+
>
|
|
167
|
+
Decline
|
|
168
|
+
</Link>
|
|
169
|
+
</div>
|
|
170
|
+
</>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|