mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 03:10:38 +09:00
Add live ActiveAgentsPanel with real-time transcript feed, SidebarContext for responsive sidebar state, agent config form with reasoning effort, improved inbox with failed run alerts, enriched issue detail with project picker, and various component refinements across pages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
128 lines
3.6 KiB
TypeScript
128 lines
3.6 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { agentsApi, type OrgNode } from "../api/agents";
|
|
import { useCompany } from "../context/CompanyContext";
|
|
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
|
import { queryKeys } from "../lib/queryKeys";
|
|
import { StatusBadge } from "../components/StatusBadge";
|
|
import { EmptyState } from "../components/EmptyState";
|
|
import { ChevronRight, GitBranch } from "lucide-react";
|
|
import { cn } from "../lib/utils";
|
|
|
|
function OrgTree({
|
|
nodes,
|
|
depth = 0,
|
|
onSelect,
|
|
}: {
|
|
nodes: OrgNode[];
|
|
depth?: number;
|
|
onSelect: (id: string) => void;
|
|
}) {
|
|
return (
|
|
<div>
|
|
{nodes.map((node) => (
|
|
<OrgTreeNode key={node.id} node={node} depth={depth} onSelect={onSelect} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function OrgTreeNode({
|
|
node,
|
|
depth,
|
|
onSelect,
|
|
}: {
|
|
node: OrgNode;
|
|
depth: number;
|
|
onSelect: (id: string) => void;
|
|
}) {
|
|
const [expanded, setExpanded] = useState(true);
|
|
const hasChildren = node.reports.length > 0;
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors cursor-pointer hover:bg-accent/50"
|
|
style={{ paddingLeft: `${depth * 16 + 12}px` }}
|
|
onClick={() => onSelect(node.id)}
|
|
>
|
|
{hasChildren ? (
|
|
<button
|
|
className="p-0.5"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setExpanded(!expanded);
|
|
}}
|
|
>
|
|
<ChevronRight
|
|
className={cn("h-3 w-3 transition-transform", expanded && "rotate-90")}
|
|
/>
|
|
</button>
|
|
) : (
|
|
<span className="w-4" />
|
|
)}
|
|
<span
|
|
className={cn(
|
|
"h-2 w-2 rounded-full shrink-0",
|
|
node.status === "active"
|
|
? "bg-green-400"
|
|
: node.status === "paused"
|
|
? "bg-yellow-400"
|
|
: node.status === "pending_approval"
|
|
? "bg-amber-400"
|
|
: node.status === "error"
|
|
? "bg-red-400"
|
|
: "bg-neutral-400"
|
|
)}
|
|
/>
|
|
<span className="font-medium flex-1">{node.name}</span>
|
|
<span className="text-xs text-muted-foreground">{node.role}</span>
|
|
<StatusBadge status={node.status} />
|
|
</div>
|
|
{hasChildren && expanded && (
|
|
<OrgTree nodes={node.reports} depth={depth + 1} onSelect={onSelect} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function Org() {
|
|
const { selectedCompanyId } = useCompany();
|
|
const { setBreadcrumbs } = useBreadcrumbs();
|
|
const navigate = useNavigate();
|
|
|
|
useEffect(() => {
|
|
setBreadcrumbs([{ label: "Org Chart" }]);
|
|
}, [setBreadcrumbs]);
|
|
|
|
const { data, isLoading, error } = useQuery({
|
|
queryKey: queryKeys.org(selectedCompanyId!),
|
|
queryFn: () => agentsApi.org(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId,
|
|
});
|
|
|
|
if (!selectedCompanyId) {
|
|
return <EmptyState icon={GitBranch} message="Select a company to view org chart." />;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
|
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
|
|
|
{data && data.length === 0 && (
|
|
<EmptyState
|
|
icon={GitBranch}
|
|
message="No agents in the organization. Create agents to build your org chart."
|
|
/>
|
|
)}
|
|
|
|
{data && data.length > 0 && (
|
|
<div className="border border-border py-1">
|
|
<OrgTree nodes={data} onSelect={(id) => navigate(`/agents/${id}`)} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|