feat: implement dataset mapping UI and preview dialog

This commit is contained in:
Shine1i 2026-02-16 01:44:26 +01:00
commit 4fd4d2fe76
15 changed files with 576 additions and 137 deletions

View file

@ -0,0 +1,162 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { AlertCircleIcon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import type { CheckFormatResponse } from "@/features/training/types/datasets";
export function HeaderPick({
label,
checked,
onCheckedChange,
}: {
label: string;
checked: boolean;
onCheckedChange: (checked: boolean) => void;
}) {
return (
<label className="flex items-center gap-1.5 text-[10px] text-muted-foreground cursor-pointer select-none">
<Checkbox
checked={checked}
onCheckedChange={(v) => onCheckedChange(v === true)}
aria-label={label}
className="h-3.5 w-3.5"
/>
<span>{label}</span>
</label>
);
}
export function DatasetMappingCard({
leftLabel,
rightLabel,
mappingOk,
input,
output,
}: {
leftLabel: string;
rightLabel: string;
mappingOk: boolean;
input: string | null;
output: string | null;
}) {
return (
<div className="rounded-xl corner-squircle ring-1 ring-amber-200/70 bg-amber-50/70 px-5 py-4 mb-4 text-amber-950 dark:ring-amber-900/50 dark:bg-amber-950/30 dark:text-amber-50">
<div className="flex items-start gap-3">
<div className="rounded-xl corner-squircle bg-amber-500/15 p-2 shrink-0">
<HugeiconsIcon
icon={AlertCircleIcon}
className="size-4 text-amber-700 dark:text-amber-300"
/>
</div>
<div className="min-w-0">
<p className="text-sm font-semibold tracking-tight">Map dataset columns</p>
<p className="text-xs text-amber-800/80 dark:text-amber-200/80 mt-0.5">
We couldn&apos;t auto-detect the format. Pick the{" "}
{leftLabel.toLowerCase()} column and the {rightLabel.toLowerCase()}{" "}
column. We&apos;ll convert it to a supported format automatically.
</p>
<div className="mt-3 flex flex-wrap items-center gap-2">
<Badge
variant="outline"
className="h-6 text-[11px] bg-white/60 dark:bg-transparent"
>
{leftLabel}: <span className="font-mono">{input ?? "--"}</span>
</Badge>
<Badge
variant="outline"
className="h-6 text-[11px] bg-white/60 dark:bg-transparent"
>
{rightLabel}: <span className="font-mono">{output ?? "--"}</span>
</Badge>
</div>
{!mappingOk && (
<p className="mt-2 text-xs text-amber-800/80 dark:text-amber-200/80">
Select exactly 1 {leftLabel.toLowerCase()} and 1{" "}
{rightLabel.toLowerCase()} column to continue.
</p>
)}
</div>
</div>
</div>
);
}
export function DatasetMappingFooter({
leftLabel,
rightLabel,
mappingOk,
isStarting,
startError,
onCancel,
onStartTraining,
}: {
leftLabel: string;
rightLabel: string;
mappingOk: boolean;
isStarting: boolean;
startError: string | null;
onCancel: () => void;
onStartTraining: () => Promise<void>;
}) {
return (
<div className="mt-3 flex flex-col gap-2">
<div className="flex items-center justify-between gap-3">
<p className="text-[11px] text-muted-foreground/70 leading-relaxed">
Tip: you can click {leftLabel} / {rightLabel} in the headers.
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="cursor-pointer"
onClick={onCancel}
>
Cancel
</Button>
<Button
size="sm"
className="cursor-pointer"
disabled={!mappingOk || isStarting}
onClick={() => void onStartTraining()}
>
{isStarting ? "Starting..." : "Continue"}
</Button>
</div>
</div>
{startError && (
<p className="text-xs text-red-500 leading-relaxed text-center">
{startError}
</p>
)}
</div>
);
}
export function deriveDefaultMapping(
data: CheckFormatResponse,
isVlm: boolean,
): { input: string | null; output: string | null } {
if (isVlm) {
const input =
data.detected_image_column ?? pickRole(data.suggested_mapping, "image");
const output =
data.detected_text_column ?? pickRole(data.suggested_mapping, "text");
return { input: input ?? null, output: output ?? null };
}
const input = pickRole(data.suggested_mapping, "user");
const output = pickRole(data.suggested_mapping, "assistant");
return { input: input ?? null, output: output ?? null };
}
function pickRole(
mapping: Record<string, string> | null | undefined,
role: string,
): string | null {
if (!mapping) return null;
for (const [col, mappedRole] of Object.entries(mapping)) {
if (mappedRole === role) return col;
}
return null;
}

View file

@ -0,0 +1,55 @@
type PreviewImagePayload = {
type: "image";
mime?: string;
width?: number;
height?: number;
data?: string;
};
export function formatCell(value: unknown): string {
if (value == null) return "";
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") return String(value);
if (Array.isArray(value) || typeof value === "object")
return JSON.stringify(value).slice(0, 500);
return String(value);
}
function isPreviewImagePayload(value: unknown): value is PreviewImagePayload {
if (!value || typeof value !== "object") return false;
const record = value as Record<string, unknown>;
return (
record.type === "image" &&
typeof record.data === "string" &&
record.data.length > 0
);
}
export function collectPreviewImages(value: unknown): PreviewImagePayload[] {
const images: PreviewImagePayload[] = [];
const stack: unknown[] = [value];
let steps = 0;
while (stack.length > 0 && steps < 200) {
steps += 1;
const current = stack.pop();
if (isPreviewImagePayload(current)) {
images.push(current);
continue;
}
if (Array.isArray(current)) {
for (const item of current) stack.push(item);
continue;
}
if (current && typeof current === "object") {
for (const nested of Object.values(current as Record<string, unknown>)) {
stack.push(nested);
}
}
}
return images;
}

View file

@ -11,30 +11,21 @@ import { Spinner } from "@/components/ui/spinner";
import { Database02Icon, AlertCircleIcon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { type ReactNode, useEffect, useMemo, useState } from "react";
import { useTrainingActions, useTrainingConfigStore } from "@/features/training";
import { checkDatasetFormat } from "@/features/training/api/datasets-api";
import type { CheckFormatResponse } from "@/features/training/types/datasets";
import { collectPreviewImages, formatCell } from "./dataset-preview-dialog-utils";
import {
DatasetMappingCard,
DatasetMappingFooter,
HeaderPick,
deriveDefaultMapping,
} from "./dataset-preview-dialog-mapping";
// ---------------------------------------------------------------------------
// Types (matches CheckFormatResponse from backend)
// Types
// ---------------------------------------------------------------------------
type CheckFormatResponse = {
requires_manual_mapping: boolean;
detected_format: string;
columns: string[];
suggested_mapping?: Record<string, string> | null;
detected_image_column?: string | null;
detected_text_column?: string | null;
preview_samples?: Record<string, unknown>[] | null;
total_rows?: number | null;
};
type PreviewImagePayload = {
type: "image";
mime?: string;
width?: number;
height?: number;
data?: string;
};
type DatasetPreviewDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
@ -42,37 +33,11 @@ type DatasetPreviewDialogProps = {
hfToken: string | null;
datasetSubset?: string | null;
datasetSplit?: string | null;
mode?: "preview" | "mapping";
initialData?: CheckFormatResponse | null;
isVlm?: boolean;
};
// ---------------------------------------------------------------------------
// 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,
subset?: string | null,
split?: string | null,
): Promise<CheckFormatResponse> {
const res = await fetch("/api/datasets/check-format", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
dataset_name: datasetName,
hf_token: hfToken || undefined,
config: subset || undefined,
split: split || "train",
}),
});
if (!res.ok) {
const body = await res.json().catch(() => null);
throw new Error(body?.detail || `Request failed (${res.status})`);
}
return res.json();
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
@ -84,22 +49,47 @@ export function DatasetPreviewDialog({
hfToken,
datasetSubset,
datasetSplit,
mode = "preview",
initialData,
isVlm = false,
}: DatasetPreviewDialogProps) {
const [data, setData] = useState<CheckFormatResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const manualMapping = useTrainingConfigStore((s) => s.datasetManualMapping);
const setManualMapping = useTrainingConfigStore((s) => s.setDatasetManualMapping);
const { isStarting, startError, startTrainingRun } = useTrainingActions();
const mappingEnabled = !!data?.requires_manual_mapping;
const showMappingFooter = mode === "mapping" && mappingEnabled;
const mappingOk = !!manualMapping.input && !!manualMapping.output;
const leftLabel = isVlm ? "Image" : "Input";
const rightLabel = isVlm ? "Text" : "Output";
useEffect(() => {
if (!open || !datasetName) {
setData(null);
setError(null);
return;
}
if (initialData) {
setData(initialData);
setError(null);
setLoading(false);
return;
}
let cancelled = false;
setLoading(true);
setError(null);
fetchCheckFormat(datasetName, hfToken, datasetSubset, datasetSplit)
checkDatasetFormat({
datasetName,
hfToken,
subset: datasetSubset,
split: datasetSplit,
isVlm,
})
.then((res) => {
if (!cancelled) {
setData(res);
@ -116,7 +106,24 @@ export function DatasetPreviewDialog({
return () => {
cancelled = true;
};
}, [open, datasetName, hfToken, datasetSubset, datasetSplit]);
}, [open, datasetName, hfToken, datasetSubset, datasetSplit, isVlm, initialData]);
useEffect(() => {
if (!open || !datasetName || !data?.requires_manual_mapping) return;
if (manualMapping.input || manualMapping.output) return;
const derived = deriveDefaultMapping(data, isVlm);
if (!derived.input && !derived.output) return;
setManualMapping(derived);
}, [
open,
datasetName,
data?.requires_manual_mapping,
isVlm,
manualMapping.input,
manualMapping.output,
setManualMapping,
data,
]);
const rows = data?.preview_samples ?? [];
const columns = data?.columns ?? [];
@ -140,9 +147,39 @@ export function DatasetPreviewDialog({
return columns.map((colName) => ({
accessorKey: colName,
header: () => (
<span className="font-heading text-[13px] font-semibold tracking-tight text-foreground">
{colName}
</span>
<div className="flex flex-col gap-2">
<span className="font-heading text-[13px] font-semibold tracking-tight text-foreground">
{colName}
</span>
{mappingEnabled && (
<div className="flex items-center gap-3">
{(manualMapping.input == null || manualMapping.input === colName) && (
<HeaderPick
label={leftLabel}
checked={manualMapping.input === colName}
onCheckedChange={(checked) => {
setManualMapping({
input: checked ? colName : null,
output: manualMapping.output,
});
}}
/>
)}
{(manualMapping.output == null || manualMapping.output === colName) && (
<HeaderPick
label={rightLabel}
checked={manualMapping.output === colName}
onCheckedChange={(checked) => {
setManualMapping({
input: manualMapping.input,
output: checked ? colName : null,
});
}}
/>
)}
</div>
)}
</div>
),
cell: ({ getValue }: { getValue: () => unknown }) => {
const value = getValue();
@ -196,7 +233,15 @@ export function DatasetPreviewDialog({
);
},
}));
}, [columns]);
}, [
columns,
manualMapping.input,
manualMapping.output,
setManualMapping,
mappingEnabled,
leftLabel,
rightLabel,
]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@ -266,10 +311,10 @@ export function DatasetPreviewDialog({
: "--"
}
/>
<MetaRow
label="Columns"
value={
<span className="flex items-center gap-1.5 flex-wrap">
<MetaRow
label="Columns"
value={
<span className="flex items-center gap-1.5 flex-wrap">
{columns.map((col) => (
<Badge
key={col}
@ -284,18 +329,51 @@ export function DatasetPreviewDialog({
/>
</div>
{mappingEnabled && (
<DatasetMappingCard
leftLabel={leftLabel}
rightLabel={rightLabel}
mappingOk={mappingOk}
input={manualMapping.input}
output={manualMapping.output}
/>
)}
{/* Data table */}
<div className="flex-1 min-h-0 rounded-xl corner-squircle ring-1 ring-border/60 overflow-auto">
<DataTable columns={tableColumns} data={rows} />
</div>
{/* Footer */}
<p className="text-[11px] text-muted-foreground/60 mt-3 text-center tabular-nums">
Showing {rows.length}
{data.total_rows != null &&
` of ${data.total_rows.toLocaleString()}`}{" "}
rows
</p>
<div className="mt-3">
<p className="text-[11px] text-muted-foreground/60 text-center tabular-nums">
Showing {rows.length}
{data.total_rows != null &&
` of ${data.total_rows.toLocaleString()}`}{" "}
rows
</p>
{mode === "preview" && mappingEnabled && (
<p className="mt-2 text-[11px] text-muted-foreground/70 text-center">
Mapping is saved automatically. You can start training anytime.
</p>
)}
{showMappingFooter && (
<DatasetMappingFooter
leftLabel={leftLabel}
rightLabel={rightLabel}
mappingOk={mappingOk}
isStarting={isStarting}
startError={startError}
onCancel={() => onOpenChange(false)}
onStartTraining={async () => {
const ok = await startTrainingRun();
if (ok) onOpenChange(false);
}}
/>
)}
</div>
</>
)}
</div>
@ -325,54 +403,4 @@ function MetaRow({
);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function formatCell(value: unknown): string {
if (value == null) return "";
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean")
return String(value);
if (Array.isArray(value) || typeof value === "object")
return JSON.stringify(value).slice(0, 500);
return String(value);
}
function isPreviewImagePayload(value: unknown): value is PreviewImagePayload {
if (!value || typeof value !== "object") return false;
const record = value as Record<string, unknown>;
return (
record.type === "image" &&
typeof record.data === "string" &&
record.data.length > 0
);
}
function collectPreviewImages(value: unknown): PreviewImagePayload[] {
const images: PreviewImagePayload[] = [];
const stack: unknown[] = [value];
let steps = 0;
while (stack.length > 0 && steps < 200) {
steps += 1;
const current = stack.pop();
if (isPreviewImagePayload(current)) {
images.push(current);
continue;
}
if (Array.isArray(current)) {
for (const item of current) stack.push(item);
continue;
}
if (current && typeof current === "object") {
for (const nested of Object.values(current as Record<string, unknown>)) {
stack.push(nested);
}
}
}
return images;
}
// mapping UI extracted to ./dataset-preview-dialog-mapping.tsx

View file

@ -30,6 +30,7 @@ import {
import { formatCompact } from "@/lib/utils";
import {
HfDatasetSubsetSplitSelectors,
useDatasetPreviewDialogStore,
useTrainingConfigStore,
} from "@/features/training";
import {
@ -43,7 +44,6 @@ import {
import { HugeiconsIcon } from "@hugeicons/react";
import { useMemo, useRef, useState } from "react";
import { useShallow } from "zustand/react/shallow";
import { DatasetPreviewDialog } from "./dataset-preview-dialog";
function isLikelyLocalDatasetRef(value: string) {
return (
@ -81,7 +81,7 @@ export function DatasetSection() {
);
const [inputValue, setInputValue] = useState("");
const [previewOpen, setPreviewOpen] = useState(false);
const openPreview = useDatasetPreviewDialogStore((s) => s.openPreview);
const selectingRef = useRef(false);
const debouncedQuery = useDebouncedValue(inputValue);
@ -341,7 +341,7 @@ export function DatasetSection() {
size="sm"
className="cursor-pointer gap-1.5"
disabled={!dataset}
onClick={() => setPreviewOpen(true)}
onClick={() => openPreview()}
>
<HugeiconsIcon icon={ViewIcon} className="size-3.5" />
View dataset
@ -349,14 +349,6 @@ export function DatasetSection() {
</div>
</div>
</SectionCard>
<DatasetPreviewDialog
open={previewOpen}
onOpenChange={setPreviewOpen}
datasetName={dataset}
hfToken={hfToken}
datasetSubset={datasetSubset}
datasetSplit={datasetSplit}
/>
</div>
);
}

View file

@ -2,6 +2,8 @@ import { Button } from "@/components/ui/button";
import {
shouldShowTrainingView,
useTrainingActions,
useDatasetPreviewDialogStore,
useTrainingConfigStore,
useTrainingRuntimeLifecycle,
useTrainingRuntimeStore,
} from "@/features/training";
@ -10,6 +12,7 @@ import { studioTourSteps } from "@/features/studio/tour";
import { ArrowLeft01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { type ReactElement, useEffect, useState } from "react";
import { DatasetPreviewDialog } from "./sections/dataset-preview-dialog";
import { DatasetSection } from "./sections/dataset-section";
import { ModelSection } from "./sections/model-section";
import { ParamsSection } from "./sections/params-section";
@ -26,6 +29,11 @@ export function StudioPage(): ReactElement {
const isHydratingRuntime = useTrainingRuntimeStore((state) => state.isHydrating);
const hasHydratedRuntime = useTrainingRuntimeStore((state) => state.hasHydrated);
const { dismissTrainingRun } = useTrainingActions();
const config = useTrainingConfigStore();
const dialogOpen = useDatasetPreviewDialogStore((s) => s.open);
const dialogMode = useDatasetPreviewDialogStore((s) => s.mode);
const dialogInitial = useDatasetPreviewDialogStore((s) => s.initialData);
const closeDialog = useDatasetPreviewDialogStore((s) => s.close);
const canGoBack = runtimePhase === "stopped" || runtimePhase === "error";
const tourEnabled = hasHydratedRuntime && !isHydratingRuntime && !showTrainingView;
@ -48,6 +56,22 @@ export function StudioPage(): ReactElement {
onComplete={() => localStorage.setItem(STUDIO_TOUR_KEY, "done")}
/>
<DatasetPreviewDialog
open={dialogOpen}
onOpenChange={(open) => {
if (!open) closeDialog();
}}
datasetName={
config.datasetSource === "huggingface" ? config.dataset : config.uploadedFile
}
hfToken={config.hfToken.trim() || null}
datasetSubset={config.datasetSubset}
datasetSplit={config.datasetSplit}
mode={dialogMode}
initialData={dialogInitial}
isVlm={config.modelType === "vision"}
/>
{canGoBack && (
<Button
variant="ghost"

View file

@ -0,0 +1,37 @@
import type { CheckFormatResponse } from "../types/datasets";
type CheckDatasetFormatArgs = {
datasetName: string;
hfToken: string | null;
subset?: string | null;
split?: string | null;
isVlm?: boolean;
};
export async function checkDatasetFormat({
datasetName,
hfToken,
subset,
split,
isVlm,
}: CheckDatasetFormatArgs): Promise<CheckFormatResponse> {
const res = await fetch("/api/datasets/check-format", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
dataset_name: datasetName,
hf_token: hfToken || undefined,
config: subset || undefined, // backend currently ignores, safe to send
split: split || "train",
is_vlm: !!isVlm,
}),
});
if (!res.ok) {
const body = await res.json().catch(() => null);
throw new Error(body?.detail || `Request failed (${res.status})`);
}
return res.json();
}

View file

@ -14,6 +14,15 @@ export function buildTrainingStartPayload(
const adapterMethod = config.trainingMethod !== "full";
const isQlorMethod = config.trainingMethod === "qlora";
const hfDataset = config.datasetSource === "huggingface" ? config.dataset : null;
const manual = config.datasetManualMapping;
const isVlm = config.modelType === "vision";
const customFormatMapping =
manual.input && manual.output
? {
[manual.input]: isVlm ? "image" : "user",
[manual.output]: isVlm ? "text" : "assistant",
}
: undefined;
return {
model_name: config.selectedModel ?? "",
@ -26,6 +35,7 @@ export function buildTrainingStartPayload(
hf_dataset_split: hfDataset ? config.datasetSplit : null,
local_datasets: [],
format_type: config.datasetFormat,
custom_format_mapping: customFormatMapping,
num_epochs: config.epochs,
learning_rate: String(config.learningRate),
batch_size: config.batchSize,

View file

@ -1,8 +1,10 @@
import { useCallback } from "react";
import { useTrainingConfigStore } from "../stores/training-config-store";
import { useTrainingRuntimeStore } from "../stores/training-runtime-store";
import { useDatasetPreviewDialogStore } from "../stores/dataset-preview-dialog-store";
import { startTraining, stopTraining, resetTraining } from "../api/train-api";
import { buildTrainingStartPayload } from "../api/mappers";
import { checkDatasetFormat } from "../api/datasets-api";
import { syncTrainingRuntimeFromBackend } from "../lib/sync-runtime";
import { validateTrainingConfig } from "../lib/validation";
@ -13,6 +15,7 @@ export function useTrainingActions() {
const startTrainingRun = useCallback(async (): Promise<boolean> => {
const config = useTrainingConfigStore.getState();
const runtimeStore = useTrainingRuntimeStore.getState();
const dialogStore = useDatasetPreviewDialogStore.getState();
runtimeStore.setStartError(null);
const validation = validateTrainingConfig(config);
@ -24,6 +27,45 @@ export function useTrainingActions() {
runtimeStore.setStarting(true);
try {
const datasetName =
config.datasetSource === "huggingface" ? config.dataset : config.uploadedFile;
const isVlm = config.modelType === "vision";
if (datasetName) {
const check = await checkDatasetFormat({
datasetName,
hfToken: config.hfToken.trim() || null,
subset: config.datasetSubset,
split: config.datasetSplit,
isVlm,
});
if (check.requires_manual_mapping) {
const existing = useTrainingConfigStore.getState().datasetManualMapping;
const hasMapping = !!existing.input && !!existing.output;
if (!hasMapping) {
const hintInput = isVlm
? check.detected_image_column
: pickRoleColumn(check.suggested_mapping, "user");
const hintOutput = isVlm
? check.detected_text_column
: pickRoleColumn(check.suggested_mapping, "assistant");
if (hintInput || hintOutput) {
useTrainingConfigStore.getState().setDatasetManualMapping({
input: hintInput ?? null,
output: hintOutput ?? null,
});
}
runtimeStore.setStarting(false);
dialogStore.openMapping(check);
return false;
}
}
}
const payload = buildTrainingStartPayload(config);
const response = await startTraining(payload);
@ -78,3 +120,14 @@ export function useTrainingActions() {
dismissTrainingRun,
};
}
function pickRoleColumn(
mapping: Record<string, string> | null | undefined,
role: string,
): string | null {
if (!mapping) return null;
for (const [col, mappedRole] of Object.entries(mapping)) {
if (mappedRole === role) return col;
}
return null;
}

View file

@ -6,4 +6,5 @@ export {
export { useTrainingActions } from "./hooks/use-training-actions";
export { useTrainingRuntimeLifecycle } from "./hooks/use-training-runtime-lifecycle";
export { HfDatasetSubsetSplitSelectors } from "./components/hf-dataset-subset-split-selectors";
export { useDatasetPreviewDialogStore } from "./stores/dataset-preview-dialog-store";
export type { TrainingPhase } from "./types/runtime";

View file

@ -0,0 +1,33 @@
import { create } from "zustand";
import type { CheckFormatResponse } from "../types/datasets";
export type DatasetPreviewDialogMode = "preview" | "mapping";
type DatasetPreviewDialogState = {
open: boolean;
mode: DatasetPreviewDialogMode;
initialData: CheckFormatResponse | null;
};
type DatasetPreviewDialogActions = {
openPreview: () => void;
openMapping: (data: CheckFormatResponse) => void;
close: () => void;
};
const initialState: DatasetPreviewDialogState = {
open: false,
mode: "preview",
initialData: null,
};
export const useDatasetPreviewDialogStore = create<
DatasetPreviewDialogState & DatasetPreviewDialogActions
>()((set) => ({
...initialState,
openPreview: () => set({ open: true, mode: "preview", initialData: null }),
openMapping: (data) => set({ open: true, mode: "mapping", initialData: data }),
close: () => set({ open: false, initialData: null, mode: "preview" }),
}));

View file

@ -18,6 +18,7 @@ const initialState: TrainingConfigState = {
dataset: null,
datasetSubset: null,
datasetSplit: null,
datasetManualMapping: { input: null, output: null },
uploadedFile: null,
...DEFAULT_HYPERPARAMS,
};
@ -58,10 +59,22 @@ export const useTrainingConfigStore = create<TrainingConfigStore>()(
setDatasetSource: (datasetSource) => set({ datasetSource }),
setDatasetFormat: (datasetFormat) => set({ datasetFormat }),
setDataset: (dataset) =>
set({ dataset, datasetSubset: null, datasetSplit: null }),
set({
dataset,
datasetSubset: null,
datasetSplit: null,
datasetManualMapping: { input: null, output: null },
}),
setDatasetSubset: (datasetSubset) =>
set({ datasetSubset, datasetSplit: null }),
setDatasetSplit: (datasetSplit) => set({ datasetSplit }),
set({
datasetSubset,
datasetSplit: null,
datasetManualMapping: { input: null, output: null },
}),
setDatasetSplit: (datasetSplit) =>
set({ datasetSplit, datasetManualMapping: { input: null, output: null } }),
setDatasetManualMapping: (datasetManualMapping) =>
set({ datasetManualMapping }),
setUploadedFile: (uploadedFile) => set({ uploadedFile }),
setEpochs: (epochs) => set({ epochs }),
setContextLength: (contextLength) => set({ contextLength }),

View file

@ -9,6 +9,7 @@ export interface TrainingStartRequest {
hf_dataset_split: string | null;
local_datasets: string[];
format_type: string;
custom_format_mapping?: Record<string, string> | null;
num_epochs: number;
learning_rate: string;
batch_size: number;

View file

@ -9,6 +9,11 @@ import type {
export type LoraVariant = "lora" | "rslora" | "loftq";
export type DatasetManualMapping = {
input: string | null;
output: string | null;
};
export interface TrainingConfigState {
currentStep: StepNumber;
modelType: ModelType | null;
@ -20,6 +25,7 @@ export interface TrainingConfigState {
dataset: string | null;
datasetSubset: string | null;
datasetSplit: string | null;
datasetManualMapping: DatasetManualMapping;
uploadedFile: string | null;
epochs: number;
contextLength: number;
@ -64,6 +70,7 @@ export interface TrainingConfigActions {
setDataset: (dataset: string | null) => void;
setDatasetSubset: (subset: string | null) => void;
setDatasetSplit: (split: string | null) => void;
setDatasetManualMapping: (mapping: DatasetManualMapping) => void;
setUploadedFile: (file: string | null) => void;
setEpochs: (epochs: number) => void;
setContextLength: (length: number) => void;

View file

@ -0,0 +1,13 @@
export type CheckFormatResponse = {
requires_manual_mapping: boolean;
detected_format: string;
columns: string[];
suggested_mapping?: Record<string, string> | null;
detected_image_column?: string | null;
detected_text_column?: string | null;
preview_samples?: Record<string, unknown>[] | null;
total_rows?: number | null;
is_multimodal?: boolean;
multimodal_columns?: string[] | null;
};

View file

@ -1,14 +1,24 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
if (!crypto.randomUUID) {
crypto.randomUUID = () =>
const globalCrypto = globalThis.crypto as Crypto | undefined;
const hasUuid =
globalCrypto && typeof (globalCrypto as Crypto).randomUUID === "function";
if (globalCrypto && !hasUuid) {
// Some envs ship `crypto` but no `randomUUID()` (or a non-function stub).
// Provide a best-effort v4 UUID using `getRandomValues` when available.
const getRandomByte = () => {
if (typeof globalCrypto.getRandomValues === "function") {
return globalCrypto.getRandomValues(new Uint8Array(1))[0];
}
return Math.floor(Math.random() * 256);
};
(globalCrypto as Crypto).randomUUID = (() =>
"10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) =>
(
+c ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))
).toString(16),
) as `${string}-${string}-${string}-${string}-${string}`;
(+c ^ (getRandomByte() & (15 >> (+c / 4)))).toString(16),
)) as Crypto["randomUUID"];
}
import "./index.css";