From 2c12f68b4e9d6de741eb9635e387a4430ee3e37a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 7 Jun 2026 18:07:13 -0400 Subject: [PATCH] Add five SVG dashboard charts to homepage AgentWorkloadChart (bar), BudgetUtilizationChart (stacked bar), RunActivityChart (7-day bar), SuccessRateChart (ring gauge), and CostTrendChart (7-day line). Pure SVG, no external charting library. Charts render from snapshot data in the DashboardCharts module. --- .../src/renderer/src/pages/HomePage.tsx | 12 +- .../src/pages/home/DashboardCharts.tsx | 179 ++++++++++++++++++ 2 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 AgentCompany-New/src/renderer/src/pages/home/DashboardCharts.tsx diff --git a/AgentCompany-New/src/renderer/src/pages/HomePage.tsx b/AgentCompany-New/src/renderer/src/pages/HomePage.tsx index 4a9dbe57..23a3f083 100644 --- a/AgentCompany-New/src/renderer/src/pages/HomePage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/HomePage.tsx @@ -12,6 +12,7 @@ import type { TaskRunRecord, } from "../../../shared/domain"; import { NewTaskDialog } from "./NewTaskDialog"; +import { AgentWorkloadChart, BudgetUtilizationChart, CostTrendChart, RunActivityChart, SuccessRateChart } from "./home/DashboardCharts"; import type { PageProps } from "./types"; import { activeOrgId, formatRelative, formatTime, money, requireActiveEmployee, requireActiveOrganization } from "./types"; @@ -37,6 +38,7 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) { const taskRuns = useMemo(() => snapshot.taskRuns.filter((run) => run.orgId === orgId), [snapshot.taskRuns, orgId]); const chatMessages = useMemo(() => snapshot.chatMessages.filter((m) => m.orgId === orgId), [snapshot.chatMessages, orgId]); const chatSpaces = useMemo(() => snapshot.chatSpaces.filter((s) => s.orgId === orgId), [snapshot.chatSpaces, orgId]); + const providerRequests = useMemo(() => snapshot.providerRequests.filter((r) => r.orgId === orgId), [snapshot.providerRequests, orgId]); const [newTaskOpen, setNewTaskOpen] = useState(false); const pendingApprovals = approvals.filter((approval) => approval.state === "pending"); @@ -82,8 +84,16 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) { +
+ + + + + +
+
- navigate("work")} /> + navigate("runs")} /> navigate("chat")} />
diff --git a/AgentCompany-New/src/renderer/src/pages/home/DashboardCharts.tsx b/AgentCompany-New/src/renderer/src/pages/home/DashboardCharts.tsx new file mode 100644 index 00000000..5b77a392 --- /dev/null +++ b/AgentCompany-New/src/renderer/src/pages/home/DashboardCharts.tsx @@ -0,0 +1,179 @@ +import type { AgentRecord, ProviderRecord, TaskRecord, TaskRunRecord } from "../../../../shared/domain"; +import { TASK_STATUS } from "../../../../shared/domain"; + +const CHART_H = 120; +const CHART_W = 260; +const BAR_GAP = 4; + +export function AgentWorkloadChart({ agents, tasks }: { agents: AgentRecord[]; tasks: TaskRecord[] }) { + const data = agents.slice(0, 8).map((a) => ({ + name: a.name.split(/\s+/)[0], + count: tasks.filter((t) => t.assigneeAgentId === a.id && t.status !== TASK_STATUS.DONE).length, + })); + const max = Math.max(1, ...data.map((d) => d.count)); + const barW = data.length > 0 ? (CHART_W - (data.length - 1) * BAR_GAP) / data.length : 0; + + return ( + + + {data.map((d, i) => { + const h = (d.count / max) * (CHART_H - 20); + const x = i * (barW + BAR_GAP); + return ( + + + {d.name} + + ); + })} + + + ); +} + +export function BudgetUtilizationChart({ agents, providers }: { agents: AgentRecord[]; providers: ProviderRecord[] }) { + const connected = providers.filter((p) => p.enabled); + const data = connected.slice(0, 6).map((p) => { + const allocated = agents.filter((a) => a.modelProviderId === p.id).reduce((s, a) => s + a.budgetCents, 0); + return { label: p.label.split(/\s+/)[0], cents: allocated }; + }); + const total = Math.max(1, data.reduce((s, d) => s + d.cents, 0)); + + return ( + + + {(() => { + let cumX = 0; + return data.map((d, i) => { + const w = (d.cents / total) * CHART_W; + const x = cumX; + cumX += w; + const colors = ["var(--accent)", "var(--green)", "var(--amber)", "var(--violet)", "var(--red)", "var(--text-3)"]; + return ( + + + {d.label} + ${Math.round(d.cents / 100)} + + ); + }); + })()} + + + ); +} + +export function RunActivityChart({ taskRuns }: { taskRuns: TaskRunRecord[] }) { + const buckets = last7DayBuckets(taskRuns.map((r) => r.createdAt)); + const max = Math.max(1, ...buckets.map((b) => b.count)); + const barW = (CHART_W - 6 * BAR_GAP) / 7; + + return ( + + + {buckets.map((b, i) => { + const h = (b.count / max) * (CHART_H - 24); + const x = i * (barW + BAR_GAP); + return ( + + + {b.label} + + ); + })} + + + ); +} + +export function SuccessRateChart({ taskRuns }: { taskRuns: TaskRunRecord[] }) { + const completed = taskRuns.filter((r) => r.state === "completed").length; + const failed = taskRuns.filter((r) => r.state === "failed").length; + const total = completed + failed; + const rate = total > 0 ? Math.round((completed / total) * 100) : 0; + const circumference = 2 * Math.PI * 42; + const offset = circumference - (rate / 100) * circumference; + + return ( + + + + + {rate}% + +
+ {completed} passed ยท {failed} failed +
+
+ ); +} + +export function CostTrendChart({ providerRequests }: { providerRequests: Array<{ createdAt: string; inputCharCount: number; outputCharCount: number }> }) { + const buckets = last7DayBuckets(providerRequests.map((r) => r.createdAt)); + const charBuckets = buckets.map((b, i) => { + const dayStart = dayStartMs(i); + const dayEnd = dayStart + 86400000; + const dayRequests = providerRequests.filter((r) => { + const t = new Date(r.createdAt).getTime(); + return t >= dayStart && t < dayEnd; + }); + return { ...b, chars: dayRequests.reduce((s, r) => s + r.inputCharCount + r.outputCharCount, 0) }; + }); + const max = Math.max(1, ...charBuckets.map((b) => b.chars)); + const points = charBuckets.map((b, i) => { + const x = (i / 6) * (CHART_W - 10) + 5; + const y = CHART_H - 20 - (b.chars / max) * (CHART_H - 30); + return `${x},${y}`; + }).join(" "); + + return ( + + + + {charBuckets.map((b, i) => { + const x = (i / 6) * (CHART_W - 10) + 5; + const y = CHART_H - 20 - (b.chars / max) * (CHART_H - 30); + return ; + })} + {charBuckets.map((b, i) => { + const x = (i / 6) * (CHART_W - 10) + 5; + return {b.label}; + })} + + + ); +} + +function ChartCard({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+
{title}
+ {children} +
+ ); +} + +function last7DayBuckets(dates: string[]): Array<{ label: string; count: number }> { + const now = Date.now(); + const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + return Array.from({ length: 7 }, (_, i) => { + const offset = 6 - i; + const start = new Date(now - offset * 86400000); + start.setHours(0, 0, 0, 0); + const end = new Date(start.getTime() + 86400000); + const count = dates.filter((d) => { + const t = new Date(d).getTime(); + return t >= start.getTime() && t < end.getTime(); + }).length; + return { label: dayNames[start.getDay()], count }; + }); +} + +function dayStartMs(bucketIndex: number): number { + const offset = 6 - bucketIndex; + const start = new Date(Date.now() - offset * 86400000); + start.setHours(0, 0, 0, 0); + return start.getTime(); +}