From eb2ba90a03b1a724ffea8a8abee4afabfc7887fa Mon Sep 17 00:00:00 2001 From: imagineer99 Date: Sun, 15 Feb 2026 18:16:46 +0000 Subject: [PATCH 1/3] feat: check dataset configs and splits before hitting check-format --- .../components/steps/dataset-step.tsx | 137 +++++++++++++- .../components/steps/summary-step.tsx | 18 ++ .../sections/dataset-preview-dialog.tsx | 25 ++- .../studio/sections/dataset-section.tsx | 178 ++++++++++++++++-- .../src/features/training/api/mappers.ts | 2 + .../training/stores/training-config-store.ts | 8 +- .../src/features/training/types/api.ts | 2 + .../src/features/training/types/config.ts | 4 + studio/frontend/src/hooks/index.ts | 1 + .../src/hooks/use-hf-dataset-splits.ts | 135 +++++++++++++ studio/frontend/src/types/training.ts | 4 + 11 files changed, 488 insertions(+), 26 deletions(-) create mode 100644 studio/frontend/src/hooks/use-hf-dataset-splits.ts diff --git a/studio/frontend/src/features/onboarding/components/steps/dataset-step.tsx b/studio/frontend/src/features/onboarding/components/steps/dataset-step.tsx index 65d802c77e..19ee08e4cd 100644 --- a/studio/frontend/src/features/onboarding/components/steps/dataset-step.tsx +++ b/studio/frontend/src/features/onboarding/components/steps/dataset-step.tsx @@ -35,6 +35,7 @@ import { import { useDebouncedValue, useHfDatasetSearch, + useHfDatasetSplits, useInfiniteScroll, } from "@/hooks"; import { cn, formatCompact } from "@/lib/utils"; @@ -47,7 +48,7 @@ import { Upload04Icon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; -import { useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useShallow } from "zustand/react/shallow"; const FORMAT_OPTIONS: { value: DatasetFormat; label: string }[] = [ @@ -67,6 +68,10 @@ export function DatasetStep() { setDatasetFormat, dataset, setDataset, + datasetConfig, + setDatasetConfig, + datasetSplit, + setDatasetSplit, uploadedFile, setUploadedFile, } = useTrainingConfigStore( @@ -79,6 +84,10 @@ export function DatasetStep() { setDatasetFormat: s.setDatasetFormat, dataset: s.dataset, setDataset: s.setDataset, + datasetConfig: s.datasetConfig, + setDatasetConfig: s.setDatasetConfig, + datasetSplit: s.datasetSplit, + setDatasetSplit: s.setDatasetSplit, uploadedFile: s.uploadedFile, setUploadedFile: s.setUploadedFile, })), @@ -104,6 +113,39 @@ export function DatasetStep() { hfResults.length, ); + // Fetch configs & splits from HF datasets-server API + const { + configs: hfConfigs, + splits: hfSplits, + hasMultipleConfigs, + hasMultipleSplits, + isLoading: splitsLoading, + error: splitsError, + } = useHfDatasetSplits( + datasetSource === "huggingface" ? dataset : null, + datasetConfig, + { accessToken: hfToken || undefined }, + ); + + // Auto-select config when there is only one + useEffect(() => { + if (hfConfigs.length === 1 && datasetConfig !== hfConfigs[0]) { + setDatasetConfig(hfConfigs[0]); + } + }, [hfConfigs, datasetConfig, setDatasetConfig]); + + // Auto-select split when there is only one, or default to "train" + useEffect(() => { + if (hfSplits.length === 0) return; + if (hfSplits.length === 1 && datasetSplit !== hfSplits[0]) { + setDatasetSplit(hfSplits[0]); + } else if (!datasetSplit && hfSplits.includes("train")) { + setDatasetSplit("train"); + } else if (!datasetSplit) { + setDatasetSplit(hfSplits[0]); + } + }, [hfSplits, datasetSplit, setDatasetSplit]); + const handleFileUpload = () => { setUploadedFile("my_dataset.jsonl"); }; @@ -261,6 +303,99 @@ export function DatasetStep() { + + {/* Config & Split selectors */} + {dataset && splitsLoading && ( +
+ + Loading dataset configs and splits... +
+ )} + + {dataset && splitsError && ( +
+ Could not fetch dataset splits: {splitsError} +
+ )} + + {dataset && !splitsLoading && !splitsError && hasMultipleConfigs && ( + + + Subset (Config) + + + + + + This dataset has multiple subsets (configurations). + Select which one to use for training. + + + + + + )} + + {dataset && !splitsLoading && !splitsError && hasMultipleSplits && ( + + + Split + + + + + + Select which split of the dataset to use for training. + + + + + + )} ) : ( <> diff --git a/studio/frontend/src/features/onboarding/components/steps/summary-step.tsx b/studio/frontend/src/features/onboarding/components/steps/summary-step.tsx index ae6edd230a..1a59b877e3 100644 --- a/studio/frontend/src/features/onboarding/components/steps/summary-step.tsx +++ b/studio/frontend/src/features/onboarding/components/steps/summary-step.tsx @@ -23,6 +23,8 @@ export function SummaryStep() { datasetSource, datasetFormat, dataset, + datasetConfig, + datasetSplit, uploadedFile, epochs, contextLength, @@ -39,6 +41,8 @@ export function SummaryStep() { datasetSource, datasetFormat, dataset, + datasetConfig, + datasetSplit, uploadedFile, epochs, contextLength, @@ -53,6 +57,8 @@ export function SummaryStep() { datasetSource, datasetFormat, dataset, + datasetConfig, + datasetSplit, uploadedFile, epochs, contextLength, @@ -147,6 +153,18 @@ export function SummaryStep() { Source {datasetSource} + {datasetConfig && ( +
+ Subset + {datasetConfig} +
+ )} + {datasetSplit && ( +
+ Split + {datasetSplit} +
+ )}
Format {datasetFormat} diff --git a/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx b/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx index 80fbcb65ee..95dd26b6cc 100644 --- a/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx +++ b/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx @@ -40,15 +40,21 @@ type DatasetPreviewDialogProps = { onOpenChange: (open: boolean) => void; datasetName: string | null; hfToken: string | null; + datasetConfig?: string | null; + datasetSplit?: string | null; }; // --------------------------------------------------------------------------- // API -- uses existing /check-format endpoint // --------------------------------------------------------------------------- +// TODO(backend): Needs to accept `config` and `split` fields (see #37). +// The frontend already sends them in the request below. async function fetchCheckFormat( datasetName: string, hfToken: string | null, + config?: string | null, + split?: string | null, ): Promise { const res = await fetch("/api/datasets/check-format", { method: "POST", @@ -56,7 +62,8 @@ async function fetchCheckFormat( body: JSON.stringify({ dataset_name: datasetName, hf_token: hfToken || undefined, - split: "train", + config: config || undefined, + split: split || "train", }), }); if (!res.ok) { @@ -75,6 +82,8 @@ export function DatasetPreviewDialog({ onOpenChange, datasetName, hfToken, + datasetConfig, + datasetSplit, }: DatasetPreviewDialogProps) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); @@ -90,7 +99,7 @@ export function DatasetPreviewDialog({ setLoading(true); setError(null); - fetchCheckFormat(datasetName, hfToken) + fetchCheckFormat(datasetName, hfToken, datasetConfig, datasetSplit) .then((res) => { if (!cancelled) { setData(res); @@ -107,7 +116,7 @@ export function DatasetPreviewDialog({ return () => { cancelled = true; }; - }, [open, datasetName, hfToken]); + }, [open, datasetName, hfToken, datasetConfig, datasetSplit]); const rows = data?.preview_samples ?? []; const columns = data?.columns ?? []; @@ -115,9 +124,15 @@ export function DatasetPreviewDialog({ // Determine source label const sourceLabel = useMemo(() => { if (!datasetName) return ""; - if (datasetName.includes("/")) return `Hugging Face (${datasetName})`; + if (datasetName.includes("/")) { + let label = `Hugging Face (${datasetName}`; + if (datasetConfig) label += ` / ${datasetConfig}`; + if (datasetSplit) label += ` / ${datasetSplit}`; + label += ")"; + return label; + } return `Local Files (${datasetName})`; - }, [datasetName]); + }, [datasetName, datasetConfig, datasetSplit]); // Build TanStack Table columns from the column names const tableColumns = useMemo>[]>(() => { diff --git a/studio/frontend/src/features/studio/sections/dataset-section.tsx b/studio/frontend/src/features/studio/sections/dataset-section.tsx index 0d325fbefd..ec876188e5 100644 --- a/studio/frontend/src/features/studio/sections/dataset-section.tsx +++ b/studio/frontend/src/features/studio/sections/dataset-section.tsx @@ -25,6 +25,7 @@ import { import { useDebouncedValue, useHfDatasetSearch, + useHfDatasetSplits, useInfiniteScroll, } from "@/hooks"; import { formatCompact } from "@/lib/utils"; @@ -38,29 +39,34 @@ import { ViewIcon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; -import { useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useShallow } from "zustand/react/shallow"; import { DatasetPreviewDialog } from "./dataset-preview-dialog"; export function DatasetSection() { - const { dataset, setDataset, datasetFormat, setDatasetFormat, hfToken } = - useTrainingConfigStore( - useShallow( - ({ - dataset, - setDataset, - datasetFormat, - setDatasetFormat, - hfToken, - }) => ({ - dataset, - setDataset, - datasetFormat, - setDatasetFormat, - hfToken, - }), - ), - ); + const { + dataset, + setDataset, + datasetFormat, + setDatasetFormat, + datasetConfig, + setDatasetConfig, + datasetSplit, + setDatasetSplit, + hfToken, + } = useTrainingConfigStore( + useShallow((s) => ({ + dataset: s.dataset, + setDataset: s.setDataset, + datasetFormat: s.datasetFormat, + setDatasetFormat: s.setDatasetFormat, + datasetConfig: s.datasetConfig, + setDatasetConfig: s.setDatasetConfig, + datasetSplit: s.datasetSplit, + setDatasetSplit: s.setDatasetSplit, + hfToken: s.hfToken, + })), + ); const [inputValue, setInputValue] = useState(""); const [previewOpen, setPreviewOpen] = useState(false); @@ -96,6 +102,37 @@ export function DatasetSection() { return ids; }, [hfResults, dataset]); + // Fetch configs & splits from HF datasets-server API + const { + configs: hfConfigs, + splits: hfSplits, + hasMultipleConfigs, + hasMultipleSplits, + isLoading: splitsLoading, + error: splitsError, + } = useHfDatasetSplits(dataset, datasetConfig, { + accessToken: hfToken || undefined, + }); + + // Auto-select config when there is only one + useEffect(() => { + if (hfConfigs.length === 1 && datasetConfig !== hfConfigs[0]) { + setDatasetConfig(hfConfigs[0]); + } + }, [hfConfigs, datasetConfig, setDatasetConfig]); + + // Auto-select split when there is only one, or default to "train" if available + useEffect(() => { + if (hfSplits.length === 0) return; + if (hfSplits.length === 1 && datasetSplit !== hfSplits[0]) { + setDatasetSplit(hfSplits[0]); + } else if (!datasetSplit && hfSplits.includes("train")) { + setDatasetSplit("train"); + } else if (!datasetSplit) { + setDatasetSplit(hfSplits[0]); + } + }, [hfSplits, datasetSplit, setDatasetSplit]); + const comboboxAnchorRef = useRef(null); const { scrollRef, sentinelRef } = useInfiniteScroll( fetchMore, @@ -221,6 +258,105 @@ export function DatasetSection() {
+ {/* Config & Split selectors - shown after dataset is selected */} + {dataset && !splitsLoading && !splitsError && (hasMultipleConfigs || hasMultipleSplits) && ( +
+ {hasMultipleConfigs && ( +
+ + Subset (Config) + + + + + + This dataset has multiple subsets (configurations). + Select which one to use for training. + + + + +
+ )} + + {hasMultipleSplits && ( +
+ + Split + + + + + + Select which split of the dataset to use for training. + + + + +
+ )} +
+ )} + + {/* Loading indicator for splits */} + {dataset && splitsLoading && ( +
+ + Loading dataset configs and splits... +
+ )} + + {/* Error fetching splits */} + {dataset && splitsError && ( +
+ Could not fetch dataset splits: {splitsError} +
+ )} +
Target Format @@ -280,6 +416,8 @@ export function DatasetSection() {

Hugging Face Dataset + {datasetConfig && ` / ${datasetConfig}`} + {datasetSplit && ` / ${datasetSplit}`}

@@ -321,6 +459,8 @@ export function DatasetSection() { onOpenChange={setPreviewOpen} datasetName={dataset} hfToken={hfToken} + datasetConfig={datasetConfig} + datasetSplit={datasetSplit} /> ); diff --git a/studio/frontend/src/features/training/api/mappers.ts b/studio/frontend/src/features/training/api/mappers.ts index 0150c62b41..3a49727cd2 100644 --- a/studio/frontend/src/features/training/api/mappers.ts +++ b/studio/frontend/src/features/training/api/mappers.ts @@ -22,6 +22,8 @@ export function buildTrainingStartPayload( load_in_4bit: adapterMethod ? isQlorMethod : false, max_seq_length: config.contextLength, hf_dataset: hfDataset, + hf_dataset_config: hfDataset ? config.datasetConfig : null, + hf_dataset_split: hfDataset ? config.datasetSplit : null, local_datasets: [], format_type: config.datasetFormat, num_epochs: config.epochs, diff --git a/studio/frontend/src/features/training/stores/training-config-store.ts b/studio/frontend/src/features/training/stores/training-config-store.ts index a9d6d37f42..afa69339dc 100644 --- a/studio/frontend/src/features/training/stores/training-config-store.ts +++ b/studio/frontend/src/features/training/stores/training-config-store.ts @@ -16,6 +16,8 @@ const initialState: TrainingConfigState = { datasetSource: "huggingface", datasetFormat: "auto", dataset: null, + datasetConfig: null, + datasetSplit: null, uploadedFile: null, ...DEFAULT_HYPERPARAMS, }; @@ -55,7 +57,11 @@ export const useTrainingConfigStore = create()( setHfToken: (hfToken) => set({ hfToken }), setDatasetSource: (datasetSource) => set({ datasetSource }), setDatasetFormat: (datasetFormat) => set({ datasetFormat }), - setDataset: (dataset) => set({ dataset }), + setDataset: (dataset) => + set({ dataset, datasetConfig: null, datasetSplit: null }), + setDatasetConfig: (datasetConfig) => + set({ datasetConfig, datasetSplit: null }), + setDatasetSplit: (datasetSplit) => set({ datasetSplit }), setUploadedFile: (uploadedFile) => set({ uploadedFile }), setEpochs: (epochs) => set({ epochs }), setContextLength: (contextLength) => set({ contextLength }), diff --git a/studio/frontend/src/features/training/types/api.ts b/studio/frontend/src/features/training/types/api.ts index 9cf789a702..6011ff6b96 100644 --- a/studio/frontend/src/features/training/types/api.ts +++ b/studio/frontend/src/features/training/types/api.ts @@ -5,6 +5,8 @@ export interface TrainingStartRequest { load_in_4bit: boolean; max_seq_length: number; hf_dataset: string | null; + hf_dataset_config: string | null; + hf_dataset_split: string | null; local_datasets: string[]; format_type: string; num_epochs: number; diff --git a/studio/frontend/src/features/training/types/config.ts b/studio/frontend/src/features/training/types/config.ts index c0d93f76a7..2b30c68342 100644 --- a/studio/frontend/src/features/training/types/config.ts +++ b/studio/frontend/src/features/training/types/config.ts @@ -18,6 +18,8 @@ export interface TrainingConfigState { datasetSource: DatasetSource; datasetFormat: DatasetFormat; dataset: string | null; + datasetConfig: string | null; + datasetSplit: string | null; uploadedFile: string | null; epochs: number; contextLength: number; @@ -60,6 +62,8 @@ export interface TrainingConfigActions { setDatasetSource: (source: DatasetSource) => void; setDatasetFormat: (format: DatasetFormat) => void; setDataset: (dataset: string | null) => void; + setDatasetConfig: (config: string | null) => void; + setDatasetSplit: (split: string | null) => void; setUploadedFile: (file: string | null) => void; setEpochs: (epochs: number) => void; setContextLength: (length: number) => void; diff --git a/studio/frontend/src/hooks/index.ts b/studio/frontend/src/hooks/index.ts index 4e338169e4..59b0686967 100644 --- a/studio/frontend/src/hooks/index.ts +++ b/studio/frontend/src/hooks/index.ts @@ -1,4 +1,5 @@ export { useDebouncedValue } from "./use-debounced-value"; export { useHfModelSearch } from "./use-hf-model-search"; export { useHfDatasetSearch } from "./use-hf-dataset-search"; +export { useHfDatasetSplits } from "./use-hf-dataset-splits"; export { useInfiniteScroll } from "./use-infinite-scroll"; diff --git a/studio/frontend/src/hooks/use-hf-dataset-splits.ts b/studio/frontend/src/hooks/use-hf-dataset-splits.ts new file mode 100644 index 0000000000..b09e1e3d60 --- /dev/null +++ b/studio/frontend/src/hooks/use-hf-dataset-splits.ts @@ -0,0 +1,135 @@ +import { useCallback, useEffect, useState } from "react"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface HfSplitEntry { + dataset: string; + config: string; + split: string; +} + +export interface HfSplitsResponse { + splits: HfSplitEntry[]; + pending: unknown[]; + failed: unknown[]; +} + +export interface HfDatasetSplitsResult { + /** All unique config (subset) names found in the dataset */ + configs: string[]; + /** All split names available for the currently selected config */ + splits: string[]; + /** Raw split entries from the API */ + entries: HfSplitEntry[]; + /** Whether the dataset has more than one config */ + hasMultipleConfigs: boolean; + /** Whether the selected config has more than one split */ + hasMultipleSplits: boolean; + /** True while the request is in-flight */ + isLoading: boolean; + /** Error message if the fetch failed */ + error: string | null; +} + +const HF_SPLITS_API = "https://datasets-server.huggingface.co/splits"; + +// --------------------------------------------------------------------------- +// Hook +// --------------------------------------------------------------------------- + +/** + * Fetches the available configs (subsets) and splits for a HuggingFace dataset + * using the datasets-server API. + * + * @param datasetName - HF dataset id (e.g. "ibm/duorc"), or null to skip. + * @param selectedConfig - Currently selected config, used to filter splits. + * @param options.accessToken - Optional HF access token for gated datasets. + */ +export function useHfDatasetSplits( + datasetName: string | null, + selectedConfig: string | null, + options?: { accessToken?: string }, +): HfDatasetSplitsResult { + const [entries, setEntries] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const accessToken = options?.accessToken; + + const fetchSplits = useCallback( + async (dataset: string, signal: AbortSignal) => { + const url = `${HF_SPLITS_API}?dataset=${encodeURIComponent(dataset)}`; + const headers: Record = {}; + if (accessToken) { + headers.Authorization = `Bearer ${accessToken}`; + } + + const res = await fetch(url, { headers, signal }); + if (!res.ok) { + const body = await res.json().catch(() => null); + throw new Error( + body?.error || `Failed to fetch splits (${res.status})`, + ); + } + + const data: HfSplitsResponse = await res.json(); + return data.splits ?? []; + }, + [accessToken], + ); + + useEffect(() => { + if (!datasetName) { + setEntries([]); + setError(null); + setIsLoading(false); + return; + } + + const controller = new AbortController(); + setIsLoading(true); + setError(null); + + fetchSplits(datasetName, controller.signal) + .then((splits) => { + if (!controller.signal.aborted) { + setEntries(splits); + setError(null); + } + }) + .catch((err) => { + if (!controller.signal.aborted) { + setError(err.message || "Failed to fetch dataset splits"); + setEntries([]); + } + }) + .finally(() => { + if (!controller.signal.aborted) { + setIsLoading(false); + } + }); + + return () => controller.abort(); + }, [datasetName, fetchSplits]); + + // Derive unique configs + const configs = Array.from(new Set(entries.map((e) => e.config))); + + // Derive splits for the selected config (or all splits if no config selected) + const filteredEntries = selectedConfig + ? entries.filter((e) => e.config === selectedConfig) + : entries; + const splits = Array.from(new Set(filteredEntries.map((e) => e.split))); + + return { + configs, + splits, + entries, + hasMultipleConfigs: configs.length > 1, + hasMultipleSplits: splits.length > 1, + isLoading, + error, + }; +} diff --git a/studio/frontend/src/types/training.ts b/studio/frontend/src/types/training.ts index 49c1dddaa3..dcb7ff8e08 100644 --- a/studio/frontend/src/types/training.ts +++ b/studio/frontend/src/types/training.ts @@ -18,6 +18,8 @@ export interface WizardState { datasetSource: DatasetSource; datasetFormat: DatasetFormat; dataset: string | null; + datasetConfig: string | null; + datasetSplit: string | null; uploadedFile: string | null; epochs: number; contextLength: number; @@ -60,6 +62,8 @@ export interface WizardActions { setDatasetSource: (source: DatasetSource) => void; setDatasetFormat: (format: DatasetFormat) => void; setDataset: (dataset: string | null) => void; + setDatasetConfig: (config: string | null) => void; + setDatasetSplit: (split: string | null) => void; setUploadedFile: (file: string | null) => void; setEpochs: (epochs: number) => void; setContextLength: (length: number) => void; From ef9e5ffc3342e105565b0a9f643c9bd6e18ae96e Mon Sep 17 00:00:00 2001 From: Shine1i Date: Sun, 15 Feb 2026 20:53:59 +0100 Subject: [PATCH 2/3] fix hf cfg/split ui --- .../components/steps/dataset-step.tsx | 140 +-------- .../studio/sections/dataset-section.tsx | 154 ++-------- .../hf-dataset-config-split-selectors.tsx | 271 ++++++++++++++++++ .../frontend/src/features/training/index.ts | 1 + .../src/hooks/use-hf-dataset-splits.ts | 14 +- 5 files changed, 315 insertions(+), 265 deletions(-) create mode 100644 studio/frontend/src/features/training/components/hf-dataset-config-split-selectors.tsx diff --git a/studio/frontend/src/features/onboarding/components/steps/dataset-step.tsx b/studio/frontend/src/features/onboarding/components/steps/dataset-step.tsx index 19ee08e4cd..d4890f698c 100644 --- a/studio/frontend/src/features/onboarding/components/steps/dataset-step.tsx +++ b/studio/frontend/src/features/onboarding/components/steps/dataset-step.tsx @@ -35,11 +35,10 @@ import { import { useDebouncedValue, useHfDatasetSearch, - useHfDatasetSplits, useInfiniteScroll, } from "@/hooks"; import { cn, formatCompact } from "@/lib/utils"; -import { useTrainingConfigStore } from "@/features/training"; +import { HfDatasetConfigSplitSelectors, useTrainingConfigStore } from "@/features/training"; import type { DatasetFormat } from "@/types/training"; import { InformationCircleIcon, @@ -48,7 +47,7 @@ import { Upload04Icon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import { useShallow } from "zustand/react/shallow"; const FORMAT_OPTIONS: { value: DatasetFormat; label: string }[] = [ @@ -113,39 +112,6 @@ export function DatasetStep() { hfResults.length, ); - // Fetch configs & splits from HF datasets-server API - const { - configs: hfConfigs, - splits: hfSplits, - hasMultipleConfigs, - hasMultipleSplits, - isLoading: splitsLoading, - error: splitsError, - } = useHfDatasetSplits( - datasetSource === "huggingface" ? dataset : null, - datasetConfig, - { accessToken: hfToken || undefined }, - ); - - // Auto-select config when there is only one - useEffect(() => { - if (hfConfigs.length === 1 && datasetConfig !== hfConfigs[0]) { - setDatasetConfig(hfConfigs[0]); - } - }, [hfConfigs, datasetConfig, setDatasetConfig]); - - // Auto-select split when there is only one, or default to "train" - useEffect(() => { - if (hfSplits.length === 0) return; - if (hfSplits.length === 1 && datasetSplit !== hfSplits[0]) { - setDatasetSplit(hfSplits[0]); - } else if (!datasetSplit && hfSplits.includes("train")) { - setDatasetSplit("train"); - } else if (!datasetSplit) { - setDatasetSplit(hfSplits[0]); - } - }, [hfSplits, datasetSplit, setDatasetSplit]); - const handleFileUpload = () => { setUploadedFile("my_dataset.jsonl"); }; @@ -304,98 +270,16 @@ export function DatasetStep() { - {/* Config & Split selectors */} - {dataset && splitsLoading && ( -
- - Loading dataset configs and splits... -
- )} - - {dataset && splitsError && ( -
- Could not fetch dataset splits: {splitsError} -
- )} - - {dataset && !splitsLoading && !splitsError && hasMultipleConfigs && ( - - - Subset (Config) - - - - - - This dataset has multiple subsets (configurations). - Select which one to use for training. - - - - - - )} - - {dataset && !splitsLoading && !splitsError && hasMultipleSplits && ( - - - Split - - - - - - Select which split of the dataset to use for training. - - - - - - )} + ) : ( <> diff --git a/studio/frontend/src/features/studio/sections/dataset-section.tsx b/studio/frontend/src/features/studio/sections/dataset-section.tsx index ec876188e5..8fb3a15735 100644 --- a/studio/frontend/src/features/studio/sections/dataset-section.tsx +++ b/studio/frontend/src/features/studio/sections/dataset-section.tsx @@ -25,11 +25,10 @@ import { import { useDebouncedValue, useHfDatasetSearch, - useHfDatasetSplits, useInfiniteScroll, } from "@/hooks"; import { formatCompact } from "@/lib/utils"; -import { useTrainingConfigStore } from "@/features/training"; +import { HfDatasetConfigSplitSelectors, useTrainingConfigStore } from "@/features/training"; import { CloudUploadIcon, Database02Icon, @@ -39,10 +38,20 @@ import { ViewIcon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import { useShallow } from "zustand/react/shallow"; import { DatasetPreviewDialog } from "./dataset-preview-dialog"; +function isLikelyLocalDatasetRef(value: string) { + return ( + value.startsWith("/") || + value.startsWith("./") || + value.startsWith("../") || + value.includes("\\") || + /\.(jsonl|json|csv|parquet)$/i.test(value) + ); +} + export function DatasetSection() { const { dataset, @@ -102,37 +111,6 @@ export function DatasetSection() { return ids; }, [hfResults, dataset]); - // Fetch configs & splits from HF datasets-server API - const { - configs: hfConfigs, - splits: hfSplits, - hasMultipleConfigs, - hasMultipleSplits, - isLoading: splitsLoading, - error: splitsError, - } = useHfDatasetSplits(dataset, datasetConfig, { - accessToken: hfToken || undefined, - }); - - // Auto-select config when there is only one - useEffect(() => { - if (hfConfigs.length === 1 && datasetConfig !== hfConfigs[0]) { - setDatasetConfig(hfConfigs[0]); - } - }, [hfConfigs, datasetConfig, setDatasetConfig]); - - // Auto-select split when there is only one, or default to "train" if available - useEffect(() => { - if (hfSplits.length === 0) return; - if (hfSplits.length === 1 && datasetSplit !== hfSplits[0]) { - setDatasetSplit(hfSplits[0]); - } else if (!datasetSplit && hfSplits.includes("train")) { - setDatasetSplit("train"); - } else if (!datasetSplit) { - setDatasetSplit(hfSplits[0]); - } - }, [hfSplits, datasetSplit, setDatasetSplit]); - const comboboxAnchorRef = useRef(null); const { scrollRef, sentinelRef } = useInfiniteScroll( fetchMore, @@ -258,104 +236,16 @@ export function DatasetSection() { - {/* Config & Split selectors - shown after dataset is selected */} - {dataset && !splitsLoading && !splitsError && (hasMultipleConfigs || hasMultipleSplits) && ( -
- {hasMultipleConfigs && ( -
- - Subset (Config) - - - - - - This dataset has multiple subsets (configurations). - Select which one to use for training. - - - - -
- )} - - {hasMultipleSplits && ( -
- - Split - - - - - - Select which split of the dataset to use for training. - - - - -
- )} -
- )} - - {/* Loading indicator for splits */} - {dataset && splitsLoading && ( -
- - Loading dataset configs and splits... -
- )} - - {/* Error fetching splits */} - {dataset && splitsError && ( -
- Could not fetch dataset splits: {splitsError} -
- )} +
diff --git a/studio/frontend/src/features/training/components/hf-dataset-config-split-selectors.tsx b/studio/frontend/src/features/training/components/hf-dataset-config-split-selectors.tsx new file mode 100644 index 0000000000..4342ac44f0 --- /dev/null +++ b/studio/frontend/src/features/training/components/hf-dataset-config-split-selectors.tsx @@ -0,0 +1,271 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Spinner } from "@/components/ui/spinner"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { + Field, + FieldLabel, +} from "@/components/ui/field"; +import { useHfDatasetSplits } from "@/hooks"; +import { InformationCircleIcon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { useEffect } from "react"; + +type Props = { + variant: "wizard" | "studio"; + enabled: boolean; + datasetName: string | null; + accessToken?: string; + datasetConfig: string | null; + setDatasetConfig: (v: string | null) => void; + datasetSplit: string | null; + setDatasetSplit: (v: string | null) => void; +}; + +export function HfDatasetConfigSplitSelectors({ + variant, + enabled, + datasetName, + accessToken, + datasetConfig, + setDatasetConfig, + datasetSplit, + setDatasetSplit, +}: Props) { + const { + configs: hfConfigs, + splits: hfSplits, + hasMultipleConfigs, + hasMultipleSplits, + isLoading, + error, + } = useHfDatasetSplits(enabled ? datasetName : null, datasetConfig, { + accessToken, + }); + + useEffect(() => { + if (hfConfigs.length === 1 && datasetConfig !== hfConfigs[0]) { + setDatasetConfig(hfConfigs[0]); + } + }, [hfConfigs, datasetConfig, setDatasetConfig]); + + useEffect(() => { + if (hfSplits.length === 0) return; + if (hasMultipleConfigs && !datasetConfig) return; + if (hfSplits.length === 1 && datasetSplit !== hfSplits[0]) { + setDatasetSplit(hfSplits[0]); + } else if (!datasetSplit && hfSplits.includes("train")) { + setDatasetSplit("train"); + } else if (!datasetSplit) { + setDatasetSplit(hfSplits[0]); + } + }, [ + hfSplits, + hasMultipleConfigs, + datasetConfig, + datasetSplit, + setDatasetSplit, + ]); + + if (!enabled || !datasetName) return null; + + return ( + <> + {isLoading && ( +
+ + Loading dataset configs and splits... +
+ )} + + {error && ( +
+ Could not fetch dataset splits: {error} +
+ )} + + {!isLoading && !error && hasMultipleConfigs && ( + <> + {variant === "wizard" ? ( + + + Subset (Config) + + + + + + This dataset has multiple subsets (configurations). Select + which one to use for training. + + + + + + ) : ( +
+ + Subset (Config) + + + + + + This dataset has multiple subsets (configurations). Select + which one to use for training. + + + + +
+ )} + + )} + + {!isLoading && !error && hasMultipleSplits && ( + <> + {variant === "wizard" ? ( + + + Split + + + + + + Select which split of the dataset to use for training. + + + + + + ) : ( +
+ + Split + + + + + + Select which split of the dataset to use for training. + + + + +
+ )} + + )} + + ); +} diff --git a/studio/frontend/src/features/training/index.ts b/studio/frontend/src/features/training/index.ts index 48b9079fd3..d32332a19b 100644 --- a/studio/frontend/src/features/training/index.ts +++ b/studio/frontend/src/features/training/index.ts @@ -5,4 +5,5 @@ export { } from "./stores/training-runtime-store"; export { useTrainingActions } from "./hooks/use-training-actions"; export { useTrainingRuntimeLifecycle } from "./hooks/use-training-runtime-lifecycle"; +export { HfDatasetConfigSplitSelectors } from "./components/hf-dataset-config-split-selectors"; export type { TrainingPhase } from "./types/runtime"; diff --git a/studio/frontend/src/hooks/use-hf-dataset-splits.ts b/studio/frontend/src/hooks/use-hf-dataset-splits.ts index b09e1e3d60..ededfacbd6 100644 --- a/studio/frontend/src/hooks/use-hf-dataset-splits.ts +++ b/studio/frontend/src/hooks/use-hf-dataset-splits.ts @@ -117,10 +117,14 @@ export function useHfDatasetSplits( // Derive unique configs const configs = Array.from(new Set(entries.map((e) => e.config))); - // Derive splits for the selected config (or all splits if no config selected) - const filteredEntries = selectedConfig - ? entries.filter((e) => e.config === selectedConfig) - : entries; + // Derive splits for the active config. + // If dataset has >1 config and none is selected yet, return no splits so UI + // doesn't auto-pick/show a split before config is chosen. + const activeConfig = + selectedConfig ?? (configs.length === 1 ? configs[0] : null); + const filteredEntries = activeConfig + ? entries.filter((e) => e.config === activeConfig) + : []; const splits = Array.from(new Set(filteredEntries.map((e) => e.split))); return { @@ -128,7 +132,7 @@ export function useHfDatasetSplits( splits, entries, hasMultipleConfigs: configs.length > 1, - hasMultipleSplits: splits.length > 1, + hasMultipleSplits: activeConfig ? splits.length > 1 : false, isLoading, error, }; From 51218702c241cb796e33647a27c0916130594df7 Mon Sep 17 00:00:00 2001 From: Shine1i Date: Sun, 15 Feb 2026 21:09:45 +0100 Subject: [PATCH 3/3] cfg->subset --- .../components/steps/dataset-step.tsx | 19 +++--- .../components/steps/summary-step.tsx | 10 +-- .../sections/dataset-preview-dialog.tsx | 16 ++--- .../studio/sections/dataset-section.tsx | 23 ++++--- .../src/features/training/api/mappers.ts | 2 +- ... => hf-dataset-subset-split-selectors.tsx} | 62 +++++++++---------- .../frontend/src/features/training/index.ts | 2 +- .../training/stores/training-config-store.ts | 18 ++++-- .../src/features/training/types/config.ts | 4 +- .../src/hooks/use-hf-dataset-splits.ts | 40 ++++++------ studio/frontend/src/types/training.ts | 4 +- 11 files changed, 108 insertions(+), 92 deletions(-) rename studio/frontend/src/features/training/components/{hf-dataset-config-split-selectors.tsx => hf-dataset-subset-split-selectors.tsx} (84%) diff --git a/studio/frontend/src/features/onboarding/components/steps/dataset-step.tsx b/studio/frontend/src/features/onboarding/components/steps/dataset-step.tsx index d4890f698c..0275e5a6da 100644 --- a/studio/frontend/src/features/onboarding/components/steps/dataset-step.tsx +++ b/studio/frontend/src/features/onboarding/components/steps/dataset-step.tsx @@ -38,7 +38,10 @@ import { useInfiniteScroll, } from "@/hooks"; import { cn, formatCompact } from "@/lib/utils"; -import { HfDatasetConfigSplitSelectors, useTrainingConfigStore } from "@/features/training"; +import { + HfDatasetSubsetSplitSelectors, + useTrainingConfigStore, +} from "@/features/training"; import type { DatasetFormat } from "@/types/training"; import { InformationCircleIcon, @@ -67,8 +70,8 @@ export function DatasetStep() { setDatasetFormat, dataset, setDataset, - datasetConfig, - setDatasetConfig, + datasetSubset, + setDatasetSubset, datasetSplit, setDatasetSplit, uploadedFile, @@ -83,8 +86,8 @@ export function DatasetStep() { setDatasetFormat: s.setDatasetFormat, dataset: s.dataset, setDataset: s.setDataset, - datasetConfig: s.datasetConfig, - setDatasetConfig: s.setDatasetConfig, + datasetSubset: s.datasetSubset, + setDatasetSubset: s.setDatasetSubset, datasetSplit: s.datasetSplit, setDatasetSplit: s.setDatasetSplit, uploadedFile: s.uploadedFile, @@ -270,13 +273,13 @@ export function DatasetStep() {
- diff --git a/studio/frontend/src/features/onboarding/components/steps/summary-step.tsx b/studio/frontend/src/features/onboarding/components/steps/summary-step.tsx index 1a59b877e3..da2758ebfa 100644 --- a/studio/frontend/src/features/onboarding/components/steps/summary-step.tsx +++ b/studio/frontend/src/features/onboarding/components/steps/summary-step.tsx @@ -23,7 +23,7 @@ export function SummaryStep() { datasetSource, datasetFormat, dataset, - datasetConfig, + datasetSubset, datasetSplit, uploadedFile, epochs, @@ -41,7 +41,7 @@ export function SummaryStep() { datasetSource, datasetFormat, dataset, - datasetConfig, + datasetSubset, datasetSplit, uploadedFile, epochs, @@ -57,7 +57,7 @@ export function SummaryStep() { datasetSource, datasetFormat, dataset, - datasetConfig, + datasetSubset, datasetSplit, uploadedFile, epochs, @@ -153,10 +153,10 @@ export function SummaryStep() { Source {datasetSource} - {datasetConfig && ( + {datasetSubset && (
Subset - {datasetConfig} + {datasetSubset}
)} {datasetSplit && ( diff --git a/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx b/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx index 95dd26b6cc..f145b17f61 100644 --- a/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx +++ b/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx @@ -40,7 +40,7 @@ type DatasetPreviewDialogProps = { onOpenChange: (open: boolean) => void; datasetName: string | null; hfToken: string | null; - datasetConfig?: string | null; + datasetSubset?: string | null; datasetSplit?: string | null; }; @@ -53,7 +53,7 @@ type DatasetPreviewDialogProps = { async function fetchCheckFormat( datasetName: string, hfToken: string | null, - config?: string | null, + subset?: string | null, split?: string | null, ): Promise { const res = await fetch("/api/datasets/check-format", { @@ -62,7 +62,7 @@ async function fetchCheckFormat( body: JSON.stringify({ dataset_name: datasetName, hf_token: hfToken || undefined, - config: config || undefined, + config: subset || undefined, split: split || "train", }), }); @@ -82,7 +82,7 @@ export function DatasetPreviewDialog({ onOpenChange, datasetName, hfToken, - datasetConfig, + datasetSubset, datasetSplit, }: DatasetPreviewDialogProps) { const [data, setData] = useState(null); @@ -99,7 +99,7 @@ export function DatasetPreviewDialog({ setLoading(true); setError(null); - fetchCheckFormat(datasetName, hfToken, datasetConfig, datasetSplit) + fetchCheckFormat(datasetName, hfToken, datasetSubset, datasetSplit) .then((res) => { if (!cancelled) { setData(res); @@ -116,7 +116,7 @@ export function DatasetPreviewDialog({ return () => { cancelled = true; }; - }, [open, datasetName, hfToken, datasetConfig, datasetSplit]); + }, [open, datasetName, hfToken, datasetSubset, datasetSplit]); const rows = data?.preview_samples ?? []; const columns = data?.columns ?? []; @@ -126,13 +126,13 @@ export function DatasetPreviewDialog({ if (!datasetName) return ""; if (datasetName.includes("/")) { let label = `Hugging Face (${datasetName}`; - if (datasetConfig) label += ` / ${datasetConfig}`; + if (datasetSubset) label += ` / ${datasetSubset}`; if (datasetSplit) label += ` / ${datasetSplit}`; label += ")"; return label; } return `Local Files (${datasetName})`; - }, [datasetName, datasetConfig, datasetSplit]); + }, [datasetName, datasetSubset, datasetSplit]); // Build TanStack Table columns from the column names const tableColumns = useMemo>[]>(() => { diff --git a/studio/frontend/src/features/studio/sections/dataset-section.tsx b/studio/frontend/src/features/studio/sections/dataset-section.tsx index 8fb3a15735..c4d691d8c7 100644 --- a/studio/frontend/src/features/studio/sections/dataset-section.tsx +++ b/studio/frontend/src/features/studio/sections/dataset-section.tsx @@ -28,7 +28,10 @@ import { useInfiniteScroll, } from "@/hooks"; import { formatCompact } from "@/lib/utils"; -import { HfDatasetConfigSplitSelectors, useTrainingConfigStore } from "@/features/training"; +import { + HfDatasetSubsetSplitSelectors, + useTrainingConfigStore, +} from "@/features/training"; import { CloudUploadIcon, Database02Icon, @@ -58,8 +61,8 @@ export function DatasetSection() { setDataset, datasetFormat, setDatasetFormat, - datasetConfig, - setDatasetConfig, + datasetSubset, + setDatasetSubset, datasetSplit, setDatasetSplit, hfToken, @@ -69,8 +72,8 @@ export function DatasetSection() { setDataset: s.setDataset, datasetFormat: s.datasetFormat, setDatasetFormat: s.setDatasetFormat, - datasetConfig: s.datasetConfig, - setDatasetConfig: s.setDatasetConfig, + datasetSubset: s.datasetSubset, + setDatasetSubset: s.setDatasetSubset, datasetSplit: s.datasetSplit, setDatasetSplit: s.setDatasetSplit, hfToken: s.hfToken, @@ -236,13 +239,13 @@ export function DatasetSection() { - @@ -306,7 +309,7 @@ export function DatasetSection() {

Hugging Face Dataset - {datasetConfig && ` / ${datasetConfig}`} + {datasetSubset && ` / ${datasetSubset}`} {datasetSplit && ` / ${datasetSplit}`}

@@ -349,7 +352,7 @@ export function DatasetSection() { onOpenChange={setPreviewOpen} datasetName={dataset} hfToken={hfToken} - datasetConfig={datasetConfig} + datasetSubset={datasetSubset} datasetSplit={datasetSplit} /> diff --git a/studio/frontend/src/features/training/api/mappers.ts b/studio/frontend/src/features/training/api/mappers.ts index 3a49727cd2..990862a27e 100644 --- a/studio/frontend/src/features/training/api/mappers.ts +++ b/studio/frontend/src/features/training/api/mappers.ts @@ -22,7 +22,7 @@ export function buildTrainingStartPayload( load_in_4bit: adapterMethod ? isQlorMethod : false, max_seq_length: config.contextLength, hf_dataset: hfDataset, - hf_dataset_config: hfDataset ? config.datasetConfig : null, + hf_dataset_config: hfDataset ? config.datasetSubset : null, hf_dataset_split: hfDataset ? config.datasetSplit : null, local_datasets: [], format_type: config.datasetFormat, diff --git a/studio/frontend/src/features/training/components/hf-dataset-config-split-selectors.tsx b/studio/frontend/src/features/training/components/hf-dataset-subset-split-selectors.tsx similarity index 84% rename from studio/frontend/src/features/training/components/hf-dataset-config-split-selectors.tsx rename to studio/frontend/src/features/training/components/hf-dataset-subset-split-selectors.tsx index 4342ac44f0..00532ce860 100644 --- a/studio/frontend/src/features/training/components/hf-dataset-config-split-selectors.tsx +++ b/studio/frontend/src/features/training/components/hf-dataset-subset-split-selectors.tsx @@ -25,42 +25,42 @@ type Props = { enabled: boolean; datasetName: string | null; accessToken?: string; - datasetConfig: string | null; - setDatasetConfig: (v: string | null) => void; + datasetSubset: string | null; + setDatasetSubset: (v: string | null) => void; datasetSplit: string | null; setDatasetSplit: (v: string | null) => void; }; -export function HfDatasetConfigSplitSelectors({ +export function HfDatasetSubsetSplitSelectors({ variant, enabled, datasetName, accessToken, - datasetConfig, - setDatasetConfig, + datasetSubset, + setDatasetSubset, datasetSplit, setDatasetSplit, }: Props) { const { - configs: hfConfigs, + subsets: hfSubsets, splits: hfSplits, - hasMultipleConfigs, + hasMultipleSubsets, hasMultipleSplits, isLoading, error, - } = useHfDatasetSplits(enabled ? datasetName : null, datasetConfig, { + } = useHfDatasetSplits(enabled ? datasetName : null, datasetSubset, { accessToken, }); useEffect(() => { - if (hfConfigs.length === 1 && datasetConfig !== hfConfigs[0]) { - setDatasetConfig(hfConfigs[0]); + if (hfSubsets.length === 1 && datasetSubset !== hfSubsets[0]) { + setDatasetSubset(hfSubsets[0]); } - }, [hfConfigs, datasetConfig, setDatasetConfig]); + }, [hfSubsets, datasetSubset, setDatasetSubset]); useEffect(() => { if (hfSplits.length === 0) return; - if (hasMultipleConfigs && !datasetConfig) return; + if (hasMultipleSubsets && !datasetSubset) return; if (hfSplits.length === 1 && datasetSplit !== hfSplits[0]) { setDatasetSplit(hfSplits[0]); } else if (!datasetSplit && hfSplits.includes("train")) { @@ -70,8 +70,8 @@ export function HfDatasetConfigSplitSelectors({ } }, [ hfSplits, - hasMultipleConfigs, - datasetConfig, + hasMultipleSubsets, + datasetSubset, datasetSplit, setDatasetSplit, ]); @@ -105,12 +105,12 @@ export function HfDatasetConfigSplitSelectors({ )} - {!isLoading && !error && hasMultipleConfigs && ( + {!isLoading && !error && hasMultipleSubsets && ( <> {variant === "wizard" ? ( - Subset (Config) + Subset