From 413369aec900128c420fcb7f6c274326b3e803d3 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Fri, 12 Jun 2026 21:58:59 +0800 Subject: [PATCH] feat: refresh dashboard homepage --- config/systemd/user/xworkspace-api.service | 14 + dashboard/src/components/AppShell.tsx | 124 ++ dashboard/src/components/ArchPipeline.tsx | 164 ++ dashboard/src/components/EmbedView.tsx | 31 + dashboard/src/components/Icon.tsx | 55 + dashboard/src/components/PanelsRow.tsx | 67 + dashboard/src/components/Sidebar.tsx | 91 + dashboard/src/components/StatusAggregate.tsx | 77 + dashboard/src/components/TerminalDrawer.tsx | 79 + dashboard/src/components/Topbar.tsx | 59 + dashboard/src/components/WorkspaceHome.tsx | 29 + dashboard/src/components/WorkspaceTabs.tsx | 50 + dashboard/src/lib/api.ts | 18 + dashboard/src/lib/data.ts | 213 ++ dashboard/src/main.tsx | 576 +----- dashboard/src/styles.css | 1920 +++++++++++++++++- dashboard/tsconfig.json | 6 +- dashboard/vite.config.ts | 6 + 18 files changed, 3004 insertions(+), 575 deletions(-) create mode 100644 config/systemd/user/xworkspace-api.service create mode 100644 dashboard/src/components/AppShell.tsx create mode 100644 dashboard/src/components/ArchPipeline.tsx create mode 100644 dashboard/src/components/EmbedView.tsx create mode 100644 dashboard/src/components/Icon.tsx create mode 100644 dashboard/src/components/PanelsRow.tsx create mode 100644 dashboard/src/components/Sidebar.tsx create mode 100644 dashboard/src/components/StatusAggregate.tsx create mode 100644 dashboard/src/components/TerminalDrawer.tsx create mode 100644 dashboard/src/components/Topbar.tsx create mode 100644 dashboard/src/components/WorkspaceHome.tsx create mode 100644 dashboard/src/components/WorkspaceTabs.tsx create mode 100644 dashboard/src/lib/api.ts create mode 100644 dashboard/src/lib/data.ts diff --git a/config/systemd/user/xworkspace-api.service b/config/systemd/user/xworkspace-api.service new file mode 100644 index 0000000..12aa627 --- /dev/null +++ b/config/systemd/user/xworkspace-api.service @@ -0,0 +1,14 @@ +[Unit] +Description=XWorkspace status API +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory=%h/xworkspace-console/api +ExecStart=/usr/local/go/bin/go run . +Restart=always +RestartSec=2 + +[Install] +WantedBy=default.target diff --git a/dashboard/src/components/AppShell.tsx b/dashboard/src/components/AppShell.tsx new file mode 100644 index 0000000..654a4ee --- /dev/null +++ b/dashboard/src/components/AppShell.tsx @@ -0,0 +1,124 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; +import { agents, customWorkspaceTabs, initialTabs, labelsEn, labelsZh, mockServices, tasks } from '@/lib/data'; +import type { NavItem, Service, Tab } from '@/lib/data'; +import { fetchServices } from '@/lib/api'; +import { Sidebar } from './Sidebar'; +import { Topbar } from './Topbar'; +import { WorkspaceTabs } from './WorkspaceTabs'; +import { WorkspaceHome } from './WorkspaceHome'; +import { EmbedView } from './EmbedView'; + +export function AppShell() { + const [selectedTab, setSelectedTab] = useState('workspace'); + const [tabs, setTabs] = useState(initialTabs); + const [services, setServices] = useState(null); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [language, setLanguage] = useState<'en' | 'zh'>('en'); + const [theme, setTheme] = useState<'light' | 'dark'>('light'); + const [remoteMode, setRemoteMode] = useState(true); + + useEffect(() => { + fetchServices().then((data) => data && setServices(data)); + }, []); + + useEffect(() => { + const stored = window.localStorage.getItem('xws-remote-mode'); + if (stored !== null) { + setRemoteMode(stored === '1'); + } else if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + setRemoteMode(true); + } + }, []); + + const toggleRemoteMode = () => { + setRemoteMode((value) => { + window.localStorage.setItem('xws-remote-mode', value ? '0' : '1'); + return !value; + }); + }; + + useEffect(() => { + const onKey = (event: KeyboardEvent) => { + if (!(event.metaKey || event.ctrlKey)) return; + if (event.key >= '1' && event.key <= '9') { + const index = Number(event.key) - 1; + setTabs((existingTabs) => { + if (existingTabs[index]) setSelectedTab(existingTabs[index].id); + return existingTabs; + }); + event.preventDefault(); + } + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, []); + + const currentServices = services ?? mockServices; + const selected = tabs.find((tab) => tab.id === selectedTab); + const labels = language === 'zh' ? labelsZh : labelsEn; + + const summary = useMemo(() => { + const runningServices = currentServices.filter((service) => service.state === 'Running').length; + const runningAgents = agents.filter((agent) => agent.state === 'Running').length; + const runningTasks = tasks.filter((task) => task[2] === 'Running').length; + return { runningServices, runningAgents, runningTasks }; + }, [currentServices]); + + const openTab = (item: NavItem | Tab) => { + setTabs((existingTabs) => { + if (existingTabs.some((tab) => tab.id === item.id)) return existingTabs; + return [...existingTabs, { ...item, closable: true }]; + }); + setSelectedTab(item.id); + }; + + const closeTab = (tabId: string) => { + setTabs((existingTabs) => { + const nextTabs = existingTabs.filter((tab) => tab.id !== tabId); + if (tabId === selectedTab) setSelectedTab(nextTabs[nextTabs.length - 1]?.id ?? 'workspace'); + return nextTabs; + }); + }; + + const addCustomTab = () => { + const nextTab = customWorkspaceTabs.find((tab) => !tabs.some((open) => open.id === tab.id)) ?? customWorkspaceTabs[0]; + openTab(nextTab); + }; + + return ( +
+ setSidebarCollapsed((value) => !value)} + onOpen={openTab} + onToggleLanguage={() => setLanguage((value) => (value === 'en' ? 'zh' : 'en'))} + onToggleTheme={() => setTheme((value) => (value === 'light' ? 'dark' : 'light'))} + theme={theme} + remoteMode={remoteMode} + onToggleRemoteMode={toggleRemoteMode} + /> + +
+ setSidebarCollapsed((value) => !value)} + /> + + + + {selected?.kind === 'embed' && selected.id !== 'workspace' ? ( + setSelectedTab('workspace')} /> + ) : ( + + )} +
+
+ ); +} diff --git a/dashboard/src/components/ArchPipeline.tsx b/dashboard/src/components/ArchPipeline.tsx new file mode 100644 index 0000000..973daa2 --- /dev/null +++ b/dashboard/src/components/ArchPipeline.tsx @@ -0,0 +1,164 @@ +'use client'; + +import { useState } from 'react'; +import { acpAgents, agents, findServiceDef, serviceRegistry, skillGroups } from '@/lib/data'; +import type { Labels, NavItem, Service } from '@/lib/data'; +import { Icon } from './Icon'; + +export function ArchPipeline({ + labels, + services, + onOpenService, +}: { + labels: Labels; + services: Service[]; + onOpenService: (item: NavItem) => void; +}) { + const [skillsOpen, setSkillsOpen] = useState(false); + + const stateOf = (id: string): Service['state'] | undefined => { + const def = serviceRegistry.find((item) => item.id === id); + const service = services.find((entry) => def?.match?.some((token) => entry.name.toLowerCase().includes(token))); + return service?.state; + }; + const dot = (state?: Service['state']) => ( + + ); + const runningAgents = agents.filter((agent) => agent.state === 'Running').map((agent) => agent.name.split(' ')[0]); + const totalSkills = skillGroups.reduce((count, group) => count + group.skills.length, 0); + const externalModels = [ + { name: 'GPT-5.5', mark: '◎', tone: 'openai' }, + { name: 'DeepSeek V4', mark: 'D', tone: 'deepseek' }, + { name: 'Gemini 3.1', mark: '✦', tone: 'gemini' }, + { name: 'GLM 5', mark: 'Z', tone: 'glm' }, + { name: 'MiniMax', mark: '〽', tone: 'minimax' }, + { name: 'Kimi', mark: 'K', tone: 'kimi' }, + { name: 'Claude', mark: 'AI', tone: 'claude' }, + { name: 'and more', mark: '…', tone: 'more' }, + ]; + + return ( +
+ + + + + +
+ + User Entry + APP Chat / Web Chat
XWorkmate Bridge
+
+ + + +
+
+ 2 + {labels.agentBand} + 4 {labels.sessions} · 8 {labels.workers} + {dot()} +
+
+ {['Main Agent', 'Memory', 'Scheduler', 'SubAgents', 'ACP Router'].map((item, index) => ( + {item} + ))} +
+
+
+ {labels.memoryCard} + QMD (Vector Search) + MEMORY.md + memory/*.md (Logs) + Session Index +
+
+ {labels.acpCard} +
+ {acpAgents.map((agent) => ( + {agent} + ))} +
+
+
+
+ + + + + + + +
+
+ 5 + External Model Services +
+
+ {externalModels.map((model) => ( + + {model.mark} + {model.name} + + ))} +
+
+
+ ); +} + +export { findServiceDef }; diff --git a/dashboard/src/components/EmbedView.tsx b/dashboard/src/components/EmbedView.tsx new file mode 100644 index 0000000..654bc6a --- /dev/null +++ b/dashboard/src/components/EmbedView.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { useState } from 'react'; +import type { Tab } from '@/lib/data'; +import { Icon } from './Icon'; + +export function EmbedView({ tab, onBack }: { tab: Tab; onBack: () => void }) { + const [reloadKey, setReloadKey] = useState(0); + return ( +
+
+
+ + {tab.label} + {tab.href} +
+ + + + +
+
+