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, };