agent-zero/webui/index.html
linkliti 7141e29580 fix(webui): paint startup overlay without blocking stylesheets
Inline the icon guard from vendor/google/google-icons.css into the index critical style with absolute font URLs and remove the last render-blocking stylesheet link, so the carried startup overlay and its logo paint immediately after the document write instead of waiting for a stylesheet. Gate the startup reveal on the main stylesheet with a bounded wait so the app never fades to unstyled white while the non-render-blocking main stylesheet is still loading; the overlay itself carries the startup background, so no html-level paint rules are needed.

Validation: startup asset, icons, offline asset, message queue static, and secrets suites pass on the merged state; the two remaining startup failures pre-exist on clean origin/ready.
2026-10-13 18:06:47 +03:00

322 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Agent Zero</title>
<style id="startup-transition-critical">
#startup-transition {
position: fixed;
inset: 0;
z-index: 2147483647;
display: grid;
place-items: center;
margin: 0;
background: #131313;
color: #fff;
opacity: 1;
transition: opacity 240ms ease;
}
body.light-mode #startup-transition { background: #fafafa; color: #000; }
#startup-transition > svg {
width: clamp(12rem, 34vw, 23rem);
max-width: 72vw;
height: auto;
opacity: 1;
animation: none;
}
#startup-transition > p {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
#startup-transition.startup-transition-leaving {
opacity: 0;
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
#startup-transition { transition: none; }
}
/* Icon guard inlined from vendor/google/google-icons.css so no stylesheet blocks first paint */
@font-face {
font-family: 'Material Symbols Outlined';
font-style: normal;
font-weight: 400;
font-display: block;
src: url(/vendor/google/google-icons.woff2) format('woff2'), url(/vendor/google/google-icons.ttf) format('truetype');
}
x-icon, .material-symbols-outlined, .material-icons-outlined {
font-family: 'Material Symbols Outlined';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 1em !important;
min-width: 1em !important;
max-width: 1em !important;
height: 1em !important;
overflow: hidden !important;
flex: 0 0 1em !important;
vertical-align: middle;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
}
html:not(.material-icons-ready) x-icon,
html:not(.material-icons-ready) .material-symbols-outlined,
html:not(.material-icons-ready) .material-icons-outlined { color: transparent !important; }
</style>
<script>
if (location.pathname === "/safe") {
const safeUrl = new URL(location.href)
safeUrl.searchParams.delete("__direct")
history.replaceState(null, "", `${safeUrl.pathname}${safeUrl.search}${safeUrl.hash}`)
if ("serviceWorker" in navigator) {
navigator.serviceWorker.getRegistrations()
.then((registrations) => Promise.allSettled(
registrations.map((registration) => registration.unregister()),
))
.catch((error) => console.warn("Unable to disable service workers.", error))
}
}
</script>
<link rel="icon" type="image/svg+xml" href="public/favicon.svg">
<link rel="preload" href="/vendor/google/google-icons.woff2" as="font" type="font/woff2" crossorigin>
<script type="module" src="/js/icons.js"></script>
<script>
(() => {
const root = document.documentElement
let applicationReady = false
let revealStarted = false
const mainStylesheetReady = () => {
const link = document.querySelector('link[href="index.css"], link[href="/index.css"]')
return !link || Boolean(link.sheet)
}
const waitForStylesheets = () => new Promise((resolve) => {
if (mainStylesheetReady()) return resolve()
const timer = setTimeout(finish, 2000)
const poll = setInterval(() => { if (mainStylesheetReady()) finish() }, 50)
function finish() { clearInterval(poll); clearTimeout(timer); resolve() }
})
const revealApplication = async (force = false) => {
if (revealStarted || (!force && !applicationReady)) return
const overlay = document.getElementById("startup-transition")
if (!overlay) return
revealStarted = true
// Non-render-blocking stylesheets may still be pending; never fade to unstyled content
await waitForStylesheets()
requestAnimationFrame(() => requestAnimationFrame(() => {
const removeOverlay = () => overlay.remove()
overlay.classList.add("startup-transition-leaving")
overlay.addEventListener("transitionend", removeOverlay, { once: true })
setTimeout(removeOverlay, 600)
}))
}
document.addEventListener("webui-extensions-loaded", () => {
applicationReady = true
revealApplication()
}, { once: true })
const loadMaterialIcons = () => {
const fontLoad = document.fonts?.load
? document.fonts.load('24px "Material Symbols Outlined"')
: Promise.resolve([])
fontLoad.then((faces) => {
if (faces.length > 0) root.classList.add("material-icons-ready")
}).catch((error) => {
console.warn("Material Symbols font unavailable.", error)
})
}
// Firefox can resolve FontFaceSet.load() with no matching faces when
// it runs while the splash is replacing the document. Wait until
// the replacement document is parsed and its font set is stable.
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", loadMaterialIcons, { once: true })
} else {
loadMaterialIcons()
}
setTimeout(() => revealApplication(true), 8000)
})()
</script>
<link rel="preload" as="style" href="/vendor/fonts/fonts.css" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" as="style" href="index.css" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" as="style" href="css/loading-indicators.css" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" as="style" href="css/messages.css" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" as="style" href="components/messages/action-buttons/simple-action-buttons.css" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" as="style" href="components/messages/process-group/process-group.css" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" as="style" href="css/toast.css" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" as="style" href="css/settings.css" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" as="style" href="css/modals.css" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" as="style" href="css/surfaces.css" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" as="style" href="css/scheduler-datepicker.css" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" as="style" href="css/scheduler.css" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" as="style" href="css/notification.css" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" as="style" href="css/buttons.css" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" as="style" href="css/overflow.css" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" as="style" href="css/tables.css" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" as="style" href="components/canvas/right-canvas.css" onload="this.onload=null;this.rel='stylesheet'">
<!-- Flatpickr for datetime picker -->
<link rel="preload" as="style" href="vendor/flatpickr/flatpickr.min.css" onload="this.onload=null;this.rel='stylesheet'">
<script defer src="vendor/flatpickr/flatpickr.min.js"></script>
<script>
globalThis.safeCall = function (name, ...args) {
if (window[name]) window[name](...args)
}
</script>
<script type="module" src="index.js"></script>
<!-- Bootstrap JS (only for logic, importing bundled CSS => UI conflicts) -->
<script defer src="vendor/bootstrap/bootstrap.bundle.min.js"></script>
<!-- Then load Alpine.js -->
<script defer src="vendor/ace-min/ace.js"></script>
<script defer src="vendor/qrcode.min.js"></script>
<script type="module" src="js/initFw.js"></script>
<link rel="preload" as="style" href="vendor/ace-min/ace.min.css" onload="this.onload=null;this.rel='stylesheet'">
<!-- KaTeX CSS -->
<link rel="preload" as="style" href="vendor/katex/katex.min.css" crossorigin="anonymous" onload="this.onload=null;this.rel='stylesheet'">
<!-- KaTeX javascript -->
<script defer src="vendor/katex/katex.min.js" crossorigin="anonymous"></script>
<script defer src="vendor/katex/katex.auto-render.min.js" crossorigin="anonymous"></script>
<!-- Link the PWA manifest file -->
<link rel="manifest" href="js/manifest.json">
<script>
// Expose git info for sidebar component
globalThis.gitinfo = { version: "{{version_no}}", commit_time: "{{version_time}}" };
// Expose runtime info for frontend components (development gating, runtime-scoped cookies).
globalThis.runtimeInfo = {
...(globalThis.runtimeInfo || {}),
id: "{{runtime_id}}",
isDevelopment: "{{runtime_is_development}}" === "true",
loggedIn: "{{logged_in}}" === "true",
timezone: "{{user_timezone_setting}}",
timeFormat: "{{user_time_format_setting}}",
uiControlVisibility: {{user_ui_control_visibility}},
webuiExtensions: {{webui_extension_manifest}},
};
</script>
<!-- Plugin head injections (scripts, stylesheets) -->
<x-extension id="page-head"></x-extension>
</head>
<body class="dark-mode device-pointer" x-data>
<script>
try {
if (localStorage.getItem("darkMode") === "false") document.body.classList.replace("dark-mode", "light-mode")
} catch (_) {}
</script>
<div class="container">
<!-- Sidebar Overlay -->
<div id="sidebar-overlay" x-data>
<template x-if="$store.sidebar">
<div class="sidebar-overlay" :class="{'visible': $store.sidebar.isOpen && $store.sidebar.isMobile()}" @click="$store.sidebar.close()"></div>
</template>
</div>
<!-- Left Sidebar (Header Icons, Quick Actions, Tabs, Chats, Tasks) -->
<x-component path="sidebar/left-sidebar.html"></x-component>
<!-- Right Panel (Message History and Input Section) -->
<div id="right-panel" class="panel"
:class="{ 'chat-active': $store.welcomeStore && !$store.welcomeStore.isVisible }">
<!-- top section with time, status etc. -->
<x-component path="chat/top-section/chat-top.html"></x-component>
<!-- Welcome Screen Component -->
<div x-data>
<template x-if="$store.welcomeStore && $store.welcomeStore.isVisible">
<div class="welcome-host">
<x-component path="welcome/welcome-screen.html"></x-component>
</div>
</template>
</div>
<!-- Message History (actual messages) -->
<div id="chat-area-wrapper" x-data x-show="!$store.welcomeStore || !$store.welcomeStore.isVisible">
<div id="chat-history"></div>
<div id="chat-loading-splash"
class="message-window-loader is-loading chat-loading-splash"
role="status"
aria-live="polite"
hidden>
<span class="loading-indicator-label">Loading chat</span>
</div>
<!-- Chat Navigation Buttons (floating over chat area) -->
<div id="chat-nav-buttons" aria-label="Chat navigation" x-cloak>
<button class="btn-icon-action" title="Scroll to top" @click="$store.chatNavigation.scrollToTop()">
<x-icon name="vertical_align_top"></x-icon>
</button>
<button class="btn-icon-action" title="Previous user message" @click="$store.chatNavigation.scrollToPrevUserMessage()">
<x-icon name="keyboard_arrow_up"></x-icon>
</button>
<button class="btn-icon-action" title="Next user message" @click="$store.chatNavigation.scrollToNextUserMessage()">
<x-icon name="keyboard_arrow_down"></x-icon>
</button>
<button class="btn-icon-action" title="Scroll to bottom" @click="$store.chatNavigation.scrollToBottom()">
<x-icon name="vertical_align_bottom"></x-icon>
</button>
</div>
</div>
<!-- NEW: Toast Stack Component -->
<div style="position: relative; height: 0;">
<x-component path="notifications/notification-toast-stack.html"></x-component>
</div>
<div id="toast" class="toast">
<div class="toast__content">
<div class="toast__title"></div>
<div class="toast__separator"></div>
<div class="toast__message"></div>
</div>
<button class="toast__copy" style="display: none;">Copy</button>
<button class="toast__close">Close</button>
</div>
<!-- Chat Input Area -->
<div x-data>
<template x-if="!$store.welcomeStore || !$store.welcomeStore.isVisible">
<div>
<!-- Progress Bar -->
<x-component path="chat/input/progress.html"></x-component>
<!-- Input Section -->
<x-component path="chat/input/chat-bar.html"></x-component>
</div>
</template>
</div>
</div>
<x-component path="canvas/right-canvas.html"></x-component>
</div>
<!-- Drag and Drop Overlay Component -->
<x-component path="chat/attachments/dragDropOverlay.html"></x-component>
</body>
</html>