From 6fdfd4414b36e42dee7c8fa627434e607178b2af Mon Sep 17 00:00:00 2001 From: Nathan Walston Date: Tue, 23 Jun 2026 11:00:00 -0700 Subject: [PATCH 1/3] feat: surface experiment metadata as dynamic columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Union of metadata keys across loaded rows, sorted alphabetically, inserted after the Models column. Each key becomes a hideable column via the existing EditColumnsMenu. Cell rendering: null/undefined → '-', object/array → JSON.stringify, primitives → String(). Values over 50 chars truncate with a tooltip showing the full string. Signed-off-by: Nathan Walston --- .../ExperimentGroupDataView/index.tsx | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx b/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx index 31a6464fca..bc09ad73b1 100644 --- a/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx @@ -113,6 +113,12 @@ export const ExperimentGroupDataView: FC = ({ [tableData] ); + // One column per metadata key: the union across loaded rows, sorted for stable ordering. + const metadataKeys = useMemo( + () => [...new Set(tableData.flatMap((e) => Object.keys(e.metadata ?? {})))].sort(), + [tableData] + ); + const makeColumns = useCallback< ComponentProps>['makeColumns'] >( @@ -181,6 +187,29 @@ export const ExperimentGroupDataView: FC = ({ enableSorting: false, cell: ({ getValue }) => {getValue() || '-'}, }), + ...metadataKeys.map((key) => + accessor((original) => original.metadata?.[key], { + id: `metadata-${key}`, + header: snakeCaseToTitleCase(key), + enableSorting: false, + cell: ({ getValue }) => { + const raw = getValue(); + if (raw == null) return -; + const str = + typeof raw === 'object' ? JSON.stringify(raw) : String(raw); + if (str.length <= 50) return {str}; + return ( + {str}} + className={tooltipClassName} + side="bottom" + > + {str.slice(0, 50)}… + + ); + }, + }) + ), ...evaluatorNames.map((name, index) => accessor((original) => original.aggregate_scores?.[name]?.mean, { id: `score-${index}`, @@ -239,7 +268,7 @@ export const ExperimentGroupDataView: FC = ({ }, }), ], - [evaluatorNames] + [evaluatorNames, metadataKeys] ); if (groupError) { From b727694654dd859fabc9b6127d955e1b5bff2680 Mon Sep 17 00:00:00 2001 From: Nathan Walston Date: Tue, 23 Jun 2026 11:01:15 -0700 Subject: [PATCH 2/3] fix: normalize metadata keys to lowercase to collapse case variants 'status' and 'Status' now produce a single column instead of two identically-labelled headers. The accessor finds the first key that lowercases to a match, so the value is still retrieved correctly regardless of how the producer cased the key. Signed-off-by: Nathan Walston --- .../dataViews/ExperimentGroupDataView/index.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx b/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx index bc09ad73b1..50b220edda 100644 --- a/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx @@ -113,9 +113,15 @@ export const ExperimentGroupDataView: FC = ({ [tableData] ); - // One column per metadata key: the union across loaded rows, sorted for stable ordering. + // One column per metadata key: keys are lowercased so case variants (e.g. "status" + // and "Status") collapse into one column rather than producing duplicate headers. const metadataKeys = useMemo( - () => [...new Set(tableData.flatMap((e) => Object.keys(e.metadata ?? {})))].sort(), + () => + [ + ...new Set( + tableData.flatMap((e) => Object.keys(e.metadata ?? {}).map((k) => k.toLowerCase())) + ), + ].sort(), [tableData] ); @@ -188,7 +194,12 @@ export const ExperimentGroupDataView: FC = ({ cell: ({ getValue }) => {getValue() || '-'}, }), ...metadataKeys.map((key) => - accessor((original) => original.metadata?.[key], { + accessor((original) => { + const meta = original.metadata ?? {}; + // Match the first key that lowercases to this column's key. + const match = Object.keys(meta).find((k) => k.toLowerCase() === key); + return match ? meta[match] : undefined; + }, { id: `metadata-${key}`, header: snakeCaseToTitleCase(key), enableSorting: false, From fb61b4ddd72d20d17b0820920097dae44818a012 Mon Sep 17 00:00:00 2001 From: Nathan Walston Date: Tue, 23 Jun 2026 11:21:46 -0700 Subject: [PATCH 3/3] style: fix prettier formatting in ExperimentGroupDataView Signed-off-by: Nathan Walston --- .../ExperimentGroupDataView/index.tsx | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx b/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx index 50b220edda..bd74d88da9 100644 --- a/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/ExperimentGroupDataView/index.tsx @@ -194,32 +194,34 @@ export const ExperimentGroupDataView: FC = ({ cell: ({ getValue }) => {getValue() || '-'}, }), ...metadataKeys.map((key) => - accessor((original) => { - const meta = original.metadata ?? {}; - // Match the first key that lowercases to this column's key. - const match = Object.keys(meta).find((k) => k.toLowerCase() === key); - return match ? meta[match] : undefined; - }, { - id: `metadata-${key}`, - header: snakeCaseToTitleCase(key), - enableSorting: false, - cell: ({ getValue }) => { - const raw = getValue(); - if (raw == null) return -; - const str = - typeof raw === 'object' ? JSON.stringify(raw) : String(raw); - if (str.length <= 50) return {str}; - return ( - {str}} - className={tooltipClassName} - side="bottom" - > - {str.slice(0, 50)}… - - ); + accessor( + (original) => { + const meta = original.metadata ?? {}; + // Match the first key that lowercases to this column's key. + const match = Object.keys(meta).find((k) => k.toLowerCase() === key); + return match ? meta[match] : undefined; }, - }) + { + id: `metadata-${key}`, + header: snakeCaseToTitleCase(key), + enableSorting: false, + cell: ({ getValue }) => { + const raw = getValue(); + if (raw == null) return -; + const str = typeof raw === 'object' ? JSON.stringify(raw) : String(raw); + if (str.length <= 50) return {str}; + return ( + {str}} + className={tooltipClassName} + side="bottom" + > + {str.slice(0, 50)}… + + ); + }, + } + ) ), ...evaluatorNames.map((name, index) => accessor((original) => original.aggregate_scores?.[name]?.mean, {