From f457768370a662dd78a227168eb25e2966a4a070 Mon Sep 17 00:00:00 2001 From: cteyton Date: Fri, 6 Feb 2026 17:35:08 +0100 Subject: [PATCH 1/4] Add individual evaluator selection with multi-select dropdown Replace the 3-option category filter (All/Errors/Suggestions) with a multi-select dropdown listing all 17 evaluators individually, grouped by type. Users can pick any combination (minimum 1 required). When all are selected, behavior is identical to before. Only shown in non-cloud mode. Adds selectedEvaluators option that flows through the full stack: frontend -> API -> engine -> runner. The existing evaluatorFilter and CLI --evaluator-filter flag remain unchanged for backward compatibility. Co-Authored-By: Claude Opus 4.6 --- frontend/src/App.tsx | 3 + .../src/components/RepositoryUrlInput.tsx | 310 ++++++++++++++++-- frontend/src/hooks/useEvaluationApi.ts | 3 + frontend/src/lib/formatters.ts | 6 + frontend/src/types/evaluator.ts | 1 + src/api/utils/evaluator-utils.ts | 18 +- src/shared/evaluation/engine.ts | 2 + src/shared/evaluation/evaluator-types.test.ts | 56 ++++ src/shared/evaluation/evaluator-types.ts | 13 + src/shared/evaluation/runner.ts | 23 +- src/shared/types/evaluation.ts | 6 + 11 files changed, 402 insertions(+), 39 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8190894..ec80422 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -595,6 +595,7 @@ function AppContent() { provider?: "claude" | "opencode" | "cursor" | "github-copilot", evaluatorFilter?: EvaluatorFilter, concurrency?: number, + selectedEvaluators?: string[], ) => { setEvaluationMode("evaluating"); setApiError(null); @@ -609,6 +610,7 @@ function AppContent() { evaluatorFilter, undefined, concurrency, + selectedEvaluators, ); setCurrentJobId(response.jobId); setSSEUrl(response.sseUrl); @@ -622,6 +624,7 @@ function AppContent() { totalEvaluators: getFilteredEvaluatorCount( evaluatorFilter || "all", evaluators, + selectedEvaluators?.length, ), completedEvaluators: 0, percentage: 0, diff --git a/frontend/src/components/RepositoryUrlInput.tsx b/frontend/src/components/RepositoryUrlInput.tsx index 5b8c830..ef7d530 100644 --- a/frontend/src/components/RepositoryUrlInput.tsx +++ b/frontend/src/components/RepositoryUrlInput.tsx @@ -1,10 +1,11 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { useFeatureFlags } from "../contexts/FeatureFlagContext"; import type { ProviderName } from "../hooks/useEvaluationApi"; import { useEvaluatorsApi } from "../hooks/useEvaluatorsApi"; import { useProviderDetection } from "../hooks/useProviderDetection"; import { isValidGitUrl } from "../lib/url-validation"; import type { EvaluatorFilter } from "../types/evaluation"; +import type { IEvaluator } from "../types/evaluator"; interface IRepositoryUrlInputProps { onSubmit: ( @@ -13,6 +14,7 @@ interface IRepositoryUrlInputProps { provider: ProviderName, evaluatorFilter: EvaluatorFilter, concurrency: number, + selectedEvaluators?: string[], ) => Promise; isLoading: boolean; error?: string | null; @@ -44,23 +46,42 @@ export const RepositoryUrlInput: React.FC = ({ const [url, setUrl] = useState(""); const [concurrency, setConcurrency] = useState(3); const [totalEvaluators, setTotalEvaluators] = useState(17); // Default fallback - const [evaluatorFilter, setEvaluatorFilter] = - useState("all"); + const [evaluatorsList, setEvaluatorsList] = useState([]); + const [selectedEvaluatorIds, setSelectedEvaluatorIds] = useState>( + new Set(), + ); + const [evaluatorDropdownOpen, setEvaluatorDropdownOpen] = useState(false); + const dropdownRef = useRef(null); const [provider, setProvider] = useState("claude"); const [validationError, setValidationError] = useState(null); - // Fetch evaluators list on mount to get the total count + // Fetch evaluators list on mount to get the total count and details useEffect(() => { fetchEvaluatorsList() .then((list) => { - const total = list.length; - setTotalEvaluators(total); + setTotalEvaluators(list.length); + setEvaluatorsList(list); + setSelectedEvaluatorIds(new Set(list.map((e) => e.id))); }) .catch(() => { // Keep the fallback default }); }, [fetchEvaluatorsList]); + // Close dropdown on click outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setEvaluatorDropdownOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + const validateUrl = useCallback((value: string): boolean => { if (!value.trim()) { setValidationError("Please enter a repository URL"); @@ -89,13 +110,19 @@ export const RepositoryUrlInput: React.FC = ({ try { // In cloud mode, use "random" sentinel to let backend pick an available provider const effectiveProvider = cloudMode ? "random" : provider; - const effectiveFilter = cloudMode ? "all" : evaluatorFilter; + const isAllSelected = + selectedEvaluatorIds.size === totalEvaluators || + selectedEvaluatorIds.size === 0; + const selectedArray = isAllSelected + ? undefined + : Array.from(selectedEvaluatorIds); await onSubmit( url.trim(), - totalEvaluators, + selectedArray ? selectedArray.length : totalEvaluators, effectiveProvider, - effectiveFilter, + "all", concurrency, + cloudMode ? undefined : selectedArray, ); } catch { // Error is handled by parent component @@ -105,7 +132,7 @@ export const RepositoryUrlInput: React.FC = ({ url, totalEvaluators, provider, - evaluatorFilter, + selectedEvaluatorIds, concurrency, validateUrl, onSubmit, @@ -294,37 +321,266 @@ export const RepositoryUrlInput: React.FC = ({ - {/* Options Row: Evaluator Filter and Provider - hidden in cloud mode */} + {/* Options Row: Evaluator Selector, Provider, Concurrency - hidden in cloud mode */} {!cloudMode && (
- {/* Evaluator Filter Selector */} -
-