| 1 | <!DOCTYPE html> |
| 2 | <html lang="en" data-splash-theme="dark"> |
| 3 | |
| 4 | <head> |
| 5 | <meta charset="UTF-8"> |
| 6 | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> |
| 7 | <title>Agent Zero</title> |
| 8 | <link rel="icon" href="data:,"> |
| 9 | <script> |
| 10 | try { |
| 11 | if (localStorage.getItem("darkMode") === "false") document.documentElement.dataset.splashTheme = "light" |
| 12 | } catch (_) {} |
| 13 | </script> |
| 14 | <style> |
| 15 | html, body { width: 100%; height: 100%; margin: 0; } |
| 16 | html { background: #131313; color: #fff; } |
| 17 | html[data-splash-theme="light"] { background: #fafafa; color: #000; } |
| 18 | body { display: grid; place-items: center; font-family: system-ui, sans-serif; } |
| 19 | #startup-transition { display: grid; justify-items: center; gap: 1rem; } |
| 20 | #startup-transition > svg { |
| 21 | width: clamp(12rem, 34vw, 23rem); max-width: 72vw; height: auto; |
| 22 | opacity: 0; animation: startup-logo-in .55s ease-out .05s forwards; |
| 23 | } |
| 24 | #startup-transition > p { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); } |
| 25 | @keyframes startup-logo-in { to { opacity: 1; } } |
| 26 | </style> |
| 27 | </head> |
| 28 | |
| 29 | <body> |
| 30 | <main id="startup-transition" role="status" aria-live="polite"> |
| 31 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 163 26" aria-hidden="true"> |
| 32 | <path fill="currentColor" d="m138.7 15.3c0.8 1.2 1.6 2.4 2.5 3.7h-2.7c-0.8-1-1.6-2.2-2.4-3.3h-3.1v3.3h-2.1v-11.3q1.8 0 3.6 0c1 0 2-0.1 3 0.1 1.6 0.2 2.8 1.1 3.2 2.7 0.3 1.7 0 3.3-1.5 4.4-0.2 0.1-0.3 0.2-0.5 0.4zm-5.7-1.7c1.5 0 2.8 0 4.2-0.1 0.9 0 1.3-0.7 1.3-1.6 0-1-0.4-1.7-1.3-1.8-1.4-0.2-2.8-0.1-4.2-0.1zm-29-5.8v1.6c-2.2 2.5-4.4 5-6.7 7.6h6.7v2.1h-9.6v-2.2c2-2.2 4-4.4 6.2-6.9h-6.2v-2.2zm-48.2-0.7c2.8 2.7 5.3 5.2 8 7.9v-6.5h2v11.4c-2.7-2.6-5.2-5.1-8-7.8v6.8h-2zm94.6 9.3c0.6-0.3 1.4-0.7 1.9-0.9 1.7 2.3 4.4 1.8 5.6 0.7 1.1-1 1.5-2.5 1-4-0.5-1.4-1.8-2.6-3.6-2.5-1.8 0.1-3.4 1.4-3.6 3.5h-2.1c0-2.1 1.5-4.6 3.9-5.3 3.1-0.9 6.1 0.6 7.3 3.5 1 2.8-0.2 5.9-3 7.2-2.6 1.3-6 0.3-7.4-2.2zm-120.5-6.3c-0.5 0.3-1.1 0.7-1.5 1-2.3-1.9-5-1.3-6 1.2-0.5 1.6 0.3 3.7 2.1 4.3 1.5 0.6 3.2 0 3.6-1.3-0.3-0.1-0.7-0.2-1.1-0.3-0.1-0.6-0.1-1.1-0.2-1.8h4.1c0.3 2.4-1.6 5-4 5.6-2.6 0.7-5.4-0.8-6.4-3.4-0.9-2.5 0.2-5.5 2.6-6.7 2.4-1.3 5.4-0.7 6.8 1.4zm-22.4-2.3c2 3.8 3.9 7.5 5.9 11.2h-2.3q-1.8-3.2-3.6-6.6c-1.3 2.3-2.4 4.4-3.6 6.6h-2.3c2-3.7 3.9-7.4 5.9-11.2zm69.1 2.5h-3.8v-1.9h9.5v1.9h-3.6v8.7h-2.1zm38.9-0.4v2.7l2.7 0.1c0 0 0 1.2 0 2.1-1.1 0-2.3 0-3.3-0.2-0.9-0.2-1.6-1-1.6-2-0.1-1.5 0-3.2 0-4.8h9.1v2.1zm-2.2 9.2v-2.9c0.5 0 1.1-0.1 1.7-0.1 0.1 0.3 0.1 0.9 0.4 0.9 0.3 0 4.5 0 6.9 0v2.1zm-71.9-8.7v2.6h2.5c0 0 0 1.2 0 2-1 0-2.1 0-3.1-0.2-0.8-0.2-1.4-0.9-1.5-1.8q0-2.2 0-4.5h8.5v1.9zm-2.1 8.5v-2.6c0.6-0.1 1.1-0.1 1.6-0.1 0.2 0.3 0.2 0.8 0.4 0.8 0.3 0 4.2 0 6.4 0v1.9z"/> |
| 33 | </svg> |
| 34 | <p id="startup-status">Starting Agent Zero</p> |
| 35 | </main> |
| 36 | |
| 37 | <script> |
| 38 | (() => { |
| 39 | const APP_DOCUMENT_PATH = "/ui/index" |
| 40 | const ACTIVATION_TIMEOUT_MS = 8000 |
| 41 | const CACHE_TIMEOUT_MS = 10000 |
| 42 | const FALLBACK_TIMEOUT_MS = 15000 |
| 43 | let replacementStarted = false |
| 44 | |
| 45 | const appDocumentPromise = fetch(APP_DOCUMENT_PATH, { |
| 46 | cache: "no-store", |
| 47 | credentials: "same-origin", |
| 48 | headers: { Accept: "text/html" }, |
| 49 | }).then(async (response) => { |
| 50 | if (!response.ok) throw new Error(`App document request failed: ${response.status}`) |
| 51 | return response.text() |
| 52 | }) |
| 53 | |
| 54 | const replaceWithApp = async () => { |
| 55 | if (replacementStarted) return |
| 56 | const appHtml = await appDocumentPromise |
| 57 | if (replacementStarted) return |
| 58 | replacementStarted = true |
| 59 | const overlay = document.getElementById("startup-transition") |
| 60 | if (overlay && document.documentElement.dataset.splashTheme === "light") { |
| 61 | overlay.dataset.theme = "light" |
| 62 | } |
| 63 | const appWithTransition = overlay |
| 64 | ? appHtml.replace( |
| 65 | /<body\b[^>]*>/i, |
| 66 | (bodyTag) => bodyTag + overlay.outerHTML, |
| 67 | ) |
| 68 | : appHtml |
| 69 | document.open() |
| 70 | document.write(appWithTransition) |
| 71 | document.close() |
| 72 | } |
| 73 | |
| 74 | const reportAppLoadFailure = (error) => { |
| 75 | console.error("WebUI document unavailable.", error) |
| 76 | const status = document.getElementById("startup-status") |
| 77 | if (status) { |
| 78 | status.textContent = "Unable to load Agent Zero. Refresh to retry." |
| 79 | status.style.cssText = "position:static;width:auto;height:auto;clip-path:none" |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | const fallbackTimer = setTimeout(() => { |
| 84 | replaceWithApp().catch(reportAppLoadFailure) |
| 85 | }, FALLBACK_TIMEOUT_MS) |
| 86 | |
| 87 | const withTimeout = (promise, timeout, label) => new Promise((resolve, reject) => { |
| 88 | const timer = setTimeout(() => reject(new Error(`${label} timed out`)), timeout) |
| 89 | promise.then( |
| 90 | (value) => { clearTimeout(timer); resolve(value) }, |
| 91 | (error) => { clearTimeout(timer); reject(error) }, |
| 92 | ) |
| 93 | }) |
| 94 | |
| 95 | const waitForActivation = (worker) => { |
| 96 | if (worker.state === "activated") return Promise.resolve(worker) |
| 97 | return new Promise((resolve, reject) => { |
| 98 | const onStateChange = () => { |
| 99 | if (worker.state === "activated") { |
| 100 | worker.removeEventListener("statechange", onStateChange) |
| 101 | resolve(worker) |
| 102 | } else if (worker.state === "redundant") { |
| 103 | worker.removeEventListener("statechange", onStateChange) |
| 104 | reject(new Error("Service worker became redundant")) |
| 105 | } |
| 106 | } |
| 107 | worker.addEventListener("statechange", onStateChange) |
| 108 | }) |
| 109 | } |
| 110 | |
| 111 | const waitForExpectedWorker = async (registration, expectedUrl) => { |
| 112 | const findWorker = () => [ |
| 113 | registration.installing, |
| 114 | registration.waiting, |
| 115 | registration.active, |
| 116 | ].find((worker) => worker?.scriptURL === expectedUrl) |
| 117 | |
| 118 | let worker = findWorker() |
| 119 | if (!worker) { |
| 120 | worker = await withTimeout(new Promise((resolve) => { |
| 121 | const onUpdateFound = () => { |
| 122 | const candidate = findWorker() |
| 123 | if (!candidate) return |
| 124 | registration.removeEventListener("updatefound", onUpdateFound) |
| 125 | resolve(candidate) |
| 126 | } |
| 127 | registration.addEventListener("updatefound", onUpdateFound) |
| 128 | }), ACTIVATION_TIMEOUT_MS, "Service worker update") |
| 129 | } |
| 130 | return withTimeout( |
| 131 | waitForActivation(worker), |
| 132 | ACTIVATION_TIMEOUT_MS, |
| 133 | "Service worker activation", |
| 134 | ) |
| 135 | } |
| 136 | |
| 137 | const waitForController = (expectedUrl) => { |
| 138 | if (navigator.serviceWorker.controller?.scriptURL === expectedUrl) { |
| 139 | return Promise.resolve(navigator.serviceWorker.controller) |
| 140 | } |
| 141 | return withTimeout(new Promise((resolve) => { |
| 142 | const onControllerChange = () => { |
| 143 | const controller = navigator.serviceWorker.controller |
| 144 | if (controller?.scriptURL !== expectedUrl) return |
| 145 | navigator.serviceWorker.removeEventListener( |
| 146 | "controllerchange", |
| 147 | onControllerChange, |
| 148 | ) |
| 149 | resolve(controller) |
| 150 | } |
| 151 | navigator.serviceWorker.addEventListener( |
| 152 | "controllerchange", |
| 153 | onControllerChange, |
| 154 | ) |
| 155 | }), ACTIVATION_TIMEOUT_MS, "Service worker control") |
| 156 | } |
| 157 | |
| 158 | const sendBundle = (worker, bundle, waitForReady) => { |
| 159 | if (!waitForReady) { |
| 160 | return Promise.resolve().then(() => { |
| 161 | worker.postMessage({ type: "preload-ui-bundle", bundle }) |
| 162 | }) |
| 163 | } |
| 164 | |
| 165 | return withTimeout(new Promise((resolve, reject) => { |
| 166 | const channel = new MessageChannel() |
| 167 | channel.port1.onmessage = (event) => { |
| 168 | channel.port1.close() |
| 169 | if (event.data?.ok && event.data.version === bundle.version) { |
| 170 | resolve() |
| 171 | } else { |
| 172 | reject(new Error(event.data?.error || "Asset cache rejected bundle")) |
| 173 | } |
| 174 | } |
| 175 | try { |
| 176 | worker.postMessage( |
| 177 | { type: "preload-ui-bundle", bundle }, |
| 178 | [channel.port2], |
| 179 | ) |
| 180 | } catch (error) { |
| 181 | channel.port1.close() |
| 182 | reject(error) |
| 183 | } |
| 184 | }), CACHE_TIMEOUT_MS, "Asset cache preparation") |
| 185 | } |
| 186 | |
| 187 | const initialize = async () => { |
| 188 | if (!navigator.serviceWorker) return |
| 189 | const response = await fetch("/ui/asset-bundle", { |
| 190 | cache: "no-cache", |
| 191 | credentials: "same-origin", |
| 192 | }) |
| 193 | if (!response.ok) throw new Error(`Bundle request failed: ${response.status}`) |
| 194 | const bundle = await response.json() |
| 195 | if (!bundle?.version || !bundle.files) throw new Error("Invalid asset bundle") |
| 196 | |
| 197 | const registrations = await navigator.serviceWorker.getRegistrations() |
| 198 | await Promise.all( |
| 199 | registrations |
| 200 | .filter((registration) => new URL(registration.scope).pathname === "/js/") |
| 201 | .map((registration) => registration.unregister()), |
| 202 | ) |
| 203 | |
| 204 | const expectedUrl = new URL( |
| 205 | `/sw.js?version=${encodeURIComponent(bundle.version)}`, |
| 206 | location.origin, |
| 207 | ).href |
| 208 | const previousWorker = navigator.serviceWorker.controller |
| 209 | if (previousWorker && previousWorker.scriptURL !== expectedUrl) { |
| 210 | sendBundle(previousWorker, bundle, false).catch(() => undefined) |
| 211 | } |
| 212 | |
| 213 | const registration = await navigator.serviceWorker.register(expectedUrl, { |
| 214 | scope: "/", |
| 215 | updateViaCache: "none", |
| 216 | }) |
| 217 | const worker = await waitForExpectedWorker(registration, expectedUrl) |
| 218 | await sendBundle(worker, bundle, true) |
| 219 | await waitForController(expectedUrl) |
| 220 | } |
| 221 | |
| 222 | initialize() |
| 223 | .catch((error) => console.warn("WebUI bootstrap cache unavailable.", error)) |
| 224 | .finally(() => { |
| 225 | clearTimeout(fallbackTimer) |
| 226 | replaceWithApp().catch(reportAppLoadFailure) |
| 227 | }) |
| 228 | })() |
| 229 | </script> |
| 230 | </body> |
| 231 | |
| 232 | </html> |