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:
parent
c6ce193256
commit
2ae6c7ed92
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
23
web-app/src/containers/ProvidersAvatar.tsx
Normal file
23
web-app/src/containers/ProvidersAvatar.tsx
Normal 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
|
||||
@ -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>
|
||||
|
||||
@ -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`
|
||||
}
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user