@agent-relay/dashboard 2.0.80 → 2.0.82
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/out/404.html +1 -1
- package/out/_next/static/chunks/{118-4c8241b0218335de.js → 118-ae2b650136a5a5fc.js} +1 -1
- package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +1 -0
- package/out/_next/static/chunks/app/app/[[...slug]]/{page-1e81c047cff17212.js → page-f7eca1b66fb4249b.js} +1 -1
- package/out/_next/static/chunks/app/{page-6892fe2dd07fb48b.js → page-0ee604f7070d14c0.js} +1 -1
- package/out/_next/static/css/8968d98ed4c4d33f.css +1 -0
- package/out/about.html +2 -2
- package/out/about.txt +1 -1
- package/out/app/onboarding.html +1 -1
- package/out/app/onboarding.txt +1 -1
- package/out/app.html +1 -1
- package/out/app.txt +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
- package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
- package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
- package/out/blog.html +2 -2
- package/out/blog.txt +1 -1
- package/out/careers.html +2 -2
- package/out/careers.txt +1 -1
- package/out/changelog.html +2 -2
- package/out/changelog.txt +1 -1
- package/out/cloud/link.html +1 -1
- package/out/cloud/link.txt +2 -2
- package/out/complete-profile.html +2 -2
- package/out/complete-profile.txt +1 -1
- package/out/connect-repos.html +1 -1
- package/out/connect-repos.txt +1 -1
- package/out/contact.html +2 -2
- package/out/contact.txt +1 -1
- package/out/docs.html +2 -2
- package/out/docs.txt +1 -1
- package/out/history.html +1 -1
- package/out/history.txt +2 -2
- package/out/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/login.html +2 -2
- package/out/login.txt +1 -1
- package/out/metrics.html +1 -1
- package/out/metrics.txt +2 -2
- package/out/pricing.html +2 -2
- package/out/pricing.txt +1 -1
- package/out/privacy.html +2 -2
- package/out/privacy.txt +1 -1
- package/out/providers/setup/claude.html +1 -1
- package/out/providers/setup/claude.txt +1 -1
- package/out/providers/setup/codex.html +1 -1
- package/out/providers/setup/codex.txt +1 -1
- package/out/providers/setup/cursor.html +1 -1
- package/out/providers/setup/cursor.txt +1 -1
- package/out/providers.html +1 -1
- package/out/providers.txt +1 -1
- package/out/security.html +2 -2
- package/out/security.txt +1 -1
- package/out/signup.html +2 -2
- package/out/signup.txt +1 -1
- package/out/terms.html +2 -2
- package/out/terms.txt +1 -1
- package/package.json +7 -1
- package/src/app/about/page.tsx +7 -0
- package/src/app/app/[[...slug]]/DashboardPageClient.tsx +853 -0
- package/src/app/app/[[...slug]]/page.tsx +23 -0
- package/src/app/app/onboarding/page.tsx +394 -0
- package/src/app/apple-icon.png +0 -0
- package/src/app/blog/go-to-bed-wake-up-to-a-finished-product/page.tsx +88 -0
- package/src/app/blog/let-them-cook-multi-agent-orchestration/page.tsx +93 -0
- package/src/app/blog/page.tsx +15 -0
- package/src/app/careers/page.tsx +7 -0
- package/src/app/changelog/page.tsx +7 -0
- package/src/app/cloud/link/page.tsx +464 -0
- package/src/app/complete-profile/page.tsx +204 -0
- package/src/app/connect-repos/page.tsx +410 -0
- package/src/app/contact/page.tsx +7 -0
- package/src/app/docs/page.tsx +7 -0
- package/src/app/favicon.png +0 -0
- package/src/app/globals.css +200 -0
- package/src/app/history/page.tsx +658 -0
- package/src/app/layout.tsx +25 -0
- package/src/app/login/page.tsx +424 -0
- package/src/app/metrics/page.tsx +781 -0
- package/src/app/page.tsx +59 -0
- package/src/app/pricing/page.tsx +7 -0
- package/src/app/privacy/page.tsx +7 -0
- package/src/app/providers/page.tsx +193 -0
- package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +197 -0
- package/src/app/providers/setup/[provider]/constants.ts +35 -0
- package/src/app/providers/setup/[provider]/page.tsx +42 -0
- package/src/app/security/page.tsx +7 -0
- package/src/app/signup/page.tsx +533 -0
- package/src/app/terms/page.tsx +7 -0
- package/src/components/ActivityFeed.tsx +216 -0
- package/src/components/AddWorkspaceModal.tsx +170 -0
- package/src/components/AgentCard.test.tsx +134 -0
- package/src/components/AgentCard.tsx +585 -0
- package/src/components/AgentList.test.tsx +147 -0
- package/src/components/AgentList.tsx +419 -0
- package/src/components/AgentLogPreview.tsx +173 -0
- package/src/components/AgentProfilePanel.tsx +569 -0
- package/src/components/App.tsx +3424 -0
- package/src/components/BillingPanel.tsx +922 -0
- package/src/components/BillingResult.tsx +447 -0
- package/src/components/BroadcastComposer.tsx +690 -0
- package/src/components/ChannelAdminPanel.tsx +773 -0
- package/src/components/ChannelBrowser.tsx +385 -0
- package/src/components/ChannelChat.tsx +261 -0
- package/src/components/ChannelSidebar.tsx +399 -0
- package/src/components/CloudSessionProvider.tsx +130 -0
- package/src/components/CommandPalette.tsx +815 -0
- package/src/components/ConfirmationDialog.tsx +133 -0
- package/src/components/ConversationHistory.tsx +518 -0
- package/src/components/CoordinatorPanel.tsx +956 -0
- package/src/components/DecisionQueue.tsx +717 -0
- package/src/components/DirectMessageView.tsx +164 -0
- package/src/components/FileAutocomplete.tsx +368 -0
- package/src/components/FleetOverview.tsx +278 -0
- package/src/components/LogViewer.tsx +310 -0
- package/src/components/LogViewerPanel.tsx +482 -0
- package/src/components/Logo.tsx +284 -0
- package/src/components/MentionAutocomplete.tsx +384 -0
- package/src/components/MessageComposer.tsx +473 -0
- package/src/components/MessageList.tsx +725 -0
- package/src/components/MessageSenderName.tsx +91 -0
- package/src/components/MessageStatusIndicator.tsx +142 -0
- package/src/components/NewConversationModal.tsx +400 -0
- package/src/components/NotificationToast.tsx +488 -0
- package/src/components/OnlineUsersIndicator.tsx +164 -0
- package/src/components/Pagination.tsx +124 -0
- package/src/components/PricingPlans.tsx +386 -0
- package/src/components/ProjectList.tsx +711 -0
- package/src/components/ProviderAuthFlow.tsx +343 -0
- package/src/components/ProviderConnectionList.tsx +375 -0
- package/src/components/ProvisioningProgress.tsx +730 -0
- package/src/components/ReactionChips.tsx +70 -0
- package/src/components/ReactionPicker.tsx +121 -0
- package/src/components/RepoAccessPanel.tsx +787 -0
- package/src/components/RepositoriesPanel.tsx +901 -0
- package/src/components/ServerCard.tsx +202 -0
- package/src/components/SessionExpiredModal.tsx +128 -0
- package/src/components/SpawnModal.test.tsx +190 -0
- package/src/components/SpawnModal.tsx +1001 -0
- package/src/components/TaskAssignmentUI.tsx +375 -0
- package/src/components/TerminalProviderSetup.tsx +517 -0
- package/src/components/ThemeProvider.tsx +159 -0
- package/src/components/ThinkingIndicator.tsx +231 -0
- package/src/components/ThreadList.tsx +198 -0
- package/src/components/ThreadPanel.tsx +405 -0
- package/src/components/TrajectoryViewer.tsx +698 -0
- package/src/components/TypingIndicator.tsx +69 -0
- package/src/components/UsageBanner.tsx +231 -0
- package/src/components/UserProfilePanel.tsx +233 -0
- package/src/components/WorkspaceContext.tsx +95 -0
- package/src/components/WorkspaceSelector.tsx +234 -0
- package/src/components/WorkspaceStatusIndicator.tsx +396 -0
- package/src/components/XTermInteractive.tsx +516 -0
- package/src/components/XTermLogViewer.tsx +719 -0
- package/src/components/channels/ChannelDialogs.tsx +1411 -0
- package/src/components/channels/ChannelHeader.tsx +317 -0
- package/src/components/channels/ChannelMessageList.tsx +463 -0
- package/src/components/channels/ChannelViewV1.tsx +146 -0
- package/src/components/channels/MessageInput.tsx +302 -0
- package/src/components/channels/SearchInput.tsx +172 -0
- package/src/components/channels/SearchResults.tsx +336 -0
- package/src/components/channels/api.test.ts +1527 -0
- package/src/components/channels/api.ts +703 -0
- package/src/components/channels/index.ts +76 -0
- package/src/components/channels/mockApi.ts +344 -0
- package/src/components/channels/types.ts +566 -0
- package/src/components/hooks/index.ts +58 -0
- package/src/components/hooks/useAgentLogs.ts +504 -0
- package/src/components/hooks/useAgents.ts +127 -0
- package/src/components/hooks/useBroadcastDedup.test.ts +371 -0
- package/src/components/hooks/useBroadcastDedup.ts +86 -0
- package/src/components/hooks/useChannelAdmin.ts +329 -0
- package/src/components/hooks/useChannelBrowser.ts +239 -0
- package/src/components/hooks/useChannelCommands.ts +138 -0
- package/src/components/hooks/useChannels.ts +367 -0
- package/src/components/hooks/useDebounce.ts +29 -0
- package/src/components/hooks/useDirectMessage.test.ts +952 -0
- package/src/components/hooks/useDirectMessage.ts +141 -0
- package/src/components/hooks/useMessages.ts +310 -0
- package/src/components/hooks/useOrchestrator.test.ts +165 -0
- package/src/components/hooks/useOrchestrator.ts +424 -0
- package/src/components/hooks/usePinnedAgents.test.ts +356 -0
- package/src/components/hooks/usePinnedAgents.ts +140 -0
- package/src/components/hooks/usePresence.test.ts +245 -0
- package/src/components/hooks/usePresence.ts +377 -0
- package/src/components/hooks/useRecentRepos.ts +130 -0
- package/src/components/hooks/useSession.ts +209 -0
- package/src/components/hooks/useThread.ts +138 -0
- package/src/components/hooks/useTrajectory.ts +265 -0
- package/src/components/hooks/useWebSocket.ts +290 -0
- package/src/components/hooks/useWorkspaceMembers.ts +132 -0
- package/src/components/hooks/useWorkspaceRepos.ts +73 -0
- package/src/components/hooks/useWorkspaceStatus.ts +237 -0
- package/src/components/index.ts +81 -0
- package/src/components/layout/Header.tsx +311 -0
- package/src/components/layout/RepoContextHeader.tsx +361 -0
- package/src/components/layout/Sidebar.archive.test.tsx +126 -0
- package/src/components/layout/Sidebar.test.tsx +691 -0
- package/src/components/layout/Sidebar.tsx +900 -0
- package/src/components/layout/index.ts +7 -0
- package/src/components/settings/BillingSettingsPanel.tsx +564 -0
- package/src/components/settings/SettingsPage.tsx +683 -0
- package/src/components/settings/TeamSettingsPanel.tsx +560 -0
- package/src/components/settings/WorkspaceSettingsPanel.tsx +1368 -0
- package/src/components/settings/index.ts +11 -0
- package/src/components/settings/types.ts +79 -0
- package/src/components/utils/messageFormatting.test.tsx +331 -0
- package/src/components/utils/messageFormatting.tsx +597 -0
- package/src/index.ts +63 -0
- package/src/landing/AboutPage.tsx +77 -0
- package/src/landing/BlogContent.tsx +187 -0
- package/src/landing/BlogPage.tsx +47 -0
- package/src/landing/CareersPage.tsx +53 -0
- package/src/landing/ChangelogPage.tsx +33 -0
- package/src/landing/ContactPage.tsx +41 -0
- package/src/landing/DocsPage.tsx +43 -0
- package/src/landing/LandingPage.tsx +702 -0
- package/src/landing/PricingPage.tsx +549 -0
- package/src/landing/PrivacyPage.tsx +117 -0
- package/src/landing/SecurityPage.tsx +42 -0
- package/src/landing/StaticPage.tsx +165 -0
- package/src/landing/TermsPage.tsx +125 -0
- package/src/landing/blogData.ts +312 -0
- package/src/landing/index.ts +18 -0
- package/src/landing/styles.css +3673 -0
- package/src/lib/agent-merge.test.ts +43 -0
- package/src/lib/agent-merge.ts +35 -0
- package/src/lib/api.ts +1294 -0
- package/src/lib/cloudApi.ts +893 -0
- package/src/lib/colors.test.ts +175 -0
- package/src/lib/colors.ts +218 -0
- package/src/lib/config.ts +109 -0
- package/src/lib/hierarchy.ts +242 -0
- package/src/lib/stuckDetection.ts +142 -0
- package/src/lib/useUrlRouting.ts +190 -0
- package/src/types/index.ts +317 -0
- package/src/types/threading.ts +7 -0
- package/out/_next/static/chunks/285-dc644487a8d6500d.js +0 -1
- package/out/_next/static/css/4c58d9cf493aa626.css +0 -1
- /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
- /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_ssgManifest.js +0 -0
- /package/out/_next/static/chunks/{528-d375bc8b46912d2c.js → 528-f5f676996d613c25.js} +0 -0
- /package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/{page-a58308f43557b908.js → page-b194f207fbd91862.js} +0 -0
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TerminalProviderSetup Component
|
|
3
|
+
*
|
|
4
|
+
* Reusable component for terminal-based provider authentication setup.
|
|
5
|
+
* Handles agent spawning, interactive terminal, and cleanup.
|
|
6
|
+
* Users copy and paste auth URLs from the terminal output.
|
|
7
|
+
*
|
|
8
|
+
* Used in:
|
|
9
|
+
* - /providers/setup/[provider] page (full-page setup)
|
|
10
|
+
* - WorkspaceSettingsPanel (embedded setup)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React, { useRef, useEffect, useCallback, useState } from 'react';
|
|
14
|
+
import { Terminal } from '@xterm/xterm';
|
|
15
|
+
import { FitAddon } from '@xterm/addon-fit';
|
|
16
|
+
|
|
17
|
+
export interface ProviderConfig {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
displayName: string;
|
|
21
|
+
color: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TerminalProviderSetupProps {
|
|
25
|
+
/** Provider configuration */
|
|
26
|
+
provider: ProviderConfig;
|
|
27
|
+
/** Workspace ID to spawn agent in */
|
|
28
|
+
workspaceId: string;
|
|
29
|
+
/** CSRF token for API requests */
|
|
30
|
+
csrfToken?: string;
|
|
31
|
+
/** Maximum height of the terminal */
|
|
32
|
+
maxHeight?: string;
|
|
33
|
+
/** Called when authentication is detected as complete */
|
|
34
|
+
onSuccess?: () => void;
|
|
35
|
+
/** Called when an error occurs */
|
|
36
|
+
onError?: (error: string) => void;
|
|
37
|
+
/** Called when cancel is requested */
|
|
38
|
+
onCancel?: () => void;
|
|
39
|
+
/** Called when user wants to connect another provider */
|
|
40
|
+
onConnectAnother?: () => void;
|
|
41
|
+
/** Whether to show header with close button */
|
|
42
|
+
showHeader?: boolean;
|
|
43
|
+
/** Custom class name */
|
|
44
|
+
className?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Terminal theme matching dashboard dark theme
|
|
48
|
+
const TERMINAL_THEME = {
|
|
49
|
+
background: '#0d0f14',
|
|
50
|
+
foreground: '#c9d1d9',
|
|
51
|
+
cursor: '#58a6ff',
|
|
52
|
+
cursorAccent: '#0d0f14',
|
|
53
|
+
selectionBackground: '#264f78',
|
|
54
|
+
selectionForeground: '#ffffff',
|
|
55
|
+
black: '#484f58',
|
|
56
|
+
red: '#f85149',
|
|
57
|
+
green: '#3fb950',
|
|
58
|
+
yellow: '#d29922',
|
|
59
|
+
blue: '#58a6ff',
|
|
60
|
+
magenta: '#bc8cff',
|
|
61
|
+
cyan: '#39c5cf',
|
|
62
|
+
white: '#b1bac4',
|
|
63
|
+
brightBlack: '#6e7681',
|
|
64
|
+
brightRed: '#ff7b72',
|
|
65
|
+
brightGreen: '#56d364',
|
|
66
|
+
brightYellow: '#e3b341',
|
|
67
|
+
brightBlue: '#79c0ff',
|
|
68
|
+
brightMagenta: '#d2a8ff',
|
|
69
|
+
brightCyan: '#56d4dd',
|
|
70
|
+
brightWhite: '#ffffff',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
export function TerminalProviderSetup({
|
|
75
|
+
provider,
|
|
76
|
+
workspaceId,
|
|
77
|
+
csrfToken: initialCsrfToken,
|
|
78
|
+
maxHeight = '400px',
|
|
79
|
+
onSuccess,
|
|
80
|
+
onError,
|
|
81
|
+
onCancel,
|
|
82
|
+
onConnectAnother,
|
|
83
|
+
showHeader = true,
|
|
84
|
+
className = '',
|
|
85
|
+
}: TerminalProviderSetupProps) {
|
|
86
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
87
|
+
const terminalRef = useRef<Terminal | null>(null);
|
|
88
|
+
const fitAddonRef = useRef<FitAddon | null>(null);
|
|
89
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
90
|
+
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
91
|
+
const hasShownConnectedRef = useRef(false); // Prevent duplicate "Connected" messages
|
|
92
|
+
const onDataDisposableRef = useRef<{ dispose: () => void } | null>(null); // Track onData handler for cleanup
|
|
93
|
+
|
|
94
|
+
const [isSpawning, setIsSpawning] = useState(false);
|
|
95
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
96
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
97
|
+
const [error, setError] = useState<string | null>(null);
|
|
98
|
+
const [agentName, setAgentName] = useState<string | null>(null);
|
|
99
|
+
const [isComplete, setIsComplete] = useState(false);
|
|
100
|
+
const [csrfToken, setCsrfToken] = useState<string | undefined>(initialCsrfToken);
|
|
101
|
+
|
|
102
|
+
// Generate unique agent name
|
|
103
|
+
const generateAgentName = useCallback(() => {
|
|
104
|
+
const timestamp = Date.now().toString(36);
|
|
105
|
+
const random = Math.random().toString(36).substring(2, 6);
|
|
106
|
+
return `__setup__${provider.id}-${timestamp}${random}`;
|
|
107
|
+
}, [provider.id]);
|
|
108
|
+
|
|
109
|
+
// Fetch CSRF token if not provided
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!csrfToken) {
|
|
112
|
+
fetch('/api/auth/session', { credentials: 'include' })
|
|
113
|
+
.then(res => {
|
|
114
|
+
const token = res.headers.get('X-CSRF-Token');
|
|
115
|
+
if (token) setCsrfToken(token);
|
|
116
|
+
})
|
|
117
|
+
.catch(() => {});
|
|
118
|
+
}
|
|
119
|
+
}, [csrfToken]);
|
|
120
|
+
|
|
121
|
+
// Cleanup agent
|
|
122
|
+
const cleanupAgent = useCallback(async () => {
|
|
123
|
+
if (!workspaceId || !agentName) return;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const headers: Record<string, string> = {};
|
|
127
|
+
if (csrfToken) headers['X-CSRF-Token'] = csrfToken;
|
|
128
|
+
|
|
129
|
+
await fetch(`/api/workspaces/${workspaceId}/agents/${encodeURIComponent(agentName)}`, {
|
|
130
|
+
method: 'DELETE',
|
|
131
|
+
credentials: 'include',
|
|
132
|
+
headers,
|
|
133
|
+
});
|
|
134
|
+
} catch {
|
|
135
|
+
// Ignore cleanup errors
|
|
136
|
+
}
|
|
137
|
+
}, [workspaceId, agentName, csrfToken]);
|
|
138
|
+
|
|
139
|
+
// Spawn agent
|
|
140
|
+
const spawnAgent = useCallback(async () => {
|
|
141
|
+
if (!workspaceId || !csrfToken) return;
|
|
142
|
+
|
|
143
|
+
setIsSpawning(true);
|
|
144
|
+
setError(null);
|
|
145
|
+
|
|
146
|
+
const name = generateAgentName();
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
150
|
+
if (csrfToken) headers['X-CSRF-Token'] = csrfToken;
|
|
151
|
+
|
|
152
|
+
const res = await fetch(`/api/workspaces/${workspaceId}/agents`, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
credentials: 'include',
|
|
155
|
+
headers,
|
|
156
|
+
body: JSON.stringify({
|
|
157
|
+
name,
|
|
158
|
+
provider: provider.id === 'anthropic' ? 'claude' : provider.id,
|
|
159
|
+
interactive: true, // Disable auto-accept prompts
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!res.ok) {
|
|
164
|
+
const data = await res.json();
|
|
165
|
+
throw new Error(data.error || 'Failed to spawn agent');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setAgentName(name);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
const message = err instanceof Error ? err.message : 'Failed to spawn agent';
|
|
171
|
+
setError(message);
|
|
172
|
+
onError?.(message);
|
|
173
|
+
} finally {
|
|
174
|
+
setIsSpawning(false);
|
|
175
|
+
}
|
|
176
|
+
}, [workspaceId, csrfToken, provider.id, generateAgentName, onError]);
|
|
177
|
+
|
|
178
|
+
// Initialize terminal
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!containerRef.current) return;
|
|
181
|
+
|
|
182
|
+
const terminal = new Terminal({
|
|
183
|
+
theme: TERMINAL_THEME,
|
|
184
|
+
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
185
|
+
fontSize: 12,
|
|
186
|
+
lineHeight: 1.4,
|
|
187
|
+
convertEol: true,
|
|
188
|
+
scrollback: 10000,
|
|
189
|
+
cursorBlink: true,
|
|
190
|
+
cursorStyle: 'block',
|
|
191
|
+
disableStdin: false,
|
|
192
|
+
allowProposedApi: true,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const fitAddon = new FitAddon();
|
|
196
|
+
terminal.loadAddon(fitAddon);
|
|
197
|
+
terminal.open(containerRef.current);
|
|
198
|
+
fitAddon.fit();
|
|
199
|
+
|
|
200
|
+
terminalRef.current = terminal;
|
|
201
|
+
fitAddonRef.current = fitAddon;
|
|
202
|
+
|
|
203
|
+
// Handle resize
|
|
204
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
205
|
+
fitAddon.fit();
|
|
206
|
+
});
|
|
207
|
+
resizeObserver.observe(containerRef.current);
|
|
208
|
+
|
|
209
|
+
return () => {
|
|
210
|
+
resizeObserver.disconnect();
|
|
211
|
+
terminal.dispose();
|
|
212
|
+
terminalRef.current = null;
|
|
213
|
+
fitAddonRef.current = null;
|
|
214
|
+
};
|
|
215
|
+
}, []);
|
|
216
|
+
|
|
217
|
+
// Connect WebSocket when agent is spawned
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (!agentName || !workspaceId) return;
|
|
220
|
+
|
|
221
|
+
// Reset the connected message flag when agent changes
|
|
222
|
+
hasShownConnectedRef.current = false;
|
|
223
|
+
|
|
224
|
+
const connectWebSocket = () => {
|
|
225
|
+
// Don't reconnect if we already have an open connection
|
|
226
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
231
|
+
const wsUrl = `${protocol}//${window.location.host}/ws/logs/${encodeURIComponent(workspaceId)}/${encodeURIComponent(agentName)}`;
|
|
232
|
+
|
|
233
|
+
setIsConnecting(true);
|
|
234
|
+
const ws = new WebSocket(wsUrl);
|
|
235
|
+
wsRef.current = ws;
|
|
236
|
+
|
|
237
|
+
ws.onopen = () => {
|
|
238
|
+
setIsConnected(true);
|
|
239
|
+
setIsConnecting(false);
|
|
240
|
+
// Only show connected message once per session
|
|
241
|
+
if (!hasShownConnectedRef.current) {
|
|
242
|
+
hasShownConnectedRef.current = true;
|
|
243
|
+
terminalRef.current?.writeln('\x1b[90m[Connected - Interactive Mode]\x1b[0m');
|
|
244
|
+
terminalRef.current?.writeln('\x1b[90m[Type directly to respond to prompts]\x1b[0m\n');
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
ws.onclose = () => {
|
|
249
|
+
setIsConnected(false);
|
|
250
|
+
setIsConnecting(false);
|
|
251
|
+
|
|
252
|
+
// Reconnect after delay (only if not intentionally closed)
|
|
253
|
+
reconnectTimeoutRef.current = setTimeout(connectWebSocket, 2000);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
ws.onmessage = (event) => {
|
|
257
|
+
try {
|
|
258
|
+
const data = JSON.parse(event.data);
|
|
259
|
+
|
|
260
|
+
if (data.type === 'history' && Array.isArray(data.lines)) {
|
|
261
|
+
data.lines.forEach((line: string) => {
|
|
262
|
+
terminalRef.current?.writeln(line);
|
|
263
|
+
});
|
|
264
|
+
} else if (data.type === 'log' || data.type === 'output') {
|
|
265
|
+
const content = data.content || data.data || data.message || '';
|
|
266
|
+
if (content) {
|
|
267
|
+
terminalRef.current?.write(content);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
if (typeof event.data === 'string') {
|
|
272
|
+
terminalRef.current?.write(event.data);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Clean up previous onData handler before adding new one
|
|
278
|
+
if (onDataDisposableRef.current) {
|
|
279
|
+
onDataDisposableRef.current.dispose();
|
|
280
|
+
onDataDisposableRef.current = null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Handle user input - store disposable for cleanup
|
|
284
|
+
// Suppress input when user has text selected (e.g., copying OAuth URL)
|
|
285
|
+
if (terminalRef.current) {
|
|
286
|
+
onDataDisposableRef.current = terminalRef.current.onData((data: string) => {
|
|
287
|
+
// Don't send input if user has text selected (likely copying a URL)
|
|
288
|
+
const selection = terminalRef.current?.getSelection();
|
|
289
|
+
if (selection && selection.length > 0) {
|
|
290
|
+
return; // User is selecting/copying text, don't send input
|
|
291
|
+
}
|
|
292
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
293
|
+
ws.send(JSON.stringify({ type: 'input', agent: agentName, data }));
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
connectWebSocket();
|
|
300
|
+
|
|
301
|
+
return () => {
|
|
302
|
+
if (reconnectTimeoutRef.current) {
|
|
303
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
304
|
+
reconnectTimeoutRef.current = null;
|
|
305
|
+
}
|
|
306
|
+
if (onDataDisposableRef.current) {
|
|
307
|
+
onDataDisposableRef.current.dispose();
|
|
308
|
+
onDataDisposableRef.current = null;
|
|
309
|
+
}
|
|
310
|
+
wsRef.current?.close();
|
|
311
|
+
wsRef.current = null;
|
|
312
|
+
};
|
|
313
|
+
}, [agentName, workspaceId]);
|
|
314
|
+
|
|
315
|
+
// Auto-spawn on mount
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
if (csrfToken && !agentName && !isSpawning) {
|
|
318
|
+
spawnAgent();
|
|
319
|
+
}
|
|
320
|
+
}, [csrfToken, agentName, isSpawning, spawnAgent]);
|
|
321
|
+
|
|
322
|
+
// Cleanup on unmount
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
return () => {
|
|
325
|
+
cleanupAgent();
|
|
326
|
+
};
|
|
327
|
+
}, [cleanupAgent]);
|
|
328
|
+
|
|
329
|
+
const handleComplete = useCallback(async () => {
|
|
330
|
+
// Mark provider as connected in the database
|
|
331
|
+
// Use provider.name (canonical backend name e.g. 'codex', 'anthropic')
|
|
332
|
+
const providerName = provider.name || provider.id;
|
|
333
|
+
try {
|
|
334
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
335
|
+
if (csrfToken) headers['X-CSRF-Token'] = csrfToken;
|
|
336
|
+
|
|
337
|
+
const response = await fetch(`/api/onboarding/mark-connected/${providerName}`, {
|
|
338
|
+
method: 'POST',
|
|
339
|
+
credentials: 'include',
|
|
340
|
+
headers,
|
|
341
|
+
body: JSON.stringify({ workspaceId }),
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (!response.ok) {
|
|
345
|
+
console.error('Failed to mark provider as connected:', await response.text());
|
|
346
|
+
}
|
|
347
|
+
} catch (err) {
|
|
348
|
+
console.error('Error marking provider as connected:', err);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
await cleanupAgent();
|
|
352
|
+
setIsComplete(true);
|
|
353
|
+
}, [cleanupAgent, provider.id, provider.name, csrfToken, workspaceId]);
|
|
354
|
+
|
|
355
|
+
const handleDone = useCallback(() => {
|
|
356
|
+
onSuccess?.();
|
|
357
|
+
}, [onSuccess]);
|
|
358
|
+
|
|
359
|
+
const handleConnectAnother = useCallback(() => {
|
|
360
|
+
onConnectAnother?.();
|
|
361
|
+
}, [onConnectAnother]);
|
|
362
|
+
|
|
363
|
+
const handleCancel = useCallback(async () => {
|
|
364
|
+
await cleanupAgent();
|
|
365
|
+
onCancel?.();
|
|
366
|
+
}, [cleanupAgent, onCancel]);
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<div className={`flex flex-col rounded-xl overflow-hidden border border-border-subtle ${className}`}>
|
|
370
|
+
{/* Header */}
|
|
371
|
+
{showHeader && (
|
|
372
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border-subtle bg-bg-tertiary">
|
|
373
|
+
<div className="flex items-center gap-3">
|
|
374
|
+
<div
|
|
375
|
+
className="w-8 h-8 rounded-lg flex items-center justify-center text-white font-bold text-sm"
|
|
376
|
+
style={{ backgroundColor: provider.color }}
|
|
377
|
+
>
|
|
378
|
+
{provider.displayName[0]}
|
|
379
|
+
</div>
|
|
380
|
+
<div>
|
|
381
|
+
<h4 className="text-sm font-semibold text-text-primary">
|
|
382
|
+
{provider.displayName} Setup
|
|
383
|
+
</h4>
|
|
384
|
+
<p className="text-xs text-text-muted">Interactive terminal</p>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
<div className="flex items-center gap-2">
|
|
388
|
+
{isConnected && (
|
|
389
|
+
<span className="flex items-center gap-1 px-2 py-1 rounded-full bg-success/15 text-xs text-success">
|
|
390
|
+
<span className="w-1.5 h-1.5 rounded-full bg-success animate-pulse" />
|
|
391
|
+
Connected
|
|
392
|
+
</span>
|
|
393
|
+
)}
|
|
394
|
+
{onCancel && (
|
|
395
|
+
<button
|
|
396
|
+
onClick={handleCancel}
|
|
397
|
+
className="p-1.5 rounded-lg hover:bg-bg-hover text-text-muted hover:text-text-primary transition-colors"
|
|
398
|
+
>
|
|
399
|
+
<CloseIcon />
|
|
400
|
+
</button>
|
|
401
|
+
)}
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
)}
|
|
405
|
+
|
|
406
|
+
{/* Success State */}
|
|
407
|
+
{isComplete ? (
|
|
408
|
+
<div className="flex flex-col items-center justify-center py-12 px-6">
|
|
409
|
+
<div
|
|
410
|
+
className="w-16 h-16 rounded-full flex items-center justify-center mb-4"
|
|
411
|
+
style={{ backgroundColor: `${provider.color}20` }}
|
|
412
|
+
>
|
|
413
|
+
<CheckIcon className="w-8 h-8" style={{ color: provider.color }} />
|
|
414
|
+
</div>
|
|
415
|
+
<h3 className="text-lg font-semibold text-text-primary mb-2">
|
|
416
|
+
{provider.displayName} Connected!
|
|
417
|
+
</h3>
|
|
418
|
+
<p className="text-sm text-text-muted mb-6 text-center">
|
|
419
|
+
Your {provider.displayName} account has been successfully connected.
|
|
420
|
+
</p>
|
|
421
|
+
<div className="flex gap-3">
|
|
422
|
+
{onConnectAnother && (
|
|
423
|
+
<button
|
|
424
|
+
onClick={handleConnectAnother}
|
|
425
|
+
className="px-4 py-2 bg-bg-hover text-text-primary text-sm font-medium rounded-lg hover:bg-bg-tertiary transition-colors border border-border-subtle"
|
|
426
|
+
>
|
|
427
|
+
Connect Another Provider
|
|
428
|
+
</button>
|
|
429
|
+
)}
|
|
430
|
+
<button
|
|
431
|
+
onClick={handleDone}
|
|
432
|
+
className="px-4 py-2 bg-accent-cyan text-bg-deep text-sm font-semibold rounded-lg hover:bg-accent-cyan/90 transition-colors"
|
|
433
|
+
>
|
|
434
|
+
Continue to Dashboard
|
|
435
|
+
</button>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
) : (
|
|
439
|
+
<>
|
|
440
|
+
{/* Error */}
|
|
441
|
+
{error && (
|
|
442
|
+
<div className="px-4 py-3 bg-error/10 border-b border-error/30 text-sm text-error flex items-center gap-2">
|
|
443
|
+
<AlertIcon />
|
|
444
|
+
<span>{error}</span>
|
|
445
|
+
<button
|
|
446
|
+
onClick={spawnAgent}
|
|
447
|
+
className="ml-auto text-xs px-2 py-1 rounded bg-error/20 hover:bg-error/30 transition-colors"
|
|
448
|
+
>
|
|
449
|
+
Retry
|
|
450
|
+
</button>
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
|
|
454
|
+
{/* Spawning indicator */}
|
|
455
|
+
{isSpawning && (
|
|
456
|
+
<div className="px-4 py-3 bg-accent-cyan/10 border-b border-accent-cyan/30 text-sm text-accent-cyan flex items-center gap-2">
|
|
457
|
+
<div className="w-4 h-4 border-2 border-accent-cyan/30 border-t-accent-cyan rounded-full animate-spin" />
|
|
458
|
+
<span>Starting {provider.displayName}...</span>
|
|
459
|
+
</div>
|
|
460
|
+
)}
|
|
461
|
+
|
|
462
|
+
{/* Terminal */}
|
|
463
|
+
<div
|
|
464
|
+
ref={containerRef}
|
|
465
|
+
className="flex-1 bg-[#0d0f14]"
|
|
466
|
+
style={{ minHeight: '300px', maxHeight }}
|
|
467
|
+
onClick={() => terminalRef.current?.focus()}
|
|
468
|
+
/>
|
|
469
|
+
|
|
470
|
+
{/* Footer with actions */}
|
|
471
|
+
<div className="flex items-center justify-between px-4 py-3 border-t border-border-subtle bg-bg-tertiary">
|
|
472
|
+
<p className="text-xs text-text-muted">
|
|
473
|
+
Respond to prompts above to complete setup
|
|
474
|
+
</p>
|
|
475
|
+
<button
|
|
476
|
+
onClick={handleComplete}
|
|
477
|
+
className="px-4 py-2 bg-accent-cyan text-bg-deep text-sm font-semibold rounded-lg hover:bg-accent-cyan/90 transition-colors"
|
|
478
|
+
>
|
|
479
|
+
Done - Continue
|
|
480
|
+
</button>
|
|
481
|
+
</div>
|
|
482
|
+
</>
|
|
483
|
+
)}
|
|
484
|
+
|
|
485
|
+
</div>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Icons
|
|
490
|
+
function CloseIcon() {
|
|
491
|
+
return (
|
|
492
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
493
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
494
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
495
|
+
</svg>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function AlertIcon() {
|
|
500
|
+
return (
|
|
501
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
502
|
+
<circle cx="12" cy="12" r="10" />
|
|
503
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
504
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
505
|
+
</svg>
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function CheckIcon({ className, style }: { className?: string; style?: React.CSSProperties }) {
|
|
510
|
+
return (
|
|
511
|
+
<svg className={className} style={style} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
512
|
+
<polyline points="20 6 9 17 4 12" />
|
|
513
|
+
</svg>
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export default TerminalProviderSetup;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThemeProvider Component
|
|
3
|
+
*
|
|
4
|
+
* Provides theme context for light/dark mode support.
|
|
5
|
+
* Handles system preference detection and persistence.
|
|
6
|
+
*
|
|
7
|
+
* Note: Theme colors are defined as CSS variables in globals.css
|
|
8
|
+
* and referenced by Tailwind config. This enables automatic theme
|
|
9
|
+
* switching without duplicate style definitions.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
|
|
13
|
+
|
|
14
|
+
export type Theme = 'light' | 'dark' | 'system';
|
|
15
|
+
export type ResolvedTheme = 'light' | 'dark';
|
|
16
|
+
|
|
17
|
+
export interface ThemeContextValue {
|
|
18
|
+
theme: Theme;
|
|
19
|
+
resolvedTheme: ResolvedTheme;
|
|
20
|
+
setTheme: (theme: Theme) => void;
|
|
21
|
+
toggleTheme: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
25
|
+
|
|
26
|
+
export interface ThemeProviderProps {
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
defaultTheme?: Theme;
|
|
29
|
+
storageKey?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function ThemeProvider({
|
|
33
|
+
children,
|
|
34
|
+
defaultTheme = 'system',
|
|
35
|
+
storageKey = 'dashboard-theme',
|
|
36
|
+
}: ThemeProviderProps) {
|
|
37
|
+
const [theme, setThemeState] = useState<Theme>(() => {
|
|
38
|
+
// Try to get from storage
|
|
39
|
+
if (typeof window !== 'undefined') {
|
|
40
|
+
const stored = localStorage.getItem(storageKey);
|
|
41
|
+
if (stored === 'light' || stored === 'dark' || stored === 'system') {
|
|
42
|
+
return stored;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return defaultTheme;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const [systemTheme, setSystemTheme] = useState<ResolvedTheme>(() => {
|
|
49
|
+
if (typeof window !== 'undefined') {
|
|
50
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
51
|
+
}
|
|
52
|
+
return 'light';
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Listen for system theme changes
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (typeof window === 'undefined') return;
|
|
58
|
+
|
|
59
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
60
|
+
const handleChange = (e: MediaQueryListEvent) => {
|
|
61
|
+
setSystemTheme(e.matches ? 'dark' : 'light');
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
65
|
+
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
// Resolve theme
|
|
69
|
+
const resolvedTheme: ResolvedTheme = theme === 'system' ? systemTheme : theme;
|
|
70
|
+
|
|
71
|
+
// Apply theme to document
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (typeof document === 'undefined') return;
|
|
74
|
+
|
|
75
|
+
const root = document.documentElement;
|
|
76
|
+
root.classList.remove('theme-light', 'theme-dark');
|
|
77
|
+
root.classList.add(`theme-${resolvedTheme}`);
|
|
78
|
+
root.style.colorScheme = resolvedTheme;
|
|
79
|
+
}, [resolvedTheme]);
|
|
80
|
+
|
|
81
|
+
// Set theme with persistence
|
|
82
|
+
const setTheme = useCallback(
|
|
83
|
+
(newTheme: Theme) => {
|
|
84
|
+
setThemeState(newTheme);
|
|
85
|
+
if (typeof localStorage !== 'undefined') {
|
|
86
|
+
localStorage.setItem(storageKey, newTheme);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
[storageKey]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Toggle between light and dark
|
|
93
|
+
const toggleTheme = useCallback(() => {
|
|
94
|
+
setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
|
|
95
|
+
}, [resolvedTheme, setTheme]);
|
|
96
|
+
|
|
97
|
+
const value = useMemo(
|
|
98
|
+
() => ({ theme, resolvedTheme, setTheme, toggleTheme }),
|
|
99
|
+
[theme, resolvedTheme, setTheme, toggleTheme]
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function useTheme() {
|
|
106
|
+
const context = useContext(ThemeContext);
|
|
107
|
+
if (!context) {
|
|
108
|
+
throw new Error('useTheme must be used within a ThemeProvider');
|
|
109
|
+
}
|
|
110
|
+
return context;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* ThemeToggle Component
|
|
115
|
+
* A simple button to toggle between themes
|
|
116
|
+
*/
|
|
117
|
+
export interface ThemeToggleProps {
|
|
118
|
+
showLabel?: boolean;
|
|
119
|
+
className?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function ThemeToggle({ showLabel = false, className = '' }: ThemeToggleProps) {
|
|
123
|
+
const { resolvedTheme, toggleTheme } = useTheme();
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<button
|
|
127
|
+
className={`flex items-center gap-2 p-2 bg-bg-tertiary border border-border rounded-lg text-text-secondary hover:bg-bg-hover hover:text-text-primary transition-all text-sm ${className}`}
|
|
128
|
+
onClick={toggleTheme}
|
|
129
|
+
aria-label={`Switch to ${resolvedTheme === 'light' ? 'dark' : 'light'} mode`}
|
|
130
|
+
>
|
|
131
|
+
{resolvedTheme === 'light' ? <MoonIcon /> : <SunIcon />}
|
|
132
|
+
{showLabel && <span>{resolvedTheme === 'light' ? 'Dark' : 'Light'} mode</span>}
|
|
133
|
+
</button>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function SunIcon() {
|
|
138
|
+
return (
|
|
139
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
140
|
+
<circle cx="12" cy="12" r="5" />
|
|
141
|
+
<line x1="12" y1="1" x2="12" y2="3" />
|
|
142
|
+
<line x1="12" y1="21" x2="12" y2="23" />
|
|
143
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
|
144
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
145
|
+
<line x1="1" y1="12" x2="3" y2="12" />
|
|
146
|
+
<line x1="21" y1="12" x2="23" y2="12" />
|
|
147
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
|
148
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
149
|
+
</svg>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function MoonIcon() {
|
|
154
|
+
return (
|
|
155
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
156
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
157
|
+
</svg>
|
|
158
|
+
);
|
|
159
|
+
}
|