import { StatusBadge } from "@/src/components/layouts/status-badge"; import { LevelCountsDisplay } from "@/src/components/level-counts-display"; import { DataTable } from "@/src/components/table/data-table"; import { DataTableToolbar } from "@/src/components/table/data-table-toolbar"; import { DataTableControlsProvider, DataTableControls, } from "@/src/components/table/data-table-controls"; import { ResizableFilterLayout } from "@/src/components/table/resizable-filter-layout"; import { type LangfuseColumnDef } from "@/src/components/table/types"; import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility"; import { InlineFilterState } from "@/src/features/filters/components/filter-builder"; import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context"; import { useSidebarFilterState } from "@/src/features/filters/hooks/useSidebarFilterState"; import { evaluatorFilterConfig } from "@/src/features/filters/config/evaluators-config"; import { type RouterOutputs, api } from "@/src/utils/api"; import { safeExtract } from "@/src/utils/map-utils"; import { type FilterState, singleFilter } from "@langfuse/shared"; import { createColumnHelper } from "@tanstack/react-table"; import { useEffect, useState } from "react"; import { useQueryParams, withDefault, NumberParam, useQueryParam, StringParam, } from "use-query-params"; import { z } from "zod/v4"; import { generateJobExecutionCounts } from "@/src/features/evals/utils/job-execution-utils"; import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState"; import TableIdOrName from "@/src/components/table/table-id"; import { MoreVertical, Loader2, ExternalLinkIcon, Edit } from "lucide-react"; import { usePeekNavigation } from "@/src/components/table/peek/hooks/usePeekNavigation"; import { PeekViewEvaluatorConfigDetail } from "@/src/components/table/peek/peek-evaluator-config-detail"; import { DropdownMenu, DropdownMenuItem, DropdownMenuLabel, DropdownMenuContent, DropdownMenuTrigger, } from "@/src/components/ui/dropdown-menu"; import { Button } from "@/src/components/ui/button"; import { showSuccessToast } from "@/src/features/notifications/showSuccessToast"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/src/components/ui/dialog"; import { EvaluatorForm } from "@/src/features/evals/components/evaluator-form"; import { useRouter } from "next/router"; import { DeleteEvalConfigButton } from "@/src/components/deleteButton"; import { RAGAS_TEMPLATE_PREFIX } from "@/src/features/evals/types"; import { MaintainerTooltip } from "@/src/features/evals/components/maintainer-tooltip"; import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess"; import { Skeleton } from "@/src/components/ui/skeleton"; import { usdFormatter } from "@/src/utils/numbers"; export type EvaluatorDataRow = { id: string; status: string; createdAt: string; updatedAt: string; maintainer: string; template?: { id: string; name: string; version: number; }; scoreName: string; target: string; // "trace" or "dataset" filter: FilterState; result: { level: string; count: number; symbol: string; }[]; logs?: string; actions?: string; totalCost?: number | null; }; export default function EvaluatorTable({ projectId }: { projectId: string }) { const router = useRouter(); const { setDetailPageList } = useDetailPageLists(); const [paginationState, setPaginationState] = useQueryParams({ pageIndex: withDefault(NumberParam, 0), pageSize: withDefault(NumberParam, 50), }); const [searchQuery, setSearchQuery] = useQueryParam( "search", withDefault(StringParam, null), ); const [editConfigId, setEditConfigId] = useState(null); const utils = api.useUtils(); const [orderByState, setOrderByState] = useOrderByState({ column: "createdAt", order: "DESC", }); const newFilterOptions = { status: ["ACTIVE", "INACTIVE"], target: ["trace", "dataset"], }; const queryFilter = useSidebarFilterState( evaluatorFilterConfig, newFilterOptions, projectId, false, ); const evaluators = api.evals.allConfigs.useQuery({ page: paginationState.pageIndex, limit: paginationState.pageSize, projectId, filter: queryFilter.filterState, orderBy: orderByState, searchQuery: searchQuery, }); const totalCount = evaluators.data?.totalCount ?? null; const existingEvaluator = api.evals.configById.useQuery( { id: editConfigId as string, projectId, }, { enabled: !!editConfigId, }, ); const hasAccess = useHasProjectAccess({ projectId, scope: "evalJob:CUD" }); const datasets = api.datasets.allDatasetMeta.useQuery({ projectId }); // Fetch costs for all evaluators const evaluatorIds = evaluators.data?.configs.map((config) => config.id) ?? []; const costs = api.evals.costByEvaluatorIds.useQuery( { projectId, evaluatorIds, }, { enabled: evaluators.isSuccess && evaluatorIds.length > 0, meta: { silentHttpCodes: [503], }, }, ); useEffect(() => { if (evaluators.isSuccess) { const { configs: configList = [] } = evaluators.data ?? {}; setDetailPageList( "evals", configList.map((evaluator) => ({ id: evaluator.id })), ); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [evaluators.isSuccess, evaluators.data]); const columnHelper = createColumnHelper(); const columns = [ columnHelper.accessor("scoreName", { id: "scoreName", header: "Generated Score Name", size: 200, cell: (row) => { const scoreName = row.getValue(); return scoreName ? : undefined; }, }), columnHelper.accessor("status", { header: "Status", id: "status", size: 80, cell: (row) => { const status = row.getValue(); return ( ); }, }), columnHelper.accessor("totalCost", { header: "Total Cost (7d)", id: "totalCost", size: 120, cell: (row) => { const totalCost = row.getValue(); if (!costs.data) return ; if (totalCost != null) return usdFormatter(totalCost, 2, 4); return "–"; }, }), columnHelper.accessor("result", { header: "Result", id: "result", size: 150, cell: (row) => { const result = row.getValue(); return ; }, }), columnHelper.accessor("logs", { header: "Logs", id: "logs", size: 150, cell: ({ row }) => { const id = row.original.id; return ( ); }, }), columnHelper.accessor("template", { id: "template", header: "Referenced Evaluator", size: 200, cell: ({ row }) => { const template = row.original.template; if (!template) return "template not found"; return (
); }, }), columnHelper.accessor("createdAt", { id: "createdAt", header: "Created At", enableSorting: true, size: 150, }), columnHelper.accessor("updatedAt", { id: "updatedAt", header: "Updated At", enableSorting: true, size: 150, }), columnHelper.accessor("target", { id: "target", header: "Target", size: 150, enableHiding: true, }), columnHelper.accessor("filter", { id: "filter", header: "Filter", size: 200, enableHiding: true, cell: (row) => { const filterState = row.getValue(); // FIX: Temporary workaround: Used to display a different value than the actual value since multiSelect doesn't support key-value pairs const newFilterState = filterState.map((filter) => { if (filter.type === "stringOptions" && filter.column === "Dataset") { return { ...filter, value: filter.value.map( (datasetId) => datasets.data?.find((d) => d.id === datasetId)?.name ?? datasetId, ), }; } return filter; }); return (
); }, }), columnHelper.accessor("id", { header: "Id", id: "id", size: 100, enableHiding: true, cell: (row) => { const id = row.getValue(); return id ? : undefined; }, }), columnHelper.accessor("actions", { header: "Actions", id: "actions", size: 100, cell: ({ row }) => { const id = row.original.id; return ( Actions { e.stopPropagation(); if (id) setEditConfigId(id); }} > Edit ); }, }), ] as LangfuseColumnDef[]; const [columnVisibility, setColumnVisibility] = useColumnVisibility( "evalConfigColumnVisibility", columns, ); const peekNavigationProps = usePeekNavigation(); const convertToTableRow = ( jobConfig: RouterOutputs["evals"]["allConfigs"]["configs"][number], ): EvaluatorDataRow => { const result = generateJobExecutionCounts(jobConfig.jobExecutionsByState); const costData = costs.data?.[jobConfig.id]; return { id: jobConfig.id, status: jobConfig.finalStatus, createdAt: jobConfig.createdAt.toLocaleString(), updatedAt: jobConfig.updatedAt.toLocaleString(), template: jobConfig.evalTemplate ? { id: jobConfig.evalTemplate.id, name: jobConfig.evalTemplate.name, version: jobConfig.evalTemplate.version, } : undefined, scoreName: jobConfig.scoreName, target: jobConfig.targetObject, filter: z.array(singleFilter).parse(jobConfig.filter), result: result, maintainer: jobConfig.evalTemplate ? jobConfig.evalTemplate.projectId ? "User maintained" : jobConfig.evalTemplate.name.startsWith(RAGAS_TEMPLATE_PREFIX) ? "Langfuse and Ragas maintained" : "Langfuse maintained" : "Not available", totalCost: costData, }; }; return (
{/* Toolbar spanning full width */} {/* Content area with sidebar and table */}
), ...peekNavigationProps, }} data={ evaluators.isLoading ? { isLoading: true, isError: false } : evaluators.isError ? { isLoading: false, isError: true, error: evaluators.error.message, } : { isLoading: false, isError: false, data: safeExtract(evaluators.data, "configs", []).map( (evaluator) => convertToTableRow(evaluator), ), } } pagination={{ totalCount, onChange: setPaginationState, state: paginationState, }} orderBy={orderByState} setOrderBy={setOrderByState} columnVisibility={columnVisibility} onColumnVisibilityChange={setColumnVisibility} />
{ if (!open) setEditConfigId(null); }} > Edit configuration {existingEvaluator.isLoading ? (
) : ( { setEditConfigId(null); void utils.evals.allConfigs.invalidate(); showSuccessToast({ title: "Evaluator updated successfully", description: "Changes will automatically be reflected future evaluator runs", }); }} /> )}
); }