feat: Jan Model Hub filter options and responsiveness (#4714)
* feat: Jan Model Hub filter options and responsiveness * chore: fix display unit * chore: fix optional wrapping * chore: correct joi component's test
This commit is contained in:
parent
1b4a91ba7e
commit
cfc6734702
@ -29,7 +29,7 @@ jest.mock('@radix-ui/react-slider', () => ({
|
|||||||
|
|
||||||
describe('@joi/core/Slider', () => {
|
describe('@joi/core/Slider', () => {
|
||||||
it('renders correctly with default props', () => {
|
it('renders correctly with default props', () => {
|
||||||
render(<Slider />)
|
render(<Slider value={[1]}/>)
|
||||||
expect(screen.getByTestId('slider-root')).toBeInTheDocument()
|
expect(screen.getByTestId('slider-root')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('slider-track')).toBeInTheDocument()
|
expect(screen.getByTestId('slider-track')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('slider-range')).toBeInTheDocument()
|
expect(screen.getByTestId('slider-range')).toBeInTheDocument()
|
||||||
|
|||||||
@ -39,7 +39,9 @@ const Slider = ({
|
|||||||
<SliderPrimitive.Track className="slider__track">
|
<SliderPrimitive.Track className="slider__track">
|
||||||
<SliderPrimitive.Range className="slider__range" />
|
<SliderPrimitive.Range className="slider__range" />
|
||||||
</SliderPrimitive.Track>
|
</SliderPrimitive.Track>
|
||||||
<SliderPrimitive.Thumb className="slider__thumb" />
|
{value?.map((_, i) => (
|
||||||
|
<SliderPrimitive.Thumb className="slider__thumb" key={i} />
|
||||||
|
))}
|
||||||
</SliderPrimitive.Root>
|
</SliderPrimitive.Root>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
84
web/screens/Hub/ModelFilter/ContextLength/index.tsx
Normal file
84
web/screens/Hub/ModelFilter/ContextLength/index.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import { Slider, Input, Tooltip } from '@janhq/joi'
|
||||||
|
|
||||||
|
import { atom, useAtom } from 'jotai'
|
||||||
|
import { InfoIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
export const hubCtxLenAtom = atom(0)
|
||||||
|
|
||||||
|
export default function ContextLengthFilter() {
|
||||||
|
const [value, setValue] = useAtom(hubCtxLenAtom)
|
||||||
|
const [inputingValue, setInputingValue] = useState(false)
|
||||||
|
|
||||||
|
const normalizeTextValue = (value: number) => {
|
||||||
|
return value === 100 ? '1M' : value === 0 ? 0 : `${value}K`
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="mb-3 flex items-center gap-x-2">
|
||||||
|
<p className="font-semibold">Context length</p>
|
||||||
|
<Tooltip
|
||||||
|
trigger={
|
||||||
|
<InfoIcon
|
||||||
|
size={16}
|
||||||
|
className="flex-shrink-0 text-[hsl(var(--text-secondary))]"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
content={''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-4">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Slider
|
||||||
|
value={[value]}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setValue(Number(e[0]))
|
||||||
|
}}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
<div className="relative mt-1 flex items-center justify-between text-[hsla(var(--text-secondary))]">
|
||||||
|
<p className="text-xs">0</p>
|
||||||
|
<p className="text-xs">1M</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
className="-mt-4 h-8 w-[60px] p-2"
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
value={inputingValue ? value : normalizeTextValue(value)}
|
||||||
|
textAlign="left"
|
||||||
|
onFocus={() => setInputingValue(true)}
|
||||||
|
onBlur={(e) => {
|
||||||
|
setInputingValue(false)
|
||||||
|
const numericValue = e.target.value.replace(/\D/g, '')
|
||||||
|
const value = Number(numericValue)
|
||||||
|
setValue(value > 100 ? 100 : value)
|
||||||
|
}}
|
||||||
|
onChange={(e) => {
|
||||||
|
// Passthru since it validates again onBlur
|
||||||
|
if (/^\d*\.?\d*$/.test(e.target.value)) {
|
||||||
|
setValue(Number(e.target.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not accept invalid value or NaN
|
||||||
|
// E.g. anything changes that trigger onValueChanged
|
||||||
|
// Which is incorrect
|
||||||
|
if (
|
||||||
|
Number(e.target.value) > 100 ||
|
||||||
|
Number(e.target.value) < 0 ||
|
||||||
|
Number.isNaN(Number(e.target.value))
|
||||||
|
)
|
||||||
|
return
|
||||||
|
setValue(Number(e.target.value))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
127
web/screens/Hub/ModelFilter/ModelSize/index.tsx
Normal file
127
web/screens/Hub/ModelFilter/ModelSize/index.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { Slider, Input, Tooltip } from '@janhq/joi'
|
||||||
|
|
||||||
|
import { atom, useAtom } from 'jotai'
|
||||||
|
import { InfoIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
export const hubModelSizeMinAtom = atom(0)
|
||||||
|
export const hubModelSizeMaxAtom = atom(100)
|
||||||
|
|
||||||
|
export default function ModelSizeFilter() {
|
||||||
|
const [value, setValue] = useAtom(hubModelSizeMinAtom)
|
||||||
|
const [valueMax, setValueMax] = useAtom(hubModelSizeMaxAtom)
|
||||||
|
const [inputingMinValue, setInputingMinValue] = useState(false)
|
||||||
|
const [inputingMaxValue, setInputingMaxValue] = useState(false)
|
||||||
|
|
||||||
|
const normalizeTextValue = (value: number) => {
|
||||||
|
return value === 100 ? '100GB' : value === 0 ? 0 : `${value}GB`
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="mb-3 flex items-center gap-x-2">
|
||||||
|
<p className="font-semibold">Model size</p>
|
||||||
|
<Tooltip
|
||||||
|
trigger={
|
||||||
|
<InfoIcon
|
||||||
|
size={16}
|
||||||
|
className="flex-shrink-0 text-[hsl(var(--text-secondary))]"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
content={''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-4">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Slider
|
||||||
|
value={[value, valueMax]}
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setValue(Number(e[0]))
|
||||||
|
setValueMax(Number(e[1]))
|
||||||
|
}}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-col items-center gap-x-4">
|
||||||
|
<div className="relative mt-1 flex w-full items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-[hsla(var(--text-secondary))]">from</p>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
className="mt-1 h-8 w-[60px] p-2"
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
value={inputingMinValue ? value : normalizeTextValue(value)}
|
||||||
|
textAlign="left"
|
||||||
|
onFocus={(e) => setInputingMinValue(true)}
|
||||||
|
onBlur={(e) => {
|
||||||
|
setInputingMinValue(false)
|
||||||
|
const numericValue = e.target.value.replace(/\D/g, '')
|
||||||
|
const value = Number(numericValue)
|
||||||
|
setValue(value > valueMax ? valueMax : value)
|
||||||
|
}}
|
||||||
|
onChange={(e) => {
|
||||||
|
// Passthru since it validates again onBlur
|
||||||
|
if (/^\d*\.?\d*$/.test(e.target.value)) {
|
||||||
|
setValue(Number(e.target.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not accept invalid value or NaN
|
||||||
|
// E.g. anything changes that trigger onValueChanged
|
||||||
|
// Which is incorrect
|
||||||
|
if (
|
||||||
|
Number(e.target.value) > 100 ||
|
||||||
|
Number(e.target.value) < 0 ||
|
||||||
|
Number.isNaN(Number(e.target.value))
|
||||||
|
)
|
||||||
|
return
|
||||||
|
setValue(Number(e.target.value))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<p className="text-xs text-[hsla(var(--text-secondary))]">to</p>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
className="mt-1 h-8 w-[60px] p-2"
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
value={inputingMaxValue ? valueMax : normalizeTextValue(valueMax)}
|
||||||
|
textAlign="left"
|
||||||
|
onFocus={(e) => setInputingMaxValue(true)}
|
||||||
|
onBlur={(e) => {
|
||||||
|
setInputingMaxValue(false)
|
||||||
|
const numericValue = e.target.value.replace(/\D/g, '')
|
||||||
|
const value = Number(numericValue)
|
||||||
|
setValueMax(value > 100 ? 100 : value)
|
||||||
|
}}
|
||||||
|
onChange={(e) => {
|
||||||
|
// Passthru since it validates again onBlur
|
||||||
|
if (/^\d*\.?\d*$/.test(e.target.value)) {
|
||||||
|
setValueMax(Number(e.target.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not accept invalid value or NaN
|
||||||
|
// E.g. anything changes that trigger onValueChanged
|
||||||
|
// Which is incorrect
|
||||||
|
if (
|
||||||
|
Number(e.target.value) > 100 ||
|
||||||
|
Number(e.target.value) < 0 ||
|
||||||
|
Number.isNaN(Number(e.target.value))
|
||||||
|
)
|
||||||
|
return
|
||||||
|
setValueMax(Number(e.target.value))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -9,7 +9,7 @@ type Props = {
|
|||||||
|
|
||||||
const ModelList = ({ models, onSelectedModel }: Props) => {
|
const ModelList = ({ models, onSelectedModel }: Props) => {
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full flex-shrink-0">
|
<div className="w-full">
|
||||||
{models.map((model) => (
|
{models.map((model) => (
|
||||||
<ModelItem
|
<ModelItem
|
||||||
key={model.id}
|
key={model.id}
|
||||||
|
|||||||
@ -7,10 +7,17 @@ import Image from 'next/image'
|
|||||||
|
|
||||||
import { ModelSource } from '@janhq/core'
|
import { ModelSource } from '@janhq/core'
|
||||||
|
|
||||||
import { ScrollArea, Button, Select, Tabs, useClickOutside } from '@janhq/joi'
|
import {
|
||||||
|
ScrollArea,
|
||||||
|
Button,
|
||||||
|
Select,
|
||||||
|
Tabs,
|
||||||
|
useClickOutside,
|
||||||
|
Switch,
|
||||||
|
} from '@janhq/joi'
|
||||||
import { motion as m } from 'framer-motion'
|
import { motion as m } from 'framer-motion'
|
||||||
|
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { ImagePlusIcon, UploadCloudIcon, UploadIcon } from 'lucide-react'
|
import { ImagePlusIcon, UploadCloudIcon, UploadIcon } from 'lucide-react'
|
||||||
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
@ -31,6 +38,11 @@ import ModelList from '@/screens/Hub/ModelList'
|
|||||||
import { extractModelRepo } from '@/utils/modelSource'
|
import { extractModelRepo } from '@/utils/modelSource'
|
||||||
import { fuzzySearch } from '@/utils/search'
|
import { fuzzySearch } from '@/utils/search'
|
||||||
|
|
||||||
|
import ContextLengthFilter, { hubCtxLenAtom } from './ModelFilter/ContextLength'
|
||||||
|
import ModelSizeFilter, {
|
||||||
|
hubModelSizeMaxAtom,
|
||||||
|
hubModelSizeMinAtom,
|
||||||
|
} from './ModelFilter/ModelSize'
|
||||||
import ModelPage from './ModelPage'
|
import ModelPage from './ModelPage'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -39,6 +51,8 @@ import {
|
|||||||
} from '@/helpers/atoms/App.atom'
|
} from '@/helpers/atoms/App.atom'
|
||||||
import { modelDetailAtom } from '@/helpers/atoms/Model.atom'
|
import { modelDetailAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
|
import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom'
|
||||||
|
|
||||||
const sortMenus = [
|
const sortMenus = [
|
||||||
{
|
{
|
||||||
name: 'Most downloaded',
|
name: 'Most downloaded',
|
||||||
@ -64,6 +78,8 @@ const filterOptions = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const hubCompatibleAtom = atom(false)
|
||||||
|
|
||||||
const HubScreen = () => {
|
const HubScreen = () => {
|
||||||
const { sources } = useGetModelSources()
|
const { sources } = useGetModelSources()
|
||||||
const { sources: remoteModelSources } = useGetEngineModelSources()
|
const { sources: remoteModelSources } = useGetEngineModelSources()
|
||||||
@ -84,6 +100,14 @@ const HubScreen = () => {
|
|||||||
const imageInputRef = useRef<HTMLInputElement>(null)
|
const imageInputRef = useRef<HTMLInputElement>(null)
|
||||||
const hubBannerSettingRef = useRef<HTMLDivElement>(null)
|
const hubBannerSettingRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const [compatible, setCompatible] = useAtom(hubCompatibleAtom)
|
||||||
|
const totalRam = useAtomValue(totalRamAtom)
|
||||||
|
const [ctxLenFilter, setCtxLenFilter] = useAtom(hubCtxLenAtom)
|
||||||
|
const [minModelSizeFilter, setMinModelSizeFilter] =
|
||||||
|
useAtom(hubModelSizeMinAtom)
|
||||||
|
const [maxModelSizeFilter, setMaxModelSizeFilter] =
|
||||||
|
useAtom(hubModelSizeMaxAtom)
|
||||||
|
|
||||||
const searchedModels = useMemo(
|
const searchedModels = useMemo(
|
||||||
() =>
|
() =>
|
||||||
searchValue.length
|
searchValue.length
|
||||||
@ -111,6 +135,32 @@ const HubScreen = () => {
|
|||||||
})
|
})
|
||||||
}, [sortSelected, sources])
|
}, [sortSelected, sources])
|
||||||
|
|
||||||
|
const filteredModels = useMemo(() => {
|
||||||
|
return sortedModels.filter((model) => {
|
||||||
|
const isCompatible =
|
||||||
|
!compatible ||
|
||||||
|
model.models?.some((e) => e.size * 1.5 < totalRam * (1 << 20))
|
||||||
|
const matchesCtxLen =
|
||||||
|
!ctxLenFilter ||
|
||||||
|
model.metadata?.gguf?.context_length > ctxLenFilter * 1000
|
||||||
|
const matchesMinSize =
|
||||||
|
!minModelSizeFilter ||
|
||||||
|
model.models.some((e) => e.size >= minModelSizeFilter * (1 << 30))
|
||||||
|
const matchesMaxSize =
|
||||||
|
maxModelSizeFilter === 100 ||
|
||||||
|
model.models.some((e) => e.size <= maxModelSizeFilter * (1 << 30))
|
||||||
|
|
||||||
|
return isCompatible && matchesCtxLen && matchesMinSize && matchesMaxSize
|
||||||
|
})
|
||||||
|
}, [
|
||||||
|
sortedModels,
|
||||||
|
compatible,
|
||||||
|
ctxLenFilter,
|
||||||
|
minModelSizeFilter,
|
||||||
|
maxModelSizeFilter,
|
||||||
|
totalRam,
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (modelDetail) {
|
if (modelDetail) {
|
||||||
setSelectedModel(sources?.find((e) => e.id === modelDetail))
|
setSelectedModel(sources?.find((e) => e.id === modelDetail))
|
||||||
@ -215,8 +265,8 @@ const HubScreen = () => {
|
|||||||
className="h-full w-full"
|
className="h-full w-full"
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<div className="relative h-40 p-4 sm:h-auto">
|
<div className="relative hidden h-40 w-full p-4 sm:h-auto md:flex">
|
||||||
<div className="group">
|
<div className="group w-full">
|
||||||
<Image
|
<Image
|
||||||
src={appBannerHub}
|
src={appBannerHub}
|
||||||
alt="Hub Banner"
|
alt="Hub Banner"
|
||||||
@ -392,55 +442,92 @@ const HubScreen = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 p-4 py-0 sm:px-16">
|
{/* Filters and Model List */}
|
||||||
<>
|
<div className="ml-4 mt-8 flex h-full w-full flex-row">
|
||||||
<div className="flex flex-row">
|
{/* Filters */}
|
||||||
<div className="flex w-full flex-col items-start justify-between gap-4 py-4 first:pt-0 sm:flex-row">
|
<div className="hidden h-full w-[224px] shrink-0 flex-col border-r border-[hsla(var(--app-border))] pr-6 md:flex">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex w-full flex-row justify-between">
|
||||||
{filterOptions.map((e) => (
|
Filters
|
||||||
<div
|
<button
|
||||||
key={e.value}
|
className="font-medium text-blue-500"
|
||||||
className={twMerge(
|
onClick={() => {
|
||||||
'rounded-md border border-[hsla(var(--app-border))] duration-200 hover:bg-[hsla(var(--secondary-bg))]',
|
setCtxLenFilter(0)
|
||||||
e.value === filterOption
|
setMinModelSizeFilter(0)
|
||||||
? 'bg-[hsla(var(--secondary-bg))]'
|
setMaxModelSizeFilter(100)
|
||||||
: 'bg-[hsla(var(--app-bg))]'
|
setCompatible(false)
|
||||||
)}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
Reset
|
||||||
theme={'ghost'}
|
</button>
|
||||||
variant={'soft'}
|
</div>
|
||||||
onClick={() => setFilterOption(e.value)}
|
<div className="mt-8 flex flex-row gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={compatible}
|
||||||
|
onChange={() => setCompatible(!compatible)}
|
||||||
|
className="w-9"
|
||||||
|
/>
|
||||||
|
Compatible with my device
|
||||||
|
</div>
|
||||||
|
<div className="mt-12">
|
||||||
|
<ContextLengthFilter />
|
||||||
|
</div>
|
||||||
|
<div className="mt-12">
|
||||||
|
<ModelSizeFilter />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Model List */}
|
||||||
|
<div className="w-full p-4 py-0 sm:px-16">
|
||||||
|
<>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<div className="flex w-full flex-col items-start justify-between gap-4 py-4 first:pt-0 sm:flex-row">
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
{filterOptions.map((e) => (
|
||||||
|
<div
|
||||||
|
key={e.value}
|
||||||
|
className={twMerge(
|
||||||
|
'shrink-0 rounded-md border border-[hsla(var(--app-border))] duration-200 hover:bg-[hsla(var(--secondary-bg))]',
|
||||||
|
e.value === filterOption
|
||||||
|
? 'bg-[hsla(var(--secondary-bg))]'
|
||||||
|
: 'bg-[hsla(var(--app-bg))]'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{e.name}
|
<Button
|
||||||
</Button>
|
theme={'ghost'}
|
||||||
</div>
|
variant={'soft'}
|
||||||
))}
|
onClick={() => setFilterOption(e.value)}
|
||||||
|
>
|
||||||
|
{e.name}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 flex w-full justify-end">
|
||||||
|
<Select
|
||||||
|
value={sortSelected}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSortSelected(value)
|
||||||
|
}}
|
||||||
|
options={sortMenus}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4 flex w-full justify-end">
|
{(filterOption === 'on-device' ||
|
||||||
<Select
|
filterOption === 'all') && (
|
||||||
value={sortSelected}
|
<ModelList
|
||||||
onValueChange={(value) => {
|
models={filteredModels}
|
||||||
setSortSelected(value)
|
onSelectedModel={(model) => setSelectedModel(model)}
|
||||||
}}
|
|
||||||
options={sortMenus}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</div>
|
{(filterOption === 'cloud' || filterOption === 'all') && (
|
||||||
{(filterOption === 'on-device' || filterOption === 'all') && (
|
<ModelList
|
||||||
<ModelList
|
models={remoteModelSources}
|
||||||
models={sortedModels}
|
onSelectedModel={(model) => setSelectedModel(model)}
|
||||||
onSelectedModel={(model) => setSelectedModel(model)}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</>
|
||||||
{(filterOption === 'cloud' || filterOption === 'all') && (
|
</div>
|
||||||
<ModelList
|
|
||||||
models={remoteModelSources}
|
|
||||||
onSelectedModel={(model) => setSelectedModel(model)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@ -98,21 +98,15 @@ const MyModels = () => {
|
|||||||
return InferenceEngine.cortex_llamacpp
|
return InferenceEngine.cortex_llamacpp
|
||||||
return x.engine
|
return x.engine
|
||||||
})
|
})
|
||||||
const groupByEngine = findByEngine
|
|
||||||
.filter(function (item, index) {
|
const groupByEngine = [...new Set(findByEngine)].sort((a, b) => {
|
||||||
if (findByEngine.indexOf(item) === index) return item
|
const aPriority = priorityEngine.indexOf(a)
|
||||||
})
|
const bPriority = priorityEngine.indexOf(b)
|
||||||
.sort((a, b) => {
|
if (aPriority !== -1 && bPriority !== -1) return aPriority - bPriority
|
||||||
if (priorityEngine.includes(a) && priorityEngine.includes(b)) {
|
if (aPriority !== -1) return -1
|
||||||
return priorityEngine.indexOf(a) - priorityEngine.indexOf(b)
|
if (bPriority !== -1) return 1
|
||||||
} else if (priorityEngine.includes(a)) {
|
return 0
|
||||||
return -1
|
})
|
||||||
} else if (priorityEngine.includes(b)) {
|
|
||||||
return 1
|
|
||||||
} else {
|
|
||||||
return 0 // Leave the rest in their original order
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const getEngineStatusReady: InferenceEngine[] = Object.entries(engines ?? {})
|
const getEngineStatusReady: InferenceEngine[] = Object.entries(engines ?? {})
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user