Skip to content

Commit 2ee8270

Browse files
nv-kasikritcKasikrit (Gus) Chantharuang
andauthored
feat(welcome-ui): parallel sandbox provisioning with inline API key flow (#7)
* Added first draft of UI extension * Refactored and reorganized code for NemoClaw * Added welcome UI scaffold * Edited nemoclaw-start.sh * Edited files to pass CI pipeline * feat(welcome-ui): parallel sandbox provisioning with inline API key flow --------- Co-authored-by: Kasikrit (Gus) Chantharuang <kchantharaun@nvidia.com>
1 parent 927d05f commit 2ee8270

File tree

8 files changed

+519
-253
lines changed

8 files changed

+519
-253
lines changed

brev/welcome-ui/app.js

Lines changed: 175 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,24 @@
1212
const closeInstall = $("#close-install");
1313
const closeInstr = $("#close-instructions");
1414

15-
// Path 1 elements
16-
const stepKey = $("#install-step-key");
17-
const stepProgress = $("#install-step-progress");
18-
const stepSuccess = $("#install-step-success");
15+
// Install modal elements
16+
const installMain = $("#install-main");
1917
const stepError = $("#install-step-error");
2018
const apiKeyInput = $("#api-key-input");
2119
const toggleKeyVis = $("#toggle-key-vis");
22-
const btnInstall = $("#btn-install");
20+
const keyHint = $("#key-hint");
21+
const btnLaunch = $("#btn-launch");
22+
const btnLaunchLabel = $("#btn-launch-label");
23+
const btnSpinner = $("#btn-spinner");
2324
const btnRetry = $("#btn-retry");
24-
const btnOpenOpenclaw = $("#btn-open-openclaw");
2525
const errorMessage = $("#error-message");
2626

27-
// Progress steps
28-
const pstepSandbox = $("#pstep-sandbox");
29-
const pstepGateway = $("#pstep-gateway");
30-
const pstepReady = $("#pstep-ready");
27+
// Console log lines
28+
const logSandbox = $("#log-sandbox");
29+
const logSandboxIcon = $("#log-sandbox-icon");
30+
const logGateway = $("#log-gateway");
31+
const logGatewayIcon = $("#log-gateway-icon");
32+
const logReady = $("#log-ready");
3133

3234
// Path 2 elements
3335
const connectCmd = $("#connect-cmd");
@@ -38,6 +40,9 @@
3840
const iconEye = `<svg viewBox="0 0 24 24"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>`;
3941
const iconEyeOff = `<svg viewBox="0 0 24 24"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" x2="23" y1="1" y2="23"/></svg>`;
4042

43+
const SPINNER_CHAR = "↻";
44+
const CHECK_CHAR = "✓";
45+
4146
// -- Modal helpers ---------------------------------------------------
4247

4348
function showOverlay(el) {
@@ -83,22 +88,33 @@
8388
}
8489
});
8590

86-
// -- Progress step state machine ------------------------------------
91+
// -- API key validation ---------------------------------------------
8792

