138 lines
4.5 KiB
Svelte
138 lines
4.5 KiB
Svelte
<script>
|
|
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";
|
|
import LogsPage from "./pages/LogsPage.svelte";
|
|
import SettingsPage from "./pages/SettingsPage.svelte";
|
|
import OverviewPage from "./pages/OverviewPage.svelte";
|
|
import ToolsPage from "./pages/ToolsPage.svelte";
|
|
import PairingPage from "./pages/PairingPage.svelte";
|
|
|
|
const pages = [
|
|
["chat", "◫", "聊天"],
|
|
["overview", "◉", "概览"],
|
|
["tools", "🧰", "工具&Skills"],
|
|
["logs", "≋", "日志"],
|
|
["memory", "◇", "记忆"],
|
|
["tasks", "⌁", "任务"],
|
|
["settings", "⚙", "配置"]
|
|
];
|
|
let current = $state("chat");
|
|
let menuOpen = $state(false);
|
|
let online = $state(false);
|
|
let version = $state("Gateway");
|
|
let theme = $state("dark");
|
|
let authReady = $state(false);
|
|
let authenticated = $state(false);
|
|
let toast = $state();
|
|
const meta = $derived(pages.find(([name]) => name === current) || pages[0]);
|
|
|
|
async function health() {
|
|
try {
|
|
const result = await api("/api/health");
|
|
online = true;
|
|
version = `Gateway v${result.version}`;
|
|
} catch {
|
|
online = false;
|
|
}
|
|
}
|
|
|
|
function selectPage(name) {
|
|
current = name;
|
|
menuOpen = false;
|
|
}
|
|
|
|
function toggleTheme() {
|
|
theme = theme === "dark" ? "light" : "dark";
|
|
applyTheme(theme);
|
|
}
|
|
|
|
async function checkAuth() {
|
|
try {
|
|
const status = await api("/api/auth/status");
|
|
authenticated = status.authenticated || !status.require_pairing;
|
|
} catch {
|
|
authenticated = false;
|
|
} finally {
|
|
authReady = true;
|
|
}
|
|
}
|
|
|
|
function paired() {
|
|
authenticated = true;
|
|
health();
|
|
}
|
|
|
|
$effect(() => {
|
|
if (authenticated) {
|
|
chat.connect();
|
|
} else {
|
|
chat.disconnect();
|
|
}
|
|
});
|
|
|
|
onMount(() => {
|
|
const preferred = preferredTheme();
|
|
applyTheme(preferred);
|
|
theme = preferred;
|
|
checkAuth().then(() => authenticated && health());
|
|
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();
|
|
};
|
|
});
|
|
</script>
|
|
|
|
{#if !authReady}
|
|
<main class="pairing-screen"><div class="auth-loading"><span class="pulse"></span>正在检查设备授权…</div></main>
|
|
{:else if !authenticated}
|
|
<PairingPage onpaired={paired} />
|
|
{:else}
|
|
<Tooltip.Provider delayDuration={350}>
|
|
<div class="shell">
|
|
<aside class:open={menuOpen} class="sidebar">
|
|
<div class="brand"><span class="brand-mark">P</span><div><strong>PicoBot</strong><small>Local agent console</small></div></div>
|
|
<nav>
|
|
{#each pages as page}
|
|
<button class:active={current === page[0]} onclick={() => selectPage(page[0])}><span>{page[1]}</span>{page[2]}</button>
|
|
{/each}
|
|
</nav>
|
|
<div class="gateway-status">
|
|
<i class:online></i>
|
|
<div><b>{online ? "运行中" : "不可用"}</b><small>{version}</small></div>
|
|
<button class="theme-toggle" aria-label={theme === "dark" ? "切换到浅色主题" : "切换到深色主题"} onclick={toggleTheme}>
|
|
<span aria-hidden="true">{theme === "dark" ? "☀" : "☾"}</span><span>{theme === "dark" ? "浅色" : "深色"}</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
<main>
|
|
<ActivitySpine {version} />
|
|
<header class="topbar">
|
|
<button class="menu" aria-label="菜单" onclick={() => (menuOpen = !menuOpen)}>☰</button>
|
|
<h1>{meta[2]}</h1>
|
|
</header>
|
|
{#if current === "chat"}<ChatPage notify={(text, error) => toast.show(text, error)} />
|
|
{:else if current === "logs"}<LogsPage />
|
|
{:else if current === "memory"}<MemoryPage />
|
|
{:else if current === "tasks"}<TasksPage />
|
|
{:else if current === "settings"}<SettingsPage notify={(text, error) => toast.show(text, error)} />
|
|
{:else if current === "overview"}<OverviewPage />
|
|
{:else if current === "tools"}<ToolsPage />
|
|
{:else}<div class="empty-card">即将上线</div>{/if}
|
|
</main>
|
|
</div>
|
|
<Toast bind:this={toast} />
|
|
</Tooltip.Provider>
|
|
{/if}
|