agent-zero/webui/index.html
frdel 93d1131ce1 Unify WebUI icons and fix message collapsing
Introduce the x-icon custom element and migrate first-party WebUI and bundled plugin markup while retaining legacy Material icon compatibility. Defer icon-font readiness until the splash-installed document is parsed so Firefox does not leave icons transparent. Centralize chat message collapse handling, avoid false Show More controls on short user messages, and align grouped-message pagination styling. Update DOX and add regression coverage for the icon and collapse behavior.
2026-07-31 13:32:41 +00:00

275 lines
13 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;
}
#startup-transition[data-theme="light"],
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; }
}
</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>
<link rel="stylesheet" href="vendor/google/google-icons.css">
<script type="module" src="/js/icons.js"></script>
<script>
(() => {
const root = document.documentElement
let applicationReady = false
let revealStarted = false
const revealApplication = (force = false) => {
if (revealStarted || (!force && !applicationReady)) return
const overlay = document.getElementById("startup-transition")
if (!overlay) return
revealStarted = true
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/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>