Skip to main content
Glama

@arizeai/phoenix-mcp

Official
by Arize-ai
ExperimentCompareTable.tsx36.4 kB
import React, { ReactNode, RefObject, startTransition, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { graphql, PreloadedQuery, usePaginationFragment, usePreloadedQuery, } from "react-relay"; import { useSearchParams } from "react-router"; import { CellContext, ColumnDef, flexRender, getCoreRowModel, Table, useReactTable, } from "@tanstack/react-table"; import { useVirtualizer } from "@tanstack/react-virtual"; import { css } from "@emotion/react"; import { Dialog, DialogTrigger, Empty, Flex, Heading, Icon, IconButton, Icons, Modal, ModalOverlay, Popover, PopoverArrow, Separator, Switch, Text, Tooltip, TooltipArrow, TooltipTrigger, TriggerWrap, View, } from "@phoenix/components"; import { AnnotationDetailsContent } from "@phoenix/components/annotation/AnnotationDetailsContent"; import { JSONText } from "@phoenix/components/code/JSONText"; import { ExperimentAverageRunTokenCosts, useExperimentColors, } from "@phoenix/components/experiment"; import { ExperimentActionMenu } from "@phoenix/components/experiment/ExperimentActionMenu"; import { ExperimentAnnotationButton } from "@phoenix/components/experiment/ExperimentAnnotationButton"; import { ExperimentAverageRunTokenCount } from "@phoenix/components/experiment/ExperimentAverageRunTokenCount"; import { CellTop, CompactJSONCell } from "@phoenix/components/table"; import { borderedTableCSS, tableCSS } from "@phoenix/components/table/styles"; import { TableEmpty } from "@phoenix/components/table/TableEmpty"; import { LatencyText } from "@phoenix/components/trace/LatencyText"; import { Truncate } from "@phoenix/components/utility/Truncate"; import { ExampleDetailsDialog } from "@phoenix/pages/example/ExampleDetailsDialog"; import { ExperimentCompareDetailsDialog } from "@phoenix/pages/experiment/ExperimentCompareDetailsDialog"; import { ExperimentComparePageQueriesCompareGridQuery } from "@phoenix/pages/experiment/ExperimentComparePageQueries"; import { ExperimentNameWithColorSwatch } from "@phoenix/pages/experiment/ExperimentNameWithColorSwatch"; import { ExperimentRunAnnotationFiltersList } from "@phoenix/pages/experiment/ExperimentRunAnnotationFiltersList"; import { TraceDetailsDialog } from "@phoenix/pages/experiment/TraceDetailsDialog"; import { floatFormatter } from "@phoenix/utils/numberFormatUtils"; import { makeSafeColumnId } from "@phoenix/utils/tableUtils"; import type { ExperimentCompareTable_comparisons$data, ExperimentCompareTable_comparisons$key, } from "./__generated__/ExperimentCompareTable_comparisons.graphql"; import type { ExperimentCompareTableQuery as ExperimentCompareTableQueryType } from "./__generated__/ExperimentCompareTableQuery.graphql"; import { ExperimentRepeatedRunGroupMetadata } from "./ExperimentRepeatedRunGroupMetadata"; import { ExperimentRepetitionSelector } from "./ExperimentRepetitionSelector"; import { ExperimentRunFilterConditionField } from "./ExperimentRunFilterConditionField"; type ExampleCompareTableProps = { queryRef: PreloadedQuery<ExperimentCompareTableQueryType>; datasetId: string; baseExperimentId: string; compareExperimentIds: string[]; }; type Experiment = NonNullable< ExperimentCompareTable_comparisons$data["dataset"]["experiments"] >["edges"][number]["experiment"]; type ExperimentInfoMap = Partial<Record<string, Experiment>>; type ExperimentComparison = ExperimentCompareTable_comparisons$data["compareExperiments"]["edges"][number]["comparison"]; type ExperimentRepeatedRunGroup = ExperimentComparison["repeatedRunGroups"][number]; type AnnotationSummary = ExperimentRepeatedRunGroup["annotationSummaries"][number]; type ExperimentRun = ExperimentRepeatedRunGroup["runs"][number]; type ExperimentRunAnnotation = ExperimentRun["annotations"]["edges"][number]["annotation"]; type TableRow = ExperimentComparison & { id: string; input: unknown; referenceOutput: unknown; repeatedRunGroupsByExperimentId: Record<string, ExperimentRepeatedRunGroup>; }; const tableWrapCSS = css` flex: 1 1 auto; overflow: auto; // Make sure the table fills up the remaining space table { min-width: 100%; td { vertical-align: top; } } `; const PAGE_SIZE = 50; export function ExperimentCompareTable(props: ExampleCompareTableProps) { const [dialog, setDialog] = useState<ReactNode>(null); const [selectedExampleIndex, setSelectedExampleIndex] = useState< number | null >(null); const [displayFullText, setDisplayFullText] = useState(false); const { datasetId, baseExperimentId, compareExperimentIds } = props; const [filterCondition, setFilterCondition] = useState(""); const tableContainerRef = useRef<HTMLDivElement>(null); const [, setSearchParams] = useSearchParams(); const { baseExperimentColor, getExperimentColor } = useExperimentColors(); const preloadedData = usePreloadedQuery( ExperimentComparePageQueriesCompareGridQuery, props.queryRef ); const { data, loadNext, hasNext, isLoadingNext, refetch } = usePaginationFragment< ExperimentCompareTableQueryType, ExperimentCompareTable_comparisons$key >( graphql` fragment ExperimentCompareTable_comparisons on Query @refetchable(queryName: "ExperimentCompareTableQuery") @argumentDefinitions( first: { type: "Int", defaultValue: 50 } after: { type: "String", defaultValue: null } baseExperimentId: { type: "ID!" } compareExperimentIds: { type: "[ID!]!" } experimentIds: { type: "[ID!]!" } datasetId: { type: "ID!" } filterCondition: { type: "String", defaultValue: null } ) { compareExperiments( first: $first after: $after baseExperimentId: $baseExperimentId compareExperimentIds: $compareExperimentIds filterCondition: $filterCondition ) @connection(key: "ExperimentCompareTable_compareExperiments") { edges { comparison: node { example { id revision { input referenceOutput: output } } repeatedRunGroups { ...ExperimentRepeatedRunGroupMetadataFragment annotationSummaries { annotationName meanScore } experimentId runs { id latencyMs repetitionNumber output error trace { traceId projectId } costSummary { total { tokens cost } } annotations { edges { annotation: node { id name score label annotatorKind explanation trace { traceId projectId } } } } } } } } } dataset: node(id: $datasetId) { id ... on Dataset { experiments(filterIds: $experimentIds) { edges { experiment: node { id name sequenceNumber metadata datasetVersionId project { id } costSummary { total { cost tokens } } averageRunLatencyMs runCount repetitions } } } } } } `, preloadedData ); const experimentInfoById = useMemo(() => { return ( data.dataset?.experiments?.edges.reduce((acc, edge) => { acc[edge.experiment.id] = { ...edge.experiment, }; return acc; }, {} as ExperimentInfoMap) || {} ); }, [data]); const baseExperiment = experimentInfoById[baseExperimentId]; const tableData: TableRow[] = useMemo( () => data.compareExperiments.edges.map((edge) => { const comparison = edge.comparison; const repeatedRunGroupsByExperimentId = comparison.repeatedRunGroups.reduce( (acc, group) => { acc[group.experimentId] = group; return acc; }, {} as Record<string, ExperimentRepeatedRunGroup> ); return { ...comparison, id: comparison.example.id, input: comparison.example.revision.input, referenceOutput: comparison.example.revision.referenceOutput, repeatedRunGroupsByExperimentId, }; }), [data] ); const exampleIds = useMemo(() => { return tableData.map((row) => row.id); }, [tableData]); const baseColumns: ColumnDef<TableRow>[] = useMemo(() => { return [ { header: "input", accessorKey: "input", enableSorting: false, cell: ({ row }) => { return ( <Flex direction="column" height="100%" css={css` overflow: hidden; `} > <CellTop extra={ <TooltipTrigger> <IconButton size="S" onPress={() => { setDialog( <ExampleDetailsDialog exampleId={row.original.example.id} datasetVersionId={baseExperiment?.datasetVersionId} /> ); }} > <Icon svg={<Icons.ExpandOutline />} /> </IconButton> <Tooltip> <TooltipArrow /> view example </Tooltip> </TooltipTrigger> } > <Text size="S" color="text-500" >{`example ${row.original.example.id}`}</Text> </CellTop> <PaddedCell> <LargeTextWrap> <JSONText json={row.original.input} disableTitle space={displayFullText ? 2 : 0} /> </LargeTextWrap> </PaddedCell> </Flex> ); }, }, { header: "reference output", accessorKey: "referenceOutput", enableSorting: false, cell: (props) => ( <> <CellTop> <Text size="S" color="text-500"> reference </Text> </CellTop> <PaddedCell> {displayFullText ? JSONCell(props) : CompactJSONCell(props)} </PaddedCell> </> ), }, ]; }, [baseExperiment?.datasetVersionId, displayFullText, setDialog]); const experimentColumns: ColumnDef<TableRow>[] = useMemo(() => { return [baseExperimentId, ...compareExperimentIds].map( (experimentId, experimentIndex) => ({ header: () => { const experiment = experimentInfoById[experimentId]; const name = experiment?.name || "unknown-experiment"; const metadata = experiment?.metadata; const projectId = experiment?.project?.id; const experimentColor = experimentIndex === 0 ? baseExperimentColor : getExperimentColor(experimentIndex - 1); return ( <Flex direction="row" gap="size-100" wrap alignItems="center" justifyContent="space-between" > <Flex direction="column" gap="size-50"> <ExperimentNameWithColorSwatch name={name} color={experimentColor} /> <div> {experiment && <ExperimentMetadata experiment={experiment} />} </div> </Flex> <Flex direction="row" wrap justifyContent="end" alignItems="center" > <ExperimentActionMenu experimentId={experimentId} metadata={metadata} projectId={projectId} canDeleteExperiment={false} /> </Flex> </Flex> ); }, accessorKey: experimentId, minSize: 500, enableSorting: false, cell: ({ row }) => { const repeatedRunGroup = row.original.repeatedRunGroupsByExperimentId[experimentId]; // if a new experiment was just added, data may not be fully loaded yet if (repeatedRunGroup == null) { return null; } const annotationSummaries = repeatedRunGroup.annotationSummaries; return ( <ExperimentRunOutputCell rowIndex={row.index} experimentRepetitionCount={ experimentInfoById[experimentId]?.repetitions ?? 0 } repeatedRunGroup={repeatedRunGroup} displayFullText={displayFullText} setDialog={setDialog} setSelectedExampleIndex={setSelectedExampleIndex} annotationSummaries={annotationSummaries} /> ); }, }) ); }, [ baseExperimentId, baseExperimentColor, compareExperimentIds, displayFullText, experimentInfoById, getExperimentColor, ]); const columns = useMemo(() => { return [...baseColumns, ...experimentColumns]; }, [baseColumns, experimentColumns]); const table = useReactTable<TableRow>({ columns: columns, data: tableData, getCoreRowModel: getCoreRowModel(), columnResizeMode: "onChange", }); /** * Instead of calling `column.getSize()` on every render for every header * and especially every data cell (very expensive), * we will calculate all column sizes at once at the root table level in a useMemo * and pass the column sizes down as CSS variables to the <table> element. */ const columnSizeVars = React.useMemo(() => { const headers = table.getFlatHeaders(); const colSizes: { [key: string]: number } = {}; for (let i = 0; i < headers.length; i++) { const header = headers[i]!; colSizes[`--header-${makeSafeColumnId(header.id)}-size`] = header.getSize(); colSizes[`--col-${makeSafeColumnId(header.column.id)}-size`] = header.column.getSize(); } return colSizes; // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps }, [table.getState().columnSizingInfo, table.getState().columnSizing]); const rows = table.getRowModel().rows; const isEmpty = rows.length === 0; const fetchMoreOnBottomReached = useCallback( (containerRefElement?: HTMLDivElement | null) => { if (containerRefElement) { const { scrollHeight, scrollTop, clientHeight } = containerRefElement; //once the user has scrolled within 300px of the bottom of the table, fetch more data if there is any if ( scrollHeight - scrollTop - clientHeight < 300 && !isLoadingNext && hasNext ) { loadNext(PAGE_SIZE); } } }, [hasNext, isLoadingNext, loadNext] ); useEffect(() => { //if the filter condition changes, we need to reset the pagination startTransition(() => { refetch( { after: null, first: PAGE_SIZE, filterCondition, baseExperimentId, compareExperimentIds, datasetId, }, { fetchPolicy: "store-and-network" } ); }); }, [ datasetId, baseExperimentId, compareExperimentIds, filterCondition, refetch, ]); return ( <View overflow="auto"> <Flex direction="column" height="100%"> <View paddingTop="size-100" paddingBottom="size-100" paddingStart="size-200" paddingEnd="size-200" borderBottomColor="grey-300" borderBottomWidth="thin" flex="none" > <Flex direction="row" gap="size-200" alignItems="center"> <ExperimentRunFilterConditionField onValidCondition={setFilterCondition} /> <Switch onChange={(isSelected) => { setDisplayFullText(isSelected); }} defaultSelected={false} labelPlacement="start" > <Text>Full Text</Text> </Switch> </Flex> </View> <div css={tableWrapCSS} onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)} ref={tableContainerRef} > <table css={css(tableCSS, borderedTableCSS)} style={{ ...columnSizeVars, width: table.getTotalSize(), minWidth: "100%", }} > <thead> {table.getHeaderGroups().map((headerGroup) => ( <tr key={headerGroup.id}> {headerGroup.headers.map((header) => ( <th key={header.id} colSpan={header.colSpan} style={{ width: `calc(var(--header-${makeSafeColumnId(header?.id)}-size) * 1px)`, }} > {header.isPlaceholder ? null : ( <> <div {...{ className: header.column.getCanSort() ? "sort" : "", onClick: header.column.getToggleSortingHandler(), style: { left: header.getStart(), width: "100%", }, }} > <Truncate maxWidth="100%"> {flexRender( header.column.columnDef.header, header.getContext() )} </Truncate> {header.column.getIsSorted() ? ( <Icon className="sort-icon" svg={ header.column.getIsSorted() === "asc" ? ( <Icons.ArrowUpFilled /> ) : ( <Icons.ArrowDownFilled /> ) } /> ) : null} </div> <div {...{ onMouseDown: header.getResizeHandler(), onTouchStart: header.getResizeHandler(), className: `resizer ${ header.column.getIsResizing() ? "isResizing" : "" }`, }} /> </> )} </th> ))} </tr> ))} </thead> {isEmpty ? ( <TableEmpty /> ) : table.getState().columnSizingInfo.isResizingColumn ? ( /* When resizing any column we will render this special memoized version of our table body */ <MemoizedTableBody table={table} tableContainerRef={tableContainerRef} /> ) : ( <TableBody table={table} tableContainerRef={tableContainerRef} /> )} </table> </div> </Flex> <ModalOverlay isOpen={selectedExampleIndex !== null} onOpenChange={(isOpen) => { if (!isOpen) { setSelectedExampleIndex(null); } }} > <Modal variant="slideover" size="fullscreen"> {selectedExampleIndex !== null && exampleIds[selectedExampleIndex] && baseExperiment && ( <ExperimentCompareDetailsDialog datasetId={datasetId} datasetVersionId={baseExperiment.datasetVersionId} selectedExampleIndex={selectedExampleIndex} selectedExampleId={exampleIds[selectedExampleIndex]} baseExperimentId={baseExperimentId} compareExperimentIds={compareExperimentIds} exampleIds={exampleIds} onExampleChange={(exampleIndex) => { if ( exampleIndex === exampleIds.length - 1 && !isLoadingNext && hasNext ) { loadNext(PAGE_SIZE); } if (exampleIndex >= 0 && exampleIndex < exampleIds.length) { setSelectedExampleIndex(exampleIndex); } }} openTraceDialog={(traceId, projectId, title) => { setDialog( <TraceDetailsDialog traceId={traceId} projectId={projectId} title={title} /> ); }} /> )} </Modal> </ModalOverlay> <ModalOverlay isOpen={!!dialog} onOpenChange={() => { // Clear the URL search params for the span selection setSearchParams( (prev) => { const newParams = new URLSearchParams(prev); newParams.delete("selectedSpanNodeId"); return newParams; }, { replace: true } ); setDialog(null); }} > <Modal variant="slideover" size="fullscreen"> {dialog} </Modal> </ModalOverlay> </View> ); } //un-memoized normal table body component - see memoized version below function TableBody<T>({ table, tableContainerRef, }: { table: Table<T>; tableContainerRef: RefObject<HTMLDivElement | null>; }) { const rows = table.getRowModel().rows; const virtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => tableContainerRef.current, estimateSize: () => 350, overscan: 5, }); const virtualRows = virtualizer.getVirtualItems(); const totalHeight = virtualizer.getTotalSize(); const spacerRowHeight = useMemo(() => { return totalHeight - virtualRows.reduce((acc, item) => acc + item.size, 0); }, [totalHeight, virtualRows]); return ( <tbody> {virtualRows.map((virtualRow, index) => { const row = rows[virtualRow.index]; return ( <tr key={row.id} style={{ height: `${virtualRow.size}px`, transform: `translateY(${ virtualRow.start - index * virtualRow.size }px)`, }} > {row.getVisibleCells().map((cell) => { return ( <td key={cell.id} style={{ width: `calc(var(--col-${makeSafeColumnId(cell.column.id)}-size) * 1px)`, maxWidth: `calc(var(--col-${makeSafeColumnId(cell.column.id)}-size) * 1px)`, padding: 0, }} > {flexRender(cell.column.columnDef.cell, cell.getContext())} </td> ); })} </tr> ); })} {/* Add a spacer row to ensure the sticky header does not scroll out of view and to make scrolling smoother */} <tr> <td style={{ height: `${spacerRowHeight}px`, padding: 0, }} colSpan={table.getAllColumns().length} /> </tr> </tbody> ); } //special memoized wrapper for our table body that we will use during column resizing export const MemoizedTableBody = React.memo( TableBody, (prev, next) => prev.table.options.data === next.table.options.data ) as typeof TableBody; function ExperimentMetadata(props: { experiment: Experiment }) { const { experiment } = props; const averageRunLatencyMs = experiment.averageRunLatencyMs; const runCount = experiment.runCount; const costTotal = experiment.costSummary.total.cost; const tokenCountTotal = experiment.costSummary.total.tokens; const averageRunCostTotal = costTotal == null || runCount == 0 ? null : costTotal / runCount; const averageRunTokenCountTotal = tokenCountTotal == null || runCount == 0 ? null : tokenCountTotal / runCount; return ( <Flex direction="row" gap="size-100" alignItems="center"> <TooltipTrigger> <TriggerWrap> <Text size="S" fontFamily="mono" color="grey-500"> AVG </Text> </TriggerWrap> <Tooltip>Averages computed over all runs in the experiment</Tooltip> </TooltipTrigger> {averageRunLatencyMs != null && ( <LatencyText size="S" latencyMs={averageRunLatencyMs} /> )} <ExperimentAverageRunTokenCount averageRunTokenCountTotal={averageRunTokenCountTotal} experimentId={experiment.id} size="S" /> {averageRunCostTotal != null && ( <ExperimentAverageRunTokenCosts averageRunCostTotal={averageRunCostTotal} experimentId={experiment.id} size="S" /> )} </Flex> ); } /** * Display the output of an experiment run. */ function ExperimentRunOutput( props: ExperimentRun & { numRepetitions: number; displayFullText: boolean; setDialog: (dialog: ReactNode) => void; annotationSummaries: readonly AnnotationSummary[]; } ) { const { output, error, annotations, displayFullText, setDialog } = props; if (error) { return <RunError error={error} />; } const annotationsList = annotations?.edges.length ? annotations.edges.map((edge) => edge.annotation) : []; return ( <Flex direction="column" height="100%" justifyContent="space-between"> <View padding="size-200" flex="1 1 auto"> <LargeTextWrap> <JSONText json={output} disableTitle space={displayFullText ? 2 : 0} /> </LargeTextWrap> </View> <ExperimentRunCellAnnotationsList annotations={annotationsList} annotationSummaries={props.annotationSummaries} numRepetitions={props.numRepetitions} onTraceClick={({ traceId, projectId, annotationName }) => { setDialog( <TraceDetailsDialog title={`Evaluator Trace: ${annotationName}`} traceId={traceId} projectId={projectId} /> ); }} /> </Flex> ); } function RunError({ error }: { error: string }) { return ( <Flex direction="row" gap="size-50" alignItems="center"> <Icon svg={<Icons.AlertCircleOutline />} color="danger" /> <Text color="danger">{error}</Text> </Flex> ); } function JSONCell<TData extends object, TValue>({ getValue, }: CellContext<TData, TValue>) { const value = getValue(); return ( <LargeTextWrap> <JSONText json={value} space={2} /> </LargeTextWrap> ); } function LargeTextWrap({ children }: { children: ReactNode }) { return ( <div css={css` height: 300px; overflow-y: auto; flex: 1 1 auto; `} > {children} </div> ); } function PaddedCell({ children }: { children: ReactNode }) { return ( <View paddingX="size-200" paddingY="size-100"> {children} </View> ); } export type ExperimentRunCellAnnotationsListProps = { annotations: ExperimentRunAnnotation[]; annotationSummaries: readonly AnnotationSummary[]; numRepetitions: number; onTraceClick: ({ annotationName, traceId, projectId, }: { annotationName: string; traceId: string; projectId: string; }) => void; }; export function ExperimentRunCellAnnotationsList( props: ExperimentRunCellAnnotationsListProps ) { const { annotations, annotationSummaries, onTraceClick, numRepetitions } = props; const annotationSummaryByAnnotationName = useMemo(() => { return annotationSummaries.reduce( (acc, summary) => { acc[summary.annotationName] = summary; return acc; }, {} as Record<string, AnnotationSummary> ); }, [annotationSummaries]); return ( <ul css={css` display: flex; flex-direction: column; flex: none; padding: 0 var(--ac-global-dimension-static-size-100) var(--ac-global-dimension-static-size-100) var(--ac-global-dimension-static-size-100); `} > {annotations.map((annotation) => { const traceId = annotation.trace?.traceId; const projectId = annotation.trace?.projectId; const hasTrace = traceId != null && projectId != null; const meanAnnotationScore = annotationSummaryByAnnotationName[annotation.name]?.meanScore; return ( <li key={annotation.id} css={css` display: flex; flex-direction: row; align-items: center; justify-content: space-between; gap: var(--ac-global-dimension-static-size-50); `} > <DialogTrigger> <ExperimentAnnotationButton annotation={annotation} extra={ meanAnnotationScore != null && numRepetitions > 1 ? ( <Flex direction="row" gap="size-100" alignItems="center"> <Text fontFamily="mono"> {floatFormatter(meanAnnotationScore)} </Text> <Text fontFamily="mono" color="grey-500"> AVG </Text> </Flex> ) : null } /> <Popover placement="top"> <PopoverArrow /> <Dialog style={{ width: 400 }}> <View padding="size-200"> <Flex direction="column" gap="size-50"> <AnnotationDetailsContent annotation={annotation} /> <Separator /> <section> <Heading level={4} weight="heavy"> Filters </Heading> <ExperimentRunAnnotationFiltersList annotation={annotation} /> </section> </Flex> </View> </Dialog> </Popover> </DialogTrigger> <TooltipTrigger> <IconButton size="S" onPress={() => { if (hasTrace) { onTraceClick({ annotationName: annotation.name, traceId, projectId, }); } }} > <Icon svg={<Icons.Trace />} /> </IconButton> <Tooltip> <TooltipArrow /> View evaluation trace </Tooltip> </TooltipTrigger> </li> ); })} </ul> ); } function ExperimentRunOutputCell({ experimentRepetitionCount, repeatedRunGroup, displayFullText, setDialog, rowIndex, setSelectedExampleIndex, annotationSummaries, }: { experimentRepetitionCount: number; repeatedRunGroup: ExperimentRepeatedRunGroup; displayFullText: boolean; setDialog: (dialog: ReactNode) => void; rowIndex: number; setSelectedExampleIndex: (index: number) => void; annotationSummaries: readonly AnnotationSummary[]; }) { const [selectedRepetitionNumber, setSelectedRepetitionNumber] = useState(1); const runsByRepetitionNumber = useMemo(() => { const runsByRepetitionNumber = repeatedRunGroup.runs.reduce( (acc, run) => { acc[run.repetitionNumber] = run; return acc; }, {} as Record<number, ExperimentRun> ); return runsByRepetitionNumber; }, [repeatedRunGroup.runs]); if (repeatedRunGroup.runs.length === 0) { return ( <PaddedCell> <Empty message="No Run" /> </PaddedCell> ); } const run: ExperimentRun | null = runsByRepetitionNumber[selectedRepetitionNumber]; const traceId = run?.trace?.traceId; const projectId = run?.trace?.projectId; const hasTrace = traceId != null && projectId != null; const runControls = ( <> {experimentRepetitionCount > 1 ? ( <ExperimentRepetitionSelector repetitionNumber={selectedRepetitionNumber} totalRepetitions={experimentRepetitionCount} setRepetitionNumber={setSelectedRepetitionNumber} /> ) : null} <TooltipTrigger> <IconButton className="expand-button" size="S" aria-label="View example run details" onPress={() => { setSelectedExampleIndex(rowIndex); }} > <Icon svg={<Icons.ExpandOutline />} /> </IconButton> <Tooltip> <TooltipArrow /> view experiment run </Tooltip> </TooltipTrigger> <TooltipTrigger isDisabled={!hasTrace}> <IconButton className="trace-button" size="S" aria-label="View run trace" onPress={() => { setDialog( <TraceDetailsDialog traceId={traceId || ""} projectId={projectId || ""} title={`Experiment Run Trace`} /> ); }} isDisabled={!hasTrace} > <Icon svg={<Icons.Trace />} /> </IconButton> <Tooltip> <TooltipArrow /> view run trace </Tooltip> </TooltipTrigger> </> ); return ( <Flex direction="column" height="100%"> <CellTop extra={runControls}> <ExperimentRepeatedRunGroupMetadata fragmentRef={repeatedRunGroup} /> </CellTop> {run ? ( <ExperimentRunOutput {...run} numRepetitions={experimentRepetitionCount} displayFullText={displayFullText} setDialog={setDialog} annotationSummaries={annotationSummaries} /> ) : ( <PaddedCell> <Empty message="Missing Repetition" /> </PaddedCell> )} </Flex> ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Arize-ai/phoenix'

If you have feedback or need assistance with the MCP directory API, please join our Discord server