diff --git a/studio/frontend/src/features/studio/sections/dataset-preview-dialog-mapping.tsx b/studio/frontend/src/features/studio/sections/dataset-preview-dialog-mapping.tsx
new file mode 100644
index 0000000000..5f8833b742
--- /dev/null
+++ b/studio/frontend/src/features/studio/sections/dataset-preview-dialog-mapping.tsx
@@ -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 (
+
+ onCheckedChange(v === true)}
+ aria-label={label}
+ className="h-3.5 w-3.5"
+ />
+ {label}
+
+ );
+}
+
+export function DatasetMappingCard({
+ leftLabel,
+ rightLabel,
+ mappingOk,
+ input,
+ output,
+}: {
+ leftLabel: string;
+ rightLabel: string;
+ mappingOk: boolean;
+ input: string | null;
+ output: string | null;
+}) {
+ return (
+
+
+
+
+
+
+
Map dataset columns
+
+ We couldn't auto-detect the format. Pick the{" "}
+ {leftLabel.toLowerCase()} column and the {rightLabel.toLowerCase()}{" "}
+ column. We'll convert it to a supported format automatically.
+
+
+
+ {leftLabel}: {input ?? "--"}
+
+
+ {rightLabel}: {output ?? "--"}
+
+
+ {!mappingOk && (
+
+ Select exactly 1 {leftLabel.toLowerCase()} and 1{" "}
+ {rightLabel.toLowerCase()} column to continue.
+
+ )}
+
+
+
+ );
+}
+
+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;
+}) {
+ return (
+
+
+
+ Tip: you can click {leftLabel} / {rightLabel} in the headers.
+
+
+
+ Cancel
+
+ void onStartTraining()}
+ >
+ {isStarting ? "Starting..." : "Continue"}
+
+
+
+
+ {startError && (
+
+ {startError}
+
+ )}
+
+ );
+}
+
+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 | 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;
+}
diff --git a/studio/frontend/src/features/studio/sections/dataset-preview-dialog-utils.ts b/studio/frontend/src/features/studio/sections/dataset-preview-dialog-utils.ts
new file mode 100644
index 0000000000..735a3abbff
--- /dev/null
+++ b/studio/frontend/src/features/studio/sections/dataset-preview-dialog-utils.ts
@@ -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;
+ 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)) {
+ stack.push(nested);
+ }
+ }
+ }
+
+ return images;
+}
+
diff --git a/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx b/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx
index f145b17f61..11de320147 100644
--- a/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx
+++ b/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx
@@ -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 | null;
- detected_image_column?: string | null;
- detected_text_column?: string | null;
- preview_samples?: Record[] | 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 {
- 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(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(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: () => (
-
- {colName}
-
+
+
+ {colName}
+
+ {mappingEnabled && (
+
+ {(manualMapping.input == null || manualMapping.input === colName) && (
+ {
+ setManualMapping({
+ input: checked ? colName : null,
+ output: manualMapping.output,
+ });
+ }}
+ />
+ )}
+ {(manualMapping.output == null || manualMapping.output === colName) && (
+ {
+ setManualMapping({
+ input: manualMapping.input,
+ output: checked ? colName : null,
+ });
+ }}
+ />
+ )}
+
+ )}
+
),
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 (
@@ -266,10 +311,10 @@ export function DatasetPreviewDialog({
: "--"
}
/>
-
+
{columns.map((col) => (
+ {mappingEnabled && (
+
+ )}
+
{/* Data table */}
{/* Footer */}
-
- Showing {rows.length}
- {data.total_rows != null &&
- ` of ${data.total_rows.toLocaleString()}`}{" "}
- rows
-
+
+
+ Showing {rows.length}
+ {data.total_rows != null &&
+ ` of ${data.total_rows.toLocaleString()}`}{" "}
+ rows
+
+
+ {mode === "preview" && mappingEnabled && (
+
+ Mapping is saved automatically. You can start training anytime.
+
+ )}
+
+ {showMappingFooter && (
+
onOpenChange(false)}
+ onStartTraining={async () => {
+ const ok = await startTrainingRun();
+ if (ok) onOpenChange(false);
+ }}
+ />
+ )}
+
>
)}
@@ -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;
- 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)) {
- stack.push(nested);
- }
- }
- }
-
- return images;
-}
+// mapping UI extracted to ./dataset-preview-dialog-mapping.tsx
diff --git a/studio/frontend/src/features/studio/sections/dataset-section.tsx b/studio/frontend/src/features/studio/sections/dataset-section.tsx
index ba9b96373a..3d7e3f810f 100644
--- a/studio/frontend/src/features/studio/sections/dataset-section.tsx
+++ b/studio/frontend/src/features/studio/sections/dataset-section.tsx
@@ -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()}
>
View dataset
@@ -349,14 +349,6 @@ export function DatasetSection() {
-
);
}
diff --git a/studio/frontend/src/features/studio/studio-page.tsx b/studio/frontend/src/features/studio/studio-page.tsx
index 6cdf5225ec..b7018dd843 100644
--- a/studio/frontend/src/features/studio/studio-page.tsx
+++ b/studio/frontend/src/features/studio/studio-page.tsx
@@ -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")}
/>
+ {
+ 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 && (
{
+ 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();
+}
+
diff --git a/studio/frontend/src/features/training/api/mappers.ts b/studio/frontend/src/features/training/api/mappers.ts
index 990862a27e..ed2c3b752d 100644
--- a/studio/frontend/src/features/training/api/mappers.ts
+++ b/studio/frontend/src/features/training/api/mappers.ts
@@ -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,
diff --git a/studio/frontend/src/features/training/hooks/use-training-actions.ts b/studio/frontend/src/features/training/hooks/use-training-actions.ts
index 4ace9dd5ed..27e36f9765 100644
--- a/studio/frontend/src/features/training/hooks/use-training-actions.ts
+++ b/studio/frontend/src/features/training/hooks/use-training-actions.ts
@@ -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 => {
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 | 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;
+}
diff --git a/studio/frontend/src/features/training/index.ts b/studio/frontend/src/features/training/index.ts
index 41d1aade75..77451140f5 100644
--- a/studio/frontend/src/features/training/index.ts
+++ b/studio/frontend/src/features/training/index.ts
@@ -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";
diff --git a/studio/frontend/src/features/training/stores/dataset-preview-dialog-store.ts b/studio/frontend/src/features/training/stores/dataset-preview-dialog-store.ts
new file mode 100644
index 0000000000..9129e31be7
--- /dev/null
+++ b/studio/frontend/src/features/training/stores/dataset-preview-dialog-store.ts
@@ -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" }),
+}));
+
diff --git a/studio/frontend/src/features/training/stores/training-config-store.ts b/studio/frontend/src/features/training/stores/training-config-store.ts
index 57d05db114..fdb0deb129 100644
--- a/studio/frontend/src/features/training/stores/training-config-store.ts
+++ b/studio/frontend/src/features/training/stores/training-config-store.ts
@@ -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()(
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 }),
diff --git a/studio/frontend/src/features/training/types/api.ts b/studio/frontend/src/features/training/types/api.ts
index 6011ff6b96..f6c43616d9 100644
--- a/studio/frontend/src/features/training/types/api.ts
+++ b/studio/frontend/src/features/training/types/api.ts
@@ -9,6 +9,7 @@ export interface TrainingStartRequest {
hf_dataset_split: string | null;
local_datasets: string[];
format_type: string;
+ custom_format_mapping?: Record | null;
num_epochs: number;
learning_rate: string;
batch_size: number;
diff --git a/studio/frontend/src/features/training/types/config.ts b/studio/frontend/src/features/training/types/config.ts
index 77706d60b2..6a28500b7e 100644
--- a/studio/frontend/src/features/training/types/config.ts
+++ b/studio/frontend/src/features/training/types/config.ts
@@ -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;
diff --git a/studio/frontend/src/features/training/types/datasets.ts b/studio/frontend/src/features/training/types/datasets.ts
new file mode 100644
index 0000000000..96cf699f89
--- /dev/null
+++ b/studio/frontend/src/features/training/types/datasets.ts
@@ -0,0 +1,13 @@
+export type CheckFormatResponse = {
+ requires_manual_mapping: boolean;
+ detected_format: string;
+ columns: string[];
+ suggested_mapping?: Record | null;
+ detected_image_column?: string | null;
+ detected_text_column?: string | null;
+ preview_samples?: Record[] | null;
+ total_rows?: number | null;
+ is_multimodal?: boolean;
+ multimodal_columns?: string[] | null;
+};
+
diff --git a/studio/frontend/src/main.tsx b/studio/frontend/src/main.tsx
index 0719812d19..9bfc4451bb 100644
--- a/studio/frontend/src/main.tsx
+++ b/studio/frontend/src/main.tsx
@@ -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";