Skip to content

Commit 927d05f

Browse files
nv-kasikritcKasikrit (Gus) Chantharuang
andauthored
feat(nemoclaw launchable): first draft of e2e basic flow (#6)
* 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 --------- Co-authored-by: Kasikrit (Gus) Chantharuang <kchantharaun@nvidia.com>
1 parent 1de4791 commit 927d05f

File tree

25 files changed

+4707
-2
lines changed

25 files changed

+4707
-2
lines changed

.github/workflows/build-sandboxes.yml

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,39 @@ jobs:
187187
tags: localhost:5000/sandboxes/base:latest
188188
cache-from: type=gha,scope=base
189189

190+
- name: Determine parent sandbox
191+
id: parent
192+
run: |
193+
set -euo pipefail
194+
DEFAULT_BASE=$(grep '^ARG BASE_IMAGE=' "sandboxes/${{ matrix.sandbox }}/Dockerfile" | head -1 | cut -d= -f2-)
195+
PARENT=$(echo "$DEFAULT_BASE" | sed -n 's|.*/sandboxes/\([^:]*\).*|\1|p')
196+
if [ -z "$PARENT" ]; then
197+
PARENT="base"
198+
fi
199+
echo "sandbox=$PARENT" >> "$GITHUB_OUTPUT"
200+
echo "Parent for ${{ matrix.sandbox }}: $PARENT"
201+
202+
# When a sandbox depends on another sandbox (not base), build that
203+
# intermediate parent locally so it is available to the buildx build.
204+
- name: Build parent sandbox locally (PR only)
205+
if: github.ref != 'refs/heads/main' && steps.parent.outputs.sandbox != 'base'
206+
uses: docker/build-push-action@v6
207+
with:
208+
context: sandboxes/${{ steps.parent.outputs.sandbox }}
209+
push: true
210+
tags: localhost:5000/sandboxes/${{ steps.parent.outputs.sandbox }}:latest
211+
build-args: |
212+
BASE_IMAGE=localhost:5000/sandboxes/base:latest
213+
cache-from: type=gha,scope=${{ steps.parent.outputs.sandbox }}
214+
190215
- name: Set BASE_IMAGE
191216
id: base
192217
run: |
218+
PARENT="${{ steps.parent.outputs.sandbox }}"
193219
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
194-
echo "image=${{ env.REGISTRY }}/${{ steps.repo.outputs.image_prefix }}/sandboxes/base:latest" >> "$GITHUB_OUTPUT"
220+
echo "image=${{ env.REGISTRY }}/${{ steps.repo.outputs.image_prefix }}/sandboxes/${PARENT}:latest" >> "$GITHUB_OUTPUT"
195221
else
196-
echo "image=localhost:5000/sandboxes/base:latest" >> "$GITHUB_OUTPUT"
222+
echo "image=localhost:5000/sandboxes/${PARENT}:latest" >> "$GITHUB_OUTPUT"
197223
fi
198224
199225
- name: Generate image metadata

brev/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
brev-start-vm.sh

brev/welcome-ui/app.js

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
(() => {
2+
"use strict";
3+
4+
const $ = (sel) => document.querySelector(sel);
5+
6+
// -- DOM refs --------------------------------------------------------
7+
8+
const cardOpenclaw = $("#card-openclaw");
9+
const cardOther = $("#card-other");
10+
const overlayInstall = $("#overlay-install");
11+
const overlayInstr = $("#overlay-instructions");
12+
const closeInstall = $("#close-install");
13+
const closeInstr = $("#close-instructions");
14+
15+
// Path 1 elements
16+
const stepKey = $("#install-step-key");
17+
const stepProgress = $("#install-step-progress");
18+
const stepSuccess = $("#install-step-success");
19+
const stepError = $("#install-step-error");
20+
const apiKeyInput = $("#api-key-input");
21+
const toggleKeyVis = $("#toggle-key-vis");
22+
const btnInstall = $("#btn-install");
23+
const btnRetry = $("#btn-retry");
24+
const btnOpenOpenclaw = $("#btn-open-openclaw");
25+
const errorMessage = $("#error-message");
26+
27+
// Progress steps
28+
const pstepSandbox = $("#pstep-sandbox");
29+
const pstepGateway = $("#pstep-gateway");
30+
const pstepReady = $("#pstep-ready");
31+
32+
// Path 2 elements
33+
const connectCmd = $("#connect-cmd");
34+
const copyConnect = $("#copy-connect");
35+
36+
// -- SVG icons -------------------------------------------------------
37+
38+
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>`;
39+
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>`;
40+
41+
// -- Modal helpers ---------------------------------------------------
42+
43+
function showOverlay(el) {
44+
el.hidden = false;
45+
}
46+
function hideOverlay(el) {
47+
el.hidden = true;
48+
}
49+
50+
function closeOnBackdrop(overlay) {
51+
overlay.addEventListener("click", (e) => {
52+
if (e.target === overlay) hideOverlay(overlay);
53+
});
54+
}
55+
56+
// -- Visibility toggle for API key ----------------------------------
57+
58+
let keyVisible = false;
59+
toggleKeyVis.addEventListener("click", () => {
60+
keyVisible = !keyVisible;
61+
apiKeyInput.type = keyVisible ? "text" : "password";
62+
toggleKeyVis.innerHTML = keyVisible ? iconEyeOff : iconEye;
63+
});
64+
65+
// -- Copy to clipboard ----------------------------------------------
66+
67+
function flashCopied(btn) {
68+
const original = btn.innerHTML;
69+
btn.innerHTML = `<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>`;
70+
btn.classList.add("copy-btn--done");
71+
setTimeout(() => {
72+
btn.innerHTML = original;
73+
btn.classList.remove("copy-btn--done");
74+
}, 1500);
75+
}
76+
77+
document.addEventListener("click", (e) => {
78+
const btn = e.target.closest(".copy-btn");
79+
if (!btn) return;
80+
const text = btn.dataset.copy || btn.closest(".code-block")?.textContent?.trim();
81+
if (text) {
82+
navigator.clipboard.writeText(text).then(() => flashCopied(btn));
83+
}
84+
});
85+
86+
// -- Progress step state machine ------------------------------------
87+
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}`);
91+
}
92+
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";
100+
}
101+
102+
let pollTimer = null;
103+
104+
function stopPolling() {
105+
if (pollTimer) {
106+
clearInterval(pollTimer);
107+
pollTimer = null;
108+
}
109+
}
110+
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;
118+
}
119+
120+
showInstallStep("progress");
121+
setStepState(pstepSandbox, "active");
122+
setStepState(pstepGateway, null);
123+
setStepState(pstepReady, null);
124+
125+
try {
126+
const res = await fetch("/api/install-openclaw", {
127+
method: "POST",
128+
headers: { "Content-Type": "application/json" },
129+
body: JSON.stringify({ apiKey }),
130+
});
131+
const data = await res.json();
132+
133+
if (!data.ok) {
134+
showError(data.error || "Failed to start sandbox creation");
135+
return;
136+
}
137+
138+
setStepState(pstepSandbox, "done");
139+
setStepState(pstepGateway, "active");
140+
startPolling();
141+
} catch (err) {
142+
showError("Could not reach the server. Please try again.");
143+
}
144+
}
145+
146+
function startPolling() {
147+
stopPolling();
148+
pollTimer = setInterval(async () => {
149+
try {
150+
const res = await fetch("/api/sandbox-status");
151+
const data = await res.json();
152+
153+
if (data.status === "running") {
154+
stopPolling();
155+
setStepState(pstepGateway, "done");
156+
setStepState(pstepReady, "done");
157+
158+
btnOpenOpenclaw.href = data.url || "http://127.0.0.1:18789/";
159+
showInstallStep("success");
160+
} else if (data.status === "error") {
161+
stopPolling();
162+
showError(data.error || "Sandbox creation failed");
163+
}
164+
} catch {
165+
// transient fetch error, keep polling
166+
}
167+
}, 3000);
168+
}
169+
170+
function showError(msg) {
171+
stopPolling();
172+
errorMessage.textContent = msg;
173+
showInstallStep("error");
174+
}
175+
176+
function resetInstall() {
177+
showInstallStep("key");
178+
setStepState(pstepSandbox, null);
179+
setStepState(pstepGateway, null);
180+
setStepState(pstepReady, null);
181+
}
182+
183+
btnInstall.addEventListener("click", startInstall);
184+
apiKeyInput.addEventListener("keydown", (e) => {
185+
if (e.key === "Enter") startInstall();
186+
});
187+
btnRetry.addEventListener("click", resetInstall);
188+
189+
// -- Path 1: Check if sandbox already running on load ---------------
190+
191+
async function checkExistingSandbox() {
192+
try {
193+
const res = await fetch("/api/sandbox-status");
194+
const data = await res.json();
195+
if (data.status === "running" && data.url) {
196+
btnOpenOpenclaw.href = data.url;
197+
showInstallStep("success");
198+
showOverlay(overlayInstall);
199+
} else if (data.status === "creating") {
200+
showInstallStep("progress");
201+
setStepState(pstepSandbox, "done");
202+
setStepState(pstepGateway, "active");
203+
showOverlay(overlayInstall);
204+
startPolling();
205+
}
206+
} catch {
207+
// server not ready yet, ignore
208+
}
209+
}
210+
211+
// -- Path 2: Load connection details --------------------------------
212+
213+
async function loadConnectionDetails() {
214+
try {
215+
const res = await fetch("/api/connection-details");
216+
const data = await res.json();
217+
const cmd = `nemoclaw cluster connect ${data.hostname}`;
218+
connectCmd.textContent = cmd;
219+
copyConnect.dataset.copy = cmd;
220+
} catch {
221+
connectCmd.textContent = "nemoclaw cluster connect <hostname>";
222+
}
223+
}
224+
225+
// -- Event wiring ---------------------------------------------------
226+
227+
cardOpenclaw.addEventListener("click", () => {
228+
showOverlay(overlayInstall);
229+
});
230+
231+
cardOther.addEventListener("click", () => {
232+
loadConnectionDetails();
233+
showOverlay(overlayInstr);
234+
});
235+
236+
closeInstall.addEventListener("click", () => hideOverlay(overlayInstall));
237+
closeInstr.addEventListener("click", () => hideOverlay(overlayInstr));
238+
239+
closeOnBackdrop(overlayInstall);
240+
closeOnBackdrop(overlayInstr);
241+
242+
document.addEventListener("keydown", (e) => {
243+
if (e.key === "Escape") {
244+
hideOverlay(overlayInstall);
245+
hideOverlay(overlayInstr);
246+
}
247+
});
248+
249+
// -- Init -----------------------------------------------------------
250+
251+
checkExistingSandbox();
252+
})();

0 commit comments

Comments
 (0)