From 22e7930d0b91296dad7412c50b43449d57d0fc84 Mon Sep 17 00:00:00 2001 From: Forgotten Date: Tue, 17 Feb 2026 09:07:32 -0600 Subject: [PATCH] Overhaul UI with shadcn components and new pages Add shadcn/ui components (badge, button, card, input, select, separator). Add company context provider. New pages: Activity, Approvals, Companies, Costs, Org chart. Restyle existing pages (Dashboard, Agents, Issues, Goals, Projects) with shadcn components and dark theme. Update layout, sidebar navigation, and routing. Co-Authored-By: Claude Opus 4.6 --- ui/components.json | 21 ++++ ui/index.html | 2 +- ui/package.json | 9 +- ui/src/App.tsx | 12 +- ui/src/api/activity.ts | 6 + ui/src/api/agents.ts | 24 +++- ui/src/api/approvals.ts | 15 +++ ui/src/api/companies.ts | 14 +++ ui/src/api/costs.ts | 18 +++ ui/src/api/dashboard.ts | 6 + ui/src/api/goals.ts | 7 +- ui/src/api/index.ts | 5 + ui/src/api/issues.ts | 17 ++- ui/src/api/projects.ts | 7 +- ui/src/components/Layout.tsx | 38 +++++- ui/src/components/Sidebar.tsx | 13 +- ui/src/components/StatusBadge.tsx | 33 +++-- ui/src/components/ui/badge.tsx | 48 ++++++++ ui/src/components/ui/button.tsx | 64 ++++++++++ ui/src/components/ui/card.tsx | 92 ++++++++++++++ ui/src/components/ui/input.tsx | 21 ++++ ui/src/components/ui/select.tsx | 188 +++++++++++++++++++++++++++++ ui/src/components/ui/separator.tsx | 28 +++++ ui/src/context/CompanyContext.tsx | 124 +++++++++++++++++++ ui/src/hooks/useAgents.ts | 7 +- ui/src/index.css | 121 +++++++++++++++++++ ui/src/lib/utils.ts | 7 +- ui/src/main.tsx | 9 +- ui/src/pages/Activity.tsx | 49 ++++++++ ui/src/pages/Agents.tsx | 90 +++++++++++--- ui/src/pages/Approvals.tsx | 91 ++++++++++++++ ui/src/pages/Companies.tsx | 114 +++++++++++++++++ ui/src/pages/Costs.tsx | 83 +++++++++++++ ui/src/pages/Dashboard.tsx | 70 ++++++++++- ui/src/pages/Goals.tsx | 57 +++++---- ui/src/pages/Issues.tsx | 72 ++++++----- ui/src/pages/Org.tsx | 52 ++++++++ ui/src/pages/Projects.tsx | 46 ++++--- ui/tsconfig.json | 6 +- ui/vite.config.ts | 6 + 40 files changed, 1555 insertions(+), 137 deletions(-) create mode 100644 ui/components.json create mode 100644 ui/src/api/activity.ts create mode 100644 ui/src/api/approvals.ts create mode 100644 ui/src/api/companies.ts create mode 100644 ui/src/api/costs.ts create mode 100644 ui/src/api/dashboard.ts create mode 100644 ui/src/components/ui/badge.tsx create mode 100644 ui/src/components/ui/button.tsx create mode 100644 ui/src/components/ui/card.tsx create mode 100644 ui/src/components/ui/input.tsx create mode 100644 ui/src/components/ui/select.tsx create mode 100644 ui/src/components/ui/separator.tsx create mode 100644 ui/src/context/CompanyContext.tsx create mode 100644 ui/src/pages/Activity.tsx create mode 100644 ui/src/pages/Approvals.tsx create mode 100644 ui/src/pages/Companies.tsx create mode 100644 ui/src/pages/Costs.tsx create mode 100644 ui/src/pages/Org.tsx diff --git a/ui/components.json b/ui/components.json new file mode 100644 index 00000000..13e1db0b --- /dev/null +++ b/ui/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/ui/index.html b/ui/index.html index 6676856f..8b5d8752 100644 --- a/ui/index.html +++ b/ui/index.html @@ -1,5 +1,5 @@ - + diff --git a/ui/package.json b/ui/package.json index ce079633..992d3717 100644 --- a/ui/package.json +++ b/ui/package.json @@ -11,12 +11,19 @@ }, "dependencies": { "@paperclip/shared": "workspace:*", + "@radix-ui/react-slot": "^1.2.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.574.0", + "radix-ui": "^1.4.3", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-router-dom": "^7.1.5" + "react-router-dom": "^7.1.5", + "tailwind-merge": "^3.4.1" }, "devDependencies": { "@tailwindcss/vite": "^4.0.7", + "@types/node": "^25.2.3", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@vitejs/plugin-react": "^4.3.4", diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 6ecfb5cc..faa0598e 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,20 +1,30 @@ import { Routes, Route } from "react-router-dom"; import { Layout } from "./components/Layout"; import { Dashboard } from "./pages/Dashboard"; +import { Companies } from "./pages/Companies"; +import { Org } from "./pages/Org"; import { Agents } from "./pages/Agents"; import { Projects } from "./pages/Projects"; import { Issues } from "./pages/Issues"; import { Goals } from "./pages/Goals"; +import { Approvals } from "./pages/Approvals"; +import { Costs } from "./pages/Costs"; +import { Activity } from "./pages/Activity"; export function App() { return ( }> } /> + } /> + } /> } /> } /> - } /> + } /> } /> + } /> + } /> + } /> ); diff --git a/ui/src/api/activity.ts b/ui/src/api/activity.ts new file mode 100644 index 00000000..6d6c0ad2 --- /dev/null +++ b/ui/src/api/activity.ts @@ -0,0 +1,6 @@ +import type { ActivityEvent } from "@paperclip/shared"; +import { api } from "./client"; + +export const activityApi = { + list: (companyId: string) => api.get(`/companies/${companyId}/activity`), +}; diff --git a/ui/src/api/agents.ts b/ui/src/api/agents.ts index ffe65eff..ef427469 100644 --- a/ui/src/api/agents.ts +++ b/ui/src/api/agents.ts @@ -1,10 +1,24 @@ -import type { Agent } from "@paperclip/shared"; +import type { Agent, AgentKeyCreated, HeartbeatRun } from "@paperclip/shared"; import { api } from "./client"; +export interface OrgNode { + id: string; + name: string; + role: string; + status: string; + reports: OrgNode[]; +} + export const agentsApi = { - list: () => api.get("/agents"), + list: (companyId: string) => api.get(`/companies/${companyId}/agents`), + org: (companyId: string) => api.get(`/companies/${companyId}/org`), get: (id: string) => api.get(`/agents/${id}`), - create: (data: Partial) => api.post("/agents", data), - update: (id: string, data: Partial) => api.patch(`/agents/${id}`, data), - remove: (id: string) => api.delete(`/agents/${id}`), + create: (companyId: string, data: Record) => + api.post(`/companies/${companyId}/agents`, data), + update: (id: string, data: Record) => api.patch(`/agents/${id}`, data), + pause: (id: string) => api.post(`/agents/${id}/pause`, {}), + resume: (id: string) => api.post(`/agents/${id}/resume`, {}), + terminate: (id: string) => api.post(`/agents/${id}/terminate`, {}), + createKey: (id: string, name: string) => api.post(`/agents/${id}/keys`, { name }), + invoke: (id: string) => api.post(`/agents/${id}/heartbeat/invoke`, {}), }; diff --git a/ui/src/api/approvals.ts b/ui/src/api/approvals.ts new file mode 100644 index 00000000..2df9cb34 --- /dev/null +++ b/ui/src/api/approvals.ts @@ -0,0 +1,15 @@ +import type { Approval } from "@paperclip/shared"; +import { api } from "./client"; + +export const approvalsApi = { + list: (companyId: string, status?: string) => + api.get( + `/companies/${companyId}/approvals${status ? `?status=${encodeURIComponent(status)}` : ""}`, + ), + create: (companyId: string, data: Record) => + api.post(`/companies/${companyId}/approvals`, data), + approve: (id: string, decisionNote?: string) => + api.post(`/approvals/${id}/approve`, { decisionNote }), + reject: (id: string, decisionNote?: string) => + api.post(`/approvals/${id}/reject`, { decisionNote }), +}; diff --git a/ui/src/api/companies.ts b/ui/src/api/companies.ts new file mode 100644 index 00000000..78b071b3 --- /dev/null +++ b/ui/src/api/companies.ts @@ -0,0 +1,14 @@ +import type { Company } from "@paperclip/shared"; +import { api } from "./client"; + +export const companiesApi = { + list: () => api.get("/companies"), + get: (companyId: string) => api.get(`/companies/${companyId}`), + create: (data: { name: string; description?: string | null; budgetMonthlyCents?: number }) => + api.post("/companies", data), + update: ( + companyId: string, + data: Partial>, + ) => api.patch(`/companies/${companyId}`, data), + archive: (companyId: string) => api.post(`/companies/${companyId}/archive`, {}), +}; diff --git a/ui/src/api/costs.ts b/ui/src/api/costs.ts new file mode 100644 index 00000000..977b2347 --- /dev/null +++ b/ui/src/api/costs.ts @@ -0,0 +1,18 @@ +import type { CostSummary } from "@paperclip/shared"; +import { api } from "./client"; + +export interface CostByEntity { + agentId?: string | null; + projectId?: string | null; + costCents: number; + inputTokens: number; + outputTokens: number; +} + +export const costsApi = { + summary: (companyId: string) => api.get(`/companies/${companyId}/costs/summary`), + byAgent: (companyId: string) => + api.get(`/companies/${companyId}/costs/by-agent`), + byProject: (companyId: string) => + api.get(`/companies/${companyId}/costs/by-project`), +}; diff --git a/ui/src/api/dashboard.ts b/ui/src/api/dashboard.ts new file mode 100644 index 00000000..5831237e --- /dev/null +++ b/ui/src/api/dashboard.ts @@ -0,0 +1,6 @@ +import type { DashboardSummary } from "@paperclip/shared"; +import { api } from "./client"; + +export const dashboardApi = { + summary: (companyId: string) => api.get(`/companies/${companyId}/dashboard`), +}; diff --git a/ui/src/api/goals.ts b/ui/src/api/goals.ts index e04df5d8..2a5a2dd5 100644 --- a/ui/src/api/goals.ts +++ b/ui/src/api/goals.ts @@ -2,9 +2,10 @@ import type { Goal } from "@paperclip/shared"; import { api } from "./client"; export const goalsApi = { - list: () => api.get("/goals"), + list: (companyId: string) => api.get(`/companies/${companyId}/goals`), get: (id: string) => api.get(`/goals/${id}`), - create: (data: Partial) => api.post("/goals", data), - update: (id: string, data: Partial) => api.patch(`/goals/${id}`, data), + create: (companyId: string, data: Record) => + api.post(`/companies/${companyId}/goals`, data), + update: (id: string, data: Record) => api.patch(`/goals/${id}`, data), remove: (id: string) => api.delete(`/goals/${id}`), }; diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index 68e7ab2b..a10ed3a4 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -1,5 +1,10 @@ export { api } from "./client"; +export { companiesApi } from "./companies"; export { agentsApi } from "./agents"; export { projectsApi } from "./projects"; export { issuesApi } from "./issues"; export { goalsApi } from "./goals"; +export { approvalsApi } from "./approvals"; +export { costsApi } from "./costs"; +export { activityApi } from "./activity"; +export { dashboardApi } from "./dashboard"; diff --git a/ui/src/api/issues.ts b/ui/src/api/issues.ts index 9cd8f522..98c51303 100644 --- a/ui/src/api/issues.ts +++ b/ui/src/api/issues.ts @@ -1,10 +1,19 @@ -import type { Issue } from "@paperclip/shared"; +import type { Issue, IssueComment } from "@paperclip/shared"; import { api } from "./client"; export const issuesApi = { - list: () => api.get("/issues"), + list: (companyId: string) => api.get(`/companies/${companyId}/issues`), get: (id: string) => api.get(`/issues/${id}`), - create: (data: Partial) => api.post("/issues", data), - update: (id: string, data: Partial) => api.patch(`/issues/${id}`, data), + create: (companyId: string, data: Record) => + api.post(`/companies/${companyId}/issues`, data), + update: (id: string, data: Record) => api.patch(`/issues/${id}`, data), remove: (id: string) => api.delete(`/issues/${id}`), + checkout: (id: string, agentId: string) => + api.post(`/issues/${id}/checkout`, { + agentId, + expectedStatuses: ["todo", "backlog", "blocked"], + }), + release: (id: string) => api.post(`/issues/${id}/release`, {}), + listComments: (id: string) => api.get(`/issues/${id}/comments`), + addComment: (id: string, body: string) => api.post(`/issues/${id}/comments`, { body }), }; diff --git a/ui/src/api/projects.ts b/ui/src/api/projects.ts index 8e293691..8fa1b366 100644 --- a/ui/src/api/projects.ts +++ b/ui/src/api/projects.ts @@ -2,9 +2,10 @@ import type { Project } from "@paperclip/shared"; import { api } from "./client"; export const projectsApi = { - list: () => api.get("/projects"), + list: (companyId: string) => api.get(`/companies/${companyId}/projects`), get: (id: string) => api.get(`/projects/${id}`), - create: (data: Partial) => api.post("/projects", data), - update: (id: string, data: Partial) => api.patch(`/projects/${id}`, data), + create: (companyId: string, data: Record) => + api.post(`/companies/${companyId}/projects`, data), + update: (id: string, data: Record) => api.patch(`/projects/${id}`, data), remove: (id: string) => api.delete(`/projects/${id}`), }; diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index db4002c1..49cfafd5 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -1,13 +1,43 @@ import { Outlet } from "react-router-dom"; import { Sidebar } from "./Sidebar"; +import { useCompany } from "../context/CompanyContext"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; export function Layout() { + const { companies, selectedCompanyId, setSelectedCompanyId } = useCompany(); + return ( -
+
-
- -
+
+
+ + +
+
+ +
+
); } diff --git a/ui/src/components/Sidebar.tsx b/ui/src/components/Sidebar.tsx index 23a1b485..38312096 100644 --- a/ui/src/components/Sidebar.tsx +++ b/ui/src/components/Sidebar.tsx @@ -3,15 +3,20 @@ import { cn } from "../lib/utils"; const links = [ { to: "/", label: "Dashboard" }, + { to: "/companies", label: "Companies" }, + { to: "/org", label: "Org" }, { to: "/agents", label: "Agents" }, + { to: "/tasks", label: "Tasks" }, { to: "/projects", label: "Projects" }, - { to: "/issues", label: "Issues" }, { to: "/goals", label: "Goals" }, + { to: "/approvals", label: "Approvals" }, + { to: "/costs", label: "Costs" }, + { to: "/activity", label: "Activity" }, ]; export function Sidebar() { return ( -