feat: add image previews in dataset dialog, enable popularity sorting in model search, refine training config serialization

This commit is contained in:
Shine1i 2026-02-13 13:17:20 +01:00
commit b0062535a7
3 changed files with 103 additions and 1 deletions

View file

@ -27,6 +27,14 @@ type CheckFormatResponse = {
total_rows?: number | null;
};
type PreviewImagePayload = {
type: "image";
mime?: string;
width?: number;
height?: number;
data?: string;
};
type DatasetPreviewDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
@ -123,6 +131,36 @@ export function DatasetPreviewDialog({
),
cell: ({ getValue }: { getValue: () => unknown }) => {
const value = getValue();
const images = collectPreviewImages(value);
if (images.length > 0) {
return (
<div className="flex flex-wrap gap-2">
{images.slice(0, 4).map((image, index) => {
const mime = image.mime || "image/jpeg";
const src = image.data ? `data:${mime};base64,${image.data}` : "";
const width = image.width ?? 128;
const height = image.height ?? 128;
return (
<img
key={`${colName}-img-${index}`}
src={src}
alt={`preview-${index}`}
className="h-16 w-auto max-w-40 rounded-md border object-contain bg-muted"
width={width}
height={height}
loading="lazy"
/>
);
})}
{images.length > 4 && (
<span className="text-xs text-muted-foreground self-end">
+{images.length - 4} more
</span>
)}
</div>
);
}
const text = formatCell(value);
if (!text) {
return (
@ -285,3 +323,41 @@ function formatCell(value: unknown): string {
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;
}

View file

@ -96,6 +96,10 @@ export const useTrainingConfigStore = create<TrainingConfigStore>()(
}),
{
name: "unsloth_training_config_v1",
partialize: (state) => {
const { modelType, ...rest } = state;
return rest;
},
},
),
);

View file

@ -23,6 +23,28 @@ const EXCLUDED_TAGS = new Set([
"ctranslate2",
]);
function withPopularitySort(
input: Parameters<typeof fetch>[0],
init?: Parameters<typeof fetch>[1],
): ReturnType<typeof fetch> {
const rawUrl =
typeof input === "string"
? input
: input instanceof URL
? input.toString()
: input.url;
const url = new URL(rawUrl);
if (!url.searchParams.has("sort")) {
url.searchParams.set("sort", "downloads");
}
if (!url.searchParams.has("direction")) {
url.searchParams.set("direction", "-1");
}
return fetch(url, init);
}
function mapModel(raw: unknown): HfModelResult | null {
const m = raw as {
name: string;
@ -53,10 +75,10 @@ export function useHfModelSearch(
listModels({
search: {
...(query.trim() ? { query } : { owner: "unsloth" }),
tags: ["transformers"],
...(task ? { task } : {}),
},
additionalFields: ["safetensors", "tags"],
fetch: withPopularitySort,
...(accessToken ? { credentials: { accessToken } } : {}),
}) as AsyncGenerator<unknown>,
[query, task, accessToken],