mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-27 01:11:40 +00:00
feat: add sidebar
This commit is contained in:
parent
7db2703b33
commit
e5d2815b41
41 changed files with 2469 additions and 1391 deletions
89
web/src/contexts/PageHeaderProvider.tsx
Normal file
89
web/src/contexts/PageHeaderProvider.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { useLayoutEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { PageHeaderContext } from "./page-header-context";
|
||||
import { resolvePageTitle } from "@/lib/resolve-page-title";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useI18n } from "@/i18n";
|
||||
|
||||
export function PageHeaderProvider({
|
||||
children,
|
||||
pluginTabs,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
pluginTabs: { path: string; label: string }[];
|
||||
}) {
|
||||
const { pathname } = useLocation();
|
||||
const { t } = useI18n();
|
||||
const [titleOverride, setTitleOverride] = useState<string | null>(null);
|
||||
const [afterTitle, setAfterTitle] = useState<ReactNode>(null);
|
||||
const [end, setEnd] = useState<ReactNode>(null);
|
||||
|
||||
// Clear any per-page title / toolbar slots when the path changes. Child routes
|
||||
// re-fill these on mount via usePageHeader.
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
useLayoutEffect(() => {
|
||||
setTitleOverride(null);
|
||||
setAfterTitle(null);
|
||||
setEnd(null);
|
||||
}, [pathname]);
|
||||
/* eslint-enable react-hooks/set-state-in-effect */
|
||||
|
||||
const defaultTitle = useMemo(
|
||||
() => resolvePageTitle(pathname, t, pluginTabs),
|
||||
[pathname, t, pluginTabs],
|
||||
);
|
||||
const displayTitle = titleOverride ?? defaultTitle;
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
setAfterTitle,
|
||||
setEnd,
|
||||
setTitle: setTitleOverride,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<PageHeaderContext.Provider value={value}>
|
||||
<div className="flex min-h-0 w-full min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<header
|
||||
className={cn(
|
||||
"z-1 box-border h-14 shrink-0 border-b border-current/20",
|
||||
"bg-background-base/40 backdrop-blur-sm",
|
||||
"overflow-hidden",
|
||||
)}
|
||||
role="banner"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-full min-w-0 flex-1 flex-col justify-center gap-2",
|
||||
"px-3 py-2 sm:px-6",
|
||||
"min-h-14 sm:min-h-0 sm:flex-row sm:items-center sm:gap-3 sm:py-0",
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 sm:gap-3">
|
||||
<h1
|
||||
className="font-expanded min-w-0 truncate text-sm font-bold tracking-[0.08em] text-midground"
|
||||
style={{ mixBlendMode: "plus-lighter" }}
|
||||
>
|
||||
{displayTitle}
|
||||
</h1>
|
||||
{afterTitle}
|
||||
</div>
|
||||
|
||||
{end ? (
|
||||
<div className="flex w-full min-w-0 justify-end sm:max-w-md sm:flex-1">
|
||||
{end}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</header>
|
||||
<main
|
||||
className="min-h-0 w-full min-w-0 flex-1 overflow-y-auto overflow-x-hidden"
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</PageHeaderContext.Provider>
|
||||
);
|
||||
}
|
||||
120
web/src/contexts/SystemActions.tsx
Normal file
120
web/src/contexts/SystemActions.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import type { ActionStatusResponse } from "@/lib/api";
|
||||
import { Toast } from "@/components/Toast";
|
||||
import { useI18n } from "@/i18n";
|
||||
import {
|
||||
SystemActionsContext,
|
||||
type SystemAction,
|
||||
} from "./system-actions-context";
|
||||
|
||||
const ACTION_NAMES: Record<SystemAction, string> = {
|
||||
restart: "gateway-restart",
|
||||
update: "hermes-update",
|
||||
};
|
||||
|
||||
export function SystemActionsProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [pendingAction, setPendingAction] = useState<SystemAction | null>(null);
|
||||
const [activeAction, setActiveAction] = useState<SystemAction | null>(null);
|
||||
const [actionStatus, setActionStatus] = useState<ActionStatusResponse | null>(
|
||||
null,
|
||||
);
|
||||
const [toast, setToast] = useState<ToastState | null>(null);
|
||||
const { t } = useI18n();
|
||||
|
||||
useEffect(() => {
|
||||
if (!toast) return;
|
||||
const timer = setTimeout(() => setToast(null), 4000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [toast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeAction) return;
|
||||
const name = ACTION_NAMES[activeAction];
|
||||
let cancelled = false;
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const resp = await api.getActionStatus(name);
|
||||
if (cancelled) return;
|
||||
setActionStatus(resp);
|
||||
if (!resp.running) {
|
||||
const ok = resp.exit_code === 0;
|
||||
setToast({
|
||||
type: ok ? "success" : "error",
|
||||
message: ok
|
||||
? t.status.actionFinished
|
||||
: `${t.status.actionFailed} (exit ${resp.exit_code ?? "?"})`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// transient fetch error; keep polling
|
||||
}
|
||||
if (!cancelled) setTimeout(poll, 1500);
|
||||
};
|
||||
|
||||
poll();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [activeAction, t.status.actionFinished, t.status.actionFailed]);
|
||||
|
||||
const runAction = useCallback(
|
||||
async (action: SystemAction) => {
|
||||
setPendingAction(action);
|
||||
setActionStatus(null);
|
||||
try {
|
||||
if (action === "restart") {
|
||||
await api.restartGateway();
|
||||
} else {
|
||||
await api.updateHermes();
|
||||
}
|
||||
setActiveAction(action);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
setToast({
|
||||
type: "error",
|
||||
message: `${t.status.actionFailed}: ${detail}`,
|
||||
});
|
||||
} finally {
|
||||
setPendingAction(null);
|
||||
}
|
||||
},
|
||||
[t.status.actionFailed],
|
||||
);
|
||||
|
||||
const dismissLog = useCallback(() => {
|
||||
setActiveAction(null);
|
||||
setActionStatus(null);
|
||||
}, []);
|
||||
|
||||
const isRunning = activeAction !== null && actionStatus?.running !== false;
|
||||
const isBusy = pendingAction !== null || isRunning;
|
||||
|
||||
return (
|
||||
<SystemActionsContext.Provider
|
||||
value={{
|
||||
actionStatus,
|
||||
activeAction,
|
||||
dismissLog,
|
||||
isBusy,
|
||||
isRunning,
|
||||
pendingAction,
|
||||
runAction,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<Toast toast={toast} />
|
||||
</SystemActionsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
interface ToastState {
|
||||
message: string;
|
||||
type: "success" | "error";
|
||||
}
|
||||
12
web/src/contexts/page-header-context.ts
Normal file
12
web/src/contexts/page-header-context.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { createContext } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export interface PageHeaderContextValue {
|
||||
setAfterTitle: (node: ReactNode) => void;
|
||||
setEnd: (node: ReactNode) => void;
|
||||
setTitle: (title: string | null) => void;
|
||||
}
|
||||
|
||||
export const PageHeaderContext = createContext<PageHeaderContextValue | null>(
|
||||
null,
|
||||
);
|
||||
18
web/src/contexts/system-actions-context.ts
Normal file
18
web/src/contexts/system-actions-context.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { createContext } from "react";
|
||||
import type { ActionStatusResponse } from "@/lib/api";
|
||||
|
||||
export const SystemActionsContext = createContext<SystemActionsState | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export type SystemAction = "restart" | "update";
|
||||
|
||||
export interface SystemActionsState {
|
||||
actionStatus: ActionStatusResponse | null;
|
||||
activeAction: SystemAction | null;
|
||||
dismissLog: () => void;
|
||||
isBusy: boolean;
|
||||
isRunning: boolean;
|
||||
pendingAction: SystemAction | null;
|
||||
runAction: (action: SystemAction) => Promise<void>;
|
||||
}
|
||||
10
web/src/contexts/usePageHeader.ts
Normal file
10
web/src/contexts/usePageHeader.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { useContext } from "react";
|
||||
import { PageHeaderContext, type PageHeaderContextValue } from "./page-header-context";
|
||||
|
||||
export function usePageHeader(): PageHeaderContextValue {
|
||||
const ctx = useContext(PageHeaderContext);
|
||||
if (!ctx) {
|
||||
throw new Error("usePageHeader must be used within a PageHeaderProvider");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
15
web/src/contexts/useSystemActions.ts
Normal file
15
web/src/contexts/useSystemActions.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { useContext } from "react";
|
||||
import {
|
||||
SystemActionsContext,
|
||||
type SystemActionsState,
|
||||
} from "./system-actions-context";
|
||||
|
||||
export function useSystemActions(): SystemActionsState {
|
||||
const ctx = useContext(SystemActionsContext);
|
||||
if (!ctx) {
|
||||
throw new Error(
|
||||
"useSystemActions must be used within a SystemActionsProvider",
|
||||
);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue