fix hf cfg/split ui

This commit is contained in:
Shine1i 2026-02-15 20:53:59 +01:00
commit ef9e5ffc33
5 changed files with 315 additions and 265 deletions

View file

@ -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() {
</div>
</Field>
{/* Config & Split selectors */}
{dataset && splitsLoading && (
<div className="flex items-center gap-2 text-xs text-muted-foreground py-1">
<Spinner className="size-3.5" />
Loading dataset configs and splits...
</div>
)}
{dataset && splitsError && (
<div className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-400">
Could not fetch dataset splits: {splitsError}
</div>
)}
{dataset && !splitsLoading && !splitsError && hasMultipleConfigs && (
<Field>
<FieldLabel className="flex items-center gap-1.5">
Subset (Config)
<Tooltip>
<TooltipTrigger asChild={true}>
<button
type="button"
className="text-muted-foreground/50 hover:text-muted-foreground"
>
<HugeiconsIcon
icon={InformationCircleIcon}
className="size-3.5"
/>
</button>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
This dataset has multiple subsets (configurations).
Select which one to use for training.
</TooltipContent>
</Tooltip>
</FieldLabel>
<Select
value={datasetConfig ?? ""}
onValueChange={(v) => setDatasetConfig(v || null)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a subset..." />
</SelectTrigger>
<SelectContent>
{hfConfigs.map((cfg) => (
<SelectItem key={cfg} value={cfg}>
{cfg}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
)}
{dataset && !splitsLoading && !splitsError && hasMultipleSplits && (
<Field>
<FieldLabel className="flex items-center gap-1.5">
Split
<Tooltip>
<TooltipTrigger asChild={true}>
<button
type="button"
className="text-muted-foreground/50 hover:text-muted-foreground"
>
<HugeiconsIcon
icon={InformationCircleIcon}
className="size-3.5"
/>
</button>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
Select which split of the dataset to use for training.
</TooltipContent>
</Tooltip>
</FieldLabel>
<Select
value={datasetSplit ?? ""}
onValueChange={(v) => setDatasetSplit(v || null)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a split..." />
</SelectTrigger>
<SelectContent>
{hfSplits.map((split) => (
<SelectItem key={split} value={split}>
{split}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
)}
<HfDatasetConfigSplitSelectors
variant="wizard"
enabled={datasetSource === "huggingface"}
datasetName={dataset}
accessToken={hfToken || undefined}
datasetConfig={datasetConfig}
setDatasetConfig={setDatasetConfig}
datasetSplit={datasetSplit}
setDatasetSplit={setDatasetSplit}
/>
</>
) : (
<>

View file

@ -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<HTMLDivElement>(null);
const { scrollRef, sentinelRef } = useInfiniteScroll(
fetchMore,
@ -258,104 +236,16 @@ export function DatasetSection() {
</div>
</div>
{/* Config & Split selectors - shown after dataset is selected */}
{dataset && !splitsLoading && !splitsError && (hasMultipleConfigs || hasMultipleSplits) && (
<div className="flex flex-col gap-3 rounded-lg border bg-muted/20 px-3.5 py-3">
{hasMultipleConfigs && (
<div className="flex flex-col gap-1.5">
<span className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
Subset (Config)
<Tooltip>
<TooltipTrigger asChild={true}>
<button
type="button"
className="text-foreground/70 hover:text-foreground"
>
<HugeiconsIcon
icon={InformationCircleIcon}
className="size-3"
/>
</button>
</TooltipTrigger>
<TooltipContent>
This dataset has multiple subsets (configurations).
Select which one to use for training.
</TooltipContent>
</Tooltip>
</span>
<Select
value={datasetConfig ?? ""}
onValueChange={(v) => setDatasetConfig(v || null)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a subset..." />
</SelectTrigger>
<SelectContent>
{hfConfigs.map((cfg) => (
<SelectItem key={cfg} value={cfg}>
{cfg}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{hasMultipleSplits && (
<div className="flex flex-col gap-1.5">
<span className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
Split
<Tooltip>
<TooltipTrigger asChild={true}>
<button
type="button"
className="text-foreground/70 hover:text-foreground"
>
<HugeiconsIcon
icon={InformationCircleIcon}
className="size-3"
/>
</button>
</TooltipTrigger>
<TooltipContent>
Select which split of the dataset to use for training.
</TooltipContent>
</Tooltip>
</span>
<Select
value={datasetSplit ?? ""}
onValueChange={(v) => setDatasetSplit(v || null)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a split..." />
</SelectTrigger>
<SelectContent>
{hfSplits.map((split) => (
<SelectItem key={split} value={split}>
{split}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
)}
{/* Loading indicator for splits */}
{dataset && splitsLoading && (
<div className="flex items-center gap-2 rounded-lg border bg-muted/20 px-3.5 py-3 text-xs text-muted-foreground">
<Spinner className="size-3.5" />
Loading dataset configs and splits...
</div>
)}
{/* Error fetching splits */}
{dataset && splitsError && (
<div className="rounded-lg border border-amber-200 bg-amber-50 px-3.5 py-2.5 text-xs text-amber-700 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-400">
Could not fetch dataset splits: {splitsError}
</div>
)}
<HfDatasetConfigSplitSelectors
variant="studio"
enabled={!!dataset && !isLikelyLocalDatasetRef(dataset)}
datasetName={dataset}
accessToken={hfToken || undefined}
datasetConfig={datasetConfig}
setDatasetConfig={setDatasetConfig}
datasetSplit={datasetSplit}
setDatasetSplit={setDatasetSplit}
/>
<div className="flex flex-col gap-2">
<span className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">

View file

@ -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 && (
<div
className={
variant === "wizard"
? "flex items-center gap-2 text-xs text-muted-foreground py-1"
: "flex items-center gap-2 rounded-lg border bg-muted/20 px-3.5 py-3 text-xs text-muted-foreground"
}
>
<Spinner className="size-3.5" />
Loading dataset configs and splits...
</div>
)}
{error && (
<div
className={
variant === "wizard"
? "rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-400"
: "rounded-lg border border-amber-200 bg-amber-50 px-3.5 py-2.5 text-xs text-amber-700 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-400"
}
>
Could not fetch dataset splits: {error}
</div>
)}
{!isLoading && !error && hasMultipleConfigs && (
<>
{variant === "wizard" ? (
<Field>
<FieldLabel className="flex items-center gap-1.5">
Subset (Config)
<Tooltip>
<TooltipTrigger asChild={true}>
<button
type="button"
className="text-muted-foreground/50 hover:text-muted-foreground"
>
<HugeiconsIcon
icon={InformationCircleIcon}
className="size-3.5"
/>
</button>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
This dataset has multiple subsets (configurations). Select
which one to use for training.
</TooltipContent>
</Tooltip>
</FieldLabel>
<Select
value={datasetConfig ?? ""}
onValueChange={(v) => setDatasetConfig(v || null)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a subset..." />
</SelectTrigger>
<SelectContent>
{hfConfigs.map((cfg) => (
<SelectItem key={cfg} value={cfg}>
{cfg}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
) : (
<div className="flex flex-col gap-1.5">
<span className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
Subset (Config)
<Tooltip>
<TooltipTrigger asChild={true}>
<button
type="button"
className="text-foreground/70 hover:text-foreground"
>
<HugeiconsIcon
icon={InformationCircleIcon}
className="size-3"
/>
</button>
</TooltipTrigger>
<TooltipContent>
This dataset has multiple subsets (configurations). Select
which one to use for training.
</TooltipContent>
</Tooltip>
</span>
<Select
value={datasetConfig ?? ""}
onValueChange={(v) => setDatasetConfig(v || null)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a subset..." />
</SelectTrigger>
<SelectContent>
{hfConfigs.map((cfg) => (
<SelectItem key={cfg} value={cfg}>
{cfg}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</>
)}
{!isLoading && !error && hasMultipleSplits && (
<>
{variant === "wizard" ? (
<Field>
<FieldLabel className="flex items-center gap-1.5">
Split
<Tooltip>
<TooltipTrigger asChild={true}>
<button
type="button"
className="text-muted-foreground/50 hover:text-muted-foreground"
>
<HugeiconsIcon
icon={InformationCircleIcon}
className="size-3.5"
/>
</button>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
Select which split of the dataset to use for training.
</TooltipContent>
</Tooltip>
</FieldLabel>
<Select
value={datasetSplit ?? ""}
onValueChange={(v) => setDatasetSplit(v || null)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a split..." />
</SelectTrigger>
<SelectContent>
{hfSplits.map((split) => (
<SelectItem key={split} value={split}>
{split}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
) : (
<div className="flex flex-col gap-1.5">
<span className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
Split
<Tooltip>
<TooltipTrigger asChild={true}>
<button
type="button"
className="text-foreground/70 hover:text-foreground"
>
<HugeiconsIcon
icon={InformationCircleIcon}
className="size-3"
/>
</button>
</TooltipTrigger>
<TooltipContent>
Select which split of the dataset to use for training.
</TooltipContent>
</Tooltip>
</span>
<Select
value={datasetSplit ?? ""}
onValueChange={(v) => setDatasetSplit(v || null)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a split..." />
</SelectTrigger>
<SelectContent>
{hfSplits.map((split) => (
<SelectItem key={split} value={split}>
{split}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</>
)}
</>
);
}

View file

@ -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";

View file

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