Compare commits

...
Sign in to create a new pull request.

4 commits

Author SHA1 Message Date
Roland Tannous
d3c37cf3d1 fix(studio): accept hf_token in load_checkpoint orchestrator method
The route was passing hf_token to load_checkpoint() but the method
didn't accept it, causing a TypeError on every /api/export/load-checkpoint
request.
2026-03-28 17:53:55 +00:00
imagineer99
c6149c0499 fix(studio): export page trust_remote_code control and label styling 2026-03-28 17:30:41 +00:00
imagineer99
c8d8ff79b1 fix(studio):fix selector ring clipping 2026-03-28 17:30:41 +00:00
imagineer99
b747b7f2a9 feat(studio): add HF/local model selection UI for GGUF export 2026-03-28 17:30:41 +00:00
2 changed files with 613 additions and 156 deletions

View file

@ -217,6 +217,7 @@ class ExportOrchestrator:
max_seq_length: int = 2048, max_seq_length: int = 2048,
load_in_4bit: bool = True, load_in_4bit: bool = True,
trust_remote_code: bool = False, trust_remote_code: bool = False,
hf_token: Optional[str] = None,
) -> Tuple[bool, str]: ) -> Tuple[bool, str]:
"""Load a checkpoint for export. """Load a checkpoint for export.
@ -227,6 +228,7 @@ class ExportOrchestrator:
"max_seq_length": max_seq_length, "max_seq_length": max_seq_length,
"load_in_4bit": load_in_4bit, "load_in_4bit": load_in_4bit,
"trust_remote_code": trust_remote_code, "trust_remote_code": trust_remote_code,
"hf_token": hf_token,
} }
# Always kill existing subprocess and spawn fresh. # Always kill existing subprocess and spawn fresh.

View file

