@chrysb/alphaclaw 0.1.14 → 0.1.15
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/lib/public/css/theme.css +1 -0
- package/lib/public/js/app.js +9 -5
- package/lib/public/js/components/credentials-modal.js +17 -18
- package/lib/public/js/components/envars.js +12 -22
- package/lib/public/js/components/onboarding/welcome-config.js +108 -0
- package/lib/public/js/components/onboarding/welcome-form-step.js +283 -0
- package/lib/public/js/components/onboarding/welcome-header.js +57 -0
- package/lib/public/js/components/onboarding/welcome-setup-step.js +45 -0
- package/lib/public/js/components/scope-picker.js +1 -1
- package/lib/public/js/components/secret-input.js +45 -0
- package/lib/public/js/components/welcome.js +102 -331
- package/lib/public/login.html +12 -4
- package/package.json +1 -1
package/lib/public/css/theme.css
CHANGED
package/lib/public/js/app.js
CHANGED
|
@@ -366,11 +366,15 @@ function App() {
|
|
|
366
366
|
|
|
367
367
|
<div class="app-content">
|
|
368
368
|
<div class="max-w-2xl w-full mx-auto space-y-4">
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
369
|
+
<div style=${{ display: tab === "general" ? "" : "none" }}>
|
|
370
|
+
<${GeneralTab} onSwitchTab=${setTab} />
|
|
371
|
+
</div>
|
|
372
|
+
<div style=${{ display: tab === "models" ? "" : "none" }}>
|
|
373
|
+
<${Models} />
|
|
374
|
+
</div>
|
|
375
|
+
<div style=${{ display: tab === "envars" ? "" : "none" }}>
|
|
376
|
+
<${Envars} />
|
|
377
|
+
</div>
|
|
374
378
|
</div>
|
|
375
379
|
</div>
|
|
376
380
|
|
|
@@ -2,6 +2,7 @@ import { h } from "https://esm.sh/preact";
|
|
|
2
2
|
import { useState, useRef } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import { saveGoogleCredentials } from "../lib/api.js";
|
|
5
|
+
import { SecretInput } from "./secret-input.js";
|
|
5
6
|
const html = htm.bind(h);
|
|
6
7
|
|
|
7
8
|
export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
@@ -110,7 +111,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
110
111
|
href="https://console.cloud.google.com/apis/credentials"
|
|
111
112
|
target="_blank"
|
|
112
113
|
class="hover:text-white"
|
|
113
|
-
style="color:
|
|
114
|
+
style="color: rgba(99, 235, 255, 0.6)"
|
|
114
115
|
>Create one →</a
|
|
115
116
|
>
|
|
116
117
|
</p>
|
|
@@ -148,7 +149,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
148
149
|
href="https://console.cloud.google.com/projectcreate"
|
|
149
150
|
target="_blank"
|
|
150
151
|
class="hover:text-white"
|
|
151
|
-
style="color:
|
|
152
|
+
style="color: rgba(99, 235, 255, 0.6)"
|
|
152
153
|
>Create a Google Cloud project</a
|
|
153
154
|
>${" "}(or use existing)
|
|
154
155
|
</li>
|
|
@@ -157,7 +158,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
157
158
|
href="https://console.cloud.google.com/auth/audience"
|
|
158
159
|
target="_blank"
|
|
159
160
|
class="hover:text-white"
|
|
160
|
-
style="color:
|
|
161
|
+
style="color: rgba(99, 235, 255, 0.6)"
|
|
161
162
|
>OAuth consent screen</a
|
|
162
163
|
>${" "}→ set to <strong>External</strong>
|
|
163
164
|
</li>
|
|
@@ -166,7 +167,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
166
167
|
href="https://console.cloud.google.com/auth/audience"
|
|
167
168
|
target="_blank"
|
|
168
169
|
class="hover:text-white"
|
|
169
|
-
style="color:
|
|
170
|
+
style="color: rgba(99, 235, 255, 0.6)"
|
|
170
171
|
>Test users</a
|
|
171
172
|
>, <strong>add your own email</strong>
|
|
172
173
|
</li>
|
|
@@ -175,7 +176,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
175
176
|
href="https://console.cloud.google.com/apis/library"
|
|
176
177
|
target="_blank"
|
|
177
178
|
class="hover:text-white"
|
|
178
|
-
style="color:
|
|
179
|
+
style="color: rgba(99, 235, 255, 0.6)"
|
|
179
180
|
>Enable APIs</a
|
|
180
181
|
>${" "}for the services you selected below
|
|
181
182
|
</li>
|
|
@@ -184,7 +185,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
184
185
|
href="https://console.cloud.google.com/apis/credentials"
|
|
185
186
|
target="_blank"
|
|
186
187
|
class="hover:text-white"
|
|
187
|
-
style="color:
|
|
188
|
+
style="color: rgba(99, 235, 255, 0.6)"
|
|
188
189
|
>Credentials</a
|
|
189
190
|
>${" "}→ Create OAuth 2.0 Client ID (Web application)
|
|
190
191
|
</li>
|
|
@@ -195,7 +196,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
195
196
|
Copy Client ID + Secret (or download credentials JSON)
|
|
196
197
|
</li>
|
|
197
198
|
</ol>
|
|
198
|
-
<p class="mt-3
|
|
199
|
+
<p class="mt-3 text-yellow-500/80">
|
|
199
200
|
⚠️ App will be in "Testing" mode. Only emails added as
|
|
200
201
|
Test Users can sign in (up to 100).
|
|
201
202
|
</p>
|
|
@@ -209,7 +210,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
209
210
|
href="https://console.cloud.google.com/projectcreate"
|
|
210
211
|
target="_blank"
|
|
211
212
|
class="hover:text-white"
|
|
212
|
-
style="color:
|
|
213
|
+
style="color: rgba(99, 235, 255, 0.6)"
|
|
213
214
|
>Create a Google Cloud project</a
|
|
214
215
|
>${" "}(or use existing)
|
|
215
216
|
</li>
|
|
@@ -218,7 +219,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
218
219
|
href="https://console.cloud.google.com/auth/audience"
|
|
219
220
|
target="_blank"
|
|
220
221
|
class="hover:text-white"
|
|
221
|
-
style="color:
|
|
222
|
+
style="color: rgba(99, 235, 255, 0.6)"
|
|
222
223
|
>OAuth consent screen</a
|
|
223
224
|
>${" "}→ set to <strong>Internal</strong> (Workspace
|
|
224
225
|
only)
|
|
@@ -228,7 +229,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
228
229
|
href="https://console.cloud.google.com/apis/library"
|
|
229
230
|
target="_blank"
|
|
230
231
|
class="hover:text-white"
|
|
231
|
-
style="color:
|
|
232
|
+
style="color: rgba(99, 235, 255, 0.6)"
|
|
232
233
|
>Enable APIs</a
|
|
233
234
|
>${" "}for the services you selected below
|
|
234
235
|
</li>
|
|
@@ -237,7 +238,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
237
238
|
href="https://console.cloud.google.com/apis/credentials"
|
|
238
239
|
target="_blank"
|
|
239
240
|
class="hover:text-white"
|
|
240
|
-
style="color:
|
|
241
|
+
style="color: rgba(99, 235, 255, 0.6)"
|
|
241
242
|
>Credentials</a
|
|
242
243
|
>${" "}→ Create OAuth 2.0 Client ID (Web application)
|
|
243
244
|
</li>
|
|
@@ -248,7 +249,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
248
249
|
Copy Client ID + Secret (or download credentials JSON)
|
|
249
250
|
</li>
|
|
250
251
|
</ol>
|
|
251
|
-
<p class="mt-3
|
|
252
|
+
<p class="mt-3 text-green-500/80">
|
|
252
253
|
✓ Internal apps skip test users and verification. Only
|
|
253
254
|
users in your Workspace org can authorize this Google app.
|
|
254
255
|
</p>
|
|
@@ -285,24 +286,22 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
|
|
|
285
286
|
</div>
|
|
286
287
|
<div>
|
|
287
288
|
<label class="text-sm text-gray-400 block mb-1">Client ID</label>
|
|
288
|
-
|
|
289
|
-
type="text"
|
|
289
|
+
<${SecretInput}
|
|
290
290
|
value=${clientId}
|
|
291
291
|
onInput=${(e) => setClientId(e.target.value)}
|
|
292
292
|
placeholder="xxxx.apps.googleusercontent.com"
|
|
293
|
-
|
|
293
|
+
inputClass="flex-1 bg-black/40 border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-gray-500"
|
|
294
294
|
/>
|
|
295
295
|
</div>
|
|
296
296
|
<div>
|
|
297
297
|
<label class="text-sm text-gray-400 block mb-1"
|
|
298
298
|
>Client Secret</label
|
|
299
299
|
>
|
|
300
|
-
|
|
301
|
-
type="password"
|
|
300
|
+
<${SecretInput}
|
|
302
301
|
value=${clientSecret}
|
|
303
302
|
onInput=${(e) => setClientSecret(e.target.value)}
|
|
304
303
|
placeholder="GOCSPX-..."
|
|
305
|
-
|
|
304
|
+
inputClass="flex-1 bg-black/40 border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-gray-500"
|
|
306
305
|
/>
|
|
307
306
|
</div>
|
|
308
307
|
<div>
|
|
@@ -3,6 +3,7 @@ import { useState, useEffect, useCallback } from "https://esm.sh/preact/hooks";
|
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import { fetchEnvVars, saveEnvVars, restartGateway } from "../lib/api.js";
|
|
5
5
|
import { showToast } from "./toast.js";
|
|
6
|
+
import { SecretInput } from "./secret-input.js";
|
|
6
7
|
const html = htm.bind(h);
|
|
7
8
|
|
|
8
9
|
const kGroupLabels = {
|
|
@@ -15,22 +16,20 @@ const kGroupLabels = {
|
|
|
15
16
|
const kGroupOrder = ["github", "channels", "tools", "custom"];
|
|
16
17
|
|
|
17
18
|
const kHintByKey = {
|
|
18
|
-
ANTHROPIC_API_KEY: html`from <a href="https://console.anthropic.com" target="_blank" class="hover:underline" style="color:
|
|
19
|
+
ANTHROPIC_API_KEY: html`from <a href="https://console.anthropic.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">console.anthropic.com</a>`,
|
|
19
20
|
ANTHROPIC_TOKEN: html`from <code class="text-xs bg-black/30 px-1 rounded">claude setup-token</code>`,
|
|
20
|
-
OPENAI_API_KEY: html`from <a href="https://platform.openai.com" target="_blank" class="hover:underline" style="color:
|
|
21
|
-
GEMINI_API_KEY: html`from <a href="https://aistudio.google.com" target="_blank" class="hover:underline" style="color:
|
|
22
|
-
GITHUB_TOKEN: html`classic PAT · <code class="text-xs bg-black/30 px-1 rounded">repo</code> scope · <a href="https://github.com/settings/tokens" target="_blank" class="hover:underline" style="color:
|
|
21
|
+
OPENAI_API_KEY: html`from <a href="https://platform.openai.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">platform.openai.com</a>`,
|
|
22
|
+
GEMINI_API_KEY: html`from <a href="https://aistudio.google.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">aistudio.google.com</a>`,
|
|
23
|
+
GITHUB_TOKEN: html`classic PAT · <code class="text-xs bg-black/30 px-1 rounded">repo</code> scope · <a href="https://github.com/settings/tokens" target="_blank" class="hover:underline" style="color: var(--accent-link)">github settings</a>`,
|
|
23
24
|
GITHUB_WORKSPACE_REPO: html`use <code class="text-xs bg-black/30 px-1 rounded">owner/repo</code> or <code class="text-xs bg-black/30 px-1 rounded">https://github.com/owner/repo</code>`,
|
|
24
|
-
TELEGRAM_BOT_TOKEN: html`from <a href="https://t.me/BotFather" target="_blank" class="hover:underline" style="color:
|
|
25
|
-
DISCORD_BOT_TOKEN: html`from <a href="https://discord.com/developers/applications" target="_blank" class="hover:underline" style="color:
|
|
26
|
-
BRAVE_API_KEY: html`from <a href="https://brave.com/search/api/" target="_blank" class="hover:underline" style="color:
|
|
25
|
+
TELEGRAM_BOT_TOKEN: html`from <a href="https://t.me/BotFather" target="_blank" class="hover:underline" style="color: var(--accent-link)">@BotFather</a> · <a href="https://docs.openclaw.ai/channels/telegram" target="_blank" class="hover:underline" style="color: var(--accent-link)">full guide</a>`,
|
|
26
|
+
DISCORD_BOT_TOKEN: html`from <a href="https://discord.com/developers/applications" target="_blank" class="hover:underline" style="color: var(--accent-link)">developer portal</a> · <a href="https://docs.openclaw.ai/channels/discord" target="_blank" class="hover:underline" style="color: var(--accent-link)">full guide</a>`,
|
|
27
|
+
BRAVE_API_KEY: html`from <a href="https://brave.com/search/api/" target="_blank" class="hover:underline" style="color: var(--accent-link)">brave.com/search/api</a> — free tier available`,
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
const getHintContent = (envVar) => kHintByKey[envVar.key] || envVar.hint || "";
|
|
30
31
|
|
|
31
32
|
const EnvRow = ({ envVar, onChange, onDelete, disabled }) => {
|
|
32
|
-
const [visible, setVisible] = useState(false);
|
|
33
|
-
const isSecret = !!envVar.value;
|
|
34
33
|
const hint = getHintContent(envVar);
|
|
35
34
|
|
|
36
35
|
return html`
|
|
@@ -45,23 +44,14 @@ const EnvRow = ({ envVar, onChange, onDelete, disabled }) => {
|
|
|
45
44
|
</div>
|
|
46
45
|
<div class="flex-1 min-w-0">
|
|
47
46
|
<div class="flex items-center gap-1">
|
|
48
|
-
|
|
49
|
-
type=${isSecret && !visible ? "password" : "text"}
|
|
47
|
+
<${SecretInput}
|
|
50
48
|
value=${envVar.value}
|
|
51
|
-
placeholder=${envVar.value ? "" : "not set"}
|
|
52
49
|
onInput=${(e) => onChange(envVar.key, e.target.value)}
|
|
53
|
-
|
|
50
|
+
placeholder=${envVar.value ? "" : "not set"}
|
|
51
|
+
isSecret=${!!envVar.value}
|
|
52
|
+
inputClass="flex-1 min-w-0 bg-black/30 border border-border rounded-lg px-3 py-1.5 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
|
|
54
53
|
disabled=${disabled}
|
|
55
54
|
/>
|
|
56
|
-
${isSecret
|
|
57
|
-
? html`<button
|
|
58
|
-
onclick=${() => setVisible(!visible)}
|
|
59
|
-
class="text-gray-500 hover:text-gray-300 px-1 text-xs shrink-0"
|
|
60
|
-
title=${visible ? "Hide" : "Show"}
|
|
61
|
-
>
|
|
62
|
-
${visible ? "Hide" : "Show"}
|
|
63
|
-
</button>`
|
|
64
|
-
: null}
|
|
65
55
|
${envVar.group === "custom"
|
|
66
56
|
? html`<button
|
|
67
57
|
onclick=${() => onDelete(envVar.key)}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { kAllAiAuthFields } from "../../lib/model-config.js";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
export const kWelcomeGroups = [
|
|
8
|
+
{
|
|
9
|
+
id: "ai",
|
|
10
|
+
title: "Primary Agent Model",
|
|
11
|
+
description: "Choose your main model and authenticate its provider",
|
|
12
|
+
fields: kAllAiAuthFields,
|
|
13
|
+
validate: (vals, ctx = {}) => !!(vals.MODEL_KEY && ctx.hasAi),
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "github",
|
|
17
|
+
title: "GitHub",
|
|
18
|
+
description: "Backs up your agent's config and workspace",
|
|
19
|
+
fields: [
|
|
20
|
+
{
|
|
21
|
+
key: "GITHUB_WORKSPACE_REPO",
|
|
22
|
+
label: "Workspace Repo",
|
|
23
|
+
hint: "A new private repo will be created for you",
|
|
24
|
+
placeholder: "username/my-agent",
|
|
25
|
+
isText: true,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: "GITHUB_TOKEN",
|
|
29
|
+
label: "Personal Access Token",
|
|
30
|
+
hint: html`Create a classic PAT on${" "}<a
|
|
31
|
+
href="https://github.com/settings/tokens"
|
|
32
|
+
target="_blank"
|
|
33
|
+
class="hover:underline"
|
|
34
|
+
style="color: var(--accent-link)"
|
|
35
|
+
>GitHub settings</a
|
|
36
|
+
>${" "}with${" "}<code class="text-xs bg-black/30 px-1 rounded"
|
|
37
|
+
>repo</code
|
|
38
|
+
>${" "}scope`,
|
|
39
|
+
placeholder: "ghp_...",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
validate: (vals) => !!(vals.GITHUB_TOKEN && vals.GITHUB_WORKSPACE_REPO),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "channels",
|
|
46
|
+
title: "Channels",
|
|
47
|
+
description: "At least one is required to talk to your agent",
|
|
48
|
+
fields: [
|
|
49
|
+
{
|
|
50
|
+
key: "TELEGRAM_BOT_TOKEN",
|
|
51
|
+
label: "Telegram Bot Token",
|
|
52
|
+
hint: html`From${" "}<a
|
|
53
|
+
href="https://t.me/BotFather"
|
|
54
|
+
target="_blank"
|
|
55
|
+
class="hover:underline"
|
|
56
|
+
style="color: var(--accent-link)"
|
|
57
|
+
>@BotFather</a
|
|
58
|
+
>${" "}·${" "}<a
|
|
59
|
+
href="https://docs.openclaw.ai/channels/telegram"
|
|
60
|
+
target="_blank"
|
|
61
|
+
class="hover:underline"
|
|
62
|
+
style="color: var(--accent-link)"
|
|
63
|
+
>full guide</a
|
|
64
|
+
>`,
|
|
65
|
+
placeholder: "123456789:AAH...",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: "DISCORD_BOT_TOKEN",
|
|
69
|
+
label: "Discord Bot Token",
|
|
70
|
+
hint: html`From${" "}<a
|
|
71
|
+
href="https://discord.com/developers/applications"
|
|
72
|
+
target="_blank"
|
|
73
|
+
class="hover:underline"
|
|
74
|
+
style="color: var(--accent-link)"
|
|
75
|
+
>Developer Portal</a
|
|
76
|
+
>${" "}·${" "}<a
|
|
77
|
+
href="https://docs.openclaw.ai/channels/discord"
|
|
78
|
+
target="_blank"
|
|
79
|
+
class="hover:underline"
|
|
80
|
+
style="color: var(--accent-link)"
|
|
81
|
+
>full guide</a
|
|
82
|
+
>`,
|
|
83
|
+
placeholder: "MTQ3...",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
validate: (vals) => !!(vals.TELEGRAM_BOT_TOKEN || vals.DISCORD_BOT_TOKEN),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "tools",
|
|
90
|
+
title: "Tools (optional)",
|
|
91
|
+
description: "Enable extra capabilities for your agent",
|
|
92
|
+
fields: [
|
|
93
|
+
{
|
|
94
|
+
key: "BRAVE_API_KEY",
|
|
95
|
+
label: "Brave Search API Key",
|
|
96
|
+
hint: html`From${" "}<a
|
|
97
|
+
href="https://brave.com/search/api/"
|
|
98
|
+
target="_blank"
|
|
99
|
+
class="hover:underline"
|
|
100
|
+
style="color: var(--accent-link)"
|
|
101
|
+
>brave.com/search/api</a
|
|
102
|
+
>${" "}-${" "}free tier available`,
|
|
103
|
+
placeholder: "BSA...",
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
validate: () => true,
|
|
107
|
+
},
|
|
108
|
+
];
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { SecretInput } from "../secret-input.js";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
export const WelcomeFormStep = ({
|
|
8
|
+
activeGroup,
|
|
9
|
+
vals,
|
|
10
|
+
hasAi,
|
|
11
|
+
setValue,
|
|
12
|
+
modelOptions,
|
|
13
|
+
modelsLoading,
|
|
14
|
+
modelsError,
|
|
15
|
+
canToggleFullCatalog,
|
|
16
|
+
showAllModels,
|
|
17
|
+
setShowAllModels,
|
|
18
|
+
selectedProvider,
|
|
19
|
+
codexLoading,
|
|
20
|
+
codexStatus,
|
|
21
|
+
startCodexAuth,
|
|
22
|
+
handleCodexDisconnect,
|
|
23
|
+
codexAuthStarted,
|
|
24
|
+
codexAuthWaiting,
|
|
25
|
+
codexManualInput,
|
|
26
|
+
setCodexManualInput,
|
|
27
|
+
completeCodexAuth,
|
|
28
|
+
codexExchanging,
|
|
29
|
+
visibleAiFieldKeys,
|
|
30
|
+
error,
|
|
31
|
+
step,
|
|
32
|
+
totalGroups,
|
|
33
|
+
currentGroupValid,
|
|
34
|
+
goBack,
|
|
35
|
+
goNext,
|
|
36
|
+
loading,
|
|
37
|
+
allValid,
|
|
38
|
+
handleSubmit,
|
|
39
|
+
}) => html`
|
|
40
|
+
<div class="flex items-center justify-between">
|
|
41
|
+
<div>
|
|
42
|
+
<h2 class="text-sm font-medium text-gray-200">${activeGroup.title}</h2>
|
|
43
|
+
<p class="text-xs text-gray-500">${activeGroup.description}</p>
|
|
44
|
+
</div>
|
|
45
|
+
${activeGroup.validate(vals, { hasAi })
|
|
46
|
+
? html`<span
|
|
47
|
+
class="text-xs font-medium px-2 py-0.5 rounded-full bg-green-900/50 text-green-400"
|
|
48
|
+
>✓</span
|
|
49
|
+
>`
|
|
50
|
+
: activeGroup.id !== "tools"
|
|
51
|
+
? html`<span
|
|
52
|
+
class="text-xs font-medium px-2 py-0.5 rounded-full bg-yellow-900/50 text-yellow-400"
|
|
53
|
+
>Required</span
|
|
54
|
+
>`
|
|
55
|
+
: null}
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
${activeGroup.id === "ai" &&
|
|
59
|
+
html`
|
|
60
|
+
<div class="space-y-1">
|
|
61
|
+
<label class="text-xs font-medium text-gray-400">Model</label>
|
|
62
|
+
<select
|
|
63
|
+
value=${vals.MODEL_KEY || ""}
|
|
64
|
+
onInput=${(e) => setValue("MODEL_KEY", e.target.value)}
|
|
65
|
+
class="w-full bg-black/30 border border-border rounded-lg pl-3 pr-8 py-2 text-sm text-gray-200 outline-none focus:border-gray-500"
|
|
66
|
+
>
|
|
67
|
+
<option value="">Select a model</option>
|
|
68
|
+
${modelOptions.map(
|
|
69
|
+
(model) => html`
|
|
70
|
+
<option value=${model.key}>${model.label || model.key}</option>
|
|
71
|
+
`,
|
|
72
|
+
)}
|
|
73
|
+
</select>
|
|
74
|
+
<p class="text-xs text-gray-600">
|
|
75
|
+
${modelsLoading
|
|
76
|
+
? "Loading model catalog..."
|
|
77
|
+
: modelsError
|
|
78
|
+
? modelsError
|
|
79
|
+
: ""}
|
|
80
|
+
</p>
|
|
81
|
+
${canToggleFullCatalog &&
|
|
82
|
+
html`
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
onclick=${() => setShowAllModels((prev) => !prev)}
|
|
86
|
+
class="text-xs text-gray-500 hover:text-gray-300"
|
|
87
|
+
>
|
|
88
|
+
${showAllModels
|
|
89
|
+
? "Show recommended models"
|
|
90
|
+
: "Show full model catalog"}
|
|
91
|
+
</button>
|
|
92
|
+
`}
|
|
93
|
+
</div>
|
|
94
|
+
`}
|
|
95
|
+
${activeGroup.id === "ai" &&
|
|
96
|
+
selectedProvider === "openai-codex" &&
|
|
97
|
+
html`
|
|
98
|
+
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
99
|
+
<div class="flex items-center justify-between">
|
|
100
|
+
<span class="text-xs text-gray-400">Codex OAuth</span>
|
|
101
|
+
${codexLoading
|
|
102
|
+
? html`<span class="text-xs text-gray-500">Checking...</span>`
|
|
103
|
+
: codexStatus.connected
|
|
104
|
+
? html`<span class="text-xs text-green-400">Connected</span>`
|
|
105
|
+
: html`<span class="text-xs text-yellow-400">Not connected</span>`}
|
|
106
|
+
</div>
|
|
107
|
+
<div class="flex gap-2">
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
onclick=${startCodexAuth}
|
|
111
|
+
class="text-xs font-medium px-3 py-1.5 rounded-lg ${codexStatus.connected
|
|
112
|
+
? "border border-border text-gray-300 hover:border-gray-500"
|
|
113
|
+
: "bg-white text-black hover:opacity-85"}"
|
|
114
|
+
>
|
|
115
|
+
${codexStatus.connected ? "Reconnect Codex" : "Connect Codex OAuth"}
|
|
116
|
+
</button>
|
|
117
|
+
${codexStatus.connected &&
|
|
118
|
+
html`
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
onclick=${handleCodexDisconnect}
|
|
122
|
+
class="text-xs font-medium px-3 py-1.5 rounded-lg border border-border text-gray-300 hover:border-gray-500"
|
|
123
|
+
>
|
|
124
|
+
Disconnect
|
|
125
|
+
</button>
|
|
126
|
+
`}
|
|
127
|
+
</div>
|
|
128
|
+
${!codexStatus.connected &&
|
|
129
|
+
codexAuthStarted &&
|
|
130
|
+
html`
|
|
131
|
+
<div class="space-y-1 pt-1">
|
|
132
|
+
<p class="text-xs text-gray-500">
|
|
133
|
+
${codexAuthWaiting
|
|
134
|
+
? "Complete login in the popup, then paste the full redirect URL from the address bar (starts with "
|
|
135
|
+
: "Paste the full redirect URL from the address bar (starts with "}
|
|
136
|
+
<code class="text-xs bg-black/30 px-1 rounded"
|
|
137
|
+
>http://localhost:1455/auth/callback</code
|
|
138
|
+
>) ${codexAuthWaiting ? " to finish setup." : " to finish setup."}
|
|
139
|
+
</p>
|
|
140
|
+
<input
|
|
141
|
+
type="text"
|
|
142
|
+
value=${codexManualInput}
|
|
143
|
+
onInput=${(e) => setCodexManualInput(e.target.value)}
|
|
144
|
+
placeholder="http://localhost:1455/auth/callback?code=...&state=..."
|
|
145
|
+
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 outline-none focus:border-gray-500"
|
|
146
|
+
/>
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
onclick=${completeCodexAuth}
|
|
150
|
+
disabled=${!codexManualInput.trim() || codexExchanging}
|
|
151
|
+
class="text-xs font-medium px-3 py-1.5 rounded-lg ${!codexManualInput.trim() ||
|
|
152
|
+
codexExchanging
|
|
153
|
+
? "bg-gray-700 text-gray-400 cursor-not-allowed"
|
|
154
|
+
: "bg-white text-black hover:opacity-85"}"
|
|
155
|
+
>
|
|
156
|
+
${codexExchanging ? "Completing..." : "Complete Codex OAuth"}
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
`}
|
|
160
|
+
</div>
|
|
161
|
+
`}
|
|
162
|
+
${(activeGroup.id === "ai"
|
|
163
|
+
? activeGroup.fields.filter((field) => visibleAiFieldKeys.has(field.key))
|
|
164
|
+
: activeGroup.fields
|
|
165
|
+
).map(
|
|
166
|
+
(field) => html`
|
|
167
|
+
<div class="space-y-1" key=${field.key}>
|
|
168
|
+
<label class="text-xs font-medium text-gray-400">${field.label}</label>
|
|
169
|
+
<${SecretInput}
|
|
170
|
+
key=${field.key}
|
|
171
|
+
value=${vals[field.key] || ""}
|
|
172
|
+
onInput=${(e) => setValue(field.key, e.target.value)}
|
|
173
|
+
placeholder=${field.placeholder || ""}
|
|
174
|
+
isSecret=${!field.isText}
|
|
175
|
+
inputClass="flex-1 bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
|
|
176
|
+
/>
|
|
177
|
+
<p class="text-xs text-gray-600">${field.hint}</p>
|
|
178
|
+
</div>
|
|
179
|
+
`,
|
|
180
|
+
)}
|
|
181
|
+
${error
|
|
182
|
+
? html`<div
|
|
183
|
+
class="bg-red-900/30 border border-red-800 rounded-xl p-3 text-red-300 text-sm"
|
|
184
|
+
>
|
|
185
|
+
${error}
|
|
186
|
+
</div>`
|
|
187
|
+
: null}
|
|
188
|
+
${step === totalGroups - 1 && (!vals.OPENAI_API_KEY || !vals.GEMINI_API_KEY)
|
|
189
|
+
? html`
|
|
190
|
+
${!vals.OPENAI_API_KEY
|
|
191
|
+
? html`<div class="space-y-1">
|
|
192
|
+
<label class="text-xs font-medium text-gray-400"
|
|
193
|
+
>OpenAI API Key</label
|
|
194
|
+
>
|
|
195
|
+
<${SecretInput}
|
|
196
|
+
value=${vals.OPENAI_API_KEY || ""}
|
|
197
|
+
onInput=${(e) => setValue("OPENAI_API_KEY", e.target.value)}
|
|
198
|
+
placeholder="sk-..."
|
|
199
|
+
isSecret=${true}
|
|
200
|
+
inputClass="flex-1 bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
|
|
201
|
+
/>
|
|
202
|
+
<p class="text-xs text-gray-600">
|
|
203
|
+
Used for memory embeddings -${" "}
|
|
204
|
+
<a
|
|
205
|
+
href="https://platform.openai.com"
|
|
206
|
+
target="_blank"
|
|
207
|
+
class="hover:underline"
|
|
208
|
+
style="color: var(--accent-link)"
|
|
209
|
+
>get key</a
|
|
210
|
+
>
|
|
211
|
+
</p>
|
|
212
|
+
</div>`
|
|
213
|
+
: null}
|
|
214
|
+
${!vals.GEMINI_API_KEY
|
|
215
|
+
? html`<div class="space-y-1">
|
|
216
|
+
<label class="text-xs font-medium text-gray-400"
|
|
217
|
+
>Gemini API Key</label
|
|
218
|
+
>
|
|
219
|
+
<${SecretInput}
|
|
220
|
+
value=${vals.GEMINI_API_KEY || ""}
|
|
221
|
+
onInput=${(e) => setValue("GEMINI_API_KEY", e.target.value)}
|
|
222
|
+
placeholder="AI..."
|
|
223
|
+
isSecret=${true}
|
|
224
|
+
inputClass="flex-1 bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
|
|
225
|
+
/>
|
|
226
|
+
<p class="text-xs text-gray-600">
|
|
227
|
+
Used for memory embeddings and Nano Banana -${" "}
|
|
228
|
+
<a
|
|
229
|
+
href="https://aistudio.google.com"
|
|
230
|
+
target="_blank"
|
|
231
|
+
class="hover:underline"
|
|
232
|
+
style="color: var(--accent-link)"
|
|
233
|
+
>get key</a
|
|
234
|
+
>
|
|
235
|
+
</p>
|
|
236
|
+
</div>`
|
|
237
|
+
: null}
|
|
238
|
+
`
|
|
239
|
+
: null}
|
|
240
|
+
|
|
241
|
+
<div class="grid grid-cols-2 gap-2 pt-3">
|
|
242
|
+
${step < totalGroups - 1
|
|
243
|
+
? html`
|
|
244
|
+
${step > 0
|
|
245
|
+
? html`<button
|
|
246
|
+
onclick=${goBack}
|
|
247
|
+
class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all border border-border text-gray-300 hover:border-gray-500"
|
|
248
|
+
>
|
|
249
|
+
Back
|
|
250
|
+
</button>`
|
|
251
|
+
: html`<div class="w-full"></div>`}
|
|
252
|
+
<button
|
|
253
|
+
onclick=${goNext}
|
|
254
|
+
disabled=${!currentGroupValid}
|
|
255
|
+
class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ${currentGroupValid
|
|
256
|
+
? "bg-white text-black hover:opacity-85"
|
|
257
|
+
: "bg-gray-800 text-gray-500 cursor-not-allowed"}"
|
|
258
|
+
>
|
|
259
|
+
Next
|
|
260
|
+
</button>
|
|
261
|
+
`
|
|
262
|
+
: html`
|
|
263
|
+
${step > 0
|
|
264
|
+
? html`<button
|
|
265
|
+
onclick=${goBack}
|
|
266
|
+
class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all border border-border text-gray-300 hover:border-gray-500"
|
|
267
|
+
>
|
|
268
|
+
Back
|
|
269
|
+
</button>`
|
|
270
|
+
: html`<div class="w-full"></div>`}
|
|
271
|
+
<button
|
|
272
|
+
onclick=${handleSubmit}
|
|
273
|
+
disabled=${!allValid || loading}
|
|
274
|
+
class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ${allValid &&
|
|
275
|
+
!loading
|
|
276
|
+
? "bg-white text-black hover:opacity-85"
|
|
277
|
+
: "bg-gray-800 text-gray-500 cursor-not-allowed"}"
|
|
278
|
+
>
|
|
279
|
+
${loading ? "Starting..." : "Complete Setup"}
|
|
280
|
+
</button>
|
|
281
|
+
`}
|
|
282
|
+
</div>
|
|
283
|
+
`;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
|
|
4
|
+
const html = htm.bind(h);
|
|
5
|
+
|
|
6
|
+
export const WelcomeHeader = ({
|
|
7
|
+
groups,
|
|
8
|
+
step,
|
|
9
|
+
isSetupStep,
|
|
10
|
+
stepNumber,
|
|
11
|
+
activeStepLabel,
|
|
12
|
+
vals,
|
|
13
|
+
hasAi,
|
|
14
|
+
}) => {
|
|
15
|
+
const progressSteps = [...groups, { id: "setup", title: "Initializing" }];
|
|
16
|
+
|
|
17
|
+
return html`
|
|
18
|
+
<div class="text-center mb-1">
|
|
19
|
+
<img
|
|
20
|
+
src="./img/logo.svg"
|
|
21
|
+
alt="alphaclaw"
|
|
22
|
+
class="mx-auto mb-3"
|
|
23
|
+
width="32"
|
|
24
|
+
height="33"
|
|
25
|
+
/>
|
|
26
|
+
<h1 class="text-2xl font-semibold mb-2">Setup</h1>
|
|
27
|
+
<p style="color: var(--text-muted)" class="text-sm">
|
|
28
|
+
Let's get your agent running
|
|
29
|
+
</p>
|
|
30
|
+
<p class="text-xs my-2" style="color: var(--text-dim)">
|
|
31
|
+
Step ${stepNumber} of ${progressSteps.length} - ${activeStepLabel}
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div class="flex items-center gap-2">
|
|
36
|
+
${progressSteps.map((group, idx) => {
|
|
37
|
+
const isFinalStep = idx === progressSteps.length - 1;
|
|
38
|
+
const isActive = idx === step;
|
|
39
|
+
const isComplete = isFinalStep
|
|
40
|
+
? isSetupStep
|
|
41
|
+
: idx < step && group.validate(vals, { hasAi });
|
|
42
|
+
const bg = isActive
|
|
43
|
+
? "rgba(99, 235, 255, 0.9)"
|
|
44
|
+
: isComplete
|
|
45
|
+
? "rgba(99, 235, 255, 0.55)"
|
|
46
|
+
: "rgba(82, 94, 122, 0.45)";
|
|
47
|
+
return html`
|
|
48
|
+
<div
|
|
49
|
+
class="h-1 flex-1 rounded-full transition-colors"
|
|
50
|
+
style=${{ background: bg }}
|
|
51
|
+
title=${group.title}
|
|
52
|
+
></div>
|
|
53
|
+
`;
|
|
54
|
+
})}
|
|
55
|
+
</div>
|
|
56
|
+
`;
|
|
57
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
|
|
4
|
+
const html = htm.bind(h);
|
|
5
|
+
|
|
6
|
+
export const WelcomeSetupStep = ({ error, loading, onRetry }) => html`
|
|
7
|
+
<div class="py-10 flex flex-col items-center text-center gap-4">
|
|
8
|
+
<svg
|
|
9
|
+
class="animate-spin h-8 w-8 text-white"
|
|
10
|
+
viewBox="0 0 24 24"
|
|
11
|
+
fill="none"
|
|
12
|
+
>
|
|
13
|
+
<circle
|
|
14
|
+
class="opacity-25"
|
|
15
|
+
cx="12"
|
|
16
|
+
cy="12"
|
|
17
|
+
r="10"
|
|
18
|
+
stroke="currentColor"
|
|
19
|
+
stroke-width="4"
|
|
20
|
+
/>
|
|
21
|
+
<path
|
|
22
|
+
class="opacity-75"
|
|
23
|
+
fill="currentColor"
|
|
24
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
<h3 class="text-lg font-semibold text-white">Initializing OpenClaw...</h3>
|
|
28
|
+
<p class="text-sm text-gray-500">This could take 10-15 seconds</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
${error
|
|
32
|
+
? html`<div class="bg-red-900/30 border border-red-800 rounded-xl p-3 text-red-300 text-sm">
|
|
33
|
+
${error}
|
|
34
|
+
</div>
|
|
35
|
+
<button
|
|
36
|
+
onclick=${onRetry}
|
|
37
|
+
disabled=${loading}
|
|
38
|
+
class="w-full text-sm font-medium px-4 py-3 rounded-xl transition-all ${loading
|
|
39
|
+
? "bg-gray-800 text-gray-500 cursor-not-allowed"
|
|
40
|
+
: "bg-white text-black hover:opacity-85"}"
|
|
41
|
+
>
|
|
42
|
+
${loading ? "Retrying..." : "Retry"}
|
|
43
|
+
</button>`
|
|
44
|
+
: null}
|
|
45
|
+
`;
|
|
@@ -46,7 +46,7 @@ export function ScopePicker({ scopes, onToggle, apiStatus, loading }) {
|
|
|
46
46
|
apiIndicator = html`<span class="text-gray-500 text-xs flex items-center gap-1"><span class="inline-block w-3 h-3 border-2 border-gray-500 border-t-transparent rounded-full animate-spin"></span></span>`;
|
|
47
47
|
} else if (api) {
|
|
48
48
|
if (api.status === 'ok') {
|
|
49
|
-
apiIndicator = html`<a href=${api.enableUrl || getApiEnableUrl(s.key)} target="_blank" class="text-green-500 hover:text-green-300 text-xs"
|
|
49
|
+
apiIndicator = html`<a href=${api.enableUrl || getApiEnableUrl(s.key)} target="_blank" class="text-green-500 hover:text-green-300 text-xs px-1.5 py-0.5 rounded bg-green-500/10">API ✓</a>`;
|
|
50
50
|
} else if (api.status === 'not_enabled') {
|
|
51
51
|
apiIndicator = html`<a href=${api.enableUrl} target="_blank" class="text-red-400 hover:text-red-300 text-xs underline">Enable API</a>`;
|
|
52
52
|
} else if (api.status === 'error') {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useState } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
const html = htm.bind(h);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reusable input with show/hide toggle for secret values.
|
|
8
|
+
*
|
|
9
|
+
* Props:
|
|
10
|
+
* value, onInput, placeholder, inputClass, disabled
|
|
11
|
+
* isSecret – treat as password field (default true)
|
|
12
|
+
*/
|
|
13
|
+
export const SecretInput = ({
|
|
14
|
+
value = "",
|
|
15
|
+
onInput,
|
|
16
|
+
placeholder = "",
|
|
17
|
+
inputClass = "",
|
|
18
|
+
disabled = false,
|
|
19
|
+
isSecret = true,
|
|
20
|
+
}) => {
|
|
21
|
+
const [visible, setVisible] = useState(false);
|
|
22
|
+
const showToggle = isSecret;
|
|
23
|
+
|
|
24
|
+
return html`
|
|
25
|
+
<div class="flex-1 min-w-0 flex items-center gap-1">
|
|
26
|
+
<input
|
|
27
|
+
type=${isSecret && !visible ? "password" : "text"}
|
|
28
|
+
value=${value}
|
|
29
|
+
placeholder=${placeholder}
|
|
30
|
+
onInput=${onInput}
|
|
31
|
+
disabled=${disabled}
|
|
32
|
+
class=${inputClass}
|
|
33
|
+
/>
|
|
34
|
+
${showToggle
|
|
35
|
+
? html`<button
|
|
36
|
+
type="button"
|
|
37
|
+
onclick=${() => setVisible((v) => !v)}
|
|
38
|
+
class="text-gray-500 hover:text-gray-300 px-1 text-xs shrink-0"
|
|
39
|
+
>
|
|
40
|
+
${visible ? "Hide" : "Show"}
|
|
41
|
+
</button>`
|
|
42
|
+
: null}
|
|
43
|
+
</div>
|
|
44
|
+
`;
|
|
45
|
+
};
|
|
@@ -12,113 +12,24 @@ import {
|
|
|
12
12
|
getModelProvider,
|
|
13
13
|
getFeaturedModels,
|
|
14
14
|
getVisibleAiFieldKeys,
|
|
15
|
-
kAllAiAuthFields,
|
|
16
15
|
} from "../lib/model-config.js";
|
|
16
|
+
import { kWelcomeGroups } from "./onboarding/welcome-config.js";
|
|
17
|
+
import { WelcomeHeader } from "./onboarding/welcome-header.js";
|
|
18
|
+
import { WelcomeSetupStep } from "./onboarding/welcome-setup-step.js";
|
|
19
|
+
import { WelcomeFormStep } from "./onboarding/welcome-form-step.js";
|
|
17
20
|
const html = htm.bind(h);
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
{
|
|
21
|
-
id: "ai",
|
|
22
|
-
title: "Primary Agent Model",
|
|
23
|
-
description: "Choose your main model and authenticate its provider",
|
|
24
|
-
fields: kAllAiAuthFields,
|
|
25
|
-
validate: (vals, ctx = {}) => !!(vals.MODEL_KEY && ctx.hasAi),
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
id: "github",
|
|
29
|
-
title: "GitHub",
|
|
30
|
-
description: "Backs up your agent's config and workspace",
|
|
31
|
-
fields: [
|
|
32
|
-
{
|
|
33
|
-
key: "GITHUB_TOKEN",
|
|
34
|
-
label: "Personal Access Token",
|
|
35
|
-
hint: html`Create a classic PAT at${" "}<a
|
|
36
|
-
href="https://github.com/settings/tokens"
|
|
37
|
-
target="_blank"
|
|
38
|
-
class="hover:underline" style="color: var(--accent)"
|
|
39
|
-
>github.com/settings/tokens</a
|
|
40
|
-
>${" "}with${" "}<code class="text-xs bg-black/30 px-1 rounded">repo</code>${" "}scope`,
|
|
41
|
-
placeholder: "ghp_...",
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
key: "GITHUB_WORKSPACE_REPO",
|
|
45
|
-
label: "Workspace Repo",
|
|
46
|
-
hint: "A new private repo will be created for you",
|
|
47
|
-
placeholder: "username/my-agent",
|
|
48
|
-
isText: true,
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
validate: (vals) => !!(vals.GITHUB_TOKEN && vals.GITHUB_WORKSPACE_REPO),
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
id: "channels",
|
|
55
|
-
title: "Channels",
|
|
56
|
-
description: "At least one is required to talk to your agent",
|
|
57
|
-
fields: [
|
|
58
|
-
{
|
|
59
|
-
key: "TELEGRAM_BOT_TOKEN",
|
|
60
|
-
label: "Telegram Bot Token",
|
|
61
|
-
hint: html`From${" "}<a
|
|
62
|
-
href="https://t.me/BotFather"
|
|
63
|
-
target="_blank"
|
|
64
|
-
class="hover:underline" style="color: var(--accent)"
|
|
65
|
-
>@BotFather</a
|
|
66
|
-
>${" "}·${" "}<a
|
|
67
|
-
href="https://docs.openclaw.ai/channels/telegram"
|
|
68
|
-
target="_blank"
|
|
69
|
-
class="hover:underline" style="color: var(--accent)"
|
|
70
|
-
>full guide</a
|
|
71
|
-
>`,
|
|
72
|
-
placeholder: "123456789:AAH...",
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
key: "DISCORD_BOT_TOKEN",
|
|
76
|
-
label: "Discord Bot Token",
|
|
77
|
-
hint: html`From${" "}<a
|
|
78
|
-
href="https://discord.com/developers/applications"
|
|
79
|
-
target="_blank"
|
|
80
|
-
class="hover:underline" style="color: var(--accent)"
|
|
81
|
-
>Developer Portal</a
|
|
82
|
-
>${" "}·${" "}<a
|
|
83
|
-
href="https://docs.openclaw.ai/channels/discord"
|
|
84
|
-
target="_blank"
|
|
85
|
-
class="hover:underline" style="color: var(--accent)"
|
|
86
|
-
>full guide</a
|
|
87
|
-
>`,
|
|
88
|
-
placeholder: "MTQ3...",
|
|
89
|
-
},
|
|
90
|
-
],
|
|
91
|
-
validate: (vals) => !!(vals.TELEGRAM_BOT_TOKEN || vals.DISCORD_BOT_TOKEN),
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
id: "tools",
|
|
95
|
-
title: "Tools (optional)",
|
|
96
|
-
description: "Enable extra capabilities for your agent",
|
|
97
|
-
fields: [
|
|
98
|
-
{
|
|
99
|
-
key: "BRAVE_API_KEY",
|
|
100
|
-
label: "Brave Search API Key",
|
|
101
|
-
hint: html`From${" "}<a
|
|
102
|
-
href="https://brave.com/search/api/"
|
|
103
|
-
target="_blank"
|
|
104
|
-
class="hover:underline" style="color: var(--accent)"
|
|
105
|
-
>brave.com/search/api</a
|
|
106
|
-
>${" "}-${" "}free tier available`,
|
|
107
|
-
placeholder: "BSA...",
|
|
108
|
-
},
|
|
109
|
-
],
|
|
110
|
-
validate: () => true,
|
|
111
|
-
},
|
|
112
|
-
];
|
|
21
|
+
const kOnboardingStorageKey = "openclaw_setup";
|
|
22
|
+
const kOnboardingStepKey = "_step";
|
|
113
23
|
|
|
114
24
|
export const Welcome = ({ onComplete }) => {
|
|
115
|
-
const [
|
|
25
|
+
const [initialSetupState] = useState(() => {
|
|
116
26
|
try {
|
|
117
|
-
return JSON.parse(localStorage.getItem(
|
|
27
|
+
return JSON.parse(localStorage.getItem(kOnboardingStorageKey) || "{}");
|
|
118
28
|
} catch {
|
|
119
29
|
return {};
|
|
120
30
|
}
|
|
121
31
|
});
|
|
32
|
+
const [vals, setVals] = useState(() => ({ ...initialSetupState }));
|
|
122
33
|
const [models, setModels] = useState([]);
|
|
123
34
|
const [modelsLoading, setModelsLoading] = useState(true);
|
|
124
35
|
const [modelsError, setModelsError] = useState(null);
|
|
@@ -133,10 +44,6 @@ export const Welcome = ({ onComplete }) => {
|
|
|
133
44
|
const [error, setError] = useState(null);
|
|
134
45
|
const codexPopupPollRef = useRef(null);
|
|
135
46
|
|
|
136
|
-
useEffect(() => {
|
|
137
|
-
localStorage.setItem("openclaw_setup", JSON.stringify(vals));
|
|
138
|
-
}, [vals]);
|
|
139
|
-
|
|
140
47
|
useEffect(() => {
|
|
141
48
|
fetchModels()
|
|
142
49
|
.then((result) => {
|
|
@@ -225,7 +132,31 @@ export const Welcome = ({ onComplete }) => {
|
|
|
225
132
|
? !!(codexStatus.connected || vals.OPENAI_API_KEY)
|
|
226
133
|
: false;
|
|
227
134
|
|
|
228
|
-
const allValid =
|
|
135
|
+
const allValid = kWelcomeGroups.every((g) => g.validate(vals, { hasAi }));
|
|
136
|
+
const kFinalSetupStep = kWelcomeGroups.length;
|
|
137
|
+
const [step, setStep] = useState(() => {
|
|
138
|
+
const parsedStep = Number.parseInt(
|
|
139
|
+
String(initialSetupState?.[kOnboardingStepKey] || ""),
|
|
140
|
+
10,
|
|
141
|
+
);
|
|
142
|
+
if (!Number.isFinite(parsedStep)) return 0;
|
|
143
|
+
return Math.max(0, Math.min(kFinalSetupStep, parsedStep));
|
|
144
|
+
});
|
|
145
|
+
const isSetupStep = step === kFinalSetupStep;
|
|
146
|
+
const activeGroup = !isSetupStep ? kWelcomeGroups[step] : null;
|
|
147
|
+
const currentGroupValid = activeGroup
|
|
148
|
+
? activeGroup.validate(vals, { hasAi })
|
|
149
|
+
: false;
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
localStorage.setItem(
|
|
153
|
+
kOnboardingStorageKey,
|
|
154
|
+
JSON.stringify({
|
|
155
|
+
...vals,
|
|
156
|
+
[kOnboardingStepKey]: step,
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
}, [vals, step]);
|
|
229
160
|
|
|
230
161
|
const startCodexAuth = () => {
|
|
231
162
|
if (codexStatus.connected) return;
|
|
@@ -287,6 +218,7 @@ export const Welcome = ({ onComplete }) => {
|
|
|
287
218
|
|
|
288
219
|
const handleSubmit = async () => {
|
|
289
220
|
if (!allValid || loading) return;
|
|
221
|
+
setStep(kFinalSetupStep);
|
|
290
222
|
setLoading(true);
|
|
291
223
|
setError(null);
|
|
292
224
|
|
|
@@ -297,7 +229,7 @@ export const Welcome = ({ onComplete }) => {
|
|
|
297
229
|
.map(([key, value]) => ({ key, value }));
|
|
298
230
|
const result = await runOnboard(vars, vals.MODEL_KEY);
|
|
299
231
|
if (!result.ok) throw new Error(result.error || "Onboarding failed");
|
|
300
|
-
localStorage.removeItem(
|
|
232
|
+
localStorage.removeItem(kOnboardingStorageKey);
|
|
301
233
|
onComplete();
|
|
302
234
|
} catch (err) {
|
|
303
235
|
console.error("Onboard error:", err);
|
|
@@ -306,237 +238,76 @@ export const Welcome = ({ onComplete }) => {
|
|
|
306
238
|
}
|
|
307
239
|
};
|
|
308
240
|
|
|
309
|
-
|
|
310
|
-
return
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
style="background: var(--bg)"
|
|
314
|
-
>
|
|
315
|
-
<div class="flex flex-col items-center gap-4">
|
|
316
|
-
<svg
|
|
317
|
-
class="animate-spin h-8 w-8 text-white"
|
|
318
|
-
viewBox="0 0 24 24"
|
|
319
|
-
fill="none"
|
|
320
|
-
>
|
|
321
|
-
<circle
|
|
322
|
-
class="opacity-25"
|
|
323
|
-
cx="12"
|
|
324
|
-
cy="12"
|
|
325
|
-
r="10"
|
|
326
|
-
stroke="currentColor"
|
|
327
|
-
stroke-width="4"
|
|
328
|
-
/>
|
|
329
|
-
<path
|
|
330
|
-
class="opacity-75"
|
|
331
|
-
fill="currentColor"
|
|
332
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
333
|
-
/>
|
|
334
|
-
</svg>
|
|
335
|
-
<h2 class="text-lg font-semibold text-white">
|
|
336
|
-
Initializing <span style="color: var(--accent)">alpha</span>claw
|
|
337
|
-
</h2>
|
|
338
|
-
<p class="text-sm text-gray-500">This could take 10–15 seconds</p>
|
|
339
|
-
</div>
|
|
340
|
-
</div>
|
|
341
|
-
`;
|
|
342
|
-
}
|
|
241
|
+
const goBack = () => {
|
|
242
|
+
if (isSetupStep) return;
|
|
243
|
+
setStep((prev) => Math.max(0, prev - 1));
|
|
244
|
+
};
|
|
343
245
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
<div>
|
|
349
|
-
<h1 class="text-2xl font-semibold">Welcome to <span style="color: var(--accent)">alpha</span>claw</h1>
|
|
350
|
-
<p style="color: var(--text-muted)" class="text-sm">Let's get your agent running</p>
|
|
351
|
-
</div>
|
|
352
|
-
</div>
|
|
246
|
+
const goNext = () => {
|
|
247
|
+
if (!activeGroup || !currentGroupValid) return;
|
|
248
|
+
setStep((prev) => Math.min(kWelcomeGroups.length - 1, prev + 1));
|
|
249
|
+
};
|
|
353
250
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
<div>
|
|
359
|
-
<h2 class="text-sm font-medium text-gray-200">
|
|
360
|
-
${group.title}
|
|
361
|
-
</h2>
|
|
362
|
-
<p class="text-xs text-gray-500">${group.description}</p>
|
|
363
|
-
</div>
|
|
364
|
-
${group.validate(vals, { hasAi })
|
|
365
|
-
? html`<span
|
|
366
|
-
class="text-xs font-medium px-2 py-0.5 rounded-full bg-green-900/50 text-green-400"
|
|
367
|
-
>✓</span
|
|
368
|
-
>`
|
|
369
|
-
: group.id !== "tools"
|
|
370
|
-
? html`<span
|
|
371
|
-
class="text-xs font-medium px-2 py-0.5 rounded-full bg-yellow-900/50 text-yellow-400"
|
|
372
|
-
>Required</span
|
|
373
|
-
>`
|
|
374
|
-
: null}
|
|
375
|
-
</div>
|
|
251
|
+
const activeStepLabel = isSetupStep
|
|
252
|
+
? "Initializing"
|
|
253
|
+
: activeGroup?.title || "Setup";
|
|
254
|
+
const stepNumber = isSetupStep ? kWelcomeGroups.length + 1 : step + 1;
|
|
376
255
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
(model) => html`
|
|
389
|
-
<option value=${model.key}>
|
|
390
|
-
${model.label || model.key}
|
|
391
|
-
</option>
|
|
392
|
-
`,
|
|
393
|
-
)}
|
|
394
|
-
</select>
|
|
395
|
-
<p class="text-xs text-gray-600">
|
|
396
|
-
${modelsLoading
|
|
397
|
-
? "Loading model catalog..."
|
|
398
|
-
: modelsError
|
|
399
|
-
? modelsError
|
|
400
|
-
: ""}
|
|
401
|
-
</p>
|
|
402
|
-
${canToggleFullCatalog &&
|
|
403
|
-
html`
|
|
404
|
-
<button
|
|
405
|
-
type="button"
|
|
406
|
-
onclick=${() => setShowAllModels((prev) => !prev)}
|
|
407
|
-
class="text-xs text-gray-500 hover:text-gray-300"
|
|
408
|
-
>
|
|
409
|
-
${showAllModels
|
|
410
|
-
? "Show recommended models"
|
|
411
|
-
: "Show full model catalog"}
|
|
412
|
-
</button>
|
|
413
|
-
`}
|
|
414
|
-
</div>
|
|
415
|
-
`}
|
|
416
|
-
${group.id === "ai" &&
|
|
417
|
-
selectedProvider === "openai-codex" &&
|
|
418
|
-
html`
|
|
419
|
-
<div
|
|
420
|
-
class="bg-black/20 border border-border rounded-lg p-3 space-y-2"
|
|
421
|
-
>
|
|
422
|
-
<div class="flex items-center justify-between">
|
|
423
|
-
<span class="text-xs text-gray-400">Codex OAuth</span>
|
|
424
|
-
${codexLoading
|
|
425
|
-
? html`<span class="text-xs text-gray-500"
|
|
426
|
-
>Checking...</span
|
|
427
|
-
>`
|
|
428
|
-
: codexStatus.connected
|
|
429
|
-
? html`<span class="text-xs text-green-400"
|
|
430
|
-
>Connected</span
|
|
431
|
-
>`
|
|
432
|
-
: html`<span class="text-xs text-yellow-400"
|
|
433
|
-
>Not connected</span
|
|
434
|
-
>`}
|
|
435
|
-
</div>
|
|
436
|
-
<div class="flex gap-2">
|
|
437
|
-
<button
|
|
438
|
-
type="button"
|
|
439
|
-
onclick=${startCodexAuth}
|
|
440
|
-
class="text-xs font-medium px-3 py-1.5 rounded-lg ${codexStatus.connected
|
|
441
|
-
? "border border-border text-gray-300 hover:border-gray-500"
|
|
442
|
-
: "bg-white text-black hover:opacity-85"}"
|
|
443
|
-
>
|
|
444
|
-
${codexStatus.connected
|
|
445
|
-
? "Reconnect Codex"
|
|
446
|
-
: "Connect Codex OAuth"}
|
|
447
|
-
</button>
|
|
448
|
-
${codexStatus.connected &&
|
|
449
|
-
html`
|
|
450
|
-
<button
|
|
451
|
-
type="button"
|
|
452
|
-
onclick=${handleCodexDisconnect}
|
|
453
|
-
class="text-xs font-medium px-3 py-1.5 rounded-lg border border-border text-gray-300 hover:border-gray-500"
|
|
454
|
-
>
|
|
455
|
-
Disconnect
|
|
456
|
-
</button>
|
|
457
|
-
`}
|
|
458
|
-
</div>
|
|
459
|
-
${!codexStatus.connected &&
|
|
460
|
-
codexAuthStarted &&
|
|
461
|
-
html`
|
|
462
|
-
<div class="space-y-1 pt-1">
|
|
463
|
-
<p class="text-xs text-gray-500">
|
|
464
|
-
${codexAuthWaiting
|
|
465
|
-
? "Complete login in the popup, then paste the full redirect URL from the address bar (starts with "
|
|
466
|
-
: "Paste the full redirect URL from the address bar (starts with "}
|
|
467
|
-
<code class="text-xs bg-black/30 px-1 rounded"
|
|
468
|
-
>http://localhost:1455/auth/callback</code
|
|
469
|
-
>)
|
|
470
|
-
${codexAuthWaiting
|
|
471
|
-
? " to finish setup."
|
|
472
|
-
: " to finish setup."}
|
|
473
|
-
</p>
|
|
474
|
-
<input
|
|
475
|
-
type="text"
|
|
476
|
-
value=${codexManualInput}
|
|
477
|
-
onInput=${(e) => setCodexManualInput(e.target.value)}
|
|
478
|
-
placeholder="http://localhost:1455/auth/callback?code=...&state=..."
|
|
479
|
-
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 outline-none focus:border-gray-500"
|
|
480
|
-
/>
|
|
481
|
-
<button
|
|
482
|
-
type="button"
|
|
483
|
-
onclick=${completeCodexAuth}
|
|
484
|
-
disabled=${!codexManualInput.trim() || codexExchanging}
|
|
485
|
-
class="text-xs font-medium px-3 py-1.5 rounded-lg ${!codexManualInput.trim() ||
|
|
486
|
-
codexExchanging
|
|
487
|
-
? "bg-gray-700 text-gray-400 cursor-not-allowed"
|
|
488
|
-
: "bg-white text-black hover:opacity-85"}"
|
|
489
|
-
>
|
|
490
|
-
${codexExchanging
|
|
491
|
-
? "Completing..."
|
|
492
|
-
: "Complete Codex OAuth"}
|
|
493
|
-
</button>
|
|
494
|
-
</div>
|
|
495
|
-
`}
|
|
496
|
-
</div>
|
|
497
|
-
`}
|
|
498
|
-
${(group.id === "ai"
|
|
499
|
-
? group.fields.filter((field) =>
|
|
500
|
-
visibleAiFieldKeys.has(field.key),
|
|
501
|
-
)
|
|
502
|
-
: group.fields
|
|
503
|
-
).map(
|
|
504
|
-
(field) => html`
|
|
505
|
-
<div class="space-y-1">
|
|
506
|
-
<label class="text-xs font-medium text-gray-400"
|
|
507
|
-
>${field.label}</label
|
|
508
|
-
>
|
|
509
|
-
<input
|
|
510
|
-
type=${field.isText ? "text" : "password"}
|
|
511
|
-
placeholder=${field.placeholder || ""}
|
|
512
|
-
value=${vals[field.key] || ""}
|
|
513
|
-
onInput=${(e) => set(field.key, e.target.value)}
|
|
514
|
-
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
|
|
515
|
-
/>
|
|
516
|
-
<p class="text-xs text-gray-600">${field.hint}</p>
|
|
517
|
-
</div>
|
|
518
|
-
`,
|
|
519
|
-
)}
|
|
520
|
-
</div>
|
|
521
|
-
`,
|
|
522
|
-
)}
|
|
523
|
-
${error
|
|
524
|
-
? html`<div
|
|
525
|
-
class="bg-red-900/30 border border-red-800 rounded-xl p-3 text-red-300 text-sm"
|
|
526
|
-
>
|
|
527
|
-
${error}
|
|
528
|
-
</div>`
|
|
529
|
-
: null}
|
|
256
|
+
return html`
|
|
257
|
+
<div class="max-w-lg w-full space-y-5">
|
|
258
|
+
<${WelcomeHeader}
|
|
259
|
+
groups=${kWelcomeGroups}
|
|
260
|
+
step=${step}
|
|
261
|
+
isSetupStep=${isSetupStep}
|
|
262
|
+
stepNumber=${stepNumber}
|
|
263
|
+
activeStepLabel=${activeStepLabel}
|
|
264
|
+
vals=${vals}
|
|
265
|
+
hasAi=${hasAi}
|
|
266
|
+
/>
|
|
530
267
|
|
|
531
|
-
<
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
268
|
+
<div class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
269
|
+
${isSetupStep
|
|
270
|
+
? html`<${WelcomeSetupStep}
|
|
271
|
+
error=${error}
|
|
272
|
+
loading=${loading}
|
|
273
|
+
onRetry=${handleSubmit}
|
|
274
|
+
/>`
|
|
275
|
+
: html`
|
|
276
|
+
<${WelcomeFormStep}
|
|
277
|
+
activeGroup=${activeGroup}
|
|
278
|
+
vals=${vals}
|
|
279
|
+
hasAi=${hasAi}
|
|
280
|
+
setValue=${set}
|
|
281
|
+
modelOptions=${modelOptions}
|
|
282
|
+
modelsLoading=${modelsLoading}
|
|
283
|
+
modelsError=${modelsError}
|
|
284
|
+
canToggleFullCatalog=${canToggleFullCatalog}
|
|
285
|
+
showAllModels=${showAllModels}
|
|
286
|
+
setShowAllModels=${setShowAllModels}
|
|
287
|
+
selectedProvider=${selectedProvider}
|
|
288
|
+
codexLoading=${codexLoading}
|
|
289
|
+
codexStatus=${codexStatus}
|
|
290
|
+
startCodexAuth=${startCodexAuth}
|
|
291
|
+
handleCodexDisconnect=${handleCodexDisconnect}
|
|
292
|
+
codexAuthStarted=${codexAuthStarted}
|
|
293
|
+
codexAuthWaiting=${codexAuthWaiting}
|
|
294
|
+
codexManualInput=${codexManualInput}
|
|
295
|
+
setCodexManualInput=${setCodexManualInput}
|
|
296
|
+
completeCodexAuth=${completeCodexAuth}
|
|
297
|
+
codexExchanging=${codexExchanging}
|
|
298
|
+
visibleAiFieldKeys=${visibleAiFieldKeys}
|
|
299
|
+
error=${error}
|
|
300
|
+
step=${step}
|
|
301
|
+
totalGroups=${kWelcomeGroups.length}
|
|
302
|
+
currentGroupValid=${currentGroupValid}
|
|
303
|
+
goBack=${goBack}
|
|
304
|
+
goNext=${goNext}
|
|
305
|
+
loading=${loading}
|
|
306
|
+
allValid=${allValid}
|
|
307
|
+
handleSubmit=${handleSubmit}
|
|
308
|
+
/>
|
|
309
|
+
`}
|
|
310
|
+
</div>
|
|
540
311
|
</div>
|
|
541
312
|
`;
|
|
542
313
|
};
|
package/lib/public/login.html
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
<span style="color: var(--accent)">alpha</span>claw
|
|
41
41
|
</h1>
|
|
42
42
|
<p style="color: var(--text-muted)" class="text-xs mb-4">
|
|
43
|
-
OpenClaw made
|
|
43
|
+
OpenClaw made easy
|
|
44
44
|
</p>
|
|
45
45
|
</div>
|
|
46
46
|
<form
|
|
@@ -72,13 +72,21 @@
|
|
|
72
72
|
const submitButtonEl = document.getElementById("submit-btn");
|
|
73
73
|
|
|
74
74
|
const kEnabledClasses = ["bg-white", "text-black", "hover:opacity-85"];
|
|
75
|
-
const kDisabledClasses = [
|
|
75
|
+
const kDisabledClasses = [
|
|
76
|
+
"bg-gray-800",
|
|
77
|
+
"text-gray-500",
|
|
78
|
+
"cursor-not-allowed",
|
|
79
|
+
];
|
|
76
80
|
|
|
77
81
|
const syncButtonState = () => {
|
|
78
82
|
const hasValue = passwordEl.value.length > 0;
|
|
79
83
|
submitButtonEl.disabled = !hasValue;
|
|
80
|
-
kEnabledClasses.forEach((c) =>
|
|
81
|
-
|
|
84
|
+
kEnabledClasses.forEach((c) =>
|
|
85
|
+
submitButtonEl.classList.toggle(c, hasValue),
|
|
86
|
+
);
|
|
87
|
+
kDisabledClasses.forEach((c) =>
|
|
88
|
+
submitButtonEl.classList.toggle(c, !hasValue),
|
|
89
|
+
);
|
|
82
90
|
};
|
|
83
91
|
|
|
84
92
|
passwordEl.addEventListener("input", syncButtonState);
|