Skip to content

Commit da5d8f9

Browse files
committed
Add model card rendering
1 parent 1423746 commit da5d8f9

File tree

7 files changed

+309
-23
lines changed

7 files changed

+309
-23
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
"prettier-plugin-svelte": "^2.10.1",
3333
"svelte": "^4.0.5",
3434
"svelte-check": "^3.4.3",
35+
"svelte-infinite-loading": "^1.3.8",
36+
"svelte-relative-time": "^0.0.4",
3537
"svooltip": "^0.7.4",
3638
"tailwindcss": "3.3.3",
3739
"tslib": "^2.4.1",

pnpm-lock.yaml

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/common.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,55 @@ export const defaultTooltipOptions: Options = {
2323
delay: [1000, 0]
2424
};
2525

26-
export type LocalStorageValue = {
26+
export enum FilterModes {
27+
TIME_ASC = 'TIME_ASC',
28+
TIME_DESC = 'TIME_DESC',
29+
LIKES_ASC = 'LIKES_ASC',
30+
LIKES_DESC = 'LIKES_DESC',
31+
DOWNLOADS_ASC = 'DOWNLOADS_ASC',
32+
DOWNLOADS_DESC = 'DOWNLOADS_DESC'
33+
}
34+
35+
export interface LocalStorageValue {
2736
lastUpdate: Date;
2837
author: string;
2938
models: Model[];
39+
filters: {
40+
name: string;
41+
allowedLicences: string[];
42+
allowedModelSizes: string[];
43+
allowedModelTypes: ModelTypes[];
44+
filterMode: FilterModes;
45+
};
46+
}
47+
48+
export enum ModelTypes {
49+
GGML = 'GGML',
50+
GPTQ = 'GPTQ',
51+
GGUF = 'GGUF',
52+
OTHER = 'OTHER'
53+
}
54+
55+
export const getModelType = (model: Model): ModelTypes => {
56+
// First iterate the siblings to check the files as a main indicator
57+
// of the model type
58+
59+
for (const sibling of model.siblings) {
60+
if (sibling.rfilename.toLowerCase().includes('.ggml')) {
61+
return ModelTypes.GGML;
62+
} else if (sibling.rfilename.toLowerCase().includes('.gguf')) {
63+
return ModelTypes.GGUF;
64+
}
65+
}
66+
67+
// If none of the filenames matched, try the model name instead
68+
if (model.id.toLowerCase().includes('ggml')) {
69+
return ModelTypes.GGML;
70+
} else if (model.id.toLowerCase().includes('gguf')) {
71+
return ModelTypes.GGUF;
72+
} else if (model.id.toLowerCase().includes('gptq')) {
73+
return ModelTypes.GPTQ;
74+
}
75+
76+
return ModelTypes.OTHER;
3077
};

src/lib/components/ModelCard.svelte

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script lang="ts">
2+
3+
import type {Model} from "$lib/huggingfaceAPI";
4+
import {relativeTime} from 'svelte-relative-time'
5+
import PhDownloadSimpleBold from '~icons/ph/download-simple-bold'
6+
import PhHeartBold from '~icons/ph/heart-bold'
7+
import PhFileTextBold from '~icons/ph/file-text-bold'
8+
9+
export let model: Model;
10+
11+
let license = "";
12+
13+
model.tags.forEach((tag: string) => {
14+
if (tag.startsWith("license:")) {
15+
license = tag.substring(8).toUpperCase();
16+
}
17+
})
18+
19+
</script>
20+
21+
22+
<a class="block card card-hover w-full flex" href="https://huggingface.co/{model.id}" target="_blank">
23+
<div class="flex-1 flex flex-col p-2 overflow-hidden">
24+
<div class="font-bold px-4 truncate text-ellipsis overflow-hidden">
25+
{model.id.split("/").pop()}
26+
</div>
27+
28+
<div class="flex-1 flex px-4 truncate text-ellipsis overflow-hidden space-x-4">
29+
<span class="opacity-75" use:relativeTime={{date: new Date(model.lastModified)}}/> <span class="opacity-90"><PhFileTextBold class="inline"/> License: {license}</span>
30+
</div>
31+
</div>
32+
<div class="card variant-soft-secondary p-2 flex items-center rounded-r-none">
33+
<PhDownloadSimpleBold class="inline mr-2"/> {model.downloads}
34+
</div>
35+
<div class="card variant-soft-tertiary p-2 flex items-center rounded-l-none">
36+
<PhHeartBold class="inline mr-2 text-primary-700"/> {model.likes}
37+
</div>
38+
</a>

src/lib/components/layout/HeaderBar.svelte

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
<script lang="ts">
2-
import {filterText} from '$lib/stores.js';
3-
import {AppBar} from '@skeletonlabs/skeleton';
2+
import {AppBar, LightSwitch} from '@skeletonlabs/skeleton';
43
import PhGearFill from '~icons/ph/gear-fill';
54
import PhMagnifyingGlass from '~icons/ph/magnifying-glass';
65
import {tooltip} from 'svooltip';
76
8-
import {sidePanelRight} from '$lib/stores';
7+
import {sidePanelRight, state} from '$lib/stores';
98
import type {SidePanel} from '$lib/common';
109
import {defaultTooltipOptions} from '$lib/common';
1110
@@ -45,11 +44,14 @@
4544
</script>
4645

4746
<AppBar gridColumns="!flex" slotDefault="place-self-center" slotTrail="place-content-end">
47+
<svelte:fragment slot="lead">
48+
<LightSwitch/>
49+
</svelte:fragment>
4850
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto] w-full">
4951
<div class="input-group-shim">
5052
<PhMagnifyingGlass />
5153
</div>
52-
<input bind:value={$filterText} placeholder="Filter by Name" type="search" />
54+
<input bind:value={$state.filters.name} placeholder="Filter by Name" type="search" />
5355
</div>
5456
<svelte:fragment slot="trail">
5557
<button

