@chrysb/alphaclaw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/alphaclaw.js +338 -0
- package/lib/public/icons/chevron-down.svg +9 -0
- package/lib/public/js/app.js +325 -0
- package/lib/public/js/components/badge.js +16 -0
- package/lib/public/js/components/channels.js +36 -0
- package/lib/public/js/components/credentials-modal.js +336 -0
- package/lib/public/js/components/device-pairings.js +72 -0
- package/lib/public/js/components/envars.js +354 -0
- package/lib/public/js/components/gateway.js +163 -0
- package/lib/public/js/components/google.js +223 -0
- package/lib/public/js/components/icons.js +23 -0
- package/lib/public/js/components/models.js +461 -0
- package/lib/public/js/components/pairings.js +74 -0
- package/lib/public/js/components/scope-picker.js +106 -0
- package/lib/public/js/components/toast.js +31 -0
- package/lib/public/js/components/welcome.js +541 -0
- package/lib/public/js/hooks/usePolling.js +29 -0
- package/lib/public/js/lib/api.js +196 -0
- package/lib/public/js/lib/model-config.js +88 -0
- package/lib/public/login.html +90 -0
- package/lib/public/setup.html +33 -0
- package/lib/scripts/systemctl +56 -0
- package/lib/server/auth-profiles.js +101 -0
- package/lib/server/commands.js +84 -0
- package/lib/server/constants.js +282 -0
- package/lib/server/env.js +78 -0
- package/lib/server/gateway.js +262 -0
- package/lib/server/helpers.js +192 -0
- package/lib/server/login-throttle.js +86 -0
- package/lib/server/onboarding/cron.js +51 -0
- package/lib/server/onboarding/github.js +49 -0
- package/lib/server/onboarding/index.js +127 -0
- package/lib/server/onboarding/openclaw.js +171 -0
- package/lib/server/onboarding/validation.js +107 -0
- package/lib/server/onboarding/workspace.js +52 -0
- package/lib/server/openclaw-version.js +179 -0
- package/lib/server/routes/auth.js +80 -0
- package/lib/server/routes/codex.js +204 -0
- package/lib/server/routes/google.js +390 -0
- package/lib/server/routes/models.js +68 -0
- package/lib/server/routes/onboarding.js +116 -0
- package/lib/server/routes/pages.js +21 -0
- package/lib/server/routes/pairings.js +134 -0
- package/lib/server/routes/proxy.js +29 -0
- package/lib/server/routes/system.js +213 -0
- package/lib/server.js +161 -0
- package/lib/setup/core-prompts/AGENTS.md +22 -0
- package/lib/setup/core-prompts/TOOLS.md +18 -0
- package/lib/setup/env.template +19 -0
- package/lib/setup/gitignore +12 -0
- package/lib/setup/hourly-git-sync.sh +86 -0
- package/lib/setup/skills/control-ui/SKILL.md +70 -0
- package/package.json +34 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useState, useEffect, useRef } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import {
|
|
5
|
+
runOnboard,
|
|
6
|
+
fetchModels,
|
|
7
|
+
fetchCodexStatus,
|
|
8
|
+
disconnectCodex,
|
|
9
|
+
exchangeCodexOAuth,
|
|
10
|
+
} from "../lib/api.js";
|
|
11
|
+
import {
|
|
12
|
+
getModelProvider,
|
|
13
|
+
getFeaturedModels,
|
|
14
|
+
getVisibleAiFieldKeys,
|
|
15
|
+
kAllAiAuthFields,
|
|
16
|
+
} from "../lib/model-config.js";
|
|
17
|
+
const html = htm.bind(h);
|
|
18
|
+
|
|
19
|
+
const kGroups = [
|
|
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="text-blue-400 hover:underline"
|
|
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="text-blue-400 hover:underline"
|
|
65
|
+
>@BotFather</a
|
|
66
|
+
>${" "}·${" "}<a
|
|
67
|
+
href="https://docs.openclaw.ai/channels/telegram"
|
|
68
|
+
target="_blank"
|
|
69
|
+
class="text-blue-400 hover:underline"
|
|
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="text-blue-400 hover:underline"
|
|
81
|
+
>Developer Portal</a
|
|
82
|
+
>${" "}·${" "}<a
|
|
83
|
+
href="https://docs.openclaw.ai/channels/discord"
|
|
84
|
+
target="_blank"
|
|
85
|
+
class="text-blue-400 hover:underline"
|
|
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="text-blue-400 hover:underline"
|
|
105
|
+
>brave.com/search/api</a
|
|
106
|
+
>${" "}-${" "}free tier available`,
|
|
107
|
+
placeholder: "BSA...",
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
validate: () => true,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
export const Welcome = ({ onComplete }) => {
|
|
115
|
+
const [vals, setVals] = useState(() => {
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(localStorage.getItem("openclaw_setup") || "{}");
|
|
118
|
+
} catch {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
const [models, setModels] = useState([]);
|
|
123
|
+
const [modelsLoading, setModelsLoading] = useState(true);
|
|
124
|
+
const [modelsError, setModelsError] = useState(null);
|
|
125
|
+
const [showAllModels, setShowAllModels] = useState(false);
|
|
126
|
+
const [codexStatus, setCodexStatus] = useState({ connected: false });
|
|
127
|
+
const [codexLoading, setCodexLoading] = useState(true);
|
|
128
|
+
const [codexManualInput, setCodexManualInput] = useState("");
|
|
129
|
+
const [codexExchanging, setCodexExchanging] = useState(false);
|
|
130
|
+
const [codexAuthStarted, setCodexAuthStarted] = useState(false);
|
|
131
|
+
const [codexAuthWaiting, setCodexAuthWaiting] = useState(false);
|
|
132
|
+
const [loading, setLoading] = useState(false);
|
|
133
|
+
const [error, setError] = useState(null);
|
|
134
|
+
const codexPopupPollRef = useRef(null);
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
localStorage.setItem("openclaw_setup", JSON.stringify(vals));
|
|
138
|
+
}, [vals]);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
fetchModels()
|
|
142
|
+
.then((result) => {
|
|
143
|
+
const list = Array.isArray(result.models) ? result.models : [];
|
|
144
|
+
const featured = getFeaturedModels(list);
|
|
145
|
+
setModels(list);
|
|
146
|
+
if (!vals.MODEL_KEY && list.length > 0) {
|
|
147
|
+
const defaultModel = featured[0] || list[0];
|
|
148
|
+
setVals((prev) => ({ ...prev, MODEL_KEY: defaultModel.key }));
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
.catch(() => setModelsError("Failed to load models"))
|
|
152
|
+
.finally(() => setModelsLoading(false));
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
const refreshCodexStatus = async () => {
|
|
156
|
+
try {
|
|
157
|
+
const status = await fetchCodexStatus();
|
|
158
|
+
setCodexStatus(status);
|
|
159
|
+
if (status?.connected) {
|
|
160
|
+
setCodexAuthStarted(false);
|
|
161
|
+
setCodexAuthWaiting(false);
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
setCodexStatus({ connected: false });
|
|
165
|
+
} finally {
|
|
166
|
+
setCodexLoading(false);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
refreshCodexStatus();
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
const onMessage = async (e) => {
|
|
176
|
+
if (e.data?.codex === "success") {
|
|
177
|
+
await refreshCodexStatus();
|
|
178
|
+
}
|
|
179
|
+
if (e.data?.codex === "error") {
|
|
180
|
+
setError(`Codex auth failed: ${e.data.message || "unknown error"}`);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
window.addEventListener("message", onMessage);
|
|
184
|
+
return () => window.removeEventListener("message", onMessage);
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
187
|
+
useEffect(
|
|
188
|
+
() => () => {
|
|
189
|
+
if (codexPopupPollRef.current) {
|
|
190
|
+
clearInterval(codexPopupPollRef.current);
|
|
191
|
+
codexPopupPollRef.current = null;
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
[],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const set = (key, value) => setVals((prev) => ({ ...prev, [key]: value }));
|
|
198
|
+
|
|
199
|
+
const selectedProvider = getModelProvider(vals.MODEL_KEY);
|
|
200
|
+
const featuredModels = getFeaturedModels(models);
|
|
201
|
+
const baseModelOptions = showAllModels
|
|
202
|
+
? models
|
|
203
|
+
: featuredModels.length > 0
|
|
204
|
+
? featuredModels
|
|
205
|
+
: models;
|
|
206
|
+
const selectedModelOption = models.find(
|
|
207
|
+
(model) => model.key === vals.MODEL_KEY,
|
|
208
|
+
);
|
|
209
|
+
const modelOptions =
|
|
210
|
+
selectedModelOption &&
|
|
211
|
+
!baseModelOptions.some((model) => model.key === selectedModelOption.key)
|
|
212
|
+
? [...baseModelOptions, selectedModelOption]
|
|
213
|
+
: baseModelOptions;
|
|
214
|
+
const canToggleFullCatalog =
|
|
215
|
+
featuredModels.length > 0 && models.length > featuredModels.length;
|
|
216
|
+
const visibleAiFieldKeys = getVisibleAiFieldKeys(selectedProvider);
|
|
217
|
+
const hasAi =
|
|
218
|
+
selectedProvider === "anthropic"
|
|
219
|
+
? !!(vals.ANTHROPIC_API_KEY || vals.ANTHROPIC_TOKEN)
|
|
220
|
+
: selectedProvider === "openai"
|
|
221
|
+
? !!vals.OPENAI_API_KEY
|
|
222
|
+
: selectedProvider === "google"
|
|
223
|
+
? !!vals.GEMINI_API_KEY
|
|
224
|
+
: selectedProvider === "openai-codex"
|
|
225
|
+
? !!(codexStatus.connected || vals.OPENAI_API_KEY)
|
|
226
|
+
: false;
|
|
227
|
+
|
|
228
|
+
const allValid = kGroups.every((g) => g.validate(vals, { hasAi }));
|
|
229
|
+
|
|
230
|
+
const startCodexAuth = () => {
|
|
231
|
+
if (codexStatus.connected) return;
|
|
232
|
+
setCodexAuthStarted(true);
|
|
233
|
+
setCodexAuthWaiting(true);
|
|
234
|
+
const authUrl = "/auth/codex/start";
|
|
235
|
+
const popup = window.open(
|
|
236
|
+
authUrl,
|
|
237
|
+
"codex-auth",
|
|
238
|
+
"popup=yes,width=640,height=780",
|
|
239
|
+
);
|
|
240
|
+
if (!popup || popup.closed) {
|
|
241
|
+
setCodexAuthWaiting(false);
|
|
242
|
+
window.location.href = authUrl;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (codexPopupPollRef.current) {
|
|
246
|
+
clearInterval(codexPopupPollRef.current);
|
|
247
|
+
}
|
|
248
|
+
codexPopupPollRef.current = setInterval(() => {
|
|
249
|
+
if (popup.closed) {
|
|
250
|
+
clearInterval(codexPopupPollRef.current);
|
|
251
|
+
codexPopupPollRef.current = null;
|
|
252
|
+
setCodexAuthWaiting(false);
|
|
253
|
+
}
|
|
254
|
+
}, 500);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const completeCodexAuth = async () => {
|
|
258
|
+
if (!codexManualInput.trim() || codexExchanging) return;
|
|
259
|
+
setCodexExchanging(true);
|
|
260
|
+
setError(null);
|
|
261
|
+
try {
|
|
262
|
+
const result = await exchangeCodexOAuth(codexManualInput.trim());
|
|
263
|
+
if (!result.ok)
|
|
264
|
+
throw new Error(result.error || "Codex OAuth exchange failed");
|
|
265
|
+
setCodexManualInput("");
|
|
266
|
+
setCodexAuthStarted(false);
|
|
267
|
+
setCodexAuthWaiting(false);
|
|
268
|
+
await refreshCodexStatus();
|
|
269
|
+
} catch (err) {
|
|
270
|
+
setError(err.message || "Codex OAuth exchange failed");
|
|
271
|
+
} finally {
|
|
272
|
+
setCodexExchanging(false);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const handleCodexDisconnect = async () => {
|
|
277
|
+
const result = await disconnectCodex();
|
|
278
|
+
if (!result.ok) {
|
|
279
|
+
setError(result.error || "Failed to disconnect Codex");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
setCodexAuthStarted(false);
|
|
283
|
+
setCodexAuthWaiting(false);
|
|
284
|
+
setCodexManualInput("");
|
|
285
|
+
await refreshCodexStatus();
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const handleSubmit = async () => {
|
|
289
|
+
if (!allValid || loading) return;
|
|
290
|
+
setLoading(true);
|
|
291
|
+
setError(null);
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const vars = Object.entries(vals)
|
|
295
|
+
.filter(([key]) => key !== "MODEL_KEY")
|
|
296
|
+
.filter(([, v]) => v)
|
|
297
|
+
.map(([key, value]) => ({ key, value }));
|
|
298
|
+
const result = await runOnboard(vars, vals.MODEL_KEY);
|
|
299
|
+
if (!result.ok) throw new Error(result.error || "Onboarding failed");
|
|
300
|
+
localStorage.removeItem("openclaw_setup");
|
|
301
|
+
onComplete();
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.error("Onboard error:", err);
|
|
304
|
+
setError(err.message);
|
|
305
|
+
setLoading(false);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
if (loading) {
|
|
310
|
+
return html`
|
|
311
|
+
<div
|
|
312
|
+
class="fixed inset-0 bg-[#0a0a0a] flex items-center justify-center z-50"
|
|
313
|
+
>
|
|
314
|
+
<div class="flex flex-col items-center gap-4">
|
|
315
|
+
<svg
|
|
316
|
+
class="animate-spin h-8 w-8 text-white"
|
|
317
|
+
viewBox="0 0 24 24"
|
|
318
|
+
fill="none"
|
|
319
|
+
>
|
|
320
|
+
<circle
|
|
321
|
+
class="opacity-25"
|
|
322
|
+
cx="12"
|
|
323
|
+
cy="12"
|
|
324
|
+
r="10"
|
|
325
|
+
stroke="currentColor"
|
|
326
|
+
stroke-width="4"
|
|
327
|
+
/>
|
|
328
|
+
<path
|
|
329
|
+
class="opacity-75"
|
|
330
|
+
fill="currentColor"
|
|
331
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
332
|
+
/>
|
|
333
|
+
</svg>
|
|
334
|
+
<h2 class="text-lg font-semibold text-white">
|
|
335
|
+
Initializing OpenClaw
|
|
336
|
+
</h2>
|
|
337
|
+
<p class="text-sm text-gray-500">This could take 10–15 seconds</p>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return html`
|
|
344
|
+
<div class="max-w-lg w-full space-y-4">
|
|
345
|
+
<div class="flex items-center gap-3">
|
|
346
|
+
<div class="text-4xl">🦞</div>
|
|
347
|
+
<div>
|
|
348
|
+
<h1 class="text-2xl font-semibold">Welcome to OpenClaw</h1>
|
|
349
|
+
<p class="text-gray-500 text-sm">Let's get your agent running</p>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
${kGroups.map(
|
|
354
|
+
(group) => html`
|
|
355
|
+
<div class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
356
|
+
<div class="flex items-center justify-between">
|
|
357
|
+
<div>
|
|
358
|
+
<h2 class="text-sm font-medium text-gray-200">
|
|
359
|
+
${group.title}
|
|
360
|
+
</h2>
|
|
361
|
+
<p class="text-xs text-gray-500">${group.description}</p>
|
|
362
|
+
</div>
|
|
363
|
+
${group.validate(vals, { hasAi })
|
|
364
|
+
? html`<span
|
|
365
|
+
class="text-xs font-medium px-2 py-0.5 rounded-full bg-green-900/50 text-green-400"
|
|
366
|
+
>✓</span
|
|
367
|
+
>`
|
|
368
|
+
: group.id !== "tools"
|
|
369
|
+
? html`<span
|
|
370
|
+
class="text-xs font-medium px-2 py-0.5 rounded-full bg-yellow-900/50 text-yellow-400"
|
|
371
|
+
>Required</span
|
|
372
|
+
>`
|
|
373
|
+
: null}
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
${group.id === "ai" &&
|
|
377
|
+
html`
|
|
378
|
+
<div class="space-y-1">
|
|
379
|
+
<label class="text-xs font-medium text-gray-400">Model</label>
|
|
380
|
+
<select
|
|
381
|
+
value=${vals.MODEL_KEY || ""}
|
|
382
|
+
onInput=${(e) => set("MODEL_KEY", e.target.value)}
|
|
383
|
+
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"
|
|
384
|
+
>
|
|
385
|
+
<option value="">Select a model</option>
|
|
386
|
+
${modelOptions.map(
|
|
387
|
+
(model) => html`
|
|
388
|
+
<option value=${model.key}>
|
|
389
|
+
${model.label || model.key}
|
|
390
|
+
</option>
|
|
391
|
+
`,
|
|
392
|
+
)}
|
|
393
|
+
</select>
|
|
394
|
+
<p class="text-xs text-gray-600">
|
|
395
|
+
${modelsLoading
|
|
396
|
+
? "Loading model catalog..."
|
|
397
|
+
: modelsError
|
|
398
|
+
? modelsError
|
|
399
|
+
: ""}
|
|
400
|
+
</p>
|
|
401
|
+
${canToggleFullCatalog &&
|
|
402
|
+
html`
|
|
403
|
+
<button
|
|
404
|
+
type="button"
|
|
405
|
+
onclick=${() => setShowAllModels((prev) => !prev)}
|
|
406
|
+
class="text-xs text-gray-500 hover:text-gray-300"
|
|
407
|
+
>
|
|
408
|
+
${showAllModels
|
|
409
|
+
? "Show recommended models"
|
|
410
|
+
: "Show full model catalog"}
|
|
411
|
+
</button>
|
|
412
|
+
`}
|
|
413
|
+
</div>
|
|
414
|
+
`}
|
|
415
|
+
${group.id === "ai" &&
|
|
416
|
+
selectedProvider === "openai-codex" &&
|
|
417
|
+
html`
|
|
418
|
+
<div
|
|
419
|
+
class="bg-black/20 border border-border rounded-lg p-3 space-y-2"
|
|
420
|
+
>
|
|
421
|
+
<div class="flex items-center justify-between">
|
|
422
|
+
<span class="text-xs text-gray-400">Codex OAuth</span>
|
|
423
|
+
${codexLoading
|
|
424
|
+
? html`<span class="text-xs text-gray-500"
|
|
425
|
+
>Checking...</span
|
|
426
|
+
>`
|
|
427
|
+
: codexStatus.connected
|
|
428
|
+
? html`<span class="text-xs text-green-400"
|
|
429
|
+
>Connected</span
|
|
430
|
+
>`
|
|
431
|
+
: html`<span class="text-xs text-yellow-400"
|
|
432
|
+
>Not connected</span
|
|
433
|
+
>`}
|
|
434
|
+
</div>
|
|
435
|
+
<div class="flex gap-2">
|
|
436
|
+
<button
|
|
437
|
+
type="button"
|
|
438
|
+
onclick=${startCodexAuth}
|
|
439
|
+
class="text-xs font-medium px-3 py-1.5 rounded-lg ${codexStatus.connected
|
|
440
|
+
? "border border-border text-gray-300 hover:border-gray-500"
|
|
441
|
+
: "bg-white text-black hover:opacity-85"}"
|
|
442
|
+
>
|
|
443
|
+
${codexStatus.connected
|
|
444
|
+
? "Reconnect Codex"
|
|
445
|
+
: "Connect Codex OAuth"}
|
|
446
|
+
</button>
|
|
447
|
+
${codexStatus.connected &&
|
|
448
|
+
html`
|
|
449
|
+
<button
|
|
450
|
+
type="button"
|
|
451
|
+
onclick=${handleCodexDisconnect}
|
|
452
|
+
class="text-xs font-medium px-3 py-1.5 rounded-lg border border-border text-gray-300 hover:border-gray-500"
|
|
453
|
+
>
|
|
454
|
+
Disconnect
|
|
455
|
+
</button>
|
|
456
|
+
`}
|
|
457
|
+
</div>
|
|
458
|
+
${!codexStatus.connected &&
|
|
459
|
+
codexAuthStarted &&
|
|
460
|
+
html`
|
|
461
|
+
<div class="space-y-1 pt-1">
|
|
462
|
+
<p class="text-xs text-gray-500">
|
|
463
|
+
${codexAuthWaiting
|
|
464
|
+
? "Complete login in the popup, then paste the full redirect URL from the address bar (starts with "
|
|
465
|
+
: "Paste the full redirect URL from the address bar (starts with "}
|
|
466
|
+
<code class="text-xs bg-black/30 px-1 rounded"
|
|
467
|
+
>http://localhost:1455/auth/callback</code
|
|
468
|
+
>)
|
|
469
|
+
${codexAuthWaiting
|
|
470
|
+
? " to finish setup."
|
|
471
|
+
: " to finish setup."}
|
|
472
|
+
</p>
|
|
473
|
+
<input
|
|
474
|
+
type="text"
|
|
475
|
+
value=${codexManualInput}
|
|
476
|
+
onInput=${(e) => setCodexManualInput(e.target.value)}
|
|
477
|
+
placeholder="http://localhost:1455/auth/callback?code=...&state=..."
|
|
478
|
+
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"
|
|
479
|
+
/>
|
|
480
|
+
<button
|
|
481
|
+
type="button"
|
|
482
|
+
onclick=${completeCodexAuth}
|
|
483
|
+
disabled=${!codexManualInput.trim() || codexExchanging}
|
|
484
|
+
class="text-xs font-medium px-3 py-1.5 rounded-lg ${!codexManualInput.trim() ||
|
|
485
|
+
codexExchanging
|
|
486
|
+
? "bg-gray-700 text-gray-400 cursor-not-allowed"
|
|
487
|
+
: "bg-white text-black hover:opacity-85"}"
|
|
488
|
+
>
|
|
489
|
+
${codexExchanging
|
|
490
|
+
? "Completing..."
|
|
491
|
+
: "Complete Codex OAuth"}
|
|
492
|
+
</button>
|
|
493
|
+
</div>
|
|
494
|
+
`}
|
|
495
|
+
</div>
|
|
496
|
+
`}
|
|
497
|
+
${(group.id === "ai"
|
|
498
|
+
? group.fields.filter((field) =>
|
|
499
|
+
visibleAiFieldKeys.has(field.key),
|
|
500
|
+
)
|
|
501
|
+
: group.fields
|
|
502
|
+
).map(
|
|
503
|
+
(field) => html`
|
|
504
|
+
<div class="space-y-1">
|
|
505
|
+
<label class="text-xs font-medium text-gray-400"
|
|
506
|
+
>${field.label}</label
|
|
507
|
+
>
|
|
508
|
+
<input
|
|
509
|
+
type=${field.isText ? "text" : "password"}
|
|
510
|
+
placeholder=${field.placeholder || ""}
|
|
511
|
+
value=${vals[field.key] || ""}
|
|
512
|
+
onInput=${(e) => set(field.key, e.target.value)}
|
|
513
|
+
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"
|
|
514
|
+
/>
|
|
515
|
+
<p class="text-xs text-gray-600">${field.hint}</p>
|
|
516
|
+
</div>
|
|
517
|
+
`,
|
|
518
|
+
)}
|
|
519
|
+
</div>
|
|
520
|
+
`,
|
|
521
|
+
)}
|
|
522
|
+
${error
|
|
523
|
+
? html`<div
|
|
524
|
+
class="bg-red-900/30 border border-red-800 rounded-xl p-3 text-red-300 text-sm"
|
|
525
|
+
>
|
|
526
|
+
${error}
|
|
527
|
+
</div>`
|
|
528
|
+
: null}
|
|
529
|
+
|
|
530
|
+
<button
|
|
531
|
+
onclick=${handleSubmit}
|
|
532
|
+
disabled=${!allValid}
|
|
533
|
+
class="w-full text-sm font-medium px-4 py-3 rounded-xl transition-all ${allValid
|
|
534
|
+
? "bg-white text-black hover:opacity-85"
|
|
535
|
+
: "bg-gray-800 text-gray-500 cursor-not-allowed"}"
|
|
536
|
+
>
|
|
537
|
+
Complete Setup
|
|
538
|
+
</button>
|
|
539
|
+
</div>
|
|
540
|
+
`;
|
|
541
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'https://esm.sh/preact/hooks';
|
|
2
|
+
|
|
3
|
+
export const usePolling = (fetcher, interval, { enabled = true } = {}) => {
|
|
4
|
+
const [data, setData] = useState(null);
|
|
5
|
+
const [error, setError] = useState(null);
|
|
6
|
+
const fetcherRef = useRef(fetcher);
|
|
7
|
+
fetcherRef.current = fetcher;
|
|
8
|
+
|
|
9
|
+
const refresh = useCallback(async () => {
|
|
10
|
+
try {
|
|
11
|
+
const result = await fetcherRef.current();
|
|
12
|
+
setData(result);
|
|
13
|
+
setError(null);
|
|
14
|
+
return result;
|
|
15
|
+
} catch (err) {
|
|
16
|
+
setError(err);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!enabled) return;
|
|
23
|
+
refresh();
|
|
24
|
+
const id = setInterval(refresh, interval);
|
|
25
|
+
return () => clearInterval(id);
|
|
26
|
+
}, [enabled, interval, refresh]);
|
|
27
|
+
|
|
28
|
+
return { data, error, refresh };
|
|
29
|
+
};
|