Skip to content

Commit 2ae6c7e

Browse files
authored
feat: filter downloaded model on hub screen (#5113)
* feat: filter downloaded model on hub screen * chore: custom avatar provider * chore: alignment dropdown
1 parent c6ce193 commit 2ae6c7e

File tree

6 files changed

+121
-78
lines changed

6 files changed

+121
-78
lines changed

web-app/src/containers/Card.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function CardItem({
3636
)}
3737
>
3838
<div className="space-y-1.5">
39-
<h1 className="font-medium line-clamp-1">{title}</h1>
39+
<h1 className="font-medium">{title}</h1>
4040
{description && (
4141
<span className="text-main-view-fg/70 leading-normal">
4242
{description}

web-app/src/containers/DropdownModelProvider.tsx

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import {
77
DropdownMenuTrigger,
88
} from '@/components/ui/dropdown-menu'
99
import { useModelProvider } from '@/hooks/useModelProvider'
10-
import { cn, getProviderLogo, getProviderTitle } from '@/lib/utils'
10+
import { cn, getProviderTitle } from '@/lib/utils'
1111
import { useEffect, useState } from 'react'
1212
import Capabilities from './Capabilities'
1313
import { IconSettings } from '@tabler/icons-react'
1414
import { useNavigate } from '@tanstack/react-router'
1515
import { route } from '@/constants/routes'
1616
import { useThreads } from '@/hooks/useThreads'
1717
import { ModelSetting } from '@/containers/ModelSetting'
18+
import ProvidersAvatar from '@/containers/ProvidersAvatar'
1819

1920
type DropdownModelProviderProps = {
2021
model?: ThreadModel
@@ -64,17 +65,15 @@ const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
6465
return (
6566
<>
6667
<DropdownMenu>
67-
<div className="bg-main-view-fg/5 hover:bg-main-view-fg/8 px-2 py-1 flex items-center gap-1.5 rounded-sm">
68+
<div className="bg-main-view-fg/5 hover:bg-main-view-fg/8 px-2 py-1 flex items-center gap-1.5 rounded-sm max-h-[32px]">
6869
<DropdownMenuTrigger asChild>
6970
<button
7071
title={displayModel}
7172
className="font-medium cursor-pointer flex items-center gap-1.5 relative z-20 max-w-38"
7273
>
73-
<img
74-
src={getProviderLogo(selectedProvider as string)}
75-
alt={`${selectedProvider} - Logo`}
76-
className="size-4"
77-
/>
74+
<div className="shrink-0">
75+
<ProvidersAvatar provider={provider as ProviderObject} />
76+
</div>
7877
<span
7978
className={cn(
8079
'text-main-view-fg/80 truncate leading-normal',
@@ -85,7 +84,7 @@ const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
8584
</span>
8685
</button>
8786
</DropdownMenuTrigger>
88-
{currentModel && (
87+
{currentModel?.settings && (
8988
<ModelSetting
9089
model={currentModel as Model}
9190
provider={provider as ProviderObject}
@@ -96,6 +95,8 @@ const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
9695
className="w-60 max-h-[320px]"
9796
side="bottom"
9897
align="start"
98+
sideOffset={10}
99+
alignOffset={-8}
99100
>
100101
<DropdownMenuGroup>
101102
{providers.map((provider, index) => {
@@ -111,11 +112,7 @@ const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
111112
>
112113
<div className="flex items-center justify-between">
113114
<DropdownMenuLabel className="flex items-center gap-1.5">
114-
<img
115-
src={getProviderLogo(provider.provider)}
116-
alt={`${provider.provider} - Logo`}
117-
className="size-4"
118-
/>
115+
<ProvidersAvatar provider={provider} />
119116
<span className="capitalize truncate text-sm">
120117
{getProviderTitle(provider.provider)}
121118
</span>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { getProviderLogo, getProviderTitle } from '@/lib/utils'
2+
3+
const ProvidersAvatar = ({ provider }: { provider: ProviderObject }) => {
4+
return (
5+
<>
6+
{getProviderLogo(provider.provider) === undefined ? (
7+
<div className="flex w-4.5 h-4.5 rounded-full border border-main-view-fg/20 items-center justify-center bg-main-view-fg/10">
8+
<p className="text-xs leading-0 capitalize">
9+
{getProviderTitle(provider.provider).charAt(0)}
10+
</p>
11+
</div>
12+
) : (
13+
<img
14+
src={getProviderLogo(provider.provider)}
15+
alt={`${provider.provider} - Logo`}
16+
className="size-4.5"
17+
/>
18+
)}
19+
</>
20+
)
21+
}
22+
23+
export default ProvidersAvatar

web-app/src/containers/ProvidersMenu.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { route } from '@/constants/routes'
22
import { useModelProvider } from '@/hooks/useModelProvider'
3-
import { cn, getProviderLogo, getProviderTitle } from '@/lib/utils'
3+
import { cn, getProviderTitle } from '@/lib/utils'
44
import { useNavigate, useMatches, Link } from '@tanstack/react-router'
55
import { IconArrowLeft, IconCirclePlus } from '@tabler/icons-react'
66
import {
@@ -16,6 +16,7 @@ import { Input } from '@/components/ui/input'
1616
import { Button } from '@/components/ui/button'
1717
import { useCallback, useState } from 'react'
1818
import { openAIProviderSettings } from '@/mock/data'
19+
import ProvidersAvatar from '@/containers/ProvidersAvatar'
1920

2021
const ProvidersMenu = ({
2122
stepSetupRemoteProvider,
@@ -85,11 +86,7 @@ const ProvidersMenu = ({
8586
})
8687
}
8788
>
88-
<img
89-
src={getProviderLogo(provider.provider)}
90-
alt={`${provider.provider} - Logo`}
91-
className="size-4"
92-
/>
89+
<ProvidersAvatar provider={provider} />
9390
<span className="capitalize">
9491
{getProviderTitle(provider.provider)}
9592
</span>
@@ -104,10 +101,8 @@ const ProvidersMenu = ({
104101
className="bg-main-view flex cursor-pointer px-4 my-1.5 items-center gap-1.5 text-main-view-fg/80"
105102
onClick={() => {}}
106103
>
107-
<IconCirclePlus size={16} />
108-
<span className="capitalize">
109-
Add Provider
110-
</span>
104+
<IconCirclePlus size={18} />
105+
<span className="capitalize">Add Provider</span>
111106
</div>
112107
</DialogTrigger>
113108
<DialogContent>

web-app/src/lib/utils.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ export function getProviderLogo(provider: string) {
2929
return '/images/model-provider/gemini.svg'
3030
case 'deepseek':
3131
return '/images/model-provider/deepseek.svg'
32-
default:
32+
case 'openai':
3333
return '/images/model-provider/openai.svg'
34+
default:
35+
return undefined
3436
}
3537
}
3638

@@ -148,27 +150,27 @@ export function isDev() {
148150
}
149151

150152
export function formatDuration(startTime: number, endTime?: number): string {
151-
const end = endTime || Date.now();
152-
const durationMs = end - startTime;
153-
153+
const end = endTime || Date.now()
154+
const durationMs = end - startTime
155+
154156
if (durationMs < 0) {
155-
return "Invalid duration (start time is in the future)";
157+
return 'Invalid duration (start time is in the future)'
156158
}
157-
158-
const seconds = Math.floor(durationMs / 1000);
159-
const minutes = Math.floor(seconds / 60);
160-
const hours = Math.floor(minutes / 60);
161-
const days = Math.floor(hours / 24);
162-
159+
160+
const seconds = Math.floor(durationMs / 1000)
161+
const minutes = Math.floor(seconds / 60)
162+
const hours = Math.floor(minutes / 60)
163+
const days = Math.floor(hours / 24)
164+
163165
if (days > 0) {
164-
return `${days}d ${hours % 24}h ${minutes % 60}m ${seconds % 60}s`;
166+
return `${days}d ${hours % 24}h ${minutes % 60}m ${seconds % 60}s`
165167
} else if (hours > 0) {
166-
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
168+
return `${hours}h ${minutes % 60}m ${seconds % 60}s`
167169
} else if (minutes > 0) {
168-
return `${minutes}m ${seconds % 60}s`;
170+
return `${minutes}m ${seconds % 60}s`
169171
} else if (seconds > 0) {
170-
return `${seconds}s`;
172+
return `${seconds}s`
171173
} else {
172-
return `${durationMs}ms`;
174+
return `${durationMs}ms`
173175
}
174176
}

web-app/src/routes/hub.tsx

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,14 @@ function Hub() {
5656
{}
5757
)
5858
const [isSearching, setIsSearching] = useState(false)
59+
const [showOnlyDownloaded, setShowOnlyDownloaded] = useState(false)
5960
const addModelSourceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
6061
null
6162
)
6263

64+
const { getProviderByName } = useModelProvider()
65+
const llamaProvider = getProviderByName('llama.cpp')
66+
6367
const toggleModelExpansion = (modelId: string) => {
6468
setExpandedModels((prev) => ({
6569
...prev,
@@ -83,16 +87,29 @@ function Hub() {
8387

8488
// Filtered models
8589
const filteredModels = useMemo(() => {
86-
// Apply additional filters here if needed
87-
return searchValue.length
88-
? sortedModels?.filter((e) =>
89-
fuzzySearch(
90-
searchValue.replace(/\s+/g, '').toLowerCase(),
91-
e.id.toLowerCase()
92-
)
90+
let filtered = sortedModels
91+
92+
// Apply search filter
93+
if (searchValue.length) {
94+
filtered = filtered?.filter((e) =>
95+
fuzzySearch(
96+
searchValue.replace(/\s+/g, '').toLowerCase(),
97+
e.id.toLowerCase()
9398
)
94-
: sortedModels
95-
}, [searchValue, sortedModels])
99+
)
100+
}
101+
102+
// Apply downloaded filter
103+
if (showOnlyDownloaded) {
104+
filtered = filtered?.filter((model) =>
105+
model.models.some((variant) =>
106+
llamaProvider?.models.some((m: { id: string }) => m.id === variant.id)
107+
)
108+
)
109+
}
110+
111+
return filtered
112+
}, [searchValue, sortedModels, showOnlyDownloaded, llamaProvider?.models])
96113

97114
useEffect(() => {
98115
fetchSources()
@@ -135,9 +152,6 @@ function Hub() {
135152
[downloads]
136153
)
137154

138-
const { getProviderByName } = useModelProvider()
139-
const llamaProvider = getProviderByName('llama.cpp')
140-
141155
const navigate = useNavigate()
142156

143157
const handleUseModel = useCallback(
@@ -207,33 +221,45 @@ function Hub() {
207221
className="w-full focus:outline-none"
208222
/>
209223
</div>
210-
<DropdownMenu>
211-
<DropdownMenuTrigger>
212-
<span
213-
title="Edit Theme"
214-
className="flex cursor-pointer items-center gap-1 px-2 py-1 rounded-sm bg-main-view-fg/15 text-sm outline-none text-main-view-fg font-medium"
215-
>
216-
{
217-
sortOptions.find((option) => option.value === sortSelected)
218-
?.name
219-
}
220-
</span>
221-
</DropdownMenuTrigger>
222-
<DropdownMenuContent side="bottom" align="end">
223-
{sortOptions.map((option) => (
224-
<DropdownMenuItem
225-
className={cn(
226-
'cursor-pointer my-0.5',
227-
sortSelected === option.value && 'bg-main-view-fg/5'
228-
)}
229-
key={option.value}
230-
onClick={() => setSortSelected(option.value)}
224+
<div className="flex items-center gap-2 shrink-0">
225+
<DropdownMenu>
226+
<DropdownMenuTrigger>
227+
<span
228+
title="Edit Theme"
229+
className="flex cursor-pointer items-center gap-1 px-2 py-1 rounded-sm bg-main-view-fg/15 text-sm outline-none text-main-view-fg font-medium"
231230
>
232-
{option.name}
233-
</DropdownMenuItem>
234-
))}
235-
</DropdownMenuContent>
236-
</DropdownMenu>
231+
{
232+
sortOptions.find(
233+
(option) => option.value === sortSelected
234+
)?.name
235+
}
236+
</span>
237+
</DropdownMenuTrigger>
238+
<DropdownMenuContent side="bottom" align="end">
239+
{sortOptions.map((option) => (
240+
<DropdownMenuItem
241+
className={cn(
242+
'cursor-pointer my-0.5',
243+
sortSelected === option.value && 'bg-main-view-fg/5'
244+
)}
245+
key={option.value}
246+
onClick={() => setSortSelected(option.value)}
247+
>
248+
{option.name}
249+
</DropdownMenuItem>
250+
))}
251+
</DropdownMenuContent>
252+
</DropdownMenu>
253+
<div className="flex items-center gap-2">
254+
<Switch
255+
checked={showOnlyDownloaded}
256+
onCheckedChange={setShowOnlyDownloaded}
257+
/>
258+
<span className="text-xs text-main-view-fg/70 font-medium whitespace-nowrap">
259+
Downloaded
260+
</span>
261+
</div>
262+
</div>
237263
</div>
238264
</HeaderPage>
239265
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">

0 commit comments

Comments
 (0)