Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions web/packages/studio/src/api/datasets/useSplitDatasetFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) ?? '',
Expand All @@ -49,17 +48,17 @@ 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 });

// Upload files to fileset
const filename = filepath.split('/').pop() ?? filepath;
const isJson = filepath.toLowerCase().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');
Expand All @@ -70,13 +69,12 @@ export const useSplitDatasetFile = ({ onError, onSuccess }: Props) => {
return undefined;
}
return {
path: `${fileSuffix[index]}/${filename}`,
path: `${fileSuffix[index]}/${outputName}`,
content,
};
})
.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' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -37,6 +39,7 @@ interface DataDesignerJobActionsMenuProps {
export const DataDesignerJobActionsMenu: FC<DataDesignerJobActionsMenuProps> = ({
job,
includeViewDetails = false,
onViewConfig,
onDeleted,
onCancelError,
}) => {
Expand Down Expand Up @@ -92,6 +95,14 @@ export const DataDesignerJobActionsMenu: FC<DataDesignerJobActionsMenuProps> = (
},
]
: []),
...(onViewConfig
? [
{
label: 'View config',
onSelect: onViewConfig,
},
]
: []),
{
label: 'Clone',
onSelect: handleClone,
Expand Down
16 changes: 10 additions & 6 deletions web/packages/studio/src/components/FileRowEditor/FileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ 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;
slotFileName?: ReactNode;
fileFormat: DataFileFormat;
rowCount: number;
columnCount: number;
Expand Down Expand Up @@ -42,6 +43,7 @@ export interface FileHeaderProps {
/** Header summary + toolbar for the {@link FileRowEditor}: file identity, stats, actions. */
export const FileHeader: FC<FileHeaderProps> = ({
fileName,
slotFileName,
fileFormat,
rowCount,
columnCount,
Expand All @@ -62,13 +64,15 @@ export const FileHeader: FC<FileHeaderProps> = ({
}) => (
<Flex align="center" gap="density-md" className="w-full shrink-0">
<Flex align="center" justify="center" className="size-10 shrink-0 rounded-md bg-surface-sunken">
<FileSpreadsheet size={20} className="text-secondary" />
<FileSpreadsheet className="size-20 text-secondary" />
Comment thread
steramae-nvidia marked this conversation as resolved.
</Flex>
<Stack gap="density-xs" className="min-w-0 flex-1">
<Flex align="center" gap="density-sm">
<Text kind="title/xs" className="truncate">
{fileName}
</Text>
<Flex align="center" gap="density-sm" className="min-w-0">
{slotFileName ?? (
<Text kind="title/xs" className="truncate">
{fileName}
</Text>
)}
<Tag kind="solid" color={FILE_FORMAT_TAG_COLOR[fileFormat]} readOnly>
{fileFormat === 'unknown' ? 'FILE' : fileFormat.toUpperCase()}
</Tag>
Expand Down
20 changes: 16 additions & 4 deletions web/packages/studio/src/components/FileRowEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down Expand Up @@ -81,6 +94,7 @@ export interface FileRowEditorProps {
*/
export const FileRowEditor: FC<FileRowEditorProps> = ({
fileName: fileNameProp = 'qa-sft-dataset-v1.parquet',
slotFileName,
fileSizeLabel: fileSizeLabelProp = '4.2 MB',
columns: columnsProp,
initialRows = [],
Expand Down Expand Up @@ -227,8 +241,6 @@ export const FileRowEditor: FC<FileRowEditorProps> = ({
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';
Expand All @@ -247,7 +259,6 @@ export const FileRowEditor: FC<FileRowEditorProps> = ({

const handleFileSelected = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
// Reset the input so selecting the same file again re-triggers change.
event.target.value = '';
if (!file) {
return;
Expand Down Expand Up @@ -282,6 +293,7 @@ export const FileRowEditor: FC<FileRowEditorProps> = ({
<Stack gap="density-xl" className={`h-full w-full min-w-0 ${className ?? ''}`}>
<FileHeader
fileName={fileName}
slotFileName={slotFileName}
fileFormat={fileFormat}
rowCount={rows.length}
columnCount={columns.length}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export const FileSplitsSliders: FC = () => {
render={({ field }) => (
<Flex gap="density-sm" align="center" className="*:flex-1">
<SliderWithTextInput
size="compact"
field={field}
defaultValue={field.value}
min={0}
Expand Down Expand Up @@ -140,7 +141,6 @@ export const FileSplitsSliders: FC = () => {
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -32,22 +33,36 @@ import { FormProvider, useForm, useWatch } from 'react-hook-form';

interface Props extends Pick<ComponentProps<typeof FormModal>, 'open' | 'onClose'> {
filepath?: string;
datasetId?: string;
fileOptions?: string[];
}

/**
* This modal is used to handle splitting a larger file
* into smaller files for training/validation/evaluation.
*/
export const CreateFileSplitsModal: FC<Props> = ({ open, onClose, filepath }) => {
export const CreateFileSplitsModal: FC<Props> = ({
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 =
Comment thread
steramae-nvidia marked this conversation as resolved.
filepath ??
fileOptions?.find((path) => /\.(json|jsonl|parquet)$/i.test(path)) ??
fileOptions?.[0] ??
'';

const formMethods = useForm<CreateFileSplitsFormFields>({
mode: 'onChange',
resolver: zodResolver(createFileSplitsSchema),
defaultValues: {
filepath,
filepath: defaultFilepath,
splitDescriptor: SELECT_SPLIT_OPTIONS[0],
training: 80,
testing: 20,
Expand All @@ -64,9 +79,14 @@ export const CreateFileSplitsModal: FC<Props> = ({ 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, {
Expand Down Expand Up @@ -127,21 +147,44 @@ export const CreateFileSplitsModal: FC<Props> = ({ open, onClose, filepath }) =>
)}
onClose={resetAndClose}
disabled={isPending}
submitDisabled={isLoadingFileContent}
submitDisabled={isLoadingFileContent || Boolean(fileContentError)}
loading={isPending}
>
<Stack gap="density-xl">
<Text className="leading-normal">
To fine-tune and evaluate a model, you need to split a dataset into three subsets:
training data, validation data, and test data.
</Text>
<ValueWithLabel
labelProps={{ className: 'font-bold' }}
label="Source File"
value={filepath}
/>
{filepath || !fileOptions?.length ? (
<ValueWithLabel
labelProps={{ className: 'font-bold' }}
label="Source File"
value={filepathForm}
/>
) : (
<ControlledSelect
items={fileOptions.map((path) => ({
value: path,
children: <LeftTruncatedText>{path}</LeftTruncatedText>,
}))}
renderValue={(value) => <LeftTruncatedText>{value}</LeftTruncatedText>}
formFieldProps={{
attributes: { Popover: { className: tooltipClassName } },
slotLabel: <Label className="font-bold">Source File</Label>,
}}
useControllerProps={{ control, name: 'filepath' }}
/>
)}
<Divider />
{isLoadingFileContent ? (
{fileContentError ? (
<Banner
kind="inline"
status="error"
attributes={{ BannerIcon: { className: 'self-start' } }}
>
{fileContentError.message}
</Banner>
) : isLoadingFileContent ? (
<Flex justify="center" align="center" className="h-full py-[80px]">
<Spinner description="Loading file content..." />
</Flex>
Expand Down Expand Up @@ -197,7 +240,7 @@ export const CreateFileSplitsModal: FC<Props> = ({ open, onClose, filepath }) =>
<ControlledRadioGroup
orientation="horizontal"
defaultValue="random"
className="flex w-full! [&>*]:flex-1"
className="flex w-full! *:flex-1"
items={[
{ children: 'Random', value: 'random' },
{ children: 'Sequential', value: 'sequential' },
Expand Down
31 changes: 31 additions & 0 deletions web/packages/studio/src/components/LeftTruncatedText/index.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Text> {
/** 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 `<bdi>` 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<LeftTruncatedTextProps> = ({
children,
className,
kind = 'inherit',
...props
}) => (
<Text {...props} kind={kind} dir="rtl" className={cn('block truncate text-left', className)}>
<bdi>{children}</bdi>
</Text>
);
Loading
Loading