mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat: web UI dashboard for managing Hermes Agent (#8756)
* feat: web UI dashboard for managing Hermes Agent (salvage of #8204/#7621) Adds an embedded web UI dashboard accessible via `hermes web`: - Status page: agent version, active sessions, gateway status, connected platforms - Config editor: schema-driven form with tabbed categories, import/export, reset - API Keys page: set, clear, and view redacted values with category grouping - Sessions, Skills, Cron, Logs, and Analytics pages Backend: - hermes_cli/web_server.py: FastAPI server with REST endpoints - hermes_cli/config.py: reload_env() utility for hot-reloading .env - hermes_cli/main.py: `hermes web` subcommand (--port, --host, --no-open) - cli.py / commands.py: /reload slash command for .env hot-reload - pyproject.toml: [web] optional dependency extra (fastapi + uvicorn) - Both update paths (git + zip) auto-build web frontend when npm available Frontend: - Vite + React + TypeScript + Tailwind v4 SPA in web/ - shadcn/ui-style components, Nous design language - Auto-refresh status page, toast notifications, masked password inputs Security: - Path traversal guard (resolve().is_relative_to()) on SPA file serving - CORS localhost-only via allow_origin_regex - Generic error messages (no internal leak), SessionDB handles closed properly Tests: 47 tests covering reload_env, redact_key, API endpoints, schema generation, path traversal, category merging, internal key stripping, and full config round-trip. Original work by @austinpickett (PR #1813), salvaged by @kshitijk4poor (PR #7621 → #8204), re-salvaged onto current main with stale-branch regressions removed. * fix(web): clean up status page cards, always rebuild on `hermes web` - Remove config version migration alert banner from status page - Remove config version card (internal noise, not surfaced in TUI) - Reorder status cards: Agent → Gateway → Active Sessions (3-col grid) - `hermes web` now always rebuilds from source before serving, preventing stale web_dist when editing frontend files * feat(web): full-text search across session messages - Add GET /api/sessions/search endpoint backed by FTS5 - Auto-append prefix wildcards so partial words match (e.g. 'nimb' → 'nimby') - Debounced search (300ms) with spinner in the search icon slot - Search results show FTS5 snippets with highlighted match delimiters - Expanding a search hit auto-scrolls to the first matching message - Matching messages get a warning ring + 'match' badge - Inline term highlighting within Markdown (text, bold, italic, headings, lists) - Clear button (x) on search input for quick reset --------- Co-authored-by: emozilla <emozilla@nousresearch.com>
This commit is contained in:
parent
c052cf0eea
commit
e2a9b5369f
55 changed files with 10187 additions and 3 deletions
117
web/src/App.tsx
Normal file
117
web/src/App.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { Activity, BarChart3, Clock, FileText, KeyRound, MessageSquare, Package, Settings } from "lucide-react";
|
||||
import StatusPage from "@/pages/StatusPage";
|
||||
import ConfigPage from "@/pages/ConfigPage";
|
||||
import EnvPage from "@/pages/EnvPage";
|
||||
import SessionsPage from "@/pages/SessionsPage";
|
||||
import LogsPage from "@/pages/LogsPage";
|
||||
import AnalyticsPage from "@/pages/AnalyticsPage";
|
||||
import CronPage from "@/pages/CronPage";
|
||||
import SkillsPage from "@/pages/SkillsPage";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ id: "status", label: "Status", icon: Activity },
|
||||
{ id: "sessions", label: "Sessions", icon: MessageSquare },
|
||||
{ id: "analytics", label: "Analytics", icon: BarChart3 },
|
||||
{ id: "logs", label: "Logs", icon: FileText },
|
||||
{ id: "cron", label: "Cron", icon: Clock },
|
||||
{ id: "skills", label: "Skills", icon: Package },
|
||||
{ id: "config", label: "Config", icon: Settings },
|
||||
{ id: "env", label: "Keys", icon: KeyRound },
|
||||
] as const;
|
||||
|
||||
type PageId = (typeof NAV_ITEMS)[number]["id"];
|
||||
|
||||
const PAGE_COMPONENTS: Record<PageId, React.FC> = {
|
||||
status: StatusPage,
|
||||
sessions: SessionsPage,
|
||||
analytics: AnalyticsPage,
|
||||
logs: LogsPage,
|
||||
cron: CronPage,
|
||||
skills: SkillsPage,
|
||||
config: ConfigPage,
|
||||
env: EnvPage,
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const [page, setPage] = useState<PageId>("status");
|
||||
const [animKey, setAnimKey] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setAnimKey((k) => k + 1);
|
||||
}, [page]);
|
||||
|
||||
const PageComponent = PAGE_COMPONENTS[page];
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-background text-foreground">
|
||||
{/* Global grain + warm glow (matches landing page) */}
|
||||
<div className="noise-overlay" />
|
||||
<div className="warm-glow" />
|
||||
|
||||
{/* ---- Header with grid-border nav ---- */}
|
||||
<header className="sticky top-0 z-40 border-b border-border bg-background/90 backdrop-blur-sm">
|
||||
<div className="mx-auto flex h-12 max-w-[1400px] items-stretch">
|
||||
{/* Brand */}
|
||||
<div className="flex items-center border-r border-border px-5 shrink-0">
|
||||
<span className="font-collapse text-xl font-bold tracking-wider uppercase blend-lighter">
|
||||
Hermes<br className="hidden sm:inline" /><span className="sm:hidden"> </span>Agent
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Nav grid — Mondwest labels like the landing page nav */}
|
||||
<nav className="flex items-stretch overflow-x-auto scrollbar-none">
|
||||
{NAV_ITEMS.map(({ id, label, icon: Icon }) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={() => setPage(id)}
|
||||
className={`group relative inline-flex items-center gap-1.5 border-r border-border px-4 py-2 font-display text-[0.8rem] tracking-[0.12em] uppercase whitespace-nowrap transition-colors cursor-pointer shrink-0 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring ${
|
||||
page === id
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{label}
|
||||
{/* Hover highlight */}
|
||||
<span className="absolute inset-0 bg-foreground pointer-events-none transition-opacity duration-150 group-hover:opacity-5 opacity-0" />
|
||||
{/* Active indicator — dither bar */}
|
||||
{page === id && (
|
||||
<span className="absolute bottom-0 left-0 right-0 h-px bg-foreground" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Version badge */}
|
||||
<div className="ml-auto flex items-center px-4 text-muted-foreground">
|
||||
<span className="font-display text-[0.7rem] tracking-[0.15em] uppercase opacity-50">
|
||||
Web UI
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main
|
||||
key={animKey}
|
||||
className="relative z-2 mx-auto w-full max-w-[1400px] flex-1 px-6 py-8"
|
||||
style={{ animation: "fade-in 150ms ease-out" }}
|
||||
>
|
||||
<PageComponent />
|
||||
</main>
|
||||
|
||||
{/* ---- Footer ---- */}
|
||||
<footer className="relative z-2 border-t border-border">
|
||||
<div className="mx-auto flex max-w-[1400px] items-center justify-between px-6 py-3">
|
||||
<span className="font-display text-[0.8rem] tracking-[0.12em] uppercase opacity-50">
|
||||
Hermes Agent
|
||||
</span>
|
||||
<span className="font-display text-[0.7rem] tracking-[0.15em] uppercase text-foreground/40">
|
||||
Nous Research
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue