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 index d7ba18ff35..e2dfb146f7 100644 --- a/studio/frontend/src/features/studio/sections/dataset-preview-dialog-mapping.tsx +++ b/studio/frontend/src/features/studio/sections/dataset-preview-dialog-mapping.tsx @@ -1,9 +1,10 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; +import type { CheckFormatResponse } from "@/features/training/types/datasets"; +import { cn } from "@/lib/utils"; import { AlertCircleIcon, CheckmarkCircle02Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; -import type { CheckFormatResponse } from "@/features/training/types/datasets"; export function HeaderPick({ label, @@ -40,30 +41,30 @@ export function DatasetMappingCard({ input: string | null; output: string | null; }) { - const tone = mappingOk ? "ok" : "warn"; return (
@@ -71,11 +72,12 @@ export function DatasetMappingCard({ {mappingOk ? "Mapping ready" : "Map dataset columns"}

{mappingOk ? "Looks good. We'll convert this dataset automatically." @@ -163,17 +165,17 @@ 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"); - if (input && output && input === output) return { input, output: null }; - return { input: input ?? null, output: output ?? null }; + const input = isVlm + ? data.detected_image_column ?? pickRole(data.suggested_mapping, "image") + : pickRole(data.suggested_mapping, "user"); + const output = isVlm + ? data.detected_text_column ?? pickRole(data.suggested_mapping, "text") + : pickRole(data.suggested_mapping, "assistant"); + + if (input && output && input === output) { + return { input, output: null }; } - const input = pickRole(data.suggested_mapping, "user"); - const output = pickRole(data.suggested_mapping, "assistant"); - if (input && output && input === output) return { input, output: null }; + return { input: input ?? null, output: output ?? null }; } 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 ce4528341a..9fc84f1013 100644 --- a/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx +++ b/studio/frontend/src/features/studio/sections/dataset-preview-dialog.tsx @@ -1,3 +1,4 @@ +import { type ReactNode, useEffect, useMemo, useState } from "react"; import type { ColumnDef } from "@tanstack/react-table"; import { Dialog, @@ -8,12 +9,12 @@ import { import { DataTable } from "@/components/ui/data-table"; import { Badge } from "@/components/ui/badge"; 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 { Database02Icon, AlertCircleIcon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { useShallow } from "zustand/react/shallow"; import { collectPreviewImages, formatCell } from "./dataset-preview-dialog-utils"; import { DatasetMappingCard, @@ -22,10 +23,6 @@ import { deriveDefaultMapping, } from "./dataset-preview-dialog-mapping"; -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - type DatasetPreviewDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; @@ -38,10 +35,6 @@ type DatasetPreviewDialogProps = { isVlm?: boolean; }; -// --------------------------------------------------------------------------- -// Component -// --------------------------------------------------------------------------- - export function DatasetPreviewDialog({ open, onOpenChange, @@ -57,8 +50,12 @@ export function DatasetPreviewDialog({ const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const manualMapping = useTrainingConfigStore((s) => s.datasetManualMapping); - const setManualMapping = useTrainingConfigStore((s) => s.setDatasetManualMapping); + const { manualMapping, setManualMapping } = useTrainingConfigStore( + useShallow((s) => ({ + manualMapping: s.datasetManualMapping, + setManualMapping: s.setDatasetManualMapping, + })), + ); const { isStarting, startError, startTrainingRun } = useTrainingActions(); const mappingEnabled = !!data?.requires_manual_mapping; @@ -79,12 +76,14 @@ export function DatasetPreviewDialog({ setError(null); return; } + if (initialData) { setData(initialData); setError(null); setLoading(false); return; } + let cancelled = false; setLoading(true); setError(null); @@ -115,21 +114,13 @@ export function DatasetPreviewDialog({ }, [open, datasetName, hfToken, datasetSubset, datasetSplit, isVlm, initialData]); useEffect(() => { - if (!open || !datasetName || !data?.requires_manual_mapping) return; + if (!open || !datasetName) return; + if (!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, - ]); + }, [open, datasetName, data, isVlm, manualMapping.input, manualMapping.output, setManualMapping]); const rows = data?.preview_samples ?? []; const columns = data?.columns ?? []; @@ -159,38 +150,30 @@ export function DatasetPreviewDialog({ {mappingEnabled && (

- {(manualMapping.output == null || manualMapping.output !== colName) && - (manualMapping.input == null || manualMapping.input === colName) && ( - { - setManualMapping({ - input: checked ? colName : null, - output: - checked && manualMapping.output === colName - ? null - : manualMapping.output, - }); - }} - /> - )} - {(manualMapping.input == null || manualMapping.input !== colName) && - (manualMapping.output == null || manualMapping.output === colName) && ( - { - setManualMapping({ - input: - checked && manualMapping.input === colName - ? null - : manualMapping.input, - output: checked ? colName : null, - }); - }} - /> - )} + {canShowInputPicker(colName, manualMapping) && ( + { + setManualMapping({ + input: checked ? colName : null, + output: manualMapping.output, + }); + }} + /> + )} + {canShowOutputPicker(colName, manualMapping) && ( + { + setManualMapping({ + input: manualMapping.input, + output: checked ? colName : null, + }); + }} + /> + )}
)}
@@ -235,8 +218,7 @@ export function DatasetPreviewDialog({ ); } - const full = - typeof value === "string" ? value : JSON.stringify(value); + const full = typeof value === "string" ? value : JSON.stringify(value); return (

- + {columns.map((col) => ( - - {(id: string) => { - const r = hfResults.find((ds) => ds.id === id); - const detail = r?.totalExamples - ? `${formatCompact(r.totalExamples)} rows` - : r?.sizeCategory - ? r.sizeCategory - : r?.downloads != null - ? `↓${formatCompact(r.downloads)}` - : null; - return ( - + {(id: string) => { + const r = hfResults.find((ds) => ds.id === id); + let detail: string | null = null; + if (r?.totalExamples) { + detail = `${formatCompact(r.totalExamples)} rows`; + } else if (r?.sizeCategory) { + detail = r.sizeCategory; + } else if (r?.downloads != null) { + detail = `↓${formatCompact(r.downloads)}`; + } + return ( + diff --git a/studio/frontend/src/features/training/api/mappers.ts b/studio/frontend/src/features/training/api/mappers.ts index ed2c3b752d..05fdd98959 100644 --- a/studio/frontend/src/features/training/api/mappers.ts +++ b/studio/frontend/src/features/training/api/mappers.ts @@ -14,15 +14,7 @@ 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; + const customFormatMapping = buildCustomFormatMapping(config); return { model_name: config.selectedModel ?? "", @@ -73,3 +65,16 @@ export function buildTrainingStartPayload( : null, }; } + +function buildCustomFormatMapping( + config: TrainingConfigState, +): Record | undefined { + const { input, output } = config.datasetManualMapping; + if (!input || !output) return undefined; + + if (config.modelType === "vision") { + return { [input]: "image", [output]: "text" }; + } + + return { [input]: "user", [output]: "assistant" }; +} 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 27e36f9765..e3600432e9 100644 --- a/studio/frontend/src/features/training/hooks/use-training-actions.ts +++ b/studio/frontend/src/features/training/hooks/use-training-actions.ts @@ -1,12 +1,13 @@ 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 { buildTrainingStartPayload } from "../api/mappers"; +import { startTraining, stopTraining, resetTraining } from "../api/train-api"; import { syncTrainingRuntimeFromBackend } from "../lib/sync-runtime"; import { validateTrainingConfig } from "../lib/validation"; +import { useDatasetPreviewDialogStore } from "../stores/dataset-preview-dialog-store"; +import { useTrainingConfigStore } from "../stores/training-config-store"; +import { useTrainingRuntimeStore } from "../stores/training-runtime-store"; +import type { TrainingConfigState } from "../types/config"; export function useTrainingActions() { const isStarting = useTrainingRuntimeStore((state) => state.isStarting); @@ -27,8 +28,7 @@ export function useTrainingActions() { runtimeStore.setStarting(true); try { - const datasetName = - config.datasetSource === "huggingface" ? config.dataset : config.uploadedFile; + const datasetName = getDatasetName(config); const isVlm = config.modelType === "vision"; if (datasetName) { @@ -40,29 +40,24 @@ export function useTrainingActions() { isVlm, }); - if (check.requires_manual_mapping) { - const existing = useTrainingConfigStore.getState().datasetManualMapping; - const hasMapping = !!existing.input && !!existing.output; + if (check.requires_manual_mapping && !hasManualMapping(config)) { + 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 (!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; + if (hintInput || hintOutput) { + useTrainingConfigStore.getState().setDatasetManualMapping({ + input: hintInput ?? null, + output: hintOutput ?? null, + }); } + + runtimeStore.setStarting(false); + dialogStore.openMapping(check); + return false; } } @@ -121,6 +116,18 @@ export function useTrainingActions() { }; } +function getDatasetName(config: TrainingConfigState): string | null { + return config.datasetSource === "huggingface" + ? config.dataset + : config.uploadedFile; +} + +function hasManualMapping(config: TrainingConfigState): boolean { + return ( + !!config.datasetManualMapping.input && !!config.datasetManualMapping.output + ); +} + function pickRoleColumn( mapping: Record | null | undefined, role: string, 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 fdb0deb129..731ded9d80 100644 --- a/studio/frontend/src/features/training/stores/training-config-store.ts +++ b/studio/frontend/src/features/training/stores/training-config-store.ts @@ -7,6 +7,10 @@ import type { TrainingConfigState, TrainingConfigStore } from "../types/config"; const MIN_STEP: StepNumber = 1; const MAX_STEP: StepNumber = STEPS.length as StepNumber; +function emptyManualMapping(): TrainingConfigState["datasetManualMapping"] { + return { input: null, output: null }; +} + const initialState: TrainingConfigState = { currentStep: MIN_STEP, modelType: null, @@ -18,7 +22,7 @@ const initialState: TrainingConfigState = { dataset: null, datasetSubset: null, datasetSplit: null, - datasetManualMapping: { input: null, output: null }, + datasetManualMapping: emptyManualMapping(), uploadedFile: null, ...DEFAULT_HYPERPARAMS, }; @@ -63,16 +67,16 @@ export const useTrainingConfigStore = create()( dataset, datasetSubset: null, datasetSplit: null, - datasetManualMapping: { input: null, output: null }, + datasetManualMapping: emptyManualMapping(), }), setDatasetSubset: (datasetSubset) => set({ datasetSubset, datasetSplit: null, - datasetManualMapping: { input: null, output: null }, + datasetManualMapping: emptyManualMapping(), }), setDatasetSplit: (datasetSplit) => - set({ datasetSplit, datasetManualMapping: { input: null, output: null } }), + set({ datasetSplit, datasetManualMapping: emptyManualMapping() }), setDatasetManualMapping: (datasetManualMapping) => set({ datasetManualMapping }), setUploadedFile: (uploadedFile) => set({ uploadedFile }), diff --git a/studio/frontend/src/main.tsx b/studio/frontend/src/main.tsx index 9bfc4451bb..4358241c94 100644 --- a/studio/frontend/src/main.tsx +++ b/studio/frontend/src/main.tsx @@ -1,29 +1,29 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -const globalCrypto = globalThis.crypto as Crypto | undefined; -const hasUuid = - globalCrypto && typeof (globalCrypto as Crypto).randomUUID === "function"; +import "./index.css"; +import { App } from "./app/app"; -if (globalCrypto && !hasUuid) { +const globalCrypto = globalThis.crypto as Crypto | undefined; + +if (globalCrypto && typeof globalCrypto.randomUUID !== "function") { // 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]; + const cryptoRef = globalCrypto; + + function getRandomByte(): number { + if (typeof cryptoRef.getRandomValues === "function") { + return cryptoRef.getRandomValues(new Uint8Array(1))[0]; } return Math.floor(Math.random() * 256); - }; + } - (globalCrypto as Crypto).randomUUID = (() => + cryptoRef.randomUUID = (() => "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => (+c ^ (getRandomByte() & (15 >> (+c / 4)))).toString(16), )) as Crypto["randomUUID"]; } -import "./index.css"; -import { App } from "./app/app"; - const rootElement = document.getElementById("root"); if (!rootElement) { throw new Error("Root element not found");