diff --git a/src/renderer/components/dashboard/ChartSection.tsx b/src/renderer/components/dashboard/ChartSection.tsx new file mode 100644 index 00000000..ec11a719 --- /dev/null +++ b/src/renderer/components/dashboard/ChartSection.tsx @@ -0,0 +1,97 @@ +import { useMemo } from "react"; +import { motion } from "framer-motion"; +import type { RunRecord, CostRecord, AgentRecord } from "@shared/types"; +import { useReducedMotion } from "../ui"; +import { + aggregateRunsByDay, + aggregateCostsByDay, + aggregateAgentWorkload, +} from "./charts/chart-utils"; +import { ChartCard } from "./charts/ChartCard"; +import { RunActivityChart } from "./charts/RunActivityChart"; +import { SuccessRateChart } from "./charts/SuccessRateChart"; +import { CostTrendChart } from "./charts/CostTrendChart"; +import { AgentWorkloadChart } from "./charts/AgentWorkloadChart"; +import { BudgetUtilization } from "./charts/BudgetUtilization"; + +interface ChartSectionProps { + runs: RunRecord[]; + costs: CostRecord[]; + agents: AgentRecord[]; + companyId: string; +} + +export function ChartSection({ runs, costs, agents, companyId }: ChartSectionProps) { + const reducedMotion = useReducedMotion(); + + const dailyRuns = useMemo(() => aggregateRunsByDay(runs, 14), [runs]); + const dailyCosts = useMemo(() => aggregateCostsByDay(costs, 14), [costs]); + const agentWorkload = useMemo( + () => aggregateAgentWorkload(runs, agents), + [runs, agents], + ); + + const hasRunData = dailyRuns.some((d) => d.total > 0); + const hasCostData = dailyCosts.some((d) => d.total > 0); + + const content = ( +
+ {/* Section header */} +
+ Performance +
+ + {/* 2-column chart grid */} +
+ s + d.total, 0)} total runs over 14 days`} + isEmpty={!hasRunData} + > + + + + + + + + + + + +
+ + {/* Budget Utilization section */} +
+ Budget Utilization +
+ +
+ ); + + if (reducedMotion) { + return content; + } + + return ( + + {content} + + ); +} diff --git a/src/renderer/components/dashboard/Dashboard.tsx b/src/renderer/components/dashboard/Dashboard.tsx index 90762256..23dc7a11 100644 --- a/src/renderer/components/dashboard/Dashboard.tsx +++ b/src/renderer/components/dashboard/Dashboard.tsx @@ -27,6 +27,7 @@ import { MetricCard } from "./MetricsGrid"; import { ActivityFeed } from "./ActivityFeed"; import { RecoveryBanner } from "./RecoveryBanner"; import { BudgetAlertBanner } from "./BudgetAlertBanner"; +import { ChartSection } from "./ChartSection"; export interface DashboardProps { companyName?: string; @@ -132,6 +133,9 @@ export function Dashboard(props: DashboardProps) { /> + {/* Performance Charts */} + {companyId ? : null} + {/* Needs Attention — moved to top, only shows when items exist */} {hasAttentionItems ? (
diff --git a/src/renderer/components/dashboard/charts/AgentWorkloadChart.tsx b/src/renderer/components/dashboard/charts/AgentWorkloadChart.tsx new file mode 100644 index 00000000..ccd4cffa --- /dev/null +++ b/src/renderer/components/dashboard/charts/AgentWorkloadChart.tsx @@ -0,0 +1,52 @@ +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; +import type { AgentWorkloadData } from "./chart-utils"; +import { ChartTooltip } from "./ChartTooltip"; + +interface AgentWorkloadChartProps { + data: AgentWorkloadData[]; + reducedMotion: boolean; +} + +export function AgentWorkloadChart({ data, reducedMotion }: AgentWorkloadChartProps) { + return ( + + + + + + } /> + + + + ); +} diff --git a/src/renderer/components/dashboard/charts/BudgetUtilization.tsx b/src/renderer/components/dashboard/charts/BudgetUtilization.tsx new file mode 100644 index 00000000..8c2a41fd --- /dev/null +++ b/src/renderer/components/dashboard/charts/BudgetUtilization.tsx @@ -0,0 +1,87 @@ +import { useMemo } from "react"; +import type { AgentRecord } from "@shared/types"; + +interface BudgetUtilizationProps { + companyId: string; + agents: AgentRecord[]; +} + +interface BudgetRow { + id: string; + name: string; + spent: number; + budget: number; + pct: number; +} + +function getBarColor(pct: number): string { + if (pct >= 100) return "bg-[color:var(--danger)]"; + if (pct >= 80) return "bg-[color:var(--warn)]"; + return "bg-[color:var(--success)]"; +} + +export function BudgetUtilization({ companyId: _companyId, agents }: BudgetUtilizationProps) { + const rows = useMemo(() => { + return agents + .filter((a) => a.budgetMonthlyUsd > 0) + .map((a) => ({ + id: a.id, + name: a.name, + spent: a.spentMonthlyUsd, + budget: a.budgetMonthlyUsd, + pct: Math.round((a.spentMonthlyUsd / a.budgetMonthlyUsd) * 100), + })) + .sort((a, b) => a.name.localeCompare(b.name)); + }, [agents]); + + if (rows.length === 0) { + return ( +
+
+ + No budget policies configured + + + Set budget policies in Settings to track utilization here. + +
+
+ ); + } + + return ( +
+
+ {rows.map((row) => ( +
+
+ + {row.name} + + + {row.pct}% + +
+
+
+ {/* 80% warning marker */} +
+
+
+ ))} +
+
+ ); +} diff --git a/src/renderer/components/dashboard/charts/CostTrendChart.tsx b/src/renderer/components/dashboard/charts/CostTrendChart.tsx new file mode 100644 index 00000000..41f69fe8 --- /dev/null +++ b/src/renderer/components/dashboard/charts/CostTrendChart.tsx @@ -0,0 +1,57 @@ +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; +import type { DailyCostData } from "./chart-utils"; +import { formatShortDate } from "./chart-utils"; +import { ChartTooltip } from "./ChartTooltip"; + +interface CostTrendChartProps { + data: DailyCostData[]; + reducedMotion: boolean; +} + +export function CostTrendChart({ data, reducedMotion }: CostTrendChartProps) { + return ( + + + + + "$" + val.toFixed(0)} + tick={{ fontSize: 11, fill: "var(--muted)" }} + axisLine={false} + tickLine={false} + /> + } /> + + + + ); +} diff --git a/src/renderer/components/dashboard/charts/RunActivityChart.tsx b/src/renderer/components/dashboard/charts/RunActivityChart.tsx new file mode 100644 index 00000000..2551b463 --- /dev/null +++ b/src/renderer/components/dashboard/charts/RunActivityChart.tsx @@ -0,0 +1,57 @@ +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; +import type { DailyRunData } from "./chart-utils"; +import { formatShortDate } from "./chart-utils"; +import { ChartTooltip } from "./ChartTooltip"; + +interface RunActivityChartProps { + data: DailyRunData[]; + reducedMotion: boolean; +} + +export function RunActivityChart({ data, reducedMotion }: RunActivityChartProps) { + return ( + + + + + + } /> + + + + ); +} diff --git a/src/renderer/components/dashboard/charts/SuccessRateChart.tsx b/src/renderer/components/dashboard/charts/SuccessRateChart.tsx new file mode 100644 index 00000000..2e6d4bde --- /dev/null +++ b/src/renderer/components/dashboard/charts/SuccessRateChart.tsx @@ -0,0 +1,66 @@ +import { useMemo } from "react"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; +import type { DailyRunData } from "./chart-utils"; +import { formatShortDate } from "./chart-utils"; +import { ChartTooltip } from "./ChartTooltip"; + +interface SuccessRateChartProps { + data: DailyRunData[]; + reducedMotion: boolean; +} + +export function SuccessRateChart({ data, reducedMotion }: SuccessRateChartProps) { + const rateData = useMemo( + () => + data.map((d) => ({ + date: d.date, + rate: d.total > 0 ? Math.round((d.succeeded / d.total) * 100) : 0, + })), + [data], + ); + + return ( + + + + + `${val}%`} + tick={{ fontSize: 11, fill: "var(--muted)" }} + axisLine={false} + tickLine={false} + /> + } /> + + + + ); +}