88-
function setStepState(el, state) {
89-
el.classList.remove("progress-step--active", "progress-step--done", "progress-step--error");
90-
if (state) el.classList.add(`progress-step--${state}`);
93+
function isApiKeyValid() {
94+
const v = apiKeyInput.value.trim();
95+
return v.startsWith("nvapi-") || v.startsWith("sk-");
9196
}
9297

93-
// -- Path 1: Install flow -------------------------------------------
94-
95-
function showInstallStep(step) {
96-
stepKey.hidden = step !== "key";
97-
stepProgress.hidden = step !== "progress";
98-
stepSuccess.hidden = step !== "success";
99-
stepError.hidden = step !== "error";
98+
// -- Console log helpers --------------------------------------------
99+
100+
function setLogIcon(iconEl, state) {
101+
if (state === "spin") {
102+
iconEl.textContent = SPINNER_CHAR;
103+
iconEl.className = "console__icon console__icon--spin";
104+
} else if (state === "done") {
105+
iconEl.textContent = CHECK_CHAR;
106+
iconEl.className = "console__icon console__icon--done";
107+
} else {
108+
iconEl.textContent = "";
109+
iconEl.className = "console__icon";
110+
}
100111
}
101112

113+
// -- Install state ---------------------------------------------------
114+
115+
let sandboxReady = false;
116+
let sandboxUrl = null;
117+
let installTriggered = false;
102118
let pollTimer = null;
103119

104120
function stopPolling() {
@@ -108,37 +124,100 @@
108124
}
109125
}
110126

111-
async function startInstall() {
112-
const apiKey = apiKeyInput.value.trim();
113-
if (!apiKey) {
114-
apiKeyInput.focus();
115-
apiKeyInput.classList.add("form-field__input--error");
116-
setTimeout(() => apiKeyInput.classList.remove("form-field__input--error"), 1500);
117-
return;
127+
/**
128+
* Four-state CTA button:
129+
* 1. API empty + tasks running -> "Waiting for API key…" (disabled)
130+
* 2. API valid + tasks running -> "Provisioning Sandbox…" (disabled, spinner)
131+
* 3. API empty + tasks complete -> "Waiting for API key…" (disabled)
132+
* 4. API valid + tasks complete -> "Open NemoClaw" (enabled)
133+
*/
134+
function updateButtonState() {
135+
const keyValid = isApiKeyValid();
136+
const keyRaw = apiKeyInput.value.trim();
137+
138+
// Hint feedback below input
139+
if (keyRaw.length === 0) {
140+
keyHint.textContent = "";
141+
keyHint.className = "form-field__hint";
142+
} else if (keyValid) {
143+
keyHint.textContent = "Valid key format";
144+
keyHint.className = "form-field__hint form-field__hint--ok";
145+
} else {
146+
keyHint.textContent = "Key must start with nvapi- or sk-";
147+
keyHint.className = "form-field__hint form-field__hint--warn";
148+
}
149+
150+
// Console "ready" line
151+
if (sandboxReady && keyValid) {
152+
logReady.hidden = false;
153+
logReady.querySelector(".console__icon").textContent = CHECK_CHAR;
154+
logReady.querySelector(".console__icon").className = "console__icon console__icon--done";
155+
} else {
156+
logReady.hidden = true;
157+
}
158+
159+
if (sandboxReady && keyValid) {
160+
btnLaunch.disabled = false;
161+
btnLaunch.classList.add("btn--ready");
162+
btnSpinner.hidden = true;
163+
btnSpinner.style.display = "none";
164+
btnLaunchLabel.textContent = "Open NemoClaw";
165+
} else if (!sandboxReady && keyValid) {
166+
btnLaunch.disabled = true;
167+
btnLaunch.classList.remove("btn--ready");
168+
btnSpinner.hidden = false;
169+
btnSpinner.style.display = "";
170+
btnLaunchLabel.textContent = "Provisioning Sandbox\u2026";
171+
} else {
172+
btnLaunch.disabled = true;
173+
btnLaunch.classList.remove("btn--ready");
174+
btnSpinner.hidden = true;
175+
btnSpinner.style.display = "none";
176+
btnLaunchLabel.textContent = "Waiting for API key\u2026";
118177
}
178+
}
179+
180+
function showMainView() {
181+
installMain.hidden = false;
182+
stepError.hidden = true;
183+
}
184+
185+
function showError(msg) {
186+
stopPolling();
187+
installMain.hidden = true;
188+
stepError.hidden = false;
189+
errorMessage.textContent = msg;
190+
}
191+
192+
async function triggerInstall() {
193+
if (installTriggered) return;
194+
installTriggered = true;
119195

120-
showInstallStep("progress");
121-
setStepState(pstepSandbox, "active");
122-
setStepState(pstepGateway, null);
123-
setStepState(pstepReady, null);
196+
setLogIcon(logSandboxIcon, "spin");
197+
setLogIcon(logGatewayIcon, null);
198+
logReady.hidden = true;
199+
updateButtonState();
124200

125201
try {
126202
const res = await fetch("/api/install-openclaw", {
127203
method: "POST",
128204
headers: { "Content-Type": "application/json" },
129-
body: JSON.stringify({ apiKey }),
130205
});
131206
const data = await res.json();
132207

133208
if (!data.ok) {
209+
installTriggered = false;
134210
showError(data.error || "Failed to start sandbox creation");
135211
return;
136212
}
137213

138-
setStepState(pstepSandbox, "done");
139-
setStepState(pstepGateway, "active");
214+
setLogIcon(logSandboxIcon, "done");
215+
logSandbox.querySelector(".console__text").textContent =
216+
"Secure NemoClaw sandbox created.";
217+
setLogIcon(logGatewayIcon, "spin");
140218
startPolling();
141-
} catch (err) {
219+
} catch {
220+
installTriggered = false;
142221
showError("Could not reach the server. Please try again.");
143222
}
144223
}
@@ -152,13 +231,16 @@
152231

153232
if (data.status === "running") {
154233
stopPolling();
155-
setStepState(pstepGateway, "done");
156-
setStepState(pstepReady, "done");
234+
sandboxReady = true;
235+
sandboxUrl = data.url || null;
157236

158-
btnOpenOpenclaw.href = data.url || "http://127.0.0.1:18789/";
159-
showInstallStep("success");
237+
setLogIcon(logGatewayIcon, "done");
238+
logGateway.querySelector(".console__text").textContent =
239+
"OpenClaw agent gateway online.";
240+
updateButtonState();
160241
} else if (data.status === "error") {
161242
stopPolling();
243+
installTriggered = false;
162244
showError(data.error || "Sandbox creation failed");
163245
}
164246
} catch {
@@ -167,39 +249,68 @@
167249
}, 3000);
168250
}
169251

