Skip to content

Commit 6ea30d1

Browse files
authored
Display all models on single page and sync filters to URL params (#438)
Remove pagination from the models page so all models are visible at once. Sync filter state (device, family, acceleration, search, sort) to URL search params so filtered views can be shared via links. Device filters use shorthand keys (e.g. /models?cpu&gpu) while other filters use named params (e.g. ?family=phi&acceleration=cuda&q=search).
1 parent a06eef6 commit 6ea30d1

2 files changed

Lines changed: 80 additions & 57 deletions

File tree

www/src/routes/models/+page.svelte

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<script lang="ts">
22
import { onMount, onDestroy } from 'svelte';
3+
import { page } from '$app/stores';
4+
import { goto } from '$app/navigation';
35
import { foundryModelService } from './service';
46
import type { GroupedFoundryModel } from './types';
57
import Nav from '$lib/components/home/nav.svelte';
@@ -9,6 +11,9 @@
911
import { toast } from 'svelte-sonner';
1012
import { ModelFilters, ModelGrid, ModelDetailsModal } from './components';
1113
14+
// Known device names used as shorthand URL params (e.g. /models?cpu)
15+
const KNOWN_DEVICES = ['cpu', 'gpu', 'npu'];
16+
1217
// Debounce timer for search
1318
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null;
1419
let debouncedSearchTerm = '';
@@ -32,14 +37,64 @@
3237
let sortBy = 'lastModified';
3338
let sortOrder: 'asc' | 'desc' = 'desc';
3439
40+
// Track whether we've initialized filters from URL
41+
let filtersInitialized = false;
42+
// Suppress URL updates while reading from URL
43+
let suppressUrlUpdate = false;
44+
3545
// Available filter options
3646
let availableDevices: string[] = [];
3747
let availableFamilies: string[] = ['deepseek', 'mistral', 'qwen', 'phi', 'whisper'];
3848
let availableAccelerations: string[] = [];
3949
40-
// Pagination
41-
let currentPage = 1;
42-
let itemsPerPage = 12;
50+
// Read filter state from URL search params
51+
function readFiltersFromUrl() {
52+
const params = $page.url.searchParams;
53+
54+
// Device filters: shorthand keys like ?cpu, ?gpu, ?npu
55+
const devices: string[] = [];
56+
for (const device of KNOWN_DEVICES) {
57+
if (params.has(device)) {
58+
devices.push(device);
59+
}
60+
}
61+
selectedDevices = devices;
62+
63+
// Named params
64+
searchTerm = params.get('q') ?? '';
65+
debouncedSearchTerm = searchTerm;
66+
selectedFamily = params.get('family') ?? '';
67+
selectedAcceleration = params.get('acceleration') ?? '';
68+
sortBy = params.get('sort') ?? 'lastModified';
69+
sortOrder = (params.get('order') as 'asc' | 'desc') ?? 'desc';
70+
}
71+
72+
// Write current filter state to URL search params (replaceState, no navigation)
73+
function updateUrlFromFilters() {
74+
if (suppressUrlUpdate) return;
75+
76+
const params = new URLSearchParams();
77+
78+
// Device filters as shorthand keys
79+
for (const device of selectedDevices) {
80+
if (KNOWN_DEVICES.includes(device)) {
81+
params.set(device, '');
82+
}
83+
}
84+
85+
if (searchTerm) params.set('q', searchTerm);
86+
if (selectedFamily) params.set('family', selectedFamily);
87+
if (selectedAcceleration) params.set('acceleration', selectedAcceleration);
88+
if (sortBy && sortBy !== 'lastModified') params.set('sort', sortBy);
89+
if (sortOrder && sortOrder !== 'desc') params.set('order', sortOrder);
90+
91+
const search = params.toString();
92+
// Clean up empty-value params: "cpu=" -> "cpu"
93+
const cleanSearch = search.replace(/=(?=&|$)/g, '');
94+
const newUrl = cleanSearch ? `/models?${cleanSearch}` : '/models';
95+
96+
goto(newUrl, { replaceState: true, noScroll: true, keepFocus: true });
97+
}
4398
4499
// Fetch all models from API
45100
async function fetchAllModels() {
@@ -150,8 +205,6 @@
150205
151206
return sortOrder === 'asc' ? (aVal > bVal ? 1 : -1) : aVal < bVal ? 1 : -1;
152207
});
153-
154-
currentPage = 1;
155208
}
156209
157210
function clearFilters() {
@@ -162,7 +215,8 @@
162215
selectedAcceleration = '';
163216
sortBy = 'lastModified';
164217
sortOrder = 'desc';
165-
currentPage = 1;
218+
// Clear URL params
219+
goto('/models', { replaceState: true, noScroll: true, keepFocus: true });
166220
}
167221
168222
async function copyModelId(modelId: string) {
@@ -234,7 +288,26 @@
234288
}
235289
}
236290
291+
// Sync filter state to URL whenever filters change (after initialization)
292+
$: if (filtersInitialized) {
293+
// Track all filter values to trigger reactivity
294+
selectedDevices;
295+
selectedFamily;
296+
selectedAcceleration;
297+
searchTerm;
298+
sortBy;
299+
sortOrder;
300+
301+
updateUrlFromFilters();
302+
}
303+
237304
onMount(() => {
305+
// Read initial filter state from URL before fetching
306+
suppressUrlUpdate = true;
307+
readFiltersFromUrl();
308+
suppressUrlUpdate = false;
309+
filtersInitialized = true;
310+
238311
fetchAllModels();
239312
});
240313
@@ -320,8 +393,6 @@
320393
{#if !loading && !error}
321394
<ModelGrid
322395
models={filteredModels}
323-
bind:currentPage
324-
{itemsPerPage}
325396
{copiedModelId}
326397
onCardClick={openModelDetails}
327398
onCopyCommand={copyRunCommand}

www/src/routes/models/components/ModelGrid.svelte

Lines changed: 1 addition & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,66 +5,18 @@
55
import ModelCard from './ModelCard.svelte';
66
77
export let models: GroupedFoundryModel[] = [];
8-
export let currentPage = 1;
9-
export let itemsPerPage = 12;
108
export let copiedModelId: string | null = null;
119
export let onCardClick: (model: GroupedFoundryModel) => void;
1210
export let onCopyCommand: (modelId: string) => void;
1311
export let onClearFilters: () => void;
14-
15-
$: totalPages = Math.ceil(models.length / itemsPerPage);
16-
$: paginatedModels = models.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
17-
18-
function goToPage(page: number) {
19-
currentPage = Math.max(1, Math.min(totalPages, page));
20-
}
2112
</script>
2213

2314
{#if models.length > 0}
2415
<div class="mt-6 grid auto-rows-fr gap-4 sm:grid-cols-2 lg:grid-cols-3">
25-
{#each paginatedModels as model (model.id)}
16+
{#each models as model (model.id)}
2617
<ModelCard {model} {copiedModelId} {onCardClick} {onCopyCommand} />
2718
{/each}
2819
</div>
29-
30-
<!-- Pagination -->
31-
{#if totalPages > 1}
32-
<div class="mt-8 flex justify-center gap-2">
33-
<Button
34-
variant="outline"
35-
size="sm"
36-
disabled={currentPage === 1}
37-
onclick={() => goToPage(currentPage - 1)}
38-
>
39-
Previous
40-
</Button>
41-
42-
<div class="flex items-center gap-2">
43-
{#each Array(totalPages).fill(0) as _, i}
44-
{#if i + 1 === 1 || i + 1 === totalPages || (i + 1 >= currentPage - 2 && i + 1 <= currentPage + 2)}
45-
<Button
46-
variant={currentPage === i + 1 ? 'default' : 'outline'}
47-
size="sm"
48-
onclick={() => goToPage(i + 1)}
49-
>
50-
{i + 1}
51-
</Button>
52-
{:else if i + 1 === currentPage - 3 || i + 1 === currentPage + 3}
53-
<span class="px-2">...</span>
54-
{/if}
55-
{/each}
56-
</div>
57-
58-
<Button
59-
variant="outline"
60-
size="sm"
61-
disabled={currentPage === totalPages}
62-
onclick={() => goToPage(currentPage + 1)}
63-
>
64-
Next
65-
</Button>
66-
</div>
67-
{/if}
6820
{:else}
6921
<Card.Root>
7022
<Card.Content class="py-12 text-center">

0 commit comments

Comments
 (0)