diff --git a/studio/frontend/src/app/routes/export.tsx b/studio/frontend/src/app/routes/export.tsx index c0356c823f..40118c6a92 100644 --- a/studio/frontend/src/app/routes/export.tsx +++ b/studio/frontend/src/app/routes/export.tsx @@ -12,10 +12,19 @@ const ExportPage = lazy(() => })), ); +export type ExportSearch = { + // Preselect a training run on the Export page (its output-dir basename, which + // equals the checkpoint scan's model name). Set when arriving from a run view. + run?: string; +}; + export const Route = createRoute({ getParentRoute: () => rootRoute, path: "/export", staticData: { title: "Export" }, beforeLoad: () => requireAuth(), + validateSearch: (search: Record): ExportSearch => ({ + run: typeof search.run === "string" ? search.run : undefined, + }), component: ExportPage, }); diff --git a/studio/frontend/src/features/export/export-page.tsx b/studio/frontend/src/features/export/export-page.tsx index 64123bc2f5..e23b3d7262 100644 --- a/studio/frontend/src/features/export/export-page.tsx +++ b/studio/frontend/src/features/export/export-page.tsx @@ -49,6 +49,7 @@ import { PackageIcon, Search01Icon, } from "@hugeicons/core-free-icons"; +import { useSearch } from "@tanstack/react-router"; import { HugeiconsIcon } from "@hugeicons/react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useShallow } from "zustand/react/shallow"; @@ -211,6 +212,29 @@ export function ExportPage() { }; }, []); + // Apply the ?run= deep link (e.g. from a finished run's "Export to GGUF" + // button) once its run appears in the checkpoint list: select the run and + // default to GGUF. The main checkpoint is auto-selected further below, after + // the model-change effect that clears the checkpoint. + const { run: preselectRun } = useSearch({ from: "/export" }); + const appliedRunRef = useRef(null); + useEffect(() => { + if (!preselectRun) { + // Deep link cleared (e.g. navigated to /export via the sidebar): stop + // treating the previously preselected run specially. + appliedRunRef.current = null; + return; + } + if (models.length === 0) return; + if (appliedRunRef.current === preselectRun) return; + const match = models.find((m) => m.name === preselectRun); + if (!match) return; + appliedRunRef.current = preselectRun; + setSourceMode("checkpoint"); + setSelectedModelIdx(match.name); + setExportMethod("gguf"); + }, [preselectRun, models]); + // ---- Fetch local models for direct export ---- useEffect(() => { const controller = new AbortController(); @@ -362,6 +386,15 @@ export function ExportPage() { setCheckpoint(null); }, [selectedModelIdx]); + // For a ?run= deep link, default to the run's main checkpoint. Declared after + // the reset effect above so it runs last and isn't clobbered back to null. + useEffect(() => { + if (appliedRunRef.current == null) return; + if (appliedRunRef.current !== selectedModelIdx) return; + if (checkpoint != null || checkpointsForModel.length === 0) return; + setCheckpoint(checkpointsForModel[0].display_name); + }, [selectedModelIdx, checkpoint, checkpointsForModel]); + // Auto-reset export method if incompatible with the selected model type useEffect(() => { if (!isAdapter && (exportMethod === "merged" || exportMethod === "lora")) { diff --git a/studio/frontend/src/features/studio/historical-training-view.tsx b/studio/frontend/src/features/studio/historical-training-view.tsx index e958f2ba0d..0bca090413 100644 --- a/studio/frontend/src/features/studio/historical-training-view.tsx +++ b/studio/frontend/src/features/studio/historical-training-view.tsx @@ -59,6 +59,7 @@ function mapToViewData( currentEpoch: metrics.final_epoch, currentNumTokens: metrics.final_num_tokens ?? null, outputDir: run.output_dir ?? null, + resumedLater: run.resumed_later ?? false, progressPercent: run.total_steps && run.final_step ? (run.final_step / run.total_steps) * 100 diff --git a/studio/frontend/src/features/studio/sections/progress-section.tsx b/studio/frontend/src/features/studio/sections/progress-section.tsx index 67800df27b..de8605e021 100644 --- a/studio/frontend/src/features/studio/sections/progress-section.tsx +++ b/studio/frontend/src/features/studio/sections/progress-section.tsx @@ -33,6 +33,7 @@ import { cn } from "@/lib/utils"; import { ChartAverageIcon, DashboardSpeed01Icon, + FolderExportIcon, Notebook01Icon, RamMemoryIcon, StopIcon, @@ -153,6 +154,21 @@ export function ProgressSection({ await navigate({ to: "/chat" }); }; + // A finished run can be exported to GGUF: deep-link to the Export page with + // this run preselected (its output-dir basename is the export model name). + const exportRunName = data.outputDir + ? (data.outputDir.replace(/[/\\]+$/, "").split(/[/\\]/).pop() || null) + : null; + const canExportGguf = + !data.isTrainingRunning && + !!exportRunName && + !data.resumedLater && + (data.phase === "completed" || data.phase === "stopped"); + const handleExportGguf = () => { + if (!exportRunName) return; + void navigate({ to: "/export", search: { run: exportRunName } }); + }; + const stoppedLoss = getDisplayMetric( data.isTrainingRunning, data.currentLoss, @@ -219,18 +235,31 @@ export function ProgressSection({ accent="emerald" className="shadow-border border border-border/60 bg-card/90 ring-0 backdrop-blur-sm" headerAction={ - isHistorical ? ( - - ) : ( - - ) +
+ {canExportGguf && ( + + )} + {isHistorical ? ( + + ) : ( + + )} +
} >
diff --git a/studio/frontend/src/features/training/types/runtime.ts b/studio/frontend/src/features/training/types/runtime.ts index 5fd8286d22..c27a2f2bed 100644 --- a/studio/frontend/src/features/training/types/runtime.ts +++ b/studio/frontend/src/features/training/types/runtime.ts @@ -147,6 +147,9 @@ export interface TrainingViewData { currentEpoch: number | null; currentNumTokens: number | null; outputDir: string | null; + // True when a newer run reused this run's output_dir (resume), so its + // on-disk contents no longer match this (older) run's metrics. + resumedLater?: boolean; progressPercent: number; elapsedSeconds: number | null; etaSeconds: number | null; diff --git a/studio/frontend/src/i18n/locales/en.ts b/studio/frontend/src/i18n/locales/en.ts index 214767bec0..7bc86b57ec 100644 --- a/studio/frontend/src/i18n/locales/en.ts +++ b/studio/frontend/src/i18n/locales/en.ts @@ -785,6 +785,7 @@ export const en = { progress: { title: "Training Progress", liveMetrics: "Live training metrics", + exportGguf: "Export to GGUF", openConfig: "Open training config", configLabel: "Training Config", hyperparams: "Hyperparams",