mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix: use grid/cell components
This commit is contained in:
parent
923539a46b
commit
60fd4b7d16
12 changed files with 434 additions and 181 deletions
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Hermes Agent</title>
|
<title>Hermes Agent - Dashboard</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
9
web/package-lock.json
generated
9
web/package-lock.json
generated
|
|
@ -29,6 +29,7 @@
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.4.0",
|
||||||
|
"three": "^0.180.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.56.1",
|
"typescript-eslint": "^8.56.1",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1"
|
||||||
|
|
@ -3840,6 +3841,14 @@
|
||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/three": {
|
||||||
|
"version": "0.180.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz",
|
||||||
|
"integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.4.0",
|
||||||
|
"three": "^0.180.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.56.1",
|
"typescript-eslint": "^8.56.1",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1"
|
||||||
|
|
|
||||||
144
web/src/App.tsx
144
web/src/App.tsx
|
|
@ -1,12 +1,30 @@
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Routes, Route, NavLink, Navigate } from "react-router-dom";
|
import { Routes, Route, NavLink, Navigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
Activity, BarChart3, Clock, FileText, KeyRound,
|
Activity,
|
||||||
MessageSquare, Package, Settings, Puzzle,
|
BarChart3,
|
||||||
Sparkles, Terminal, Globe, Database, Shield,
|
Clock,
|
||||||
Wrench, Zap, Heart, Star, Code, Eye,
|
FileText,
|
||||||
|
KeyRound,
|
||||||
|
MessageSquare,
|
||||||
|
Package,
|
||||||
|
Settings,
|
||||||
|
Puzzle,
|
||||||
|
Sparkles,
|
||||||
|
Terminal,
|
||||||
|
Globe,
|
||||||
|
Database,
|
||||||
|
Shield,
|
||||||
|
Wrench,
|
||||||
|
Zap,
|
||||||
|
Heart,
|
||||||
|
Star,
|
||||||
|
Code,
|
||||||
|
Eye,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { Cell, Grid } from "@nous-research/ui/ui/components/grid/index";
|
||||||
import { SelectionSwitcher } from "@nous-research/ui/ui/components/selection-switcher";
|
import { SelectionSwitcher } from "@nous-research/ui/ui/components/selection-switcher";
|
||||||
|
import { Typography } from "@nous-research/ui/ui/components/typography/index";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Backdrop } from "@/components/Backdrop";
|
import { Backdrop } from "@/components/Backdrop";
|
||||||
import StatusPage from "@/pages/StatusPage";
|
import StatusPage from "@/pages/StatusPage";
|
||||||
|
|
@ -25,8 +43,18 @@ import type { RegisteredPlugin } from "@/plugins";
|
||||||
|
|
||||||
const BUILTIN_NAV: NavItem[] = [
|
const BUILTIN_NAV: NavItem[] = [
|
||||||
{ path: "/", labelKey: "status", label: "Status", icon: Activity },
|
{ path: "/", labelKey: "status", label: "Status", icon: Activity },
|
||||||
{ path: "/sessions", labelKey: "sessions", label: "Sessions", icon: MessageSquare },
|
{
|
||||||
{ path: "/analytics", labelKey: "analytics", label: "Analytics", icon: BarChart3 },
|
path: "/sessions",
|
||||||
|
labelKey: "sessions",
|
||||||
|
label: "Sessions",
|
||||||
|
icon: MessageSquare,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/analytics",
|
||||||
|
labelKey: "analytics",
|
||||||
|
label: "Analytics",
|
||||||
|
icon: BarChart3,
|
||||||
|
},
|
||||||
{ path: "/logs", labelKey: "logs", label: "Logs", icon: FileText },
|
{ path: "/logs", labelKey: "logs", label: "Logs", icon: FileText },
|
||||||
{ path: "/cron", labelKey: "cron", label: "Cron", icon: Clock },
|
{ path: "/cron", labelKey: "cron", label: "Cron", icon: Clock },
|
||||||
{ path: "/skills", labelKey: "skills", label: "Skills", icon: Package },
|
{ path: "/skills", labelKey: "skills", label: "Skills", icon: Package },
|
||||||
|
|
@ -37,17 +65,38 @@ const BUILTIN_NAV: NavItem[] = [
|
||||||
// Plugins can reference any of these by name in their manifest — keeps bundle
|
// Plugins can reference any of these by name in their manifest — keeps bundle
|
||||||
// size sane vs. importing the full lucide-react set.
|
// size sane vs. importing the full lucide-react set.
|
||||||
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
|
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||||
Activity, BarChart3, Clock, FileText, KeyRound,
|
Activity,
|
||||||
MessageSquare, Package, Settings, Puzzle,
|
BarChart3,
|
||||||
Sparkles, Terminal, Globe, Database, Shield,
|
Clock,
|
||||||
Wrench, Zap, Heart, Star, Code, Eye,
|
FileText,
|
||||||
|
KeyRound,
|
||||||
|
MessageSquare,
|
||||||
|
Package,
|
||||||
|
Settings,
|
||||||
|
Puzzle,
|
||||||
|
Sparkles,
|
||||||
|
Terminal,
|
||||||
|
Globe,
|
||||||
|
Database,
|
||||||
|
Shield,
|
||||||
|
Wrench,
|
||||||
|
Zap,
|
||||||
|
Heart,
|
||||||
|
Star,
|
||||||
|
Code,
|
||||||
|
Eye,
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveIcon(name: string): React.ComponentType<{ className?: string }> {
|
function resolveIcon(
|
||||||
|
name: string,
|
||||||
|
): React.ComponentType<{ className?: string }> {
|
||||||
return ICON_MAP[name] ?? Puzzle;
|
return ICON_MAP[name] ?? Puzzle;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildNavItems(builtIn: NavItem[], plugins: RegisteredPlugin[]): NavItem[] {
|
function buildNavItems(
|
||||||
|
builtIn: NavItem[],
|
||||||
|
plugins: RegisteredPlugin[],
|
||||||
|
): NavItem[] {
|
||||||
const items = [...builtIn];
|
const items = [...builtIn];
|
||||||
|
|
||||||
for (const { manifest } of plugins) {
|
for (const { manifest } of plugins) {
|
||||||
|
|
@ -97,28 +146,34 @@ export default function App() {
|
||||||
"bg-background-base/90 backdrop-blur-sm",
|
"bg-background-base/90 backdrop-blur-sm",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="mx-auto flex h-12 max-w-[1600px] items-stretch">
|
<div className="mx-auto flex h-12 max-w-[1600px]">
|
||||||
<div className="flex items-center border-r border-current/20 px-3 sm:px-5 shrink-0">
|
<div className="min-w-0 flex-1 overflow-x-auto scrollbar-none">
|
||||||
<span
|
<Grid
|
||||||
className="font-sans font-bold text-[1.0625rem] sm:text-[1.125rem] leading-[0.95] tracking-[0.0525rem] text-midground"
|
className="h-full !border-t-0 !border-b-0"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `auto repeat(${navItems.length}, auto)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Cell className="flex items-center !p-0 !px-3 sm:!px-5">
|
||||||
|
<Typography
|
||||||
|
className="font-bold text-[1.0625rem] sm:text-[1.125rem] leading-[0.95] tracking-[0.0525rem] text-midground"
|
||||||
style={{ mixBlendMode: "plus-lighter" }}
|
style={{ mixBlendMode: "plus-lighter" }}
|
||||||
>
|
>
|
||||||
Hermes
|
Hermes
|
||||||
<br />
|
<br />
|
||||||
Agent
|
Agent
|
||||||
</span>
|
</Typography>
|
||||||
</div>
|
</Cell>
|
||||||
|
|
||||||
<nav className="flex items-stretch overflow-x-auto scrollbar-none">
|
|
||||||
{navItems.map(({ path, label, labelKey, icon: Icon }) => (
|
{navItems.map(({ path, label, labelKey, icon: Icon }) => (
|
||||||
|
<Cell key={path} className="relative !p-0">
|
||||||
<NavLink
|
<NavLink
|
||||||
key={path}
|
|
||||||
to={path}
|
to={path}
|
||||||
end={path === "/"}
|
end={path === "/"}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
cn(
|
cn(
|
||||||
"group relative inline-flex items-center gap-1.5 shrink-0",
|
"group relative flex h-full w-full items-center gap-1.5",
|
||||||
"border-r border-current/20 px-2.5 sm:px-4 py-2",
|
"px-2.5 sm:px-4 py-2",
|
||||||
"font-mondwest text-[0.65rem] sm:text-[0.8rem] tracking-[0.12em]",
|
"font-mondwest text-[0.65rem] sm:text-[0.8rem] tracking-[0.12em]",
|
||||||
"whitespace-nowrap transition-colors cursor-pointer",
|
"whitespace-nowrap transition-colors cursor-pointer",
|
||||||
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground",
|
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground",
|
||||||
|
|
@ -132,7 +187,11 @@ export default function App() {
|
||||||
<>
|
<>
|
||||||
<Icon className="h-3.5 w-3.5 shrink-0" />
|
<Icon className="h-3.5 w-3.5 shrink-0" />
|
||||||
<span className="hidden sm:inline">
|
<span className="hidden sm:inline">
|
||||||
{labelKey ? (t.app.nav as Record<string, string>)[labelKey] ?? label : label}
|
{labelKey
|
||||||
|
? ((t.app.nav as Record<string, string>)[
|
||||||
|
labelKey
|
||||||
|
] ?? label)
|
||||||
|
: label}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
|
|
@ -150,16 +209,23 @@ export default function App() {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
</Cell>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</Grid>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="ml-auto flex items-center gap-2 border-l border-current/20 px-2 sm:px-4">
|
<Grid className="h-full shrink-0 !border-t-0 !border-b-0">
|
||||||
|
<Cell className="flex items-center gap-2 !p-0 !px-2 sm:!px-4">
|
||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
<span className="hidden sm:inline font-mondwest text-[0.7rem] tracking-[0.15em] opacity-50">
|
<Typography
|
||||||
|
mondwest
|
||||||
|
className="hidden sm:inline text-[0.7rem] tracking-[0.15em] opacity-50"
|
||||||
|
>
|
||||||
{t.app.webUi}
|
{t.app.webUi}
|
||||||
</span>
|
</Typography>
|
||||||
</div>
|
</Cell>
|
||||||
|
</Grid>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -187,17 +253,25 @@ export default function App() {
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="relative z-2 border-t border-current/20">
|
<footer className="relative z-2 border-t border-current/20">
|
||||||
<div className="mx-auto flex max-w-[1600px] items-center justify-between px-3 sm:px-6 py-3">
|
<Grid className="mx-auto max-w-[1600px] !border-t-0 !border-b-0">
|
||||||
<span className="font-mondwest text-[0.7rem] sm:text-[0.8rem] tracking-[0.12em] opacity-60">
|
<Cell className="flex items-center !px-3 sm:!px-6 !py-3">
|
||||||
|
<Typography
|
||||||
|
mondwest
|
||||||
|
className="text-[0.7rem] sm:text-[0.8rem] tracking-[0.12em] opacity-60"
|
||||||
|
>
|
||||||
{t.app.footer.name}
|
{t.app.footer.name}
|
||||||
</span>
|
</Typography>
|
||||||
<span
|
</Cell>
|
||||||
className="font-mondwest text-[0.6rem] sm:text-[0.7rem] tracking-[0.15em] text-midground"
|
<Cell className="flex items-center justify-end !px-3 sm:!px-6 !py-3">
|
||||||
|
<Typography
|
||||||
|
mondwest
|
||||||
|
className="text-[0.6rem] sm:text-[0.7rem] tracking-[0.15em] text-midground"
|
||||||
style={{ mixBlendMode: "plus-lighter" }}
|
style={{ mixBlendMode: "plus-lighter" }}
|
||||||
>
|
>
|
||||||
{t.app.footer.org}
|
{t.app.footer.org}
|
||||||
</span>
|
</Typography>
|
||||||
</div>
|
</Cell>
|
||||||
|
</Grid>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Typography } from "@nous-research/ui/ui/components/typography/index";
|
||||||
import { useI18n } from "@/i18n/context";
|
import { useI18n } from "@/i18n/context";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -19,9 +20,12 @@ export function LanguageSwitcher() {
|
||||||
>
|
>
|
||||||
{/* Show the *current* language's flag — tooltip advertises the click action */}
|
{/* Show the *current* language's flag — tooltip advertises the click action */}
|
||||||
<span className="text-base leading-none">{locale === "en" ? "🇬🇧" : "🇨🇳"}</span>
|
<span className="text-base leading-none">{locale === "en" ? "🇬🇧" : "🇨🇳"}</span>
|
||||||
<span className="hidden sm:inline font-mondwest tracking-wide uppercase text-[0.65rem]">
|
<Typography
|
||||||
|
mondwest
|
||||||
|
className="hidden sm:inline tracking-wide uppercase text-[0.65rem]"
|
||||||
|
>
|
||||||
{locale === "en" ? "EN" : "中文"}
|
{locale === "en" ? "EN" : "中文"}
|
||||||
</span>
|
</Typography>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { ExternalLink, Copy, X, Check, Loader2 } from "lucide-react";
|
import { ExternalLink, Copy, X, Check, Loader2 } from "lucide-react";
|
||||||
|
import { Typography } from "@nous-research/ui/ui/components/typography/index";
|
||||||
import { api, type OAuthProvider, type OAuthStartResponse } from "@/lib/api";
|
import { api, type OAuthProvider, type OAuthStartResponse } from "@/lib/api";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
@ -12,9 +13,21 @@ interface Props {
|
||||||
onError: (msg: string) => void;
|
onError: (msg: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Phase = "idle" | "starting" | "awaiting_user" | "submitting" | "polling" | "approved" | "error";
|
type Phase =
|
||||||
|
| "idle"
|
||||||
|
| "starting"
|
||||||
|
| "awaiting_user"
|
||||||
|
| "submitting"
|
||||||
|
| "polling"
|
||||||
|
| "approved"
|
||||||
|
| "error";
|
||||||
|
|
||||||
export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props) {
|
export function OAuthLoginModal({
|
||||||
|
provider,
|
||||||
|
onClose,
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
}: Props) {
|
||||||
const [phase, setPhase] = useState<Phase>("starting");
|
const [phase, setPhase] = useState<Phase>("starting");
|
||||||
const [start, setStart] = useState<OAuthStartResponse | null>(null);
|
const [start, setStart] = useState<OAuthStartResponse | null>(null);
|
||||||
const [pkceCode, setPkceCode] = useState("");
|
const [pkceCode, setPkceCode] = useState("");
|
||||||
|
|
@ -81,13 +94,15 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
|
||||||
if (!isMounted.current) return;
|
if (!isMounted.current) return;
|
||||||
if (resp.status === "approved") {
|
if (resp.status === "approved") {
|
||||||
setPhase("approved");
|
setPhase("approved");
|
||||||
if (pollTimer.current !== null) window.clearInterval(pollTimer.current);
|
if (pollTimer.current !== null)
|
||||||
|
window.clearInterval(pollTimer.current);
|
||||||
onSuccess(`${provider.name} connected`);
|
onSuccess(`${provider.name} connected`);
|
||||||
window.setTimeout(() => isMounted.current && onClose(), 1500);
|
window.setTimeout(() => isMounted.current && onClose(), 1500);
|
||||||
} else if (resp.status !== "pending") {
|
} else if (resp.status !== "pending") {
|
||||||
setPhase("error");
|
setPhase("error");
|
||||||
setErrorMsg(resp.error_message || `Login ${resp.status}`);
|
setErrorMsg(resp.error_message || `Login ${resp.status}`);
|
||||||
if (pollTimer.current !== null) window.clearInterval(pollTimer.current);
|
if (pollTimer.current !== null)
|
||||||
|
window.clearInterval(pollTimer.current);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!isMounted.current) return;
|
if (!isMounted.current) return;
|
||||||
|
|
@ -107,7 +122,11 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
|
||||||
setPhase("submitting");
|
setPhase("submitting");
|
||||||
setErrorMsg(null);
|
setErrorMsg(null);
|
||||||
try {
|
try {
|
||||||
const resp = await api.submitOAuthCode(provider.id, start.session_id, pkceCode.trim());
|
const resp = await api.submitOAuthCode(
|
||||||
|
provider.id,
|
||||||
|
start.session_id,
|
||||||
|
pkceCode.trim(),
|
||||||
|
);
|
||||||
if (!isMounted.current) return;
|
if (!isMounted.current) return;
|
||||||
if (resp.ok && resp.status === "approved") {
|
if (resp.ok && resp.status === "approved") {
|
||||||
setPhase("approved");
|
setPhase("approved");
|
||||||
|
|
@ -175,12 +194,22 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
|
||||||
</button>
|
</button>
|
||||||
<div className="p-6 flex flex-col gap-4">
|
<div className="p-6 flex flex-col gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 id="oauth-modal-title" className="font-mondwest text-base tracking-wider uppercase">
|
<Typography
|
||||||
|
as="h2"
|
||||||
|
mondwest
|
||||||
|
id="oauth-modal-title"
|
||||||
|
className="text-base tracking-wider uppercase"
|
||||||
|
>
|
||||||
{t.oauth.connect} {provider.name}
|
{t.oauth.connect} {provider.name}
|
||||||
</h2>
|
</Typography>
|
||||||
{secondsLeft !== null && phase !== "approved" && phase !== "error" && (
|
{secondsLeft !== null &&
|
||||||
|
phase !== "approved" &&
|
||||||
|
phase !== "error" && (
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
{t.oauth.sessionExpires.replace("{time}", fmtTime(secondsLeft))}
|
{t.oauth.sessionExpires.replace(
|
||||||
|
"{time}",
|
||||||
|
fmtTime(secondsLeft),
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -211,7 +240,10 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-2 justify-between">
|
<div className="flex items-center gap-2 justify-between">
|
||||||
<a
|
<a
|
||||||
href={(start as Extract<OAuthStartResponse, { flow: "pkce" }>).auth_url}
|
href={
|
||||||
|
(start as Extract<OAuthStartResponse, { flow: "pkce" }>)
|
||||||
|
.auth_url
|
||||||
|
}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-xs text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
|
className="text-xs text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
|
||||||
|
|
@ -219,7 +251,11 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
|
||||||
<ExternalLink className="h-3 w-3" />
|
<ExternalLink className="h-3 w-3" />
|
||||||
{t.oauth.reOpenAuth}
|
{t.oauth.reOpenAuth}
|
||||||
</a>
|
</a>
|
||||||
<Button onClick={handleSubmitPkceCode} disabled={!pkceCode.trim()} size="sm">
|
<Button
|
||||||
|
onClick={handleSubmitPkceCode}
|
||||||
|
disabled={!pkceCode.trim()}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
{t.oauth.submitCode}
|
{t.oauth.submitCode}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -243,23 +279,46 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-between gap-2 border border-border bg-secondary/30 p-4">
|
<div className="flex items-center justify-between gap-2 border border-border bg-secondary/30 p-4">
|
||||||
<code className="font-mono-ui text-2xl tracking-widest text-foreground">
|
<code className="font-mono-ui text-2xl tracking-widest text-foreground">
|
||||||
{(start as Extract<OAuthStartResponse, { flow: "device_code" }>).user_code}
|
{
|
||||||
|
(
|
||||||
|
start as Extract<
|
||||||
|
OAuthStartResponse,
|
||||||
|
{ flow: "device_code" }
|
||||||
|
>
|
||||||
|
).user_code
|
||||||
|
}
|
||||||
</code>
|
</code>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleCopyUserCode(
|
handleCopyUserCode(
|
||||||
(start as Extract<OAuthStartResponse, { flow: "device_code" }>).user_code,
|
(
|
||||||
|
start as Extract<
|
||||||
|
OAuthStartResponse,
|
||||||
|
{ flow: "device_code" }
|
||||||
|
>
|
||||||
|
).user_code,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
>
|
>
|
||||||
{codeCopied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
|
{codeCopied ? (
|
||||||
|
<Check className="h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href={(start as Extract<OAuthStartResponse, { flow: "device_code" }>).verification_url}
|
href={
|
||||||
|
(
|
||||||
|
start as Extract<
|
||||||
|
OAuthStartResponse,
|
||||||
|
{ flow: "device_code" }
|
||||||
|
>
|
||||||
|
).verification_url
|
||||||
|
}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-xs text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
|
className="text-xs text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
|
||||||
|
|
@ -302,17 +361,32 @@ export function OAuthLoginModal({ provider, onClose, onSuccess, onError }: Props
|
||||||
setStart(null);
|
setStart(null);
|
||||||
setPkceCode("");
|
setPkceCode("");
|
||||||
setPhase("starting");
|
setPhase("starting");
|
||||||
api.startOAuthLogin(provider.id).then((resp) => {
|
api
|
||||||
|
.startOAuthLogin(provider.id)
|
||||||
|
.then((resp) => {
|
||||||
if (!isMounted.current) return;
|
if (!isMounted.current) return;
|
||||||
setStart(resp);
|
setStart(resp);
|
||||||
setSecondsLeft(resp.expires_in);
|
setSecondsLeft(resp.expires_in);
|
||||||
setPhase(resp.flow === "device_code" ? "polling" : "awaiting_user");
|
setPhase(
|
||||||
|
resp.flow === "device_code"
|
||||||
|
? "polling"
|
||||||
|
: "awaiting_user",
|
||||||
|
);
|
||||||
if (resp.flow === "pkce") {
|
if (resp.flow === "pkce") {
|
||||||
window.open(resp.auth_url, "_blank", "noopener,noreferrer");
|
window.open(
|
||||||
|
resp.auth_url,
|
||||||
|
"_blank",
|
||||||
|
"noopener,noreferrer",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
window.open(resp.verification_url, "_blank", "noopener,noreferrer");
|
window.open(
|
||||||
|
resp.verification_url,
|
||||||
|
"_blank",
|
||||||
|
"noopener,noreferrer",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}).catch((e) => {
|
})
|
||||||
|
.catch((e) => {
|
||||||
if (!isMounted.current) return;
|
if (!isMounted.current) return;
|
||||||
setPhase("error");
|
setPhase("error");
|
||||||
setErrorMsg(`${t.common.retry} failed: ${e}`);
|
setErrorMsg(`${t.common.retry} failed: ${e}`);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Palette, Check } from "lucide-react";
|
import { Palette, Check } from "lucide-react";
|
||||||
|
import { Typography } from "@nous-research/ui/ui/components/typography/index";
|
||||||
import { BUILTIN_THEMES, useTheme } from "@/themes";
|
import { BUILTIN_THEMES, useTheme } from "@/themes";
|
||||||
import { useI18n } from "@/i18n";
|
import { useI18n } from "@/i18n";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
@ -56,9 +57,12 @@ export function ThemeSwitcher() {
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
>
|
>
|
||||||
<Palette className="h-3.5 w-3.5" />
|
<Palette className="h-3.5 w-3.5" />
|
||||||
<span className="hidden sm:inline font-mondwest tracking-wide uppercase text-[0.65rem]">
|
<Typography
|
||||||
|
mondwest
|
||||||
|
className="hidden sm:inline tracking-wide uppercase text-[0.65rem]"
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</Typography>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{open && (
|
{open && (
|
||||||
|
|
@ -72,9 +76,12 @@ export function ThemeSwitcher() {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="border-b border-current/20 px-3 py-2">
|
<div className="border-b border-current/20 px-3 py-2">
|
||||||
<span className="font-mondwest text-[0.65rem] tracking-[0.15em] uppercase text-midground/70">
|
<Typography
|
||||||
|
mondwest
|
||||||
|
className="text-[0.65rem] tracking-[0.15em] uppercase text-midground/70"
|
||||||
|
>
|
||||||
{t.theme?.title ?? "Theme"}
|
{t.theme?.title ?? "Theme"}
|
||||||
</span>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{availableThemes.map((th) => {
|
{availableThemes.map((th) => {
|
||||||
|
|
@ -100,13 +107,16 @@ export function ThemeSwitcher() {
|
||||||
{preset ? <ThemeSwatch theme={preset.name} /> : <PlaceholderSwatch />}
|
{preset ? <ThemeSwatch theme={preset.name} /> : <PlaceholderSwatch />}
|
||||||
|
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||||
<span className="truncate font-mondwest text-[0.75rem] tracking-wide uppercase">
|
<Typography
|
||||||
|
mondwest
|
||||||
|
className="truncate text-[0.75rem] tracking-wide uppercase"
|
||||||
|
>
|
||||||
{th.label}
|
{th.label}
|
||||||
</span>
|
</Typography>
|
||||||
{th.description && (
|
{th.description && (
|
||||||
<span className="truncate font-sans text-[0.65rem] normal-case tracking-normal text-midground/50">
|
<Typography className="truncate text-[0.65rem] normal-case tracking-normal text-midground/50">
|
||||||
{th.description}
|
{th.description}
|
||||||
</span>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Clock, Pause, Play, Plus, Trash2, Zap } from "lucide-react";
|
import { Clock, Pause, Play, Plus, Trash2, Zap } from "lucide-react";
|
||||||
|
import { H2 } from "@nous-research/ui/ui/components/typography/h2";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import type { CronJob } from "@/lib/api";
|
import type { CronJob } from "@/lib/api";
|
||||||
import { useToast } from "@/hooks/useToast";
|
import { useToast } from "@/hooks/useToast";
|
||||||
|
|
@ -195,10 +196,10 @@ export default function CronPage() {
|
||||||
|
|
||||||
{/* Jobs list */}
|
{/* Jobs list */}
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<h2 className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
<H2 variant="sm" className="flex items-center gap-2 text-muted-foreground">
|
||||||
<Clock className="h-4 w-4" />
|
<Clock className="h-4 w-4" />
|
||||||
{t.cron.scheduledJobs} ({jobs.length})
|
{t.cron.scheduledJobs} ({jobs.length})
|
||||||
</h2>
|
</H2>
|
||||||
|
|
||||||
{jobs.length === 0 && (
|
{jobs.length === 0 && (
|
||||||
<Card>
|
<Card>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useEffect, useState, useCallback, useRef } from "react";
|
import { useEffect, useState, useCallback, useRef } from "react";
|
||||||
import { FileText, RefreshCw, ChevronRight } from "lucide-react";
|
import { FileText, RefreshCw, ChevronRight } from "lucide-react";
|
||||||
|
import { H2 } from "@nous-research/ui/ui/components/typography/h2";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -104,7 +105,7 @@ export default function LogsPage() {
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FileText className="h-5 w-5 text-muted-foreground" />
|
<FileText className="h-5 w-5 text-muted-foreground" />
|
||||||
<h1 className="text-base font-semibold">{t.logs.title}</h1>
|
<H2 variant="sm">{t.logs.title}</H2>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
Hash,
|
Hash,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { H2 } from "@nous-research/ui/ui/components/typography/h2";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import type { SessionInfo, SessionMessage, SessionSearchResult } from "@/lib/api";
|
import type { SessionInfo, SessionMessage, SessionSearchResult } from "@/lib/api";
|
||||||
import { timeAgo } from "@/lib/utils";
|
import { timeAgo } from "@/lib/utils";
|
||||||
|
|
@ -383,7 +384,7 @@ export default function SessionsPage() {
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:justify-between">
|
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<MessageSquare className="h-5 w-5 text-muted-foreground" />
|
<MessageSquare className="h-5 w-5 text-muted-foreground" />
|
||||||
<h1 className="text-base font-semibold">{t.sessions.title}</h1>
|
<H2 variant="sm">{t.sessions.title}</H2>
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
{total}
|
{total}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
Code,
|
Code,
|
||||||
Zap,
|
Zap,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { H2 } from "@nous-research/ui/ui/components/typography/h2";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import type { SkillInfo, ToolsetInfo } from "@/lib/api";
|
import type { SkillInfo, ToolsetInfo } from "@/lib/api";
|
||||||
import { useToast } from "@/hooks/useToast";
|
import { useToast } from "@/hooks/useToast";
|
||||||
|
|
@ -193,7 +194,7 @@ export default function SkillsPage() {
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Package className="h-5 w-5 text-muted-foreground" />
|
<Package className="h-5 w-5 text-muted-foreground" />
|
||||||
<h1 className="text-base font-semibold">{t.skills.title}</h1>
|
<H2 variant="sm">{t.skills.title}</H2>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{t.skills.enabledOf.replace("{enabled}", String(enabledCount)).replace("{total}", String(skills.length))}
|
{t.skills.enabledOf.replace("{enabled}", String(enabledCount)).replace("{total}", String(skills.length))}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
Wifi,
|
Wifi,
|
||||||
WifiOff,
|
WifiOff,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { Cell, Grid } from "@nous-research/ui/ui/components/grid/index";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import type { PlatformStatus, SessionInfo, StatusResponse } from "@/lib/api";
|
import type { PlatformStatus, SessionInfo, StatusResponse } from "@/lib/api";
|
||||||
import { timeAgo, isoTimeAgo } from "@/lib/utils";
|
import { timeAgo, isoTimeAgo } from "@/lib/utils";
|
||||||
|
|
@ -23,8 +24,14 @@ export default function StatusPage() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const load = () => {
|
const load = () => {
|
||||||
api.getStatus().then(setStatus).catch(() => {});
|
api
|
||||||
api.getSessions(50).then((resp) => setSessions(resp.sessions)).catch(() => {});
|
.getStatus()
|
||||||
|
.then(setStatus)
|
||||||
|
.catch(() => {});
|
||||||
|
api
|
||||||
|
.getSessions(50)
|
||||||
|
.then((resp) => setSessions(resp.sessions))
|
||||||
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
load();
|
load();
|
||||||
const interval = setInterval(load, 5000);
|
const interval = setInterval(load, 5000);
|
||||||
|
|
@ -39,13 +46,19 @@ export default function StatusPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const PLATFORM_STATE_BADGE: Record<string, { variant: "success" | "warning" | "destructive"; label: string }> = {
|
const PLATFORM_STATE_BADGE: Record<
|
||||||
|
string,
|
||||||
|
{ variant: "success" | "warning" | "destructive"; label: string }
|
||||||
|
> = {
|
||||||
connected: { variant: "success", label: t.status.connected },
|
connected: { variant: "success", label: t.status.connected },
|
||||||
disconnected: { variant: "warning", label: t.status.disconnected },
|
disconnected: { variant: "warning", label: t.status.disconnected },
|
||||||
fatal: { variant: "destructive", label: t.status.error },
|
fatal: { variant: "destructive", label: t.status.error },
|
||||||
};
|
};
|
||||||
|
|
||||||
const GATEWAY_STATE_DISPLAY: Record<string, { badge: "success" | "warning" | "destructive" | "outline"; label: string }> = {
|
const GATEWAY_STATE_DISPLAY: Record<
|
||||||
|
string,
|
||||||
|
{ badge: "success" | "warning" | "destructive" | "outline"; label: string }
|
||||||
|
> = {
|
||||||
running: { badge: "success", label: t.status.running },
|
running: { badge: "success", label: t.status.running },
|
||||||
starting: { badge: "warning", label: t.status.starting },
|
starting: { badge: "warning", label: t.status.starting },
|
||||||
startup_failed: { badge: "destructive", label: t.status.failed },
|
startup_failed: { badge: "destructive", label: t.status.failed },
|
||||||
|
|
@ -53,15 +66,19 @@ export default function StatusPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
function gatewayValue(): string {
|
function gatewayValue(): string {
|
||||||
if (status!.gateway_running && status!.gateway_health_url) return status!.gateway_health_url;
|
if (status!.gateway_running && status!.gateway_health_url)
|
||||||
if (status!.gateway_running && status!.gateway_pid) return `${t.status.pid} ${status!.gateway_pid}`;
|
return status!.gateway_health_url;
|
||||||
|
if (status!.gateway_running && status!.gateway_pid)
|
||||||
|
return `${t.status.pid} ${status!.gateway_pid}`;
|
||||||
if (status!.gateway_running) return t.status.runningRemote;
|
if (status!.gateway_running) return t.status.runningRemote;
|
||||||
if (status!.gateway_state === "startup_failed") return t.status.startFailed;
|
if (status!.gateway_state === "startup_failed") return t.status.startFailed;
|
||||||
return t.status.notRunning;
|
return t.status.notRunning;
|
||||||
}
|
}
|
||||||
|
|
||||||
function gatewayBadge() {
|
function gatewayBadge() {
|
||||||
const info = status!.gateway_state ? GATEWAY_STATE_DISPLAY[status!.gateway_state] : null;
|
const info = status!.gateway_state
|
||||||
|
? GATEWAY_STATE_DISPLAY[status!.gateway_state]
|
||||||
|
: null;
|
||||||
if (info) return info;
|
if (info) return info;
|
||||||
return status!.gateway_running
|
return status!.gateway_running
|
||||||
? { badge: "success" as const, label: t.status.running }
|
? { badge: "success" as const, label: t.status.running }
|
||||||
|
|
@ -88,9 +105,14 @@ export default function StatusPage() {
|
||||||
{
|
{
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
label: t.status.activeSessions,
|
label: t.status.activeSessions,
|
||||||
value: status.active_sessions > 0 ? `${status.active_sessions} ${t.status.running.toLowerCase()}` : t.status.noneRunning,
|
value:
|
||||||
|
status.active_sessions > 0
|
||||||
|
? `${status.active_sessions} ${t.status.running.toLowerCase()}`
|
||||||
|
: t.status.noneRunning,
|
||||||
badgeText: status.active_sessions > 0 ? t.common.live : t.common.off,
|
badgeText: status.active_sessions > 0 ? t.common.live : t.common.off,
|
||||||
badgeVariant: (status.active_sessions > 0 ? "success" : "outline") as "success" | "outline",
|
badgeVariant: (status.active_sessions > 0 ? "success" : "outline") as
|
||||||
|
| "success"
|
||||||
|
| "outline",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -106,9 +128,14 @@ export default function StatusPage() {
|
||||||
detail: status.gateway_exit_reason ?? undefined,
|
detail: status.gateway_exit_reason ?? undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const failedPlatforms = platforms.filter(([, info]) => info.state === "fatal" || info.state === "disconnected");
|
const failedPlatforms = platforms.filter(
|
||||||
|
([, info]) => info.state === "fatal" || info.state === "disconnected",
|
||||||
|
);
|
||||||
for (const [name, info] of failedPlatforms) {
|
for (const [name, info] of failedPlatforms) {
|
||||||
const stateLabel = info.state === "fatal" ? t.status.platformError : t.status.platformDisconnected;
|
const stateLabel =
|
||||||
|
info.state === "fatal"
|
||||||
|
? t.status.platformError
|
||||||
|
: t.status.platformDisconnected;
|
||||||
alerts.push({
|
alerts.push({
|
||||||
message: `${name.charAt(0).toUpperCase() + name.slice(1)} ${stateLabel}`,
|
message: `${name.charAt(0).toUpperCase() + name.slice(1)} ${stateLabel}`,
|
||||||
detail: info.error_message ?? undefined,
|
detail: info.error_message ?? undefined,
|
||||||
|
|
@ -117,7 +144,6 @@ export default function StatusPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{/* Alert banner — breaks grid monotony for critical states */}
|
|
||||||
{alerts.length > 0 && (
|
{alerts.length > 0 && (
|
||||||
<div className="border border-destructive/30 bg-destructive/[0.06] p-4">
|
<div className="border border-destructive/30 bg-destructive/[0.06] p-4">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
|
|
@ -125,9 +151,13 @@ export default function StatusPage() {
|
||||||
<div className="flex flex-col gap-2 min-w-0">
|
<div className="flex flex-col gap-2 min-w-0">
|
||||||
{alerts.map((alert, i) => (
|
{alerts.map((alert, i) => (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
<p className="text-sm font-medium text-destructive">{alert.message}</p>
|
<p className="text-sm font-medium text-destructive">
|
||||||
|
{alert.message}
|
||||||
|
</p>
|
||||||
{alert.detail && (
|
{alert.detail && (
|
||||||
<p className="text-xs text-destructive/70 mt-0.5">{alert.detail}</p>
|
<p className="text-xs text-destructive/70 mt-0.5">
|
||||||
|
{alert.detail}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -136,32 +166,41 @@ export default function StatusPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
<Grid className="border-b lg:!grid-cols-3">
|
||||||
{items.map(({ icon: Icon, label, value, badgeText, badgeVariant }) => (
|
{items.map(({ icon: Icon, label, value, badgeText, badgeVariant }) => (
|
||||||
<Card key={label} className="min-w-0 overflow-hidden">
|
<Cell
|
||||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
key={label}
|
||||||
|
className="flex min-w-0 flex-col gap-2 overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="text-sm font-medium">{label}</CardTitle>
|
<CardTitle className="text-sm font-medium">{label}</CardTitle>
|
||||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</div>
|
||||||
|
|
||||||
<CardContent>
|
<div
|
||||||
<div className="text-2xl font-bold font-mondwest truncate" title={value}>{value}</div>
|
className="truncate text-2xl font-bold font-mondwest"
|
||||||
|
title={value}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
|
||||||
{badgeText && (
|
{badgeText && (
|
||||||
<Badge variant={badgeVariant} className="mt-2">
|
<Badge variant={badgeVariant} className="self-start">
|
||||||
{badgeVariant === "success" && (
|
{badgeVariant === "success" && (
|
||||||
<span className="mr-1 inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-current" />
|
<span className="mr-1 inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-current" />
|
||||||
)}
|
)}
|
||||||
{badgeText}
|
{badgeText}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</Cell>
|
||||||
</Card>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</Grid>
|
||||||
|
|
||||||
{platforms.length > 0 && (
|
{platforms.length > 0 && (
|
||||||
<PlatformsCard platforms={platforms} platformStateBadge={PLATFORM_STATE_BADGE} />
|
<PlatformsCard
|
||||||
|
platforms={platforms}
|
||||||
|
platformStateBadge={PLATFORM_STATE_BADGE}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeSessions.length > 0 && (
|
{activeSessions.length > 0 && (
|
||||||
|
|
@ -169,7 +208,9 @@ export default function StatusPage() {
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Activity className="h-5 w-5 text-success" />
|
<Activity className="h-5 w-5 text-success" />
|
||||||
<CardTitle className="text-base">{t.status.activeSessions}</CardTitle>
|
<CardTitle className="text-base">
|
||||||
|
{t.status.activeSessions}
|
||||||
|
</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
|
|
@ -181,7 +222,9 @@ export default function StatusPage() {
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-1 min-w-0 w-full">
|
<div className="flex flex-col gap-1 min-w-0 w-full">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium text-sm truncate">{s.title ?? t.common.untitled}</span>
|
<span className="font-medium text-sm truncate">
|
||||||
|
{s.title ?? t.common.untitled}
|
||||||
|
</span>
|
||||||
|
|
||||||
<Badge variant="success" className="text-[10px] shrink-0">
|
<Badge variant="success" className="text-[10px] shrink-0">
|
||||||
<span className="mr-1 inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-current" />
|
<span className="mr-1 inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-current" />
|
||||||
|
|
@ -190,7 +233,11 @@ export default function StatusPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="text-xs text-muted-foreground truncate">
|
<span className="text-xs text-muted-foreground truncate">
|
||||||
<span className="font-mono-ui">{(s.model ?? t.common.unknown).split("/").pop()}</span> · {s.message_count} {t.common.msgs} · {timeAgo(s.last_active)}
|
<span className="font-mono-ui">
|
||||||
|
{(s.model ?? t.common.unknown).split("/").pop()}
|
||||||
|
</span>{" "}
|
||||||
|
· {s.message_count} {t.common.msgs} ·{" "}
|
||||||
|
{timeAgo(s.last_active)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -204,7 +251,9 @@ export default function StatusPage() {
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clock className="h-5 w-5 text-muted-foreground" />
|
<Clock className="h-5 w-5 text-muted-foreground" />
|
||||||
<CardTitle className="text-base">{t.status.recentSessions}</CardTitle>
|
<CardTitle className="text-base">
|
||||||
|
{t.status.recentSessions}
|
||||||
|
</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
|
|
@ -215,10 +264,16 @@ export default function StatusPage() {
|
||||||
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 border border-border p-3 w-full"
|
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 border border-border p-3 w-full"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-1 min-w-0 w-full">
|
<div className="flex flex-col gap-1 min-w-0 w-full">
|
||||||
<span className="font-medium text-sm truncate">{s.title ?? t.common.untitled}</span>
|
<span className="font-medium text-sm truncate">
|
||||||
|
{s.title ?? t.common.untitled}
|
||||||
|
</span>
|
||||||
|
|
||||||
<span className="text-xs text-muted-foreground truncate">
|
<span className="text-xs text-muted-foreground truncate">
|
||||||
<span className="font-mono-ui">{(s.model ?? t.common.unknown).split("/").pop()}</span> · {s.message_count} {t.common.msgs} · {timeAgo(s.last_active)}
|
<span className="font-mono-ui">
|
||||||
|
{(s.model ?? t.common.unknown).split("/").pop()}
|
||||||
|
</span>{" "}
|
||||||
|
· {s.message_count} {t.common.msgs} ·{" "}
|
||||||
|
{timeAgo(s.last_active)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{s.preview && (
|
{s.preview && (
|
||||||
|
|
@ -228,7 +283,10 @@ export default function StatusPage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Badge variant="outline" className="text-[10px] shrink-0 self-start sm:self-center">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-[10px] shrink-0 self-start sm:self-center"
|
||||||
|
>
|
||||||
<Database className="mr-1 h-3 w-3" />
|
<Database className="mr-1 h-3 w-3" />
|
||||||
{s.source ?? "local"}
|
{s.source ?? "local"}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
@ -249,7 +307,9 @@ function PlatformsCard({ platforms, platformStateBadge }: PlatformsCardProps) {
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Radio className="h-5 w-5 text-muted-foreground" />
|
<Radio className="h-5 w-5 text-muted-foreground" />
|
||||||
<CardTitle className="text-base">{t.status.connectedPlatforms}</CardTitle>
|
<CardTitle className="text-base">
|
||||||
|
{t.status.connectedPlatforms}
|
||||||
|
</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
|
|
@ -259,7 +319,12 @@ function PlatformsCard({ platforms, platformStateBadge }: PlatformsCardProps) {
|
||||||
variant: "outline" as const,
|
variant: "outline" as const,
|
||||||
label: info.state,
|
label: info.state,
|
||||||
};
|
};
|
||||||
const IconComponent = info.state === "connected" ? Wifi : info.state === "fatal" ? AlertTriangle : WifiOff;
|
const IconComponent =
|
||||||
|
info.state === "connected"
|
||||||
|
? Wifi
|
||||||
|
: info.state === "fatal"
|
||||||
|
? AlertTriangle
|
||||||
|
: WifiOff;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -267,19 +332,25 @@ function PlatformsCard({ platforms, platformStateBadge }: PlatformsCardProps) {
|
||||||
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 border border-border p-3 w-full"
|
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 border border-border p-3 w-full"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 min-w-0 w-full">
|
<div className="flex items-center gap-3 min-w-0 w-full">
|
||||||
<IconComponent className={`h-4 w-4 shrink-0 ${
|
<IconComponent
|
||||||
|
className={`h-4 w-4 shrink-0 ${
|
||||||
info.state === "connected"
|
info.state === "connected"
|
||||||
? "text-success"
|
? "text-success"
|
||||||
: info.state === "fatal"
|
: info.state === "fatal"
|
||||||
? "text-destructive"
|
? "text-destructive"
|
||||||
: "text-warning"
|
: "text-warning"
|
||||||
}`} />
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col gap-0.5 min-w-0">
|
<div className="flex flex-col gap-0.5 min-w-0">
|
||||||
<span className="text-sm font-medium capitalize truncate">{name}</span>
|
<span className="text-sm font-medium capitalize truncate">
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
|
||||||
{info.error_message && (
|
{info.error_message && (
|
||||||
<span className="text-xs text-destructive">{info.error_message}</span>
|
<span className="text-xs text-destructive">
|
||||||
|
{info.error_message}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{info.updated_at && (
|
{info.updated_at && (
|
||||||
|
|
@ -290,7 +361,10 @@ function PlatformsCard({ platforms, platformStateBadge }: PlatformsCardProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Badge variant={display.variant} className="shrink-0 self-start sm:self-center">
|
<Badge
|
||||||
|
variant={display.variant}
|
||||||
|
className="shrink-0 self-start sm:self-center"
|
||||||
|
>
|
||||||
{display.variant === "success" && (
|
{display.variant === "success" && (
|
||||||
<span className="mr-1 inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-current" />
|
<span className="mr-1 inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-current" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -306,5 +380,8 @@ function PlatformsCard({ platforms, platformStateBadge }: PlatformsCardProps) {
|
||||||
|
|
||||||
interface PlatformsCardProps {
|
interface PlatformsCardProps {
|
||||||
platforms: [string, PlatformStatus][];
|
platforms: [string, PlatformStatus][];
|
||||||
platformStateBadge: Record<string, { variant: "success" | "warning" | "destructive"; label: string }>;
|
platformStateBadge: Record<
|
||||||
|
string,
|
||||||
|
{ variant: "success" | "warning" | "destructive"; label: string }
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue