From 80e28e0bdd8e68025cec6abf39523f32e64a715a Mon Sep 17 00:00:00 2001 From: Recoup Agent Date: Tue, 24 Mar 2026 14:51:03 +0000 Subject: [PATCH] feat: add tag filter chips to coding agent page Adds clickable user filter chips fetched from GET /api/admins/coding-agent/slack-tags to the coding agent admin page. Selecting a chip filters the mentions list via GET /api/admins/coding/slack?tag=... with toggle and clear-filter support. Co-Authored-By: Claude Sonnet 4.6 --- .../CodingAgentSlackTagsPage.tsx | 57 ++++++++++++++++++- hooks/useSlackTagOptions.ts | 23 ++++++++ hooks/useSlackTags.ts | 7 ++- lib/recoup/fetchSlackTagOptions.ts | 24 ++++++++ lib/recoup/fetchSlackTags.ts | 3 + types/coding-agent.ts | 12 ++++ 6 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 hooks/useSlackTagOptions.ts create mode 100644 lib/recoup/fetchSlackTagOptions.ts diff --git a/components/CodingAgentSlackTags/CodingAgentSlackTagsPage.tsx b/components/CodingAgentSlackTags/CodingAgentSlackTagsPage.tsx index 7882f2e..be49b6d 100644 --- a/components/CodingAgentSlackTags/CodingAgentSlackTagsPage.tsx +++ b/components/CodingAgentSlackTags/CodingAgentSlackTagsPage.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import PageBreadcrumb from "@/components/Sandboxes/PageBreadcrumb"; import ApiDocsLink from "@/components/ApiDocs/ApiDocsLink"; import { useSlackTags } from "@/hooks/useSlackTags"; +import { useSlackTagOptions } from "@/hooks/useSlackTagOptions"; import { useCodingPrStatus } from "@/hooks/useCodingPrStatus"; import SlackTagsTable from "./SlackTagsTable"; import AdminLineChart from "@/components/Admin/AdminLineChart"; @@ -15,12 +16,23 @@ import type { AdminPeriod } from "@/types/admin"; export default function CodingAgentSlackTagsPage() { const [period, setPeriod] = useState("all"); - const { data, isLoading, error } = useSlackTags(period); + const [selectedTag, setSelectedTag] = useState(undefined); + + const { data: tagOptions } = useSlackTagOptions(); + const { data, isLoading, error } = useSlackTags(period, selectedTag); const { data: mergedPrUrls } = useCodingPrStatus(data?.tags); const tagsByDate = data ? getTagsByDate(data.tags, mergedPrUrls) : []; const totalMergedPrs = mergedPrUrls?.size ?? 0; + function handleTagClick(tagId: string) { + setSelectedTag((prev) => (prev === tagId ? undefined : tagId)); + } + + function handleClearTag() { + setSelectedTag(undefined); + } + return (
@@ -36,6 +48,47 @@ export default function CodingAgentSlackTagsPage() {
+ {tagOptions && tagOptions.tags.length > 0 && ( +
+ Filter by: + {tagOptions.tags.map((tag) => { + const isActive = selectedTag === tag.id; + return ( + + ); + })} + {selectedTag && ( + + )} +
+ )} +
{data && ( @@ -81,7 +134,7 @@ export default function CodingAgentSlackTagsPage() { {!isLoading && !error && data && data.tags.length === 0 && (
- No tags found for this period. + No tags found for this period{selectedTag ? " and filter" : ""}.
)} diff --git a/hooks/useSlackTagOptions.ts b/hooks/useSlackTagOptions.ts new file mode 100644 index 0000000..bd6262c --- /dev/null +++ b/hooks/useSlackTagOptions.ts @@ -0,0 +1,23 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { usePrivy } from "@privy-io/react-auth"; +import { fetchSlackTagOptions } from "@/lib/recoup/fetchSlackTagOptions"; + +/** + * Fetches the distinct Slack user tags (filter options) for the coding agent page. + * Authenticates with the Privy access token (admin Bearer auth). + */ +export function useSlackTagOptions() { + const { ready, authenticated, getAccessToken } = usePrivy(); + + return useQuery({ + queryKey: ["admin", "coding-agent", "slack-tag-options"], + queryFn: async () => { + const token = await getAccessToken(); + if (!token) throw new Error("Not authenticated"); + return fetchSlackTagOptions(token); + }, + enabled: ready && authenticated, + }); +} diff --git a/hooks/useSlackTags.ts b/hooks/useSlackTags.ts index c7f37f9..94e978b 100644 --- a/hooks/useSlackTags.ts +++ b/hooks/useSlackTags.ts @@ -7,17 +7,18 @@ import type { SlackTagsPeriod } from "@/types/coding-agent"; /** * Fetches Slack tagging analytics for the Recoup Coding Agent for the given period. + * Optionally filters by a specific Slack user ID (tag). * Authenticates with the Privy access token (admin Bearer auth). */ -export function useSlackTags(period: SlackTagsPeriod) { +export function useSlackTags(period: SlackTagsPeriod, tag?: string) { const { ready, authenticated, getAccessToken } = usePrivy(); return useQuery({ - queryKey: ["admin", "coding-agent", "slack-tags", period], + queryKey: ["admin", "coding-agent", "slack-tags", period, tag], queryFn: async () => { const token = await getAccessToken(); if (!token) throw new Error("Not authenticated"); - return fetchSlackTags(token, period); + return fetchSlackTags(token, period, tag); }, enabled: ready && authenticated, }); diff --git a/lib/recoup/fetchSlackTagOptions.ts b/lib/recoup/fetchSlackTagOptions.ts new file mode 100644 index 0000000..b80c6a3 --- /dev/null +++ b/lib/recoup/fetchSlackTagOptions.ts @@ -0,0 +1,24 @@ +import { API_BASE_URL } from "@/lib/consts"; +import type { SlackTagOptionsResponse } from "@/types/coding-agent"; + +/** + * Fetches the distinct set of Slack users who have tagged the Recoup Coding Agent. + * Used to populate filter chips in the admin coding page. + * + * @param accessToken - Privy access token from getAccessToken() + * @returns SlackTagOptionsResponse with unique tag options + */ +export async function fetchSlackTagOptions(accessToken: string): Promise { + const url = `${API_BASE_URL}/api/admins/coding-agent/slack-tags`; + + const res = await fetch(url, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.error ?? body.message ?? `HTTP ${res.status}`); + } + + return res.json(); +} diff --git a/lib/recoup/fetchSlackTags.ts b/lib/recoup/fetchSlackTags.ts index 12500e8..61699f4 100644 --- a/lib/recoup/fetchSlackTags.ts +++ b/lib/recoup/fetchSlackTags.ts @@ -7,14 +7,17 @@ import type { SlackTagsPeriod, SlackTagsResponse } from "@/types/coding-agent"; * * @param accessToken - Privy access token from getAccessToken() * @param period - Time period: "all", "daily", "weekly", or "monthly" + * @param tag - Optional Slack user ID to filter results * @returns SlackTagsResponse with total count and list of tag events */ export async function fetchSlackTags( accessToken: string, period: SlackTagsPeriod, + tag?: string, ): Promise { const url = new URL(`${API_BASE_URL}/api/admins/coding/slack`); url.searchParams.set("period", period); + if (tag) url.searchParams.set("tag", tag); const res = await fetch(url.toString(), { headers: { Authorization: `Bearer ${accessToken}` }, diff --git a/types/coding-agent.ts b/types/coding-agent.ts index 6d3a179..414cde3 100644 --- a/types/coding-agent.ts +++ b/types/coding-agent.ts @@ -28,3 +28,15 @@ export interface CodingPrStatusResponse { status: "success"; pull_requests: CodingPrStatus[]; } + +export interface SlackTagOption { + id: string; + name: string; + avatar: string | null; +} + +export interface SlackTagOptionsResponse { + status: "success"; + total: number; + tags: SlackTagOption[]; +}