main
html 232 lines 11.5 KB
Raw
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>