@swarmclawai/swarmclaw 0.7.3 → 0.7.5
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/README.md +47 -40
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +17 -0
- package/src/app/api/agents/[id]/thread/route.ts +4 -87
- package/src/app/api/agents/route.ts +23 -1
- package/src/app/api/auth/route.ts +1 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/route.ts +12 -0
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +7 -1
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +1 -1
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +6 -10
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +2 -1
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/page.tsx +126 -15
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +34 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +20 -4
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-sheet.tsx +249 -7
- package/src/components/agents/inspector-panel.tsx +3 -2
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +41 -14
- package/src/components/chat/chat-card.tsx +2 -1
- package/src/components/chat/chat-header.tsx +8 -13
- package/src/components/chat/chat-list.tsx +58 -20
- package/src/components/chat/message-list.tsx +142 -18
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +157 -86
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +2 -0
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/projects/project-detail.tsx +7 -2
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/section-heartbeat.tsx +11 -6
- package/src/components/shared/settings/section-orchestrator.tsx +3 -0
- package/src/components/shared/settings/settings-page.tsx +5 -3
- package/src/components/tasks/approvals-panel.tsx +7 -1
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/agent-thread-session.test.ts +85 -0
- package/src/lib/server/agent-thread-session.ts +123 -0
- package/src/lib/server/approvals-auto-approve.test.ts +59 -0
- package/src/lib/server/build-llm.test.ts +13 -5
- package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
- package/src/lib/server/chat-execution.ts +159 -71
- package/src/lib/server/chatroom-helpers.test.ts +7 -0
- package/src/lib/server/chatroom-helpers.ts +99 -6
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/manager.ts +89 -61
- package/src/lib/server/connectors/slack.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -2
- package/src/lib/server/data-dir.test.ts +56 -0
- package/src/lib/server/data-dir.ts +15 -9
- package/src/lib/server/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +23 -8
- package/src/lib/server/heartbeat-wake.ts +6 -2
- package/src/lib/server/main-agent-loop.ts +13 -6
- package/src/lib/server/openclaw-exec-config.ts +4 -2
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/orchestrator-lg.ts +1 -2
- package/src/lib/server/orchestrator.ts +3 -2
- package/src/lib/server/plugins.test.ts +9 -1
- package/src/lib/server/plugins.ts +12 -2
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +1 -1
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
- package/src/lib/server/session-tools/crud.ts +27 -3
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +18 -8
- package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
- package/src/lib/server/session-tools/file.ts +8 -2
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/index.ts +31 -1
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/monitor.ts +14 -7
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +9 -2
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/session-info.ts +22 -1
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +3 -1
- package/src/lib/server/session-tools/web.ts +73 -30
- package/src/lib/server/storage.ts +29 -3
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +139 -4
- package/src/lib/server/structured-extract.ts +1 -1
- package/src/lib/server/task-mention.ts +0 -1
- package/src/lib/server/tool-aliases.ts +37 -6
- package/src/lib/server/tool-capability-policy.ts +1 -1
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.ts +55 -1
- package/src/stores/use-app-store.ts +43 -1
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +189 -6
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
import { useMemo, useState } from 'react'
|
|
4
4
|
import { api } from '@/lib/api-client'
|
|
5
5
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
|
-
import type { ProviderType, Credential } from '@/types'
|
|
6
|
+
import type { ProviderType, Credential, GatewayProfile } from '@/types'
|
|
7
7
|
import {
|
|
8
|
+
ONBOARDING_PATHS,
|
|
8
9
|
SETUP_PROVIDERS,
|
|
9
|
-
|
|
10
|
+
STARTER_KITS,
|
|
11
|
+
getDefaultModelForProvider,
|
|
12
|
+
type OnboardingPath,
|
|
10
13
|
type SetupProvider,
|
|
14
|
+
type StarterKitAgentTemplate,
|
|
11
15
|
} from '@/lib/setup-defaults'
|
|
12
16
|
|
|
17
|
+
type SetupStep = 'path' | 'providers' | 'connect' | 'agents' | 'done'
|
|
13
18
|
type CheckState = 'idle' | 'checking' | 'ok' | 'error'
|
|
14
19
|
|
|
15
20
|
interface ProviderCheckResponse {
|
|
@@ -17,6 +22,8 @@ interface ProviderCheckResponse {
|
|
|
17
22
|
message: string
|
|
18
23
|
normalizedEndpoint?: string
|
|
19
24
|
recommendedModel?: string
|
|
25
|
+
errorCode?: string
|
|
26
|
+
deviceId?: string
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
interface SetupDoctorCheck {
|
|
@@ -39,10 +46,118 @@ interface SetupWizardProps {
|
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
interface ConfiguredProvider {
|
|
49
|
+
id: string
|
|
42
50
|
provider: SetupProvider
|
|
51
|
+
name: string
|
|
52
|
+
credentialId: string | null
|
|
53
|
+
endpoint: string | null
|
|
54
|
+
defaultModel: string
|
|
55
|
+
gatewayProfileId: string | null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface StarterDraftAgent {
|
|
59
|
+
id: string
|
|
60
|
+
templateId: string
|
|
61
|
+
name: string
|
|
62
|
+
description: string
|
|
63
|
+
systemPrompt: string
|
|
64
|
+
providerConfigId: string | null
|
|
65
|
+
provider: SetupProvider | null
|
|
66
|
+
model: string
|
|
43
67
|
credentialId: string | null
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
apiEndpoint: string | null
|
|
69
|
+
gatewayProfileId: string | null
|
|
70
|
+
tools: string[]
|
|
71
|
+
capabilities: string[]
|
|
72
|
+
platformAssignScope: 'self' | 'all'
|
|
73
|
+
enabled: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface CreatedAgentSummary {
|
|
77
|
+
id: string
|
|
78
|
+
name: string
|
|
79
|
+
provider: SetupProvider
|
|
80
|
+
providerName: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const STEP_ORDER: SetupStep[] = ['path', 'providers', 'agents', 'done']
|
|
84
|
+
const CONNECTOR_ICONS = [
|
|
85
|
+
{ name: 'Discord', icon: 'D' },
|
|
86
|
+
{ name: 'Slack', icon: 'S' },
|
|
87
|
+
{ name: 'Telegram', icon: 'T' },
|
|
88
|
+
{ name: 'WhatsApp', icon: 'W' },
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
function stepIndex(step: SetupStep): number {
|
|
92
|
+
if (step === 'connect') return STEP_ORDER.indexOf('providers')
|
|
93
|
+
return STEP_ORDER.indexOf(step)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function defaultKitForPath(path: OnboardingPath): string {
|
|
97
|
+
if (path === 'manual') return 'blank_workspace'
|
|
98
|
+
return 'personal_assistant'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function applyIntentContext(prompt: string, intentText: string): string {
|
|
102
|
+
const trimmed = intentText.trim()
|
|
103
|
+
if (!trimmed) return prompt
|
|
104
|
+
return `${prompt}
|
|
105
|
+
|
|
106
|
+
Current user intent:
|
|
107
|
+
- ${trimmed}
|
|
108
|
+
|
|
109
|
+
Keep your help aligned to this intent unless the user changes direction.`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatAgentCount(count: number): string {
|
|
113
|
+
if (count === 0) return 'Blank'
|
|
114
|
+
if (count === 1) return '1 agent'
|
|
115
|
+
return `${count} agents`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function withHttpScheme(value: string): string {
|
|
119
|
+
return /^(https?|wss?):\/\//i.test(value) ? value : `http://${value}`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseProviderUrl(value: string | null | undefined): URL | null {
|
|
123
|
+
const trimmed = typeof value === 'string' ? value.trim() : ''
|
|
124
|
+
if (!trimmed) return null
|
|
125
|
+
try {
|
|
126
|
+
return new URL(withHttpScheme(trimmed))
|
|
127
|
+
} catch {
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatEndpointHost(value: string | null | undefined): string | null {
|
|
133
|
+
const parsed = parseProviderUrl(value)
|
|
134
|
+
if (!parsed) return null
|
|
135
|
+
return parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isLocalOpenClawEndpoint(value: string | null | undefined): boolean {
|
|
139
|
+
const parsed = parseProviderUrl(value)
|
|
140
|
+
if (!parsed) return false
|
|
141
|
+
const host = parsed.hostname.trim().toLowerCase()
|
|
142
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '0.0.0.0'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolveOpenClawPort(value: string | null | undefined): number {
|
|
146
|
+
const parsed = parseProviderUrl(value)
|
|
147
|
+
const port = parsed ? Number(parsed.port) : NaN
|
|
148
|
+
return Number.isFinite(port) && port > 0 ? port : 18789
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function resolveOpenClawDashboardUrl(value: string | null | undefined): string {
|
|
152
|
+
const parsed = parseProviderUrl(value)
|
|
153
|
+
if (!parsed) return 'http://localhost:18789'
|
|
154
|
+
const next = new URL(parsed.toString())
|
|
155
|
+
if (next.protocol === 'wss:') next.protocol = 'https:'
|
|
156
|
+
if (next.protocol === 'ws:') next.protocol = 'http:'
|
|
157
|
+
next.pathname = ''
|
|
158
|
+
next.search = ''
|
|
159
|
+
next.hash = ''
|
|
160
|
+
return next.toString().replace(/\/+$/, '')
|
|
46
161
|
}
|
|
47
162
|
|
|
48
163
|
function SparkleIcon() {
|
|
@@ -99,83 +214,191 @@ function SkipLink({ onClick, label }: { onClick: () => void; label?: string }) {
|
|
|
99
214
|
)
|
|
100
215
|
}
|
|
101
216
|
|
|
102
|
-
function
|
|
103
|
-
if (!label) return null
|
|
104
|
-
return (
|
|
105
|
-
<span className="ml-2 inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-accent-bright/15 text-accent-bright text-[10px] uppercase tracking-[0.08em] font-600">
|
|
106
|
-
{label}
|
|
107
|
-
</span>
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function ConfiguredChips({ providers }: { providers: ConfiguredProvider[] }) {
|
|
217
|
+
function ConfiguredProviderChips({ providers }: { providers: ConfiguredProvider[] }) {
|
|
112
218
|
if (providers.length === 0) return null
|
|
113
219
|
return (
|
|
114
220
|
<div className="flex flex-wrap gap-2 justify-center mb-6">
|
|
115
221
|
{providers.map((cp) => (
|
|
116
222
|
<span
|
|
117
|
-
key={cp.
|
|
223
|
+
key={cp.id}
|
|
118
224
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/25 text-emerald-300 text-[12px] font-500"
|
|
119
225
|
>
|
|
120
226
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
|
121
|
-
{cp.
|
|
227
|
+
{cp.name}
|
|
228
|
+
<span className="text-emerald-300/70">
|
|
229
|
+
{cp.provider === 'openclaw' && formatEndpointHost(cp.endpoint)
|
|
230
|
+
? `· ${formatEndpointHost(cp.endpoint)}`
|
|
231
|
+
: ''}
|
|
232
|
+
{cp.defaultModel ? ` · ${cp.defaultModel}` : ''}
|
|
233
|
+
</span>
|
|
122
234
|
</span>
|
|
123
235
|
))}
|
|
124
236
|
</div>
|
|
125
237
|
)
|
|
126
238
|
}
|
|
127
239
|
|
|
128
|
-
const CONNECTOR_ICONS = [
|
|
129
|
-
{ name: 'Discord', icon: 'D' },
|
|
130
|
-
{ name: 'Slack', icon: 'S' },
|
|
131
|
-
{ name: 'Telegram', icon: 'T' },
|
|
132
|
-
{ name: 'WhatsApp', icon: 'W' },
|
|
133
|
-
]
|
|
134
|
-
|
|
135
240
|
function getOpenClawErrorHint(message: string): string | null {
|
|
136
241
|
const lower = message.toLowerCase()
|
|
137
|
-
if (lower.includes('timeout') || lower.includes('timed out'))
|
|
242
|
+
if (lower.includes('timeout') || lower.includes('timed out')) {
|
|
138
243
|
return 'Ensure the port is open and reachable from this machine.'
|
|
139
|
-
|
|
244
|
+
}
|
|
245
|
+
if (lower.includes('401') || lower.includes('unauthorized')) {
|
|
140
246
|
return 'Check your gateway auth token.'
|
|
141
|
-
|
|
247
|
+
}
|
|
248
|
+
if (lower.includes('405') || lower.includes('method not allowed')) {
|
|
142
249
|
return 'Enable chatCompletions in your OpenClaw config: openclaw config set gateway.http.endpoints.chatCompletions.enabled true'
|
|
143
|
-
|
|
250
|
+
}
|
|
251
|
+
if (lower.includes('econnrefused') || lower.includes('connection refused') || lower.includes('connect econnrefused')) {
|
|
144
252
|
return 'Verify that the OpenClaw gateway is running on the target host.'
|
|
253
|
+
}
|
|
145
254
|
return null
|
|
146
255
|
}
|
|
147
256
|
|
|
257
|
+
function preferredConfiguredProvider(
|
|
258
|
+
template: StarterKitAgentTemplate,
|
|
259
|
+
configuredProviders: ConfiguredProvider[],
|
|
260
|
+
fallbackProviderConfigId?: string | null,
|
|
261
|
+
fallbackProvider?: SetupProvider | null,
|
|
262
|
+
): ConfiguredProvider | null {
|
|
263
|
+
if (fallbackProviderConfigId) {
|
|
264
|
+
const exact = configuredProviders.find((candidate) => candidate.id === fallbackProviderConfigId)
|
|
265
|
+
if (exact) return exact
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (fallbackProvider) {
|
|
269
|
+
const exact = configuredProviders.find((candidate) => candidate.provider === fallbackProvider)
|
|
270
|
+
if (exact) return exact
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (const provider of template.recommendedProviders || []) {
|
|
274
|
+
const exact = configuredProviders.find((candidate) => candidate.provider === provider)
|
|
275
|
+
if (exact) return exact
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return configuredProviders[0] || null
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function buildStarterDrafts(args: {
|
|
282
|
+
starterKitId: string | null
|
|
283
|
+
intentText: string
|
|
284
|
+
configuredProviders: ConfiguredProvider[]
|
|
285
|
+
previousDrafts?: StarterDraftAgent[]
|
|
286
|
+
}): StarterDraftAgent[] {
|
|
287
|
+
const { starterKitId, intentText, configuredProviders, previousDrafts = [] } = args
|
|
288
|
+
const starterKit = STARTER_KITS.find((kit) => kit.id === starterKitId)
|
|
289
|
+
if (!starterKit) return []
|
|
290
|
+
|
|
291
|
+
const previousById = new Map(previousDrafts.map((draft) => [draft.id, draft]))
|
|
292
|
+
|
|
293
|
+
return starterKit.agents.map((template) => {
|
|
294
|
+
const id = `${starterKit.id}:${template.id}`
|
|
295
|
+
const previous = previousById.get(id)
|
|
296
|
+
const configuredProvider = preferredConfiguredProvider(
|
|
297
|
+
template,
|
|
298
|
+
configuredProviders,
|
|
299
|
+
previous?.providerConfigId,
|
|
300
|
+
previous?.provider,
|
|
301
|
+
)
|
|
302
|
+
const oldProvider = previous?.provider || null
|
|
303
|
+
const oldProviderDefault = oldProvider ? getDefaultModelForProvider(oldProvider) : ''
|
|
304
|
+
const nextProviderDefault = configuredProvider?.defaultModel || ''
|
|
305
|
+
const shouldRefreshModel =
|
|
306
|
+
!previous?.model
|
|
307
|
+
|| (oldProvider !== configuredProvider?.provider && previous.model === oldProviderDefault)
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
id,
|
|
311
|
+
templateId: template.id,
|
|
312
|
+
name: previous?.name || template.name,
|
|
313
|
+
description: previous?.description || template.description,
|
|
314
|
+
systemPrompt: previous?.systemPrompt || applyIntentContext(template.systemPrompt, intentText),
|
|
315
|
+
providerConfigId: configuredProvider?.id || null,
|
|
316
|
+
provider: configuredProvider?.provider || null,
|
|
317
|
+
model: shouldRefreshModel ? nextProviderDefault : previous.model,
|
|
318
|
+
credentialId: configuredProvider?.credentialId || null,
|
|
319
|
+
apiEndpoint: configuredProvider?.endpoint || null,
|
|
320
|
+
gatewayProfileId: configuredProvider?.gatewayProfileId || null,
|
|
321
|
+
tools: template.tools,
|
|
322
|
+
capabilities: previous?.capabilities || template.capabilities || [],
|
|
323
|
+
platformAssignScope: previous?.platformAssignScope || template.platformAssignScope || 'self',
|
|
324
|
+
enabled: previous?.enabled ?? true,
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
148
329
|
export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
149
|
-
const [step, setStep] = useState(
|
|
330
|
+
const [step, setStep] = useState<SetupStep>('path')
|
|
331
|
+
const [onboardingPath, setOnboardingPath] = useState<OnboardingPath | null>(null)
|
|
332
|
+
const [starterKitId, setStarterKitId] = useState<string | null>(null)
|
|
333
|
+
const [intentText, setIntentText] = useState('')
|
|
334
|
+
|
|
150
335
|
const [provider, setProvider] = useState<SetupProvider | null>(null)
|
|
336
|
+
const [providerLabel, setProviderLabel] = useState('')
|
|
151
337
|
const [endpoint, setEndpoint] = useState('')
|
|
152
338
|
const [apiKey, setApiKey] = useState('')
|
|
153
339
|
const [credentialId, setCredentialId] = useState<string | null>(null)
|
|
154
340
|
const [checkState, setCheckState] = useState<CheckState>('idle')
|
|
155
341
|
const [checkMessage, setCheckMessage] = useState('')
|
|
342
|
+
const [checkErrorCode, setCheckErrorCode] = useState<string | null>(null)
|
|
343
|
+
const [openclawDeviceId, setOpenclawDeviceId] = useState<string | null>(null)
|
|
344
|
+
const [providerSuggestedModel, setProviderSuggestedModel] = useState('')
|
|
345
|
+
const [commandCopyState, setCommandCopyState] = useState<'idle' | 'copied' | 'failed'>('idle')
|
|
346
|
+
|
|
156
347
|
const [doctorState, setDoctorState] = useState<'idle' | 'checking' | 'done' | 'error'>('idle')
|
|
157
348
|
const [doctorError, setDoctorError] = useState('')
|
|
158
349
|
const [doctorReport, setDoctorReport] = useState<SetupDoctorResponse | null>(null)
|
|
350
|
+
const [configuredProviders, setConfiguredProviders] = useState<ConfiguredProvider[]>([])
|
|
351
|
+
const [draftAgents, setDraftAgents] = useState<StarterDraftAgent[]>([])
|
|
352
|
+
const [createdAgents, setCreatedAgents] = useState<CreatedAgentSummary[]>([])
|
|
159
353
|
const [saving, setSaving] = useState(false)
|
|
160
354
|
const [error, setError] = useState('')
|
|
161
355
|
|
|
162
|
-
const [agentName, setAgentName] = useState('')
|
|
163
|
-
const [agentDescription, setAgentDescription] = useState('')
|
|
164
|
-
const [agentPrompt, setAgentPrompt] = useState('')
|
|
165
|
-
const [agentModel, setAgentModel] = useState('')
|
|
166
|
-
|
|
167
|
-
const [configuredProviders, setConfiguredProviders] = useState<ConfiguredProvider[]>([])
|
|
168
|
-
|
|
169
356
|
const selectedProvider = useMemo(
|
|
170
|
-
() => SETUP_PROVIDERS.find((
|
|
357
|
+
() => SETUP_PROVIDERS.find((candidate) => candidate.id === provider) || null,
|
|
171
358
|
[provider],
|
|
172
359
|
)
|
|
173
|
-
const
|
|
360
|
+
const selectedStarterKit = useMemo(
|
|
361
|
+
() => STARTER_KITS.find((candidate) => candidate.id === starterKitId) || null,
|
|
362
|
+
[starterKitId],
|
|
363
|
+
)
|
|
364
|
+
const totalSteps = STEP_ORDER.length
|
|
365
|
+
const singleUseProviderIds = new Set(
|
|
366
|
+
configuredProviders
|
|
367
|
+
.filter((cp) => !SETUP_PROVIDERS.find((candidate) => candidate.id === cp.provider)?.allowMultiple)
|
|
368
|
+
.map((cp) => cp.provider),
|
|
369
|
+
)
|
|
370
|
+
|
|
174
371
|
const requiresKey = selectedProvider?.requiresKey || false
|
|
175
372
|
const supportsEndpoint = selectedProvider?.supportsEndpoint || false
|
|
176
373
|
const keyIsOptional = selectedProvider?.optionalKey || false
|
|
177
374
|
const requiresVerifiedConnection = provider === 'openclaw'
|
|
178
|
-
const
|
|
375
|
+
const canContinueFromProviders = configuredProviders.length > 0 || (selectedStarterKit?.agents.length || 0) === 0
|
|
376
|
+
const openClawEndpointValue = provider === 'openclaw'
|
|
377
|
+
? (endpoint.trim() || selectedProvider?.defaultEndpoint || 'http://localhost:18789/v1')
|
|
378
|
+
: null
|
|
379
|
+
const openClawEndpointHost = openClawEndpointValue ? formatEndpointHost(openClawEndpointValue) : null
|
|
380
|
+
const openClawDashboardUrl = provider === 'openclaw'
|
|
381
|
+
? resolveOpenClawDashboardUrl(openClawEndpointValue)
|
|
382
|
+
: null
|
|
383
|
+
const openClawLocal = provider === 'openclaw' ? isLocalOpenClawEndpoint(openClawEndpointValue) : false
|
|
384
|
+
const openClawPort = provider === 'openclaw' ? resolveOpenClawPort(openClawEndpointValue) : 18789
|
|
385
|
+
const openClawLocalCommand = `npx openclaw gateway run --bind loopback --port ${openClawPort} --verbose`
|
|
386
|
+
const openClawLocalCommandPnpm = `pnpm openclaw gateway run --bind loopback --port ${openClawPort} --verbose`
|
|
387
|
+
|
|
388
|
+
const resetProviderForm = () => {
|
|
389
|
+
setProvider(null)
|
|
390
|
+
setProviderLabel('')
|
|
391
|
+
setEndpoint('')
|
|
392
|
+
setApiKey('')
|
|
393
|
+
setCredentialId(null)
|
|
394
|
+
setCheckState('idle')
|
|
395
|
+
setCheckMessage('')
|
|
396
|
+
setCheckErrorCode(null)
|
|
397
|
+
setOpenclawDeviceId(null)
|
|
398
|
+
setProviderSuggestedModel('')
|
|
399
|
+
setCommandCopyState('idle')
|
|
400
|
+
setError('')
|
|
401
|
+
}
|
|
179
402
|
|
|
180
403
|
const skip = async () => {
|
|
181
404
|
try {
|
|
@@ -186,24 +409,47 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
186
409
|
onComplete()
|
|
187
410
|
}
|
|
188
411
|
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
412
|
+
const applyPathSelection = (nextPath: OnboardingPath) => {
|
|
413
|
+
setOnboardingPath(nextPath)
|
|
414
|
+
setStarterKitId((current) => current || defaultKitForPath(nextPath))
|
|
415
|
+
setError('')
|
|
416
|
+
}
|
|
192
417
|
|
|
193
|
-
|
|
418
|
+
const continueFromPath = () => {
|
|
419
|
+
if (!onboardingPath) {
|
|
420
|
+
setError('Choose how you want to get started.')
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
if (!starterKitId) {
|
|
424
|
+
setError('Choose a starter kit or blank workspace.')
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
setDraftAgents(buildStarterDrafts({
|
|
429
|
+
starterKitId,
|
|
430
|
+
intentText,
|
|
431
|
+
configuredProviders,
|
|
432
|
+
}))
|
|
433
|
+
setError('')
|
|
434
|
+
setStep('providers')
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const selectProvider = (nextProvider: SetupProvider) => {
|
|
438
|
+
const meta = SETUP_PROVIDERS.find((candidate) => candidate.id === nextProvider)
|
|
439
|
+
const nextCount = configuredProviders.filter((candidate) => candidate.provider === nextProvider).length + 1
|
|
440
|
+
setProvider(nextProvider)
|
|
441
|
+
setProviderLabel(meta?.allowMultiple ? `${meta.name} ${nextCount}` : (meta?.name || ''))
|
|
194
442
|
setEndpoint(meta?.defaultEndpoint || '')
|
|
195
443
|
setApiKey('')
|
|
196
444
|
setCredentialId(null)
|
|
197
445
|
setCheckState('idle')
|
|
198
446
|
setCheckMessage('')
|
|
447
|
+
setCheckErrorCode(null)
|
|
448
|
+
setOpenclawDeviceId(null)
|
|
449
|
+
setProviderSuggestedModel(getDefaultModelForProvider(nextProvider))
|
|
450
|
+
setCommandCopyState('idle')
|
|
199
451
|
setError('')
|
|
200
|
-
|
|
201
|
-
setAgentName(defaults.name)
|
|
202
|
-
setAgentDescription(defaults.description)
|
|
203
|
-
setAgentPrompt(defaults.systemPrompt)
|
|
204
|
-
setAgentModel(defaults.model)
|
|
205
|
-
|
|
206
|
-
setStep(1)
|
|
452
|
+
setStep('connect')
|
|
207
453
|
}
|
|
208
454
|
|
|
209
455
|
const runConnectionCheck = async (): Promise<boolean> => {
|
|
@@ -216,31 +462,30 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
216
462
|
|
|
217
463
|
setCheckState('checking')
|
|
218
464
|
setCheckMessage('')
|
|
465
|
+
setCheckErrorCode(null)
|
|
219
466
|
setError('')
|
|
220
467
|
try {
|
|
221
468
|
const result = await api<ProviderCheckResponse>('POST', '/setup/check-provider', {
|
|
222
469
|
provider,
|
|
223
470
|
apiKey: apiKey.trim() || undefined,
|
|
224
471
|
endpoint: supportsEndpoint ? endpoint.trim() || undefined : undefined,
|
|
225
|
-
model: agentModel.trim() || undefined,
|
|
226
472
|
})
|
|
227
473
|
|
|
228
474
|
if (result.normalizedEndpoint && supportsEndpoint) {
|
|
229
475
|
setEndpoint(result.normalizedEndpoint)
|
|
230
476
|
}
|
|
231
|
-
if (result.recommendedModel
|
|
232
|
-
|
|
233
|
-
const defaultModel = DEFAULT_AGENTS[provider].model
|
|
234
|
-
if (!currentModel || currentModel === defaultModel) {
|
|
235
|
-
setAgentModel(result.recommendedModel)
|
|
236
|
-
}
|
|
477
|
+
if (result.recommendedModel) {
|
|
478
|
+
setProviderSuggestedModel(result.recommendedModel)
|
|
237
479
|
}
|
|
480
|
+
setCheckErrorCode(result.errorCode || null)
|
|
481
|
+
setOpenclawDeviceId(result.deviceId || null)
|
|
238
482
|
setCheckState(result.ok ? 'ok' : 'error')
|
|
239
483
|
setCheckMessage(result.message || (result.ok ? 'Connected successfully.' : 'Connection failed.'))
|
|
240
484
|
return !!result.ok
|
|
241
485
|
} catch (err: unknown) {
|
|
242
486
|
setCheckState('error')
|
|
243
487
|
setCheckMessage(err instanceof Error ? err.message : String(err))
|
|
488
|
+
setCheckErrorCode(null)
|
|
244
489
|
return false
|
|
245
490
|
}
|
|
246
491
|
}
|
|
@@ -259,7 +504,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
259
504
|
}
|
|
260
505
|
}
|
|
261
506
|
|
|
262
|
-
const
|
|
507
|
+
const saveProvider = async () => {
|
|
263
508
|
if (!provider || !selectedProvider) return
|
|
264
509
|
if (requiresKey && !apiKey.trim()) {
|
|
265
510
|
setError('This provider requires an API key.')
|
|
@@ -283,14 +528,33 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
283
528
|
if (shouldSaveCredential && !nextCredentialId) {
|
|
284
529
|
const cred = await api<Credential>('POST', '/credentials', {
|
|
285
530
|
provider,
|
|
286
|
-
name: `${selectedProvider.name} key`,
|
|
531
|
+
name: `${providerLabel.trim() || selectedProvider.name} key`,
|
|
287
532
|
apiKey: apiKey.trim(),
|
|
288
533
|
})
|
|
289
534
|
nextCredentialId = cred.id
|
|
290
535
|
}
|
|
291
536
|
|
|
537
|
+
const configuredProvider: ConfiguredProvider = {
|
|
538
|
+
id: crypto.randomUUID(),
|
|
539
|
+
provider,
|
|
540
|
+
name: providerLabel.trim() || selectedProvider.name,
|
|
541
|
+
credentialId: nextCredentialId || null,
|
|
542
|
+
endpoint: supportsEndpoint ? (endpoint.trim() || selectedProvider.defaultEndpoint || null) : null,
|
|
543
|
+
defaultModel: providerSuggestedModel || getDefaultModelForProvider(provider),
|
|
544
|
+
gatewayProfileId: null,
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const nextConfigured = [...configuredProviders, configuredProvider]
|
|
292
548
|
setCredentialId(nextCredentialId || null)
|
|
293
|
-
|
|
549
|
+
setConfiguredProviders(nextConfigured)
|
|
550
|
+
setDraftAgents((current) => buildStarterDrafts({
|
|
551
|
+
starterKitId,
|
|
552
|
+
intentText,
|
|
553
|
+
configuredProviders: nextConfigured,
|
|
554
|
+
previousDrafts: current,
|
|
555
|
+
}))
|
|
556
|
+
resetProviderForm()
|
|
557
|
+
setStep('providers')
|
|
294
558
|
} catch (err: unknown) {
|
|
295
559
|
setError(err instanceof Error ? err.message : String(err))
|
|
296
560
|
} finally {
|
|
@@ -298,87 +562,165 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
298
562
|
}
|
|
299
563
|
}
|
|
300
564
|
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
565
|
+
const goToAgentReview = () => {
|
|
566
|
+
setDraftAgents((current) => buildStarterDrafts({
|
|
567
|
+
starterKitId,
|
|
568
|
+
intentText,
|
|
569
|
+
configuredProviders,
|
|
570
|
+
previousDrafts: current,
|
|
571
|
+
}))
|
|
572
|
+
setError('')
|
|
573
|
+
setStep('agents')
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const updateDraftAgent = (id: string, patch: Partial<StarterDraftAgent>) => {
|
|
577
|
+
setDraftAgents((current) => current.map((draft) => (
|
|
578
|
+
draft.id === id
|
|
579
|
+
? { ...draft, ...patch }
|
|
580
|
+
: draft
|
|
581
|
+
)))
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const updateDraftAgentProvider = (id: string, nextProviderConfigId: string) => {
|
|
585
|
+
const configuredProvider = configuredProviders.find((candidate) => candidate.id === nextProviderConfigId)
|
|
586
|
+
if (!configuredProvider) return
|
|
587
|
+
|
|
588
|
+
setDraftAgents((current) => current.map((draft) => {
|
|
589
|
+
if (draft.id !== id) return draft
|
|
590
|
+
const previousDefault = draft.provider ? getDefaultModelForProvider(draft.provider) : ''
|
|
591
|
+
const nextModel = !draft.model || draft.model === previousDefault
|
|
592
|
+
? configuredProvider.defaultModel
|
|
593
|
+
: draft.model
|
|
594
|
+
return {
|
|
595
|
+
...draft,
|
|
596
|
+
providerConfigId: configuredProvider.id,
|
|
597
|
+
provider: configuredProvider.provider,
|
|
598
|
+
credentialId: configuredProvider.credentialId,
|
|
599
|
+
apiEndpoint: configuredProvider.endpoint,
|
|
600
|
+
gatewayProfileId: configuredProvider.gatewayProfileId,
|
|
601
|
+
model: nextModel,
|
|
602
|
+
}
|
|
603
|
+
}))
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const copyOpenClawLocalCommand = async () => {
|
|
607
|
+
try {
|
|
608
|
+
await navigator.clipboard.writeText(openClawLocalCommand)
|
|
609
|
+
setCommandCopyState('copied')
|
|
610
|
+
window.setTimeout(() => setCommandCopyState('idle'), 1200)
|
|
611
|
+
} catch {
|
|
612
|
+
setCommandCopyState('failed')
|
|
613
|
+
window.setTimeout(() => setCommandCopyState('idle'), 1800)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const createAgentsAndFinish = async () => {
|
|
618
|
+
const enabledDrafts = draftAgents.filter((draft) => draft.enabled)
|
|
619
|
+
if (enabledDrafts.some((draft) => !draft.provider)) {
|
|
620
|
+
setError('Every enabled agent needs a provider assignment before you continue.')
|
|
306
621
|
return
|
|
307
622
|
}
|
|
623
|
+
|
|
308
624
|
setSaving(true)
|
|
309
625
|
setError('')
|
|
310
626
|
try {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
627
|
+
const gatewayProfileIdsByProviderConfig = new Map<string, string>()
|
|
628
|
+
const openClawProviders = configuredProviders.filter((candidate) => candidate.provider === 'openclaw')
|
|
629
|
+
if (openClawProviders.length > 0) {
|
|
630
|
+
const existingGateways = await api<GatewayProfile[]>('GET', '/gateways')
|
|
631
|
+
let shouldCreateDefault = existingGateways.length === 0
|
|
632
|
+
|
|
633
|
+
for (const configuredProvider of openClawProviders) {
|
|
634
|
+
const normalizedEndpoint = (configuredProvider.endpoint || 'http://localhost:18789').trim()
|
|
635
|
+
const existing = existingGateways.find((gateway) => (
|
|
636
|
+
gateway.provider === 'openclaw'
|
|
637
|
+
&& gateway.endpoint === normalizedEndpoint
|
|
638
|
+
&& (gateway.credentialId || null) === (configuredProvider.credentialId || null)
|
|
639
|
+
))
|
|
640
|
+
if (existing) {
|
|
641
|
+
gatewayProfileIdsByProviderConfig.set(configuredProvider.id, existing.id)
|
|
642
|
+
continue
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const createdGateway = await api<GatewayProfile>('POST', '/gateways', {
|
|
646
|
+
name: configuredProvider.name,
|
|
647
|
+
endpoint: normalizedEndpoint,
|
|
648
|
+
credentialId: configuredProvider.credentialId || null,
|
|
649
|
+
tags: ['onboarding'],
|
|
650
|
+
notes: `Created during setup for ${configuredProvider.name}.`,
|
|
651
|
+
isDefault: shouldCreateDefault,
|
|
652
|
+
})
|
|
653
|
+
gatewayProfileIdsByProviderConfig.set(configuredProvider.id, createdGateway.id)
|
|
654
|
+
existingGateways.push(createdGateway)
|
|
655
|
+
shouldCreateDefault = false
|
|
656
|
+
}
|
|
323
657
|
}
|
|
324
658
|
|
|
325
|
-
const
|
|
326
|
-
let
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
659
|
+
const existingAgents = await api<Record<string, { id: string }>>('GET', '/agents')
|
|
660
|
+
let canReuseDefault = !!existingAgents.default && Object.keys(existingAgents).length === 1
|
|
661
|
+
const created: CreatedAgentSummary[] = []
|
|
662
|
+
|
|
663
|
+
for (const draft of enabledDrafts) {
|
|
664
|
+
const payload: Record<string, unknown> = {
|
|
665
|
+
name: draft.name.trim(),
|
|
666
|
+
description: draft.description.trim(),
|
|
667
|
+
systemPrompt: draft.systemPrompt.trim(),
|
|
668
|
+
provider: draft.provider as ProviderType,
|
|
669
|
+
model: draft.model.trim() || getDefaultModelForProvider(draft.provider as SetupProvider),
|
|
670
|
+
credentialId: draft.credentialId || null,
|
|
671
|
+
plugins: draft.tools,
|
|
672
|
+
capabilities: draft.capabilities,
|
|
673
|
+
platformAssignScope: draft.platformAssignScope,
|
|
674
|
+
}
|
|
334
675
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
676
|
+
if (draft.apiEndpoint) {
|
|
677
|
+
payload.apiEndpoint = draft.apiEndpoint
|
|
678
|
+
}
|
|
679
|
+
const gatewayProfileId = (draft.providerConfigId && gatewayProfileIdsByProviderConfig.get(draft.providerConfigId)) || draft.gatewayProfileId
|
|
680
|
+
if (gatewayProfileId) {
|
|
681
|
+
payload.gatewayProfileId = gatewayProfileId
|
|
682
|
+
}
|
|
338
683
|
|
|
684
|
+
let agentId: string
|
|
339
685
|
if (canReuseDefault) {
|
|
340
686
|
await api('PUT', '/agents/default', payload)
|
|
687
|
+
agentId = 'default'
|
|
688
|
+
canReuseDefault = false
|
|
689
|
+
} else {
|
|
690
|
+
agentId = (await api<{ id: string }>('POST', '/agents', payload)).id
|
|
341
691
|
}
|
|
342
692
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
} else {
|
|
350
|
-
// Additional providers just create the agent
|
|
351
|
-
agentId = (await api<{ id: string }>('POST', '/agents', payload)).id
|
|
693
|
+
created.push({
|
|
694
|
+
id: agentId,
|
|
695
|
+
name: draft.name.trim(),
|
|
696
|
+
provider: draft.provider as SetupProvider,
|
|
697
|
+
providerName: configuredProviders.find((candidate) => candidate.id === draft.providerConfigId)?.name || draft.provider as SetupProvider,
|
|
698
|
+
})
|
|
352
699
|
}
|
|
353
700
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
agentName: agentName.trim(),
|
|
701
|
+
if (created[0]) {
|
|
702
|
+
const appState = useAppStore.getState()
|
|
703
|
+
await appState.updateSettings({ defaultAgentId: created[0].id })
|
|
704
|
+
await appState.setCurrentAgent(created[0].id)
|
|
359
705
|
}
|
|
360
|
-
const nextConfigured = [...configuredProviders, newConfigured]
|
|
361
|
-
setConfiguredProviders(nextConfigured)
|
|
362
706
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
setStep(3)
|
|
381
|
-
}
|
|
707
|
+
await api('PUT', '/settings', { setupCompleted: true })
|
|
708
|
+
setCreatedAgents(created)
|
|
709
|
+
setStep('done')
|
|
710
|
+
} catch (err: unknown) {
|
|
711
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
712
|
+
} finally {
|
|
713
|
+
setSaving(false)
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const finishWithoutAgents = async () => {
|
|
718
|
+
setSaving(true)
|
|
719
|
+
setError('')
|
|
720
|
+
try {
|
|
721
|
+
await api('PUT', '/settings', { setupCompleted: true })
|
|
722
|
+
setCreatedAgents([])
|
|
723
|
+
setStep('done')
|
|
382
724
|
} catch (err: unknown) {
|
|
383
725
|
setError(err instanceof Error ? err.message : String(err))
|
|
384
726
|
} finally {
|
|
@@ -399,35 +741,171 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
399
741
|
</div>
|
|
400
742
|
|
|
401
743
|
<div
|
|
402
|
-
className="relative max-w-[
|
|
744
|
+
className="relative max-w-[760px] w-full text-center"
|
|
403
745
|
style={{ animation: 'fade-in 0.6s cubic-bezier(0.16, 1, 0.3, 1)' }}
|
|
404
746
|
>
|
|
405
747
|
<SparkleIcon />
|
|
406
|
-
<StepDots current={step} total={totalSteps} />
|
|
748
|
+
<StepDots current={stepIndex(step)} total={totalSteps} />
|
|
407
749
|
|
|
408
|
-
{step ===
|
|
750
|
+
{step === 'path' && (
|
|
409
751
|
<>
|
|
410
752
|
<h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
|
|
411
|
-
|
|
753
|
+
Choose Your Setup Path
|
|
412
754
|
</h1>
|
|
413
755
|
<p className="text-[15px] text-text-2 mb-2">
|
|
414
|
-
|
|
415
|
-
? 'Pick another provider to set up, or finish below.'
|
|
416
|
-
: 'No coding required. Pick a provider, paste a key if needed, and start chatting.'}
|
|
756
|
+
Start from your intent, start from your provider, or build it yourself.
|
|
417
757
|
</p>
|
|
418
758
|
<p className="text-[13px] text-text-3 mb-8">
|
|
419
|
-
You can change providers, models, and
|
|
759
|
+
You can still change providers, models, agents, and templates later.
|
|
420
760
|
</p>
|
|
421
761
|
|
|
422
|
-
<
|
|
762
|
+
<div className="grid gap-3 md:grid-cols-3 text-left mb-6">
|
|
763
|
+
{ONBOARDING_PATHS.map((path) => {
|
|
764
|
+
const active = onboardingPath === path.id
|
|
765
|
+
return (
|
|
766
|
+
<button
|
|
767
|
+
key={path.id}
|
|
768
|
+
onClick={() => applyPathSelection(path.id)}
|
|
769
|
+
className={`rounded-[16px] border px-5 py-4 transition-all duration-200 cursor-pointer ${
|
|
770
|
+
active
|
|
771
|
+
? 'border-accent-bright/40 bg-accent-bright/10'
|
|
772
|
+
: 'border-white/[0.08] bg-surface hover:border-accent-bright/20 hover:bg-surface-hover'
|
|
773
|
+
}`}
|
|
774
|
+
>
|
|
775
|
+
<div className="flex items-center gap-2 mb-2">
|
|
776
|
+
<span className="text-[15px] font-display font-700 text-text">{path.title}</span>
|
|
777
|
+
{path.badge && (
|
|
778
|
+
<span className="inline-flex items-center px-2 py-0.5 rounded-md bg-accent-bright/15 text-accent-bright text-[10px] uppercase tracking-[0.08em] font-600">
|
|
779
|
+
{path.badge}
|
|
780
|
+
</span>
|
|
781
|
+
)}
|
|
782
|
+
</div>
|
|
783
|
+
<p className="text-[13px] text-text-2 leading-relaxed mb-2">{path.description}</p>
|
|
784
|
+
<p className="text-[12px] text-text-3 leading-relaxed">{path.detail}</p>
|
|
785
|
+
</button>
|
|
786
|
+
)
|
|
787
|
+
})}
|
|
788
|
+
</div>
|
|
423
789
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
790
|
+
{onboardingPath === 'intent' && (
|
|
791
|
+
<div className="mb-6 text-left rounded-[16px] border border-white/[0.08] bg-surface p-4">
|
|
792
|
+
<label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">
|
|
793
|
+
What do you want SwarmClaw to help with?
|
|
794
|
+
</label>
|
|
795
|
+
<textarea
|
|
796
|
+
value={intentText}
|
|
797
|
+
onChange={(e) => setIntentText(e.target.value)}
|
|
798
|
+
rows={3}
|
|
799
|
+
placeholder="Examples: help me research AI products, build a SaaS app, manage personal projects, write better content..."
|
|
800
|
+
className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
|
|
801
|
+
text-text text-[14px] outline-none transition-all duration-200 resize-none
|
|
802
|
+
focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
|
|
803
|
+
/>
|
|
804
|
+
<p className="mt-2 text-[12px] text-text-3">
|
|
805
|
+
This is used to tailor your starter agents. You can leave it blank and refine later.
|
|
806
|
+
</p>
|
|
807
|
+
</div>
|
|
808
|
+
)}
|
|
809
|
+
|
|
810
|
+
<div className="text-left mb-6">
|
|
811
|
+
<div className="flex items-center justify-between mb-3">
|
|
812
|
+
<div>
|
|
813
|
+
<div className="text-[13px] font-600 text-text">Starter Kits</div>
|
|
814
|
+
<div className="text-[12px] text-text-3">Choose a template or start blank. You can opt individual agents in or out on the next screen.</div>
|
|
815
|
+
</div>
|
|
816
|
+
</div>
|
|
817
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
818
|
+
{STARTER_KITS.map((kit) => {
|
|
819
|
+
const active = starterKitId === kit.id
|
|
820
|
+
return (
|
|
821
|
+
<button
|
|
822
|
+
key={kit.id}
|
|
823
|
+
onClick={() => setStarterKitId(kit.id)}
|
|
824
|
+
className={`rounded-[16px] border px-5 py-4 text-left transition-all duration-200 cursor-pointer ${
|
|
825
|
+
active
|
|
826
|
+
? 'border-accent-bright/40 bg-accent-bright/10'
|
|
827
|
+
: 'border-white/[0.08] bg-surface hover:border-accent-bright/20 hover:bg-surface-hover'
|
|
828
|
+
}`}
|
|
829
|
+
>
|
|
830
|
+
<div className="flex items-center gap-2 mb-2">
|
|
831
|
+
<span className="text-[15px] font-display font-700 text-text">{kit.name}</span>
|
|
832
|
+
{kit.badge && (
|
|
833
|
+
<span className="inline-flex items-center px-2 py-0.5 rounded-md bg-white/[0.06] text-text-2 text-[10px] uppercase tracking-[0.08em] font-600">
|
|
834
|
+
{kit.badge}
|
|
835
|
+
</span>
|
|
836
|
+
)}
|
|
837
|
+
</div>
|
|
838
|
+
<p className="text-[13px] text-text-2 mb-2">{kit.description}</p>
|
|
839
|
+
<p className="text-[12px] text-text-3 leading-relaxed mb-3">{kit.detail}</p>
|
|
840
|
+
<div className="flex items-center gap-2 text-[11px] text-text-3">
|
|
841
|
+
<span className="inline-flex items-center px-2 py-0.5 rounded-md bg-white/[0.04] border border-white/[0.06]">
|
|
842
|
+
{formatAgentCount(kit.agents.length)}
|
|
843
|
+
</span>
|
|
844
|
+
{kit.recommendedFor?.includes(onboardingPath || 'quick') && (
|
|
845
|
+
<span className="inline-flex items-center px-2 py-0.5 rounded-md bg-emerald-500/10 border border-emerald-500/20 text-emerald-300">
|
|
846
|
+
Fits this path
|
|
847
|
+
</span>
|
|
848
|
+
)}
|
|
849
|
+
</div>
|
|
850
|
+
</button>
|
|
851
|
+
)
|
|
852
|
+
})}
|
|
853
|
+
</div>
|
|
854
|
+
</div>
|
|
855
|
+
|
|
856
|
+
{error && <p className="mb-4 text-[13px] text-red-400">{error}</p>}
|
|
857
|
+
|
|
858
|
+
<div className="flex items-center justify-center gap-3">
|
|
859
|
+
<button
|
|
860
|
+
onClick={continueFromPath}
|
|
861
|
+
disabled={!onboardingPath || !starterKitId}
|
|
862
|
+
className="px-8 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-display font-600
|
|
863
|
+
cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
|
|
864
|
+
shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
|
|
865
|
+
>
|
|
866
|
+
Continue
|
|
867
|
+
</button>
|
|
868
|
+
</div>
|
|
869
|
+
|
|
870
|
+
<SkipLink onClick={skip} />
|
|
871
|
+
</>
|
|
872
|
+
)}
|
|
873
|
+
|
|
874
|
+
{step === 'providers' && (
|
|
875
|
+
<>
|
|
876
|
+
<h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
|
|
877
|
+
Connect Providers
|
|
878
|
+
</h1>
|
|
879
|
+
<p className="text-[15px] text-text-2 mb-2">
|
|
880
|
+
Add one or more providers, then map them onto your starter agents.
|
|
881
|
+
</p>
|
|
882
|
+
<p className="text-[13px] text-text-3 mb-8">
|
|
883
|
+
Providers are reusable. You will choose or change the provider and model for each starter agent on the next step.
|
|
884
|
+
</p>
|
|
885
|
+
|
|
886
|
+
{selectedStarterKit && (
|
|
887
|
+
<div className="mb-6 p-4 rounded-[14px] border border-white/[0.08] bg-surface text-left">
|
|
888
|
+
<div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Starter Kit</div>
|
|
889
|
+
<div className="text-[14px] text-text mb-1">{selectedStarterKit.name}</div>
|
|
890
|
+
<div className="text-[12px] text-text-3">{selectedStarterKit.detail}</div>
|
|
891
|
+
{!!intentText.trim() && (
|
|
892
|
+
<div className="mt-3 text-[12px] text-text-2">
|
|
893
|
+
Intent: <span className="text-text-3">{intentText.trim()}</span>
|
|
894
|
+
</div>
|
|
895
|
+
)}
|
|
896
|
+
</div>
|
|
897
|
+
)}
|
|
898
|
+
|
|
899
|
+
<ConfiguredProviderChips providers={configuredProviders} />
|
|
900
|
+
|
|
901
|
+
<div className="flex flex-col gap-3 max-h-[42vh] overflow-y-auto pr-1">
|
|
902
|
+
{SETUP_PROVIDERS.map((candidate) => {
|
|
903
|
+
const isConfigured = !candidate.allowMultiple && singleUseProviderIds.has(candidate.id)
|
|
904
|
+
const configuredCount = configuredProviders.filter((cp) => cp.provider === candidate.id).length
|
|
427
905
|
return (
|
|
428
906
|
<button
|
|
429
|
-
key={
|
|
430
|
-
onClick={() => !isConfigured && selectProvider(
|
|
907
|
+
key={candidate.id}
|
|
908
|
+
onClick={() => !isConfigured && selectProvider(candidate.id)}
|
|
431
909
|
disabled={isConfigured}
|
|
432
910
|
className={`w-full px-5 py-4 rounded-[14px] border border-white/[0.08] bg-surface text-left
|
|
433
911
|
transition-all duration-200 flex items-start gap-4
|
|
@@ -438,19 +916,26 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
438
916
|
>
|
|
439
917
|
<div className="w-10 h-10 rounded-[10px] bg-white/[0.04] border border-white/[0.06] flex items-center justify-center shrink-0 mt-0.5">
|
|
440
918
|
<span className="text-[16px] font-display font-700 text-accent-bright">
|
|
441
|
-
{
|
|
919
|
+
{candidate.icon}
|
|
442
920
|
</span>
|
|
443
921
|
</div>
|
|
444
922
|
<div>
|
|
445
923
|
<div className="text-[15px] font-display font-600 text-text mb-1">
|
|
446
|
-
{
|
|
447
|
-
{isConfigured
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
924
|
+
{candidate.name}
|
|
925
|
+
{isConfigured ? (
|
|
926
|
+
<span className="ml-2 text-[10px] text-emerald-400 uppercase tracking-[0.08em]">Ready</span>
|
|
927
|
+
) : candidate.allowMultiple && configuredCount > 0 ? (
|
|
928
|
+
<span className="ml-2 inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-emerald-500/10 text-emerald-300 text-[10px] uppercase tracking-[0.08em] font-600">
|
|
929
|
+
{configuredCount} saved
|
|
930
|
+
</span>
|
|
931
|
+
) : candidate.badge ? (
|
|
932
|
+
<span className="ml-2 inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-accent-bright/15 text-accent-bright text-[10px] uppercase tracking-[0.08em] font-600">
|
|
933
|
+
{candidate.badge}
|
|
934
|
+
</span>
|
|
935
|
+
) : null}
|
|
451
936
|
</div>
|
|
452
|
-
<div className="text-[13px] text-text-3 leading-relaxed">{
|
|
453
|
-
{!
|
|
937
|
+
<div className="text-[13px] text-text-3 leading-relaxed">{candidate.description}</div>
|
|
938
|
+
{!candidate.requiresKey && !isConfigured && (
|
|
454
939
|
<div className="mt-1.5 inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-emerald-500/10 text-emerald-400 text-[11px] font-500">
|
|
455
940
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
|
|
456
941
|
No API key required
|
|
@@ -481,7 +966,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
481
966
|
<div className={`text-[12px] font-600 ${doctorReport.ok ? 'text-emerald-300' : 'text-amber-300'}`}>
|
|
482
967
|
{doctorReport.summary}
|
|
483
968
|
</div>
|
|
484
|
-
{doctorReport.checks.filter((
|
|
969
|
+
{doctorReport.checks.filter((check) => check.status !== 'pass').slice(0, 3).map((check) => (
|
|
485
970
|
<div key={check.id} className="mt-1 text-[11px] text-text-3">
|
|
486
971
|
- {check.label}: {check.detail}
|
|
487
972
|
</div>
|
|
@@ -495,31 +980,64 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
495
980
|
)}
|
|
496
981
|
</div>
|
|
497
982
|
|
|
498
|
-
<
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
983
|
+
{error && <p className="mt-4 text-[13px] text-red-400">{error}</p>}
|
|
984
|
+
|
|
985
|
+
<div className="mt-6 flex items-center justify-center gap-3">
|
|
986
|
+
<button
|
|
987
|
+
onClick={() => setStep('path')}
|
|
988
|
+
className="px-6 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px]
|
|
989
|
+
font-display font-500 cursor-pointer hover:bg-white/[0.03] transition-all duration-200"
|
|
990
|
+
>
|
|
991
|
+
Back
|
|
992
|
+
</button>
|
|
993
|
+
<button
|
|
994
|
+
onClick={goToAgentReview}
|
|
995
|
+
disabled={!canContinueFromProviders}
|
|
996
|
+
className="px-8 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-display font-600
|
|
997
|
+
cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
|
|
998
|
+
shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
|
|
999
|
+
>
|
|
1000
|
+
{selectedStarterKit?.agents.length ? 'Review Starter Agents' : 'Continue'}
|
|
1001
|
+
</button>
|
|
1002
|
+
</div>
|
|
1003
|
+
|
|
1004
|
+
<SkipLink onClick={skip} />
|
|
505
1005
|
</>
|
|
506
1006
|
)}
|
|
507
1007
|
|
|
508
|
-
{step ===
|
|
1008
|
+
{step === 'connect' && provider && selectedProvider && (
|
|
509
1009
|
<>
|
|
510
1010
|
<h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
|
|
511
1011
|
Connect {selectedProvider.name}
|
|
512
1012
|
</h1>
|
|
513
1013
|
<p className="text-[15px] text-text-2 mb-2">
|
|
514
|
-
|
|
1014
|
+
Save this provider once, then reuse it across your starter agents.
|
|
515
1015
|
</p>
|
|
516
1016
|
<p className="text-[13px] text-text-3 mb-7">
|
|
517
1017
|
{requiresVerifiedConnection
|
|
518
1018
|
? 'OpenClaw must pass connection check before you can continue.'
|
|
519
|
-
: 'You can
|
|
1019
|
+
: 'You can still continue even if the check fails and fix details later.'}
|
|
520
1020
|
</p>
|
|
521
1021
|
|
|
522
1022
|
<div className="flex flex-col gap-3 text-left mb-4">
|
|
1023
|
+
<div>
|
|
1024
|
+
<label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">
|
|
1025
|
+
Connection name
|
|
1026
|
+
</label>
|
|
1027
|
+
<input
|
|
1028
|
+
type="text"
|
|
1029
|
+
value={providerLabel}
|
|
1030
|
+
onChange={(e) => setProviderLabel(e.target.value)}
|
|
1031
|
+
placeholder={selectedProvider.name}
|
|
1032
|
+
className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-surface
|
|
1033
|
+
text-text text-[14px] outline-none transition-all duration-200
|
|
1034
|
+
focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
|
|
1035
|
+
/>
|
|
1036
|
+
<p className="mt-1.5 text-[11px] text-text-3">
|
|
1037
|
+
Helpful for multiple OpenClaw gateways or distinct provider profiles.
|
|
1038
|
+
</p>
|
|
1039
|
+
</div>
|
|
1040
|
+
|
|
523
1041
|
{supportsEndpoint && (
|
|
524
1042
|
<div>
|
|
525
1043
|
<label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">
|
|
@@ -536,18 +1054,76 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
536
1054
|
/>
|
|
537
1055
|
{provider === 'openclaw' && (
|
|
538
1056
|
<div className="mt-2 space-y-0.5">
|
|
539
|
-
<p className="text-[12px] text-text-3">Works with local (<code className="text-text-2">localhost:18789</code>) or remote OpenClaw instances.</p>
|
|
540
|
-
<p className="text-[12px] text-text-3">
|
|
541
|
-
<p className="text-[12px] text-text-3 mt-1">
|
|
542
|
-
<a href="/docs/openclaw-setup" target="_blank" rel="noopener noreferrer" className="text-accent-bright hover:underline">
|
|
543
|
-
Setup guide →
|
|
544
|
-
</a>
|
|
545
|
-
</p>
|
|
1057
|
+
<p className="text-[12px] text-text-3">Works with local (<code className="text-text-2">http://localhost:18789/v1</code>) or remote OpenClaw instances.</p>
|
|
1058
|
+
<p className="text-[12px] text-text-3">Remote example: <code className="text-text-2">https://your-gateway.ts.net/v1</code>.</p>
|
|
546
1059
|
</div>
|
|
547
1060
|
)}
|
|
548
1061
|
</div>
|
|
549
1062
|
)}
|
|
550
1063
|
|
|
1064
|
+
{provider === 'openclaw' && (
|
|
1065
|
+
<div className="rounded-[14px] border border-white/[0.08] bg-surface p-4 space-y-4">
|
|
1066
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
1067
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-3">
|
|
1068
|
+
<div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Remote gateway</div>
|
|
1069
|
+
<p className="text-[13px] text-text-2 leading-relaxed">
|
|
1070
|
+
Recommended when your OpenClaw node runs on another machine or VPS. Use a URL reachable from the machine running SwarmClaw.
|
|
1071
|
+
</p>
|
|
1072
|
+
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1073
|
+
Tailscale example: <code className="text-text-2">https://<gateway-host>.ts.net/v1</code>
|
|
1074
|
+
</p>
|
|
1075
|
+
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1076
|
+
If you only have a WebSocket gateway URL, you can still paste it here. SwarmClaw will normalize it for agent chat.
|
|
1077
|
+
</p>
|
|
1078
|
+
</div>
|
|
1079
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-3">
|
|
1080
|
+
<div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Run locally</div>
|
|
1081
|
+
<p className="text-[13px] text-text-2 leading-relaxed">
|
|
1082
|
+
Use this when SwarmClaw and OpenClaw are on the same host. <code className="text-text-2">localhost</code> always refers to the SwarmClaw host.
|
|
1083
|
+
</p>
|
|
1084
|
+
<div className="mt-3 rounded-[10px] border border-white/[0.06] bg-surface px-3 py-2">
|
|
1085
|
+
<code className="block overflow-x-auto whitespace-nowrap text-[12px] text-text-2">
|
|
1086
|
+
{openClawLocalCommand}
|
|
1087
|
+
</code>
|
|
1088
|
+
</div>
|
|
1089
|
+
<div className="mt-2 flex items-center gap-2">
|
|
1090
|
+
<button
|
|
1091
|
+
type="button"
|
|
1092
|
+
onClick={copyOpenClawLocalCommand}
|
|
1093
|
+
className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-white/[0.03] text-[12px] text-text cursor-pointer hover:bg-white/[0.06] transition-all duration-200"
|
|
1094
|
+
>
|
|
1095
|
+
{commandCopyState === 'copied'
|
|
1096
|
+
? 'Copied'
|
|
1097
|
+
: commandCopyState === 'failed'
|
|
1098
|
+
? 'Copy failed'
|
|
1099
|
+
: 'Copy command'}
|
|
1100
|
+
</button>
|
|
1101
|
+
<button
|
|
1102
|
+
type="button"
|
|
1103
|
+
onClick={() => { setEndpoint(selectedProvider.defaultEndpoint || 'http://localhost:18789/v1'); setCheckState('idle'); setCheckMessage(''); setCheckErrorCode(null) }}
|
|
1104
|
+
className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-white/[0.03] text-[12px] text-text cursor-pointer hover:bg-white/[0.06] transition-all duration-200"
|
|
1105
|
+
>
|
|
1106
|
+
Use local default
|
|
1107
|
+
</button>
|
|
1108
|
+
</div>
|
|
1109
|
+
<p className="mt-2 text-[11px] text-text-3">
|
|
1110
|
+
In a source checkout, use <code className="text-text-2">{openClawLocalCommandPnpm}</code>.
|
|
1111
|
+
</p>
|
|
1112
|
+
</div>
|
|
1113
|
+
</div>
|
|
1114
|
+
|
|
1115
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-bg px-4 py-3">
|
|
1116
|
+
<div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Connection mental model</div>
|
|
1117
|
+
<p className="text-[12px] text-text-3 leading-relaxed">
|
|
1118
|
+
SwarmClaw talks to this endpoint from its own host. If SwarmClaw is on a server, <code className="text-text-2">localhost</code> means that server, not your laptop.
|
|
1119
|
+
</p>
|
|
1120
|
+
<p className="mt-2 text-[12px] text-text-3 leading-relaxed">
|
|
1121
|
+
Current target: <span className="text-text-2">{openClawEndpointHost || 'localhost:18789'}</span>{openClawLocal ? ' · local route' : ' · remote route'}
|
|
1122
|
+
</p>
|
|
1123
|
+
</div>
|
|
1124
|
+
</div>
|
|
1125
|
+
)}
|
|
1126
|
+
|
|
551
1127
|
{(requiresKey || keyIsOptional) && (
|
|
552
1128
|
<div>
|
|
553
1129
|
<label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">
|
|
@@ -594,6 +1170,48 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
594
1170
|
const hint = getOpenClawErrorHint(checkMessage)
|
|
595
1171
|
return hint ? <p className="mt-1.5 text-[11px] text-text-3">{hint}</p> : null
|
|
596
1172
|
})()}
|
|
1173
|
+
{providerSuggestedModel && (
|
|
1174
|
+
<p className="mt-1.5 text-[11px] text-text-3">Suggested model: {providerSuggestedModel}</p>
|
|
1175
|
+
)}
|
|
1176
|
+
{provider === 'openclaw' && checkState === 'ok' && openclawDeviceId && (
|
|
1177
|
+
<p className="mt-1.5 text-[11px] text-text-3">
|
|
1178
|
+
Device paired as <code className="text-text-2">{openclawDeviceId.slice(0, 12)}...</code>.
|
|
1179
|
+
</p>
|
|
1180
|
+
)}
|
|
1181
|
+
</div>
|
|
1182
|
+
)}
|
|
1183
|
+
|
|
1184
|
+
{provider === 'openclaw' && checkState === 'error' && checkErrorCode === 'PAIRING_REQUIRED' && (
|
|
1185
|
+
<div className="mb-4 rounded-[12px] border border-emerald-500/20 bg-emerald-500/5 px-4 py-3 text-left">
|
|
1186
|
+
<div className="text-[13px] font-600 text-emerald-300">Awaiting gateway approval</div>
|
|
1187
|
+
<p className="mt-1.5 text-[12px] text-text-3 leading-relaxed">
|
|
1188
|
+
This device is pending approval on that OpenClaw gateway. Approve it from Nodes, then run the connection check again.
|
|
1189
|
+
{openclawDeviceId ? (
|
|
1190
|
+
<> Device: <code className="text-text-2">{openclawDeviceId}</code>.</>
|
|
1191
|
+
) : null}
|
|
1192
|
+
</p>
|
|
1193
|
+
{openClawDashboardUrl && (
|
|
1194
|
+
<a
|
|
1195
|
+
href={openClawDashboardUrl}
|
|
1196
|
+
target="_blank"
|
|
1197
|
+
rel="noopener noreferrer"
|
|
1198
|
+
className="mt-3 inline-flex items-center gap-1.5 rounded-[10px] border border-white/[0.08] bg-white/[0.03] px-3 py-2 text-[12px] text-text hover:bg-white/[0.06] transition-all duration-200"
|
|
1199
|
+
>
|
|
1200
|
+
Open gateway dashboard
|
|
1201
|
+
</a>
|
|
1202
|
+
)}
|
|
1203
|
+
</div>
|
|
1204
|
+
)}
|
|
1205
|
+
|
|
1206
|
+
{provider === 'openclaw' && checkState === 'error' && checkErrorCode === 'DEVICE_AUTH_INVALID' && (
|
|
1207
|
+
<div className="mb-4 rounded-[12px] border border-white/[0.08] bg-surface px-4 py-3 text-left">
|
|
1208
|
+
<div className="text-[13px] font-600 text-text">Device not paired</div>
|
|
1209
|
+
<p className="mt-1.5 text-[12px] text-text-3 leading-relaxed">
|
|
1210
|
+
The gateway does not recognize this device yet. Add or approve it from Nodes, then retry.
|
|
1211
|
+
{openclawDeviceId ? (
|
|
1212
|
+
<> Device: <code className="text-text-2">{openclawDeviceId}</code>.</>
|
|
1213
|
+
) : null}
|
|
1214
|
+
</p>
|
|
597
1215
|
</div>
|
|
598
1216
|
)}
|
|
599
1217
|
|
|
@@ -601,7 +1219,7 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
601
1219
|
|
|
602
1220
|
<div className="flex items-center justify-center gap-3">
|
|
603
1221
|
<button
|
|
604
|
-
onClick={() => {
|
|
1222
|
+
onClick={() => { resetProviderForm(); setStep('providers') }}
|
|
605
1223
|
className="px-6 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px]
|
|
606
1224
|
font-display font-500 cursor-pointer hover:bg-white/[0.03] transition-all duration-200"
|
|
607
1225
|
>
|
|
@@ -616,171 +1234,248 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
616
1234
|
{checkState === 'checking' ? 'Checking...' : 'Check Connection'}
|
|
617
1235
|
</button>
|
|
618
1236
|
<button
|
|
619
|
-
onClick={
|
|
1237
|
+
onClick={saveProvider}
|
|
620
1238
|
disabled={(requiresKey && !apiKey.trim()) || saving}
|
|
621
1239
|
className="px-8 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-display font-600
|
|
622
1240
|
cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
|
|
623
1241
|
shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
|
|
624
1242
|
>
|
|
625
|
-
{saving
|
|
626
|
-
? 'Saving...'
|
|
627
|
-
: requiresVerifiedConnection
|
|
628
|
-
? 'Verify & Continue'
|
|
629
|
-
: 'Save & Continue'}
|
|
1243
|
+
{saving ? 'Saving...' : 'Save Provider'}
|
|
630
1244
|
</button>
|
|
631
1245
|
</div>
|
|
632
1246
|
|
|
633
|
-
<SkipLink
|
|
634
|
-
onClick={configuredProviders.length > 0 ? async () => {
|
|
635
|
-
await api('PUT', '/settings', { setupCompleted: true })
|
|
636
|
-
setStep(3)
|
|
637
|
-
} : skip}
|
|
638
|
-
label={configuredProviders.length > 0 ? 'Finish Setup' : 'Skip setup for now'}
|
|
639
|
-
/>
|
|
1247
|
+
<SkipLink onClick={skip} />
|
|
640
1248
|
</>
|
|
641
1249
|
)}
|
|
642
1250
|
|
|
643
|
-
{step ===
|
|
1251
|
+
{step === 'agents' && (
|
|
644
1252
|
<>
|
|
645
1253
|
<h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
|
|
646
|
-
|
|
1254
|
+
Review Starter Agents
|
|
647
1255
|
</h1>
|
|
648
|
-
<p className="text-[15px] text-text-2 mb-
|
|
649
|
-
|
|
1256
|
+
<p className="text-[15px] text-text-2 mb-2">
|
|
1257
|
+
Choose which agents to start with, then adjust provider and model per agent.
|
|
1258
|
+
</p>
|
|
1259
|
+
<p className="text-[13px] text-text-3 mb-7">
|
|
1260
|
+
These are just starting points. You can edit them later from Agents.
|
|
650
1261
|
</p>
|
|
651
1262
|
|
|
652
|
-
|
|
653
|
-
<div className="
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
<div className="text-[12px]
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
<div className="mt-
|
|
663
|
-
|
|
664
|
-
</div>
|
|
665
|
-
|
|
666
|
-
<details className="mb-6 text-left rounded-[14px] border border-white/[0.08] bg-surface px-4 py-3">
|
|
667
|
-
<summary className="cursor-pointer text-[13px] text-text-2 font-600">
|
|
668
|
-
Advanced agent settings (optional)
|
|
669
|
-
</summary>
|
|
670
|
-
<div className="mt-3 space-y-3">
|
|
671
|
-
<div>
|
|
672
|
-
<label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Name</label>
|
|
673
|
-
<input
|
|
674
|
-
type="text"
|
|
675
|
-
value={agentName}
|
|
676
|
-
onChange={(e) => setAgentName(e.target.value)}
|
|
677
|
-
className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
|
|
678
|
-
text-text text-[14px] outline-none transition-all duration-200
|
|
679
|
-
focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
|
|
680
|
-
/>
|
|
681
|
-
</div>
|
|
682
|
-
<div>
|
|
683
|
-
<label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Description</label>
|
|
684
|
-
<input
|
|
685
|
-
type="text"
|
|
686
|
-
value={agentDescription}
|
|
687
|
-
onChange={(e) => setAgentDescription(e.target.value)}
|
|
688
|
-
className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
|
|
689
|
-
text-text text-[14px] outline-none transition-all duration-200
|
|
690
|
-
focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
|
|
691
|
-
/>
|
|
692
|
-
</div>
|
|
693
|
-
<div>
|
|
694
|
-
<label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">System Prompt</label>
|
|
695
|
-
<textarea
|
|
696
|
-
value={agentPrompt}
|
|
697
|
-
onChange={(e) => setAgentPrompt(e.target.value)}
|
|
698
|
-
rows={3}
|
|
699
|
-
className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
|
|
700
|
-
text-text text-[14px] outline-none transition-all duration-200 resize-none
|
|
701
|
-
focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
|
|
702
|
-
/>
|
|
703
|
-
</div>
|
|
704
|
-
<div>
|
|
705
|
-
<label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Model</label>
|
|
706
|
-
<input
|
|
707
|
-
type="text"
|
|
708
|
-
value={agentModel}
|
|
709
|
-
onChange={(e) => setAgentModel(e.target.value)}
|
|
710
|
-
className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
|
|
711
|
-
text-text text-[14px] font-mono outline-none transition-all duration-200
|
|
712
|
-
focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
|
|
713
|
-
/>
|
|
1263
|
+
{selectedStarterKit && (
|
|
1264
|
+
<div className="mb-5 p-4 rounded-[14px] border border-white/[0.08] bg-surface text-left">
|
|
1265
|
+
<div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-2">Setup Summary</div>
|
|
1266
|
+
<div className="text-[14px] text-text mb-1">{selectedStarterKit.name}</div>
|
|
1267
|
+
<div className="text-[12px] text-text-3">{selectedStarterKit.detail}</div>
|
|
1268
|
+
{!!intentText.trim() && (
|
|
1269
|
+
<div className="mt-3 text-[12px] text-text-2">
|
|
1270
|
+
Intent: <span className="text-text-3">{intentText.trim()}</span>
|
|
1271
|
+
</div>
|
|
1272
|
+
)}
|
|
1273
|
+
<div className="mt-3 text-[12px] text-text-3">
|
|
1274
|
+
Providers ready: {configuredProviders.length || 'none'}
|
|
714
1275
|
</div>
|
|
715
1276
|
</div>
|
|
716
|
-
|
|
1277
|
+
)}
|
|
1278
|
+
|
|
1279
|
+
{draftAgents.length === 0 ? (
|
|
1280
|
+
<div className="mb-6 p-6 rounded-[16px] border border-white/[0.08] bg-surface text-left">
|
|
1281
|
+
<div className="text-[16px] font-display font-700 text-text mb-2">Blank workspace selected</div>
|
|
1282
|
+
<p className="text-[13px] text-text-3 leading-relaxed">
|
|
1283
|
+
Finish setup now and create your first provider, agent, task, or project later from inside the app.
|
|
1284
|
+
</p>
|
|
1285
|
+
</div>
|
|
1286
|
+
) : (
|
|
1287
|
+
<div className="space-y-4 max-h-[46vh] overflow-y-auto pr-1 text-left mb-6">
|
|
1288
|
+
{draftAgents.map((draft) => (
|
|
1289
|
+
<div key={draft.id} className="rounded-[16px] border border-white/[0.08] bg-surface p-4">
|
|
1290
|
+
<div className="flex items-center justify-between gap-4 mb-4">
|
|
1291
|
+
<div>
|
|
1292
|
+
<div className="text-[15px] font-display font-700 text-text">{draft.name}</div>
|
|
1293
|
+
<div className="text-[12px] text-text-3">{draft.description}</div>
|
|
1294
|
+
</div>
|
|
1295
|
+
<label className="inline-flex items-center gap-2 text-[12px] text-text-2">
|
|
1296
|
+
<input
|
|
1297
|
+
type="checkbox"
|
|
1298
|
+
checked={draft.enabled}
|
|
1299
|
+
onChange={(e) => updateDraftAgent(draft.id, { enabled: e.target.checked })}
|
|
1300
|
+
/>
|
|
1301
|
+
Start with this agent
|
|
1302
|
+
</label>
|
|
1303
|
+
</div>
|
|
1304
|
+
|
|
1305
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
1306
|
+
<div>
|
|
1307
|
+
<label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Name</label>
|
|
1308
|
+
<input
|
|
1309
|
+
type="text"
|
|
1310
|
+
value={draft.name}
|
|
1311
|
+
onChange={(e) => updateDraftAgent(draft.id, { name: e.target.value })}
|
|
1312
|
+
className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
|
|
1313
|
+
text-text text-[14px] outline-none transition-all duration-200
|
|
1314
|
+
focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
|
|
1315
|
+
/>
|
|
1316
|
+
</div>
|
|
1317
|
+
<div>
|
|
1318
|
+
<label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Provider</label>
|
|
1319
|
+
<select
|
|
1320
|
+
value={draft.providerConfigId || ''}
|
|
1321
|
+
onChange={(e) => updateDraftAgentProvider(draft.id, e.target.value)}
|
|
1322
|
+
className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
|
|
1323
|
+
text-text text-[14px] outline-none transition-all duration-200
|
|
1324
|
+
focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
|
|
1325
|
+
>
|
|
1326
|
+
<option value="">Choose provider</option>
|
|
1327
|
+
{configuredProviders.map((configuredProvider) => (
|
|
1328
|
+
<option key={configuredProvider.id} value={configuredProvider.id}>
|
|
1329
|
+
{configuredProvider.name}
|
|
1330
|
+
{configuredProvider.provider === 'openclaw' && formatEndpointHost(configuredProvider.endpoint)
|
|
1331
|
+
? ` · ${formatEndpointHost(configuredProvider.endpoint)}`
|
|
1332
|
+
: ''}
|
|
1333
|
+
{configuredProvider.defaultModel ? ` · ${configuredProvider.defaultModel}` : ''}
|
|
1334
|
+
</option>
|
|
1335
|
+
))}
|
|
1336
|
+
</select>
|
|
1337
|
+
</div>
|
|
1338
|
+
<div className="md:col-span-2">
|
|
1339
|
+
<label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Description</label>
|
|
1340
|
+
<input
|
|
1341
|
+
type="text"
|
|
1342
|
+
value={draft.description}
|
|
1343
|
+
onChange={(e) => updateDraftAgent(draft.id, { description: e.target.value })}
|
|
1344
|
+
className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
|
|
1345
|
+
text-text text-[14px] outline-none transition-all duration-200
|
|
1346
|
+
focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
|
|
1347
|
+
/>
|
|
1348
|
+
</div>
|
|
1349
|
+
<div>
|
|
1350
|
+
<label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Model</label>
|
|
1351
|
+
<input
|
|
1352
|
+
type="text"
|
|
1353
|
+
value={draft.model}
|
|
1354
|
+
onChange={(e) => updateDraftAgent(draft.id, { model: e.target.value })}
|
|
1355
|
+
className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
|
|
1356
|
+
text-text text-[14px] font-mono outline-none transition-all duration-200
|
|
1357
|
+
focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
|
|
1358
|
+
/>
|
|
1359
|
+
</div>
|
|
1360
|
+
<div>
|
|
1361
|
+
<label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Mode</label>
|
|
1362
|
+
<select
|
|
1363
|
+
value={draft.platformAssignScope}
|
|
1364
|
+
onChange={(e) => updateDraftAgent(draft.id, { platformAssignScope: e.target.value as 'self' | 'all' })}
|
|
1365
|
+
className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-bg
|
|
1366
|
+
text-text text-[14px] outline-none transition-all duration-200
|
|
1367
|
+
focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
|
|
1368
|
+
>
|
|
1369
|
+
<option value="self">Focused agent</option>
|
|
1370
|
+
<option value="all">Delegating orchestrator</option>
|
|
1371
|
+
</select>
|
|
1372
|
+
</div>
|
|
1373
|
+
</div>
|
|
1374
|
+
|
|
1375
|
+
<details className="mt-4 rounded-[12px] border border-white/[0.08] bg-bg px-4 py-3">
|
|
1376
|
+
<summary className="cursor-pointer text-[13px] text-text-2 font-600">
|
|
1377
|
+
Prompt and tools
|
|
1378
|
+
</summary>
|
|
1379
|
+
<div className="mt-3 space-y-3">
|
|
1380
|
+
<div>
|
|
1381
|
+
<label className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">System Prompt</label>
|
|
1382
|
+
<textarea
|
|
1383
|
+
value={draft.systemPrompt}
|
|
1384
|
+
onChange={(e) => updateDraftAgent(draft.id, { systemPrompt: e.target.value })}
|
|
1385
|
+
rows={5}
|
|
1386
|
+
className="w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-surface
|
|
1387
|
+
text-text text-[14px] outline-none transition-all duration-200 resize-none
|
|
1388
|
+
focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
|
|
1389
|
+
/>
|
|
1390
|
+
</div>
|
|
1391
|
+
<div>
|
|
1392
|
+
<div className="block text-[12px] text-text-3 font-500 mb-1.5 ml-1">Tools</div>
|
|
1393
|
+
<div className="flex flex-wrap gap-2">
|
|
1394
|
+
{draft.tools.map((tool) => (
|
|
1395
|
+
<span
|
|
1396
|
+
key={tool}
|
|
1397
|
+
className="inline-flex items-center px-2 py-0.5 rounded-md bg-white/[0.04] border border-white/[0.06] text-[11px] text-text-2"
|
|
1398
|
+
>
|
|
1399
|
+
{tool}
|
|
1400
|
+
</span>
|
|
1401
|
+
))}
|
|
1402
|
+
</div>
|
|
1403
|
+
</div>
|
|
1404
|
+
</div>
|
|
1405
|
+
</details>
|
|
1406
|
+
</div>
|
|
1407
|
+
))}
|
|
1408
|
+
</div>
|
|
1409
|
+
)}
|
|
717
1410
|
|
|
718
1411
|
{error && <p className="mb-4 text-[13px] text-red-400">{error}</p>}
|
|
719
1412
|
|
|
720
1413
|
<div className="flex items-center justify-center gap-3">
|
|
721
1414
|
<button
|
|
722
|
-
onClick={() =>
|
|
1415
|
+
onClick={() => setStep('providers')}
|
|
723
1416
|
className="px-6 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px]
|
|
724
1417
|
font-display font-500 cursor-pointer hover:bg-white/[0.03] transition-all duration-200"
|
|
725
1418
|
>
|
|
726
1419
|
Back
|
|
727
1420
|
</button>
|
|
728
1421
|
<button
|
|
729
|
-
onClick={() =>
|
|
730
|
-
disabled={!agentName.trim() || saving}
|
|
1422
|
+
onClick={() => setStep('providers')}
|
|
731
1423
|
className="px-6 py-3.5 rounded-[14px] border border-white/[0.08] bg-white/[0.03] text-text text-[14px]
|
|
732
|
-
font-display font-600 cursor-pointer hover:bg-white/[0.06] transition-all duration-200
|
|
1424
|
+
font-display font-600 cursor-pointer hover:bg-white/[0.06] transition-all duration-200"
|
|
733
1425
|
>
|
|
734
|
-
|
|
1426
|
+
Add Another Provider
|
|
735
1427
|
</button>
|
|
736
1428
|
<button
|
|
737
|
-
onClick={
|
|
738
|
-
disabled={
|
|
1429
|
+
onClick={draftAgents.length === 0 ? finishWithoutAgents : createAgentsAndFinish}
|
|
1430
|
+
disabled={saving}
|
|
739
1431
|
className="px-8 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-display font-600
|
|
740
1432
|
cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
|
|
741
1433
|
shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
|
|
742
1434
|
>
|
|
743
|
-
{saving
|
|
1435
|
+
{saving
|
|
1436
|
+
? 'Saving...'
|
|
1437
|
+
: draftAgents.length === 0
|
|
1438
|
+
? 'Finish Setup'
|
|
1439
|
+
: `Create ${draftAgents.filter((draft) => draft.enabled).length} Agent${draftAgents.filter((draft) => draft.enabled).length === 1 ? '' : 's'}`}
|
|
744
1440
|
</button>
|
|
745
1441
|
</div>
|
|
746
1442
|
|
|
747
|
-
<SkipLink
|
|
748
|
-
onClick={configuredProviders.length > 0 ? async () => {
|
|
749
|
-
await api('PUT', '/settings', { setupCompleted: true })
|
|
750
|
-
setStep(3)
|
|
751
|
-
} : skip}
|
|
752
|
-
label={configuredProviders.length > 0 ? 'Finish Setup' : 'Skip setup for now'}
|
|
753
|
-
/>
|
|
1443
|
+
<SkipLink onClick={skip} />
|
|
754
1444
|
</>
|
|
755
1445
|
)}
|
|
756
1446
|
|
|
757
|
-
{step ===
|
|
1447
|
+
{step === 'done' && (
|
|
758
1448
|
<>
|
|
759
1449
|
<h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
|
|
760
1450
|
You're All Set
|
|
761
1451
|
</h1>
|
|
762
1452
|
<p className="text-[15px] text-text-2 mb-7">
|
|
763
|
-
{
|
|
764
|
-
? 'Your
|
|
765
|
-
:
|
|
1453
|
+
{createdAgents.length === 0
|
|
1454
|
+
? 'Your workspace is ready. Add providers and agents whenever you want.'
|
|
1455
|
+
: createdAgents.length === 1
|
|
1456
|
+
? 'Your starter agent is ready to chat.'
|
|
1457
|
+
: `${createdAgents.length} starter agents are ready to go.`}
|
|
766
1458
|
</p>
|
|
767
1459
|
|
|
768
|
-
{
|
|
1460
|
+
{createdAgents.length > 0 && (
|
|
769
1461
|
<div className="mb-6 p-4 rounded-[14px] border border-white/[0.08] bg-surface text-left">
|
|
770
1462
|
<div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-3">Agents Created</div>
|
|
771
1463
|
<div className="space-y-2">
|
|
772
|
-
{
|
|
773
|
-
const meta = SETUP_PROVIDERS.find((
|
|
1464
|
+
{createdAgents.map((agent) => {
|
|
1465
|
+
const meta = SETUP_PROVIDERS.find((candidate) => candidate.id === agent.provider)
|
|
774
1466
|
return (
|
|
775
|
-
<div key={
|
|
1467
|
+
<div key={agent.id} className="flex items-center gap-3">
|
|
776
1468
|
<div className="w-8 h-8 rounded-[8px] bg-white/[0.04] border border-white/[0.06] flex items-center justify-center shrink-0">
|
|
777
1469
|
<span className="text-[13px] font-display font-700 text-accent-bright">
|
|
778
1470
|
{meta?.icon || '?'}
|
|
779
1471
|
</span>
|
|
780
1472
|
</div>
|
|
781
1473
|
<div>
|
|
782
|
-
<div className="text-[14px] text-text font-500">{
|
|
783
|
-
<div className="text-[12px] text-text-3">
|
|
1474
|
+
<div className="text-[14px] text-text font-500">{agent.name}</div>
|
|
1475
|
+
<div className="text-[12px] text-text-3">
|
|
1476
|
+
{agent.providerName}
|
|
1477
|
+
{agent.providerName !== (meta?.name || agent.provider) ? ` · ${meta?.name || agent.provider}` : ''}
|
|
1478
|
+
</div>
|
|
784
1479
|
</div>
|
|
785
1480
|
</div>
|
|
786
1481
|
)
|
|
@@ -790,17 +1485,17 @@ export function SetupWizard({ onComplete }: SetupWizardProps) {
|
|
|
790
1485
|
)}
|
|
791
1486
|
|
|
792
1487
|
<div className="mb-8 p-4 rounded-[14px] border border-white/[0.08] bg-surface text-left">
|
|
793
|
-
<div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-3">
|
|
1488
|
+
<div className="text-[12px] uppercase tracking-[0.08em] text-text-3 mb-3">Next Up — Connectors</div>
|
|
794
1489
|
<p className="text-[13px] text-text-2 mb-3">
|
|
795
|
-
Bridge your agents to chat platforms
|
|
1490
|
+
Bridge your agents to chat platforms any time from Connectors.
|
|
796
1491
|
</p>
|
|
797
1492
|
<div className="flex gap-3">
|
|
798
|
-
{CONNECTOR_ICONS.map((
|
|
799
|
-
<div key={
|
|
1493
|
+
{CONNECTOR_ICONS.map((connector) => (
|
|
1494
|
+
<div key={connector.name} className="flex flex-col items-center gap-1.5">
|
|
800
1495
|
<div className="w-10 h-10 rounded-[10px] bg-white/[0.04] border border-white/[0.06] flex items-center justify-center">
|
|
801
|
-
<span className="text-[14px] font-display font-600 text-text-3">{
|
|
1496
|
+
<span className="text-[14px] font-display font-600 text-text-3">{connector.icon}</span>
|
|
802
1497
|
</div>
|
|
803
|
-
<span className="text-[10px] text-text-3">{
|
|
1498
|
+
<span className="text-[10px] text-text-3">{connector.name}</span>
|
|
804
1499
|
</div>
|
|
805
1500
|
))}
|
|
806
1501
|
</div>
|