From 14ca5dd7b48e039db1312eb97db8e32e260ac057 Mon Sep 17 00:00:00 2001 From: xiaoxixi Date: Fri, 24 Jul 2026 10:01:07 +0800 Subject: [PATCH] feat(webui): app shell with flat nav, activity spine, and theme init --- src/gateway/http.rs | 7 ++++ src/gateway/mod.rs | 1 + webui/index.html | 3 +- webui/public/theme-init.js | 5 +++ webui/src/App.svelte | 65 ++++++++++++++++++++++++-------------- 5 files changed, 56 insertions(+), 25 deletions(-) create mode 100644 webui/public/theme-init.js diff --git a/src/gateway/http.rs b/src/gateway/http.rs index 2d5bb40..16f6295 100644 --- a/src/gateway/http.rs +++ b/src/gateway/http.rs @@ -78,6 +78,13 @@ pub async fn webui_styles() -> Response { ) } +pub async fn webui_theme_init() -> Response { + static_response( + "text/javascript; charset=utf-8", + include_str!(concat!(env!("OUT_DIR"), "/webui/theme-init.js")), + ) +} + const EMBEDDED_FONTS: &[(&str, &[u8])] = &[ ( "space-grotesk-500.woff2", diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 78ede72..d0937e7 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -593,6 +593,7 @@ fn build_router(state: Arc) -> Router { .route("/", routing::get(http::webui_index)) .route("/app.js", routing::get(http::webui_script)) .route("/styles.css", routing::get(http::webui_styles)) + .route("/theme-init.js", routing::get(http::webui_theme_init)) .route("/fonts/{name}", routing::get(http::webui_font)) .route("/health", routing::get(http::health)) .route("/api/auth/status", routing::get(auth::status)) diff --git a/webui/index.html b/webui/index.html index 0c1f0b7..8cd1b98 100644 --- a/webui/index.html +++ b/webui/index.html @@ -4,7 +4,8 @@ - + + PicoBot Console diff --git a/webui/public/theme-init.js b/webui/public/theme-init.js new file mode 100644 index 0000000..fbf6b46 --- /dev/null +++ b/webui/public/theme-init.js @@ -0,0 +1,5 @@ +try { + var t = localStorage.getItem("picobot-theme"); + if (!t) t = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + document.documentElement.dataset.theme = t; +} catch (e) {} diff --git a/webui/src/App.svelte b/webui/src/App.svelte index 6f1453b..c2f33cf 100644 --- a/webui/src/App.svelte +++ b/webui/src/App.svelte @@ -2,7 +2,10 @@ import { onMount } from "svelte"; import { Tooltip } from "bits-ui"; import { api } from "./lib/api.js"; + import { chat } from "./lib/chat.svelte.js"; + import { applyTheme, preferredTheme } from "./lib/theme.js"; import Toast from "./lib/Toast.svelte"; + import ActivitySpine from "./lib/components/ActivitySpine.svelte"; import ChatPage from "./pages/ChatPage.svelte"; import TasksPage from "./pages/TasksPage.svelte"; import MemoryPage from "./pages/MemoryPage.svelte"; @@ -11,11 +14,13 @@ import PairingPage from "./pages/PairingPage.svelte"; const pages = [ - ["chat", "◉", "在线聊天", "与你的 PicoBot 实时对话"], - ["tasks", "⌁", "任务执行", "查看定时任务、运行记录与后台子任务"], - ["memory", "◇", "记忆", "检索 Knowledge 与 Timeline"], - ["logs", "≋", "运行日志", "查看 Gateway 最近的本地日志"], - ["settings", "⚙", "配置", "管理运行配置与助手档案"] + ["chat", "◫", "聊天"], + ["overview", "◉", "概览"], + ["tools", "🧰", "工具&Skills"], + ["logs", "≋", "日志"], + ["memory", "◇", "记忆"], + ["tasks", "⌁", "任务"], + ["settings", "⚙", "配置"] ]; let current = $state("chat"); let menuOpen = $state(false); @@ -42,12 +47,9 @@ menuOpen = false; } - function applyTheme(next) { - theme = next; - document.documentElement.dataset.theme = next; - document.documentElement.style.colorScheme = next; - document.querySelector('meta[name="theme-color"]')?.setAttribute("content", next === "dark" ? "#0d1117" : "#f6f7f9"); - localStorage.setItem("picobot-theme", next); + function toggleTheme() { + theme = theme === "dark" ? "light" : "dark"; + applyTheme(theme); } async function checkAuth() { @@ -66,16 +68,26 @@ health(); } + $effect(() => { + if (authenticated) { + chat.connect(); + } else { + chat.disconnect(); + } + }); + onMount(() => { - const saved = localStorage.getItem("picobot-theme"); - applyTheme(saved === "light" || saved === "dark" ? saved : (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")); + const preferred = preferredTheme(); + applyTheme(preferred); + theme = preferred; checkAuth().then(() => authenticated && health()); - const authRequired = () => authenticated = false; + const authRequired = () => (authenticated = false); window.addEventListener("picobot-auth-required", authRequired); const timer = setInterval(() => authenticated && health(), 30_000); return () => { clearInterval(timer); window.removeEventListener("picobot-auth-required", authRequired); + chat.disconnect(); }; }); @@ -94,21 +106,26 @@ {/each} -
{online ? "运行中" : "不可用"}{version}
- -
-
- -

{meta[2]}

{meta[3]}

- + + +
+ +
+ +

{meta[2]}

{#if current === "chat"} toast.show(text, error)} /> - {:else if current === "tasks"} - {:else if current === "memory"} {:else if current === "logs"} - {:else} toast.show(text, error)} />{/if} + {:else if current === "memory"} + {:else if current === "tasks"} + {:else if current === "settings"} toast.show(text, error)} /> + {:else}
即将上线
{/if}