refactor: simplify model and dataset combobox logic, remove curated items, and streamline search handling across components
This commit is contained in:
parent
e423174c0b
commit
45df407b78
9 changed files with 139 additions and 462 deletions
|
|
@ -4,7 +4,7 @@ import type * as React from "react";
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
delayDuration = 400,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import {
|
|||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { DATASETS } from "@/config/training";
|
||||
import {
|
||||
useDebouncedValue,
|
||||
useHfDatasetSearch,
|
||||
|
|
@ -86,88 +85,24 @@ export function DatasetStep() {
|
|||
);
|
||||
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const selectingRef = useRef(false);
|
||||
const debouncedQuery = useDebouncedValue(inputValue);
|
||||
const {
|
||||
results: hfResults,
|
||||
isLoading,
|
||||
isLoadingMore,
|
||||
hasMore,
|
||||
fetchMore,
|
||||
} = useHfDatasetSearch(debouncedQuery, {
|
||||
accessToken: hfToken || undefined,
|
||||
});
|
||||
|
||||
const curatedDatasets = useMemo(
|
||||
() =>
|
||||
[...DATASETS].sort(
|
||||
(a, b) => (b.recommended ? 1 : 0) - (a.recommended ? 1 : 0),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const datasetMap = useMemo(() => {
|
||||
const map = new Map<
|
||||
string,
|
||||
{
|
||||
label: string;
|
||||
description?: string;
|
||||
size?: string;
|
||||
totalExamples?: number;
|
||||
sizeCategory?: string;
|
||||
downloads?: number;
|
||||
recommended?: boolean;
|
||||
}
|
||||
>();
|
||||
for (const d of curatedDatasets) {
|
||||
map.set(d.id, {
|
||||
label: d.name,
|
||||
description: d.description,
|
||||
size: d.size,
|
||||
recommended: d.recommended,
|
||||
});
|
||||
}
|
||||
for (const r of hfResults) {
|
||||
if (!map.has(r.id)) {
|
||||
map.set(r.id, {
|
||||
label: r.id,
|
||||
downloads: r.downloads,
|
||||
totalExamples: r.totalExamples,
|
||||
sizeCategory: r.sizeCategory,
|
||||
});
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [curatedDatasets, hfResults]);
|
||||
|
||||
const displayIds = useMemo(() => {
|
||||
if (!debouncedQuery.trim()) {
|
||||
return curatedDatasets.map((d) => d.id);
|
||||
}
|
||||
const q = debouncedQuery.toLowerCase();
|
||||
const curatedIds = curatedDatasets
|
||||
.filter(
|
||||
(d) =>
|
||||
d.name.toLowerCase().includes(q) || d.id.toLowerCase().includes(q),
|
||||
)
|
||||
.map((d) => d.id);
|
||||
const liveIds = hfResults
|
||||
.map((r) => r.id)
|
||||
.filter((id) => !curatedIds.includes(id));
|
||||
return [...curatedIds, ...liveIds];
|
||||
}, [debouncedQuery, curatedDatasets, hfResults]);
|
||||
|
||||
const allIds = useMemo(
|
||||
() => [
|
||||
...new Set([
|
||||
...curatedDatasets.map((d) => d.id),
|
||||
...hfResults.map((r) => r.id),
|
||||
]),
|
||||
],
|
||||
[curatedDatasets, hfResults],
|
||||
);
|
||||
const resultIds = useMemo(() => hfResults.map((r) => r.id), [hfResults]);
|
||||
|
||||
const comboboxAnchorRef = useRef<HTMLDivElement>(null);
|
||||
const { scrollRef, sentinelRef } = useInfiniteScroll(fetchMore);
|
||||
const { scrollRef, sentinelRef } = useInfiniteScroll(
|
||||
fetchMore,
|
||||
hfResults.length,
|
||||
);
|
||||
|
||||
const handleFileUpload = () => {
|
||||
setUploadedFile("my_dataset.jsonl");
|
||||
|
|
@ -239,13 +174,13 @@ export function DatasetStep() {
|
|||
<FieldLabel>Search datasets</FieldLabel>
|
||||
<div ref={comboboxAnchorRef}>
|
||||
<Combobox
|
||||
items={allIds}
|
||||
filteredItems={displayIds}
|
||||
items={resultIds}
|
||||
filteredItems={resultIds}
|
||||
filter={null}
|
||||
value={dataset}
|
||||
onValueChange={(id) => setDataset(id)}
|
||||
onInputValueChange={(val) => setInputValue(val)}
|
||||
itemToStringValue={(id) => datasetMap.get(id)?.label ?? id}
|
||||
onValueChange={(id) => { selectingRef.current = true; setDataset(id); }}
|
||||
onInputValueChange={(val) => { if (selectingRef.current) { selectingRef.current = false; return; } setInputValue(val); }}
|
||||
itemToStringValue={(id) => id}
|
||||
autoHighlight={true}
|
||||
>
|
||||
<ComboboxInput
|
||||
|
|
@ -259,7 +194,7 @@ export function DatasetStep() {
|
|||
<ComboboxContent anchor={comboboxAnchorRef}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-4 gap-2 text-xs text-muted-foreground">
|
||||
<Spinner className="size-4" /> Searching…
|
||||
<Spinner className="size-4" /> Searching...
|
||||
</div>
|
||||
) : (
|
||||
<ComboboxEmpty>No datasets found</ComboboxEmpty>
|
||||
|
|
@ -270,13 +205,10 @@ export function DatasetStep() {
|
|||
>
|
||||
<ComboboxList className="p-1 !max-h-none !overflow-visible">
|
||||
{(id: string) => {
|
||||
const meta = datasetMap.get(id);
|
||||
const label = meta?.label ?? id;
|
||||
const rowLabel =
|
||||
meta?.size ??
|
||||
(meta?.totalExamples
|
||||
? `${formatCompact(meta.totalExamples)} rows`
|
||||
: null);
|
||||
const r = hfResults.find((r) => r.id === id);
|
||||
const detail = r?.totalExamples
|
||||
? `${formatCompact(r.totalExamples)} rows`
|
||||
: (r?.sizeCategory ?? null);
|
||||
return (
|
||||
<ComboboxItem
|
||||
key={id}
|
||||
|
|
@ -284,41 +216,32 @@ export function DatasetStep() {
|
|||
className="justify-between"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<div className="flex flex-col gap-0.5 min-w-0 flex-1">
|
||||
<span className="truncate">{label}</span>
|
||||
{meta?.description && (
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
{meta.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{id}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="left"
|
||||
className="max-w-xs break-all"
|
||||
>
|
||||
{label}
|
||||
{id}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{rowLabel ? (
|
||||
<Badge variant="outline" className="shrink-0">
|
||||
{rowLabel}
|
||||
</Badge>
|
||||
) : meta?.sizeCategory ? (
|
||||
{detail ? (
|
||||
<span className="text-[10px] text-muted-foreground shrink-0">
|
||||
{meta.sizeCategory}
|
||||
{detail}
|
||||
</span>
|
||||
) : meta?.downloads != null ? (
|
||||
) : r?.downloads != null ? (
|
||||
<span className="text-[10px] text-muted-foreground shrink-0">
|
||||
↓{formatCompact(meta.downloads)}
|
||||
↓{formatCompact(r.downloads)}
|
||||
</span>
|
||||
) : null}
|
||||
</ComboboxItem>
|
||||
);
|
||||
}}
|
||||
</ComboboxList>
|
||||
{hasMore && <div ref={sentinelRef} className="h-px" />}
|
||||
<div ref={sentinelRef} className="h-px" />
|
||||
{isLoadingMore && (
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<Spinner className="size-3.5 text-muted-foreground" />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxContent,
|
||||
|
|
@ -31,7 +30,7 @@ import {
|
|||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { MODELS, MODEL_TYPE_TO_HF_TASK } from "@/config/training";
|
||||
import { MODEL_TYPE_TO_HF_TASK } from "@/config/training";
|
||||
import {
|
||||
useDebouncedValue,
|
||||
useHfModelSearch,
|
||||
|
|
@ -71,92 +70,26 @@ export function ModelSelectionStep() {
|
|||
);
|
||||
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const selectingRef = useRef(false);
|
||||
const debouncedQuery = useDebouncedValue(inputValue);
|
||||
const task = modelType ? MODEL_TYPE_TO_HF_TASK[modelType] : undefined;
|
||||
const {
|
||||
results: hfResults,
|
||||
isLoading,
|
||||
isLoadingMore,
|
||||
hasMore,
|
||||
fetchMore,
|
||||
} = useHfModelSearch(debouncedQuery, {
|
||||
task,
|
||||
accessToken: hfToken || undefined,
|
||||
});
|
||||
|
||||
const curatedModels = useMemo(() => {
|
||||
if (!modelType) {
|
||||
return [];
|
||||
}
|
||||
return MODELS.filter((m) => m.type === modelType).sort(
|
||||
(a, b) => (b.recommended ? 1 : 0) - (a.recommended ? 1 : 0),
|
||||
);
|
||||
}, [modelType]);
|
||||
const resultIds = useMemo(() => hfResults.map((r) => r.id), [hfResults]);
|
||||
|
||||
const modelMap = useMemo(() => {
|
||||
const map = new Map<
|
||||
string,
|
||||
{
|
||||
label: string;
|
||||
params?: string;
|
||||
totalParams?: number;
|
||||
downloads?: number;
|
||||
recommended?: boolean;
|
||||
}
|
||||
>();
|
||||
for (const m of curatedModels) {
|
||||
map.set(m.hfRepo ?? m.id, {
|
||||
label: m.name,
|
||||
params: m.params,
|
||||
recommended: m.recommended,
|
||||
});
|
||||
}
|
||||
for (const r of hfResults) {
|
||||
if (!map.has(r.id)) {
|
||||
map.set(r.id, {
|
||||
label: r.id,
|
||||
downloads: r.downloads,
|
||||
totalParams: r.totalParams,
|
||||
});
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [curatedModels, hfResults]);
|
||||
|
||||
const displayIds = useMemo(() => {
|
||||
if (!debouncedQuery.trim()) {
|
||||
return curatedModels.map((m) => m.hfRepo ?? m.id);
|
||||
}
|
||||
const q = debouncedQuery.toLowerCase();
|
||||
const curatedIds = curatedModels
|
||||
.filter(
|
||||
(m) =>
|
||||
m.name.toLowerCase().includes(q) ||
|
||||
m.id.toLowerCase().includes(q) ||
|
||||
m.hfRepo?.toLowerCase().includes(q),
|
||||
)
|
||||
.map((m) => m.hfRepo ?? m.id);
|
||||
const liveIds = hfResults
|
||||
.map((r) => r.id)
|
||||
.filter((id) => !curatedIds.includes(id));
|
||||
return [...curatedIds, ...liveIds];
|
||||
}, [debouncedQuery, curatedModels, hfResults]);
|
||||
|
||||
const allIds = useMemo(
|
||||
() => [
|
||||
...new Set([
|
||||
...curatedModels.map((m) => m.hfRepo ?? m.id),
|
||||
...hfResults.map((r) => r.id),
|
||||
]),
|
||||
],
|
||||
[curatedModels, hfResults],
|
||||
);
|
||||
|
||||
const selectedModelData = MODELS.find(
|
||||
(m) => m.id === selectedModel || m.hfRepo === selectedModel,
|
||||
);
|
||||
const comboboxAnchorRef = useRef<HTMLDivElement>(null);
|
||||
const { scrollRef, sentinelRef } = useInfiniteScroll(fetchMore);
|
||||
const { scrollRef, sentinelRef } = useInfiniteScroll(
|
||||
fetchMore,
|
||||
hfResults.length,
|
||||
);
|
||||
|
||||
return (
|
||||
<FieldGroup>
|
||||
|
|
@ -219,13 +152,13 @@ export function ModelSelectionStep() {
|
|||
</FieldLabel>
|
||||
<div ref={comboboxAnchorRef}>
|
||||
<Combobox
|
||||
items={allIds}
|
||||
filteredItems={displayIds}
|
||||
items={resultIds}
|
||||
filteredItems={resultIds}
|
||||
filter={null}
|
||||
value={selectedModel}
|
||||
onValueChange={(id) => setSelectedModel(id)}
|
||||
onInputValueChange={(val) => setInputValue(val)}
|
||||
itemToStringValue={(id) => modelMap.get(id)?.label ?? id}
|
||||
onValueChange={(id) => { selectingRef.current = true; setSelectedModel(id); }}
|
||||
onInputValueChange={(val) => { if (selectingRef.current) { selectingRef.current = false; return; } setInputValue(val); }}
|
||||
itemToStringValue={(id) => id}
|
||||
autoHighlight={true}
|
||||
>
|
||||
<ComboboxInput placeholder="Search models..." className="w-full">
|
||||
|
|
@ -247,13 +180,10 @@ export function ModelSelectionStep() {
|
|||
>
|
||||
<ComboboxList className="p-1 !max-h-none !overflow-visible">
|
||||
{(id: string) => {
|
||||
const meta = modelMap.get(id);
|
||||
const label = meta?.label ?? id;
|
||||
const sizeLabel =
|
||||
meta?.params ??
|
||||
(meta?.totalParams
|
||||
? formatCompact(meta.totalParams)
|
||||
: null);
|
||||
const r = hfResults.find((r) => r.id === id);
|
||||
const sizeLabel = r?.totalParams
|
||||
? formatCompact(r.totalParams)
|
||||
: null;
|
||||
return (
|
||||
<ComboboxItem
|
||||
key={id}
|
||||
|
|
@ -263,38 +193,30 @@ export function ModelSelectionStep() {
|
|||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{label}
|
||||
{id}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="left"
|
||||
className="max-w-xs break-all"
|
||||
>
|
||||
{label}
|
||||
{id}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className="flex items-center gap-1.5 shrink-0">
|
||||
{meta?.recommended && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px] px-1.5 py-0 text-emerald-600 border-emerald-200 dark:border-emerald-800 dark:text-emerald-400"
|
||||
>
|
||||
Recommended
|
||||
</Badge>
|
||||
)}
|
||||
{sizeLabel ? (
|
||||
<Badge variant="outline">{sizeLabel}</Badge>
|
||||
) : meta?.downloads != null ? (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
↓{formatCompact(meta.downloads)}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{sizeLabel ? (
|
||||
<span className="text-xs text-muted-foreground shrink-0">
|
||||
{sizeLabel}
|
||||
</span>
|
||||
) : r?.downloads != null ? (
|
||||
<span className="text-[10px] text-muted-foreground shrink-0">
|
||||
↓{formatCompact(r.downloads)}
|
||||
</span>
|
||||
) : null}
|
||||
</ComboboxItem>
|
||||
);
|
||||
}}
|
||||
</ComboboxList>
|
||||
{hasMore && <div ref={sentinelRef} className="h-px" />}
|
||||
<div ref={sentinelRef} className="h-px" />
|
||||
{isLoadingMore && (
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<Spinner className="size-3.5 text-muted-foreground" />
|
||||
|
|
@ -306,7 +228,7 @@ export function ModelSelectionStep() {
|
|||
</div>
|
||||
</Field>
|
||||
|
||||
{(selectedModelData || selectedModel) && (
|
||||
{selectedModel && (
|
||||
<Field>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
|
|
@ -340,8 +262,7 @@ export function ModelSelectionStep() {
|
|||
</Tooltip>
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Choose how to fine-tune{" "}
|
||||
{selectedModelData?.name ?? selectedModel}
|
||||
Choose how to fine-tune {selectedModel}
|
||||
</FieldDescription>
|
||||
</div>
|
||||
<Select
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
import { SectionCard } from "@/components/section-card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxContent,
|
||||
|
|
@ -27,7 +22,6 @@ import {
|
|||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { DATASETS } from "@/config/training";
|
||||
import {
|
||||
useDebouncedValue,
|
||||
useHfDatasetSearch,
|
||||
|
|
@ -36,7 +30,6 @@ import {
|
|||
import { formatCompact } from "@/lib/utils";
|
||||
import { useWizardStore } from "@/stores/training";
|
||||
import {
|
||||
ArrowDown01Icon,
|
||||
CloudUploadIcon,
|
||||
Database02Icon,
|
||||
FileAttachmentIcon,
|
||||
|
|
@ -59,89 +52,26 @@ export function DatasetSection() {
|
|||
hfToken: s.hfToken,
|
||||
})),
|
||||
);
|
||||
const [recOpen, setRecOpen] = useState(false);
|
||||
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const selectingRef = useRef(false);
|
||||
const debouncedQuery = useDebouncedValue(inputValue);
|
||||
const {
|
||||
results: hfResults,
|
||||
isLoading,
|
||||
isLoadingMore,
|
||||
hasMore,
|
||||
fetchMore,
|
||||
} = useHfDatasetSearch(debouncedQuery, {
|
||||
accessToken: hfToken || undefined,
|
||||
});
|
||||
|
||||
const curatedDatasets = useMemo(
|
||||
() =>
|
||||
[...DATASETS].sort(
|
||||
(a, b) => (b.recommended ? 1 : 0) - (a.recommended ? 1 : 0),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const datasetMap = useMemo(() => {
|
||||
const map = new Map<
|
||||
string,
|
||||
{
|
||||
label: string;
|
||||
description?: string;
|
||||
size?: string;
|
||||
totalExamples?: number;
|
||||
sizeCategory?: string;
|
||||
downloads?: number;
|
||||
}
|
||||
>();
|
||||
for (const d of curatedDatasets) {
|
||||
map.set(d.id, {
|
||||
label: d.name,
|
||||
description: d.description,
|
||||
size: d.size,
|
||||
});
|
||||
}
|
||||
for (const r of hfResults) {
|
||||
if (!map.has(r.id)) {
|
||||
map.set(r.id, {
|
||||
label: r.id,
|
||||
downloads: r.downloads,
|
||||
totalExamples: r.totalExamples,
|
||||
sizeCategory: r.sizeCategory,
|
||||
});
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [curatedDatasets, hfResults]);
|
||||
|
||||
const displayIds = useMemo(() => {
|
||||
if (!debouncedQuery.trim()) {
|
||||
return curatedDatasets.map((d) => d.id);
|
||||
}
|
||||
const q = debouncedQuery.toLowerCase();
|
||||
const curatedIds = curatedDatasets
|
||||
.filter(
|
||||
(d) =>
|
||||
d.name.toLowerCase().includes(q) || d.id.toLowerCase().includes(q),
|
||||
)
|
||||
.map((d) => d.id);
|
||||
const liveIds = hfResults
|
||||
.map((r) => r.id)
|
||||
.filter((id) => !curatedIds.includes(id));
|
||||
return [...curatedIds, ...liveIds];
|
||||
}, [debouncedQuery, curatedDatasets, hfResults]);
|
||||
|
||||
const allIds = useMemo(
|
||||
() => [
|
||||
...new Set([
|
||||
...curatedDatasets.map((d) => d.id),
|
||||
...hfResults.map((r) => r.id),
|
||||
]),
|
||||
],
|
||||
[curatedDatasets, hfResults],
|
||||
);
|
||||
const resultIds = useMemo(() => hfResults.map((r) => r.id), [hfResults]);
|
||||
|
||||
const comboboxAnchorRef = useRef<HTMLDivElement>(null);
|
||||
const { scrollRef, sentinelRef } = useInfiniteScroll(fetchMore);
|
||||
const { scrollRef, sentinelRef } = useInfiniteScroll(
|
||||
fetchMore,
|
||||
hfResults.length,
|
||||
);
|
||||
|
||||
return (
|
||||
<SectionCard
|
||||
|
|
@ -184,13 +114,13 @@ export function DatasetSection() {
|
|||
</span>
|
||||
<div ref={comboboxAnchorRef}>
|
||||
<Combobox
|
||||
items={allIds}
|
||||
filteredItems={displayIds}
|
||||
items={resultIds}
|
||||
filteredItems={resultIds}
|
||||
filter={null}
|
||||
value={dataset}
|
||||
onValueChange={(id) => setDataset(id)}
|
||||
onInputValueChange={(val) => setInputValue(val)}
|
||||
itemToStringValue={(id) => datasetMap.get(id)?.label ?? id}
|
||||
onValueChange={(id) => { selectingRef.current = true; setDataset(id); }}
|
||||
onInputValueChange={(val) => { if (selectingRef.current) { selectingRef.current = false; return; } setInputValue(val); }}
|
||||
itemToStringValue={(id) => id}
|
||||
autoHighlight={true}
|
||||
>
|
||||
<ComboboxInput
|
||||
|
|
@ -204,7 +134,7 @@ export function DatasetSection() {
|
|||
<ComboboxContent anchor={comboboxAnchorRef}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-4 gap-2 text-xs text-muted-foreground">
|
||||
<Spinner className="size-4" /> Searching…
|
||||
<Spinner className="size-4" /> Searching...
|
||||
</div>
|
||||
) : (
|
||||
<ComboboxEmpty>No datasets found</ComboboxEmpty>
|
||||
|
|
@ -215,13 +145,10 @@ export function DatasetSection() {
|
|||
>
|
||||
<ComboboxList className="p-1 !max-h-none !overflow-visible">
|
||||
{(id: string) => {
|
||||
const meta = datasetMap.get(id);
|
||||
const label = meta?.label ?? id;
|
||||
const rowLabel =
|
||||
meta?.size ??
|
||||
(meta?.totalExamples
|
||||
? `${formatCompact(meta.totalExamples)} rows`
|
||||
: null);
|
||||
const r = hfResults.find((r) => r.id === id);
|
||||
const detail = r?.totalExamples
|
||||
? `${formatCompact(r.totalExamples)} rows`
|
||||
: (r?.sizeCategory ?? null);
|
||||
return (
|
||||
<ComboboxItem
|
||||
key={id}
|
||||
|
|
@ -231,34 +158,30 @@ export function DatasetSection() {
|
|||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{label}
|
||||
{id}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="left"
|
||||
className="max-w-xs break-all"
|
||||
>
|
||||
{label}
|
||||
{id}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{rowLabel ? (
|
||||
<span className="text-xs text-muted-foreground shrink-0">
|
||||
{rowLabel}
|
||||
</span>
|
||||
) : meta?.sizeCategory ? (
|
||||
{detail ? (
|
||||
<span className="text-[10px] text-muted-foreground shrink-0">
|
||||
{meta.sizeCategory}
|
||||
{detail}
|
||||
</span>
|
||||
) : meta?.downloads != null ? (
|
||||
) : r?.downloads != null ? (
|
||||
<span className="text-[10px] text-muted-foreground shrink-0">
|
||||
↓{formatCompact(meta.downloads)}
|
||||
↓{formatCompact(r.downloads)}
|
||||
</span>
|
||||
) : null}
|
||||
</ComboboxItem>
|
||||
);
|
||||
}}
|
||||
</ComboboxList>
|
||||
{hasMore && <div ref={sentinelRef} className="h-px" />}
|
||||
<div ref={sentinelRef} className="h-px" />
|
||||
{isLoadingMore && (
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<Spinner className="size-3.5 text-muted-foreground" />
|
||||
|
|
@ -346,41 +269,6 @@ export function DatasetSection() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Recommended */}
|
||||
<Collapsible open={recOpen} onOpenChange={setRecOpen}>
|
||||
<CollapsibleTrigger className="flex w-full cursor-pointer items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<HugeiconsIcon
|
||||
icon={ArrowDown01Icon}
|
||||
className={`size-3.5 transition-transform ${recOpen ? "rotate-180" : ""}`}
|
||||
/>
|
||||
Common Datasets
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-3 flex flex-col gap-1.5">
|
||||
{DATASETS.filter((d) => d.recommended).map((d) => (
|
||||
<button
|
||||
type="button"
|
||||
key={d.id}
|
||||
onClick={() => setDataset(d.id)}
|
||||
className="flex w-full corner-squircle cursor-pointer items-center gap-2.5 rounded-2xl border bg-muted/30 px-3 py-2.5 text-left text-sm transition-colors hover:bg-muted/60"
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={Database02Icon}
|
||||
className="size-4 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs font-medium">{d.name}</p>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{d.description}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-[10px] font-mono text-muted-foreground mt-0.5">
|
||||
{d.size}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import {
|
|||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { MODELS, MODEL_TYPE_TO_HF_TASK } from "@/config/training";
|
||||
import { MODEL_TYPE_TO_HF_TASK } from "@/config/training";
|
||||
import {
|
||||
useDebouncedValue,
|
||||
useHfModelSearch,
|
||||
|
|
@ -78,89 +78,26 @@ export function ModelSection() {
|
|||
);
|
||||
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const selectingRef = useRef(false);
|
||||
const debouncedQuery = useDebouncedValue(inputValue);
|
||||
const task = modelType ? MODEL_TYPE_TO_HF_TASK[modelType] : undefined;
|
||||
const {
|
||||
results: hfResults,
|
||||
isLoading,
|
||||
isLoadingMore,
|
||||
hasMore,
|
||||
fetchMore,
|
||||
} = useHfModelSearch(debouncedQuery, {
|
||||
task,
|
||||
accessToken: hfToken || undefined,
|
||||
});
|
||||
|
||||
const curatedModels = useMemo(() => {
|
||||
if (!modelType) {
|
||||
return MODELS;
|
||||
}
|
||||
return MODELS.filter((m) => m.type === modelType).sort(
|
||||
(a, b) => (b.recommended ? 1 : 0) - (a.recommended ? 1 : 0),
|
||||
);
|
||||
}, [modelType]);
|
||||
|
||||
const modelMap = useMemo(() => {
|
||||
const map = new Map<
|
||||
string,
|
||||
{
|
||||
label: string;
|
||||
params?: string;
|
||||
totalParams?: number;
|
||||
downloads?: number;
|
||||
recommended?: boolean;
|
||||
}
|
||||
>();
|
||||
for (const m of curatedModels) {
|
||||
map.set(m.hfRepo ?? m.id, {
|
||||
label: m.name,
|
||||
params: m.params,
|
||||
recommended: m.recommended,
|
||||
});
|
||||
}
|
||||
for (const r of hfResults) {
|
||||
if (!map.has(r.id)) {
|
||||
map.set(r.id, {
|
||||
label: r.id,
|
||||
downloads: r.downloads,
|
||||
totalParams: r.totalParams,
|
||||
});
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [curatedModels, hfResults]);
|
||||
|
||||
const displayIds = useMemo(() => {
|
||||
if (!debouncedQuery.trim()) {
|
||||
return curatedModels.map((m) => m.hfRepo ?? m.id);
|
||||
}
|
||||
const q = debouncedQuery.toLowerCase();
|
||||
const curatedIds = curatedModels
|
||||
.filter(
|
||||
(m) =>
|
||||
m.name.toLowerCase().includes(q) ||
|
||||
m.id.toLowerCase().includes(q) ||
|
||||
m.hfRepo?.toLowerCase().includes(q),
|
||||
)
|
||||
.map((m) => m.hfRepo ?? m.id);
|
||||
const liveIds = hfResults
|
||||
.map((r) => r.id)
|
||||
.filter((id) => !curatedIds.includes(id));
|
||||
return [...curatedIds, ...liveIds];
|
||||
}, [debouncedQuery, curatedModels, hfResults]);
|
||||
|
||||
const allIds = useMemo(
|
||||
() => [
|
||||
...new Set([
|
||||
...curatedModels.map((m) => m.hfRepo ?? m.id),
|
||||
...hfResults.map((r) => r.id),
|
||||
]),
|
||||
],
|
||||
[curatedModels, hfResults],
|
||||
);
|
||||
const resultIds = useMemo(() => hfResults.map((r) => r.id), [hfResults]);
|
||||
|
||||
const comboboxAnchorRef = useRef<HTMLDivElement>(null);
|
||||
const { scrollRef, sentinelRef } = useInfiniteScroll(fetchMore);
|
||||
const { scrollRef, sentinelRef } = useInfiniteScroll(
|
||||
fetchMore,
|
||||
hfResults.length,
|
||||
);
|
||||
|
||||
return (
|
||||
<SectionCard
|
||||
|
|
@ -200,14 +137,7 @@ export function ModelSection() {
|
|||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
placeholder="./models/my-model"
|
||||
value={
|
||||
selectedModel
|
||||
? (MODELS.find(
|
||||
(m) =>
|
||||
m.id === selectedModel || m.hfRepo === selectedModel,
|
||||
)?.hfRepo ?? selectedModel)
|
||||
: ""
|
||||
}
|
||||
value={selectedModel ?? ""}
|
||||
onChange={(e) => setSelectedModel(e.target.value || null)}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
|
@ -244,13 +174,13 @@ export function ModelSection() {
|
|||
</span>
|
||||
<div ref={comboboxAnchorRef}>
|
||||
<Combobox
|
||||
items={allIds}
|
||||
filteredItems={displayIds}
|
||||
items={resultIds}
|
||||
filteredItems={resultIds}
|
||||
filter={null}
|
||||
value={selectedModel}
|
||||
onValueChange={(id) => setSelectedModel(id)}
|
||||
onInputValueChange={(val) => setInputValue(val)}
|
||||
itemToStringValue={(id) => modelMap.get(id)?.label ?? id}
|
||||
onValueChange={(id) => { selectingRef.current = true; setSelectedModel(id); }}
|
||||
onInputValueChange={(val) => { if (selectingRef.current) { selectingRef.current = false; return; } setInputValue(val); }}
|
||||
itemToStringValue={(id) => id}
|
||||
autoHighlight={true}
|
||||
>
|
||||
<ComboboxInput placeholder="Search models..." className="w-full">
|
||||
|
|
@ -272,13 +202,10 @@ export function ModelSection() {
|
|||
>
|
||||
<ComboboxList className="p-1 !max-h-none !overflow-visible">
|
||||
{(id: string) => {
|
||||
const meta = modelMap.get(id);
|
||||
const label = meta?.label ?? id;
|
||||
const sizeLabel =
|
||||
meta?.params ??
|
||||
(meta?.totalParams
|
||||
? formatCompact(meta.totalParams)
|
||||
: null);
|
||||
const r = hfResults.find((r) => r.id === id);
|
||||
const sizeLabel = r?.totalParams
|
||||
? formatCompact(r.totalParams)
|
||||
: null;
|
||||
return (
|
||||
<ComboboxItem
|
||||
key={id}
|
||||
|
|
@ -288,30 +215,30 @@ export function ModelSection() {
|
|||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{label}
|
||||
{id}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="left"
|
||||
className="max-w-xs break-all"
|
||||
>
|
||||
{label}
|
||||
{id}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{sizeLabel ? (
|
||||
<span className="text-xs text-muted-foreground shrink-0">
|
||||
{sizeLabel}
|
||||
</span>
|
||||
) : meta?.downloads != null ? (
|
||||
) : r?.downloads != null ? (
|
||||
<span className="text-[10px] text-muted-foreground shrink-0">
|
||||
↓{formatCompact(meta.downloads)}
|
||||
↓{formatCompact(r.downloads)}
|
||||
</span>
|
||||
) : null}
|
||||
</ComboboxItem>
|
||||
);
|
||||
}}
|
||||
</ComboboxList>
|
||||
{hasMore && <div ref={sentinelRef} className="h-px" />}
|
||||
<div ref={sentinelRef} className="h-px" />
|
||||
{isLoadingMore && (
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<Spinner className="size-3.5 text-muted-foreground" />
|
||||
|
|
|
|||
|
|
@ -76,12 +76,12 @@ export function useHfDatasetSearch(
|
|||
const createIter = useCallback(
|
||||
() =>
|
||||
listDatasets({
|
||||
search: { query },
|
||||
search: query.trim() ? { query } : {},
|
||||
additionalFields: ["cardData"],
|
||||
...(accessToken ? { credentials: { accessToken } } : {}),
|
||||
}) as AsyncGenerator<unknown>,
|
||||
[query, accessToken],
|
||||
);
|
||||
|
||||
return useHfPaginatedSearch(query, createIter, mapDataset);
|
||||
return useHfPaginatedSearch(createIter, mapDataset);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,30 @@ export interface HfModelResult {
|
|||
totalParams?: number;
|
||||
}
|
||||
|
||||
function mapModel(raw: unknown): HfModelResult {
|
||||
const EXCLUDED_TAGS = new Set([
|
||||
"gguf",
|
||||
"gptq",
|
||||
"awq",
|
||||
"exl2",
|
||||
"mlx",
|
||||
"onnx",
|
||||
"openvino",
|
||||
"coreml",
|
||||
"tflite",
|
||||
"ctranslate2",
|
||||
]);
|
||||
|
||||
function mapModel(raw: unknown): HfModelResult | null {
|
||||
const m = raw as {
|
||||
name: string;
|
||||
downloads: number;
|
||||
likes: number;
|
||||
safetensors?: { total: number };
|
||||
tags?: string[];
|
||||
};
|
||||
if (m.tags?.some((t) => EXCLUDED_TAGS.has(t))) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: m.name,
|
||||
downloads: m.downloads,
|
||||
|
|
@ -34,12 +51,16 @@ export function useHfModelSearch(
|
|||
const createIter = useCallback(
|
||||
() =>
|
||||
listModels({
|
||||
search: { query, ...(task ? { task } : {}) },
|
||||
additionalFields: ["safetensors"],
|
||||
search: {
|
||||
...(query.trim() ? { query } : {}),
|
||||
tags: ["transformers"],
|
||||
...(task ? { task } : {}),
|
||||
},
|
||||
additionalFields: ["safetensors", "tags"],
|
||||
...(accessToken ? { credentials: { accessToken } } : {}),
|
||||
}) as AsyncGenerator<unknown>,
|
||||
[query, task, accessToken],
|
||||
);
|
||||
|
||||
return useHfPaginatedSearch(query, createIter, mapModel);
|
||||
return useHfPaginatedSearch(createIter, mapModel);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,24 +19,26 @@ const BATCH = 20;
|
|||
|
||||
async function pullBatch<T>(
|
||||
iter: AsyncGenerator<unknown>,
|
||||
mapItem: (raw: unknown) => T,
|
||||
mapItem: (raw: unknown) => T | null,
|
||||
size: number,
|
||||
) {
|
||||
const items: T[] = [];
|
||||
for (let i = 0; i < size; i++) {
|
||||
while (items.length < size) {
|
||||
const result = await iter.next();
|
||||
if (result.done) {
|
||||
return { items, done: true };
|
||||
}
|
||||
items.push(mapItem(result.value));
|
||||
const mapped = mapItem(result.value);
|
||||
if (mapped !== null) {
|
||||
items.push(mapped);
|
||||
}
|
||||
}
|
||||
return { items, done: false };
|
||||
}
|
||||
|
||||
export function useHfPaginatedSearch<T>(
|
||||
query: string,
|
||||
createIter: () => AsyncGenerator<unknown>,
|
||||
mapItem: (raw: unknown) => T,
|
||||
mapItem: (raw: unknown) => T | null,
|
||||
): HfPaginatedState<T> & { fetchMore: () => void } {
|
||||
const [state, setState] = useState<HfPaginatedState<T>>(
|
||||
INITIAL as HfPaginatedState<T>,
|
||||
|
|
@ -51,11 +53,6 @@ export function useHfPaginatedSearch<T>(
|
|||
const v = ++versionRef.current;
|
||||
iterRef.current = null;
|
||||
|
||||
if (!query.trim()) {
|
||||
setState(INITIAL as HfPaginatedState<T>);
|
||||
return;
|
||||
}
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
results: [],
|
||||
|
|
@ -92,7 +89,7 @@ export function useHfPaginatedSearch<T>(
|
|||
error: err instanceof Error ? err.message : "Search failed",
|
||||
});
|
||||
});
|
||||
}, [query, createIter, mapItem]);
|
||||
}, [createIter, mapItem]);
|
||||
|
||||
const fetchMore = useCallback(() => {
|
||||
const iter = iterRef.current;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
|
||||
export function useInfiniteScroll(fetchMore: () => void) {
|
||||
export function useInfiniteScroll(fetchMore: () => void, itemCount: number) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ export function useInfiniteScroll(fetchMore: () => void) {
|
|||
);
|
||||
obs.observe(el);
|
||||
return () => obs.disconnect();
|
||||
}, [fetchMore]);
|
||||
}, [fetchMore, itemCount]);
|
||||
|
||||
return { scrollRef, sentinelRef };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue