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 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-17 09:07:32 -06:00
parent abadd469bc
commit 22e7930d0b
40 changed files with 1555 additions and 137 deletions

6
ui/src/api/activity.ts Normal file
View File

@@ -0,0 +1,6 @@
import type { ActivityEvent } from "@paperclip/shared";
import { api } from "./client";
export const activityApi = {
list: (companyId: string) => api.get<ActivityEvent[]>(`/companies/${companyId}/activity`),
};

View File

@@ -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<Agent[]>("/agents"),
list: (companyId: string) => api.get<Agent[]>(`/companies/${companyId}/agents`),
org: (companyId: string) => api.get<OrgNode[]>(`/companies/${companyId}/org`),
get: (id: string) => api.get<Agent>(`/agents/${id}`),
create: (data: Partial<Agent>) => api.post<Agent>("/agents", data),
update: (id: string, data: Partial<Agent>) => api.patch<Agent>(`/agents/${id}`, data),
remove: (id: string) => api.delete<Agent>(`/agents/${id}`),
create: (companyId: string, data: Record<string, unknown>) =>
api.post<Agent>(`/companies/${companyId}/agents`, data),
update: (id: string, data: Record<string, unknown>) => api.patch<Agent>(`/agents/${id}`, data),
pause: (id: string) => api.post<Agent>(`/agents/${id}/pause`, {}),
resume: (id: string) => api.post<Agent>(`/agents/${id}/resume`, {}),
terminate: (id: string) => api.post<Agent>(`/agents/${id}/terminate`, {}),
createKey: (id: string, name: string) => api.post<AgentKeyCreated>(`/agents/${id}/keys`, { name }),
invoke: (id: string) => api.post<HeartbeatRun>(`/agents/${id}/heartbeat/invoke`, {}),
};

15
ui/src/api/approvals.ts Normal file
View File

@@ -0,0 +1,15 @@
import type { Approval } from "@paperclip/shared";
import { api } from "./client";
export const approvalsApi = {
list: (companyId: string, status?: string) =>
api.get<Approval[]>(
`/companies/${companyId}/approvals${status ? `?status=${encodeURIComponent(status)}` : ""}`,
),
create: (companyId: string, data: Record<string, unknown>) =>
api.post<Approval>(`/companies/${companyId}/approvals`, data),
approve: (id: string, decisionNote?: string) =>
api.post<Approval>(`/approvals/${id}/approve`, { decisionNote }),
reject: (id: string, decisionNote?: string) =>
api.post<Approval>(`/approvals/${id}/reject`, { decisionNote }),
};

14
ui/src/api/companies.ts Normal file
View File

@@ -0,0 +1,14 @@
import type { Company } from "@paperclip/shared";
import { api } from "./client";
export const companiesApi = {
list: () => api.get<Company[]>("/companies"),
get: (companyId: string) => api.get<Company>(`/companies/${companyId}`),
create: (data: { name: string; description?: string | null; budgetMonthlyCents?: number }) =>
api.post<Company>("/companies", data),
update: (
companyId: string,
data: Partial<Pick<Company, "name" | "description" | "status" | "budgetMonthlyCents">>,
) => api.patch<Company>(`/companies/${companyId}`, data),
archive: (companyId: string) => api.post<Company>(`/companies/${companyId}/archive`, {}),
};

18
ui/src/api/costs.ts Normal file
View File

@@ -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<CostSummary>(`/companies/${companyId}/costs/summary`),
byAgent: (companyId: string) =>
api.get<CostByEntity[]>(`/companies/${companyId}/costs/by-agent`),
byProject: (companyId: string) =>
api.get<CostByEntity[]>(`/companies/${companyId}/costs/by-project`),
};

6
ui/src/api/dashboard.ts Normal file
View File

@@ -0,0 +1,6 @@
import type { DashboardSummary } from "@paperclip/shared";
import { api } from "./client";
export const dashboardApi = {
summary: (companyId: string) => api.get<DashboardSummary>(`/companies/${companyId}/dashboard`),
};

View File

@@ -2,9 +2,10 @@ import type { Goal } from "@paperclip/shared";
import { api } from "./client";
export const goalsApi = {
list: () => api.get<Goal[]>("/goals"),
list: (companyId: string) => api.get<Goal[]>(`/companies/${companyId}/goals`),
get: (id: string) => api.get<Goal>(`/goals/${id}`),
create: (data: Partial<Goal>) => api.post<Goal>("/goals", data),
update: (id: string, data: Partial<Goal>) => api.patch<Goal>(`/goals/${id}`, data),
create: (companyId: string, data: Record<string, unknown>) =>
api.post<Goal>(`/companies/${companyId}/goals`, data),
update: (id: string, data: Record<string, unknown>) => api.patch<Goal>(`/goals/${id}`, data),
remove: (id: string) => api.delete<Goal>(`/goals/${id}`),
};

View File

@@ -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";

View File

@@ -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<Issue[]>("/issues"),
list: (companyId: string) => api.get<Issue[]>(`/companies/${companyId}/issues`),
get: (id: string) => api.get<Issue>(`/issues/${id}`),
create: (data: Partial<Issue>) => api.post<Issue>("/issues", data),
update: (id: string, data: Partial<Issue>) => api.patch<Issue>(`/issues/${id}`, data),
create: (companyId: string, data: Record<string, unknown>) =>
api.post<Issue>(`/companies/${companyId}/issues`, data),
update: (id: string, data: Record<string, unknown>) => api.patch<Issue>(`/issues/${id}`, data),
remove: (id: string) => api.delete<Issue>(`/issues/${id}`),
checkout: (id: string, agentId: string) =>
api.post<Issue>(`/issues/${id}/checkout`, {
agentId,
expectedStatuses: ["todo", "backlog", "blocked"],
}),
release: (id: string) => api.post<Issue>(`/issues/${id}/release`, {}),
listComments: (id: string) => api.get<IssueComment[]>(`/issues/${id}/comments`),
addComment: (id: string, body: string) => api.post<IssueComment>(`/issues/${id}/comments`, { body }),
};

View File

@@ -2,9 +2,10 @@ import type { Project } from "@paperclip/shared";
import { api } from "./client";
export const projectsApi = {
list: () => api.get<Project[]>("/projects"),
list: (companyId: string) => api.get<Project[]>(`/companies/${companyId}/projects`),
get: (id: string) => api.get<Project>(`/projects/${id}`),
create: (data: Partial<Project>) => api.post<Project>("/projects", data),
update: (id: string, data: Partial<Project>) => api.patch<Project>(`/projects/${id}`, data),
create: (companyId: string, data: Record<string, unknown>) =>
api.post<Project>(`/companies/${companyId}/projects`, data),
update: (id: string, data: Record<string, unknown>) => api.patch<Project>(`/projects/${id}`, data),
remove: (id: string) => api.delete<Project>(`/projects/${id}`),
};