@ -3,6 +3,19 @@
import { SectionCard } from "@/components/section-card"; import { SectionCard } from "@/components/section-card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@/components/ui/combobox";
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group";
import { import {
Select, Select,
SelectContent, SelectContent,
@ -11,17 +24,34 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { useTrainingConfigStore } from "@/features/training"; import {
import { AlertCircleIcon, InformationCircleIcon, PackageIcon } from "@hugeicons/core-free-icons"; listLocalModels,
type LocalModelInfo,
useTrainingConfigStore,
} from "@/features/training";
import {
useDebouncedValue,
useHfModelSearch,
useHfTokenValidation,
} from "@/hooks";
import {
AlertCircleIcon,
FolderSearchIcon,
InformationCircleIcon,
Key01Icon,
PackageIcon,
Search01Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react"; import { HugeiconsIcon } from "@hugeicons/react";
import { AnimatePresence, motion } from "motion/react"; import { AnimatePresence, motion } from "motion/react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
import { collapseAnim } from "./anim"; import { collapseAnim } from "./anim";
import type { ModelCheckpoints } from "./api/export-api"; import type { ModelCheckpoints } from "./api/export-api";
@ -60,6 +90,21 @@ export function ExportPage() {
const [selectedModelIdx, setSelectedModelIdx] = useState<string | null>(null); const [selectedModelIdx, setSelectedModelIdx] = useState<string | null>(null);
const [checkpoint, setCheckpoint] = useState<string | null>(null); const [checkpoint, setCheckpoint] = useState<string | null>(null);
const [sourceMode, setSourceMode] = useState<"checkpoint" | "model">(
"checkpoint",
);
const [modelSource, setModelSource] = useState<"hf" | "local">("hf");
const [hfExportTrustRemoteCode, setHfExportTrustRemoteCode] =
useState(true);
const [modelInput, setModelInput] = useState("");
const [selectedSourceModel, setSelectedSourceModel] = useState<string | null>(
null,
);
const [localModelInput, setLocalModelInput] = useState("");
const [localModels, setLocalModels] = useState<LocalModelInfo[]>([]);
const [isLoadingLocalModels, setIsLoadingLocalModels] = useState(true);
const [localModelsError, setLocalModelsError] = useState<string | null>(null);
const debouncedModelQuery = useDebouncedValue(modelInput);
const [exportMethod, setExportMethod] = useState<ExportMethod | null>(null); const [exportMethod, setExportMethod] = useState<ExportMethod | null>(null);
const [quantLevels, setQuantLevels] = useState<string[]>([]); const [quantLevels, setQuantLevels] = useState<string[]>([]);
@ -74,6 +119,9 @@ export function ExportPage() {
const [exportError, setExportError] = useState<string | null>(null); const [exportError, setExportError] = useState<string | null>(null);
const [exportSuccess, setExportSuccess] = useState(false); const [exportSuccess, setExportSuccess] = useState(false);
const hfComboboxAnchorRef = useRef<HTMLDivElement>(null);
const localComboboxAnchorRef = useRef<HTMLDivElement>(null);
const tour = useGuidedTourController({ const tour = useGuidedTourController({
id: "export", id: "export",
steps: exportTourSteps, steps: exportTourSteps,
@ -105,6 +153,27 @@ export function ExportPage() {
}; };
}, []); }, []);
// ---- Fetch local models for direct export ----
useEffect(() => {
const controller = new AbortController();
void listLocalModels(controller.signal)
.then((models) => {
if (controller.signal.aborted) return;
setLocalModels(models);
})
.catch((error) => {
if (controller.signal.aborted) return;
setLocalModelsError(
error instanceof Error ? error.message : "Failed to load local models",
);
})
.finally(() => {
if (controller.signal.aborted) return;
setIsLoadingLocalModels(false);
});
return () => controller.abort();
}, []);
// ---- Derived state ---- // ---- Derived state ----
const selectedModelData = useMemo( const selectedModelData = useMemo(
() => () =>
@ -127,6 +196,83 @@ export function ExportPage() {
const trainingMethodLabel = selectedModelData?.peft_type const trainingMethodLabel = selectedModelData?.peft_type
? "LoRA / QLoRA" ? "LoRA / QLoRA"
: "Full Fine-tune"; : "Full Fine-tune";
const sourceBaseModelName = sourceMode === "model"
? selectedSourceModel ?? "—"
: baseModelName;
const {
results: hfResults,
isLoading: isLoadingHfModels,
error: hfSearchError,
} = useHfModelSearch(debouncedModelQuery, {
accessToken: hfToken || undefined,
excludeGguf: true,
});
const { error: tokenValidationError, isChecking: isCheckingToken } =
useHfTokenValidation(hfToken);
const hfResultIds = useMemo(() => {
const ids = hfResults.map((r) => r.id);
if (
selectedSourceModel &&
modelSource === "hf" &&
!ids.includes(selectedSourceModel)
) {
ids.push(selectedSourceModel);
}
return ids;
}, [hfResults, modelSource, selectedSourceModel]);
const exportableLocalModels = useMemo(
() =>
localModels.filter((m) => {
if (m.path.endsWith(".gguf")) return false;
if (m.id.toLowerCase().includes("-gguf")) return false;
return true;
}),
[localModels],
);
const localMetaById = useMemo(() => {
const map = new Map<string, LocalModelInfo>();
for (const model of exportableLocalModels) map.set(model.id, model);
return map;
}, [exportableLocalModels]);
const localResultIds = useMemo(() => {
const ids = exportableLocalModels.map((model) => model.id);
const manual = localModelInput.trim();
if (manual && !ids.includes(manual)) {
ids.unshift(manual);
}
return ids;
}, [exportableLocalModels, localModelInput]);
const localFilteredIds = useMemo(() => {
const q = localModelInput.trim().toLowerCase();
if (!q) return localResultIds;
return localResultIds.filter((id) => {
const meta = localMetaById.get(id);
if (id.toLowerCase().includes(q)) return true;
if (meta?.display_name.toLowerCase().includes(q)) return true;
if (meta?.path.toLowerCase().includes(q)) return true;
return false;
});
}, [localMetaById, localModelInput, localResultIds]);
const exportGuideSteps = useMemo(
() =>
sourceMode === "model"
? [
"Select a Hugging Face or local model to export from",
"GGUF is used for non-finetuned model exports",
"Pick one or more GGUF quantization levels",
"Click Export and choose your destination",
"Test your model and compare outputs in Chat",
]
: GUIDE_STEPS,
[sourceMode],
);
// Reset checkpoint when the selected model changes // Reset checkpoint when the selected model changes
useEffect(() => { useEffect(() => {
@ -144,6 +290,25 @@ export function ExportPage() {
} }
}, [isAdapter, isQuantized, exportMethod]); }, [isAdapter, isQuantized, exportMethod]);
const handleSourceModeSwitch = useCallback(
(next: "checkpoint" | "model") => {
setSourceMode(next);
if (next === "model") {
setExportMethod("gguf");
}
setSelectedSourceModel(null);
setLocalModelInput("");
setModelInput("");
},
[],
);
useEffect(() => {
setSelectedSourceModel(null);
setLocalModelInput("");
setModelInput("");
}, [modelSource]);
const handleMethodChange = (method: ExportMethod) => { const handleMethodChange = (method: ExportMethod) => {
setExportMethod(method); setExportMethod(method);
if (method !== "gguf") { if (method !== "gguf") {
@ -152,19 +317,24 @@ export function ExportPage() {
}; };
const estimatedSize = getEstimatedSize(exportMethod, quantLevels); const estimatedSize = getEstimatedSize(exportMethod, quantLevels);
const canExport = const selectedExportSource =
checkpoint && sourceMode === "checkpoint" ? checkpoint : selectedSourceModel;
const canExport = !!(
selectedExportSource &&
exportMethod && exportMethod &&
(exportMethod !== "gguf" || quantLevels.length > 0); (exportMethod !== "gguf" || quantLevels.length > 0)
);
// ---- Export handler ---- // ---- Export handler ----
const handleExport = useCallback(async () => { const handleExport = useCallback(async () => {
if (!checkpoint) return; const source = sourceMode === "checkpoint" ? checkpoint : selectedSourceModel;
if (!source) return;
const selectedCp = checkpointsForModel.find( const selectedCp = sourceMode === "checkpoint"
(cp) => cp.display_name === checkpoint, ? checkpointsForModel.find((cp) => cp.display_name === checkpoint)
); : null;
if (!selectedCp) return; if (sourceMode === "checkpoint" && !selectedCp) return;
const checkpointPath = selectedCp?.path;
setExporting(true); setExporting(true);
setExportError(null); setExportError(null);
@ -174,7 +344,8 @@ export function ExportPage() {
// For other formats, nest under training-run/checkpoint // For other formats, nest under training-run/checkpoint
const saveDir = const saveDir =
exportMethod === "gguf" exportMethod === "gguf"
? `${baseModelName.split("/").pop() ?? selectedModelIdx ?? "model"}-finetune-gguf` ? `${(sourceBaseModelName.split("/").pop() ?? selectedModelIdx ?? "model")
.replace(/[^a-zA-Z0-9._-]/g, "-")}-gguf`
: `${selectedModelIdx ?? "model"}/${checkpoint}`; : `${selectedModelIdx ?? "model"}/${checkpoint}`;
const pushToHub = destination === "hub"; const pushToHub = destination === "hub";
const repoId = pushToHub && hfUsername && modelName const repoId = pushToHub && hfUsername && modelName
@ -183,8 +354,18 @@ export function ExportPage() {
const token = pushToHub && hfToken ? hfToken : undefined; const token = pushToHub && hfToken ? hfToken : undefined;
try { try {
// 1. Load checkpoint // 1. Load model source
await loadCheckpoint({ checkpoint_path: selectedCp.path }); if (sourceMode === "checkpoint") {
if (!checkpointPath) return;
await loadCheckpoint({ checkpoint_path: checkpointPath });
} else {
await loadCheckpoint({
checkpoint_path: source,
load_in_4bit: false,
trust_remote_code:
modelSource === "hf" ? hfExportTrustRemoteCode : true,
});
}
// 2. Run export based on method // 2. Run export based on method
if (exportMethod === "merged") { if (exportMethod === "merged") {
@ -242,16 +423,21 @@ export function ExportPage() {
}, [ }, [
checkpoint, checkpoint,
checkpointsForModel, checkpointsForModel,
sourceMode,
selectedSourceModel,
selectedModelIdx, selectedModelIdx,
selectedModelData, selectedModelData,
exportMethod, exportMethod,
isAdapter, isAdapter,
sourceBaseModelName,
quantLevels, quantLevels,
destination, destination,
hfUsername, hfUsername,
modelName, modelName,
hfToken, hfToken,
privateRepo, privateRepo,
modelSource,
hfExportTrustRemoteCode,
]); ]);
// ---- Render ---- // ---- Render ----
@ -265,14 +451,14 @@ export function ExportPage() {
Export Model Export Model
</h1> </h1>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Export your fine-tuned model for deployment Export fine-tuned or base models for deployment
</p> </p>
</div> </div>
<SectionCard <SectionCard
icon={<HugeiconsIcon icon={PackageIcon} className="size-5" />} icon={<HugeiconsIcon icon={PackageIcon} className="size-5" />}
title="Export Configuration" title="Export Configuration"
description="Select checkpoint, method, and quantization" description="Select source, method, and quantization"
accent="emerald" accent="emerald"
featured={true} featured={true}
className="shadow-border ring-1 ring-border" className="shadow-border ring-1 ring-border"
@ -296,11 +482,10 @@ export function ExportPage() {
<> <>
{/* Top row: Dropdowns + metadata | Guide */} {/* Top row: Dropdowns + metadata | Guide */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 md:gap-8"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2 md:gap-8">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-2">
{/* Training run dropdown */} <div className="flex items-end justify-between">
<div data-tour="export-training-run" className="flex flex-col gap-2">
<label className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground"> <label className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
Training Run {sourceMode === "checkpoint" ? "Training Run" : "Model Source"}
<Tooltip> <Tooltip>
<TooltipTrigger asChild={true}> <TooltipTrigger asChild={true}>
<button <button
@ -314,147 +499,415 @@ export function ExportPage() {
</button> </button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
Select the training run that produced the checkpoints {sourceMode === "checkpoint"
you want to export. ? "Select the training run that produced the checkpoints you want to export."
: "Select a Hugging Face model or local model path to export directly to GGUF."}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</label> </label>
<Select <button
value={selectedModelIdx ?? ""} type="button"
onValueChange={setSelectedModelIdx} onClick={() =>
handleSourceModeSwitch(
sourceMode === "checkpoint" ? "model" : "checkpoint",
)
}
className="text-xs text-primary underline cursor-pointer leading-none"
> >
<SelectTrigger className="w-full"> {sourceMode === "checkpoint"
<SelectValue ? "Use Hugging Face / Local Model"
placeholder={ : "Use Training Checkpoints"}
models.length === 0 </button>
? "No training runs found" </div>
: "Select a training run…"
} <AnimatePresence mode="wait" initial={false}>
/> {sourceMode === "checkpoint" ? (
</SelectTrigger> <motion.div
<SelectContent> key="checkpoint"
{models.map((m) => { initial={{ height: 0, opacity: 0 }}
const tsMatch = m.name.match(/_(\d{10,})$/); animate={{ height: "auto", opacity: 1 }}
const displayName = tsMatch ? m.name.slice(0, tsMatch.index) : m.name; exit={{ height: 0, opacity: 0 }}
const timeStr = tsMatch transition={{ duration: 0.25, ease: [0.25, 0.1, 0.25, 1] }}
? new Date(Number(tsMatch[1]) * 1000).toLocaleString(undefined, { className="flex flex-col gap-2 overflow-visible"
dateStyle: "medium", >
timeStyle: "short", <div data-tour="export-training-run" className="flex flex-col gap-2">
}) <Select
: null; value={selectedModelIdx ?? ""}
return ( onValueChange={setSelectedModelIdx}
<SelectItem key={m.name} value={m.name}> >
<span className="flex items-center gap-2"> <SelectTrigger className="w-full">
{displayName} <SelectValue
<span className="text-muted-foreground text-xs"> placeholder={
{m.checkpoints.length} checkpoint models.length === 0
{m.checkpoints.length !== 1 ? "s" : ""} ? "No training runs found"
</span> : "Select a training run…"
{timeStr && ( }
<span className="text-muted-foreground text-xs"> />
· {timeStr} </SelectTrigger>
<SelectContent>
{models.map((m) => {
const tsMatch = m.name.match(/_(\d{10,})$/);
const displayName = tsMatch
? m.name.slice(0, tsMatch.index)
: m.name;
const timeStr = tsMatch
? new Date(Number(tsMatch[1]) * 1000).toLocaleString(
undefined,
{
dateStyle: "medium",
timeStyle: "short",
},
)
: null;
return (
<SelectItem key={m.name} value={m.name}>
<span className="flex items-center gap-2">
{displayName}
<span className="text-muted-foreground text-xs">
{m.checkpoints.length} checkpoint
{m.checkpoints.length !== 1 ? "s" : ""}
</span>
{timeStr && (
<span className="text-muted-foreground text-xs">
· {timeStr}
</span>
)}
</span> </span>
)} </SelectItem>
</span> );
</SelectItem> })}
); </SelectContent>
})} </Select>
</SelectContent> </div>
</Select>
</div>
{/* Checkpoint dropdown */} <div data-tour="export-checkpoint" className="flex flex-col gap-2">
<div data-tour="export-checkpoint" className="flex flex-col gap-2"> <label className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
<label className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground"> Checkpoint
Checkpoint <Tooltip>
<Tooltip> <TooltipTrigger asChild={true}>
<TooltipTrigger asChild={true}> <button
<button type="button"
type="button" className="text-foreground/70 hover:text-foreground"
className="text-foreground/70 hover:text-foreground" >
> <HugeiconsIcon
<HugeiconsIcon icon={InformationCircleIcon}
icon={InformationCircleIcon} className="size-3"
className="size-3" />
</button>
</TooltipTrigger>
<TooltipContent>
Choose a saved checkpoint to export. Lower loss
generally means better quality.{" "}
<a
href="https://unsloth.ai/docs/basics/inference-and-deployment"
target="_blank"
rel="noopener noreferrer"
className="text-primary underline"
>
Read more
</a>
</TooltipContent>
</Tooltip>
</label>
<Select
value={checkpoint ?? ""}
onValueChange={setCheckpoint}
disabled={!selectedModelIdx}
>
<SelectTrigger className="w-full">
<SelectValue
placeholder={
!selectedModelIdx
? "Select a training run first"
: checkpointsForModel.length === 0
? "No checkpoints found"
: "Select a checkpoint…"
}
/> />
</button> </SelectTrigger>
</TooltipTrigger> <SelectContent>
<TooltipContent> {checkpointsForModel.map((cp) => (
Choose a saved checkpoint to export. Lower loss <SelectItem key={cp.path} value={cp.display_name}>
generally means better quality.{" "} <span className="flex items-center gap-2">
<a {cp.display_name}
href="https://unsloth.ai/docs/basics/inference-and-deployment" {cp.loss != null && (
target="_blank" <span className="text-muted-foreground text-xs">
rel="noopener noreferrer" loss: {cp.loss.toFixed(4)}
className="text-primary underline" </span>
> )}
Read more
</a>
</TooltipContent>
</Tooltip>
</label>
<Select
value={checkpoint ?? ""}
onValueChange={setCheckpoint}
disabled={!selectedModelIdx}
>
<SelectTrigger className="w-full">
<SelectValue
placeholder={
!selectedModelIdx
? "Select a training run first"
: checkpointsForModel.length === 0
? "No checkpoints found"
: "Select a checkpoint…"
}
/>
</SelectTrigger>
<SelectContent>
{checkpointsForModel.map((cp) => (
<SelectItem key={cp.path} value={cp.display_name}>
<span className="flex items-center gap-2">
{cp.display_name}
{cp.loss != null && (
<span className="text-muted-foreground text-xs">
loss: {cp.loss.toFixed(4)}
</span> </span>
)} </SelectItem>
</span> ))}
</SelectItem> </SelectContent>
))} </Select>
</SelectContent> </div>
</Select> </motion.div>
</div> ) : (
<motion.div
key="model"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25, ease: [0.25, 0.1, 0.25, 1] }}
className="flex flex-col gap-2 overflow-visible"
>
<div className="flex gap-2">
<Button
variant={modelSource === "hf" ? "dark" : "outline"}
className="flex-1"
onClick={() => setModelSource("hf")}
>
Hugging Face
</Button>
<Button
variant={modelSource === "local" ? "dark" : "outline"}
className="flex-1"
onClick={() => setModelSource("local")}
>
Local Model
</Button>
</div>
<div className="rounded-xl bg-muted/50 p-3 flex flex-col gap-2"> {modelSource === "hf" ? (
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider"> <>
Training Info <div className="flex flex-col gap-2">
</span> <label className="text-xs font-medium text-muted-foreground">
<div className="grid grid-cols-1 gap-x-6 gap-y-1.5 text-xs sm:grid-cols-2"> Hugging Face Model
<div className="flex justify-between"> </label>
<span className="text-muted-foreground">Base Model</span> <div ref={hfComboboxAnchorRef}>
<span className="font-medium">{baseModelName}</span> <Combobox
</div> items={hfResultIds}
<div className="flex justify-between"> filteredItems={hfResultIds}
<span className="text-muted-foreground">Method</span> filter={null}
<span className="font-medium"> value={selectedSourceModel}
{trainingMethodLabel} onValueChange={setSelectedSourceModel}
</span> onInputValueChange={(val) => {
</div> setModelInput(val);
<div className="flex justify-between"> if (!val.trim()) setSelectedSourceModel(null);
<span className="text-muted-foreground">Checkpoints</span> }}
<span className="font-medium"> itemToStringValue={(id) => id}
{checkpointsForModel.length} autoHighlight={true}
</span> >
</div> <ComboboxInput placeholder="Search models..." className="w-full">
{isAdapter && ( <InputGroupAddon>
<div className="flex justify-between"> <HugeiconsIcon icon={Search01Icon} className="size-4" />
<span className="text-muted-foreground">LoRA Rank</span> </InputGroupAddon>
<span className="font-medium">{loraRank}</span> </ComboboxInput>
<ComboboxContent anchor={hfComboboxAnchorRef}>
{isLoadingHfModels ? (
<div className="flex items-center justify-center py-4 gap-2 text-xs text-muted-foreground">
<Spinner className="size-4" /> Searching
</div>
) : (
<ComboboxEmpty>No models found</ComboboxEmpty>
)}
<ComboboxList className="p-1 !max-h-none !overflow-visible">
{(id: string) => (
<ComboboxItem key={id} value={id} className="gap-2">
<span className="block min-w-0 flex-1 truncate">
{id}
</span>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
{(tokenValidationError ?? hfSearchError) && (
<p className="text-xs text-destructive">
{tokenValidationError ?? hfSearchError}
</p>
)}
</div>
<div className="flex items-center gap-2">
<Switch
id="hf-export-trust-remote-code"
size="sm"
checked={hfExportTrustRemoteCode}
onCheckedChange={setHfExportTrustRemoteCode}
disabled={exporting}
/>
<label
htmlFor="hf-export-trust-remote-code"
className="cursor-pointer text-xs font-medium text-muted-foreground hover:text-foreground"
>
Trust remote code
</label>
<Tooltip>
<TooltipTrigger asChild={true}>
<button
type="button"
className="text-muted-foreground hover:text-foreground -m-1 inline-flex rounded p-1"
aria-label="About trust remote code"
>
<HugeiconsIcon
icon={InformationCircleIcon}
className="size-3.5"
/>
</button>
</TooltipTrigger>
<TooltipContent
side="top"
className="max-w-[260px] text-xs"
>
Loads custom Python from the repo if the model
needs it. Turn off if you do not trust the
source.
</TooltipContent>
</Tooltip>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium text-muted-foreground">
Hugging Face Token (Optional)
</label>
<InputGroup>
<InputGroupAddon>
<HugeiconsIcon icon={Key01Icon} className="size-4" />
</InputGroupAddon>
<InputGroupInput
type="password"
autoComplete="new-password"
name="hf-token-export-source"
placeholder="hf_..."
value={hfToken}
onChange={(e) => setHfToken(e.target.value)}
/>
</InputGroup>
{isCheckingToken && (
<p className="text-xs text-muted-foreground">Checking token</p>
)}
</div>
</>
) : (
<div className="flex flex-col gap-2">
<label className="text-xs font-medium text-muted-foreground">
Local Model Path
</label>
<div ref={localComboboxAnchorRef}>
<Combobox
items={localResultIds}
filteredItems={localFilteredIds}
filter={null}
value={localModelInput || null}
onValueChange={(id) => {
const next = id ?? "";
setLocalModelInput(next);
setSelectedSourceModel(next || null);
}}
onInputValueChange={setLocalModelInput}
itemToStringValue={(id) => id}
autoHighlight={true}
>
<ComboboxInput
placeholder={
isLoadingLocalModels
? "Scanning local and cached models..."
: "./models/my-model"
}
className="w-full"
onBlur={() =>
setSelectedSourceModel(localModelInput.trim() || null)
}
onKeyDown={(event) => {
if (event.key !== "Enter") return;
event.preventDefault();
setSelectedSourceModel(localModelInput.trim() || null);
}}
>
<InputGroupAddon>
<HugeiconsIcon icon={FolderSearchIcon} className="size-4" />
</InputGroupAddon>
</ComboboxInput>
<ComboboxContent anchor={localComboboxAnchorRef}>
{isLoadingLocalModels ? (
<div className="flex items-center justify-center gap-2 py-4 text-xs text-muted-foreground">
<Spinner className="size-4" /> Scanning...
</div>
) : localModelsError ? (
<div className="px-3 py-2 text-xs text-red-500">
{localModelsError}
</div>
) : (
<ComboboxEmpty>No local models found</ComboboxEmpty>
)}
<ComboboxList className="p-1 !max-h-none !overflow-visible">
{(id: string) => {
const model = localMetaById.get(id);
const source =
model?.source === "hf_cache"
? "HF cache"
: "Local dir";
return (
<ComboboxItem key={id} value={id} className="gap-2">
<span className="block min-w-0 flex-1 truncate">
{model?.display_name ?? id}
</span>
<span className="ml-auto shrink-0 text-[10px] text-muted-foreground">
{source}
</span>
</ComboboxItem>
);
}}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
{isLoadingLocalModels ? (
<p className="text-[10px] text-muted-foreground">
Scanning local models...
</p>
) : localModelsError ? (
<p className="text-[10px] text-red-500">{localModelsError}</p>
) : (
<p className="text-[10px] text-muted-foreground">
{exportableLocalModels.length > 0
? `${exportableLocalModels.length} local/cached models found`
: "No local models found. Enter path manually."}
</p>
)}
</div> </div>
)} )}
<div className="rounded-xl bg-muted/50 p-3">
<p className="text-[11px] text-muted-foreground">
Direct model exports currently support GGUF only.
</p>
</div>
</motion.div>
)}
</AnimatePresence>
{sourceMode === "checkpoint" && (
<div className="rounded-xl bg-muted/50 p-3 flex flex-col gap-2">
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
Training Info
</span>
<div className="grid grid-cols-1 gap-x-6 gap-y-1.5 text-xs sm:grid-cols-2">
<div className="flex justify-between">
<span className="text-muted-foreground">Base Model</span>
<span className="font-medium">{baseModelName}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Method</span>
<span className="font-medium">
{trainingMethodLabel}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Checkpoints</span>
<span className="font-medium">
{checkpointsForModel.length}
</span>
</div>
{isAdapter && (
<div className="flex justify-between">
<span className="text-muted-foreground">LoRA Rank</span>
<span className="font-medium">{loraRank}</span>
</div>
)}
</div>
</div> </div>
</div> )}
</div> </div>
<div className="flex flex-col gap-2.5"> <div className="flex flex-col gap-2.5">
@ -462,7 +915,7 @@ export function ExportPage() {
Quick Guide Quick Guide
</span> </span>
<ol className="flex flex-col gap-3"> <ol className="flex flex-col gap-3">
{GUIDE_STEPS.map((step, i) => ( {exportGuideSteps.map((step, i) => (
<li <li
key={step} key={step}
className="flex items-start gap-2 text-xs text-muted-foreground" className="flex items-start gap-2 text-xs text-muted-foreground"
@ -483,22 +936,24 @@ export function ExportPage() {
disabledMethods={ disabledMethods={
!isAdapter && isQuantized !isAdapter && isQuantized
? ["merged", "lora", "gguf"] ? ["merged", "lora", "gguf"]
: !isAdapter : !isAdapter || sourceMode === "model"
? ["merged", "lora"] ? ["merged", "lora"]
: [] : []
} }
disabledReason={ disabledReason={
!isAdapter && isQuantized !isAdapter && isQuantized
? "Pre-quantized (BNB 4-bit) models cannot be exported without LoRA adapters" ? "Pre-quantized (BNB 4-bit) models cannot be exported without LoRA adapters"
: !isAdapter : sourceMode === "model"
? "Not available for full fine-tune checkpoints (no LoRA adapters)" ? "Only GGUF export is available for direct model export"
: undefined : !isAdapter
? "Not available for full fine-tune checkpoints (no LoRA adapters)"
: undefined
} }
/> />
<AnimatePresence> <AnimatePresence>
{exportMethod === "gguf" && ( {exportMethod === "gguf" && (
<motion.div {...collapseAnim} className="overflow-hidden"> <motion.div {...collapseAnim} className="overflow-visible">
<QuantPicker value={quantLevels} onChange={setQuantLevels} /> <QuantPicker value={quantLevels} onChange={setQuantLevels} />
</motion.div> </motion.div>
)} )}
@ -530,12 +985,12 @@ export function ExportPage() {
<ExportDialog <ExportDialog
open={dialogOpen} open={dialogOpen}
onOpenChange={setDialogOpen} onOpenChange={setDialogOpen}
checkpoint={checkpoint}
exportMethod={exportMethod} exportMethod={exportMethod}
quantLevels={quantLevels} quantLevels={quantLevels}
estimatedSize={estimatedSize} estimatedSize={estimatedSize}
baseModelName={baseModelName} checkpoint={selectedExportSource}
isAdapter={isAdapter} baseModelName={sourceBaseModelName}
isAdapter={sourceMode === "checkpoint" && isAdapter}
destination={destination} destination={destination}
onDestinationChange={setDestination} onDestinationChange={setDestination}
hfUsername={hfUsername} hfUsername={hfUsername}