feat: enhance dataset seed handling with inspection and UI improvements
This commit is contained in:
parent
3b29d088c0
commit
869ac64e18
11 changed files with 695 additions and 256 deletions
|
|
@ -35,3 +35,20 @@ class ValidateResponse(BaseModel):
|
|||
|
||||
class JobCreateResponse(BaseModel):
|
||||
job_id: str
|
||||
|
||||
|
||||
class SeedInspectRequest(BaseModel):
|
||||
dataset_name: str = Field(min_length=1)
|
||||
hf_token: str | None = None
|
||||
subset: str | None = None
|
||||
split: str | None = "train"
|
||||
preview_size: int = Field(default=10, ge=1, le=50)
|
||||
|
||||
|
||||
class SeedInspectResponse(BaseModel):
|
||||
dataset_name: str
|
||||
resolved_path: str
|
||||
columns: list[str] = Field(default_factory=list)
|
||||
preview_rows: list[dict[str, Any]] = Field(default_factory=list)
|
||||
split: str | None = None
|
||||
subset: str | None = None
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Data Recipe routes (DataDesigner runner).
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from itertools import islice
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -18,9 +19,162 @@ if str(backend_path) not in sys.path:
|
|||
|
||||
from core.data_recipe.jobs import get_job_manager
|
||||
from core.data_recipe.service import validate_recipe
|
||||
from models.data_recipe import JobCreateResponse, RecipePayload, ValidateError, ValidateResponse
|
||||
from models.data_recipe import (
|
||||
JobCreateResponse,
|
||||
RecipePayload,
|
||||
SeedInspectRequest,
|
||||
SeedInspectResponse,
|
||||
ValidateError,
|
||||
ValidateResponse,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
DATA_EXTS = (".parquet", ".jsonl", ".json", ".csv")
|
||||
|
||||
|
||||
def _serialize_preview_value(value: Any) -> Any:
|
||||
if value is None or isinstance(value, (str, int, float, bool)):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
return {str(key): _serialize_preview_value(item) for key, item in value.items()}
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [_serialize_preview_value(item) for item in value]
|
||||
return str(value)
|
||||
|
||||
|
||||
def _serialize_preview_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{str(key): _serialize_preview_value(value) for key, value in row.items()}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
def _select_best_file(data_files: list[str], split: str | None) -> str | None:
|
||||
if not data_files:
|
||||
return None
|
||||
if not split:
|
||||
return data_files[0]
|
||||
split_lower = split.lower()
|
||||
|
||||
def score(path: str) -> tuple[int, int]:
|
||||
name = path.lower()
|
||||
if f"/{split_lower}/" in name:
|
||||
return (0, len(path))
|
||||
if (
|
||||
f"_{split_lower}." in name
|
||||
or f"-{split_lower}." in name
|
||||
or f"/{split_lower}." in name
|
||||
or f"/{split_lower}_" in name
|
||||
or f"/{split_lower}-" in name
|
||||
):
|
||||
return (1, len(path))
|
||||
return (2, len(path))
|
||||
|
||||
return sorted(data_files, key=score)[0]
|
||||
|
||||
|
||||
def _resolve_seed_hf_path(dataset_name: str, data_files: list[str], split: str | None) -> str | None:
|
||||
selected = _select_best_file(data_files, split)
|
||||
if not selected:
|
||||
return None
|
||||
|
||||
ext = Path(selected).suffix.lower()
|
||||
if ext not in DATA_EXTS:
|
||||
return f"datasets/{dataset_name}/{selected}"
|
||||
|
||||
parent = Path(selected).parent.as_posix()
|
||||
if not parent or parent == ".":
|
||||
return f"datasets/{dataset_name}/**/*{ext}"
|
||||
return f"datasets/{dataset_name}/{parent}/**/*{ext}"
|
||||
|
||||
|
||||
@router.post("/seed/inspect", response_model=SeedInspectResponse)
|
||||
def inspect_seed_dataset(payload: SeedInspectRequest) -> SeedInspectResponse:
|
||||
dataset_name = payload.dataset_name.strip()
|
||||
if not dataset_name or dataset_name.count("/") < 1:
|
||||
raise HTTPException(status_code=400, detail="dataset_name must be a Hugging Face repo id like org/repo")
|
||||
|
||||
try:
|
||||
from datasets import load_dataset
|
||||
from huggingface_hub import HfApi
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"seed inspect dependencies unavailable: {exc}") from exc
|
||||
|
||||
split = (payload.split or "train").strip() or "train"
|
||||
subset = payload.subset.strip() if payload.subset else None
|
||||
token = payload.hf_token.strip() if payload.hf_token else None
|
||||
preview_size = int(payload.preview_size)
|
||||
|
||||
preview_rows: list[dict[str, Any]] = []
|
||||
data_files: list[str] = []
|
||||
|
||||
try:
|
||||
api = HfApi()
|
||||
repo_files = api.list_repo_files(dataset_name, repo_type="dataset", token=token)
|
||||
data_files = [f for f in repo_files if f.lower().endswith(DATA_EXTS)]
|
||||
except Exception:
|
||||
data_files = []
|
||||
|
||||
selected_file = _select_best_file(data_files, split)
|
||||
if selected_file:
|
||||
try:
|
||||
load_kwargs: dict[str, Any] = {
|
||||
"path": dataset_name,
|
||||
"data_files": [selected_file],
|
||||
"split": "train",
|
||||
"streaming": True,
|
||||
}
|
||||
if subset:
|
||||
load_kwargs["name"] = subset
|
||||
if token:
|
||||
load_kwargs["token"] = token
|
||||
streamed_ds = load_dataset(**load_kwargs)
|
||||
preview_rows = [_row for _row in islice(streamed_ds, preview_size)]
|
||||
except Exception:
|
||||
preview_rows = []
|
||||
|
||||
if not preview_rows:
|
||||
try:
|
||||
load_kwargs = {
|
||||
"path": dataset_name,
|
||||
"split": split,
|
||||
"streaming": True,
|
||||
}
|
||||
if subset:
|
||||
load_kwargs["name"] = subset
|
||||
if token:
|
||||
load_kwargs["token"] = token
|
||||
streamed_ds = load_dataset(**load_kwargs)
|
||||
preview_rows = [_row for _row in islice(streamed_ds, preview_size)]
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=422, detail=f"seed inspect failed: {exc}") from exc
|
||||
|
||||
if not preview_rows:
|
||||
raise HTTPException(status_code=422, detail="dataset appears empty or unreadable")
|
||||
preview_rows = _serialize_preview_rows(preview_rows)
|
||||
|
||||
columns_seen: dict[str, None] = {}
|
||||
for row in preview_rows:
|
||||
for key in row.keys():
|
||||
columns_seen[str(key)] = None
|
||||
columns = list(columns_seen.keys())
|
||||
|
||||
if not data_files:
|
||||
# Best effort path fallback when file list is unavailable.
|
||||
resolved_path = f"datasets/{dataset_name}/**/*.parquet"
|
||||
else:
|
||||
resolved_path = _resolve_seed_hf_path(dataset_name, data_files, split)
|
||||
if not resolved_path:
|
||||
raise HTTPException(status_code=422, detail="unable to resolve seed dataset path")
|
||||
|
||||
return SeedInspectResponse(
|
||||
dataset_name=dataset_name,
|
||||
resolved_path=resolved_path,
|
||||
columns=columns,
|
||||
preview_rows=preview_rows,
|
||||
split=split,
|
||||
subset=subset,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/validate", response_model=ValidateResponse)
|
||||
|
|
|
|||
|
|
@ -70,6 +70,29 @@ export type JobEvent = {
|
|||
payload: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type SeedInspectRequest = {
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
dataset_name: string;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
hf_token?: string;
|
||||
subset?: string;
|
||||
split?: string;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
preview_size?: number;
|
||||
};
|
||||
|
||||
export type SeedInspectResponse = {
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
dataset_name: string;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
resolved_path: string;
|
||||
columns: string[];
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
preview_rows: Record<string, unknown>[];
|
||||
split?: string | null;
|
||||
subset?: string | null;
|
||||
};
|
||||
|
||||
export type ValidateError = {
|
||||
message: string;
|
||||
path?: string | null;
|
||||
|
|
@ -203,6 +226,12 @@ export async function cancelRecipeJob(jobId: string): Promise<JobStatusResponse>
|
|||
return postJson<JobStatusResponse>(`/jobs/${jobId}/cancel`, {});
|
||||
}
|
||||
|
||||
export async function inspectSeedDataset(
|
||||
payload: SeedInspectRequest,
|
||||
): Promise<SeedInspectResponse> {
|
||||
return postJson<SeedInspectResponse>("/seed/inspect", payload);
|
||||
}
|
||||
|
||||
export async function streamRecipeJobEvents(options: {
|
||||
jobId: string;
|
||||
signal: AbortSignal;
|
||||
|
|
|
|||
|
|
@ -170,10 +170,13 @@ function getConfigSummary(config: NodeConfig | undefined): string {
|
|||
}
|
||||
|
||||
if (config.kind === "seed") {
|
||||
if (config.hf_repo_id.trim()) {
|
||||
return config.hf_repo_id.trim();
|
||||
}
|
||||
if (config.hf_path.trim()) {
|
||||
return config.hf_path.trim();
|
||||
}
|
||||
return "Set HF dataset path";
|
||||
return "Set HF dataset repo";
|
||||
}
|
||||
|
||||
return "Open details for config";
|
||||
|
|
|
|||
|
|
@ -113,26 +113,16 @@ export function CategoryDialog({
|
|||
</div>
|
||||
</div>
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<div className="rounded-2xl border border-border/60">
|
||||
<CollapsibleTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between px-3 py-2 text-left"
|
||||
>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
Advanced
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Weights and conditional rules.
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{advancedOpen ? "Hide" : "Show"}
|
||||
</span>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-3 border-t border-border/60 p-3">
|
||||
<CollapsibleTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between text-left text-xs text-muted-foreground"
|
||||
>
|
||||
<span className="font-semibold uppercase">Advanced</span>
|
||||
<span>{advancedOpen ? "Hide" : "Show"}</span>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-2 space-y-3">
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Weights (optional)"
|
||||
|
|
@ -286,8 +276,7 @@ export function CategoryDialog({
|
|||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CollapsibleContent>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,17 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
} from "@/components/ui/combobox";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
|
|
@ -7,6 +20,7 @@ import {
|
|||
EmptyTitle,
|
||||
} from "@/components/ui/empty";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { InputGroupAddon } from "@/components/ui/input-group";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
@ -14,6 +28,7 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -28,7 +43,18 @@ import {
|
|||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/components/ui/tabs";
|
||||
import { type ReactElement, useMemo, useState } from "react";
|
||||
import {
|
||||
useDebouncedValue,
|
||||
useHfDatasetSearch,
|
||||
useHfDatasetSplits,
|
||||
useHfTokenValidation,
|
||||
useInfiniteScroll,
|
||||
} from "@/hooks";
|
||||
import { formatCompact } from "@/lib/utils";
|
||||
import { Search01Icon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { type ReactElement, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { inspectSeedDataset } from "../../api";
|
||||
import type {
|
||||
SeedConfig,
|
||||
SeedSamplingStrategy,
|
||||
|
|
@ -52,26 +78,6 @@ type SeedDialogProps = {
|
|||
onUpdate: (patch: Partial<SeedConfig>) => void;
|
||||
};
|
||||
|
||||
function parseHfDatasetRepoId(input: string): string | null {
|
||||
const raw = input.trim();
|
||||
if (!raw) return null;
|
||||
if (!raw.includes("://") && raw.split("/").length === 2) {
|
||||
return raw;
|
||||
}
|
||||
try {
|
||||
const url = new URL(raw);
|
||||
const parts = url.pathname.split("/").filter(Boolean);
|
||||
const datasetsIdx = parts.indexOf("datasets");
|
||||
if (datasetsIdx === -1) return null;
|
||||
const org = parts[datasetsIdx + 1];
|
||||
const repo = parts[datasetsIdx + 2];
|
||||
if (!org || !repo) return null;
|
||||
return `${org}/${repo}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function stringifyCell(value: unknown): string {
|
||||
if (value === null || value === undefined) return "";
|
||||
if (typeof value === "string") return value;
|
||||
|
|
@ -84,34 +90,145 @@ function stringifyCell(value: unknown): string {
|
|||
}
|
||||
|
||||
export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [inspectError, setInspectError] = useState<string | null>(null);
|
||||
const [previewError, setPreviewError] = useState<string | null>(null);
|
||||
const [isInspecting, setIsInspecting] = useState(false);
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
const [previewRows, setPreviewRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
const repoId = useMemo(
|
||||
() => parseHfDatasetRepoId(config.hf_url ?? ""),
|
||||
[config.hf_url],
|
||||
);
|
||||
const selectingRef = useRef(false);
|
||||
const comboboxAnchorRef = useRef<HTMLDivElement>(null);
|
||||
const debouncedQuery = useDebouncedValue(inputValue);
|
||||
|
||||
const hfToken = config.hf_token?.trim() ?? "";
|
||||
const dataset = config.hf_repo_id.trim();
|
||||
|
||||
const {
|
||||
results: hfResults,
|
||||
isLoading,
|
||||
isLoadingMore,
|
||||
fetchMore,
|
||||
error: hfSearchError,
|
||||
} = useHfDatasetSearch(debouncedQuery, {
|
||||
accessToken: hfToken || undefined,
|
||||
});
|
||||
|
||||
const { error: tokenValidationError, isChecking: isCheckingToken } =
|
||||
useHfTokenValidation(hfToken);
|
||||
|
||||
const { scrollRef, sentinelRef } = useInfiniteScroll(fetchMore, hfResults.length);
|
||||
|
||||
const {
|
||||
subsets,
|
||||
splits,
|
||||
hasMultipleSubsets,
|
||||
hasMultipleSplits,
|
||||
isLoading: splitsLoading,
|
||||
error: splitsError,
|
||||
} = useHfDatasetSplits(dataset || null, config.hf_subset || null, {
|
||||
accessToken: hfToken || undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (subsets.length === 1 && config.hf_subset !== subsets[0]) {
|
||||
onUpdate({ hf_subset: subsets[0] });
|
||||
}
|
||||
}, [subsets, config.hf_subset, onUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (splits.length === 0) return;
|
||||
if (hasMultipleSubsets && !config.hf_subset) return;
|
||||
|
||||
if (splits.length === 1 && config.hf_split !== splits[0]) {
|
||||
onUpdate({ hf_split: splits[0] });
|
||||
return;
|
||||
}
|
||||
if (!config.hf_split && splits.includes("train")) {
|
||||
onUpdate({ hf_split: "train" });
|
||||
return;
|
||||
}
|
||||
if (!config.hf_split) {
|
||||
onUpdate({ hf_split: splits[0] });
|
||||
}
|
||||
}, [
|
||||
splits,
|
||||
hasMultipleSubsets,
|
||||
config.hf_subset,
|
||||
config.hf_split,
|
||||
onUpdate,
|
||||
]);
|
||||
|
||||
const resultIds = useMemo(() => {
|
||||
const ids = hfResults.map((result) => result.id);
|
||||
if (dataset && !ids.includes(dataset)) {
|
||||
ids.push(dataset);
|
||||
}
|
||||
return ids;
|
||||
}, [hfResults, dataset]);
|
||||
|
||||
const pathId = `${config.id}-hf-path`;
|
||||
const urlId = `${config.id}-hf-url`;
|
||||
const tokenId = `${config.id}-hf-token`;
|
||||
const splitId = `${config.id}-hf-split`;
|
||||
const samplingId = `${config.id}-sampling`;
|
||||
const selectionId = `${config.id}-selection`;
|
||||
const tokenId = `${config.id}-hf-token`;
|
||||
const subsetId = `${config.id}-hf-subset`;
|
||||
const splitId = `${config.id}-hf-split`;
|
||||
|
||||
async function onInspect(): Promise<void> {
|
||||
setInspectError("Seed inspect disabled (backend /seed/inspect removed).");
|
||||
}
|
||||
|
||||
async function onPreview(): Promise<void> {
|
||||
setPreviewError("Seed preview disabled (backend /seed/preview removed).");
|
||||
function handleDatasetSelect(id: string | null): void {
|
||||
selectingRef.current = true;
|
||||
const value = id ?? "";
|
||||
onUpdate({
|
||||
hf_repo_id: value,
|
||||
hf_subset: "",
|
||||
hf_split: "",
|
||||
hf_path: "",
|
||||
seed_columns: [],
|
||||
});
|
||||
setInspectError(null);
|
||||
setPreviewRows([]);
|
||||
}
|
||||
|
||||
function handleInputChange(value: string): void {
|
||||
if (selectingRef.current) {
|
||||
selectingRef.current = false;
|
||||
return;
|
||||
}
|
||||
setInputValue(value);
|
||||
}
|
||||
|
||||
async function loadSeedMetadata(): Promise<void> {
|
||||
const datasetName = config.hf_repo_id.trim();
|
||||
if (!datasetName) {
|
||||
setInspectError("Select a dataset first.");
|
||||
return;
|
||||
}
|
||||
|
||||
setInspectError(null);
|
||||
setIsInspecting(true);
|
||||
try {
|
||||
const response = await inspectSeedDataset({
|
||||
dataset_name: datasetName,
|
||||
hf_token: hfToken || undefined,
|
||||
subset: config.hf_subset || undefined,
|
||||
split: config.hf_split || "train",
|
||||
preview_size: 10,
|
||||
});
|
||||
onUpdate({
|
||||
hf_path: response.resolved_path,
|
||||
seed_columns: response.columns,
|
||||
hf_split: response.split ?? config.hf_split ?? "",
|
||||
hf_subset: response.subset ?? config.hf_subset ?? "",
|
||||
});
|
||||
setPreviewRows(response.preview_rows ?? []);
|
||||
} catch (error) {
|
||||
setInspectError(error instanceof Error ? error.message : "Failed to load seed metadata.");
|
||||
setPreviewRows([]);
|
||||
} finally {
|
||||
setIsInspecting(false);
|
||||
}
|
||||
}
|
||||
|
||||
const previewColumns = useMemo(() => {
|
||||
const cols = config.seed_columns ?? [];
|
||||
if (cols.length > 0) return cols;
|
||||
const loadedColumns = config.seed_columns ?? [];
|
||||
if (loadedColumns.length > 0) return loadedColumns;
|
||||
if (previewRows[0]) return Object.keys(previewRows[0]);
|
||||
return [];
|
||||
}, [config.seed_columns, previewRows]);
|
||||
|
|
@ -127,57 +244,134 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
<div className="space-y-4">
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="HF dataset URL"
|
||||
htmlFor={urlId}
|
||||
hint="Dataset URL or org/repo used to bootstrap seed columns."
|
||||
label="Dataset"
|
||||
hint="Search and select a Hugging Face dataset repo (org/repo)."
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id={urlId}
|
||||
className="nodrag flex-1"
|
||||
placeholder="https://huggingface.co/datasets/org/repo"
|
||||
value={config.hf_url ?? ""}
|
||||
onChange={(e) => onUpdate({ hf_url: e.target.value })}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="nodrag"
|
||||
onClick={() => void onInspect()}
|
||||
disabled
|
||||
<div
|
||||
ref={comboboxAnchorRef}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter") return;
|
||||
if (!(event.target instanceof HTMLInputElement)) return;
|
||||
event.preventDefault();
|
||||
if (hfResults.length > 0) {
|
||||
handleDatasetSelect(hfResults[0].id);
|
||||
} else {
|
||||
const text = event.target.value.trim();
|
||||
if (text) handleDatasetSelect(text);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Combobox
|
||||
items={resultIds}
|
||||
filteredItems={resultIds}
|
||||
filter={null}
|
||||
value={dataset || null}
|
||||
onValueChange={handleDatasetSelect}
|
||||
onInputValueChange={handleInputChange}
|
||||
itemToStringValue={(id) => id}
|
||||
autoHighlight={true}
|
||||
>
|
||||
Load (disabled)
|
||||
</Button>
|
||||
<ComboboxInput placeholder="Search datasets..." className="nodrag w-full">
|
||||
<InputGroupAddon>
|
||||
<HugeiconsIcon icon={Search01Icon} className="size-4" />
|
||||
</InputGroupAddon>
|
||||
</ComboboxInput>
|
||||
<ComboboxContent anchor={comboboxAnchorRef}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center gap-2 py-4 text-xs text-muted-foreground">
|
||||
<Spinner className="size-4" /> Searching...
|
||||
</div>
|
||||
) : (
|
||||
<ComboboxEmpty>No datasets found</ComboboxEmpty>
|
||||
)}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="max-h-64 overflow-y-auto overscroll-contain [scrollbar-width:thin]"
|
||||
>
|
||||
<ComboboxList className="p-1 !max-h-none !overflow-visible">
|
||||
{(id: string) => {
|
||||
const result = hfResults.find((entry) => entry.id === id);
|
||||
let detail: string | null = null;
|
||||
if (result?.totalExamples) {
|
||||
detail = `${formatCompact(result.totalExamples)} rows`;
|
||||
} else if (result?.sizeCategory) {
|
||||
detail = result.sizeCategory;
|
||||
} else if (result?.downloads != null) {
|
||||
detail = `↓${formatCompact(result.downloads)}`;
|
||||
}
|
||||
return (
|
||||
<ComboboxItem key={id} value={id} className="justify-between">
|
||||
<span className="min-w-0 flex-1 truncate">{id}</span>
|
||||
{detail && (
|
||||
<span className="shrink-0 text-[10px] text-muted-foreground">{detail}</span>
|
||||
)}
|
||||
</ComboboxItem>
|
||||
);
|
||||
}}
|
||||
</ComboboxList>
|
||||
<div ref={sentinelRef} className="h-px" />
|
||||
{isLoadingMore && (
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<Spinner className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Repo: {repoId ?? "-"}
|
||||
</p>
|
||||
</div>
|
||||
{inspectError && (
|
||||
<p className="text-xs text-red-600">{inspectError}</p>
|
||||
)}
|
||||
|
||||
{(config.seed_splits?.length ?? 0) > 0 && (
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="HF token (optional)"
|
||||
htmlFor={tokenId}
|
||||
hint="Only needed for private or gated datasets."
|
||||
/>
|
||||
<Input
|
||||
id={tokenId}
|
||||
className="nodrag"
|
||||
placeholder="hf_..."
|
||||
value={config.hf_token ?? ""}
|
||||
onChange={(event) => onUpdate({ hf_token: event.target.value })}
|
||||
/>
|
||||
{(tokenValidationError ?? hfSearchError) && (
|
||||
<p className="text-xs text-destructive">{tokenValidationError ?? hfSearchError}</p>
|
||||
)}
|
||||
{isCheckingToken && (
|
||||
<p className="text-xs text-muted-foreground">Checking token…</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{splitsLoading && dataset ? (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Spinner className="size-3.5" /> Loading subsets and splits...
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{splitsError ? (
|
||||
<p className="text-xs text-amber-700 dark:text-amber-400">
|
||||
Could not fetch dataset splits: {splitsError}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{hasMultipleSubsets && (
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Split"
|
||||
htmlFor={splitId}
|
||||
hint="Dataset split to sample from (train/validation/test)."
|
||||
label="Subset"
|
||||
htmlFor={subsetId}
|
||||
hint="Pick a dataset subset/config when multiple are available."
|
||||
/>
|
||||
<Select
|
||||
value={config.hf_split ?? ""}
|
||||
onValueChange={(value) => {
|
||||
const nextPath = config.seed_globs_by_split?.[value] ?? "";
|
||||
onUpdate({ hf_split: value, hf_path: nextPath || config.hf_path });
|
||||
}}
|
||||
value={config.hf_subset ?? ""}
|
||||
onValueChange={(value) => onUpdate({ hf_subset: value || "", hf_split: "" })}
|
||||
>
|
||||
<SelectTrigger className="nodrag w-full" id={splitId}>
|
||||
<SelectValue placeholder="Select split" />
|
||||
<SelectTrigger className="nodrag w-full" id={subsetId}>
|
||||
<SelectValue placeholder="Select subset" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(config.seed_splits ?? []).map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{value}
|
||||
{subsets.map((subset) => (
|
||||
<SelectItem key={subset} value={subset}>
|
||||
{subset}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
|
@ -185,149 +379,143 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="HF path (auto)"
|
||||
htmlFor={pathId}
|
||||
hint="Resolved dataset file path/pattern."
|
||||
/>
|
||||
<Input
|
||||
id={pathId}
|
||||
className="nodrag"
|
||||
placeholder="datasets/org/repo/data/train-*.parquet"
|
||||
value={config.hf_path}
|
||||
onChange={(e) => onUpdate({ hf_path: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="HF token (optional)"
|
||||
htmlFor={tokenId}
|
||||
hint="Optional private dataset access token."
|
||||
/>
|
||||
<Input
|
||||
id={tokenId}
|
||||
className="nodrag"
|
||||
placeholder="hf_..."
|
||||
value={config.hf_token ?? ""}
|
||||
onChange={(e) => onUpdate({ hf_token: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Sampling strategy"
|
||||
htmlFor={samplingId}
|
||||
hint="Ordered keeps row order. shuffle randomizes sampled rows."
|
||||
/>
|
||||
<Select
|
||||
value={config.sampling_strategy}
|
||||
onValueChange={(value) =>
|
||||
onUpdate({ sampling_strategy: value as SeedSamplingStrategy })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="nodrag w-full" id={samplingId}>
|
||||
<SelectValue placeholder="Select sampling" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SAMPLING_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Selection strategy"
|
||||
htmlFor={selectionId}
|
||||
hint="Select all, a row range, or partition block."
|
||||
/>
|
||||
<Select
|
||||
value={config.selection_type}
|
||||
onValueChange={(value) =>
|
||||
onUpdate({ selection_type: value as SeedSelectionType })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="nodrag w-full" id={selectionId}>
|
||||
<SelectValue placeholder="Select selection" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SELECTION_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{config.selection_type === "index_range" && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Start"
|
||||
hint="Inclusive start row index for index_range."
|
||||
/>
|
||||
<Input
|
||||
className="nodrag"
|
||||
inputMode="numeric"
|
||||
value={config.selection_start ?? ""}
|
||||
onChange={(e) => onUpdate({ selection_start: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="End"
|
||||
hint="Inclusive end row index for index_range."
|
||||
/>
|
||||
<Input
|
||||
className="nodrag"
|
||||
inputMode="numeric"
|
||||
value={config.selection_end ?? ""}
|
||||
onChange={(e) => onUpdate({ selection_end: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
{hasMultipleSplits && (
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Split"
|
||||
htmlFor={splitId}
|
||||
hint="Pick split used for preview sampling."
|
||||
/>
|
||||
<Select
|
||||
value={config.hf_split ?? ""}
|
||||
onValueChange={(value) => onUpdate({ hf_split: value || "" })}
|
||||
>
|
||||
<SelectTrigger className="nodrag w-full" id={splitId}>
|
||||
<SelectValue placeholder="Select split" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{splits.map((split) => (
|
||||
<SelectItem key={split} value={split}>
|
||||
{split}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config.selection_type === "partition_block" && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Index"
|
||||
hint="Partition index to load."
|
||||
/>
|
||||
<Input
|
||||
className="nodrag"
|
||||
inputMode="numeric"
|
||||
value={config.selection_index ?? ""}
|
||||
onChange={(e) => onUpdate({ selection_index: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Partitions"
|
||||
hint="Total number of partitions."
|
||||
/>
|
||||
<Input
|
||||
className="nodrag"
|
||||
inputMode="numeric"
|
||||
value={config.selection_num_partitions ?? ""}
|
||||
onChange={(e) =>
|
||||
onUpdate({ selection_num_partitions: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{inspectError && <p className="text-xs text-red-600">{inspectError}</p>}
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Seed columns auto-add. Reference by name (ex: {"{{ rubrics }}"}).
|
||||
</p>
|
||||
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
||||
<CollapsibleTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between text-left text-xs text-muted-foreground"
|
||||
>
|
||||
<span className="font-semibold uppercase">Advanced</span>
|
||||
<span>{advancedOpen ? "Hide" : "Show"}</span>
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-2 space-y-3">
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Sampling strategy"
|
||||
htmlFor={samplingId}
|
||||
hint="Ordered keeps row order. Shuffle randomizes sampled rows."
|
||||
/>
|
||||
<Select
|
||||
value={config.sampling_strategy}
|
||||
onValueChange={(value) =>
|
||||
onUpdate({ sampling_strategy: value as SeedSamplingStrategy })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="nodrag w-full" id={samplingId}>
|
||||
<SelectValue placeholder="Select sampling" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SAMPLING_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel
|
||||
label="Selection strategy"
|
||||
htmlFor={selectionId}
|
||||
hint="Select all, a row range, or partition block."
|
||||
/>
|
||||
<Select
|
||||
value={config.selection_type}
|
||||
onValueChange={(value) =>
|
||||
onUpdate({ selection_type: value as SeedSelectionType })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="nodrag w-full" id={selectionId}>
|
||||
<SelectValue placeholder="Select selection" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SELECTION_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{config.selection_type === "index_range" && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel label="Start" hint="Inclusive start row index for index_range." />
|
||||
<Input
|
||||
className="nodrag"
|
||||
inputMode="numeric"
|
||||
value={config.selection_start ?? ""}
|
||||
onChange={(event) => onUpdate({ selection_start: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel label="End" hint="Inclusive end row index for index_range." />
|
||||
<Input
|
||||
className="nodrag"
|
||||
inputMode="numeric"
|
||||
value={config.selection_end ?? ""}
|
||||
onChange={(event) => onUpdate({ selection_end: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config.selection_type === "partition_block" && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel label="Index" hint="Partition index to load." />
|
||||
<Input
|
||||
className="nodrag"
|
||||
inputMode="numeric"
|
||||
value={config.selection_index ?? ""}
|
||||
onChange={(event) => onUpdate({ selection_index: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<FieldLabel label="Partitions" hint="Total number of partitions." />
|
||||
<Input
|
||||
className="nodrag"
|
||||
inputMode="numeric"
|
||||
value={config.selection_num_partitions ?? ""}
|
||||
onChange={(event) =>
|
||||
onUpdate({ selection_num_partitions: event.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
|
|
@ -337,9 +525,9 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
<div className="flex w-full items-center justify-center">
|
||||
<Empty className="max-w-lg">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Preview samples</EmptyTitle>
|
||||
<EmptyTitle>Seed preview</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Load 10 rows to see columns and sample values.
|
||||
Click load to fetch 10 rows from the selected dataset.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
|
|
@ -347,10 +535,10 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
type="button"
|
||||
variant="outline"
|
||||
className="nodrag"
|
||||
onClick={() => void onPreview()}
|
||||
disabled
|
||||
onClick={() => void loadSeedMetadata()}
|
||||
disabled={isInspecting || !dataset}
|
||||
>
|
||||
Load 10 rows (disabled)
|
||||
{isInspecting ? "Loading..." : "Load 10 rows"}
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
|
|
@ -362,28 +550,28 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
type="button"
|
||||
variant="outline"
|
||||
className="nodrag"
|
||||
onClick={() => void onPreview()}
|
||||
disabled
|
||||
onClick={() => void loadSeedMetadata()}
|
||||
disabled={isInspecting || !dataset}
|
||||
>
|
||||
Reload 10 rows (disabled)
|
||||
{isInspecting ? "Loading..." : "Reload 10 rows"}
|
||||
</Button>
|
||||
</div>
|
||||
<Table className="border border-border/60 rounded-xl">
|
||||
<Table className="rounded-xl border border-border/60">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{previewColumns.map((col) => (
|
||||
<TableHead key={col} className="max-w-[260px]">
|
||||
{col}
|
||||
{previewColumns.map((column) => (
|
||||
<TableHead key={column} className="max-w-[260px]">
|
||||
{column}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{previewRows.map((row, idx) => (
|
||||
<TableRow key={idx}>
|
||||
{previewColumns.map((col) => (
|
||||
<TableCell key={col} className="max-w-[260px]">
|
||||
<div className="truncate">{stringifyCell(row[col])}</div>
|
||||
{previewRows.map((row, index) => (
|
||||
<TableRow key={index}>
|
||||
{previewColumns.map((column) => (
|
||||
<TableCell key={column} className="max-w-[260px]">
|
||||
<div className="truncate">{stringifyCell(row[column])}</div>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
|
|
@ -392,7 +580,7 @@ export function SeedDialog({ config, onUpdate }: SeedDialogProps): ReactElement
|
|||
</Table>
|
||||
</div>
|
||||
)}
|
||||
{previewError && <p className="text-xs text-red-600">{previewError}</p>}
|
||||
{inspectError && <p className="text-xs text-red-600">{inspectError}</p>}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { InformationCircleIcon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
type FieldLabelProps = {
|
||||
label: string;
|
||||
htmlFor?: string;
|
||||
hint?: string;
|
||||
};
|
||||
|
||||
export function FieldLabel({
|
||||
label,
|
||||
htmlFor,
|
||||
hint,
|
||||
}: FieldLabelProps): ReactElement {
|
||||
return (
|
||||
<label
|
||||
className="flex items-center gap-1.5 text-xs font-semibold uppercase text-muted-foreground"
|
||||
htmlFor={htmlFor}
|
||||
>
|
||||
<span>{label}</span>
|
||||
{hint && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex size-3.5 items-center justify-center rounded-full text-muted-foreground/80 hover:text-foreground"
|
||||
aria-label={`More info: ${label}`}
|
||||
>
|
||||
<HugeiconsIcon icon={InformationCircleIcon} className="size-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{hint}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -220,8 +220,8 @@ export type SeedConfig = {
|
|||
name: string;
|
||||
drop?: boolean;
|
||||
// ui-only (serialized in seed_config)
|
||||
hf_url?: string;
|
||||
hf_repo_id?: string;
|
||||
hf_repo_id: string;
|
||||
hf_subset?: string;
|
||||
hf_split?: string;
|
||||
hf_path: string;
|
||||
hf_token?: string;
|
||||
|
|
|
|||
|
|
@ -285,8 +285,8 @@ export function makeSeedConfig(
|
|||
kind: "seed",
|
||||
name: nextName(existing, "seed"),
|
||||
drop: false,
|
||||
hf_url: "",
|
||||
hf_repo_id: "",
|
||||
hf_subset: "",
|
||||
hf_split: "",
|
||||
hf_path: "",
|
||||
hf_token: "",
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ function makeDefaultSeedConfig(id: string): SeedConfig {
|
|||
kind: "seed",
|
||||
name: "seed",
|
||||
drop: false,
|
||||
hf_url: "",
|
||||
hf_repo_id: "",
|
||||
hf_subset: "",
|
||||
hf_split: "",
|
||||
hf_path: "",
|
||||
hf_token: "",
|
||||
|
|
@ -35,6 +35,19 @@ function makeDefaultSeedConfig(id: string): SeedConfig {
|
|||
};
|
||||
}
|
||||
|
||||
function inferRepoIdFromSeedPath(path: string): string {
|
||||
const trimmed = path.trim();
|
||||
if (!trimmed) return "";
|
||||
const parts = trimmed.split("/").filter(Boolean);
|
||||
if (parts.length >= 3 && parts[0] === "datasets") {
|
||||
return `${parts[1]}/${parts[2]}`;
|
||||
}
|
||||
if (parts.length >= 2) {
|
||||
return `${parts[0]}/${parts[1]}`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function parseSeedSettings(seedConfigRaw: unknown): Partial<SeedConfig> {
|
||||
if (!isRecord(seedConfigRaw)) {
|
||||
return {};
|
||||
|
|
@ -45,11 +58,13 @@ function parseSeedSettings(seedConfigRaw: unknown): Partial<SeedConfig> {
|
|||
let hf_path = "";
|
||||
let hf_token = "";
|
||||
let hf_endpoint = "https://huggingface.co";
|
||||
let hf_repo_id = "";
|
||||
const sourceRaw = seedConfigRaw.source;
|
||||
if (isRecord(sourceRaw) && readString(sourceRaw.seed_type) === "hf") {
|
||||
hf_path = readString(sourceRaw.path) ?? "";
|
||||
hf_token = readString(sourceRaw.token) ?? "";
|
||||
hf_endpoint = readString(sourceRaw.endpoint) ?? hf_endpoint;
|
||||
hf_repo_id = inferRepoIdFromSeedPath(hf_path);
|
||||
}
|
||||
|
||||
let selection_type: SeedSelectionType = "none";
|
||||
|
|
@ -77,6 +92,7 @@ function parseSeedSettings(seedConfigRaw: unknown): Partial<SeedConfig> {
|
|||
}
|
||||
|
||||
return {
|
||||
hf_repo_id,
|
||||
hf_path,
|
||||
hf_token,
|
||||
hf_endpoint,
|
||||
|
|
|
|||
|
|
@ -180,8 +180,11 @@ export function getConfigErrors(config: NodeConfig | null): string[] {
|
|||
}
|
||||
}
|
||||
if (config.kind === "seed") {
|
||||
if (!config.hf_repo_id.trim()) {
|
||||
errors.push("Seed dataset repo is required.");
|
||||
}
|
||||
if (!config.hf_path.trim()) {
|
||||
errors.push("HF dataset path is required.");
|
||||
errors.push("Seed metadata not loaded. Click 'Load columns + 10 rows'.");
|
||||
}
|
||||
if (config.hf_endpoint?.trim() && !config.hf_endpoint.trim().startsWith("http")) {
|
||||
errors.push("HF endpoint must start with http.");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue