mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
I mean, it's at a good place rn...
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useStopAgent } from "@/hooks/use-agents";
|
||||
import { AgentStatusResponse } from "@/types";
|
||||
import { AgentDefinition } from "@/lib/agent-definitions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { MoreHorizontal, Activity, Square } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { AgentStateBadge } from "./agent-state-badge";
|
||||
import { SpawnAgentDialog } from "./spawn-agent-dialog";
|
||||
|
||||
interface AgentCardProps {
|
||||
agent: AgentDefinition;
|
||||
agentStatus: AgentStatusResponse | null;
|
||||
}
|
||||
|
||||
export function AgentCard({ agent, agentStatus }: AgentCardProps) {
|
||||
const stopAgent = useStopAgent();
|
||||
const state = agentStatus?.state || "stopped";
|
||||
const isActive = ["running", "ready", "starting", "waiting_long"].includes(state);
|
||||
|
||||
const handleStop = async (graceful: boolean) => {
|
||||
try {
|
||||
await stopAgent.mutateAsync({ agentId: agent.id, graceful });
|
||||
const message = graceful ? "stopping gracefully" : "force stopped";
|
||||
toast.success("Agent " + agent.name + " " + message);
|
||||
} catch {
|
||||
toast.error("Failed to stop agent");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={isActive ? "border-green-500/50" : ""}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">{agent.name || "Unknown Agent"}</CardTitle>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{!isActive && (
|
||||
<SpawnAgentDialog agentId={agent.id} agentName={agent.name} />
|
||||
)}
|
||||
{isActive && (
|
||||
<>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={"/agents/" + agent.id}>
|
||||
<Activity className="h-4 w-4 mr-2" />
|
||||
View Details
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => handleStop(true)}>
|
||||
<Square className="h-4 w-4 mr-2" />
|
||||
Stop Gracefully
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleStop(false)}
|
||||
className="text-red-600"
|
||||
>
|
||||
<Square className="h-4 w-4 mr-2" />
|
||||
Force Stop
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<CardDescription className="text-xs">
|
||||
{agent.role?.replace(/_/g, " ") || "N/A"}
|
||||
{agent.team && " • " + agent.team.replace(/_/g, " ")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AgentStateBadge state={state} />
|
||||
{agentStatus?.task_id && (
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Task: {agentStatus.task_id.slice(0, 8)}...
|
||||
</p>
|
||||
)}
|
||||
{agentStatus?.waiting_for && (
|
||||
<p className="text-xs text-yellow-600 mt-2 truncate">
|
||||
Waiting: {agentStatus.waiting_for}
|
||||
</p>
|
||||
)}
|
||||
{agentStatus && agentStatus.error_count > 0 && (
|
||||
<p className="text-xs text-red-500 mt-2">
|
||||
Errors: {agentStatus.error_count}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { AgentStatusResponse } from "@/types";
|
||||
import { AgentDefinition } from "@/lib/agent-definitions";
|
||||
import { Card, CardHeader } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { AgentCard } from "./agent-card";
|
||||
|
||||
interface AgentGridProps {
|
||||
title: string;
|
||||
agents: AgentDefinition[];
|
||||
agentStatuses: Record<string, AgentStatusResponse>;
|
||||
isLoading: boolean;
|
||||
columns?: number;
|
||||
}
|
||||
|
||||
export function AgentGrid({
|
||||
title,
|
||||
agents,
|
||||
agentStatuses,
|
||||
isLoading,
|
||||
columns = 4
|
||||
}: AgentGridProps) {
|
||||
const gridCols = {
|
||||
3: "md:grid-cols-3",
|
||||
4: "md:grid-cols-3 lg:grid-cols-4",
|
||||
5: "md:grid-cols-3 lg:grid-cols-5",
|
||||
}[columns] || "md:grid-cols-3 lg:grid-cols-4";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">{title}</h2>
|
||||
<div className={"grid gap-4 " + gridCols}>
|
||||
{isLoading ? (
|
||||
Array.from({ length: agents.length || 3 }).map((_, i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-32" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
agents.map((agent) => (
|
||||
<AgentCard
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
agentStatus={agentStatuses[agent.id] || null}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useAgentDefinitions } from "@/hooks/use-agents";
|
||||
import { Team, AgentRole } from "@/types";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { User, Users } from "lucide-react";
|
||||
import { resolveToSlug } from "@/lib/agent-utils";
|
||||
|
||||
interface AgentSelectorProps {
|
||||
value: string | null;
|
||||
onChange: (value: string | null) => void;
|
||||
placeholder?: string;
|
||||
filterByTeam?: Team;
|
||||
filterByRoles?: AgentRole[];
|
||||
disabled?: boolean;
|
||||
allowClear?: boolean;
|
||||
}
|
||||
|
||||
// Role display names
|
||||
const ROLE_LABELS: Record<AgentRole, string> = {
|
||||
[AgentRole.SYSTEM]: "System",
|
||||
[AgentRole.CEO]: "CEO",
|
||||
[AgentRole.PRODUCT_OWNER]: "Product Owner",
|
||||
[AgentRole.HEAD_MARKETING]: "Head Marketing",
|
||||
[AgentRole.AUDITOR]: "Auditor",
|
||||
[AgentRole.MAIN_PM]: "Main PM",
|
||||
[AgentRole.CELL_PM]: "Cell PM",
|
||||
[AgentRole.DEVELOPER]: "Developer",
|
||||
[AgentRole.QA]: "QA",
|
||||
[AgentRole.DOCUMENTER]: "Documenter",
|
||||
};
|
||||
|
||||
export function AgentSelector({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "Select agent...",
|
||||
filterByTeam,
|
||||
filterByRoles,
|
||||
disabled = false,
|
||||
allowClear = true,
|
||||
}: AgentSelectorProps) {
|
||||
const { data: agents = [], isLoading } = useAgentDefinitions();
|
||||
|
||||
// Group agents by team
|
||||
const groupedAgents = useMemo(() => {
|
||||
let filtered = agents;
|
||||
|
||||
// Apply team filter - also match by role for Board and Main PM
|
||||
if (filterByTeam) {
|
||||
filtered = filtered.filter((a) => {
|
||||
// Direct team match
|
||||
if (a.team === filterByTeam) return true;
|
||||
|
||||
// For Board team, also include board-level roles
|
||||
if (filterByTeam === Team.BOARD && (
|
||||
a.role === AgentRole.PRODUCT_OWNER ||
|
||||
a.role === AgentRole.HEAD_MARKETING ||
|
||||
a.role === AgentRole.AUDITOR
|
||||
)) return true;
|
||||
|
||||
// For Main PM team, also include Main PM role
|
||||
if (filterByTeam === Team.MAIN_PM && a.role === AgentRole.MAIN_PM) return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply role filter
|
||||
if (filterByRoles && filterByRoles.length > 0) {
|
||||
filtered = filtered.filter((a) => a.role && filterByRoles.includes(a.role));
|
||||
}
|
||||
|
||||
// Group by team following org hierarchy
|
||||
const groups: Record<string, typeof filtered> = {
|
||||
board: [],
|
||||
main_pm: [],
|
||||
backend: [],
|
||||
frontend: [],
|
||||
ux_ui: [],
|
||||
marketing: [],
|
||||
};
|
||||
|
||||
for (const agent of filtered) {
|
||||
if (agent.team === Team.BOARD ||
|
||||
agent.role === AgentRole.PRODUCT_OWNER ||
|
||||
agent.role === AgentRole.HEAD_MARKETING ||
|
||||
agent.role === AgentRole.AUDITOR) {
|
||||
groups.board.push(agent);
|
||||
} else if (agent.team === Team.MAIN_PM || agent.role === AgentRole.MAIN_PM) {
|
||||
groups.main_pm.push(agent);
|
||||
} else if (agent.team === Team.BACKEND) {
|
||||
groups.backend.push(agent);
|
||||
} else if (agent.team === Team.FRONTEND) {
|
||||
groups.frontend.push(agent);
|
||||
} else if (agent.team === Team.UX_UI) {
|
||||
groups.ux_ui.push(agent);
|
||||
} else if (agent.team === Team.MARKETING) {
|
||||
groups.marketing.push(agent);
|
||||
}
|
||||
// Note: Agents without team are not grouped - they remain ungrouped
|
||||
// The orchestrator handles automatic routing for unassigned tasks
|
||||
}
|
||||
|
||||
return groups;
|
||||
}, [agents, filterByTeam, filterByRoles]);
|
||||
|
||||
// Find selected agent for display (resolve UUID to slug if needed)
|
||||
const selectedAgent = useMemo(() => {
|
||||
if (!value) return null;
|
||||
const resolvedValue = resolveToSlug(value);
|
||||
return agents.find((a) => a.id === resolvedValue || a.id === value);
|
||||
}, [agents, value]);
|
||||
|
||||
const handleValueChange = (newValue: string) => {
|
||||
if (newValue === "__clear__") {
|
||||
onChange(null);
|
||||
} else {
|
||||
onChange(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
// Resolve value to slug for proper Select matching
|
||||
const selectValue = value ? resolveToSlug(value) || value : "";
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={selectValue}
|
||||
onValueChange={handleValueChange}
|
||||
disabled={disabled || isLoading}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={placeholder}>
|
||||
{selectedAgent ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="h-4 w-4" />
|
||||
<span>{selectedAgent.name}</span>
|
||||
{selectedAgent.role && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{ROLE_LABELS[selectedAgent.role] || selectedAgent.role}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
placeholder
|
||||
)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{allowClear && value && (
|
||||
<SelectItem value="__clear__" className="text-muted-foreground">
|
||||
<span className="flex items-center gap-2">
|
||||
<Users className="h-4 w-4" />
|
||||
Unassigned
|
||||
</span>
|
||||
</SelectItem>
|
||||
)}
|
||||
|
||||
{/* Board */}
|
||||
{groupedAgents.board.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Board</SelectLabel>
|
||||
{groupedAgents.board.map((agent) => (
|
||||
<SelectItem key={agent.id} value={agent.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{agent.name}</span>
|
||||
{agent.role && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{ROLE_LABELS[agent.role] || agent.role}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
)}
|
||||
|
||||
{/* Main PM */}
|
||||
{groupedAgents.main_pm.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Main PM</SelectLabel>
|
||||
{groupedAgents.main_pm.map((agent) => (
|
||||
<SelectItem key={agent.id} value={agent.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{agent.name}</span>
|
||||
{agent.role && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{ROLE_LABELS[agent.role] || agent.role}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
)}
|
||||
|
||||
{/* Backend */}
|
||||
{groupedAgents.backend.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Backend Team</SelectLabel>
|
||||
{groupedAgents.backend.map((agent) => (
|
||||
<SelectItem key={agent.id} value={agent.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{agent.name}</span>
|
||||
{agent.role && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{ROLE_LABELS[agent.role] || agent.role}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
)}
|
||||
|
||||
{/* Frontend */}
|
||||
{groupedAgents.frontend.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Frontend Team</SelectLabel>
|
||||
{groupedAgents.frontend.map((agent) => (
|
||||
<SelectItem key={agent.id} value={agent.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{agent.name}</span>
|
||||
{agent.role && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{ROLE_LABELS[agent.role] || agent.role}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
)}
|
||||
|
||||
{/* UX/UI */}
|
||||
{groupedAgents.ux_ui.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>UX/UI Team</SelectLabel>
|
||||
{groupedAgents.ux_ui.map((agent) => (
|
||||
<SelectItem key={agent.id} value={agent.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{agent.name}</span>
|
||||
{agent.role && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{ROLE_LABELS[agent.role] || agent.role}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
)}
|
||||
|
||||
{/* Marketing */}
|
||||
{groupedAgents.marketing.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Marketing Team</SelectLabel>
|
||||
{groupedAgents.marketing.map((agent) => (
|
||||
<SelectItem key={agent.id} value={agent.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{agent.name}</span>
|
||||
{agent.role && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{ROLE_LABELS[agent.role] || agent.role}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Clock, RefreshCw, Activity, AlertTriangle, Square } from "lucide-react";
|
||||
|
||||
// Agent states as returned by backend orchestrator
|
||||
type AgentStateString =
|
||||
| "idle"
|
||||
| "starting"
|
||||
| "ready"
|
||||
| "running"
|
||||
| "waiting_long"
|
||||
| "error"
|
||||
| "stopped"
|
||||
| "terminated";
|
||||
|
||||
const stateColors: Record<string, string> = {
|
||||
idle: "bg-gray-500",
|
||||
starting: "bg-yellow-500",
|
||||
ready: "bg-blue-500",
|
||||
running: "bg-green-500",
|
||||
waiting_long: "bg-orange-500",
|
||||
error: "bg-red-500",
|
||||
stopped: "bg-gray-400",
|
||||
terminated: "bg-gray-600",
|
||||
};
|
||||
|
||||
const stateIcons: Record<string, React.ReactNode> = {
|
||||
idle: <Clock className="h-4 w-4" />,
|
||||
starting: <RefreshCw className="h-4 w-4 animate-spin" />,
|
||||
ready: <Activity className="h-4 w-4" />,
|
||||
running: <Activity className="h-4 w-4" />,
|
||||
waiting_long: <AlertTriangle className="h-4 w-4" />,
|
||||
error: <AlertTriangle className="h-4 w-4" />,
|
||||
stopped: <Square className="h-4 w-4" />,
|
||||
terminated: <Square className="h-4 w-4" />,
|
||||
};
|
||||
|
||||
interface AgentStateBadgeProps {
|
||||
state: AgentStateString | string;
|
||||
showIcon?: boolean;
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
export function AgentStateBadge({ state, showIcon = true, size = "md" }: AgentStateBadgeProps) {
|
||||
const sizeClasses = {
|
||||
sm: "text-xs px-2 py-0.5",
|
||||
md: "text-sm px-2.5 py-0.5",
|
||||
lg: "text-lg px-3 py-1",
|
||||
};
|
||||
|
||||
const color = stateColors[state] || "bg-gray-400";
|
||||
const icon = stateIcons[state] || <Square className="h-4 w-4" />;
|
||||
|
||||
return (
|
||||
<Badge className={`${color} text-white ${sizeClasses[size]}`}>
|
||||
{showIcon && <span className="mr-1">{icon}</span>}
|
||||
{state.replace(/_/g, " ")}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export { stateColors, stateIcons };
|
||||
@@ -0,0 +1,73 @@
|
||||
import Link from "next/link";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { AgentStatusResponse } from "@/types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Activity, FileText, Clock, AlertCircle } from "lucide-react";
|
||||
import { AgentStateBadge } from "./agent-state-badge";
|
||||
|
||||
interface AgentStatusCardsProps {
|
||||
agent: AgentStatusResponse;
|
||||
}
|
||||
|
||||
export function AgentStatusCards({ agent }: AgentStatusCardsProps) {
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">State</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AgentStateBadge state={agent.state} size="lg" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Current Task</CardTitle>
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{agent.task_id ? (
|
||||
<Link href={"/tasks/" + agent.task_id} className="text-blue-500 hover:underline">
|
||||
{agent.task_id.slice(0, 8)}...
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-muted-foreground">No task assigned</span>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Started At</CardTitle>
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{agent.started_at ? (
|
||||
<span>{formatDistanceToNow(new Date(agent.started_at))} ago</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">Not started</span>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Error Count</CardTitle>
|
||||
<AlertCircle className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<span className={agent.error_count > 0 ? "text-red-600 font-semibold" : ""}>
|
||||
{agent.error_count}
|
||||
</span>
|
||||
{agent.waiting_for && (
|
||||
<p className="text-xs text-yellow-600 mt-1 truncate">
|
||||
Waiting: {agent.waiting_for}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export { AgentStateBadge, stateColors, stateIcons } from "./agent-state-badge";
|
||||
export { AgentCard } from "./agent-card";
|
||||
export { AgentGrid } from "./agent-grid";
|
||||
export { AgentStatusCards } from "./agent-status-cards";
|
||||
export { SpawnAgentDialog } from "./spawn-agent-dialog";
|
||||
export { ResolveWaitDialog } from "./resolve-wait-dialog";
|
||||
export { AgentStreamViewer } from "./stream-viewer";
|
||||
export { OrchestratorStatusCards } from "./orchestrator-status";
|
||||
export { WaitingAgentsAlert } from "./waiting-agents-alert";
|
||||
@@ -0,0 +1,77 @@
|
||||
import { OrchestratorStatus as OrchestratorStatusType } from "@/types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Server, Users, Clock, Activity } from "lucide-react";
|
||||
|
||||
interface OrchestratorStatusCardsProps {
|
||||
status: OrchestratorStatusType | undefined;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function OrchestratorStatusCards({ status, isLoading }: OrchestratorStatusCardsProps) {
|
||||
// Calculate running agents from by_state
|
||||
const runningCount = status?.by_state?.running || 0;
|
||||
const readyCount = status?.by_state?.ready || 0;
|
||||
const activeCount = runningCount + readyCount;
|
||||
const isRunning = status && status.total_agents > 0;
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Orchestrator</CardTitle>
|
||||
<Server className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-8 w-24" />
|
||||
) : (
|
||||
<Badge className={isRunning ? "bg-green-500" : "bg-red-500"}>
|
||||
{isRunning ? "Running" : "Stopped"}
|
||||
</Badge>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Agents</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-8 w-12" />
|
||||
) : (
|
||||
<div className="text-2xl font-bold">{status?.total_agents || 0}</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Active</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-8 w-12" />
|
||||
) : (
|
||||
<div className="text-2xl font-bold">{activeCount}</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Waiting</CardTitle>
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-8 w-12" />
|
||||
) : (
|
||||
<div className="text-2xl font-bold">{status?.waiting_count || 0}</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useResolveWait } from "@/hooks/use-agents";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Send } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface ResolveWaitDialogProps {
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [resolution, setResolution] = useState("");
|
||||
const resolveWait = useResolveWait();
|
||||
|
||||
const handleResolve = async () => {
|
||||
if (!resolution.trim()) {
|
||||
toast.error("Please provide a resolution");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await resolveWait.mutateAsync({ agentId, resolution });
|
||||
toast.success("Resolution sent to agent");
|
||||
setOpen(false);
|
||||
setResolution("");
|
||||
} catch {
|
||||
toast.error("Failed to resolve wait");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Send className="h-4 w-4 mr-2" />
|
||||
Resolve Wait
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Resolve Agent Wait</DialogTitle>
|
||||
<DialogDescription>
|
||||
Provide context or instructions to help the agent continue.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="resolution">Resolution Message</Label>
|
||||
<Textarea
|
||||
id="resolution"
|
||||
value={resolution}
|
||||
onChange={(e) => setResolution(e.target.value)}
|
||||
placeholder="Provide the information or decision the agent needs to continue..."
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleResolve} disabled={resolveWait.isPending}>
|
||||
{resolveWait.isPending ? "Sending..." : "Send Resolution"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useSpawnAgent } from "@/hooks/use-agents";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { Play } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface SpawnAgentDialogProps {
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
trigger?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function SpawnAgentDialog({ agentId, agentName, trigger }: SpawnAgentDialogProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [taskId, setTaskId] = useState("");
|
||||
const [initialPrompt, setInitialPrompt] = useState("");
|
||||
const spawnAgent = useSpawnAgent();
|
||||
|
||||
const handleSpawn = async () => {
|
||||
try {
|
||||
await spawnAgent.mutateAsync({
|
||||
agentId,
|
||||
request: {
|
||||
task_id: taskId || undefined,
|
||||
initial_prompt: initialPrompt || undefined,
|
||||
},
|
||||
});
|
||||
toast.success(`Agent ${agentName} spawned successfully`);
|
||||
setOpen(false);
|
||||
resetForm();
|
||||
} catch {
|
||||
toast.error("Failed to spawn agent");
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setTaskId("");
|
||||
setInitialPrompt("");
|
||||
};
|
||||
|
||||
const defaultTrigger = (
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
Spawn
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{trigger || defaultTrigger}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Spawn {agentName}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Start this agent with optional task assignment and initial prompt.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="taskId">Task ID (optional)</Label>
|
||||
<Input
|
||||
id="taskId"
|
||||
value={taskId}
|
||||
onChange={(e) => setTaskId(e.target.value)}
|
||||
placeholder="UUID of task to assign"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="initialPrompt">Initial Prompt (optional)</Label>
|
||||
<Input
|
||||
id="initialPrompt"
|
||||
value={initialPrompt}
|
||||
onChange={(e) => setInitialPrompt(e.target.value)}
|
||||
placeholder="Initial instructions for the agent"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSpawn} disabled={spawnAgent.isPending}>
|
||||
{spawnAgent.isPending ? "Spawning..." : "Spawn Agent"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAgentStream, ConnectionState } from "@/hooks/use-websocket";
|
||||
import { Wifi, WifiOff, Loader2, Trash2 } from "lucide-react";
|
||||
|
||||
interface AgentStreamViewerProps {
|
||||
agentId: string;
|
||||
agentName?: string;
|
||||
}
|
||||
|
||||
const stateColors: Record<ConnectionState, string> = {
|
||||
connected: "bg-green-500",
|
||||
connecting: "bg-yellow-500",
|
||||
reconnecting: "bg-orange-500",
|
||||
disconnected: "bg-gray-500",
|
||||
};
|
||||
|
||||
const stateLabels: Record<ConnectionState, string> = {
|
||||
connected: "Connected",
|
||||
connecting: "Connecting...",
|
||||
reconnecting: "Reconnecting...",
|
||||
disconnected: "Disconnected",
|
||||
};
|
||||
|
||||
export function AgentStreamViewer({ agentId, agentName }: AgentStreamViewerProps) {
|
||||
const {
|
||||
state,
|
||||
streamOutput,
|
||||
streamChunks,
|
||||
clearMessages,
|
||||
isConnected,
|
||||
isConnecting
|
||||
} = useAgentStream(agentId);
|
||||
|
||||
const outputRef = useRef<HTMLPreElement>(null);
|
||||
|
||||
// Auto-scroll to bottom when new content arrives
|
||||
useEffect(() => {
|
||||
if (outputRef.current) {
|
||||
outputRef.current.scrollTop = outputRef.current.scrollHeight;
|
||||
}
|
||||
}, [streamOutput]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
Agent Output Stream
|
||||
{isConnecting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-yellow-500" />
|
||||
) : isConnected ? (
|
||||
<Wifi className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<WifiOff className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Real-time LLM output from {agentName || agentId}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className={stateColors[state] + " text-white"}>
|
||||
{stateLabels[state]}
|
||||
</Badge>
|
||||
{streamChunks.length > 0 && (
|
||||
<Button variant="ghost" size="icon" onClick={clearMessages}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre
|
||||
ref={outputRef}
|
||||
className="bg-slate-950 text-slate-50 rounded-lg p-4 h-96 overflow-auto font-mono text-sm whitespace-pre-wrap"
|
||||
>
|
||||
{streamOutput || (
|
||||
<span className="text-slate-500">
|
||||
{isConnected
|
||||
? "Waiting for agent output..."
|
||||
: isConnecting
|
||||
? "Connecting to agent stream..."
|
||||
: "Agent stream disconnected"}
|
||||
</span>
|
||||
)}
|
||||
</pre>
|
||||
<div className="flex justify-between items-center mt-2 text-sm text-muted-foreground">
|
||||
<span>{streamChunks.length} chunks received</span>
|
||||
<span>{streamOutput.length} characters</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import Link from "next/link";
|
||||
import { WaitingAgent } from "@/types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { getAgentDisplayName } from "@/lib/agent-utils";
|
||||
|
||||
interface WaitingAgentsAlertProps {
|
||||
waitingAgents: WaitingAgent[];
|
||||
}
|
||||
|
||||
export function WaitingAgentsAlert({ waitingAgents }: WaitingAgentsAlertProps) {
|
||||
if (waitingAgents.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Card className="border-orange-500/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-orange-500 flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5" />
|
||||
Agents Waiting for Input
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{waitingAgents.map((agent) => (
|
||||
<div key={agent.agent_id} className="flex items-center justify-between p-2 bg-muted rounded">
|
||||
<div>
|
||||
<span className="font-medium">{getAgentDisplayName(agent.agent_id)}</span>
|
||||
<span className="text-muted-foreground ml-2">waiting for: {agent.waiting_for}</span>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href={"/agents/" + agent.agent_id}>Resolve</Link>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user