diff --git a/web/oss/src/components/CustomUIs/CustomTreeComponent/assets/styles.ts b/web/oss/src/components/CustomUIs/CustomTreeComponent/assets/styles.ts index 7e156bb88b..3090759158 100644 --- a/web/oss/src/components/CustomUIs/CustomTreeComponent/assets/styles.ts +++ b/web/oss/src/components/CustomUIs/CustomTreeComponent/assets/styles.ts @@ -9,12 +9,13 @@ export const useStyles = createUseStyles((theme: JSSTheme) => ({ position: "absolute", left: 6, top: 0, - bottom: -6, + bottom: -12, width: 1, backgroundColor: theme.colorBorder, }, "&.last::before": { height: "50%", + bottom: "auto", }, }, nodeLabel: { @@ -22,8 +23,8 @@ export const useStyles = createUseStyles((theme: JSSTheme) => ({ cursor: "default", display: "flex", alignItems: "center", - marginTop: 4, - marginBottom: 4, + marginTop: 12, + marginBottom: 12, "&::before": { content: "''", position: "absolute", diff --git a/web/oss/src/components/CustomUIs/CustomTreeComponent/index.tsx b/web/oss/src/components/CustomUIs/CustomTreeComponent/index.tsx index 6fd0ec11cf..a7b78b0e1b 100644 --- a/web/oss/src/components/CustomUIs/CustomTreeComponent/index.tsx +++ b/web/oss/src/components/CustomUIs/CustomTreeComponent/index.tsx @@ -1,67 +1,96 @@ -import React, {useState} from "react" +import React, {useMemo, useState} from "react" import {MinusSquareOutlined, PlusSquareOutlined} from "@ant-design/icons" -import {TreeContent} from "@/oss/components/SharedDrawers/TraceDrawer/components/TraceTree" -import {TraceSpanNode} from "@/oss/services/tracing/types" - import {useStyles} from "./assets/styles" /** * CustomTree is a recursive tree view component for rendering a hierarchy of nodes. * * This component is highly customizable and highlights the selected node. - * It supports displaying additional metrics like latency, cost, and token usage. + * It supports custom node rendering and optional default expansion. * * Example usage: * ```tsx * node.id} + * getChildren={(node) => node.children} + * renderLabel={(node) => node.title} * selectedKey={selectedNodeId} - * onSelect={(key) => setSelectedNodeId(key)} + * onSelect={(key, node) => setSelectedNodeId(key)} * /> * ``` */ -interface TreeProps { +interface TreeProps { /** * Root node of the hierarchical data structure. */ - data: TraceSpanNode + data: TNode + + /** + * Returns a stable key for a node. + */ + getKey: (node: TNode) => string /** - * Settings for what additional metrics to show in each node. + * Returns child nodes for a node. */ - settings: { - latency: boolean - cost: boolean - tokens: boolean - } + getChildren: (node: TNode) => TNode[] | undefined + + /** + * Render the label content for a node. + */ + renderLabel: (node: TNode) => React.ReactNode /** * The currently selected node key (ID). */ - selectedKey: string | null + selectedKey?: string | null /** * Function to handle when a node is selected. */ - onSelect: (key: string) => void + onSelect?: (key: string, node: TNode) => void + + /** + * Default expansion state for nodes without explicit `expanded` metadata. + */ + defaultExpanded?: boolean } -const TreeNodeComponent: React.FC<{ - node: TraceSpanNode +const TreeNodeComponent = ({ + node, + isLast, + getKey, + getChildren, + renderLabel, + selectedKey, + onSelect, + defaultExpanded = true, + isRoot = false, +}: { + node: TNode isLast: boolean - settings: {latency: boolean; cost: boolean; tokens: boolean} - selectedKey: string | null - onSelect: (key: string) => void + getKey: (node: TNode) => string + getChildren: (node: TNode) => TNode[] | undefined + renderLabel: (node: TNode) => React.ReactNode + selectedKey?: string | null + onSelect?: (key: string, node: TNode) => void + defaultExpanded?: boolean isRoot?: boolean -}> = ({node, isLast, settings, selectedKey, onSelect, isRoot = false}) => { +}) => { const classes = useStyles() - const [expanded, setExpanded] = useState( - typeof (node as any).expanded === "boolean" ? (node as any).expanded : true, - ) - const hasChildren = node.children && node.children.length > 0 + const initialExpanded = useMemo(() => { + if (typeof (node as {expanded?: boolean}).expanded === "boolean") { + return (node as {expanded?: boolean}).expanded as boolean + } + return defaultExpanded + }, [defaultExpanded, node]) + const [expanded, setExpanded] = useState(initialExpanded) + const children = getChildren(node) ?? [] + const hasChildren = children.length > 0 + const nodeKey = getKey(node) const toggle = () => setExpanded((prev) => !prev) @@ -81,7 +110,7 @@ const TreeNodeComponent: React.FC<{ ? `${classes.nodeLabel} ${shouldShowAsLast ? "last" : ""}` : "flex items-center" } - onClick={() => onSelect(node.span_id)} + onClick={() => onSelect?.(nodeKey, node)} > {hasChildren && ( - + {renderLabel(node)} {hasChildren && expanded && (
- {node.children!.map((child, index) => ( + {children.map((child, index) => ( ))} @@ -125,15 +157,26 @@ const TreeNodeComponent: React.FC<{ ) } -const CustomTree: React.FC = ({data, settings, selectedKey, onSelect}) => { +const CustomTree = ({ + data, + getKey, + getChildren, + renderLabel, + selectedKey, + onSelect, + defaultExpanded, +}: TreeProps) => { return ( -
+
diff --git a/web/oss/src/components/EvalRunDetails/Table.tsx b/web/oss/src/components/EvalRunDetails/Table.tsx index 5fe9ad2850..e274a3d36a 100644 --- a/web/oss/src/components/EvalRunDetails/Table.tsx +++ b/web/oss/src/components/EvalRunDetails/Table.tsx @@ -5,7 +5,6 @@ import {useAtomValue, useStore} from "jotai" import {message} from "@/oss/components/AppMessageContext" import VirtualizedScenarioTableAnnotateDrawer from "@/oss/components/EvalRunDetails/components/AnnotateDrawer/VirtualizedScenarioTableAnnotateDrawer" -import ScenarioColumnVisibilityPopoverContent from "@/oss/components/EvalRunDetails/components/columnVisibility/ColumnVisibilityPopoverContent" import { InfiniteVirtualTableFeatureShell, type TableFeaturePagination, @@ -24,6 +23,7 @@ import {runDisplayNameAtomFamily} from "./atoms/runDerived" import type {EvaluationTableColumn} from "./atoms/table" import {DEFAULT_SCENARIO_PAGE_SIZE} from "./atoms/table" import type {PreviewTableRow} from "./atoms/tableRows" +import ScenarioColumnVisibilityPopoverContent from "./components/columnVisibility/ColumnVisibilityPopoverContent" import { evaluationPreviewDatasetStore, evaluationPreviewTableStore, @@ -40,12 +40,6 @@ import {patchFocusDrawerQueryParams} from "./state/urlFocusDrawer" type TableRowData = PreviewTableRow -// Alternating background colors for timestamp-based batch grouping -const TIMESTAMP_GROUP_COLORS = [ - "rgba(59, 130, 246, 0.06)", // blue - "rgba(16, 185, 129, 0.06)", // green -] - interface EvalRunDetailsTableProps { runId: string evaluationType: "auto" | "human" | "online" @@ -93,26 +87,13 @@ const EvalRunDetailsTable = ({ const previewColumns = usePreviewColumns({columnResult, evaluationType}) - // Inject synthetic columns for comparison exports (hidden in table display) - const columnsWithSyntheticColumns = useMemo(() => { + // Inject synthetic columns for comparison exports (do not render in UI) + const exportColumns = useMemo(() => { const hasCompareRuns = compareSlots.some(Boolean) if (!hasCompareRuns) { return previewColumns.columns } - const hiddenColumnStyle = { - display: "none", - width: 0, - minWidth: 0, - maxWidth: 0, - padding: 0, - margin: 0, - border: "none", - visibility: "hidden", - position: "absolute", - left: "-9999px", - } as const - // Create synthetic "Run" column for export only (completely hidden in table) const runColumn = { key: "__run_type__", @@ -124,8 +105,6 @@ const EvalRunDetailsTable = ({ render: () => null, exportEnabled: true, exportLabel: "Run", - onHeaderCell: () => ({style: hiddenColumnStyle}), - onCell: () => ({style: hiddenColumnStyle}), } // Create synthetic "Run ID" column for export only (completely hidden in table) @@ -139,8 +118,6 @@ const EvalRunDetailsTable = ({ render: () => null, exportEnabled: true, exportLabel: "Run ID", - onHeaderCell: () => ({style: hiddenColumnStyle}), - onCell: () => ({style: hiddenColumnStyle}), } return [runColumn, runIdColumn, ...previewColumns.columns] @@ -306,21 +283,6 @@ const EvalRunDetailsTable = ({ [handleLoadMore, handleResetPages, mergedRows], ) - // Build timestamp color map for row grouping (only for online evaluations) - const timestampColorMap = useMemo(() => { - const map = new Map() - if (evaluationType !== "online") return map - - // Process rows in order to assign consistent colors - mergedRows.forEach((row) => { - if (row.timestamp && !map.has(row.timestamp)) { - const colorIndex = map.size % TIMESTAMP_GROUP_COLORS.length - map.set(row.timestamp, TIMESTAMP_GROUP_COLORS[colorIndex]) - } - }) - return map - }, [evaluationType, mergedRows]) - // Build group map for export label resolution const groupMap = useMemo(() => { return buildGroupMap(columnResult?.groups) @@ -851,17 +813,27 @@ const EvalRunDetailsTable = ({ resolveColumnLabel, filename: `${runDisplayName || runId}-scenarios.csv`, beforeExport: loadAllPagesBeforeExport, + columnsOverride: exportColumns, }), - [exportResolveValue, resolveColumnLabel, runId, runDisplayName, loadAllPagesBeforeExport], + [ + exportResolveValue, + resolveColumnLabel, + runId, + runDisplayName, + loadAllPagesBeforeExport, + exportColumns, + ], ) + const hasCompareRuns = compareSlots.some(Boolean) + return ( -
-
+
+
datasetStore={evaluationPreviewDatasetStore} tableScope={tableScope} - columns={columnsWithSyntheticColumns} + columns={previewColumns.columns} rowKey={(record) => record.key} tableClassName={clsx( "agenta-scenario-table", @@ -898,17 +870,13 @@ const EvalRunDetailsTable = ({ bordered: true, tableLayout: "fixed", onRow: (record) => { - // Determine background color: comparison color takes precedence, then timestamp grouping - let backgroundColor: string | undefined - if (record.compareIndex) { - backgroundColor = getComparisonColor(record.compareIndex) - } else if ( - evaluationType === "online" && - record.timestamp && - timestampColorMap.has(record.timestamp) - ) { - backgroundColor = timestampColorMap.get(record.timestamp) - } + const backgroundColor = hasCompareRuns + ? getComparisonColor( + typeof record.compareIndex === "number" + ? record.compareIndex + : 0, + ) + : "#fff" return { onClick: (event) => { diff --git a/web/oss/src/components/EvalRunDetails/atoms/compare.ts b/web/oss/src/components/EvalRunDetails/atoms/compare.ts index d86d1ecf79..281feefce6 100644 --- a/web/oss/src/components/EvalRunDetails/atoms/compare.ts +++ b/web/oss/src/components/EvalRunDetails/atoms/compare.ts @@ -168,7 +168,20 @@ export const deriveRunComparisonStructure = ({ } /** Terminal statuses that allow comparison */ -const TERMINAL_STATUSES = new Set(["success", "failure", "errors", "cancelled"]) +export const TERMINAL_STATUSES = new Set([ + "success", + "failure", + "failed", + "errors", + "cancelled", + "completed", + "finished", + "ok", + "evaluation_finished", + "evaluation_finished_with_errors", + "evaluation_failed", + "evaluation_aggregation_failed", +]) /** Check if a status is terminal (run has finished) */ export const isTerminalStatus = (status: string | undefined | null): boolean => { diff --git a/web/oss/src/components/EvalRunDetails/atoms/table/run.ts b/web/oss/src/components/EvalRunDetails/atoms/table/run.ts index abddb442c3..29e4d7624b 100644 --- a/web/oss/src/components/EvalRunDetails/atoms/table/run.ts +++ b/web/oss/src/components/EvalRunDetails/atoms/table/run.ts @@ -4,10 +4,14 @@ import {atomWithQuery} from "jotai-tanstack-query" import axios from "@/oss/lib/api/assets/axiosConfig" import {buildRunIndex} from "@/oss/lib/evaluations/buildRunIndex" import {snakeToCamelCaseKeys} from "@/oss/lib/helpers/casing" +import { + getPreviewRunBatcher, + invalidatePreviewRunCache, +} from "@/oss/lib/hooks/usePreviewEvaluations/assets/previewRunBatcher" +import {TERMINAL_STATUSES} from "../compare" import {effectiveProjectIdAtom} from "../run" -import {getPreviewRunBatcher} from "@/agenta-oss-common/lib/hooks/usePreviewEvaluations/assets/previewRunBatcher" import type {EvaluationRun} from "@/agenta-oss-common/lib/hooks/usePreviewEvaluations/types" export interface EvaluationRunQueryResult { @@ -16,6 +20,11 @@ export interface EvaluationRunQueryResult { runIndex: ReturnType } +const isTerminalStatus = (status: string | null | undefined) => { + if (!status) return false + return TERMINAL_STATUSES.has(status.toLowerCase()) +} + const patchedRunRevisionSet = new Set() const buildRevisionPayload = (references: Record | undefined) => { @@ -309,6 +318,11 @@ export const evaluationRunQueryAtomFamily = atomFamily((runId: string | null) => gcTime: 5 * 60 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false, + refetchInterval: (query) => { + const status = + query.state.data?.rawRun?.status ?? query.state.data?.camelRun?.status + return isTerminalStatus(status) ? false : 5000 + }, queryFn: async () => { if (!runId) { throw new Error("evaluationRunQueryAtomFamily requires a run id") @@ -317,6 +331,7 @@ export const evaluationRunQueryAtomFamily = atomFamily((runId: string | null) => throw new Error("evaluationRunQueryAtomFamily requires a project id") } + invalidatePreviewRunCache(projectId, runId) const batcher = getPreviewRunBatcher() const rawRun = await batcher({projectId, runId}) if (!rawRun) { diff --git a/web/oss/src/components/EvalRunDetails/components/CompareRunsMenu.tsx b/web/oss/src/components/EvalRunDetails/components/CompareRunsMenu.tsx index 21759f1631..781e18a2c2 100644 --- a/web/oss/src/components/EvalRunDetails/components/CompareRunsMenu.tsx +++ b/web/oss/src/components/EvalRunDetails/components/CompareRunsMenu.tsx @@ -1,9 +1,12 @@ import {memo, useCallback, useEffect, useMemo, useState} from "react" import {Button, Checkbox, Input, List, Popover, Space, Tag, Tooltip, Typography} from "antd" +import clsx from "clsx" import {useAtomValue, useSetAtom} from "jotai" +import Image from "next/image" import {message} from "@/oss/components/AppMessageContext" +import EmptyComponent from "@/oss/components/Placeholders/EmptyComponent" import ReferenceTag from "@/oss/components/References/ReferenceTag" import axios from "@/oss/lib/api/assets/axiosConfig" import dayjs from "@/oss/lib/helpers/dateTimeHelper/dayjs" @@ -104,9 +107,11 @@ const CompareRunsMenu = ({runId}: CompareRunsMenuProps) => { { - const ids = new Set() - candidates.forEach((candidate) => { - candidate.structure.testsetIds.forEach((id) => ids.add(id)) - }) - return Array.from(ids) - }, [candidates]) - const candidateTestsetNameMap = useTestsetNameMap(candidateTestsetIds) - const filteredCandidates = useMemo(() => { const query = searchTerm.trim().toLowerCase() return candidates.filter((candidate) => { @@ -211,6 +207,10 @@ const CompareRunsPopoverContent = memo(({runId, availability}: CompareRunsPopove }) }, [candidates, searchTerm, statusFilter]) + const hasLoadedRuns = Boolean((swrData as any)?.data) + const showLoading = Boolean(swrData.isLoading && !hasLoadedRuns) + const showEmptyState = !showLoading && filteredCandidates.length === 0 + const handleToggle = useCallback( (targetId: string) => { setCompareIds((prev) => { @@ -228,167 +228,172 @@ const CompareRunsPopoverContent = memo(({runId, availability}: CompareRunsPopove [setCompareIds], ) - const handleRemove = useCallback( - (targetId: string) => { - setCompareIds((prev) => prev.filter((id) => id !== targetId)) - }, - [setCompareIds], - ) - const handleClearAll = useCallback(() => { setCompareIds([]) }, [setCompareIds]) - const selectedDetails = useMemo(() => { - const map = new Map() - candidates.forEach((candidate) => { - map.set(candidate.id, candidate) - }) - return compareIds.map( - (id) => - map.get(id) ?? { - id, - name: id, - status: undefined, - createdAt: undefined, - testsetNames: [], - structure: {testsetIds: [], hasQueryInput: false, inputStepCount: 0}, - }, - ) - }, [candidates, compareIds]) - return ( - -
- - {availability.testsetIds.length ? ( - - {availability.testsetIds.map((id) => { - const label = matchingTestsetNameMap[id] ?? id - const copyValue = id - const href = buildTestsetHref(id) - - return ( - - ) - })} - - ) : null} - -
- -
- - Selected {compareIds.length}/{MAX_COMPARISON_RUNS} - -
- {selectedDetails.map((run) => ( - { - event.preventDefault() - handleRemove(run.id) - }} - > - {run.name} - - ))} + +
+
+
+ Testset: + {availability.testsetIds.length ? ( +
+ {availability.testsetIds.map((id) => { + const label = matchingTestsetNameMap[id] ?? id + const copyValue = id + const href = buildTestsetHref(id) + + return ( + + ) + })} +
+ ) : ( + + )} +
+ + + Selected: {compareIds.length}/{MAX_COMPARISON_RUNS} + + {compareIds.length ? ( + + ) : null} +
- {compareIds.length ? ( - - ) : null} + + setSearchTerm(event.target.value)} + bordered={false} + /> + +
- setSearchTerm(event.target.value)} - /> - - - - { - const isChecked = compareIds.includes(item.id) - const createdLabel = item.createdAt - ? dayjs(item.createdAt).format("DD MMM YYYY") - : "" - const _resolvedTestsetNames = - item.testsetNames.length > 0 - ? item.testsetNames - : item.structure.testsetIds - .map((id) => candidateTestsetNameMap[id]) - .filter((name): name is string => Boolean(name)) - return ( - handleToggle(item.id)} - className="compare-run-row flex flex-col !items-start justify-start" - > -
- event.stopPropagation()} - onChange={(event) => { - event.stopPropagation() - handleToggle(item.id) - }} - > -
- {item.name} - - {item.description?.trim() - ? item.description - : "No description"} - -
-
- - - {item.status ? : null} - {createdLabel ? ( - - {createdLabel} - - ) : null} - + {showLoading ? ( +
+ Loading evaluations... +
+ ) : showEmptyState ? ( +
+ + } + description={ +
+
+ No evaluations to compare +
+
+ Run another evaluation using the same test set to enable + comparison. +
- - ) - }} - /> + } + /> +
+ ) : ( + { + const isChecked = compareIds.includes(item.id) + const createdLabel = item.createdAt + ? dayjs(item.createdAt).format("DD MMM YYYY") + : "" + + return ( + handleToggle(item.id)} + className={clsx( + "compare-run-row flex flex-col !items-start justify-start", + "!py-1 !px-2", + "border-b border-[#EAEFF5]", + "last:border-b-0", + isChecked && "compare-run-row--selected", + )} + style={{borderBottomStyle: "solid"}} + > +
+ event.stopPropagation()} + onChange={(event) => { + event.stopPropagation() + handleToggle(item.id) + }} + > +
+ {item.name} + + {item.description?.trim() + ? item.description + : "No description"} + +
+
+ + + {item.status ? : null} + {createdLabel ? ( + + {createdLabel} + + ) : null} + +
+
+ ) + }} + /> + )} ) }) @@ -457,8 +462,8 @@ const TestsetReferenceTag = ({ label={label} copyValue={copyValue} href={href} - tone="testset" className="max-w-[200px]" + showIcon={false} /> ) diff --git a/web/oss/src/components/EvalRunDetails/components/EvaluationRunTag.tsx b/web/oss/src/components/EvalRunDetails/components/EvaluationRunTag.tsx new file mode 100644 index 0000000000..4871c2cd10 --- /dev/null +++ b/web/oss/src/components/EvalRunDetails/components/EvaluationRunTag.tsx @@ -0,0 +1,64 @@ +import {ReactNode} from "react" + +import {PushpinFilled} from "@ant-design/icons" +import {Tag} from "antd" +import clsx from "clsx" + +import {getComparisonColor, getComparisonSolidColor} from "../atoms/compare" + +interface EvaluationRunTagProps { + label: string + compareIndex?: number + isBaseRun?: boolean + closable?: boolean + closeIcon?: ReactNode + onClose?: (event: React.MouseEvent) => void + className?: string +} + +const EvaluationRunTag = ({ + label, + compareIndex, + isBaseRun, + closable, + closeIcon, + onClose, + className, +}: EvaluationRunTagProps) => { + const resolvedCompareIndex = compareIndex ?? 0 + const resolvedIsBaseRun = isBaseRun ?? resolvedCompareIndex === 0 + const tagColor = getComparisonSolidColor(resolvedCompareIndex) + const tagBg = getComparisonColor(resolvedCompareIndex) + + return ( + + ) : undefined + } + closable={closable} + closeIcon={closeIcon} + onClose={onClose} + > + + {label} + + + ) +} + +export default EvaluationRunTag diff --git a/web/oss/src/components/EvalRunDetails/components/EvaluatorMetricsChart/index.tsx b/web/oss/src/components/EvalRunDetails/components/EvaluatorMetricsChart/index.tsx index a822f96bc7..69204c5588 100644 --- a/web/oss/src/components/EvalRunDetails/components/EvaluatorMetricsChart/index.tsx +++ b/web/oss/src/components/EvalRunDetails/components/EvaluatorMetricsChart/index.tsx @@ -41,6 +41,38 @@ interface EvaluatorLabelProps { fallbackLabel: string } +type MetricDeltaTone = "positive" | "negative" | "neutral" + +interface MetricStripEntry { + key: string + label: string + color: string + value: number | null + displayValue: string + isMain: boolean + deltaText: string + deltaTone: MetricDeltaTone +} + +const getMainEvaluatorSeries = (entries: MetricStripEntry[]) => + entries.find((entry) => entry.isMain) ?? entries[0] + +const computeDeltaPercent = (current: number | null, baseline: number | null) => { + if (typeof current !== "number" || typeof baseline !== "number") return null + if (!Number.isFinite(current) || !Number.isFinite(baseline) || baseline === 0) return null + return ((current - baseline) / baseline) * 100 +} + +const formatDelta = (delta: number | null): {text: string; tone: MetricDeltaTone} => { + if (delta === null || !Number.isFinite(delta)) { + return {text: "-", tone: "neutral"} + } + const rounded = Math.round(delta) + if (rounded > 0) return {text: `+${rounded}%`, tone: "positive"} + if (rounded < 0) return {text: `${rounded}%`, tone: "negative"} + return {text: "0%", tone: "neutral"} +} + const EvaluatorMetricsChartTitle = memo( ({runId, evaluatorRef, fallbackLabel}: EvaluatorLabelProps) => { const evaluatorAtom = useMemo( @@ -243,25 +275,145 @@ const EvaluatorMetricsChart = ({ (isBooleanMetric && booleanChartData.length > 0) || hasCategoricalFrequency - const summaryValue = useMemo((): string | null => { - if (isBooleanMetric) { - const percentage = booleanHistogram.percentages.true - return Number.isFinite(percentage) ? `${percentage.toFixed(2)}%` : "—" - } - if (hasCategoricalFrequency && categoricalFrequencyData.length) { - return null + const comparisonBooleanPercentMap = useMemo(() => { + const map = new Map() + comparisonBooleanHistograms.forEach((entry) => { + if (Number.isFinite(entry.histogram.percentages.true)) { + map.set(entry.runId, entry.histogram.percentages.true) + } + }) + return map + }, [comparisonBooleanHistograms]) + + const summaryItems = useMemo(() => { + const baseValue = (() => { + if (!resolvedStats) return {value: null, displayValue: "—"} + if (isBooleanMetric) { + const percentage = booleanHistogram.percentages.true + return Number.isFinite(percentage) + ? {value: percentage, displayValue: `${percentage.toFixed(2)}%`} + : {value: null, displayValue: "—"} + } + if (hasCategoricalFrequency) { + return {value: null, displayValue: "—"} + } + if (typeof resolvedStats.mean === "number" && Number.isFinite(resolvedStats.mean)) { + return {value: resolvedStats.mean, displayValue: format3Sig(resolvedStats.mean)} + } + return {value: null, displayValue: "—"} + })() + + const baseEntry: MetricStripEntry = { + key: baseSeriesKey, + label: resolvedRunName, + color: resolvedBaseColor, + value: baseValue.value, + displayValue: baseValue.displayValue, + isMain: true, + deltaText: "-", + deltaTone: "neutral", } - if (typeof stats.mean === "number") return format3Sig(stats.mean) - return "—" + + const comparisonEntries = comparisonSeries.map((entry) => { + const statsValue = entry.stats + if (!statsValue) { + return { + key: entry.runId, + label: entry.runName, + color: entry.color, + value: null, + displayValue: "—", + isMain: false, + deltaText: "-", + deltaTone: "neutral", + } + } + if (isBooleanMetric) { + const percentage = comparisonBooleanPercentMap.get(entry.runId) + return { + key: entry.runId, + label: entry.runName, + color: entry.color, + value: typeof percentage === "number" ? percentage : null, + displayValue: + typeof percentage === "number" && Number.isFinite(percentage) + ? `${percentage.toFixed(2)}%` + : "—", + isMain: false, + deltaText: "-", + deltaTone: "neutral", + } + } + if (hasCategoricalFrequency) { + return { + key: entry.runId, + label: entry.runName, + color: entry.color, + value: null, + displayValue: "—", + isMain: false, + deltaText: "-", + deltaTone: "neutral", + } + } + if (typeof statsValue.mean === "number" && Number.isFinite(statsValue.mean)) { + return { + key: entry.runId, + label: entry.runName, + color: entry.color, + value: statsValue.mean, + displayValue: format3Sig(statsValue.mean), + isMain: false, + deltaText: "-", + deltaTone: "neutral", + } + } + return { + key: entry.runId, + label: entry.runName, + color: entry.color, + value: null, + displayValue: "—", + isMain: false, + deltaText: "-", + deltaTone: "neutral", + } + }) + + const entries = [baseEntry, ...comparisonEntries] + const mainSeries = getMainEvaluatorSeries(entries) + + return entries.map((entry) => { + if (entry.isMain) { + return entry + } + const delta = computeDeltaPercent(entry.value, mainSeries?.value ?? null) + const formatted = formatDelta(delta) + return { + ...entry, + deltaText: formatted.text, + deltaTone: formatted.tone, + } + }) }, [ + baseSeriesKey, booleanHistogram.percentages.true, - categoricalFrequencyData, - effectiveScenarioCount, + comparisonBooleanPercentMap, + comparisonSeries, hasCategoricalFrequency, isBooleanMetric, - stats, + resolvedBaseColor, + resolvedRunName, + resolvedStats, ]) + const metricsGridClass = useMemo(() => { + if (summaryItems.length <= 1) return "grid-cols-1" + if (summaryItems.length === 2) return "grid-cols-2" + if (summaryItems.length === 3) return "grid-cols-3" + return "grid-cols-2 sm:grid-cols-4" + }, [summaryItems.length]) + const chartContent = () => { if (isBooleanMetric) { if (!booleanChartData.length) { @@ -277,13 +429,13 @@ const EvaluatorMetricsChart = ({ key: baseSeriesKey, name: resolvedRunName, color: resolvedBaseColor, - barProps: {radius: [8, 8, 0, 0]}, + barProps: {radius: [8, 8, 0, 0], minPointSize: 2}, }, ...comparisonBooleanHistograms.map((entry) => ({ key: entry.runId, name: entry.runName, color: entry.color, - barProps: {radius: [8, 8, 0, 0]}, + barProps: {radius: [8, 8, 0, 0], minPointSize: 2}, })), ] @@ -297,8 +449,8 @@ const EvaluatorMetricsChart = ({ yDomain={[0, 100]} series={series} barCategoryGap="20%" - showLegend={stableComparisons.length > 0} - reserveLegendSpace={stableComparisons.length > 0} + showLegend={false} + reserveLegendSpace={false} /> ) } @@ -363,13 +515,13 @@ const EvaluatorMetricsChart = ({ key: baseSeriesKey, name: resolvedRunName, color: resolvedBaseColor, - barProps: {radius: [8, 8, 0, 0]}, + barProps: {radius: [8, 8, 0, 0], minPointSize: 2}, }, ...comparisonMaps.map((entry) => ({ key: entry.runId, name: entry.runName, color: entry.color, - barProps: {radius: [8, 8, 0, 0]}, + barProps: {radius: [8, 8, 0, 0], minPointSize: 2}, })), ] @@ -383,8 +535,8 @@ const EvaluatorMetricsChart = ({ yDomain={[0, "auto"]} series={series} barCategoryGap="20%" - showLegend={stableComparisons.length > 0} - reserveLegendSpace={stableComparisons.length > 0} + showLegend={false} + reserveLegendSpace={false} /> ) } @@ -443,10 +595,11 @@ const EvaluatorMetricsChart = ({ return ( + > +
+
- } - > -
- {stableComparisons.length === 0 && ( -
- {summaryValue !== null ? ( - - {summaryValue} - - ) : null} +
+
+ {summaryItems.map((entry) => ( +
+ + {entry.displayValue} + + + {entry.deltaText} + +
+ ))} +
+
+
+
+
+ {isLoading ? ( + + ) : hasError && !resolvedStats ? ( +
+ Unable to load metric data. +
+ ) : ( + chartContent() + )}
- )} -
0 ? "h-[370px]" : "h-[300px]"}> - {isLoading ? ( - - ) : hasError && !resolvedStats ? ( -
- Unable to load metric data. -
- ) : ( - chartContent() - )}
diff --git a/web/oss/src/components/EvalRunDetails/components/EvaluatorMetricsSpiderChart/EvaluatorMetricsSpiderChart.tsx b/web/oss/src/components/EvalRunDetails/components/EvaluatorMetricsSpiderChart/EvaluatorMetricsSpiderChart.tsx index 46e63411ae..8362595711 100644 --- a/web/oss/src/components/EvalRunDetails/components/EvaluatorMetricsSpiderChart/EvaluatorMetricsSpiderChart.tsx +++ b/web/oss/src/components/EvalRunDetails/components/EvaluatorMetricsSpiderChart/EvaluatorMetricsSpiderChart.tsx @@ -76,8 +76,8 @@ const EvaluatorMetricsSpiderChart = ({ ) } - const LABEL_OFFSET = 12 - const NUDGE = 5 + const LABEL_OFFSET = 10 + const NUDGE = 0 const RAD = Math.PI / 180 return ( @@ -126,10 +126,12 @@ const EvaluatorMetricsSpiderChart = ({ } const lines = clampLines(label, 18) + const lineHeight = 12 + const blockOffset = -((lines.length - 1) * lineHeight) / 2 return ( {lines.map((ln, i) => ( - + {ln} ))} diff --git a/web/oss/src/components/EvalRunDetails/components/FocusDrawer.tsx b/web/oss/src/components/EvalRunDetails/components/FocusDrawer.tsx index 224f37b6f0..e2c65a5158 100644 --- a/web/oss/src/components/EvalRunDetails/components/FocusDrawer.tsx +++ b/web/oss/src/components/EvalRunDetails/components/FocusDrawer.tsx @@ -1,7 +1,10 @@ -import {memo, useCallback, useMemo} from "react" +import type {KeyboardEvent, ReactNode} from "react" +import {memo, useCallback, useMemo, useRef, useState} from "react" import {isValidElement} from "react" -import {Popover, Skeleton, Tag, Typography} from "antd" +import {DownOutlined} from "@ant-design/icons" +import {Button, Popover, Skeleton, Typography} from "antd" +import clsx from "clsx" import {useAtomValue, useSetAtom} from "jotai" import {AlertCircle} from "lucide-react" import dynamic from "next/dynamic" @@ -9,9 +12,10 @@ import dynamic from "next/dynamic" import {previewRunMetricStatsSelectorFamily} from "@/oss/components/Evaluations/atoms/runMetrics" import MetricDetailsPreviewPopover from "@/oss/components/Evaluations/components/MetricDetailsPreviewPopover" import GenericDrawer from "@/oss/components/GenericDrawer" +import SharedGenerationResultUtils from "@/oss/components/SharedGenerationResultUtils" -import ReadOnlyBox from "../../pages/evaluations/onlineEvaluation/components/ReadOnlyBox" -import {getComparisonSolidColor} from "../atoms/compare" +import {compareRunIdsAtom, MAX_COMPARISON_RUNS} from "../atoms/compare" +import {invocationTraceSummaryAtomFamily} from "../atoms/invocationTraceSummary" import { applicationReferenceQueryAtomFamily, testsetReferenceQueryAtomFamily, @@ -43,16 +47,13 @@ import {clearFocusDrawerQueryParams} from "../state/urlFocusDrawer" import {renderScenarioChatMessages} from "../utils/chatMessages" import {formatMetricDisplay, METRIC_EMPTY_PLACEHOLDER} from "../utils/metricFormatter" +import EvaluationRunTag from "./EvaluationRunTag" import FocusDrawerHeader from "./FocusDrawerHeader" import FocusDrawerSidePanel from "./FocusDrawerSidePanel" +import {SectionCard} from "./views/ConfigurationView/components/SectionPrimitives" const JsonEditor = dynamic(() => import("@/oss/components/Editor/Editor"), {ssr: false}) -const SECTION_CARD_CLASS = "rounded-xl border border-[#EAECF0] bg-white" - -// Color palette for category tags (same as MetricCell) -const TAG_COLORS = ["green", "blue", "purple", "orange", "cyan", "magenta", "gold", "lime"] - const toSectionAnchorId = (value: string) => `focus-section-${value .toLowerCase() @@ -81,7 +82,7 @@ const buildStaticMetricColumn = ( } as EvaluationTableColumn & {__source: "runMetric"} } -const {Text, Title} = Typography +const {Text} = Typography type FocusDrawerColumn = EvaluationTableColumn & {__source?: "runMetric"} @@ -125,6 +126,32 @@ const resolveRunMetricScalar = (stats: any): unknown => { return undefined } +const FocusValueCard = ({ + label, + children, + className, +}: { + label: ReactNode + children: ReactNode + className?: string +}) => ( +
+ {label} +
{children}
+
+) + +const MetricValuePill = ({value, muted}: {value: ReactNode; muted?: boolean}) => ( + + {value} + +) + interface FocusDrawerContentProps { runId: string scenarioId: string @@ -170,12 +197,11 @@ const useFocusDrawerSections = (runId: string | null) => { return groups .map((group) => { - if (group.kind === "metric" && group.id === "metrics:human") { + if (group.kind === "metric") { return null } - const sectionLabel = - group.kind === "metric" && group.id === "metrics:auto" ? "Metrics" : group.label + const sectionLabel = group.label const dynamicColumns: SectionColumnEntry[] = group.columnIds .map((columnId) => columnMap.get(columnId)) @@ -285,14 +311,14 @@ const FocusGroupLabel = ({ ) if (group?.kind === "input" && testsetId && testsetQuery.data?.name) { - return <>{`Testset ${testsetQuery.data.name}`} + return "Input" } if (group?.kind === "invocation") { const applicationLabel = appQuery.data?.name ?? appQuery.data?.slug ?? appQuery.data?.id ?? applicationId ?? null - if (applicationLabel) return <>{`Application ${applicationLabel}`} + if (applicationLabel) return "Outputs" } return <>{label} @@ -358,32 +384,23 @@ const RunMetricValue = memo( return (
{column.displayLabel ?? column.label ?? column.id} - - {isLoading ? ( - - ) : ( - - - {formattedValue} - - - )} - + {isLoading ? ( + + ) : ( + + + + )}
) }, @@ -534,52 +551,57 @@ const ScenarioColumnValue = memo( })() // Render array metrics as tags in a vertical stack + const isLongTextMetric = + !arrayTags.length && + typeof formattedValue === "string" && + (formattedValue.length > 80 || formattedValue.includes("\n")) + const renderMetricContent = () => { if (arrayTags.length > 0) { return ( -
+
{arrayTags.map((tag, index) => ( - - {tag} - + ))}
) } - return ( - - {formattedValue} - - ) + if (isLongTextMetric) { + return ( + + {formattedValue} + + ) + } + return + } + + const metricContent = showSkeleton ? ( + + ) : ( + + {renderMetricContent()} + + ) + + if (isLongTextMetric) { + return {metricContent} } return ( -
- {displayLabel} - - {showSkeleton ? ( - - ) : ( - - {renderMetricContent()} - - )} - +
+ {displayLabel} + {metricContent}
) } @@ -707,16 +729,169 @@ const ScenarioColumnValue = memo( } } + return {renderValue()} + }, +) + +ScenarioColumnValue.displayName = "ScenarioColumnValue" + +const EvalOutputMetaRow = memo( + ({ + runId, + scenarioId, + compareIndex, + }: { + runId: string + scenarioId: string + compareIndex?: number + }) => { + const runDisplayNameAtom = useMemo(() => runDisplayNameAtomFamily(runId), [runId]) + const runDisplayName = useAtomValue(runDisplayNameAtom) + const traceSummaryAtom = useMemo( + () => invocationTraceSummaryAtomFamily({scenarioId, runId}), + [runId, scenarioId], + ) + const traceSummary = useAtomValue(traceSummaryAtom) + const resolvedCompareIndex = compareIndex ?? 0 + return ( -
- {displayLabel} - {renderValue()} +
+ +
) }, ) -ScenarioColumnValue.displayName = "ScenarioColumnValue" +EvalOutputMetaRow.displayName = "EvalOutputMetaRow" + +const FocusSectionHeader = ({ + title, + collapsed, + onToggle, +}: { + title: ReactNode + collapsed: boolean + onToggle: () => void +}) => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault() + onToggle() + } + } + + return ( +
+ {title} +
+ ) +} + +const FocusSectionContent = memo( + ({ + section, + runId, + scenarioId, + }: { + section: FocusDrawerSection + runId: string + scenarioId: string + }) => { + const isInputSection = section.group?.kind === "input" + + return ( +
+ {section.group?.kind === "invocation" ? ( + + ) : null} + + {section.columns.map(({column, descriptor}) => ( + + ))} +
+ ) + }, +) + +FocusSectionContent.displayName = "FocusSectionContent" + +const FocusDrawerSectionCard = memo( + ({ + section, + runId, + scenarioId, + }: { + section: FocusDrawerSection + runId: string + scenarioId: string + }) => { + const [collapsed, setCollapsed] = useState(false) + const sectionLabelNode = useMemo( + () => , + [runId, section.group, section.label], + ) + + return ( +
+ setCollapsed((value) => !value)} + /> + {!collapsed ? ( + + + + ) : null} +
+ ) + }, +) + +FocusDrawerSectionCard.displayName = "FocusDrawerSectionCard" const InvocationMetaChips = memo( ({group, runId}: {group: EvaluationTableColumnGroup | null; runId: string | null}) => { @@ -765,7 +940,7 @@ const InvocationMetaChips = memo( : null return ( -
+
{appLabel ? {appLabel} : null} {variantLabel ? (
@@ -792,65 +967,105 @@ const CompareRunColumnContent = memo( runId, scenarioId, section, - compareIndex, }: { runId: string scenarioId: string section: FocusDrawerSection - compareIndex: number }) => { - const runDisplayNameAtom = useMemo(() => runDisplayNameAtomFamily(runId), [runId]) - const runDisplayName = useAtomValue(runDisplayNameAtom) - return ( -
- {/* Run header with color indicator */} -
-
- - {runDisplayName || - (compareIndex === 0 ? "Base Run" : `Comparison ${compareIndex}`)} - -
+ + + + ) + }, +) - {/* Invocation meta chips if applicable */} - {section.group?.kind === "invocation" ? ( - - ) : null} +CompareRunColumnContent.displayName = "CompareRunColumnContent" - {/* Column values */} -
- {section.columns.map(({column, descriptor}) => ( - - ))} +const CompareMetaRow = memo( + ({ + compareScenarios, + columnMinWidth, + registerScrollContainer, + onScrollSync, + }: { + compareScenarios: { + runId: string | null + scenarioId: string | null + compareIndex: number + }[] + columnMinWidth: number + registerScrollContainer: (node: HTMLDivElement | null) => void + onScrollSync: (node: HTMLDivElement) => void + }) => { + const scrollRef = useRef(null) + const columnsCount = compareScenarios.length + const rowGridStyle = useMemo( + () => ({ + gridTemplateColumns: `repeat(${columnsCount}, 1fr)`, + }), + [columnsCount], + ) + const handleScroll = useCallback(() => { + if (scrollRef.current) { + onScrollSync(scrollRef.current) + } + }, [onScrollSync]) + + return ( + +
{ + scrollRef.current = node + registerScrollContainer(node) + }} + className="overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" + onScroll={handleScroll} + > +
+ {compareScenarios.map(({runId, scenarioId, compareIndex}) => { + if (!runId || !scenarioId) { + return ( +
+ +
+ ) + } + + return ( + + ) + })} +
-
+ ) }, ) -CompareRunColumnContent.displayName = "CompareRunColumnContent" +CompareMetaRow.displayName = "CompareMetaRow" /** - * A single section card containing all runs side-by-side + * A single compare section rendered as a collapsible row, aligned to shared columns. */ -const CompareSectionCard = memo( +const CompareSectionRow = memo( ({ sectionId, sectionLabel, sectionGroup, compareScenarios, sectionMapsPerRun, + columnMinWidth, + registerScrollContainer, + onScrollSync, }: { sectionId: string sectionLabel: string @@ -861,60 +1076,87 @@ const CompareSectionCard = memo( compareIndex: number }[] sectionMapsPerRun: Map[] + columnMinWidth: number + registerScrollContainer: (node: HTMLDivElement | null) => void + onScrollSync: (node: HTMLDivElement) => void }) => { - // Get the first available section for the label + const [collapsed, setCollapsed] = useState(false) + const scrollRef = useRef(null) const firstSection = sectionMapsPerRun.find((map) => map.get(sectionId))?.get(sectionId) - + const sectionLabelNode = ( + <> + {sectionGroup && firstSection ? ( + + ) : ( + sectionLabel + )} + + ) + const columnsCount = compareScenarios.length + const rowGridStyle = useMemo( + () => ({ + gridTemplateColumns: `repeat(${columnsCount}, 1fr)`, + }), + [columnsCount], + ) + const handleScroll = useCallback(() => { + if (scrollRef.current) { + onScrollSync(scrollRef.current) + } + }, [onScrollSync]) return ( -
- {/* Section header */} -
- - {sectionGroup && firstSection ? ( - <FocusGroupLabel - group={sectionGroup} - label={sectionLabel} - runId={compareScenarios[0]?.runId ?? ""} - /> - ) : ( - sectionLabel - )} - -
- - {/* Run columns side by side */} -
- {compareScenarios.map(({runId, scenarioId, compareIndex}) => { - const section = sectionMapsPerRun[compareIndex]?.get(sectionId) - - if (!runId || !scenarioId || !section) { - return ( -
- -
- ) - } +
+ setCollapsed((value) => !value)} + /> + {!collapsed ? ( +
{ + scrollRef.current = node + registerScrollContainer(node) + }} + className="overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" + onScroll={handleScroll} + > +
+ {compareScenarios.map(({runId, scenarioId, compareIndex}) => { + const section = sectionMapsPerRun[compareIndex]?.get(sectionId) + + if (!runId || !scenarioId || !section) { + return ( +
+ +
+ ) + } - return ( - - ) - })} -
-
+ return ( + + ) + })} +
+
+ ) : null} +
) }, ) -CompareSectionCard.displayName = "CompareSectionCard" +CompareSectionRow.displayName = "CompareSectionRow" /** * Inner component that handles the section data fetching for compare mode @@ -929,29 +1171,28 @@ const FocusDrawerCompareContentInner = ({ compareIndex: number }[] }) => { - // Get sections for base run (index 0) - const baseRunId = compareScenarios[0]?.runId ?? null - const {sections: baseSections} = useFocusDrawerSections(baseRunId) - - // Get sections for comparison run 1 (index 1) - const compare1RunId = compareScenarios[1]?.runId ?? null - const {sections: compare1Sections} = useFocusDrawerSections(compare1RunId) - - // Get sections for comparison run 2 (index 2) - const compare2RunId = compareScenarios[2]?.runId ?? null - const {sections: compare2Sections} = useFocusDrawerSections(compare2RunId) + const expectedColumns = Math.min(compareScenarios.length, MAX_COMPARISON_RUNS + 1) + const runId0 = compareScenarios[0]?.runId ?? null + const runId1 = compareScenarios[1]?.runId ?? null + const runId2 = compareScenarios[2]?.runId ?? null + const runId3 = compareScenarios[3]?.runId ?? null + const runId4 = compareScenarios[4]?.runId ?? null + + const {sections: sections0} = useFocusDrawerSections(runId0) + const {sections: sections1} = useFocusDrawerSections(runId1) + const {sections: sections2} = useFocusDrawerSections(runId2) + const {sections: sections3} = useFocusDrawerSections(runId3) + const {sections: sections4} = useFocusDrawerSections(runId4) // Collect all sections per run const sectionsPerRun = useMemo(() => { - const result: FocusDrawerSection[][] = [baseSections] - if (compareScenarios.length > 1) result.push(compare1Sections) - if (compareScenarios.length > 2) result.push(compare2Sections) - return result - }, [baseSections, compare1Sections, compare2Sections, compareScenarios.length]) + const all = [sections0, sections1, sections2, sections3, sections4] + return all.slice(0, expectedColumns) + }, [expectedColumns, sections0, sections1, sections2, sections3, sections4]) // Normalize section key for matching across runs // Use group.kind for invocation/input sections (which have run-specific IDs) - // Use section.id for metric sections (which have stable IDs like "metrics:auto") + // Use section.id for other stable sections const getNormalizedSectionKey = (section: FocusDrawerSection): string => { const kind = section.group?.kind if (kind === "invocation" || kind === "input") { @@ -992,18 +1233,96 @@ const FocusDrawerCompareContentInner = ({ }) }, [sectionsPerRun]) + const inputSectionEntry = useMemo(() => { + for (let index = 0; index < sectionMapsPerRun.length; index += 1) { + const section = sectionMapsPerRun[index]?.get("input") + const runId = compareScenarios[index]?.runId ?? null + const scenarioId = compareScenarios[index]?.scenarioId ?? null + if (section && runId && scenarioId) { + return {section, runId, scenarioId} + } + } + return null + }, [compareScenarios, sectionMapsPerRun]) + + const compareSections = useMemo( + () => + allSections.filter( + (section) => + section.normalizedKey !== "input" && section.normalizedKey !== "invocation", + ), + [allSections], + ) + const invocationSectionEntry = useMemo( + () => allSections.find((section) => section.normalizedKey === "invocation") ?? null, + [allSections], + ) + + const compareColumnMinWidth = 480 + const scrollContainersRef = useRef([]) + const isSyncingRef = useRef(false) + const registerScrollContainer = useCallback((node: HTMLDivElement | null) => { + if (!node) return + const list = scrollContainersRef.current + if (list.includes(node)) return + list.push(node) + }, []) + const onScrollSync = useCallback((source: HTMLDivElement) => { + if (isSyncingRef.current) return + isSyncingRef.current = true + const left = source.scrollLeft + scrollContainersRef.current.forEach((node) => { + if (node !== source && node.scrollLeft !== left) { + node.scrollLeft = left + } + }) + isSyncingRef.current = false + }, []) + return ( -
- {allSections.map(({normalizedKey, label, group}) => ( - + {inputSectionEntry ? ( + - ))} + ) : null} +
+ {invocationSectionEntry ? ( + + ) : null} + {invocationSectionEntry ? ( + + ) : null} + {compareSections.map(({normalizedKey, label, group}) => ( + + ))} +
) } @@ -1023,7 +1342,7 @@ const FocusDrawerCompareContent = () => { } return ( -
+
) @@ -1041,6 +1360,12 @@ export const FocusDrawerContent = ({ const runIndex = useAtomValue( useMemo(() => evaluationRunIndexAtomFamily(runId ?? null), [runId]), ) + const compareRunIds = useAtomValue(compareRunIdsAtom) + const compareIndex = useMemo(() => { + if (!runId) return 0 + const idx = compareRunIds.findIndex((id) => id === runId) + return idx === -1 ? 0 : idx + 1 + }, [compareRunIds, runId]) const groups = columnResult.groups ?? [] const columnMap = useMemo(() => { @@ -1057,12 +1382,11 @@ export const FocusDrawerContent = ({ return groups .map((group) => { - if (group.kind === "metric" && group.id === "metrics:human") { + if (group.kind === "metric") { return null } - const sectionLabel = - group.kind === "metric" && group.id === "metrics:auto" ? "Metrics" : group.label + const sectionLabel = group.label const dynamicColumns: SectionColumnEntry[] = group.columnIds .map((columnId) => columnMap.get(columnId)) @@ -1106,41 +1430,40 @@ export const FocusDrawerContent = ({ return (
- {sections.map((section) => ( -
-
- - <FocusGroupLabel - group={section.group} - label={section.label} - runId={runId} - /> - -
- {section.group?.kind === "invocation" ? ( - - ) : null} -
- {section.columns.map(({column, descriptor}) => ( - { + if (section.group?.kind === "invocation") { + return ( +
+ + + + - ))} -
-
- ))} +
+ ) + } + return ( + + ) + })}
) } @@ -1182,7 +1505,7 @@ const FocusDrawer = () => { afterOpenChange={handleAfterOpenChange} closeOnLayoutClick={false} expandable - className="[&_.ant-drawer-body]:p-0 [&_.ant-drawer-body]:bg-[#F8FAFC]" + className="[&_.ant-drawer-body]:p-0 [&_.ant-drawer-header]:p-4" sideContentDefaultSize={240} headerExtra={ shouldRenderContent ? ( diff --git a/web/oss/src/components/EvalRunDetails/components/FocusDrawerHeader.tsx b/web/oss/src/components/EvalRunDetails/components/FocusDrawerHeader.tsx index 401bb10827..7d830b330d 100644 --- a/web/oss/src/components/EvalRunDetails/components/FocusDrawerHeader.tsx +++ b/web/oss/src/components/EvalRunDetails/components/FocusDrawerHeader.tsx @@ -1,9 +1,10 @@ import {memo, useCallback, useEffect, useMemo} from "react" -import {LeftOutlined, RightOutlined} from "@ant-design/icons" +import {CaretDownIcon, CaretUpIcon} from "@phosphor-icons/react" import {Button, Select, SelectProps, Tag, Typography} from "antd" import {useAtomValue} from "jotai" +import TooltipWithCopyAction from "@/oss/components/EnhancedUIs/Tooltip" import {useInfiniteTablePagination} from "@/oss/components/InfiniteVirtualTable" import {evaluationPreviewTableStore} from "../evaluationPreviewTableStore" @@ -127,17 +128,17 @@ const FocusDrawerHeader = ({runId, scenarioId, onScenarioChange}: FocusDrawerHea ) return ( -
+
+ {selectedOption?.description ? ( - - + + {selectedOption.description} - - + + ) : null}
) diff --git a/web/oss/src/components/EvalRunDetails/components/FocusDrawerSidePanel.tsx b/web/oss/src/components/EvalRunDetails/components/FocusDrawerSidePanel.tsx index 73b76e5b8a..fb78d62c5a 100644 --- a/web/oss/src/components/EvalRunDetails/components/FocusDrawerSidePanel.tsx +++ b/web/oss/src/components/EvalRunDetails/components/FocusDrawerSidePanel.tsx @@ -1,10 +1,11 @@ -import {memo, useCallback, useMemo} from "react" -import type {Key} from "react" +import {memo, useCallback, useMemo, useState} from "react" +import type {ReactNode} from "react" import {TreeStructure, Download, Sparkle, Speedometer} from "@phosphor-icons/react" -import {Skeleton, Tree, type TreeDataNode} from "antd" +import {Skeleton} from "antd" import {useAtomValue} from "jotai" +import CustomTreeComponent from "@/oss/components/CustomUIs/CustomTreeComponent" import {useInfiniteTablePagination} from "@/oss/components/InfiniteVirtualTable" import {evaluationPreviewTableStore} from "../evaluationPreviewTableStore" @@ -16,7 +17,14 @@ const toSectionAnchorId = (value: string) => .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "")}` -type AnchorTreeNode = TreeDataNode & {anchorId?: string} +interface FocusTreeNode { + id: string + title: string + icon?: ReactNode + anchorId?: string + children?: FocusTreeNode[] + expanded?: boolean +} interface FocusDrawerSidePanelProps { runId: string @@ -26,6 +34,7 @@ interface FocusDrawerSidePanelProps { const FocusDrawerSidePanel = ({runId, scenarioId}: FocusDrawerSidePanelProps) => { const {columnResult} = usePreviewTableData({runId}) const evalType = useAtomValue(previewEvalTypeAtom) + const [selectedKey, setSelectedKey] = useState(null) const {rows} = useInfiniteTablePagination({ store: evaluationPreviewTableStore, @@ -57,11 +66,11 @@ const FocusDrawerSidePanel = ({runId, scenarioId}: FocusDrawerSidePanelProps) => return map }, [columnResult?.groups]) - const evaluatorNodes = useMemo(() => { + const evaluatorNodes = useMemo(() => { if (!columnResult?.evaluators?.length) return [] return columnResult.evaluators.map((evaluator) => ({ title: evaluator.name ?? evaluator.slug ?? "Evaluator", - key: `evaluator:${evaluator.id ?? evaluator.slug ?? evaluator.name}`, + id: `evaluator:${evaluator.id ?? evaluator.slug ?? evaluator.name}`, icon: , anchorId: (evaluator.id && groupAnchorMap.get(`annotation:${evaluator.id}`)) ?? @@ -70,25 +79,13 @@ const FocusDrawerSidePanel = ({runId, scenarioId}: FocusDrawerSidePanelProps) => })) }, [columnResult?.evaluators, groupAnchorMap]) - const metricNodes = useMemo(() => { - if (!columnResult?.groups?.length) return [] - return columnResult.groups - .filter((group) => group.kind === "metric" && group.id !== "metrics:human") - .map((group) => ({ - title: group.label, - key: `metric:${group.id}`, - icon: , - anchorId: groupAnchorMap.get(group.id) ?? toSectionAnchorId(group.id), - })) - }, [columnResult?.groups, groupAnchorMap]) - - const treeData = useMemo(() => { - if (!columnResult) return [] - - const children: AnchorTreeNode[] = [ + const treeData = useMemo(() => { + if (!columnResult) return null + + const children: FocusTreeNode[] = [ { title: "Input", - key: "input", + id: "input", icon: , anchorId: groupAnchorMap.get("inputs") ?? @@ -97,7 +94,7 @@ const FocusDrawerSidePanel = ({runId, scenarioId}: FocusDrawerSidePanelProps) => }, { title: "Output", - key: "output", + id: "output", icon: , anchorId: groupAnchorMap.get("outputs") ?? @@ -109,7 +106,7 @@ const FocusDrawerSidePanel = ({runId, scenarioId}: FocusDrawerSidePanelProps) => if (evaluatorNodes.length) { children.push({ title: "Evaluator", - key: "evaluator", + id: "evaluator", icon: , children: evaluatorNodes, anchorId: @@ -119,32 +116,17 @@ const FocusDrawerSidePanel = ({runId, scenarioId}: FocusDrawerSidePanelProps) => }) } - if (metricNodes.length) { - children.push({ - title: "Metrics", - key: "metrics", - icon: , - children: metricNodes, - anchorId: - groupAnchorMap.get("metrics:auto") ?? - groupAnchorMap.get("metric") ?? - toSectionAnchorId("metrics-auto"), - }) + return { + title: parentTitle, + id: "evaluation", + icon: , + children, + expanded: true, } + }, [columnResult, evaluatorNodes, groupAnchorMap, parentTitle]) - return [ - { - title: parentTitle, - key: "evaluation", - icon: , - children, - }, - ] - }, [columnResult, parentTitle, metricNodes, evaluatorNodes]) - - const handleSelect = useCallback((_selectedKeys: Key[], info: any) => { + const handleSelect = useCallback((key: string, node: FocusTreeNode) => { if (typeof window === "undefined") return - const node = info?.node as AnchorTreeNode | undefined const anchorId = node?.anchorId if (!anchorId) return const target = document.getElementById(anchorId) @@ -161,20 +143,25 @@ const FocusDrawerSidePanel = ({runId, scenarioId}: FocusDrawerSidePanelProps) => ) } - return ( -
-
- -
-
- ) + return treeData ? ( + node.id} + getChildren={(node) => node.children} + renderLabel={(node) => ( +
+ {node.icon} + {node.title} +
+ )} + selectedKey={selectedKey} + onSelect={(key, node) => { + setSelectedKey(key) + handleSelect(key, node) + }} + defaultExpanded + /> + ) : null } export default memo(FocusDrawerSidePanel) diff --git a/web/oss/src/components/EvalRunDetails/components/Page.tsx b/web/oss/src/components/EvalRunDetails/components/Page.tsx index 2693c8cb91..4827164d24 100644 --- a/web/oss/src/components/EvalRunDetails/components/Page.tsx +++ b/web/oss/src/components/EvalRunDetails/components/Page.tsx @@ -130,6 +130,7 @@ const EvalRunPreviewPage = ({runId, evaluationType, projectId = null}: EvalRunPr return ( setActiveViewParam(v)} /> } - headerClassName="px-2" + headerClassName="px-4 pt-2" > -
+
+
), diff --git a/web/oss/src/components/EvalRunDetails/components/PreviewEvalRunHeader.tsx b/web/oss/src/components/EvalRunDetails/components/PreviewEvalRunHeader.tsx index a3b75e74bb..78ad5c7612 100644 --- a/web/oss/src/components/EvalRunDetails/components/PreviewEvalRunHeader.tsx +++ b/web/oss/src/components/EvalRunDetails/components/PreviewEvalRunHeader.tsx @@ -1,34 +1,26 @@ import {memo, useCallback, useMemo, useState} from "react" -import {Pause, Play} from "@phosphor-icons/react" +import {PauseIcon, PlayIcon, XCircleIcon} from "@phosphor-icons/react" import {useQueryClient} from "@tanstack/react-query" -import {Button, Space, Tabs, Tag, Tooltip} from "antd" +import {Button, Tabs, Tooltip, Typography} from "antd" import clsx from "clsx" -import {useAtomValue} from "jotai" +import {atom, useAtomValue, useSetAtom} from "jotai" import {message} from "@/oss/components/AppMessageContext" -import dayjs from "@/oss/lib/helpers/dateTimeHelper/dayjs" import {invalidatePreviewRunCache} from "@/oss/lib/hooks/usePreviewEvaluations/assets/previewRunBatcher" import {startSimpleEvaluation, stopSimpleEvaluation} from "@/oss/services/onlineEvaluations/api" +import {compareRunIdsAtom, compareRunIdsWriteAtom, getComparisonSolidColor} from "../atoms/compare" import { + runDisplayNameAtomFamily, runInvocationRefsAtomFamily, runTestsetIdsAtomFamily, runFlagsAtomFamily, } from "../atoms/runDerived" -import {evaluationRunQueryAtomFamily} from "../atoms/table" import {previewEvalTypeAtom} from "../state/evalType" import CompareRunsMenu from "./CompareRunsMenu" - -const statusColor = (status?: string | null) => { - if (!status) return "default" - const normalized = status.toLowerCase() - if (normalized.includes("success") || normalized.includes("completed")) return "green" - if (normalized.includes("fail") || normalized.includes("error")) return "red" - if (normalized.includes("running") || normalized.includes("queued")) return "blue" - return "default" -} +import EvaluationRunTag from "./EvaluationRunTag" type ActiveView = "overview" | "focus" | "scenarios" | "configuration" @@ -150,49 +142,77 @@ const PreviewEvalRunMeta = ({ projectId?: string | null className?: string }) => { - const runQueryAtom = useMemo(() => evaluationRunQueryAtomFamily(runId), [runId]) - const runQuery = useAtomValue(runQueryAtom) const _invocationRefs = useAtomValue(useMemo(() => runInvocationRefsAtomFamily(runId), [runId])) const _testsetIds = useAtomValue(useMemo(() => runTestsetIdsAtomFamily(runId), [runId])) const {canStopOnline, handleOnlineAction, onlineAction, showOnlineAction} = useOnlineEvaluationActions(runId, projectId) - - const runData = runQuery.data?.camelRun ?? runQuery.data?.rawRun ?? null - const runStatus = runData?.status ?? null - const updatedTs = - (runData as any)?.updatedAt || - (runData as any)?.updated_at || - (runData as any)?.createdAt || - (runData as any)?.created_at || - null - const updatedMoment = updatedTs ? dayjs(updatedTs) : null - const lastUpdated = updatedMoment?.isValid() ? updatedMoment.fromNow() : undefined + const compareRunIds = useAtomValue(compareRunIdsAtom) + const setCompareRunIds = useSetAtom(compareRunIdsWriteAtom) + + const orderedRunIds = useMemo(() => { + const ids = [runId, ...compareRunIds].filter((id): id is string => Boolean(id)) + const seen = new Set() + return ids.filter((id) => { + if (seen.has(id)) return false + seen.add(id) + return true + }) + }, [compareRunIds, runId]) + + const runDescriptorsAtom = useMemo( + () => + atom((get) => + orderedRunIds.map((id) => ({ + id, + name: get(runDisplayNameAtomFamily(id)), + })), + ), + [orderedRunIds], + ) + const runDescriptors = useAtomValue(runDescriptorsAtom) return ( -
- - {runStatus ? ( - <> - - {runStatus} - - - ) : null} - {lastUpdated ? ( - - - Updated {lastUpdated} - - - ) : null} - +
+
+ Evaluations: +
+ {runDescriptors.map((run, index) => { + const isBaseRun = index === 0 + const tagColor = getComparisonSolidColor(index) + return ( + + ) : undefined + } + onClose={ + !isBaseRun + ? (event) => { + event.preventDefault() + setCompareRunIds((prev) => + prev.filter((id) => id !== run.id), + ) + } + : undefined + } + /> + ) + })} +
+
+
{showOnlineAction ? (
+
+ {!collapsed ? ( +
+ {rawEvaluator ? ( + <> {evaluator.description ? ( {evaluator.description} ) : null} -
-
- {hasEvaluatorJson ? ( - setView(val as "details" | "json")} - /> - ) : null} -
-
- - {!collapsed ? ( - <> -
- {view === "json" && hasEvaluatorJson ? ( -
- -
- ) : ( - - )} -
- - {metricsFallback.length > 0 ? ( -
- Metrics -
- {metricsFallback.map((metric) => ( - - {metric.displayLabel ?? metric.name} - - ))} -
- ) : null} + ) : ( + + )} - ) : null} -
- ) : ( -
- - Evaluator configuration snapshot is unavailable for this run. - - {metricsFallback.length ? ( -
- {metricsFallback.map((metric) => ( - - {metric.displayLabel ?? metric.name} - - ))} + ) : ( + + Evaluator configuration snapshot is unavailable for this run. + + )} + + {metricsFallback.length > 0 ? ( +
+ Metrics +
+ {metricsFallback.map((metric) => ( + + {metric.displayLabel ?? metric.name} + + ))} +
) : null}
- )} + ) : null} ) diff --git a/web/oss/src/components/EvalRunDetails/components/views/ConfigurationView/components/GeneralSection.tsx b/web/oss/src/components/EvalRunDetails/components/views/ConfigurationView/components/GeneralSection.tsx index 6a4aa67553..c8df149005 100644 --- a/web/oss/src/components/EvalRunDetails/components/views/ConfigurationView/components/GeneralSection.tsx +++ b/web/oss/src/components/EvalRunDetails/components/views/ConfigurationView/components/GeneralSection.tsx @@ -21,6 +21,7 @@ const {Text} = Typography interface GeneralSectionProps { runId: string showActions?: boolean + showHeader?: boolean } const GeneralSectionHeader = ({runId, index}: {runId: string; index: number}) => { @@ -31,7 +32,7 @@ const GeneralSectionHeader = ({runId, index}: {runId: string; index: number}) => ) } -const GeneralSection = ({runId, showActions = true}: GeneralSectionProps) => { +const GeneralSection = ({runId, showActions = true, showHeader = true}: GeneralSectionProps) => { const [collapsed, setCollapsed] = useState(false) const projectId = useAtomValue(effectiveProjectIdAtom) const invalidateRunsTable = useSetAtom(invalidateEvaluationRunsTableAtom) @@ -116,18 +117,22 @@ const GeneralSection = ({runId, showActions = true}: GeneralSectionProps) => { return (
- } - right={ -
- ) : null} - {!collapsed ? content : null} + {content} ) @@ -403,19 +348,89 @@ const ConfigurationSectionColumn = memo( }, ) +const EvaluationRunTagsRow = memo( + ({ + runIds, + registerScrollContainer, + syncScroll, + }: { + runIds: string[] + registerScrollContainer: (key: string, node: HTMLDivElement | null) => void + syncScroll: (key: string, scrollLeft: number) => void + }) => { + const columnClass = + runIds.length > 1 ? "auto-cols-[minmax(480px,1fr)]" : "auto-cols-[minmax(320px,1fr)]" + const refKey = "section-evaluations" + const handleRef = useCallback( + (node: HTMLDivElement | null) => registerScrollContainer(refKey, node), + [refKey, registerScrollContainer], + ) + const handleScroll = useCallback( + (event: UIEvent) => syncScroll(refKey, event.currentTarget.scrollLeft), + [refKey, syncScroll], + ) + + return ( + +
+ {runIds.map((runId, index) => ( + + ))} +
+
+ ) + }, +) + +const EvaluationRunTagItem = memo(({runId, index}: {runId: string; index: number}) => { + const runDisplayNameAtom = useMemo(() => runDisplayNameAtomFamily(runId), [runId]) + const runDisplayName = useAtomValue(runDisplayNameAtom) + const summaryAtom = useMemo( + () => configurationRunSummaryAtomFamily({runId, compareIndex: index}), + [runId, index], + ) + const summary = useAtomValue(summaryAtom) + const label = resolveLabel( + runDisplayName, + summary.runName !== "—" ? summary.runName : undefined, + summary.runSlug ?? undefined, + summary.runId, + ) + + return ( +
+ {summary.isLoading ? ( +
+ ) : ( + + )} +
+ ) +}) + const ConfigurationSectionRow = memo( ({ section, runIds, runIdsSignature, - runDescriptors, registerScrollContainer, syncScroll, }: { section: SectionDefinition runIds: string[] runIdsSignature: string - runDescriptors: RunDescriptor[] registerScrollContainer: (key: string, node: HTMLDivElement | null) => void syncScroll: (key: string, scrollLeft: number) => void }) => { @@ -455,14 +470,13 @@ const ConfigurationSectionRow = memo( return null } - const showRowHeader = false - // section.key === "general" || section.key === "query" - + const columnClass = + runIds.length > 1 ? "auto-cols-[minmax(480px,1fr)]" : "auto-cols-[minmax(320px,1fr)]" const grid = (
{runIds.map((runId, index) => ( setCollapsed((v) => !v) : undefined} /> ))}
) - return
{grid}
+ return ( +
+
setCollapsed((value) => !value)} + onKeyDown={(event: KeyboardEvent) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault() + setCollapsed((value) => !value) + } + }} + > + {section.title} + +
+ {!collapsed ? grid : null} +
+ ) }, ) const ConfigurationLayout = memo(({runIds}: {runIds: string[]}) => { const runIdsSignature = useMemo(() => runIds.join("|"), [runIds]) const {register, syncScroll} = useScrollSync() - const {runDescriptors} = useRunMetricData(runIds) return ( -
+
+ {sectionDefinitions.map((section) => ( ))} - {/* Render evaluators without a shared wrapper; each run renders its own evaluator cards directly */} -
- {runIds.map((runId) => ( -
- -
- ))} -
) }) @@ -534,10 +581,8 @@ const ConfigurationView = ({runId}: ConfigurationViewProps) => { } return ( -
-
- -
+
+
) } diff --git a/web/oss/src/components/EvalRunDetails/components/views/OverviewView.tsx b/web/oss/src/components/EvalRunDetails/components/views/OverviewView.tsx index 86a81b7ea0..774bbaea5d 100644 --- a/web/oss/src/components/EvalRunDetails/components/views/OverviewView.tsx +++ b/web/oss/src/components/EvalRunDetails/components/views/OverviewView.tsx @@ -36,9 +36,9 @@ const OverviewView = ({runId}: OverviewViewProps) => { const comparisonRunIds = useMemo(() => runIds.slice(1), [runIds]) return ( -
+
-
+
{baseRunId ? ( } return ( - +
-
-
- +
+
+ + Evaluator Scores Overview + + + Average evaluator score across evaluations +
-
- +
+
+ +
+
+ +
diff --git a/web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/BaseRunMetricsSection.tsx b/web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/BaseRunMetricsSection.tsx index 707f24753f..3b96690766 100644 --- a/web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/BaseRunMetricsSection.tsx +++ b/web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/BaseRunMetricsSection.tsx @@ -1,6 +1,6 @@ import {memo, useMemo} from "react" -import {Alert, Card, Typography} from "antd" +import {Alert} from "antd" import {isBooleanMetricStats} from "@/oss/components/EvalRunDetails/utils/metricDistributions" import type {TemporalMetricPoint} from "@/oss/components/Evaluations/atoms/runMetrics" @@ -335,18 +335,9 @@ const BaseRunMetricsSection = ({baseRunId, comparisonRunIds}: BaseRunMetricsSect } return ( - - {runDisplayName} -
- } - > -
-
{renderContent()}
-
- +
+
{renderContent()}
+
) } diff --git a/web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/MetadataSummaryTable.tsx b/web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/MetadataSummaryTable.tsx index 0ca7cdf8d5..2f405dadf6 100644 --- a/web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/MetadataSummaryTable.tsx +++ b/web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/MetadataSummaryTable.tsx @@ -10,7 +10,6 @@ import useEvaluatorReference from "@/oss/components/References/hooks/useEvaluato import type {BasicStats} from "@/oss/lib/metricUtils" import {useProjectData} from "@/oss/state/project" -import {getComparisonColor} from "../../../../atoms/compare" import {evaluationQueryRevisionAtomFamily} from "../../../../atoms/query" import { runCreatedAtAtomFamily, @@ -206,14 +205,19 @@ const StatusCell = ({runId}: MetadataCellProps) => { } const ApplicationCell = ({runId, projectURL}: MetadataCellProps) => ( -
- +
+
) const LegacyVariantCell = memo(({runId}: MetadataCellProps) => ( -
- +
+
)) @@ -235,8 +239,10 @@ const MetadataRunNameCell = memo( runId ?? "—" const accent = - accentColor ?? - (typeof runData?.accentColor === "string" ? (runData as any).accentColor : null) + accentColor === null + ? null + : (accentColor ?? + (typeof runData?.accentColor === "string" ? (runData as any).accentColor : null)) return (
@@ -248,7 +254,18 @@ const MetadataRunNameCell = memo( const LegacyTestsetsCell = memo(({runId, projectURL}: MetadataCellProps) => { const testsetAtom = useMemo(() => runTestsetIdsAtomFamily(runId), [runId]) const testsetIds = useAtomValueWithSchedule(testsetAtom, {priority: LOW_PRIORITY}) ?? [] - return + return ( +
+ +
+ ) }) const formatCurrency = (value: number | undefined | null) => { @@ -362,7 +379,14 @@ const InvocationErrorsCell = makeMetricCell("attributes.ag.metrics.errors.cumula }) const METADATA_ROWS: MetadataRowRecord[] = [ - {key: "evaluations", label: "Evaluations", Cell: MetadataRunNameCell}, + { + key: "testsets", + label: "Test set", + Cell: LegacyTestsetsCell, + shouldDisplay: ({snapshots}) => + snapshots.some(({testsetIds}) => (testsetIds?.length ?? 0) > 0), + }, + {key: "evaluation", label: "Evaluation", Cell: MetadataRunNameCell}, {key: "status", label: "Status", Cell: StatusCell}, {key: "created", label: "Created at", Cell: CreatedCell}, {key: "updated", label: "Updated at", Cell: UpdatedCell}, @@ -400,13 +424,6 @@ const METADATA_ROWS: MetadataRowRecord[] = [ ) }), }, - { - key: "testsets", - label: "Test sets", - Cell: LegacyTestsetsCell, - shouldDisplay: ({snapshots}) => - snapshots.some(({testsetIds}) => (testsetIds?.length ?? 0) > 0), - }, // {key: "scenarios", label: "Scenarios evaluated", Cell: ScenarioCountCell}, {key: "invocation_cost", label: "Cost (Total)", Cell: InvocationCostCell}, {key: "invocation_duration", label: "Duration (Total)", Cell: InvocationDurationCell}, @@ -422,7 +439,7 @@ const EvaluatorNameLabel = ({evaluatorId}: {evaluatorId: string}) => { const MetadataSummaryTable = ({runIds, projectURL}: MetadataSummaryTableProps) => { const orderedRunIds = useMemo(() => runIds.filter((id): id is string => Boolean(id)), [runIds]) - const {metricSelections, runColorMap, runDescriptors} = useRunMetricData(orderedRunIds) + const {metricSelections, runDescriptors} = useRunMetricData(orderedRunIds) const runReferenceSnapshotsAtom = useMemo( () => atom((get) => @@ -605,8 +622,6 @@ const MetadataSummaryTable = ({runIds, projectURL}: MetadataSummaryTableProps) = return rows }, [anyHasQuery, evaluatorMetricRows, rowContext]) - const isComparison = orderedRunIds.length > 1 - const columns = useMemo>(() => { const baseColumn = { title: null, @@ -625,47 +640,44 @@ const MetadataSummaryTable = ({runIds, projectURL}: MetadataSummaryTableProps) = key: runId, width: 160, onCell: (record: MetadataRowRecord) => { - if (!isComparison || record.key === "query_config") { - return {} + if (record.key === "testsets") { + return index === 0 ? {colSpan: orderedRunIds.length} : {colSpan: 0} } - const tone = getComparisonColor(index) - return tone ? {style: {backgroundColor: tone}} : {} + return {} + }, + render: (_: unknown, record: MetadataRowRecord) => { + if (record.key === "testsets" && index !== 0) { + return null + } + return ( + + ) }, - render: (_: unknown, record: MetadataRowRecord) => ( - - ), })) return [baseColumn, ...runColumns] - }, [isComparison, orderedRunIds, projectURL, runColorMap, runNameMap]) + }, [orderedRunIds, projectURL, runNameMap]) return ( -
-
- Evaluator Scores Overview - - Average evaluator score across evaluations - -
-
-
- - className="metadata-summary-table" - rowKey="key" - size="small" - pagination={false} - columns={columns} - dataSource={dataSource} - scroll={{x: "max-content"}} - showHeader={false} - /> -
+
+
+ + className="metadata-summary-table" + rowKey="key" + size="small" + pagination={false} + columns={columns} + dataSource={dataSource} + scroll={{x: "max-content"}} + showHeader={false} + bordered={true} + />
) diff --git a/web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/OverviewPlaceholders.tsx b/web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/OverviewPlaceholders.tsx index 25bdb8062d..e304041fae 100644 --- a/web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/OverviewPlaceholders.tsx +++ b/web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/OverviewPlaceholders.tsx @@ -187,6 +187,7 @@ export const OverviewLoadingPlaceholder = ({ className={clsx( "flex w-full items-center justify-center rounded-lg bg-[#F8FAFC]", "border border-dashed border-[#E2E8F0]", + "h-full", )} style={{minHeight}} > @@ -204,6 +205,7 @@ export const OverviewEmptyPlaceholder = ({ className={clsx( "flex w-full flex-col items-center justify-center gap-2 rounded-lg bg-[#F8FAFC] px-6 py-10", "border border-dashed border-[#E2E8F0] text-center", + "h-full", )} style={{minHeight}} > diff --git a/web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/RunNameTag.tsx b/web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/RunNameTag.tsx index a30be878b2..4ffa5ff94f 100644 --- a/web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/RunNameTag.tsx +++ b/web/oss/src/components/EvalRunDetails/components/views/OverviewView/components/RunNameTag.tsx @@ -59,12 +59,6 @@ const formatDateTime = (value: string | number | Date | null | undefined) => { const RunNameTag = ({runId, label, accentColor}: RunNameTagProps) => { const style = useMemo(() => buildAccentStyle(accentColor), [accentColor]) - const tooltip = useMemo(() => { - if (!label) return runId - if (label === runId) return label - return `${label} (${runId})` - }, [label, runId]) - const runQuery = useAtomValueWithSchedule( useMemo(() => evaluationRunQueryAtomFamily(runId), [runId]), {priority: LOW_PRIORITY}, @@ -97,10 +91,12 @@ const RunNameTag = ({runId, label, accentColor}: RunNameTagProps) => { const popoverContent = (
-
- - {label || runId} - +
+
+ + {label || runId} + +
Run details
{isLoading ? ( @@ -162,13 +158,7 @@ const RunNameTag = ({runId, label, accentColor}: RunNameTagProps) => { return ( - + ) } diff --git a/web/oss/src/components/EvalRunDetails/components/views/OverviewView/utils/evaluatorMetrics.ts b/web/oss/src/components/EvalRunDetails/components/views/OverviewView/utils/evaluatorMetrics.ts index 83f85fdc14..c17f436f6a 100644 --- a/web/oss/src/components/EvalRunDetails/components/views/OverviewView/utils/evaluatorMetrics.ts +++ b/web/oss/src/components/EvalRunDetails/components/views/OverviewView/utils/evaluatorMetrics.ts @@ -92,7 +92,9 @@ export const buildEvaluatorMetricEntries = ( if (!rawKey) return const canonicalKey = canonicalizeMetricKey(rawKey) if (hasSchema && !allowedCanonicalKeys.has(canonicalKey)) { - return + if (!rawKey.startsWith("attributes.ag.data.outputs.")) { + return + } } if (!unique.has(canonicalKey)) { const fallbackDefinition = fallbackByCanonicalKey.get(canonicalKey) diff --git a/web/oss/src/components/EvaluationRunsTablePOC/components/EvaluationRunsCreateButton.tsx b/web/oss/src/components/EvaluationRunsTablePOC/components/EvaluationRunsCreateButton.tsx index 3d6c012f7e..6eee82b2bb 100644 --- a/web/oss/src/components/EvaluationRunsTablePOC/components/EvaluationRunsCreateButton.tsx +++ b/web/oss/src/components/EvaluationRunsTablePOC/components/EvaluationRunsCreateButton.tsx @@ -1,7 +1,7 @@ import {useCallback, useEffect, useMemo} from "react" -import {CaretDown, Check, Plus} from "@phosphor-icons/react" -import {Button, Dropdown, Space, Tooltip, type MenuProps} from "antd" +import {PlusIcon} from "@phosphor-icons/react" +import {Button, Dropdown, Tooltip, type MenuProps} from "antd" import {useAtom, useAtomValue} from "jotai" import { @@ -37,13 +37,17 @@ const createTypeCopy: Record< }, } -const isSupportedCreateType = (value: string): value is SupportedCreateType => - SUPPORTED_CREATE_TYPES.includes(value as SupportedCreateType) +const isSupportedCreateType = (value: unknown): value is SupportedCreateType => { + return typeof value === "string" && (SUPPORTED_CREATE_TYPES as string[]).includes(value) +} + +const FALLBACK_CREATE_TYPE: SupportedCreateType = "auto" const EvaluationRunsCreateButton = () => { const {createEnabled, createTooltip, evaluationKind, defaultCreateType, scope} = useAtomValue( evaluationRunsTableHeaderStateAtom, ) + const isAllTab = evaluationKind === "all" const isAppScoped = scope === "app" const [createOpen, setCreateOpen] = useAtom(evaluationRunsCreateModalOpenAtom) const [selectedCreateType, setSelectedCreateType] = useAtom( @@ -52,40 +56,50 @@ const EvaluationRunsCreateButton = () => { const [createTypePreference, setCreateTypePreference] = useAtom( evaluationRunsCreateTypePreferenceAtom, ) - const isAllTab = evaluationKind === "all" + + const availableTypes = useMemo(() => { + if (!isAllTab) return [] + if (isAppScoped) return SUPPORTED_CREATE_TYPES.filter((t) => t !== "online") + return SUPPORTED_CREATE_TYPES + }, [isAllTab, isAppScoped]) + + const normalizeAllTabType = useCallback( + (value: unknown): SupportedCreateType => { + const candidate = isSupportedCreateType(value) ? value : FALLBACK_CREATE_TYPE + return availableTypes.includes(candidate) + ? candidate + : (availableTypes[0] ?? FALLBACK_CREATE_TYPE) + }, + [availableTypes], + ) useEffect(() => { - if (!createEnabled && createOpen) { - setCreateOpen(false) - } + if (!createEnabled && createOpen) setCreateOpen(false) }, [createEnabled, createOpen, setCreateOpen]) useEffect(() => { - if (!isAllTab && defaultCreateType && selectedCreateType !== defaultCreateType) { - setSelectedCreateType(defaultCreateType) - } + if (isAllTab) return + if (!defaultCreateType) return + if (selectedCreateType !== defaultCreateType) setSelectedCreateType(defaultCreateType) }, [defaultCreateType, isAllTab, selectedCreateType, setSelectedCreateType]) useEffect(() => { if (!isAllTab) return - const normalizedPreference = isSupportedCreateType(createTypePreference) - ? createTypePreference - : "auto" - if (!isSupportedCreateType(createTypePreference)) { - setCreateTypePreference(normalizedPreference) - } - if (selectedCreateType !== normalizedPreference) { - setSelectedCreateType(normalizedPreference) - } + + const normalized = normalizeAllTabType(createTypePreference) + + if (createTypePreference !== normalized) setCreateTypePreference(normalized) + if (selectedCreateType !== normalized) setSelectedCreateType(normalized) }, [ - createTypePreference, isAllTab, + createTypePreference, selectedCreateType, setCreateTypePreference, setSelectedCreateType, + normalizeAllTabType, ]) - const handlePrimaryClick = useCallback(() => { + const openCreateModal = useCallback(() => { if (!createEnabled) return setCreateOpen(true) }, [createEnabled, setCreateOpen]) @@ -93,74 +107,56 @@ const EvaluationRunsCreateButton = () => { const handleMenuClick = useCallback>( ({key}) => { if (!isSupportedCreateType(key)) return - setSelectedCreateType(key) - setCreateTypePreference(key) - if (!createEnabled) return - setCreateOpen(true) + + const normalized = normalizeAllTabType(key) + + setSelectedCreateType(normalized) + setCreateTypePreference(normalized) + openCreateModal() }, - [createEnabled, setCreateOpen, setCreateTypePreference, setSelectedCreateType], + [normalizeAllTabType, openCreateModal, setCreateTypePreference, setSelectedCreateType], ) - const dropdownMenuItems = useMemo(() => { + const menuItems = useMemo(() => { if (!isAllTab) return [] - // Filter out "online" (Live Evaluation) in app-scoped views - const availableTypes = isAppScoped - ? SUPPORTED_CREATE_TYPES.filter((type) => type !== "online") - : SUPPORTED_CREATE_TYPES + return availableTypes.map((type) => { const copy = createTypeCopy[type] - const isActive = selectedCreateType === type return { key: type, label: ( -
-
- {isActive ? : null} -
-
- {copy.title} - {copy.description} -
+
+ {copy.title} + {copy.description}
), } }) - }, [isAllTab, isAppScoped, selectedCreateType]) - - const buttonLabel = useMemo(() => { - if (!isAllTab) return "New Evaluation" - const shortLabel = isSupportedCreateType(selectedCreateType) - ? createTypeCopy[selectedCreateType]?.short - : null - return shortLabel ? `New ${shortLabel} Evaluation` : "New Evaluation" - }, [isAllTab, selectedCreateType]) + }, [availableTypes, isAllTab]) return (
{isAllTab ? ( - + - - diff --git a/web/oss/src/components/Evaluations/components/MetricDetailsPreviewPopover.tsx b/web/oss/src/components/Evaluations/components/MetricDetailsPreviewPopover.tsx index eb86ce193a..5c2c20b834 100644 --- a/web/oss/src/components/Evaluations/components/MetricDetailsPreviewPopover.tsx +++ b/web/oss/src/components/Evaluations/components/MetricDetailsPreviewPopover.tsx @@ -668,6 +668,7 @@ const MetricDetailsPreviewPopover = memo( prefetchedStats, evaluationType, scenarioTimestamp, + fullWidth = true, children, }: { runId?: string @@ -684,6 +685,8 @@ const MetricDetailsPreviewPopover = memo( evaluationType?: "auto" | "human" | "online" | "custom" /** Timestamp for the scenario row (used for online evaluations to get temporal stats) */ scenarioTimestamp?: string | number | null + /** Controls whether the trigger wrapper stretches to full width */ + fullWidth?: boolean children: React.ReactNode }) => { const [shouldLoad, setShouldLoad] = useState(false) @@ -716,7 +719,7 @@ const MetricDetailsPreviewPopover = memo( /> } > -
{children}
+
{children}
) }, diff --git a/web/oss/src/components/InfiniteVirtualTable/components/ColumnVisibilityHeader.tsx b/web/oss/src/components/InfiniteVirtualTable/components/ColumnVisibilityHeader.tsx index 5f893a0ec4..6bb9d61c6a 100644 --- a/web/oss/src/components/InfiniteVirtualTable/components/ColumnVisibilityHeader.tsx +++ b/web/oss/src/components/InfiniteVirtualTable/components/ColumnVisibilityHeader.tsx @@ -35,7 +35,10 @@ const ColumnVisibilityHeader = forwardRef + {children} ) diff --git a/web/oss/src/components/InfiniteVirtualTable/components/columnVisibility/TableSettingsDropdown.tsx b/web/oss/src/components/InfiniteVirtualTable/components/columnVisibility/TableSettingsDropdown.tsx index ae66ac805e..f8fb6e81f3 100644 --- a/web/oss/src/components/InfiniteVirtualTable/components/columnVisibility/TableSettingsDropdown.tsx +++ b/web/oss/src/components/InfiniteVirtualTable/components/columnVisibility/TableSettingsDropdown.tsx @@ -117,10 +117,14 @@ const TableSettingsDropdown = ({ return ( { + if (!open) { + setColumnVisibilityOpen(false) + } + }} content={renderColumnVisibilityContent(controls, handleCloseColumnVisibility)} destroyOnHidden > diff --git a/web/oss/src/components/InfiniteVirtualTable/features/InfiniteVirtualTableFeatureShell.tsx b/web/oss/src/components/InfiniteVirtualTable/features/InfiniteVirtualTableFeatureShell.tsx index 601e52e2e9..623720b0bc 100644 --- a/web/oss/src/components/InfiniteVirtualTable/features/InfiniteVirtualTableFeatureShell.tsx +++ b/web/oss/src/components/InfiniteVirtualTable/features/InfiniteVirtualTableFeatureShell.tsx @@ -1,7 +1,7 @@ import type {CSSProperties, Key, ReactNode} from "react" import {useCallback, useEffect, useMemo, useState} from "react" -import {Trash} from "@phosphor-icons/react" +import {TrashIcon} from "@phosphor-icons/react" import {Button, Grid, Tabs, Tooltip} from "antd" import type {MenuProps} from "antd" import clsx from "clsx" @@ -304,6 +304,7 @@ function InfiniteVirtualTableFeatureShellBase( beforeExport, resolveValue, resolveColumnLabel, + columnsOverride: exportColumnsOverride, } = exportOptions ?? {} const resolvedExportFilename = exportOptionsFilename ?? exportFilename ?? "table-export.csv" const exportHandler = useCallback(async () => { @@ -321,7 +322,7 @@ function InfiniteVirtualTableFeatureShellBase( }) : pagination.rows await tableExport({ - columns, + columns: exportColumnsOverride ?? columns, rows: rowsToExport, filename: resolvedExportFilename, isColumnExportable, @@ -371,7 +372,7 @@ function InfiniteVirtualTableFeatureShellBase( ) diff --git a/web/oss/src/components/InfiniteVirtualTable/hooks/useTableExport.ts b/web/oss/src/components/InfiniteVirtualTable/hooks/useTableExport.ts index cc14508d30..728d7f8940 100644 --- a/web/oss/src/components/InfiniteVirtualTable/hooks/useTableExport.ts +++ b/web/oss/src/components/InfiniteVirtualTable/hooks/useTableExport.ts @@ -177,6 +177,7 @@ export interface TableExportOptions { beforeExport?: (rows: Row[]) => void | Row[] | Promise resolveValue?: (args: TableExportResolveArgs) => unknown | Promise resolveColumnLabel?: (context: TableExportColumnContext) => string | undefined + columnsOverride?: ColumnsType } export interface TableExportParams< diff --git a/web/oss/src/components/PageLayout/PageLayout.tsx b/web/oss/src/components/PageLayout/PageLayout.tsx index dfca3c9610..b74a1b1370 100644 --- a/web/oss/src/components/PageLayout/PageLayout.tsx +++ b/web/oss/src/components/PageLayout/PageLayout.tsx @@ -5,6 +5,7 @@ import classNames from "classnames" interface PageLayoutProps { title?: ReactNode + titleLevel?: 1 | 2 | 3 | 4 | 5 headerTabs?: ReactNode headerTabsProps?: TabsProps children: ReactNode @@ -14,12 +15,14 @@ interface PageLayoutProps { const PageLayout = ({ title, + titleLevel = 5, headerTabs, headerTabsProps, children, className, headerClassName, }: PageLayoutProps) => { + const titleText = typeof title === "string" || typeof title === "number" ? String(title) : "" const headerTabsContent = headerTabsProps ? ( ) : ( @@ -35,9 +38,15 @@ const PageLayout = ({ headerClassName, )} > - - {title} - +
+ + {title} + +
{headerTabsContent ? (
{headerTabsContent} diff --git a/web/oss/src/components/References/ReferenceLabels.tsx b/web/oss/src/components/References/ReferenceLabels.tsx index 0bdfe6d58a..e3f138232d 100644 --- a/web/oss/src/components/References/ReferenceLabels.tsx +++ b/web/oss/src/components/References/ReferenceLabels.tsx @@ -14,6 +14,7 @@ import { queryReferenceAtomFamily, variantConfigAtomFamily, } from "./atoms/entityReferences" +import type {ReferenceTone} from "./referenceColors" import ReferenceTag from "./ReferenceTag" const {Text} = Typography @@ -29,12 +30,16 @@ export const TestsetTag = memo( revisionId, projectId, projectURL, + toneOverride, + showIconOverride, openExternally = false, }: { testsetId: string revisionId?: string | null projectId: string | null projectURL?: string | null + toneOverride?: ReferenceTone | null + showIconOverride?: boolean openExternally?: boolean }) => { const queryAtom = useMemo( @@ -85,7 +90,8 @@ export const TestsetTag = memo( tooltip={isDeleted ? `Testset ${testsetId} was deleted` : label} copyValue={testsetId} className="max-w-[220px] w-fit" - tone="testset" + tone={toneOverride === null ? undefined : (toneOverride ?? "testset")} + showIcon={showIconOverride ?? true} openExternally={openExternally} /> ) @@ -200,6 +206,8 @@ export const TestsetTagList = memo( projectId, projectURL, className, + toneOverride, + showIconOverride, openExternally = false, }: { ids: string[] @@ -207,6 +215,8 @@ export const TestsetTagList = memo( projectId: string | null projectURL?: string | null className?: string + toneOverride?: ReferenceTone | null + showIconOverride?: boolean openExternally?: boolean }) => { if (!ids.length) { @@ -222,6 +232,8 @@ export const TestsetTagList = memo( revisionId={revisionMap?.get(id)} projectId={projectId} projectURL={projectURL} + toneOverride={toneOverride} + showIconOverride={showIconOverride} openExternally={openExternally} /> ))} @@ -242,6 +254,8 @@ export const ApplicationReferenceLabel = memo( href: explicitHref, openExternally = false, label: customLabel, + toneOverride, + showIconOverride, }: { applicationId: string | null projectId: string | null @@ -249,6 +263,8 @@ export const ApplicationReferenceLabel = memo( href?: string | null openExternally?: boolean label?: string + toneOverride?: ReferenceTone | null + showIconOverride?: boolean }) => { const queryAtom = useMemo( () => appReferenceAtomFamily({projectId, appId: applicationId}), @@ -288,7 +304,8 @@ export const ApplicationReferenceLabel = memo( tooltip={isDeleted ? `Application ${applicationId} was deleted` : label} copyValue={applicationId ?? undefined} className="max-w-[220px] w-fit" - tone="app" + tone={toneOverride === null ? undefined : (toneOverride ?? "app")} + showIcon={showIconOverride ?? true} openExternally={openExternally} /> ) @@ -310,6 +327,8 @@ export const VariantReferenceLabel = memo( href: explicitHref, openExternally = false, label: customLabel, + toneOverride, + showIconOverride, }: { revisionId?: string | null projectId: string | null @@ -319,6 +338,8 @@ export const VariantReferenceLabel = memo( href?: string | null openExternally?: boolean label?: string + toneOverride?: ReferenceTone | null + showIconOverride?: boolean }) => { const queryAtom = useMemo( () => variantConfigAtomFamily({projectId, revisionId}), @@ -362,7 +383,8 @@ export const VariantReferenceLabel = memo( tooltip={isDeleted ? `Variant ${revisionId} was deleted` : label} copyValue={revisionId ?? undefined} className="max-w-[220px]" - tone="variant" + tone={toneOverride === null ? undefined : (toneOverride ?? "variant")} + showIcon={showIconOverride ?? true} openExternally={openExternally} /> {showVersionPill && resolvedVersion ? ( @@ -388,6 +410,8 @@ export const VariantRevisionLabel = memo( fallbackVariantName, fallbackRevision, href: explicitHref, + toneOverride, + showIconOverride, }: { variantId?: string | null revisionId?: string | null @@ -395,6 +419,8 @@ export const VariantRevisionLabel = memo( fallbackVariantName?: string | null fallbackRevision?: number | string | null href?: string | null + toneOverride?: ReferenceTone | null + showIconOverride?: boolean }) => { // Fetch variant config using revisionId to get revision number const configQueryAtom = useMemo( @@ -444,7 +470,8 @@ export const VariantRevisionLabel = memo( tooltip={isDeleted ? `Variant ${revisionId ?? variantId} was deleted` : label} copyValue={revisionId ?? variantId ?? undefined} className="max-w-[220px]" - tone="variant" + tone={toneOverride === null ? undefined : (toneOverride ?? "variant")} + showIcon={showIconOverride ?? true} /> ) }, @@ -503,6 +530,8 @@ export const EvaluatorReferenceLabel = memo( href: explicitHref, openExternally = false, label: customLabel, + toneOverride, + className, }: { evaluatorId?: string | null evaluatorSlug?: string | null @@ -510,6 +539,8 @@ export const EvaluatorReferenceLabel = memo( href?: string | null openExternally?: boolean label?: string + toneOverride?: ReferenceTone | null + className?: string }) => { const queryAtom = useMemo( () => evaluatorReferenceAtomFamily({projectId, slug: evaluatorSlug, id: evaluatorId}), @@ -523,7 +554,7 @@ export const EvaluatorReferenceLabel = memo( ) } @@ -556,8 +587,8 @@ export const EvaluatorReferenceLabel = memo( href={href ?? undefined} tooltip={isDeleted ? `Evaluator ${displayId} was deleted` : label} copyValue={displayId} - className="max-w-[220px] w-fit" - tone="evaluator" + className={clsx("max-w-[220px] w-fit", className)} + tone={toneOverride === null ? undefined : (toneOverride ?? "evaluator")} openExternally={openExternally} /> ) diff --git a/web/oss/src/components/References/ReferenceTag.tsx b/web/oss/src/components/References/ReferenceTag.tsx index 4de9929967..0025ae0992 100644 --- a/web/oss/src/components/References/ReferenceTag.tsx +++ b/web/oss/src/components/References/ReferenceTag.tsx @@ -76,7 +76,7 @@ const ReferenceTag = ({ aria-label="Open link" size={14} className="transition-transform duration-200 group-hover:translate-x-0.5 group-hover:-translate-y-0.5 cursor-pointer" - style={{color: toneColors?.text ?? "#2563eb"}} + style={{color: toneColors?.text ?? "currentColor"}} onClick={(e) => { e.preventDefault() e.stopPropagation() diff --git a/web/oss/src/components/SharedDrawers/SessionDrawer/components/SessionTree/index.tsx b/web/oss/src/components/SharedDrawers/SessionDrawer/components/SessionTree/index.tsx index 1b9a05a9b1..e44f15f30d 100644 --- a/web/oss/src/components/SharedDrawers/SessionDrawer/components/SessionTree/index.tsx +++ b/web/oss/src/components/SharedDrawers/SessionDrawer/components/SessionTree/index.tsx @@ -1,4 +1,4 @@ -import {useMemo, useState} from "react" +import {useCallback, useMemo, useState} from "react" import {MagnifyingGlass, SlidersHorizontal} from "@phosphor-icons/react" import {Button, Divider, Input, Popover} from "antd" @@ -10,6 +10,7 @@ import CustomTreeComponent from "@/oss/components/CustomUIs/CustomTreeComponent" import {filterTree} from "@/oss/components/pages/observability/assets/utils" import {TraceSpanNode} from "@/oss/services/tracing/types" +import {TreeContent} from "../../../TraceDrawer/components/TraceTree" import TraceTreeSettings from "../../../TraceDrawer/components/TraceTreeSettings" import {openTraceDrawerAtom} from "../../../TraceDrawer/store/traceDrawerStore" import {useSessionDrawer} from "../../hooks/useSessionDrawer" @@ -90,6 +91,11 @@ const SessionTree = ({selected, setSelected}: SessionTreeProps) => { const filteredTree = treeRoot + const renderTraceLabel = useCallback( + (node: TraceSpanNode) => , + [traceTreeSettings], + ) + const handleSelect = (key: string) => { setSelected(key) const element = document.getElementById(key) @@ -168,9 +174,12 @@ const SessionTree = ({selected, setSelected}: SessionTreeProps) => { node.span_id} + getChildren={(node) => node.children as TraceSpanNode[] | undefined} + renderLabel={renderTraceLabel} selectedKey={selected} - onSelect={handleSelect} + onSelect={(key) => handleSelect(key)} + defaultExpanded />
) diff --git a/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTree/index.tsx b/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTree/index.tsx index 722c9ada6a..8009787052 100644 --- a/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTree/index.tsx +++ b/web/oss/src/components/SharedDrawers/TraceDrawer/components/TraceTree/index.tsx @@ -1,4 +1,4 @@ -import {useMemo, useState} from "react" +import {useCallback, useMemo, useState} from "react" import {Coins, MagnifyingGlass, PlusCircle, SlidersHorizontal, Timer} from "@phosphor-icons/react" import {Button, Divider, Input, Popover, Space, Tooltip, Typography} from "antd" @@ -138,6 +138,11 @@ const TraceTree = ({activeTrace: active, activeTraceId, selected, setSelected}: return result || {...treeRoot, children: []} }, [searchValue, treeRoot]) + const renderTraceLabel = useCallback( + (node: TraceSpanNode) => , + [traceTreeSettings], + ) + if (!activeTrace) { return
} @@ -179,9 +184,12 @@ const TraceTree = ({activeTrace: active, activeTraceId, selected, setSelected}: node.span_id} + getChildren={(node) => node.children as TraceSpanNode[] | undefined} + renderLabel={renderTraceLabel} selectedKey={selected} - onSelect={setSelected} + onSelect={(key) => setSelected(key)} + defaultExpanded />
) diff --git a/web/oss/src/components/Sidebar/SettingsSidebar.tsx b/web/oss/src/components/Sidebar/SettingsSidebar.tsx index 8dbfbe4cb1..9927d1a3fb 100644 --- a/web/oss/src/components/Sidebar/SettingsSidebar.tsx +++ b/web/oss/src/components/Sidebar/SettingsSidebar.tsx @@ -1,7 +1,7 @@ import {FC, useMemo} from "react" -import {ApartmentOutlined, KeyOutlined} from "@ant-design/icons" -import {ArrowLeft, Sparkle, Receipt} from "@phosphor-icons/react" +import {ApartmentOutlined} from "@ant-design/icons" +import {ArrowLeftIcon, SparkleIcon, ReceiptIcon, KeyIcon} from "@phosphor-icons/react" import {Button, Divider} from "antd" import clsx from "clsx" import {useAtom} from "jotai" @@ -34,19 +34,19 @@ const SettingsSidebar: FC = ({lastPath}) => { { key: "secrets", title: "Model Hub", - icon: , + icon: , }, { key: "apiKeys", title: "API Keys", - icon: , + icon: , }, ] if (isDemo()) { list.push({ key: "billing", title: "Usage & Billing", - icon: , + icon: , }) } return list @@ -70,7 +70,7 @@ const SettingsSidebar: FC = ({lastPath}) => {