From 53ed6859fa7c8325edfb446bbff47cb110a0a411 Mon Sep 17 00:00:00 2001 From: Sr Dev Agent Date: Thu, 2 Apr 2026 18:11:04 +0000 Subject: [PATCH 1/2] feat: add pie charts for content agent usage by user and template Add reusable AdminPieChart component using Recharts PieChart with donut style. Add two pie charts to the Content Agent page showing who triggered the content agent most and which templates are used most. Co-Authored-By: Paperclip Co-Authored-By: Claude Opus 4.6 --- components/Admin/AdminPieChart.tsx | 74 ++++++++++++++++++++ components/ContentSlack/ContentSlackPage.tsx | 9 +++ lib/contentSlack/getTagsByTemplate.ts | 19 +++++ lib/contentSlack/getTagsByUser.ts | 19 +++++ 4 files changed, 121 insertions(+) create mode 100644 components/Admin/AdminPieChart.tsx create mode 100644 lib/contentSlack/getTagsByTemplate.ts create mode 100644 lib/contentSlack/getTagsByUser.ts diff --git a/components/Admin/AdminPieChart.tsx b/components/Admin/AdminPieChart.tsx new file mode 100644 index 0000000..8ec3d4f --- /dev/null +++ b/components/Admin/AdminPieChart.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { Pie, PieChart, Cell } from "recharts"; +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, + ChartLegend, + ChartLegendContent, + type ChartConfig, +} from "@/components/ui/chart"; + +const COLORS = [ + "#345A5D", + "#6B8E93", + "#4A90A4", + "#8FBCC4", + "#2A4648", + "#5BA3B5", + "#3D7A7E", + "#97C4CC", +]; + +export interface PieChartSlice { + name: string; + value: number; +} + +interface AdminPieChartProps { + title: string; + data: PieChartSlice[]; +} + +export default function AdminPieChart({ title, data }: AdminPieChartProps) { + if (data.length === 0) return null; + + const chartConfig = Object.fromEntries( + data.map((slice, i) => [ + slice.name, + { label: slice.name, color: COLORS[i % COLORS.length] }, + ]), + ) satisfies ChartConfig; + + return ( +
+

+ {title} +

+ + + } /> + + {data.map((slice, i) => ( + + ))} + + } /> + + +
+ ); +} diff --git a/components/ContentSlack/ContentSlackPage.tsx b/components/ContentSlack/ContentSlackPage.tsx index 07e251d..1afc3ea 100644 --- a/components/ContentSlack/ContentSlackPage.tsx +++ b/components/ContentSlack/ContentSlackPage.tsx @@ -8,9 +8,12 @@ import ContentSlackTable from "@/components/ContentSlack/ContentSlackTable"; import ContentSlackStats from "@/components/ContentSlack/ContentSlackStats"; import PeriodSelector from "@/components/Admin/PeriodSelector"; import AdminLineChart from "@/components/Admin/AdminLineChart"; +import AdminPieChart from "@/components/Admin/AdminPieChart"; import TableSkeleton from "@/components/Sandboxes/TableSkeleton"; import ChartSkeleton from "@/components/PrivyLogins/ChartSkeleton"; import { getTagsByDate } from "@/lib/coding-agent/getTagsByDate"; +import { getTagsByUser } from "@/lib/contentSlack/getTagsByUser"; +import { getTagsByTemplate } from "@/lib/contentSlack/getTagsByTemplate"; import type { AdminPeriod } from "@/types/admin"; export default function ContentSlackPage() { @@ -25,6 +28,8 @@ export default function ContentSlackPage() { })), ) : []; + const tagsByUser = data ? getTagsByUser(data.tags) : []; + const tagsByTemplate = data ? getTagsByTemplate(data.tags) : []; return (
@@ -76,6 +81,10 @@ export default function ContentSlackPage() { label: "Tags with Videos", }} /> +
+ + +
)} diff --git a/lib/contentSlack/getTagsByTemplate.ts b/lib/contentSlack/getTagsByTemplate.ts new file mode 100644 index 0000000..00d1c66 --- /dev/null +++ b/lib/contentSlack/getTagsByTemplate.ts @@ -0,0 +1,19 @@ +import type { ContentSlackTag } from "@/types/contentSlack"; +import type { PieChartSlice } from "@/components/Admin/AdminPieChart"; + +/** + * Aggregates content slack tags by prompt (template) for pie chart display. + * Returns slices sorted descending by count. + */ +export function getTagsByTemplate(tags: ContentSlackTag[]): PieChartSlice[] { + const counts = new Map(); + + for (const tag of tags) { + const template = tag.prompt || "No Template"; + counts.set(template, (counts.get(template) ?? 0) + 1); + } + + return Array.from(counts.entries()) + .map(([name, value]) => ({ name, value })) + .sort((a, b) => b.value - a.value); +} diff --git a/lib/contentSlack/getTagsByUser.ts b/lib/contentSlack/getTagsByUser.ts new file mode 100644 index 0000000..ed3ae26 --- /dev/null +++ b/lib/contentSlack/getTagsByUser.ts @@ -0,0 +1,19 @@ +import type { ContentSlackTag } from "@/types/contentSlack"; +import type { PieChartSlice } from "@/components/Admin/AdminPieChart"; + +/** + * Aggregates content slack tags by user_name for pie chart display. + * Returns slices sorted descending by count. + */ +export function getTagsByUser(tags: ContentSlackTag[]): PieChartSlice[] { + const counts = new Map(); + + for (const tag of tags) { + const name = tag.user_name || "Unknown"; + counts.set(name, (counts.get(name) ?? 0) + 1); + } + + return Array.from(counts.entries()) + .map(([name, value]) => ({ name, value })) + .sort((a, b) => b.value - a.value); +} From e0996110bc5960592c613d99199d1a0bd98e6c31 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Thu, 2 Apr 2026 15:11:28 -0500 Subject: [PATCH 2/2] refactor: reduce scope to Tags by User pie chart only - Remove Tags by Template pie chart and getTagsByTemplate util - Add PieChartSkeleton with circular placeholder for loading state - Single pie chart no longer needs 2-column grid Co-Authored-By: Claude Opus 4.6 (1M context) --- components/ContentSlack/ContentSlackPage.tsx | 7 +++---- components/ContentSlack/PieChartSkeleton.tsx | 10 ++++++++++ lib/contentSlack/getTagsByTemplate.ts | 19 ------------------- 3 files changed, 13 insertions(+), 23 deletions(-) create mode 100644 components/ContentSlack/PieChartSkeleton.tsx delete mode 100644 lib/contentSlack/getTagsByTemplate.ts diff --git a/components/ContentSlack/ContentSlackPage.tsx b/components/ContentSlack/ContentSlackPage.tsx index 1afc3ea..9d18ab0 100644 --- a/components/ContentSlack/ContentSlackPage.tsx +++ b/components/ContentSlack/ContentSlackPage.tsx @@ -11,9 +11,9 @@ import AdminLineChart from "@/components/Admin/AdminLineChart"; import AdminPieChart from "@/components/Admin/AdminPieChart"; import TableSkeleton from "@/components/Sandboxes/TableSkeleton"; import ChartSkeleton from "@/components/PrivyLogins/ChartSkeleton"; +import PieChartSkeleton from "@/components/ContentSlack/PieChartSkeleton"; import { getTagsByDate } from "@/lib/coding-agent/getTagsByDate"; import { getTagsByUser } from "@/lib/contentSlack/getTagsByUser"; -import { getTagsByTemplate } from "@/lib/contentSlack/getTagsByTemplate"; import type { AdminPeriod } from "@/types/admin"; export default function ContentSlackPage() { @@ -29,7 +29,6 @@ export default function ContentSlackPage() { ) : []; const tagsByUser = data ? getTagsByUser(data.tags) : []; - const tagsByTemplate = data ? getTagsByTemplate(data.tags) : []; return (
@@ -54,6 +53,7 @@ export default function ContentSlackPage() { {isLoading && ( <> + )} @@ -81,9 +81,8 @@ export default function ContentSlackPage() { label: "Tags with Videos", }} /> -
+
-
diff --git a/components/ContentSlack/PieChartSkeleton.tsx b/components/ContentSlack/PieChartSkeleton.tsx new file mode 100644 index 0000000..ed71bce --- /dev/null +++ b/components/ContentSlack/PieChartSkeleton.tsx @@ -0,0 +1,10 @@ +export default function PieChartSkeleton() { + return ( +
+
+
+
+
+
+ ); +} diff --git a/lib/contentSlack/getTagsByTemplate.ts b/lib/contentSlack/getTagsByTemplate.ts deleted file mode 100644 index 00d1c66..0000000 --- a/lib/contentSlack/getTagsByTemplate.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { ContentSlackTag } from "@/types/contentSlack"; -import type { PieChartSlice } from "@/components/Admin/AdminPieChart"; - -/** - * Aggregates content slack tags by prompt (template) for pie chart display. - * Returns slices sorted descending by count. - */ -export function getTagsByTemplate(tags: ContentSlackTag[]): PieChartSlice[] { - const counts = new Map(); - - for (const tag of tags) { - const template = tag.prompt || "No Template"; - counts.set(template, (counts.get(template) ?? 0) + 1); - } - - return Array.from(counts.entries()) - .map(([name, value]) => ({ name, value })) - .sort((a, b) => b.value - a.value); -}