src/lib/stores.ts

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,110 @@
1-
import { type Writable, writable } from 'svelte/store';
1+
import { derived, type Writable, writable } from 'svelte/store';
22
import type { LocalStorageValue, SidePanel } from './common';
3+
import { FilterModes } from './common';
34

45
import { localStorageStore } from '@skeletonlabs/skeleton';
56
import * as devalue from 'devalue';
67

7-
export const filterText = writable('');
8-
98
export const sidePanelRight = writable<SidePanel>({
109
width: 300,
1110
offset: 0,
1211
visible: true,
1312
side: 'right'
1413
});
1514

16-
export const state: Writable<LocalStorageValue> = localStorageStore(
15+
export const state: Writable<LocalStorageValue> = localStorageStore<LocalStorageValue>(
1716
'state',
1817
{
1918
lastUpdate: new Date(0),
2019
author: 'TheBloke',
21-
models: []
20+
models: [],
21+
filters: {
22+
name: '',
23+
allowedLicences: [],
24+
allowedModelSizes: [],
25+
allowedModelTypes: [],
26+
filterMode: FilterModes.TIME_DESC
27+
}
2228
},
2329
{
2430
serializer: devalue
2531
}
2632
);
33+
34+
// Derivative stores
35+
36+
// Extract all possible licences from the models
37+
export const availableLicences = derived(state, ($state) => {
38+
const licences = new Set<string>();
39+
$state.models.forEach((model) => {
40+
model.tags.forEach((tag) => {
41+
if (tag.startsWith('license:')) {
42+
licences.add(tag.substring(8));
43+
}
44+
});
45+
});
46+
return Array.from(licences);
47+
});
48+
49+
// Available Model Metadatas
50+
export const availableModelSizes = derived(state, ($state) => {
51+
const modelSizes = new Set<string>();
52+
$state.models.forEach((model) => {
53+
const nameParts = model.id.split('-');
54+
// The part with /^\d+B$/ is the model size
55+
nameParts.forEach((part) => {
56+
if (/^\d+B$/.test(part)) {
57+
modelSizes.add(part);
58+
}
59+
});
60+
});
61+
return Array.from(modelSizes);
62+
});
63+
64+
export const filteredModels = derived(state, ($state) => {
65+
let filtered = $state.models.filter((model) => {
66+
if (
67+
$state.filters.name !== '' &&
68+
!model.id.toLowerCase().includes($state.filters.name.toLowerCase())
69+
) {
70+
return false;
71+
}
72+
73+
return true;
74+
});
75+
76+
switch ($state.filters.filterMode) {
77+
case FilterModes.TIME_ASC:
78+
filtered = filtered.sort((a, b) => {
79+
return a.lastModified.getTime() - b.lastModified.getTime();
80+
});
81+
break;
82+
case FilterModes.TIME_DESC:
83+
filtered = filtered.sort((a, b) => {
84+
return b.lastModified.getTime() - a.lastModified.getTime();
85+
});
86+
break;
87+
case FilterModes.LIKES_ASC:
88+
filtered = filtered.sort((a, b) => {
89+
return a.likes - b.likes;
90+
});
91+
break;
92+
case FilterModes.LIKES_DESC:
93+
filtered = filtered.sort((a, b) => {
94+
return b.likes - a.likes;
95+
});
96+
break;
97+
case FilterModes.DOWNLOADS_ASC:
98+
filtered = filtered.sort((a, b) => {
99+
return a.downloads - b.downloads;
100+
});
101+
break;
102+
case FilterModes.DOWNLOADS_DESC:
103+
filtered = filtered.sort((a, b) => {
104+
return b.downloads - a.downloads;
105+
});
106+
break;
107+
}
108+
109+
return filtered;
110+
});

0 commit comments

Comments
 (0)