* chore: add react developer tools to electron * feat: add small convert modal * feat: separate modals and add hugging face extension * feat: fully implement hugging face converter * fix: forgot to uncomment this... * fix: typo * feat: try hf-to-gguf script first and then use convert.py HF-to-GGUF has support for some unusual models maybe using convert.py first would be better but we can change the usage order later * fix: pre-install directory changed * fix: sometimes exit code is undefined * chore: download additional files for qwen * fix: event handling changed * chore: add one more necessary package * feat: download gguf-py from llama.cpp * fix: cannot interpret wildcards on GNU tar Co-authored-by: hiento09 <136591877+hiento09@users.noreply.github.com> --------- Co-authored-by: hiento09 <136591877+hiento09@users.noreply.github.com>
178 lines
6.3 KiB
TypeScript
178 lines
6.3 KiB
TypeScript
import { useCallback, useState } from 'react'
|
|
|
|
import { openExternalUrl } from '@janhq/core'
|
|
import {
|
|
Input,
|
|
ScrollArea,
|
|
Select,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
SelectContent,
|
|
SelectGroup,
|
|
SelectItem,
|
|
} from '@janhq/uikit'
|
|
|
|
import { useAtomValue } from 'jotai'
|
|
import { SearchIcon } from 'lucide-react'
|
|
|
|
import ExploreModelList from './ExploreModelList'
|
|
import { HuggingFaceModal } from './HuggingFaceModal'
|
|
|
|
import {
|
|
configuredModelsAtom,
|
|
downloadedModelsAtom,
|
|
} from '@/helpers/atoms/Model.atom'
|
|
|
|
const ExploreModelsScreen = () => {
|
|
const configuredModels = useAtomValue(configuredModelsAtom)
|
|
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
|
const [searchValue, setsearchValue] = useState('')
|
|
const [sortSelected, setSortSelected] = useState('All Models')
|
|
const sortMenu = ['All Models', 'Recommended', 'Downloaded']
|
|
const [showHuggingFaceModal, setShowHuggingFaceModal] = useState(false)
|
|
|
|
const filteredModels = configuredModels.filter((x) => {
|
|
if (sortSelected === 'Downloaded') {
|
|
return (
|
|
x.name.toLowerCase().includes(searchValue.toLowerCase()) &&
|
|
downloadedModels.some((y) => y.id === x.id)
|
|
)
|
|
} else if (sortSelected === 'Recommended') {
|
|
return (
|
|
x.name.toLowerCase().includes(searchValue.toLowerCase()) &&
|
|
x.metadata.tags.includes('Featured')
|
|
)
|
|
} else {
|
|
return x.name.toLowerCase().includes(searchValue.toLowerCase())
|
|
}
|
|
})
|
|
|
|
const onHowToImportModelClick = useCallback(() => {
|
|
openExternalUrl('https://jan.ai/guides/using-models/import-manually/')
|
|
}, [])
|
|
|
|
const onHuggingFaceConverterClick = () => {
|
|
setShowHuggingFaceModal(true)
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="flex h-full w-full overflow-y-auto bg-background"
|
|
data-testid="hub-container-test-id"
|
|
>
|
|
<div className="h-full w-full p-4">
|
|
<div className="h-full">
|
|
<HuggingFaceModal
|
|
open={showHuggingFaceModal}
|
|
onOpenChange={setShowHuggingFaceModal}
|
|
/>
|
|
<ScrollArea>
|
|
<div className="relative">
|
|
<img
|
|
src="./images/hub-banner.png"
|
|
alt="Hub Banner"
|
|
className="w-full object-cover"
|
|
/>
|
|
<div className="absolute left-1/2 top-1/2 w-1/3 -translate-x-1/2 -translate-y-1/2">
|
|
<div className="relative">
|
|
<SearchIcon
|
|
size={20}
|
|
className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground"
|
|
/>
|
|
<Input
|
|
placeholder="Search models"
|
|
className="bg-white pl-9 dark:bg-background"
|
|
onChange={(e) => {
|
|
setsearchValue(e.target.value)
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="mt-2 text-center">
|
|
<p
|
|
onClick={onHowToImportModelClick}
|
|
className="cursor-pointer font-semibold text-white underline"
|
|
>
|
|
How to manually import models
|
|
</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-white">or</p>
|
|
<p
|
|
onClick={onHuggingFaceConverterClick}
|
|
className="cursor-pointer font-semibold text-white underline"
|
|
>
|
|
Convert from Hugging Face
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="mx-auto w-4/5 py-6">
|
|
<div className="flex items-center justify-end">
|
|
{/* Temporary hide tabs */}
|
|
{/* <div className="inline-flex overflow-hidden rounded-lg border border-border">
|
|
<div
|
|
className={twMerge(
|
|
'flex cursor-pointer items-center space-x-2 border-r border-border px-3 py-2',
|
|
tabActive === 'Model' && 'bg-secondary'
|
|
)}
|
|
onClick={() => setTabActive('Model')}
|
|
>
|
|
<Code2Icon size={20} className="text-muted-foreground" />
|
|
<span className="font-semibold">Model</span>
|
|
</div>
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<div
|
|
className={twMerge(
|
|
'pointer-events-none flex cursor-pointer items-center space-x-2 px-3 py-2 text-muted-foreground',
|
|
tabActive === 'Assistant' && 'bg-secondary'
|
|
)}
|
|
onClick={() => setTabActive('Assistant')}
|
|
>
|
|
<UserIcon size={20} className="text-muted-foreground" />
|
|
<span className="font-semibold">Assistant</span>
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top" sideOffset={10}>
|
|
<span className="font-bold">Coming Soon</span>
|
|
<TooltipArrow />
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div> */}
|
|
|
|
<Select
|
|
value={sortSelected}
|
|
onValueChange={(value) => {
|
|
setSortSelected(value)
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-[200px]">
|
|
<SelectValue placeholder="Sort By"></SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent className="right-0 block w-full min-w-[200px] pr-0">
|
|
<SelectGroup>
|
|
{sortMenu.map((x, i) => {
|
|
return (
|
|
<SelectItem key={i} value={x}>
|
|
<span className="line-clamp-1 block">{x}</span>
|
|
</SelectItem>
|
|
)
|
|
})}
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="mt-6">
|
|
<ExploreModelList models={filteredModels} />
|
|
</div>
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default ExploreModelsScreen
|