/* eslint-disable @typescript-eslint/naming-convention */ import { useCallback, useMemo, useRef, useState, useEffect } from 'react' import { useDropzone } from 'react-dropzone' import Image from 'next/image' import { ModelSource } from '@janhq/core' import { ScrollArea, Button, Select, Tabs, useClickOutside, Switch, } from '@janhq/joi' import { motion as m } from 'framer-motion' import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' import { ImagePlusIcon, UploadCloudIcon, UploadIcon } from 'lucide-react' import { twMerge } from 'tailwind-merge' import CenterPanelContainer from '@/containers/CenterPanelContainer' import ModelSearch from '@/containers/ModelSearch' import { useGetEngineModelSources } from '@/hooks/useEngineManagement' import { setImportModelStageAtom } from '@/hooks/useImportModel' import { useGetModelSources, useModelSourcesMutation, } from '@/hooks/useModelSource' import ModelList from '@/screens/Hub/ModelList' import { toGigabytes } from '@/utils/converter' import { extractModelRepo } from '@/utils/modelSource' import { fuzzySearch } from '@/utils/search' import ContextLengthFilter, { hubCtxLenAtom } from './ModelFilter/ContextLength' import ModelSizeFilter, { hubModelSizeMaxAtom, hubModelSizeMinAtom, } from './ModelFilter/ModelSize' import ModelPage from './ModelPage' import { getAppBannerHubAtom, setAppBannerHubAtom, } from '@/helpers/atoms/App.atom' import { modelDetailAtom } from '@/helpers/atoms/Model.atom' import { showScrollBarAtom } from '@/helpers/atoms/Setting.atom' import { totalRamAtom } from '@/helpers/atoms/SystemBar.atom' const sortMenus = [ { name: 'Most downloaded', value: 'most-downloaded', }, { name: 'Newest', value: 'newest', }, ] const filterOptions = [ { name: 'All', value: 'all', }, { name: 'On-device', value: 'on-device', }, { name: 'Cloud', value: 'cloud', }, ] const hubCompatibleAtom = atom(false) const HubScreen = () => { const { sources } = useGetModelSources() const { sources: remoteModelSources } = useGetEngineModelSources() const { addModelSource } = useModelSourcesMutation() const [searchValue, setSearchValue] = useState('') const [sortSelected, setSortSelected] = useState('newest') const [filterOption, setFilterOption] = useState('all') const [hubBannerOption, setHubBannerOption] = useState('gallery') const [showHubBannerSetting, setShowHubBannerSetting] = useState(false) const appBannerHub = useAtomValue(getAppBannerHubAtom) const setAppBannerHub = useSetAtom(setAppBannerHubAtom) const [selectedModel, setSelectedModel] = useState( undefined ) const showScrollBar = useAtomValue(showScrollBarAtom) const [modelDetail, setModelDetail] = useAtom(modelDetailAtom) const setImportModelStage = useSetAtom(setImportModelStageAtom) const dropdownRef = useRef(null) const imageInputRef = useRef(null) const hubBannerSettingRef = useRef(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 largestModel = sources && sources .flatMap((model) => model.models) .reduce((max, model) => (model.size > max.size ? model : max), { size: 0, }) const searchedModels = useMemo( () => searchValue.length ? (sources?.filter((e) => fuzzySearch( searchValue.replaceAll(' ', '').toLowerCase(), e.id.toLowerCase() ) ) ?? []) : [], [sources, searchValue] ) const filteredModels = useMemo(() => { return (sources ?? []).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 === largestModel?.size || model.models.some((e) => e.size <= maxModelSizeFilter * (1 << 30)) return isCompatible && matchesCtxLen && matchesMinSize && matchesMaxSize }) }, [ sources, compatible, ctxLenFilter, minModelSizeFilter, maxModelSizeFilter, totalRam, ]) const sortedModels = useMemo(() => { return filteredModels.sort((a, b) => { if (sortSelected === 'most-downloaded') { return b.metadata.downloads - a.metadata.downloads } else { return ( new Date(b.metadata.createdAt).getTime() - new Date(a.metadata.createdAt).getTime() ) } }) }, [sortSelected, filteredModels]) useEffect(() => { if (modelDetail) { setSelectedModel(sources?.find((e) => e.id === modelDetail)) setModelDetail(undefined) } }, [modelDetail, sources, setModelDetail, addModelSource]) useEffect(() => { if (largestModel) { setMaxModelSizeFilter( Number( toGigabytes(Number(largestModel?.size), { hideUnit: true, toFixed: 0, }) ) ) } }, [largestModel]) useEffect(() => { if (selectedModel) { // Try add the model source again to update it's data addModelSource(selectedModel.id).catch(console.debug) } }, [sources, selectedModel, addModelSource, setSelectedModel]) useClickOutside( () => { setSearchValue('') }, null, [dropdownRef.current] ) useClickOutside( () => { setShowHubBannerSetting(false) }, null, [hubBannerSettingRef.current] ) const onImportModelClick = useCallback(() => { setImportModelStage('SELECTING_MODEL') }, [setImportModelStage]) const onSearchUpdate = useCallback((input: string) => { setSearchValue(input) }, []) const setBannerHubImage = (image: string) => { setShowHubBannerSetting(false) setAppBannerHub(image) } /** * Handles the change event of the extension file input element by setting the file name state. * Its to be used to display the extension file name of the selected file. * @param event - The change event object. */ const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0] if (!file) return const fileType = file.type if (!fileType.startsWith('image/')) { alert('Please upload an image file.') return } const reader = new FileReader() reader.onload = () => { // FileReader result is already in a valid Base64 format setBannerHubImage(reader.result as string) } reader.readAsDataURL(file) } const { isDragActive, getRootProps } = useDropzone({ noClick: true, multiple: true, accept: { 'image/jpeg': ['.jpeg'], 'image/png': ['.png'], 'image/jpg': ['.jpg'], }, onDrop: (files) => { const reader = new FileReader() reader.onload = () => { // FileReader result is already in a valid Base64 format setBannerHubImage(reader.result as string) } reader.readAsDataURL(files[0]) }, }) return ( {!selectedModel && ( <>
Hub Banner
setShowHubBannerSetting(!showHubBannerSetting) } >
setHubBannerOption(value)} />
{hubBannerOption === 'gallery' && ( {Array.from({ length: 30 }, (_, i) => i + 1).map( (e) => { return (
setBannerHubImage( `./images/HubBanner/banner-${e}.jpg` ) } > banner-img
) } )}
)} {hubBannerOption === 'upload' && (
{ imageInputRef.current?.click() }} >
{!isDragActive && ( <> Click to upload   or drag and drop

Image size: 920x200

)} {isDragActive && ( Drop here )}
)}
0 && 'visible' )} > {searchedModels.length === 0 ? (
No results found
) : (
{searchedModels.map((model) => (
{ setSelectedModel(model) e.stopPropagation() }} > {searchValue.includes('huggingface.co') && ( <> {' '} )} {extractModelRepo(model.id)}
))}
)}
{/* Filters and Model List */}
{/* Filters */}
Filters
setCompatible(!compatible)} className="w-9" /> Compatible with my device
{/* Model List */}
<>
{filterOptions.map((e) => (
))}