Skip to content

Commit f7c6a0b

Browse files
authored
[SDK] Add group filtering (#1595)
1 parent e9801c7 commit f7c6a0b

10 files changed

Lines changed: 975 additions & 16 deletions

File tree

mcpjam-inspector/client/src/components/CiEvalsTab.tsx

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
} from "@/components/ui/resizable";
1212
import { useSharedAppState } from "@/state/app-state-context";
1313
import { useCiEvalsRoute, navigateToCiEvalsRoute } from "@/lib/ci-evals-router";
14-
import { aggregateSuite } from "./evals/helpers";
14+
import { aggregateSuite, groupSuitesByTag } from "./evals/helpers";
15+
import { TagAggregationPanel } from "./evals/tag-aggregation-panel";
1516
import { useEvalMutations } from "./evals/use-eval-mutations";
1617
import { useEvalQueries } from "./evals/use-eval-queries";
1718
import { useEvalHandlers } from "./evals/use-eval-handlers";
@@ -33,6 +34,7 @@ export function CiEvalsTab({ convexWorkspaceId }: CiEvalsTabProps) {
3334

3435
const [deletingSuiteId, setDeletingSuiteId] = useState<string | null>(null);
3536
const [deletingRunId, setDeletingRunId] = useState<string | null>(null);
37+
const [filterTag, setFilterTag] = useState<string | null>(null);
3638

3739
const selectedSuiteId =
3840
route.type === "suite-overview" ||
@@ -84,6 +86,14 @@ export function CiEvalsTab({ convexWorkspaceId }: CiEvalsTabProps) {
8486
[queries.sortedSuites],
8587
);
8688

89+
const tagGroups = useMemo(() => groupSuitesByTag(sdkSuites), [sdkSuites]);
90+
const hasTags = tagGroups.some((g) => g.tag !== "Untagged");
91+
const allTags = useMemo(
92+
() =>
93+
Array.from(new Set(sdkSuites.flatMap((e) => e.suite.tags ?? []))).sort(),
94+
[sdkSuites],
95+
);
96+
8797
const selectedSuiteEntry = useMemo(() => {
8898
if (!selectedSuiteId) return null;
8999
return (
@@ -127,6 +137,10 @@ export function CiEvalsTab({ convexWorkspaceId }: CiEvalsTabProps) {
127137
navigateToCiEvalsRoute({ type: "suite-overview", suiteId });
128138
}, []);
129139

140+
const handleSelectOverview = useCallback(() => {
141+
navigateToCiEvalsRoute({ type: "list" });
142+
}, []);
143+
130144
const handleDeleteSuite = useCallback(
131145
async (suite: EvalSuite) => {
132146
if (deletingSuiteId) return;
@@ -246,7 +260,11 @@ export function CiEvalsTab({ convexWorkspaceId }: CiEvalsTabProps) {
246260
suites={sdkSuites}
247261
selectedSuiteId={selectedSuiteId}
248262
onSelectSuite={handleSelectSuite}
263+
onSelectOverview={handleSelectOverview}
264+
isOverviewSelected={!selectedSuiteId && hasTags}
249265
isLoading={queries.isOverviewLoading}
266+
filterTag={filterTag}
267+
hasTags={hasTags}
250268
/>
251269
</ResizablePanel>
252270

@@ -272,20 +290,30 @@ export function CiEvalsTab({ convexWorkspaceId }: CiEvalsTabProps) {
272290
</div>
273291
</div>
274292
) : route.type === "list" || !selectedSuite ? (
275-
<div className="flex-1 flex items-center justify-center">
276-
<div className="text-center max-w-md mx-auto p-8">
277-
<div className="w-20 h-20 bg-muted rounded-full flex items-center justify-center mx-auto mb-6">
278-
<GitBranch className="h-10 w-10 text-muted-foreground" />
293+
hasTags ? (
294+
<TagAggregationPanel
295+
tagGroups={tagGroups.filter((g) => g.tag !== "Untagged")}
296+
allTags={allTags}
297+
filterTag={filterTag}
298+
onFilterTagChange={setFilterTag}
299+
onSelectSuite={handleSelectSuite}
300+
/>
301+
) : (
302+
<div className="flex-1 flex items-center justify-center">
303+
<div className="text-center max-w-md mx-auto p-8">
304+
<div className="w-20 h-20 bg-muted rounded-full flex items-center justify-center mx-auto mb-6">
305+
<GitBranch className="h-10 w-10 text-muted-foreground" />
306+
</div>
307+
<h2 className="text-2xl font-semibold text-foreground mb-2">
308+
Select a suite
309+
</h2>
310+
<p className="text-sm text-muted-foreground">
311+
Choose a CI suite from the sidebar to inspect runs and test
312+
iterations.
313+
</p>
279314
</div>
280-
<h2 className="text-2xl font-semibold text-foreground mb-2">
281-
Select a suite
282-
</h2>
283-
<p className="text-sm text-muted-foreground">
284-
Choose a CI suite from the sidebar to inspect runs and test
285-
iterations.
286-
</p>
287315
</div>
288-
</div>
316+
)
289317
) : queries.isSuiteDetailsLoading ? (
290318
<div className="flex h-full items-center justify-center">
291319
<div className="text-center">

mcpjam-inspector/client/src/components/evals/ci-suite-list-sidebar.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { useState, useEffect } from "react";
2+
import { BarChart3 } from "lucide-react";
23
import { cn } from "@/lib/utils";
34
import type { EvalSuiteOverviewEntry } from "./types";
5+
import { TagBadges } from "./tag-editor";
46

57
/** Force a re-render every `intervalMs` so relative timestamps stay fresh. */
68
function useTick(intervalMs = 60_000) {
@@ -15,7 +17,12 @@ interface CiSuiteListSidebarProps {
1517
suites: EvalSuiteOverviewEntry[];
1618
selectedSuiteId: string | null;
1719
onSelectSuite: (suiteId: string) => void;
20+
onSelectOverview: () => void;
21+
isOverviewSelected: boolean;
1822
isLoading?: boolean;
23+
filterTag?: string | null;
24+
onFilterTagChange?: (tag: string | null) => void;
25+
hasTags: boolean;
1926
}
2027

2128
function getStatusDot(entry: EvalSuiteOverviewEntry): {
@@ -61,28 +68,55 @@ export function CiSuiteListSidebar({
6168
suites,
6269
selectedSuiteId,
6370
onSelectSuite,
71+
onSelectOverview,
72+
isOverviewSelected,
6473
isLoading = false,
74+
filterTag,
75+
hasTags,
6576
}: CiSuiteListSidebarProps) {
6677
useTick(); // keep "Xm ago" labels ticking
6778

79+
const filteredSuites = filterTag
80+
? suites.filter((e) => e.suite.tags?.includes(filterTag))
81+
: suites;
82+
6883
return (
6984
<div className="flex h-full flex-col">
7085
<div className="border-b px-4 py-3">
7186
<h2 className="text-sm font-semibold">Eval suites</h2>
7287
</div>
7388

7489
<div className="flex-1 overflow-y-auto">
90+
{hasTags && (
91+
<button
92+
onClick={onSelectOverview}
93+
className={cn(
94+
"w-full px-4 py-2.5 text-left transition-colors hover:bg-accent/50 border-b",
95+
isOverviewSelected && "bg-accent",
96+
)}
97+
>
98+
<div className="flex items-center gap-2.5">
99+
<BarChart3 className="h-4 w-4 shrink-0 text-muted-foreground" />
100+
<div className="min-w-0 flex-1">
101+
<div className="text-sm font-medium">Overview</div>
102+
<div className="text-[11px] text-muted-foreground">
103+
Compare suite groups
104+
</div>
105+
</div>
106+
</div>
107+
</button>
108+
)}
75109
{isLoading ? (
76110
<div className="p-4 text-center text-xs text-muted-foreground">
77111
Loading suites...
78112
</div>
79-
) : suites.length === 0 ? (
113+
) : filteredSuites.length === 0 ? (
80114
<div className="p-4 text-center text-xs text-muted-foreground">
81115
No SDK suites found.
82116
</div>
83117
) : (
84118
<div>
85-
{suites.map((entry) => {
119+
{filteredSuites.map((entry) => {
86120
const latestRun = entry.latestRun;
87121
const status = getStatusDot(entry);
88122
const trend = entry.passRateTrend
@@ -115,6 +149,9 @@ export function CiSuiteListSidebar({
115149
<div className="truncate text-sm font-medium">
116150
{entry.suite.name || "Untitled suite"}
117151
</div>
152+
{entry.suite.tags && entry.suite.tags.length > 0 && (
153+
<TagBadges tags={entry.suite.tags} className="mt-0.5" />
154+
)}
118155
<div className="text-[11px] text-muted-foreground">
119156
{timestamp}
120157
</div>

mcpjam-inspector/client/src/components/evals/helpers.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { EvalCase, EvalIteration, EvalSuite, SuiteAggregate } from "./types";
1+
import {
2+
EvalCase,
3+
EvalIteration,
4+
EvalSuite,
5+
EvalSuiteOverviewEntry,
6+
SuiteAggregate,
7+
TagGroupAggregate,
8+
} from "./types";
29
import { computeIterationResult } from "./pass-criteria";
310
import { toast } from "sonner";
411
import { RESULT_STATUS } from "./constants";
@@ -226,3 +233,54 @@ export const formatters = {
226233
percentage: formatPercentage,
227234
tokens: formatTokens,
228235
} as const;
236+
237+
/**
238+
* Group overview entries by tag and compute aggregated stats per tag.
239+
*/
240+
export function groupSuitesByTag(
241+
overview: EvalSuiteOverviewEntry[],
242+
): TagGroupAggregate[] {
243+
const buckets = new Map<string, EvalSuiteOverviewEntry[]>();
244+
245+
for (const entry of overview) {
246+
const tags = entry.suite.tags;
247+
if (!tags || tags.length === 0) {
248+
const bucket = buckets.get("Untagged") ?? [];
249+
bucket.push(entry);
250+
buckets.set("Untagged", bucket);
251+
} else {
252+
for (const tag of tags) {
253+
const bucket = buckets.get(tag) ?? [];
254+
bucket.push(entry);
255+
buckets.set(tag, bucket);
256+
}
257+
}
258+
}
259+
260+
const groups: TagGroupAggregate[] = [];
261+
for (const [tag, entries] of buckets) {
262+
const totals = { passed: 0, failed: 0, runs: 0 };
263+
for (const e of entries) {
264+
totals.passed += e.totals.passed;
265+
totals.failed += e.totals.failed;
266+
totals.runs += e.totals.runs;
267+
}
268+
const total = totals.passed + totals.failed;
269+
groups.push({
270+
tag,
271+
suiteCount: entries.length,
272+
totals,
273+
passRate: total > 0 ? Math.round((totals.passed / total) * 100) : 0,
274+
entries,
275+
});
276+
}
277+
278+
// Sort alphabetically, "Untagged" last
279+
groups.sort((a, b) => {
280+
if (a.tag === "Untagged") return 1;
281+
if (b.tag === "Untagged") return -1;
282+
return a.tag.localeCompare(b.tag);
283+
});
284+
285+
return groups;
286+
}

mcpjam-inspector/client/src/components/evals/suite-header.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import type { ModelDefinition } from "@/shared/types";
3737
import { isMCPJamProvidedModel } from "@/shared/types";
3838
import { ProviderLogo } from "@/components/chat-v2/chat-input/model/provider-logo";
3939
import { CiMetadataDisplay } from "./ci-metadata-display";
40+
import { TagEditor, TagBadges } from "./tag-editor";
4041

4142
interface ModelInfo {
4243
model: string;
@@ -530,6 +531,25 @@ export function SuiteHeader({
530531
</ChartContainer>
531532
</div>
532533
)}
534+
{!readOnlyConfig && (
535+
<TagEditor
536+
tags={suite.tags ?? []}
537+
onTagsChange={async (newTags) => {
538+
try {
539+
await updateSuite({
540+
suiteId: suite._id,
541+
tags: newTags,
542+
});
543+
} catch (error) {
544+
toast.error("Failed to update tags");
545+
console.error("Failed to update tags:", error);
546+
}
547+
}}
548+
/>
549+
)}
550+
{readOnlyConfig && suite.tags && suite.tags.length > 0 && (
551+
<TagBadges tags={suite.tags} />
552+
)}
533553
</div>
534554
<div className="flex items-center gap-2 shrink-0">
535555
{/* Models picker - compact dropdown */}

0 commit comments

Comments
 (0)