From ff9f74f6894a01b927fef38bf45357d91b9c0703 Mon Sep 17 00:00:00 2001 From: Sean Teramae Date: Wed, 22 Jul 2026 15:18:08 -0700 Subject: [PATCH 1/3] feat(studio): Split file into training set in DD details Signed-off-by: Sean Teramae --- .../src/api/datasets/useSplitDatasetFile.ts | 9 +- .../DataDesignerJobActionsMenu/index.tsx | 11 ++ .../components/FileRowEditor/FileHeader.tsx | 20 +++- .../src/components/FileRowEditor/index.tsx | 20 +++- .../FileSplitsSliders/index.tsx | 2 +- .../CreateFileSplitsModal/index.tsx | 76 +++++++++++-- .../components/LeftTruncatedText/index.tsx | 31 +++++ .../JobDatasetEditorSection.tsx | 107 +++++++++--------- .../DataDesignerJobDetailsRoute/index.tsx | 31 ++++- 9 files changed, 224 insertions(+), 83 deletions(-) create mode 100644 web/packages/studio/src/components/LeftTruncatedText/index.tsx diff --git a/web/packages/studio/src/api/datasets/useSplitDatasetFile.ts b/web/packages/studio/src/api/datasets/useSplitDatasetFile.ts index 00c12b2582..1d4d074459 100644 --- a/web/packages/studio/src/api/datasets/useSplitDatasetFile.ts +++ b/web/packages/studio/src/api/datasets/useSplitDatasetFile.ts @@ -55,11 +55,12 @@ export const useSplitDatasetFile = ({ onError, onSuccess }: Props) => { ? splitRandomDistribution(rows, splits, seed) : splitSequentialDistribution(rows, splits, { key: sortKey }); - // Upload files to fileset - const filename = filepath.split('/').pop() ?? filepath; + const isJson = filepath.endsWith('json'); + const sourceName = filepath.split('/').pop() ?? filepath; + const baseName = sourceName.replace(/\.[^./]+$/, ''); + const outputName = `${baseName}.${isJson ? 'json' : 'jsonl'}`; const toUpload = splits .map((_, index) => { - const isJson = filepath.endsWith('json'); let content = splitList[index] .map((row) => JSON.stringify(row)) .join(isJson ? ',\n' : '\n'); @@ -70,7 +71,7 @@ export const useSplitDatasetFile = ({ onError, onSuccess }: Props) => { return undefined; } return { - path: `${fileSuffix[index]}/${filename}`, + path: `${fileSuffix[index]}/${outputName}`, content, }; }) diff --git a/web/packages/studio/src/components/DataDesignerJobActionsMenu/index.tsx b/web/packages/studio/src/components/DataDesignerJobActionsMenu/index.tsx index 93ea33bef2..8179e17adf 100644 --- a/web/packages/studio/src/components/DataDesignerJobActionsMenu/index.tsx +++ b/web/packages/studio/src/components/DataDesignerJobActionsMenu/index.tsx @@ -23,6 +23,8 @@ interface DataDesignerJobActionsMenuProps { job: DataDesignerJob; /** Include a "View details" entry. Used in the table row, omitted on the details page. */ includeViewDetails?: boolean; + /** When provided, adds a "View config" entry that invokes this callback. */ + onViewConfig?: () => void; /** Called after the job is successfully deleted, e.g. to navigate away from the details page. */ onDeleted?: () => void; /** Surface a cancel error (or `undefined` to clear) so the caller can render it. */ @@ -37,6 +39,7 @@ interface DataDesignerJobActionsMenuProps { export const DataDesignerJobActionsMenu: FC = ({ job, includeViewDetails = false, + onViewConfig, onDeleted, onCancelError, }) => { @@ -92,6 +95,14 @@ export const DataDesignerJobActionsMenu: FC = ( }, ] : []), + ...(onViewConfig + ? [ + { + label: 'View config', + onSelect: onViewConfig, + }, + ] + : []), { label: 'Clone', onSelect: handleClone, diff --git a/web/packages/studio/src/components/FileRowEditor/FileHeader.tsx b/web/packages/studio/src/components/FileRowEditor/FileHeader.tsx index dd3d1ab3fd..50fe4d3e9c 100644 --- a/web/packages/studio/src/components/FileRowEditor/FileHeader.tsx +++ b/web/packages/studio/src/components/FileRowEditor/FileHeader.tsx @@ -5,10 +5,15 @@ import { Button, Flex, Spinner, Stack, Tag, Text } from '@nvidia/foundations-rea import { FILE_FORMAT_TAG_COLOR } from '@studio/components/FileRowEditor/constants'; import type { DataFileFormat } from '@studio/components/FileRowEditor/parse'; import { Download, FileSpreadsheet, FolderOpen, Plus, Save } from 'lucide-react'; -import { type ChangeEvent, type FC, type RefObject } from 'react'; +import { type ChangeEvent, type FC, type ReactNode, type RefObject } from 'react'; export interface FileHeaderProps { fileName: string; + /** + * Replaces the static file name with a custom node (e.g. a file picker) so the header + * doubles as the file selector. The format tag and stats still reflect {@link fileName}. + */ + slotFileName?: ReactNode; fileFormat: DataFileFormat; rowCount: number; columnCount: number; @@ -42,6 +47,7 @@ export interface FileHeaderProps { /** Header summary + toolbar for the {@link FileRowEditor}: file identity, stats, actions. */ export const FileHeader: FC = ({ fileName, + slotFileName, fileFormat, rowCount, columnCount, @@ -62,13 +68,15 @@ export const FileHeader: FC = ({ }) => ( - + - - - {fileName} - + + {slotFileName ?? ( + + {fileName} + + )} {fileFormat === 'unknown' ? 'FILE' : fileFormat.toUpperCase()} diff --git a/web/packages/studio/src/components/FileRowEditor/index.tsx b/web/packages/studio/src/components/FileRowEditor/index.tsx index c74e86e849..0cfbe7f302 100644 --- a/web/packages/studio/src/components/FileRowEditor/index.tsx +++ b/web/packages/studio/src/components/FileRowEditor/index.tsx @@ -32,11 +32,24 @@ import { type DataFileRow, } from '@studio/components/FileRowEditor/types'; import { Trash } from 'lucide-react'; -import { type ChangeEvent, type FC, useCallback, useMemo, useRef, useState } from 'react'; +import { + type ChangeEvent, + type FC, + type ReactNode, + useCallback, + useMemo, + useRef, + useState, +} from 'react'; export interface FileRowEditorProps { /** File name shown in the header. Its extension drives the format chip. */ fileName?: string; + /** + * Replaces the header's static file name with a custom node (e.g. a file picker), letting + * the header double as the file selector. The format chip and stats still track `fileName`. + */ + slotFileName?: ReactNode; /** File size label shown in the header summary. */ fileSizeLabel?: string; /** @@ -81,6 +94,7 @@ export interface FileRowEditorProps { */ export const FileRowEditor: FC = ({ fileName: fileNameProp = 'qa-sft-dataset-v1.parquet', + slotFileName, fileSizeLabel: fileSizeLabelProp = '4.2 MB', columns: columnsProp, initialRows = [], @@ -227,8 +241,6 @@ export const FileRowEditor: FC = ({ const handleOpenFileClick = () => fileInputRef.current?.click(); const handleDownload = () => { - // Parquet/unknown files have no in-browser binary form, so export the current rows as - // JSON; text formats round-trip to their own extension. const downloadFormat: DataFileFormat = TEXT_PARSEABLE_FORMATS.includes(fileFormat) ? fileFormat : 'json'; @@ -247,7 +259,6 @@ export const FileRowEditor: FC = ({ const handleFileSelected = async (event: ChangeEvent) => { const file = event.target.files?.[0]; - // Reset the input so selecting the same file again re-triggers change. event.target.value = ''; if (!file) { return; @@ -282,6 +293,7 @@ export const FileRowEditor: FC = ({ { render={({ field }) => ( { className: 'max-w-[64px]', onFocus: () => setIsFocused(fieldName), onBlur: () => setIsFocused(null), - // Prevent the user from submitting form on enter onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); diff --git a/web/packages/studio/src/components/FilesTable/CreateFileSplitsModal/index.tsx b/web/packages/studio/src/components/FilesTable/CreateFileSplitsModal/index.tsx index d6daf6b108..9057c8604a 100644 --- a/web/packages/studio/src/components/FilesTable/CreateFileSplitsModal/index.tsx +++ b/web/packages/studio/src/components/FilesTable/CreateFileSplitsModal/index.tsx @@ -20,6 +20,7 @@ import { CreateFileSplitsFormFields, createFileSplitsSchema, } from '@studio/components/FilesTable/CreateFileSplitsModal/types'; +import { LeftTruncatedText } from '@studio/components/LeftTruncatedText'; import { ValueWithLabel } from '@studio/components/ValueWithLabel'; import { useSelectedDatasetId } from '@studio/hooks/useSelectedDatasetId'; import { tooltipClassName } from '@studio/styles/common'; @@ -31,23 +32,46 @@ import { ComponentProps, FC, useMemo } from 'react'; import { FormProvider, useForm, useWatch } from 'react-hook-form'; interface Props extends Pick, 'open' | 'onClose'> { + /** Fixed source file. When set, the file is shown read-only. */ filepath?: string; + /** + * Dataset/fileset reference (`workspace/name`) whose files are being split. + * Falls back to the route's selected dataset when omitted. + */ + datasetId?: string; + /** + * Selectable source files. When provided (and no fixed `filepath`), the modal + * renders a picker so the user chooses which file to split. + */ + fileOptions?: string[]; } /** * This modal is used to handle splitting a larger file * into smaller files for training/validation/evaluation. */ -export const CreateFileSplitsModal: FC = ({ open, onClose, filepath }) => { +export const CreateFileSplitsModal: FC = ({ + open, + onClose, + filepath, + datasetId, + fileOptions, +}) => { const toast = useToast(); - const datasetId = useSelectedDatasetId(); - const datasetNameSplit = getPartsFromReference(datasetId); + const resolvedDatasetId = useSelectedDatasetId({ datasetId }); + const datasetNameSplit = getPartsFromReference(resolvedDatasetId); + + const defaultFilepath = + filepath ?? + fileOptions?.find((path) => /\.(json|jsonl|parquet)$/i.test(path)) ?? + fileOptions?.[0] ?? + ''; const formMethods = useForm({ mode: 'onChange', resolver: zodResolver(createFileSplitsSchema), defaultValues: { - filepath, + filepath: defaultFilepath, splitDescriptor: SELECT_SPLIT_OPTIONS[0], training: 80, testing: 20, @@ -64,9 +88,14 @@ export const CreateFileSplitsModal: FC = ({ open, onClose, filepath }) => onClose(); }; - const { data: fileContent, isLoading: isLoadingFileContent } = useDatasetFileContent({ + const { + data: fileContent, + isLoading: isLoadingFileContent, + error: fileContentError, + } = useDatasetFileContent({ ...datasetNameSplit, path: filepathForm, + fullContent: true, }); const { total_rows } = useMemo(() => { const contentSchema = getContentSchema(fileContent, { @@ -127,7 +156,7 @@ export const CreateFileSplitsModal: FC = ({ open, onClose, filepath }) => )} onClose={resetAndClose} disabled={isPending} - submitDisabled={isLoadingFileContent} + submitDisabled={isLoadingFileContent || Boolean(fileContentError)} loading={isPending} > @@ -135,13 +164,36 @@ export const CreateFileSplitsModal: FC = ({ open, onClose, filepath }) => To fine-tune and evaluate a model, you need to split a dataset into three subsets: training data, validation data, and test data. - + {filepath || !fileOptions?.length ? ( + + ) : ( + ({ + value: path, + children: {path}, + }))} + renderValue={(value) => {value}} + formFieldProps={{ + attributes: { Popover: { className: tooltipClassName } }, + slotLabel: , + }} + useControllerProps={{ control, name: 'filepath' }} + /> + )} - {isLoadingFileContent ? ( + {fileContentError ? ( + + {fileContentError.message} + + ) : isLoadingFileContent ? ( diff --git a/web/packages/studio/src/components/LeftTruncatedText/index.tsx b/web/packages/studio/src/components/LeftTruncatedText/index.tsx new file mode 100644 index 0000000000..ff53c8ebe1 --- /dev/null +++ b/web/packages/studio/src/components/LeftTruncatedText/index.tsx @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Text } from '@nvidia/foundations-react-core'; +import cn from 'classnames'; +import { ComponentProps, FC } from 'react'; + +interface LeftTruncatedTextProps extends ComponentProps { + /** The string to render with left-side truncation (ellipsis at the start). */ + children: string; +} + +/** + * Renders text that truncates from the left, keeping the end of the string + * (e.g. the file name at the tail of a long path) visible. The `` wrapper + * preserves the string's natural character order despite the RTL flip that + * moves the ellipsis to the start. + * + * Defaults to `kind="inherit"` so it adopts the surrounding text style; pass + * `kind` (or any other Text prop) to override. + */ +export const LeftTruncatedText: FC = ({ + children, + className, + kind = 'inherit', + ...props +}) => ( + + {children} + +); diff --git a/web/packages/studio/src/routes/DataDesignerJobDetailsRoute/JobDatasetEditorSection.tsx b/web/packages/studio/src/routes/DataDesignerJobDetailsRoute/JobDatasetEditorSection.tsx index 4b4b20dc84..4246f3a780 100644 --- a/web/packages/studio/src/routes/DataDesignerJobDetailsRoute/JobDatasetEditorSection.tsx +++ b/web/packages/studio/src/routes/DataDesignerJobDetailsRoute/JobDatasetEditorSection.tsx @@ -12,7 +12,6 @@ import { SelectTrigger, Spinner, Stack, - Text, } from '@nvidia/foundations-react-core'; import { EDITOR_MAX_BYTES, @@ -73,7 +72,6 @@ export const JobDatasetEditorSection: FC = () => { const { filesetWorkspace, filesetName, files, isResultsLoading, isFilesLoading } = useDataDesignerArtifactsFileset(); - // Data files only — exclude the builder config and any non-row formats. const dataFiles = useMemo( () => files.filter( @@ -190,40 +188,46 @@ export const JobDatasetEditorSection: FC = () => { const isResolving = isResultsLoading || isFilesLoading; + // Rendered inside the editor's header (as `slotFileName`) so the file picker and the file + // identity share one row; falls back to a slim bar above the centered states below. const fileSelector = dataFiles.length > 1 ? ( - - - File - - setSelectedPath(value)} - > - - typeof value === 'string' ? ( - - {getFileNameFromPath(value)} - - ) : null - } - /> - - - {dataFiles.map((file) => ( - - {getFileNameFromPath(file.path)} - - ))} - - - - + setSelectedPath(value)} + > + + typeof value === 'string' ? ( + + {getFileNameFromPath(value)} + + ) : null + } + /> + + + {dataFiles.map((file) => ( + + {getFileNameFromPath(file.path)} + + ))} + + + ) : null; - const renderBody = () => { + const showEditor = + dataFiles.length > 0 && + !isTooLargeToEdit && + !isContentLoading && + parsed != null && + !isContentError && + !parsed.error; + + const renderCenteredState = () => { if (isResolving && dataFiles.length === 0) { return centered(); } @@ -259,31 +263,28 @@ export const JobDatasetEditorSection: FC = () => { ); } - if (parsed.error) { - return centered(); - } - - return ( - - ); + return centered(); }; return ( - {fileSelector ? ( - - {fileSelector} - - ) : null} - {renderBody()} + {showEditor ? ( + + ) : ( + <> + {fileSelector ? {fileSelector} : null} + {renderCenteredState()} + + )} ); }; diff --git a/web/packages/studio/src/routes/DataDesignerJobDetailsRoute/index.tsx b/web/packages/studio/src/routes/DataDesignerJobDetailsRoute/index.tsx index acfbaf096c..fc4538bb46 100644 --- a/web/packages/studio/src/routes/DataDesignerJobDetailsRoute/index.tsx +++ b/web/packages/studio/src/routes/DataDesignerJobDetailsRoute/index.tsx @@ -16,16 +16,18 @@ import { } from '@nvidia/foundations-react-core'; import { AccessibleTitle } from '@studio/components/AccessibleTitle'; import { DataDesignerJobActionsMenu } from '@studio/components/DataDesignerJobActionsMenu'; +import { CreateFileSplitsModal } from '@studio/components/FilesTable/CreateFileSplitsModal'; import { Loading } from '@studio/components/Layouts/Loading'; import { useBreadcrumbs } from '@studio/providers/breadcrumbs/useBreadcrumbs'; import { DataDesignerConfigPanel } from '@studio/routes/DataDesignerJobDetailsRoute/DataDesignerConfigPanel'; import { DatasetProfilerSection } from '@studio/routes/DataDesignerJobDetailsRoute/DatasetProfilerSection'; import { JobDatasetEditorSection } from '@studio/routes/DataDesignerJobDetailsRoute/JobDatasetEditorSection'; import { JobOutputFilesetSection } from '@studio/routes/DataDesignerJobDetailsRoute/JobOutputFilesetSection'; +import { useDataDesignerArtifactsFileset } from '@studio/routes/DataDesignerJobDetailsRoute/useDataDesignerArtifactsFileset'; import { useDataDesignerJobFromRoute } from '@studio/routes/DataDesignerJobDetailsRoute/useDataDesignerJobFromRoute'; import { getDataDesignerJobListRoute } from '@studio/routes/utils'; import { formatDateTime } from '@studio/util/date'; -import { ArrowLeft, FileJson } from 'lucide-react'; +import { ArrowLeft, Split } from 'lucide-react'; import { useState, type FC } from 'react'; import { Link, useNavigate } from 'react-router-dom'; @@ -41,8 +43,15 @@ export const DataDesignerJobDetailsRoute: FC = () => { const navigate = useNavigate(); const [isConfigPanelOpen, setIsConfigPanelOpen] = useState(false); + const [isSplitModalOpen, setIsSplitModalOpen] = useState(false); const [cancelError, setCancelError] = useState(undefined); + const { filesetWorkspace, filesetName, files } = useDataDesignerArtifactsFileset(); + const splitDatasetId = + filesetWorkspace && filesetName ? `${filesetWorkspace}/${filesetName}` : undefined; + const splitFileOptions = files.map((file) => file.path); + const canSplit = Boolean(splitDatasetId) && splitFileOptions.length > 0; + useBreadcrumbs({ items: [ { @@ -92,11 +101,18 @@ export const DataDesignerJobDetailsRoute: FC = () => { {job.status ? : null} - setIsConfigPanelOpen(true)} onDeleted={() => navigate(getDataDesignerJobListRoute(workspace))} onCancelError={setCancelError} /> @@ -151,6 +167,15 @@ export const DataDesignerJobDetailsRoute: FC = () => { open={isConfigPanelOpen} onClose={() => setIsConfigPanelOpen(false)} /> + + {isSplitModalOpen && ( + setIsSplitModalOpen(false)} + datasetId={splitDatasetId} + fileOptions={splitFileOptions} + /> + )} ); }; From 3ae898f49039c2673f80a80ed001d28a9485f10c Mon Sep 17 00:00:00 2001 From: Sean Teramae Date: Wed, 22 Jul 2026 15:18:56 -0700 Subject: [PATCH 2/3] fix lint Signed-off-by: Sean Teramae --- .../src/components/FilesTable/CreateFileSplitsModal/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/packages/studio/src/components/FilesTable/CreateFileSplitsModal/index.tsx b/web/packages/studio/src/components/FilesTable/CreateFileSplitsModal/index.tsx index 9057c8604a..dffb29ac59 100644 --- a/web/packages/studio/src/components/FilesTable/CreateFileSplitsModal/index.tsx +++ b/web/packages/studio/src/components/FilesTable/CreateFileSplitsModal/index.tsx @@ -249,7 +249,7 @@ export const CreateFileSplitsModal: FC = ({ Date: Fri, 24 Jul 2026 09:16:53 -0700 Subject: [PATCH 3/3] fix PR comments Signed-off-by: Sean Teramae --- .../studio/src/api/datasets/useSplitDatasetFile.ts | 5 +---- .../studio/src/components/FileRowEditor/FileHeader.tsx | 4 ---- .../FilesTable/CreateFileSplitsModal/index.tsx | 9 --------- .../JobDatasetEditorSection.tsx | 2 -- .../src/routes/DataDesignerJobDetailsRoute/index.tsx | 4 +++- 5 files changed, 4 insertions(+), 20 deletions(-) diff --git a/web/packages/studio/src/api/datasets/useSplitDatasetFile.ts b/web/packages/studio/src/api/datasets/useSplitDatasetFile.ts index 1d4d074459..a86cba2fef 100644 --- a/web/packages/studio/src/api/datasets/useSplitDatasetFile.ts +++ b/web/packages/studio/src/api/datasets/useSplitDatasetFile.ts @@ -40,7 +40,6 @@ export const useSplitDatasetFile = ({ onError, onSuccess }: Props) => { seed, sortKey, }: FileSplitsProps) => { - // Parse JSON objects const { rows, failures } = parseFileContent({ content: fileContent, fileType: getFileExtension(filepath) ?? '', @@ -49,13 +48,12 @@ export const useSplitDatasetFile = ({ onError, onSuccess }: Props) => { toast.error(`${failures.length} Line(s) had parsing errors.`); } - // Split rows into randomly distributed lists const splitList = distributionType === 'random' ? splitRandomDistribution(rows, splits, seed) : splitSequentialDistribution(rows, splits, { key: sortKey }); - const isJson = filepath.endsWith('json'); + const isJson = filepath.toLowerCase().endsWith('json'); const sourceName = filepath.split('/').pop() ?? filepath; const baseName = sourceName.replace(/\.[^./]+$/, ''); const outputName = `${baseName}.${isJson ? 'json' : 'jsonl'}`; @@ -77,7 +75,6 @@ export const useSplitDatasetFile = ({ onError, onSuccess }: Props) => { }) .filter(isDefined); - // Upload each file using v2 API const results = await Promise.all( toUpload.map(async (details) => { const blob = new Blob([details.content], { type: 'application/json' }); diff --git a/web/packages/studio/src/components/FileRowEditor/FileHeader.tsx b/web/packages/studio/src/components/FileRowEditor/FileHeader.tsx index 50fe4d3e9c..a338cd33a5 100644 --- a/web/packages/studio/src/components/FileRowEditor/FileHeader.tsx +++ b/web/packages/studio/src/components/FileRowEditor/FileHeader.tsx @@ -9,10 +9,6 @@ import { type ChangeEvent, type FC, type ReactNode, type RefObject } from 'react export interface FileHeaderProps { fileName: string; - /** - * Replaces the static file name with a custom node (e.g. a file picker) so the header - * doubles as the file selector. The format tag and stats still reflect {@link fileName}. - */ slotFileName?: ReactNode; fileFormat: DataFileFormat; rowCount: number; diff --git a/web/packages/studio/src/components/FilesTable/CreateFileSplitsModal/index.tsx b/web/packages/studio/src/components/FilesTable/CreateFileSplitsModal/index.tsx index dffb29ac59..82df2da4ac 100644 --- a/web/packages/studio/src/components/FilesTable/CreateFileSplitsModal/index.tsx +++ b/web/packages/studio/src/components/FilesTable/CreateFileSplitsModal/index.tsx @@ -32,17 +32,8 @@ import { ComponentProps, FC, useMemo } from 'react'; import { FormProvider, useForm, useWatch } from 'react-hook-form'; interface Props extends Pick, 'open' | 'onClose'> { - /** Fixed source file. When set, the file is shown read-only. */ filepath?: string; - /** - * Dataset/fileset reference (`workspace/name`) whose files are being split. - * Falls back to the route's selected dataset when omitted. - */ datasetId?: string; - /** - * Selectable source files. When provided (and no fixed `filepath`), the modal - * renders a picker so the user chooses which file to split. - */ fileOptions?: string[]; } diff --git a/web/packages/studio/src/routes/DataDesignerJobDetailsRoute/JobDatasetEditorSection.tsx b/web/packages/studio/src/routes/DataDesignerJobDetailsRoute/JobDatasetEditorSection.tsx index 4246f3a780..c0560306e8 100644 --- a/web/packages/studio/src/routes/DataDesignerJobDetailsRoute/JobDatasetEditorSection.tsx +++ b/web/packages/studio/src/routes/DataDesignerJobDetailsRoute/JobDatasetEditorSection.tsx @@ -188,8 +188,6 @@ export const JobDatasetEditorSection: FC = () => { const isResolving = isResultsLoading || isFilesLoading; - // Rendered inside the editor's header (as `slotFileName`) so the file picker and the file - // identity share one row; falls back to a slim bar above the centered states below. const fileSelector = dataFiles.length > 1 ? ( { const { filesetWorkspace, filesetName, files } = useDataDesignerArtifactsFileset(); const splitDatasetId = filesetWorkspace && filesetName ? `${filesetWorkspace}/${filesetName}` : undefined; - const splitFileOptions = files.map((file) => file.path); + const splitFileOptions = files + .map((file) => file.path) + .filter((path) => /\.(json|jsonl|parquet)$/i.test(path)); const canSplit = Boolean(splitDatasetId) && splitFileOptions.length > 0; useBreadcrumbs({