From 2ec78f37dac28661be33dba14f4c98d2def9ec33 Mon Sep 17 00:00:00 2001 From: mukunda katta Date: Sun, 19 Apr 2026 20:15:23 -0700 Subject: [PATCH] feat(frontend): add predictions page search filter --- helm-frontend/src/components/Instances.tsx | 59 ++++++++++++++++++- .../src/components/instanceFilters.test.ts | 39 ++++++++++++ .../src/components/instanceFilters.ts | 34 +++++++++++ 3 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 helm-frontend/src/components/instanceFilters.test.ts create mode 100644 helm-frontend/src/components/instanceFilters.ts diff --git a/helm-frontend/src/components/Instances.tsx b/helm-frontend/src/components/Instances.tsx index c85f2e929af..8d9269cdb2e 100644 --- a/helm-frontend/src/components/Instances.tsx +++ b/helm-frontend/src/components/Instances.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useSearchParams } from "react-router-dom"; import type Instance from "@/types/Instance"; import type MetricFieldMap from "@/types/MetricFieldMap"; @@ -10,6 +10,7 @@ import DisplayRequest from "@/types/DisplayRequest"; import getInstances from "@/services/getInstances"; import getDisplayPredictionsByName from "@/services/getDisplayPredictionsByName"; import getDisplayRequestsByName from "@/services/getDisplayRequestsByName"; +import { matchesInstanceSearch } from "./instanceFilters"; const INSTANCES_PAGE_SIZE = 10; @@ -41,6 +42,9 @@ export default function Instances({ undefined | Record> >(); const [currentInstancesPage, setCurrentInstancesPage] = useState(1); + const [instanceQuery, setInstanceQuery] = useState( + searchParams.get("instanceQuery") ?? "", + ); useEffect(() => { const controller = new AbortController(); @@ -101,11 +105,26 @@ export default function Instances({ return () => controller.abort(); }, [runName, suite, userAgreed]); - const pagedInstances = instances.slice( + const filteredInstances = useMemo( + () => + instances.filter((instance) => + matchesInstanceSearch(instance, instanceQuery), + ), + [instanceQuery, instances], + ); + + useEffect(() => { + setCurrentInstancesPage(1); + }, [instanceQuery]); + + const pagedInstances = filteredInstances.slice( (currentInstancesPage - 1) * INSTANCES_PAGE_SIZE, (currentInstancesPage - 1) * INSTANCES_PAGE_SIZE + INSTANCES_PAGE_SIZE, ); - const totalInstancesPages = Math.ceil(instances.length / INSTANCES_PAGE_SIZE); + const totalInstancesPages = Math.max( + 1, + Math.ceil(filteredInstances.length / INSTANCES_PAGE_SIZE), + ); // Handle scrolling to anchored instance useEffect(() => { @@ -145,6 +164,35 @@ export default function Instances({ return ( <> +
+ +

+ Showing {filteredInstances.length} of {instances.length} instances. +

+
{pagedInstances.map((instance, idx) => (
@@ -183,6 +231,11 @@ export default function Instances({
))}
+ {filteredInstances.length === 0 ? ( +

+ No instances match the current search. +

+ ) : null} { diff --git a/helm-frontend/src/components/instanceFilters.test.ts b/helm-frontend/src/components/instanceFilters.test.ts new file mode 100644 index 00000000000..e771e36acf9 --- /dev/null +++ b/helm-frontend/src/components/instanceFilters.test.ts @@ -0,0 +1,39 @@ +import "@testing-library/jest-dom"; +import { describe, expect, it } from "vitest"; + +import type Instance from "@/types/Instance"; + +import { matchesInstanceSearch } from "./instanceFilters"; + +const baseInstance: Instance = { + id: "instance-42", + split: "test", + input: { + text: "The capital of France is Paris.", + multimedia_content: undefined, + messages: undefined, + }, + references: [{ output: { text: "Paris" }, tags: [] }], +}; + +describe("matchesInstanceSearch", () => { + it("matches the instance id", () => { + expect(matchesInstanceSearch(baseInstance, "instance-42")).toBe(true); + }); + + it("matches the input text case-insensitively", () => { + expect(matchesInstanceSearch(baseInstance, "capital of france")).toBe(true); + }); + + it("matches reference text", () => { + expect(matchesInstanceSearch(baseInstance, "paris")).toBe(true); + }); + + it("returns true for an empty query", () => { + expect(matchesInstanceSearch(baseInstance, "")).toBe(true); + }); + + it("returns false for unrelated text", () => { + expect(matchesInstanceSearch(baseInstance, "tokyo")).toBe(false); + }); +}); diff --git a/helm-frontend/src/components/instanceFilters.ts b/helm-frontend/src/components/instanceFilters.ts new file mode 100644 index 00000000000..05a4cd9caad --- /dev/null +++ b/helm-frontend/src/components/instanceFilters.ts @@ -0,0 +1,34 @@ +import type Instance from "@/types/Instance"; + +function normalize(value: string): string { + return value.trim().toLowerCase(); +} + +function getSearchableText(instance: Instance): string { + const referenceText = instance.references + .map((reference) => reference.output.text) + .join(" "); + + return normalize( + [ + instance.id, + instance.split, + instance.perturbation?.name ?? "", + instance.input.text, + referenceText, + ].join(" "), + ); +} + +export function matchesInstanceSearch( + instance: Instance, + query: string, +): boolean { + const normalizedQuery = normalize(query); + + if (!normalizedQuery) { + return true; + } + + return getSearchableText(instance).includes(normalizedQuery); +}