[taskboard] migrate fleet console to nextjs
This commit is contained in:
111
components/agents-client.tsx
Normal file
111
components/agents-client.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import type { FleetAgent } from "@/lib/types";
|
||||
|
||||
export function AgentsClient({ agents }: { agents: FleetAgent[] }) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [family, setFamily] = useState("");
|
||||
|
||||
const filteredAgents = useMemo(() => {
|
||||
return agents.filter((agent) => {
|
||||
const matchesQuery =
|
||||
query.length === 0 ||
|
||||
agent.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
agent.host.toLowerCase().includes(query.toLowerCase()) ||
|
||||
agent.role.toLowerCase().includes(query.toLowerCase());
|
||||
const matchesFamily = family.length === 0 || agent.family === family;
|
||||
return matchesQuery && matchesFamily;
|
||||
});
|
||||
}, [agents, family, query]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Configured Agent Runtimes</CardTitle>
|
||||
<CardDescription>
|
||||
OpenClaw swarm members and ZeroClaw host runtimes are shown from the deployed fleet model.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 md:grid-cols-[1fr_220px]">
|
||||
<Input placeholder="Search by name, host, or role" value={query} onChange={(event) => setQuery(event.target.value)} />
|
||||
<Select value={family} onChange={(event) => setFamily(event.target.value)}>
|
||||
<option value="">All families</option>
|
||||
<option value="openclaw">OpenClaw</option>
|
||||
<option value="zeroclaw">ZeroClaw</option>
|
||||
</Select>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{filteredAgents.map((agent) => (
|
||||
<Card key={agent.slug}>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<span>{agent.emoji}</span>
|
||||
<span>{agent.name}</span>
|
||||
</CardTitle>
|
||||
<CardDescription>{agent.role}</CardDescription>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Badge variant={agent.family === "openclaw" ? "default" : "success"}>{agent.family}</Badge>
|
||||
<Badge variant="secondary">{agent.status}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<dl className="grid gap-2 text-sm text-slate-300 md:grid-cols-2">
|
||||
<div>
|
||||
<dt className="text-xs uppercase tracking-[0.2em] text-slate-500">Host</dt>
|
||||
<dd>{agent.host}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs uppercase tracking-[0.2em] text-slate-500">Model</dt>
|
||||
<dd>{agent.model || "Host-local/runtime-defined"}</dd>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<dt className="text-xs uppercase tracking-[0.2em] text-slate-500">Runtime</dt>
|
||||
<dd className="font-mono text-xs text-cyan-100">{agent.runtimePath}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs uppercase tracking-[0.2em] text-slate-500">Workload</dt>
|
||||
<dd>{agent.workload} active</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs uppercase tracking-[0.2em] text-slate-500">Current task</dt>
|
||||
<dd>{agent.currentTask || "No heartbeat task"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-slate-500">Channels</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{agent.channels.map((channel) => (
|
||||
<Badge key={`${agent.slug}-${channel.label}`} variant="outline">
|
||||
{channel.label}: {channel.value}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-slate-500">Tools</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{agent.tools.length ? agent.tools.map((tool) => <Badge key={tool} variant="secondary">{tool}</Badge>) : <span className="text-sm text-slate-400">No parsed tools.</span>}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
67
components/app-shell.tsx
Normal file
67
components/app-shell.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import Link from "next/link";
|
||||
import { Network, NotebookTabs, PanelsTopLeft, ScrollText, Settings2, UsersRound } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const navItems = [
|
||||
{ href: "/tasks", label: "Tasks", icon: PanelsTopLeft },
|
||||
{ href: "/agents", label: "Agents", icon: UsersRound },
|
||||
{ href: "/architecture", label: "Architecture", icon: Network },
|
||||
{ href: "/wiki", label: "Wiki", icon: NotebookTabs },
|
||||
{ href: "/usage", label: "Usage", icon: ScrollText },
|
||||
{ href: "/gitea", label: "Gitea", icon: Settings2 },
|
||||
];
|
||||
|
||||
export function AppShell({
|
||||
pathname,
|
||||
children,
|
||||
}: {
|
||||
pathname: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(34,211,238,0.18),_transparent_30%),radial-gradient(circle_at_top_right,_rgba(245,158,11,0.14),_transparent_28%),linear-gradient(180deg,#07111f_0%,#091321_44%,#0f172a_100%)] text-foreground">
|
||||
<header className="border-b border-white/10 bg-slate-950/60 backdrop-blur-xl">
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-6">
|
||||
<div>
|
||||
<p className="font-mono text-xs uppercase tracking-[0.3em] text-cyan-300/80">
|
||||
OpenClaw Taskboard
|
||||
</p>
|
||||
<h1 className="mt-1 text-2xl font-semibold tracking-tight text-white">
|
||||
Claw Fleet Console
|
||||
</h1>
|
||||
<p className="mt-1 max-w-2xl text-sm text-slate-300">
|
||||
Unified operations view for OpenClaw orchestration, ZeroClaw host runtimes, and deployed architecture.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="mx-auto grid max-w-7xl gap-6 px-6 py-8 lg:grid-cols-[240px_minmax(0,1fr)]">
|
||||
<nav className="space-y-2">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-xl border px-4 py-3 text-sm transition",
|
||||
isActive
|
||||
? "border-cyan-300/30 bg-cyan-300/10 text-cyan-100 shadow-panel"
|
||||
: "border-white/10 bg-slate-950/35 text-slate-300 hover:border-white/20 hover:bg-white/5 hover:text-white",
|
||||
)}
|
||||
href={item.href}
|
||||
key={item.href}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
84
components/architecture-view.tsx
Normal file
84
components/architecture-view.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import type { ArchitectureDocument } from "@/lib/types";
|
||||
|
||||
export function ArchitectureView({ architecture }: { architecture: ArchitectureDocument }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{architecture.title}</CardTitle>
|
||||
<CardDescription>Generated from the deployed fleet model and tracked channel/runtime definitions.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{architecture.overview.map((line) => (
|
||||
<Badge key={line} variant="secondary">
|
||||
{line}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<pre className="rounded-xl border border-white/10 bg-slate-950/60 p-4 font-mono text-xs text-cyan-100">
|
||||
{architecture.topologyDiagram}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-6">
|
||||
{architecture.sections.map((section) => (
|
||||
<Card key={section.id}>
|
||||
<CardHeader>
|
||||
<CardTitle>{section.title}</CardTitle>
|
||||
<CardDescription>{section.summary}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_340px]">
|
||||
<div className="space-y-4">
|
||||
<pre className="rounded-xl border border-white/10 bg-slate-950/60 p-4 font-mono text-xs text-cyan-100">
|
||||
{section.diagram}
|
||||
</pre>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{section.configuredAgents.map((agentName) => (
|
||||
<Badge key={agentName}>{agentName}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-slate-500">Runtime</p>
|
||||
<div className="space-y-2">
|
||||
{section.runtime.map((entry) => (
|
||||
<div className="rounded-lg border border-white/10 bg-white/5 p-3 text-sm" key={`${section.id}-${entry.label}`}>
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-slate-500">{entry.label}</p>
|
||||
<p>{entry.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-slate-500">Channels</p>
|
||||
<div className="space-y-2">
|
||||
{section.channels.map((entry) => (
|
||||
<div className="rounded-lg border border-white/10 bg-white/5 p-3 text-sm" key={`${section.id}-${entry.label}`}>
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-slate-500">{entry.label}</p>
|
||||
<p>{entry.value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-slate-500">Notes</p>
|
||||
<ul className="space-y-2 text-sm text-slate-300">
|
||||
{section.notes.map((note) => (
|
||||
<li key={note}>- {note}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
178
components/tasks-client.tsx
Normal file
178
components/tasks-client.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { FleetAgent, TaskPriority, TaskRecord, TaskStatus } from "@/lib/types";
|
||||
|
||||
const COLUMNS: TaskStatus[] = ["Backlog", "Todo", "In Progress", "Review", "Done"];
|
||||
const PRIORITIES: TaskPriority[] = ["Low", "Medium", "High", "Critical"];
|
||||
|
||||
export function TasksClient({
|
||||
initialTasks,
|
||||
agents,
|
||||
}: {
|
||||
initialTasks: TaskRecord[];
|
||||
agents: FleetAgent[];
|
||||
}) {
|
||||
const [tasks, setTasks] = useState(initialTasks);
|
||||
const [formState, setFormState] = useState({
|
||||
title: "",
|
||||
description: "",
|
||||
assignee: "",
|
||||
priority: "Medium" as TaskPriority,
|
||||
tags: "",
|
||||
});
|
||||
|
||||
async function refreshTasks() {
|
||||
const response = await fetch("/api/tasks");
|
||||
const nextTasks = (await response.json()) as TaskRecord[];
|
||||
setTasks(nextTasks);
|
||||
}
|
||||
|
||||
async function createTask(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
await fetch("/api/tasks", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
title: formState.title,
|
||||
description: formState.description,
|
||||
assignee: formState.assignee,
|
||||
priority: formState.priority,
|
||||
tags: formState.tags
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean),
|
||||
}),
|
||||
});
|
||||
|
||||
setFormState({
|
||||
title: "",
|
||||
description: "",
|
||||
assignee: "",
|
||||
priority: "Medium",
|
||||
tags: "",
|
||||
});
|
||||
await refreshTasks();
|
||||
}
|
||||
|
||||
async function moveToDone(taskId: number) {
|
||||
await fetch(`/api/tasks/${taskId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ status: "Done" }),
|
||||
});
|
||||
await refreshTasks();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Unified Task Intake</CardTitle>
|
||||
<CardDescription>
|
||||
Assign work to OpenClaw swarm agents or ZeroClaw host runtimes from a single board.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="grid gap-3 md:grid-cols-2" onSubmit={createTask}>
|
||||
<Input
|
||||
placeholder="Task title"
|
||||
required
|
||||
value={formState.title}
|
||||
onChange={(event) => setFormState((current) => ({ ...current, title: event.target.value }))}
|
||||
/>
|
||||
<Select
|
||||
value={formState.assignee}
|
||||
onChange={(event) => setFormState((current) => ({ ...current, assignee: event.target.value }))}
|
||||
>
|
||||
<option value="">Select agent</option>
|
||||
{agents.map((agent) => (
|
||||
<option key={agent.slug} value={agent.assignmentKey}>
|
||||
{agent.name} • {agent.family} • {agent.host}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
value={formState.priority}
|
||||
onChange={(event) =>
|
||||
setFormState((current) => ({ ...current, priority: event.target.value as TaskPriority }))
|
||||
}
|
||||
>
|
||||
{PRIORITIES.map((priority) => (
|
||||
<option key={priority} value={priority}>
|
||||
{priority}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
placeholder="Tags (comma-separated)"
|
||||
value={formState.tags}
|
||||
onChange={(event) => setFormState((current) => ({ ...current, tags: event.target.value }))}
|
||||
/>
|
||||
<div className="md:col-span-2">
|
||||
<Textarea
|
||||
placeholder="Describe the task, host target, and expected outcome"
|
||||
value={formState.description}
|
||||
onChange={(event) =>
|
||||
setFormState((current) => ({ ...current, description: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<Button type="submit">Create Task</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-4 xl:grid-cols-5">
|
||||
{COLUMNS.map((column) => {
|
||||
const columnTasks = tasks.filter((task) => task.status === column);
|
||||
return (
|
||||
<Card className="min-h-[420px]" key={column}>
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="flex items-center justify-between text-base">
|
||||
<span>{column}</span>
|
||||
<Badge variant="secondary">{columnTasks.length}</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{columnTasks.map((task) => (
|
||||
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-4" key={task.id}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<h3 className="font-medium text-white">{task.title}</h3>
|
||||
<Badge variant={task.priority === "Critical" ? "warning" : "outline"}>
|
||||
{task.priority}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-slate-300">{task.description || "No description"}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2 text-xs text-slate-400">
|
||||
<Badge variant="secondary">{task.assignee || "Unassigned"}</Badge>
|
||||
{task.tags.map((tag) => (
|
||||
<Badge key={tag} variant="outline">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
{task.status !== "Done" ? (
|
||||
<Button className="mt-4 w-full" size="sm" variant="outline" onClick={() => moveToDone(task.id)}>
|
||||
Mark Done
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
components/ui/badge.tsx
Normal file
29
components/ui/badge.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold tracking-wide",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-transparent bg-primary/15 text-primary",
|
||||
secondary: "border-border bg-secondary/70 text-secondary-foreground",
|
||||
outline: "border-border/70 text-foreground",
|
||||
success: "border-emerald-400/30 bg-emerald-400/10 text-emerald-300",
|
||||
warning: "border-amber-400/30 bg-amber-400/10 text-amber-300",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export function Badge({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof badgeVariants>) {
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||
}
|
||||
41
components/ui/button.tsx
Normal file
41
components/ui/button.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
outline: "border border-border bg-transparent hover:bg-secondary/40",
|
||||
ghost: "hover:bg-secondary/40",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, ...props }, ref) => (
|
||||
<button
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
46
components/ui/card.tsx
Normal file
46
components/ui/card.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Card({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-xl border border-border/70 bg-card/90 text-card-foreground shadow-panel backdrop-blur-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CardHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn("flex flex-col gap-2 p-6", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLHeadingElement>) {
|
||||
return <h3 className={cn("text-lg font-semibold tracking-tight", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLParagraphElement>) {
|
||||
return <p className={cn("text-sm text-muted-foreground", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardContent({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn("p-6 pt-0", className)} {...props} />;
|
||||
}
|
||||
17
components/ui/input.tsx
Normal file
17
components/ui/input.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<input
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background/70 px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
20
components/ui/select.tsx
Normal file
20
components/ui/select.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const Select = React.forwardRef<
|
||||
HTMLSelectElement,
|
||||
React.SelectHTMLAttributes<HTMLSelectElement>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<select
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background/70 px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
));
|
||||
Select.displayName = "Select";
|
||||
18
components/ui/textarea.tsx
Normal file
18
components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[100px] w-full rounded-md border border-input bg-background/70 px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Textarea.displayName = "Textarea";
|
||||
58
components/usage-view.tsx
Normal file
58
components/usage-view.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
export function UsageView({
|
||||
stats,
|
||||
}: {
|
||||
stats: {
|
||||
totalRequests: number;
|
||||
totalTokens: number;
|
||||
totalCost: number;
|
||||
byAgent: Record<string, { requests: number; tokens: number; cost: number }>;
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Total Requests</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-3xl font-semibold">{stats.totalRequests}</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Total Tokens</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-3xl font-semibold">{stats.totalTokens}</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Estimated Cost</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-3xl font-semibold">${stats.totalCost.toFixed(2)}</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>By Agent</CardTitle>
|
||||
<CardDescription>Aggregated from the taskboard usage tracking table.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{Object.entries(stats.byAgent).map(([agent, value]) => (
|
||||
<div className="flex items-center justify-between rounded-xl border border-white/10 bg-slate-950/40 p-4" key={agent}>
|
||||
<div>
|
||||
<p className="font-medium">{agent}</p>
|
||||
<p className="text-sm text-slate-400">{value.tokens} tokens</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Badge variant="secondary">{value.requests} req</Badge>
|
||||
<Badge variant="outline">${value.cost.toFixed(2)}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
components/wiki-view.tsx
Normal file
60
components/wiki-view.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { WikiPageSummary } from "@/lib/types";
|
||||
|
||||
export function WikiView({
|
||||
pages,
|
||||
initialPageContent,
|
||||
}: {
|
||||
pages: WikiPageSummary[];
|
||||
initialPageContent: { title: string; content: string } | null;
|
||||
}) {
|
||||
const [activeTitle, setActiveTitle] = useState(initialPageContent?.title || "Select a page");
|
||||
const [activeContent, setActiveContent] = useState(initialPageContent?.content || "");
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
const filteredPages = pages.filter((page) =>
|
||||
filter.length === 0 ? true : page.title.toLowerCase().includes(filter.toLowerCase()),
|
||||
);
|
||||
|
||||
async function openPage(filename: string) {
|
||||
const response = await fetch(`/api/wiki/${filename}`);
|
||||
const page = await response.json();
|
||||
setActiveTitle(page.metadata.title);
|
||||
setActiveContent(page.content);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-6 lg:grid-cols-[280px_minmax(0,1fr)]">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Wiki Pages</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Input placeholder="Search wiki" value={filter} onChange={(event) => setFilter(event.target.value)} />
|
||||
<div className="space-y-2">
|
||||
{filteredPages.map((page) => (
|
||||
<Button className="w-full justify-start" key={page.filename} variant="ghost" onClick={() => openPage(page.filename)}>
|
||||
{page.title}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{activeTitle}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="prose prose-invert max-w-none prose-pre:rounded-xl prose-pre:border prose-pre:border-white/10 prose-pre:bg-slate-950/70">
|
||||
{activeContent ? <ReactMarkdown>{activeContent}</ReactMarkdown> : <p className="text-slate-400">Select a wiki page to view it.</p>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user