Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions new-ui/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ In production, configure `apiUrl` in the config panel or pass `?apiUrl=...&apiKe

## Theme

The UI uses a "beehive" theme with amber/gold/honey colors.
Dark mode is default. Toggle via theme button.
CSS variables are defined in `src/styles/globals.css`.
"Mission Control" design — clean, information-dense, professional.
- **Base:** Zinc-neutral palette (shadcn/ui v4 oklch tokens)
- **Accent:** Amber as brand `--primary` only — interactive elements, active states
- **Dark mode** is default. Toggle via header button.
- **Typography:** Space Grotesk (sans) + Space Mono (mono). No display fonts.
- **Status colors:** Semantic — emerald (success), amber (active/busy), red (error), zinc (inactive)
- CSS variables defined in `src/styles/globals.css`. AG Grid themed via `src/styles/ag-grid.css`.
4 changes: 2 additions & 2 deletions new-ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Agent Swarm Dashboard</title>
<meta name="description" content="Multi-agent orchestration dashboard for managing AI agent swarms, tasks, and workflows." />
<meta name="theme-color" content="#d97706" />
<meta name="theme-color" content="#18181b" />
<meta property="og:title" content="Agent Swarm Dashboard" />
<meta property="og:description" content="Monitor and manage your AI agent swarm — tasks, agents, epics, and more." />
<meta property="og:type" content="website" />
Expand All @@ -14,7 +14,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Graduate&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap"
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap"
rel="stylesheet"
/>
</head>
Expand Down
4 changes: 2 additions & 2 deletions new-ui/src/components/layout/app-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ export function AppHeader() {
className={cn(
"size-2 rounded-full",
isHealthy
? "bg-terminal-green animate-heartbeat"
: "bg-terminal-red",
? "bg-emerald-500"
: "bg-red-500",
)}
/>
<span className="hidden sm:inline">
Expand Down
98 changes: 56 additions & 42 deletions new-ui/src/components/layout/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
Expand All @@ -25,17 +26,35 @@ import {
SidebarTrigger,
} from "@/components/ui/sidebar";

const navItems = [
{ title: "Dashboard", path: "/", icon: LayoutDashboard },
{ title: "Agents", path: "/agents", icon: Users },
{ title: "Tasks", path: "/tasks", icon: ListTodo },
{ title: "Epics", path: "/epics", icon: Milestone },
{ title: "Chat", path: "/chat", icon: MessageSquare },
{ title: "Services", path: "/services", icon: Server },
{ title: "Schedules", path: "/schedules", icon: Clock },
{ title: "Usage", path: "/usage", icon: BarChart3 },
{ title: "Config", path: "/config", icon: Settings },
{ title: "Repos", path: "/repos", icon: GitBranch },
const navGroups = [
{
label: "Core",
items: [
{ title: "Dashboard", path: "/", icon: LayoutDashboard },
{ title: "Agents", path: "/agents", icon: Users },
{ title: "Tasks", path: "/tasks", icon: ListTodo },
{ title: "Epics", path: "/epics", icon: Milestone },
],
},
{
label: "Communication",
items: [{ title: "Chat", path: "/chat", icon: MessageSquare }],
},
{
label: "Operations",
items: [
{ title: "Services", path: "/services", icon: Server },
{ title: "Schedules", path: "/schedules", icon: Clock },
{ title: "Usage", path: "/usage", icon: BarChart3 },
],
},
{
label: "System",
items: [
{ title: "Config", path: "/config", icon: Settings },
{ title: "Repos", path: "/repos", icon: GitBranch },
],
},
];

export function AppSidebar() {
Expand All @@ -50,43 +69,38 @@ export function AppSidebar() {
alt="Agent Swarm"
className="h-8 w-8 rounded"
/>
<span className="font-display text-lg tracking-wider text-hive-amber group-data-[collapsible=icon]:hidden">
<span className="text-lg font-semibold tracking-tight text-sidebar-foreground group-data-[collapsible=icon]:hidden">
Agent Swarm
</span>
</NavLink>
</SidebarHeader>

<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{navItems.map((item) => {
const isActive =
item.path === "/"
? location.pathname === "/"
: location.pathname.startsWith(item.path);
return (
<SidebarMenuItem key={item.path}>
<SidebarMenuButton asChild isActive={isActive}>
<NavLink
to={item.path}
end={item.path === "/"}
className={
isActive
? "text-hive-amber border-l-2 border-hive-amber"
: "text-sidebar-foreground hover:text-hive-amber"
}
>
<item.icon className="size-4" />
<span>{item.title}</span>
</NavLink>
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
{navGroups.map((group) => (
<SidebarGroup key={group.label}>
<SidebarGroupLabel>{group.label}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{group.items.map((item) => {
const isActive =
item.path === "/"
? location.pathname === "/"
: location.pathname.startsWith(item.path);
return (
<SidebarMenuItem key={item.path}>
<SidebarMenuButton asChild isActive={isActive}>
<NavLink to={item.path} end={item.path === "/"}>
<item.icon className="size-4" />
<span>{item.title}</span>
</NavLink>
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
))}
</SidebarContent>

<SidebarFooter className="border-t border-sidebar-border">
Expand Down
2 changes: 1 addition & 1 deletion new-ui/src/components/shared/error-boundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
<RotateCcw className="h-4 w-4" />
Try Again
</Button>
<Button asChild className="gap-1.5 bg-amber-600 hover:bg-amber-700">
<Button asChild className="gap-1.5 bg-primary hover:bg-primary/90">
<Link to="/">
<Home className="h-4 w-4" />
Go Home
Expand Down
184 changes: 87 additions & 97 deletions new-ui/src/components/shared/stats-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,51 @@
import { cn } from "@/lib/utils";
import { formatCompactNumber } from "@/lib/utils";
import {
Activity,
Bot,
CheckCircle2,
Clock,
Loader2,
Milestone,
XCircle,
Zap,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { cn, formatCompactNumber } from "@/lib/utils";

interface HexStatProps {
interface StatCardProps {
icon: LucideIcon;
label: string;
value: number | string;
color?: string;
active?: boolean;
onClick?: () => void;
variant?: "default" | "success" | "warning" | "danger";
}

function HexStat({ label, value, color = "text-amber-400", active, onClick }: HexStatProps) {
const hexClip = "polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)";
const variantStyles = {
default: "text-foreground",
success: "text-emerald-600 dark:text-emerald-400",
warning: "text-amber-600 dark:text-amber-400",
danger: "text-red-600 dark:text-red-400",
} as const;

function StatCard({ icon: Icon, label, value, variant = "default" }: StatCardProps) {
return (
<button
type="button"
onClick={onClick}
disabled={!onClick}
className={cn(
"group relative flex-shrink-0 cursor-default transition-transform hover:scale-105",
onClick && "cursor-pointer",
)}
>
{/* Border layer */}
<div
className="absolute inset-0 bg-amber-500/20"
style={{ clipPath: hexClip }}
/>
{/* Background layer */}
<div
className={cn(
"absolute inset-[1.5px] bg-card",
active && "animate-[breathe_3s_ease-in-out_infinite]",
)}
style={{ clipPath: hexClip }}
/>
{/* Content layer */}
<div
className="relative flex flex-col items-center justify-center w-[100px] h-[110px] md:w-[120px] md:h-[130px]"
style={{ clipPath: hexClip }}
>
<span className={cn("text-xl md:text-2xl font-bold font-mono tabular-nums", color)}>
{typeof value === "number" ? formatCompactNumber(value) : value}
</span>
<span className="text-[9px] md:text-[10px] font-semibold uppercase tracking-widest text-muted-foreground mt-0.5">
{label}
</span>
</div>
</button>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-2 text-muted-foreground">
<Icon className="h-4 w-4" />
<span className="text-xs uppercase tracking-wider">{label}</span>
</div>
<div className="mt-2">
<span
className={cn(
"font-mono tabular-nums text-2xl font-bold",
variantStyles[variant],
)}
>
{typeof value === "number" ? formatCompactNumber(value) : value}
</span>
</div>
</CardContent>
</Card>
);
}

Expand All @@ -60,60 +58,52 @@ interface StatsBarProps {

export function StatsBar({ agents, tasks, epics, healthy }: StatsBarProps) {
return (
<div className="w-full overflow-x-auto pb-2">
{/* Top row */}
<div className="flex items-center justify-center gap-1 md:gap-2">
<HexStat
label="Agents"
value={agents?.total ?? 0}
color="text-amber-400"
/>
<HexStat
label="Busy"
value={agents?.busy ?? 0}
color="text-amber-300"
active={(agents?.busy ?? 0) > 0}
/>
<HexStat
label="Idle"
value={agents?.idle ?? 0}
color="text-emerald-400"
/>
<HexStat
label="Epics"
value={epics?.active ?? 0}
color="text-blue-400"
/>
<HexStat
label="Health"
value={healthy ? "OK" : "ERR"}
color={healthy ? "text-emerald-400" : "text-red-400"}
/>
</div>
{/* Bottom row — offset for honeycomb */}
<div className="flex items-center justify-center gap-1 md:gap-2 -mt-[28px] md:-mt-[32px] ml-[52px] md:ml-[62px]">
<HexStat
label="Pending"
value={tasks?.pending ?? 0}
color="text-yellow-400"
/>
<HexStat
label="Running"
value={tasks?.in_progress ?? 0}
color="text-amber-400"
active={(tasks?.in_progress ?? 0) > 0}
/>
<HexStat
label="Done"
value={tasks?.completed ?? 0}
color="text-emerald-400"
/>
<HexStat
label="Failed"
value={tasks?.failed ?? 0}
color={(tasks?.failed ?? 0) > 0 ? "text-red-400" : "text-zinc-500"}
/>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-3">
<StatCard
icon={Bot}
label="Agents"
value={agents?.total ?? 0}
/>
<StatCard
icon={Zap}
label="Busy"
value={agents?.busy ?? 0}
variant={(agents?.busy ?? 0) > 0 ? "warning" : "default"}
/>
<StatCard
icon={Clock}
label="Pending"
value={tasks?.pending ?? 0}
/>
<StatCard
icon={Loader2}
label="Running"
value={tasks?.in_progress ?? 0}
variant={(tasks?.in_progress ?? 0) > 0 ? "warning" : "default"}
/>
<StatCard
icon={CheckCircle2}
label="Done"
value={tasks?.completed ?? 0}
variant="success"
/>
<StatCard
icon={XCircle}
label="Failed"
value={tasks?.failed ?? 0}
variant={(tasks?.failed ?? 0) > 0 ? "danger" : "default"}
/>
<StatCard
icon={Milestone}
label="Epics"
value={epics?.active ?? 0}
/>
<StatCard
icon={Activity}
label="Health"
value={healthy ? "OK" : "ERR"}
variant={healthy ? "success" : "danger"}
/>
</div>
);
}
Loading