170-
function showError(msg) {
171-
stopPolling();
172-
errorMessage.textContent = msg;
173-
showInstallStep("error");
252+
function openOpenClaw() {
253+
if (!sandboxReady || !isApiKeyValid() || !sandboxUrl) return;
254+
255+
const apiKey = apiKeyInput.value.trim();
256+
const url = new URL(sandboxUrl);
257+
url.searchParams.set("nvapi", apiKey);
258+
window.open(url.toString(), "_blank", "noopener,noreferrer");
174259
}
175260

176261
function resetInstall() {
177-
showInstallStep("key");
178-
setStepState(pstepSandbox, null);
179-
setStepState(pstepGateway, null);
180-
setStepState(pstepReady, null);
262+
sandboxReady = false;
263+
sandboxUrl = null;
264+
installTriggered = false;
265+
stopPolling();
266+
267+
setLogIcon(logSandboxIcon, null);
268+
setLogIcon(logGatewayIcon, null);
269+
logSandbox.querySelector(".console__text").textContent =
270+
"Initializing secure NemoClaw sandbox...";
271+
logGateway.querySelector(".console__text").textContent =
272+
"Launching OpenClaw agent gateway...";
273+
logReady.hidden = true;
274+
275+
showMainView();
276+
updateButtonState();
277+
triggerInstall();
181278
}
182279

183-
btnInstall.addEventListener("click", startInstall);
184-
apiKeyInput.addEventListener("keydown", (e) => {
185-
if (e.key === "Enter") startInstall();
186-
});
280+
apiKeyInput.addEventListener("input", updateButtonState);
281+
btnLaunch.addEventListener("click", openOpenClaw);
187282
btnRetry.addEventListener("click", resetInstall);
188283

189-
// -- Path 1: Check if sandbox already running on load ---------------
284+
// -- Check if sandbox already running on load -----------------------
190285

191286
async function checkExistingSandbox() {
192287
try {
193288
const res = await fetch("/api/sandbox-status");
194289
const data = await res.json();
290+
195291
if (data.status === "running" && data.url) {
196-
btnOpenOpenclaw.href = data.url;
197-
showInstallStep("success");
292+
sandboxReady = true;
293+
sandboxUrl = data.url;
294+
installTriggered = true;
295+
296+
setLogIcon(logSandboxIcon, "done");
297+
logSandbox.querySelector(".console__text").textContent =
298+
"Secure NemoClaw sandbox created.";
299+
setLogIcon(logGatewayIcon, "done");
300+
logGateway.querySelector(".console__text").textContent =
301+
"OpenClaw agent gateway online.";
302+
updateButtonState();
303+
198304
showOverlay(overlayInstall);
199305
} else if (data.status === "creating") {
200-
showInstallStep("progress");
201-
setStepState(pstepSandbox, "done");
202-
setStepState(pstepGateway, "active");
306+
installTriggered = true;
307+
308+
setLogIcon(logSandboxIcon, "done");
309+
logSandbox.querySelector(".console__text").textContent =
310+
"Secure NemoClaw sandbox created.";
311+
setLogIcon(logGatewayIcon, "spin");
312+
updateButtonState();
313+
203314
showOverlay(overlayInstall);
204315
startPolling();
205316
}
@@ -226,6 +337,12 @@
226337

227338
cardOpenclaw.addEventListener("click", () => {
228339
showOverlay(overlayInstall);
340+
showMainView();
341+
if (!installTriggered) {
342+
triggerInstall();
343+
}
344+
apiKeyInput.focus();
345+
updateButtonState();
229346
});
230347

231348
cardOther.addEventListener("click", () => {

0 commit comments

Comments
 (0)