|
1 | 1 | <script lang="ts"> |
2 | 2 | import { onMount, onDestroy } from 'svelte'; |
| 3 | + import { page } from '$app/stores'; |
| 4 | + import { goto } from '$app/navigation'; |
3 | 5 | import { foundryModelService } from './service'; |
4 | 6 | import type { GroupedFoundryModel } from './types'; |
5 | 7 | import Nav from '$lib/components/home/nav.svelte'; |
|
9 | 11 | import { toast } from 'svelte-sonner'; |
10 | 12 | import { ModelFilters, ModelGrid, ModelDetailsModal } from './components'; |
11 | 13 |
|
| 14 | + // Known device names used as shorthand URL params (e.g. /models?cpu) |
| 15 | + const KNOWN_DEVICES = ['cpu', 'gpu', 'npu']; |
| 16 | +
|
12 | 17 | // Debounce timer for search |
13 | 18 | let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null; |
14 | 19 | let debouncedSearchTerm = ''; |
|
32 | 37 | let sortBy = 'lastModified'; |
33 | 38 | let sortOrder: 'asc' | 'desc' = 'desc'; |
34 | 39 |
|
| 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 | +
|
35 | 45 | // Available filter options |
36 | 46 | let availableDevices: string[] = []; |
37 | 47 | let availableFamilies: string[] = ['deepseek', 'mistral', 'qwen', 'phi', 'whisper']; |
38 | 48 | let availableAccelerations: string[] = []; |
39 | 49 |
|
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 | + } |
43 | 98 |
|
44 | 99 | // Fetch all models from API |
45 | 100 | async function fetchAllModels() { |
|
150 | 205 |
|
151 | 206 | return sortOrder === 'asc' ? (aVal > bVal ? 1 : -1) : aVal < bVal ? 1 : -1; |
152 | 207 | }); |
153 | | -
|
154 | | - currentPage = 1; |
155 | 208 | } |
156 | 209 |
|
157 | 210 | function clearFilters() { |
|
162 | 215 | selectedAcceleration = ''; |
163 | 216 | sortBy = 'lastModified'; |
164 | 217 | sortOrder = 'desc'; |
165 | | - currentPage = 1; |
| 218 | + // Clear URL params |
| 219 | + goto('/models', { replaceState: true, noScroll: true, keepFocus: true }); |
166 | 220 | } |
167 | 221 |
|
168 | 222 | async function copyModelId(modelId: string) { |
|
234 | 288 | } |
235 | 289 | } |
236 | 290 |
|
| 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 | +
|
237 | 304 | onMount(() => { |
| 305 | + // Read initial filter state from URL before fetching |
| 306 | + suppressUrlUpdate = true; |
| 307 | + readFiltersFromUrl(); |
| 308 | + suppressUrlUpdate = false; |
| 309 | + filtersInitialized = true; |
| 310 | +
|
238 | 311 | fetchAllModels(); |
239 | 312 | }); |
240 | 313 |
|
|
320 | 393 | {#if !loading && !error} |
321 | 394 | <ModelGrid |
322 | 395 | models={filteredModels} |
323 | | - bind:currentPage |
324 | | - {itemsPerPage} |
325 | 396 | {copiedModelId} |
326 | 397 | onCardClick={openModelDetails} |
327 | 398 | onCopyCommand={copyRunCommand} |
|
0 commit comments