From dd24023a91247c13459f724aa62e9cd0d8812416 Mon Sep 17 00:00:00 2001 From: DBT Date: Wed, 25 Feb 2026 13:40:45 +0000 Subject: [PATCH] webui: adopt openclaw-like left menu layout and section views --- webui/src/App.tsx | 196 ++++++++++++++++++++----------------------- webui/src/styles.css | 22 +++-- 2 files changed, 103 insertions(+), 115 deletions(-) diff --git a/webui/src/App.tsx b/webui/src/App.tsx index 0f6fadc..a555bbd 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -2,15 +2,15 @@ import { useEffect, useMemo, useState } from 'react' type ChatItem = { role: 'user' | 'assistant'; text: string } type Session = { key: string; title: string } -type CronJob = { id: string; name: string; enabled: boolean; schedule?: { kind?: string } } +type CronJob = { id: string; name: string; enabled: boolean } type Cfg = Record +type View = 'chat' | 'config' | 'cron' | 'nodes' const defaultSessions: Session[] = [{ key: 'webui:default', title: 'Default' }] function getPath(obj: any, path: string, fallback: any = '') { return path.split('.').reduce((acc, k) => (acc && acc[k] !== undefined ? acc[k] : undefined), obj) ?? fallback } - function setPath(obj: any, path: string, value: any) { const keys = path.split('.') const next = JSON.parse(JSON.stringify(obj || {})) @@ -25,6 +25,7 @@ function setPath(obj: any, path: string, value: any) { } export function App() { + const [view, setView] = useState('chat') const [token, setToken] = useState('') const [cfg, setCfg] = useState({}) const [cfgRaw, setCfgRaw] = useState('{}') @@ -36,60 +37,34 @@ export function App() { const [nodes, setNodes] = useState('[]') const [cron, setCron] = useState([]) const activeChat = useMemo(() => chat[active] || [], [chat, active]) - const q = token ? `?token=${encodeURIComponent(token)}` : '' async function loadConfig() { const r = await fetch(`/webui/api/config${q}`) const txt = await r.text() setCfgRaw(txt) - try { - setCfg(JSON.parse(txt)) - } catch { - setCfg({}) - } + try { setCfg(JSON.parse(txt)) } catch { setCfg({}) } } - async function saveConfig() { const payload = showRaw ? JSON.parse(cfgRaw) : cfg const r = await fetch(`/webui/api/config${q}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) alert(await r.text()) } - - const bindText = (path: string) => ({ - value: String(getPath(cfg, path, '')), - onChange: (e: React.ChangeEvent) => setCfg((v) => setPath(v, path, e.target.value)), - }) - const bindNum = (path: string) => ({ - value: Number(getPath(cfg, path, 0)), - onChange: (e: React.ChangeEvent) => setCfg((v) => setPath(v, path, Number(e.target.value || 0))), - }) - const bindBool = (path: string) => ({ - checked: Boolean(getPath(cfg, path, false)), - onChange: (e: React.ChangeEvent) => setCfg((v) => setPath(v, path, e.target.checked)), - }) - async function refreshNodes() { const r = await fetch(`/webui/api/nodes${q}`) const j = await r.json() setNodes(JSON.stringify(j.nodes || [], null, 2)) } - async function refreshCron() { const r = await fetch(`/webui/api/cron${q}`) const j = await r.json() setCron(j.jobs || []) } - async function cronAction(action: 'delete' | 'enable' | 'disable', id: string) { await fetch(`/webui/api/cron${q}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ action, id }), + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, id }), }) await refreshCron() } @@ -99,110 +74,117 @@ export function App() { const input = document.getElementById('file') as HTMLInputElement | null const f = input?.files?.[0] if (f) { - const fd = new FormData() - fd.append('file', f) + const fd = new FormData(); fd.append('file', f) const ur = await fetch(`/webui/api/upload${q}`, { method: 'POST', body: fd }) - const uj = await ur.json() - media = uj.path || '' + const uj = await ur.json(); media = uj.path || '' } - const userText = msg + (media ? ` [file:${media}]` : '') setChat((prev) => ({ ...prev, [active]: [...(prev[active] || []), { role: 'user', text: userText }] })) - const payload = { session: active, message: msg, media } - setMsg('') - const r = await fetch(`/webui/api/chat${q}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: active, message: msg, media }), }) const t = await r.text() setChat((prev) => ({ ...prev, [active]: [...(prev[active] || []), { role: 'assistant', text: t }] })) + setMsg('') if (input) input.value = '' } + const bindText = (path: string) => ({ value: String(getPath(cfg, path, '')), onChange: (e: React.ChangeEvent) => setCfg((v) => setPath(v, path, e.target.value)) }) + const bindNum = (path: string) => ({ value: Number(getPath(cfg, path, 0)), onChange: (e: React.ChangeEvent) => setCfg((v) => setPath(v, path, Number(e.target.value || 0))) }) + const bindBool = (path: string) => ({ checked: Boolean(getPath(cfg, path, false)), onChange: (e: React.ChangeEvent) => setCfg((v) => setPath(v, path, e.target.checked)) }) + function addSession() { const n = `webui:${Date.now()}` const s = { key: n, title: `Session-${sessions.length + 1}` } - setSessions((v) => [...v, s]) - setActive(n) - setChat((prev) => ({ ...prev, [n]: [] })) + setSessions((v) => [...v, s]); setActive(n); setChat((prev) => ({ ...prev, [n]: [] })) } - useEffect(() => { - loadConfig().catch(() => {}) - refreshNodes().catch(() => {}) - refreshCron().catch(() => {}) - }, []) + useEffect(() => { loadConfig().catch(() => {}); refreshNodes().catch(() => {}); refreshCron().catch(() => {}) }, []) return ( -
-
- ClawGo WebUI (React/Vite) - setToken(e.target.value)} placeholder="gateway token" /> +
+
+ ClawGo Control + setToken(e.target.value)} placeholder='gateway token' />
-
-