feat: filter downloaded model on hub screen (#5113)

* feat: filter downloaded model on hub screen

* chore: custom avatar provider

* chore: alignment dropdown
This commit is contained in:
Faisal Amir 2025-05-27 15:17:07 +07:00 committed by GitHub
parent c6ce193256
commit 2ae6c7ed92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 121 additions and 78 deletions

View File

@ -36,7 +36,7 @@ export function CardItem({
)}
>
<div className="space-y-1.5">
<h1 className="font-medium line-clamp-1">{title}</h1>
<h1 className="font-medium">{title}</h1>
{description && (
<span className="text-main-view-fg/70 leading-normal">
{description}

View File

@ -7,7 +7,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useModelProvider } from '@/hooks/useModelProvider'
import { cn, getProviderLogo, getProviderTitle } from '@/lib/utils'
import { cn, getProviderTitle } from '@/lib/utils'
import { useEffect, useState } from 'react'
import Capabilities from './Capabilities'
import { IconSettings } from '@tabler/icons-react'
@ -15,6 +15,7 @@ import { useNavigate } from '@tanstack/react-router'
import { route } from '@/constants/routes'
import { useThreads } from '@/hooks/useThreads'
import { ModelSetting } from '@/containers/ModelSetting'
import ProvidersAvatar from '@/containers/ProvidersAvatar'
type DropdownModelProviderProps = {
model?: ThreadModel
@ -64,17 +65,15 @@ const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
return (
<>
<DropdownMenu>
<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">
<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]">
<DropdownMenuTrigger asChild>
<button
title={displayModel}
className="font-medium cursor-pointer flex items-center gap-1.5 relative z-20 max-w-38"
>
<img
src={getProviderLogo(selectedProvider as string)}
alt={`${selectedProvider} - Logo`}
className="size-4"
/>
<div className="shrink-0">
<ProvidersAvatar provider={provider as ProviderObject} />
</div>
<span
className={cn(
'text-main-view-fg/80 truncate leading-normal',
@ -85,7 +84,7 @@ const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
</span>
</button>
</DropdownMenuTrigger>
{currentModel && (
{currentModel?.settings && (
<ModelSetting
model={currentModel as Model}
provider={provider as ProviderObject}
@ -96,6 +95,8 @@ const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
className="w-60 max-h-[320px]"
side="bottom"
align="start"
sideOffset={10}
alignOffset={-8}
>
<DropdownMenuGroup>
{providers.map((provider, index) => {
@ -111,11 +112,7 @@ const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
>
<div className="flex items-center justify-between">
<DropdownMenuLabel className="flex items-center gap-1.5">
<img
src={getProviderLogo(provider.provider)}
alt={`${provider.provider} - Logo`}
className="size-4"
/>
<ProvidersAvatar provider={provider} />
<span className="capitalize truncate text-sm">
{getProviderTitle(provider.provider)}
</span>

View File

@ -0,0 +1,23 @@
import { getProviderLogo, getProviderTitle } from '@/lib/utils'
const ProvidersAvatar = ({ provider }: { provider: ProviderObject }) => {
return (
<>
{getProviderLogo(provider.provider) === undefined ? (
<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">
<p className="text-xs leading-0 capitalize">
{getProviderTitle(provider.provider).charAt(0)}
</p>
</div>
) : (
<img
src={getProviderLogo(provider.provider)}
alt={`${provider.provider} - Logo`}
className="size-4.5"
/>
)}
</>
)
}
export default ProvidersAvatar

View File

@ -1,6 +1,6 @@
import { route } from '@/constants/routes'
import { useModelProvider } from '@/hooks/useModelProvider'
import { cn, getProviderLogo, getProviderTitle } from '@/lib/utils'
import { cn, getProviderTitle } from '@/lib/utils'
import { useNavigate, useMatches, Link } from '@tanstack/react-router'
import { IconArrowLeft, IconCirclePlus } from '@tabler/icons-react'
import {
@ -16,6 +16,7 @@ import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { useCallback, useState } from 'react'
import { openAIProviderSettings } from '@/mock/data'
import ProvidersAvatar from '@/containers/ProvidersAvatar'
const ProvidersMenu = ({
stepSetupRemoteProvider,
@ -85,11 +86,7 @@ const ProvidersMenu = ({
})
}
>
<img
src={getProviderLogo(provider.provider)}
alt={`${provider.provider} - Logo`}
className="size-4"
/>
<ProvidersAvatar provider={provider} />
<span className="capitalize">
{getProviderTitle(provider.provider)}
</span>
@ -104,10 +101,8 @@ const ProvidersMenu = ({
className="bg-main-view flex cursor-pointer px-4 my-1.5 items-center gap-1.5 text-main-view-fg/80"
onClick={() => {}}
>
<IconCirclePlus size={16} />
<span className="capitalize">
Add Provider
</span>
<IconCirclePlus size={18} />
<span className="capitalize">Add Provider</span>
</div>
</DialogTrigger>
<DialogContent>

View File

@ -29,8 +29,10 @@ export function getProviderLogo(provider: string) {
return '/images/model-provider/gemini.svg'
case 'deepseek':
return '/images/model-provider/deepseek.svg'
default:
case 'openai':
return '/images/model-provider/openai.svg'
default:
return undefined
}
}
@ -148,27 +150,27 @@ export function isDev() {
}
export function formatDuration(startTime: number, endTime?: number): string {
const end = endTime || Date.now();
const durationMs = end - startTime;
const end = endTime || Date.now()
const durationMs = end - startTime
if (durationMs < 0) {
return "Invalid duration (start time is in the future)";
return 'Invalid duration (start time is in the future)'
}
const seconds = Math.floor(durationMs / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const seconds = Math.floor(durationMs / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
if (days > 0) {
return `${days}d ${hours % 24}h ${minutes % 60}m ${seconds % 60}s`;
return `${days}d ${hours % 24}h ${minutes % 60}m ${seconds % 60}s`
} else if (hours > 0) {
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
return `${hours}h ${minutes % 60}m ${seconds % 60}s`
} else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
return `${minutes}m ${seconds % 60}s`
} else if (seconds > 0) {
return `${seconds}s`;
return `${seconds}s`
} else {
return `${durationMs}ms`;
return `${durationMs}ms`
}
}

View File

@ -56,10 +56,14 @@ function Hub() {
{}
)
const [isSearching, setIsSearching] = useState(false)
const [showOnlyDownloaded, setShowOnlyDownloaded] = useState(false)
const addModelSourceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
null
)
const { getProviderByName } = useModelProvider()
const llamaProvider = getProviderByName('llama.cpp')
const toggleModelExpansion = (modelId: string) => {
setExpandedModels((prev) => ({
...prev,
@ -83,16 +87,29 @@ function Hub() {
// Filtered models
const filteredModels = useMemo(() => {
// Apply additional filters here if needed
return searchValue.length
? sortedModels?.filter((e) =>
fuzzySearch(
searchValue.replace(/\s+/g, '').toLowerCase(),
e.id.toLowerCase()
)
let filtered = sortedModels
// Apply search filter
if (searchValue.length) {
filtered = filtered?.filter((e) =>
fuzzySearch(
searchValue.replace(/\s+/g, '').toLowerCase(),
e.id.toLowerCase()
)
: sortedModels
}, [searchValue, sortedModels])
)
}
// Apply downloaded filter
if (showOnlyDownloaded) {
filtered = filtered?.filter((model) =>
model.models.some((variant) =>
llamaProvider?.models.some((m: { id: string }) => m.id === variant.id)
)
)
}
return filtered
}, [searchValue, sortedModels, showOnlyDownloaded, llamaProvider?.models])
useEffect(() => {
fetchSources()
@ -135,9 +152,6 @@ function Hub() {
[downloads]
)
const { getProviderByName } = useModelProvider()
const llamaProvider = getProviderByName('llama.cpp')
const navigate = useNavigate()
const handleUseModel = useCallback(
@ -207,33 +221,45 @@ function Hub() {
className="w-full focus:outline-none"
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger>
<span
title="Edit Theme"
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"
>
{
sortOptions.find((option) => option.value === sortSelected)
?.name
}
</span>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end">
{sortOptions.map((option) => (
<DropdownMenuItem
className={cn(
'cursor-pointer my-0.5',
sortSelected === option.value && 'bg-main-view-fg/5'
)}
key={option.value}
onClick={() => setSortSelected(option.value)}
<div className="flex items-center gap-2 shrink-0">
<DropdownMenu>
<DropdownMenuTrigger>
<span
title="Edit Theme"
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"
>
{option.name}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{
sortOptions.find(
(option) => option.value === sortSelected
)?.name
}
</span>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end">
{sortOptions.map((option) => (
<DropdownMenuItem
className={cn(
'cursor-pointer my-0.5',
sortSelected === option.value && 'bg-main-view-fg/5'
)}
key={option.value}
onClick={() => setSortSelected(option.value)}
>
{option.name}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center gap-2">
<Switch
checked={showOnlyDownloaded}
onCheckedChange={setShowOnlyDownloaded}
/>
<span className="text-xs text-main-view-fg/70 font-medium whitespace-nowrap">
Downloaded
</span>
</div>
</div>
</div>
</HeaderPage>
<div className="p-4 w-full h-[calc(100%-32px)] overflow-y-auto">