feat webui react+vite responsive app with clawgo api adapter

This commit is contained in:
DBT
2026-02-25 12:59:16 +00:00
parent b435589060
commit b2ac3afcf4
11 changed files with 2009 additions and 0 deletions

132
webui/src/App.tsx Normal file
View File

@@ -0,0 +1,132 @@
import { useEffect, useMemo, useState } from 'react'
type ChatItem = { role: 'user' | 'assistant'; text: string }
type Session = { key: string; title: string }
const defaultSessions: Session[] = [{ key: 'webui:default', title: 'Default' }]
export function App() {
const [token, setToken] = useState('')
const [cfgText, setCfgText] = useState('{}')
const [sessions, setSessions] = useState<Session[]>(defaultSessions)
const [active, setActive] = useState('webui:default')
const [chat, setChat] = useState<Record<string, ChatItem[]>>({ 'webui:default': [] })
const [msg, setMsg] = useState('')
const [nodes, setNodes] = useState<string>('[]')
const activeChat = useMemo(() => chat[active] || [], [chat, active])
const q = token ? `?token=${encodeURIComponent(token)}` : ''
async function loadConfig() {
const r = await fetch(`/webui/api/config${q}`)
setCfgText(await r.text())
}
async function saveConfig() {
const parsed = JSON.parse(cfgText)
const r = await fetch(`/webui/api/config${q}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(parsed),
})
alert(await r.text())
}
async function refreshNodes() {
const payload = {
session: active,
message: '调用nodes工具action=status并输出JSON。',
}
const r = await fetch(`/webui/api/chat${q}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
const t = await r.text()
setNodes(t)
}
async function send() {
let media = ''
const input = document.getElementById('file') as HTMLInputElement | null
const f = input?.files?.[0]
if (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 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),
})
const t = await r.text()
setChat((prev) => ({ ...prev, [active]: [...(prev[active] || []), { role: 'assistant', text: t }] }))
if (input) input.value = ''
}
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]: [] }))
}
useEffect(() => {
loadConfig().catch(() => {})
}, [])
return (
<div className="app">
<header className="topbar">
<strong>ClawGo WebUI (React/Vite)</strong>
<input value={token} onChange={(e) => setToken(e.target.value)} placeholder="gateway token" />
</header>
<div className="layout">
<aside className="panel sessions">
<div className="panel-title">Sessions <button onClick={addSession}>+</button></div>
{sessions.map((s) => (
<button key={s.key} className={s.key === active ? 'active' : ''} onClick={() => setActive(s.key)}>
{s.title}
</button>
))}
</aside>
<main className="panel chat">
<div className="panel-title">Chat</div>
<div className="chatlog">
{activeChat.map((m, i) => (
<div key={i} className={`bubble ${m.role}`}>
{m.text}
</div>
))}
</div>
<div className="composer">
<input value={msg} onChange={(e) => setMsg(e.target.value)} placeholder="Type message..." />
<input id="file" type="file" />
<button onClick={send}>Send</button>
</div>
</main>
<section className="panel right">
<div className="panel-title">Config</div>
<div className="row"><button onClick={loadConfig}>Load</button><button onClick={saveConfig}>Save+Reload</button></div>
<textarea value={cfgText} onChange={(e) => setCfgText(e.target.value)} />
<div className="panel-title">Nodes</div>
<div className="row"><button onClick={refreshNodes}>Refresh</button></div>
<pre>{nodes}</pre>
</section>
</div>
</div>
)
}

10
webui/src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './App'
import './styles.css'
createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)

21
webui/src/styles.css Normal file
View File

@@ -0,0 +1,21 @@
*{box-sizing:border-box} body{margin:0;font-family:Inter,system-ui,Segoe UI,Arial;background:#0b1020;color:#e5e7eb}
.app{height:100vh;display:flex;flex-direction:column}
.topbar{display:flex;gap:12px;align-items:center;padding:10px 14px;background:#111827;border-bottom:1px solid #1f2937}
.topbar input{margin-left:auto;min-width:260px;background:#0b1220;color:#fff;border:1px solid #374151;padding:6px 8px;border-radius:8px}
.layout{flex:1;display:grid;grid-template-columns:220px 1fr 360px;gap:10px;padding:10px;min-height:0}
.panel{background:#111827;border:1px solid #1f2937;border-radius:12px;padding:10px;min-height:0;display:flex;flex-direction:column}
.panel-title{font-weight:700;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center}
.sessions button{width:100%;margin-bottom:6px;background:#0f172a;color:#d1d5db;border:1px solid #374151;padding:8px;border-radius:8px;text-align:left}
.sessions button.active{background:#1d4ed8;color:white}
.chatlog{flex:1;overflow:auto;padding:6px;background:#0b1220;border:1px solid #1f2937;border-radius:10px}
.bubble{max-width:88%;padding:8px 10px;border-radius:10px;margin:6px 0;white-space:pre-wrap}
.bubble.user{margin-left:auto;background:#1d4ed8}
.bubble.assistant{margin-right:auto;background:#1f2937}
.composer{display:grid;grid-template-columns:1fr auto auto;gap:8px;margin-top:8px}
.composer input{background:#0b1220;color:#fff;border:1px solid #374151;padding:8px;border-radius:8px}
button{cursor:pointer;border:1px solid #374151;background:#0f172a;color:#fff;padding:6px 10px;border-radius:8px}
.row{display:flex;gap:8px;margin-bottom:8px}
textarea{width:100%;min-height:180px;background:#0b1220;color:#e5e7eb;border:1px solid #374151;border-radius:8px;padding:8px}
pre{flex:1;overflow:auto;background:#0b1220;border:1px solid #374151;border-radius:8px;padding:8px;white-space:pre-wrap}
@media (max-width: 1024px){.layout{grid-template-columns:200px 1fr}.right{grid-column:1 / span 2;max-height:42vh}}
@media (max-width: 768px){.layout{grid-template-columns:1fr}.sessions{order:2}.chat{order:1;min-height:50vh}.right{order:3}.topbar input{min-width:160px;width:52%}}