diff --git a/brev/welcome-ui/app.js b/brev/welcome-ui/app.js index 31979c8..645eb7a 100644 --- a/brev/welcome-ui/app.js +++ b/brev/welcome-ui/app.js @@ -12,22 +12,24 @@ const closeInstall = $("#close-install"); const closeInstr = $("#close-instructions"); - // Path 1 elements - const stepKey = $("#install-step-key"); - const stepProgress = $("#install-step-progress"); - const stepSuccess = $("#install-step-success"); + // Install modal elements + const installMain = $("#install-main"); const stepError = $("#install-step-error"); const apiKeyInput = $("#api-key-input"); const toggleKeyVis = $("#toggle-key-vis"); - const btnInstall = $("#btn-install"); + const keyHint = $("#key-hint"); + const btnLaunch = $("#btn-launch"); + const btnLaunchLabel = $("#btn-launch-label"); + const btnSpinner = $("#btn-spinner"); const btnRetry = $("#btn-retry"); - const btnOpenOpenclaw = $("#btn-open-openclaw"); const errorMessage = $("#error-message"); - // Progress steps - const pstepSandbox = $("#pstep-sandbox"); - const pstepGateway = $("#pstep-gateway"); - const pstepReady = $("#pstep-ready"); + // Console log lines + const logSandbox = $("#log-sandbox"); + const logSandboxIcon = $("#log-sandbox-icon"); + const logGateway = $("#log-gateway"); + const logGatewayIcon = $("#log-gateway-icon"); + const logReady = $("#log-ready"); // Path 2 elements const connectCmd = $("#connect-cmd"); @@ -38,6 +40,9 @@ const iconEye = ``; const iconEyeOff = ``; + const SPINNER_CHAR = "↻"; + const CHECK_CHAR = "✓"; + // -- Modal helpers --------------------------------------------------- function showOverlay(el) { @@ -83,22 +88,33 @@ } }); - // -- Progress step state machine ------------------------------------ + // -- API key validation --------------------------------------------- - function setStepState(el, state) { - el.classList.remove("progress-step--active", "progress-step--done", "progress-step--error"); - if (state) el.classList.add(`progress-step--${state}`); + function isApiKeyValid() { + const v = apiKeyInput.value.trim(); + return v.startsWith("nvapi-") || v.startsWith("sk-"); } - // -- Path 1: Install flow ------------------------------------------- - - function showInstallStep(step) { - stepKey.hidden = step !== "key"; - stepProgress.hidden = step !== "progress"; - stepSuccess.hidden = step !== "success"; - stepError.hidden = step !== "error"; + // -- Console log helpers -------------------------------------------- + + function setLogIcon(iconEl, state) { + if (state === "spin") { + iconEl.textContent = SPINNER_CHAR; + iconEl.className = "console__icon console__icon--spin"; + } else if (state === "done") { + iconEl.textContent = CHECK_CHAR; + iconEl.className = "console__icon console__icon--done"; + } else { + iconEl.textContent = ""; + iconEl.className = "console__icon"; + } } + // -- Install state --------------------------------------------------- + + let sandboxReady = false; + let sandboxUrl = null; + let installTriggered = false; let pollTimer = null; function stopPolling() { @@ -108,37 +124,100 @@ } } - async function startInstall() { - const apiKey = apiKeyInput.value.trim(); - if (!apiKey) { - apiKeyInput.focus(); - apiKeyInput.classList.add("form-field__input--error"); - setTimeout(() => apiKeyInput.classList.remove("form-field__input--error"), 1500); - return; + /** + * Four-state CTA button: + * 1. API empty + tasks running -> "Waiting for API key…" (disabled) + * 2. API valid + tasks running -> "Provisioning Sandbox…" (disabled, spinner) + * 3. API empty + tasks complete -> "Waiting for API key…" (disabled) + * 4. API valid + tasks complete -> "Open NemoClaw" (enabled) + */ + function updateButtonState() { + const keyValid = isApiKeyValid(); + const keyRaw = apiKeyInput.value.trim(); + + // Hint feedback below input + if (keyRaw.length === 0) { + keyHint.textContent = ""; + keyHint.className = "form-field__hint"; + } else if (keyValid) { + keyHint.textContent = "Valid key format"; + keyHint.className = "form-field__hint form-field__hint--ok"; + } else { + keyHint.textContent = "Key must start with nvapi- or sk-"; + keyHint.className = "form-field__hint form-field__hint--warn"; + } + + // Console "ready" line + if (sandboxReady && keyValid) { + logReady.hidden = false; + logReady.querySelector(".console__icon").textContent = CHECK_CHAR; + logReady.querySelector(".console__icon").className = "console__icon console__icon--done"; + } else { + logReady.hidden = true; + } + + if (sandboxReady && keyValid) { + btnLaunch.disabled = false; + btnLaunch.classList.add("btn--ready"); + btnSpinner.hidden = true; + btnSpinner.style.display = "none"; + btnLaunchLabel.textContent = "Open NemoClaw"; + } else if (!sandboxReady && keyValid) { + btnLaunch.disabled = true; + btnLaunch.classList.remove("btn--ready"); + btnSpinner.hidden = false; + btnSpinner.style.display = ""; + btnLaunchLabel.textContent = "Provisioning Sandbox\u2026"; + } else { + btnLaunch.disabled = true; + btnLaunch.classList.remove("btn--ready"); + btnSpinner.hidden = true; + btnSpinner.style.display = "none"; + btnLaunchLabel.textContent = "Waiting for API key\u2026"; } + } + + function showMainView() { + installMain.hidden = false; + stepError.hidden = true; + } + + function showError(msg) { + stopPolling(); + installMain.hidden = true; + stepError.hidden = false; + errorMessage.textContent = msg; + } + + async function triggerInstall() { + if (installTriggered) return; + installTriggered = true; - showInstallStep("progress"); - setStepState(pstepSandbox, "active"); - setStepState(pstepGateway, null); - setStepState(pstepReady, null); + setLogIcon(logSandboxIcon, "spin"); + setLogIcon(logGatewayIcon, null); + logReady.hidden = true; + updateButtonState(); try { const res = await fetch("/api/install-openclaw", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ apiKey }), }); const data = await res.json(); if (!data.ok) { + installTriggered = false; showError(data.error || "Failed to start sandbox creation"); return; } - setStepState(pstepSandbox, "done"); - setStepState(pstepGateway, "active"); + setLogIcon(logSandboxIcon, "done"); + logSandbox.querySelector(".console__text").textContent = + "Secure NemoClaw sandbox created."; + setLogIcon(logGatewayIcon, "spin"); startPolling(); - } catch (err) { + } catch { + installTriggered = false; showError("Could not reach the server. Please try again."); } } @@ -152,13 +231,16 @@ if (data.status === "running") { stopPolling(); - setStepState(pstepGateway, "done"); - setStepState(pstepReady, "done"); + sandboxReady = true; + sandboxUrl = data.url || null; - btnOpenOpenclaw.href = data.url || "http://127.0.0.1:18789/"; - showInstallStep("success"); + setLogIcon(logGatewayIcon, "done"); + logGateway.querySelector(".console__text").textContent = + "OpenClaw agent gateway online."; + updateButtonState(); } else if (data.status === "error") { stopPolling(); + installTriggered = false; showError(data.error || "Sandbox creation failed"); } } catch { @@ -167,39 +249,68 @@ }, 3000); } - function showError(msg) { - stopPolling(); - errorMessage.textContent = msg; - showInstallStep("error"); + function openOpenClaw() { + if (!sandboxReady || !isApiKeyValid() || !sandboxUrl) return; + + const apiKey = apiKeyInput.value.trim(); + const url = new URL(sandboxUrl); + url.searchParams.set("nvapi", apiKey); + window.open(url.toString(), "_blank", "noopener,noreferrer"); } function resetInstall() { - showInstallStep("key"); - setStepState(pstepSandbox, null); - setStepState(pstepGateway, null); - setStepState(pstepReady, null); + sandboxReady = false; + sandboxUrl = null; + installTriggered = false; + stopPolling(); + + setLogIcon(logSandboxIcon, null); + setLogIcon(logGatewayIcon, null); + logSandbox.querySelector(".console__text").textContent = + "Initializing secure NemoClaw sandbox..."; + logGateway.querySelector(".console__text").textContent = + "Launching OpenClaw agent gateway..."; + logReady.hidden = true; + + showMainView(); + updateButtonState(); + triggerInstall(); } - btnInstall.addEventListener("click", startInstall); - apiKeyInput.addEventListener("keydown", (e) => { - if (e.key === "Enter") startInstall(); - }); + apiKeyInput.addEventListener("input", updateButtonState); + btnLaunch.addEventListener("click", openOpenClaw); btnRetry.addEventListener("click", resetInstall); - // -- Path 1: Check if sandbox already running on load --------------- + // -- Check if sandbox already running on load ----------------------- async function checkExistingSandbox() { try { const res = await fetch("/api/sandbox-status"); const data = await res.json(); + if (data.status === "running" && data.url) { - btnOpenOpenclaw.href = data.url; - showInstallStep("success"); + sandboxReady = true; + sandboxUrl = data.url; + installTriggered = true; + + setLogIcon(logSandboxIcon, "done"); + logSandbox.querySelector(".console__text").textContent = + "Secure NemoClaw sandbox created."; + setLogIcon(logGatewayIcon, "done"); + logGateway.querySelector(".console__text").textContent = + "OpenClaw agent gateway online."; + updateButtonState(); + showOverlay(overlayInstall); } else if (data.status === "creating") { - showInstallStep("progress"); - setStepState(pstepSandbox, "done"); - setStepState(pstepGateway, "active"); + installTriggered = true; + + setLogIcon(logSandboxIcon, "done"); + logSandbox.querySelector(".console__text").textContent = + "Secure NemoClaw sandbox created."; + setLogIcon(logGatewayIcon, "spin"); + updateButtonState(); + showOverlay(overlayInstall); startPolling(); } @@ -226,6 +337,12 @@ cardOpenclaw.addEventListener("click", () => { showOverlay(overlayInstall); + showMainView(); + if (!installTriggered) { + triggerInstall(); + } + apiKeyInput.focus(); + updateButtonState(); }); cardOther.addEventListener("click", () => { diff --git a/brev/welcome-ui/index.html b/brev/welcome-ui/index.html index c891641..7a7ab28 100644 --- a/brev/welcome-ui/index.html +++ b/brev/welcome-ui/index.html @@ -4,7 +4,7 @@
- Enter your NVIDIA API key to power the OpenClaw agent. The key is passed into the - sandbox and never stored on this server. -
-+ Enter your NVIDIA API key below. Your secure sandbox is being + provisioned in the background so you'll be ready to go immediately. +
+- The agent is live inside a NemoClaw sandbox. When it hits a policy denial, it will - diagnose the block and propose a policy update. You approve or deny from the UI. -
- - Open OpenClaw - - -