import { useEffect, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { dashboardApi } from "../api/dashboard"; import { activityApi } from "../api/activity"; import { issuesApi } from "../api/issues"; import { agentsApi } from "../api/agents"; import { projectsApi } from "../api/projects"; import { useCompany } from "../context/CompanyContext"; import { useDialog } from "../context/DialogContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; import { MetricCard } from "../components/MetricCard"; import { EmptyState } from "../components/EmptyState"; import { StatusIcon } from "../components/StatusIcon"; import { PriorityIcon } from "../components/PriorityIcon"; import { Identity } from "../components/Identity"; import { timeAgo } from "../lib/timeAgo"; import { formatCents } from "../lib/utils"; import { Bot, CircleDot, DollarSign, ShieldCheck, LayoutDashboard, Clock } from "lucide-react"; import type { Agent, Issue } from "@paperclip/shared"; const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000; const ACTION_VERBS: Record = { "issue.created": "created", "issue.updated": "updated", "issue.checked_out": "checked out", "issue.released": "released", "issue.comment_added": "commented on", "issue.commented": "commented on", "issue.deleted": "deleted", "agent.created": "created", "agent.updated": "updated", "agent.paused": "paused", "agent.resumed": "resumed", "agent.terminated": "terminated", "agent.key_created": "created API key for", "heartbeat.invoked": "invoked heartbeat for", "heartbeat.cancelled": "cancelled heartbeat for", "approval.created": "requested approval", "approval.approved": "approved", "approval.rejected": "rejected", "project.created": "created", "project.updated": "updated", "goal.created": "created", "goal.updated": "updated", "cost.reported": "reported cost for", "cost.recorded": "recorded cost for", "company.created": "created company", "company.updated": "updated company", }; function entityLink(entityType: string, entityId: string): string | null { switch (entityType) { case "issue": return `/issues/${entityId}`; case "agent": return `/agents/${entityId}`; case "project": return `/projects/${entityId}`; case "goal": return `/goals/${entityId}`; default: return null; } } function getStaleIssues(issues: Issue[]): Issue[] { const now = Date.now(); return issues .filter( (i) => ["in_progress", "todo"].includes(i.status) && now - new Date(i.updatedAt).getTime() > STALE_THRESHOLD_MS ) .sort((a, b) => new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()); } export function Dashboard() { const { selectedCompanyId, selectedCompany, companies } = useCompany(); const { openOnboarding } = useDialog(); const { setBreadcrumbs } = useBreadcrumbs(); const navigate = useNavigate(); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), queryFn: () => agentsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); useEffect(() => { setBreadcrumbs([{ label: "Dashboard" }]); }, [setBreadcrumbs]); const { data, isLoading, error } = useQuery({ queryKey: queryKeys.dashboard(selectedCompanyId!), queryFn: () => dashboardApi.summary(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: activity } = useQuery({ queryKey: queryKeys.activity(selectedCompanyId!), queryFn: () => activityApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: issues } = useQuery({ queryKey: queryKeys.issues.list(selectedCompanyId!), queryFn: () => issuesApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const { data: projects } = useQuery({ queryKey: queryKeys.projects.list(selectedCompanyId!), queryFn: () => projectsApi.list(selectedCompanyId!), enabled: !!selectedCompanyId, }); const staleIssues = issues ? getStaleIssues(issues) : []; const agentMap = useMemo(() => { const map = new Map(); for (const a of agents ?? []) map.set(a.id, a); return map; }, [agents]); const entityNameMap = useMemo(() => { const map = new Map(); for (const i of issues ?? []) map.set(`issue:${i.id}`, i.title); for (const a of agents ?? []) map.set(`agent:${a.id}`, a.name); for (const p of projects ?? []) map.set(`project:${p.id}`, p.name); return map; }, [issues, agents, projects]); const agentName = (id: string | null) => { if (!id || !agents) return null; return agents.find((a) => a.id === id)?.name ?? null; }; if (!selectedCompanyId) { if (companies.length === 0) { return ( ); } return ( ); } return (
{selectedCompany && (

{selectedCompany.name}

)} {isLoading &&

Loading...

} {error &&

{error.message}

} {data && ( <>
{/* Recent Activity */} {activity && activity.length > 0 && (

Recent Activity

{activity.slice(0, 10).map((event) => { const verb = ACTION_VERBS[event.action] ?? event.action.replace(/[._]/g, " "); const name = entityNameMap.get(`${event.entityType}:${event.entityId}`); const link = entityLink(event.entityType, event.entityId); const actor = event.actorType === "agent" ? agentMap.get(event.actorId) : null; return (
navigate(link) : undefined} >
{verb} {name && {name}}
{timeAgo(event.createdAt)}
); })}
)} {/* Stale Tasks */}

Stale Tasks

{staleIssues.length === 0 ? (

No stale tasks. All work is up to date.

) : (
{staleIssues.slice(0, 10).map((issue) => (
navigate(`/issues/${issue.id}`)} > {issue.title} {issue.assigneeAgentId && (() => { const name = agentName(issue.assigneeAgentId); return name ? : {issue.assigneeAgentId.slice(0, 8)}; })()} {timeAgo(issue.updatedAt)}
))}
)}
)}
); }