Faisal Amir e8ee694abd
feat: new starter screen (#3217)
* feat: starter screen

* chore: update flow starter screen

* fix CI

Signed-off-by: James <namnh0122@gmail.com>

* chore: update variable name

* chore: fix CI

* update download cortex binary

Signed-off-by: James <namnh0122@gmail.com>

---------

Signed-off-by: James <namnh0122@gmail.com>
Co-authored-by: James <namnh0122@gmail.com>
2024-07-31 16:41:39 +07:00

189 lines
6.5 KiB
TypeScript

import React, { Fragment, useCallback, useState } from 'react'
import Image from 'next/image'
import { Model, RemoteEngine, RemoteEngines } from '@janhq/core'
import { Input } from '@janhq/joi'
import { useSetAtom } from 'jotai'
import { SearchIcon, PlusIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import Spinner from '@/containers/Loader/Spinner'
import useModelHub from '@/hooks/useModelHub'
import BuiltInModelCard from '@/screens/HubScreen2/components/BuiltInModelCard'
import { HfModelEntry } from '@/utils/huggingface'
import { getTitleByCategory } from '@/utils/model-engine'
import { MainViewState, mainViewStateAtom } from '@/helpers/atoms/App.atom'
import { localModelModalStageAtom } from '@/helpers/atoms/DownloadLocalModel.atom'
import { hubFilterAtom } from '@/helpers/atoms/Hub.atom'
import { setUpRemoteModelStageAtom } from '@/helpers/atoms/SetupRemoteModel.atom'
const OnDeviceStarterScreen = () => {
const { data } = useModelHub()
const [searchValue, setSearchValue] = useState('')
const setLocalModelModalStage = useSetAtom(localModelModalStageAtom)
const setUpRemoteModelStage = useSetAtom(setUpRemoteModelStageAtom)
const setMainViewState = useSetAtom(mainViewStateAtom)
const setFilter = useSetAtom(hubFilterAtom)
const onItemClick = useCallback(
(name: string) => {
setLocalModelModalStage('MODEL_LIST', name)
},
[setLocalModelModalStage]
)
if (!data) return <Spinner />
const builtInModels: HfModelEntry[] =
data.modelCategories.get('BuiltInModels') || []
const huggingFaceModels: HfModelEntry[] =
data.modelCategories.get('HuggingFace') || []
const engineModelMap = new Map<typeof RemoteEngines, HfModelEntry[]>()
Object.entries(data.modelCategories).forEach(([key, value]) => {
if (key !== 'HuggingFace' && key !== 'BuiltInModels') {
engineModelMap.set(key as unknown as typeof RemoteEngines, value)
}
})
const models: HfModelEntry[] = builtInModels.concat(huggingFaceModels)
const filteredModels = models.filter((model) => {
return model.name.toLowerCase().includes(searchValue.toLowerCase())
})
const recommendModels = models.filter((model) => {
return (
model.name.toLowerCase().includes('cortexso/tinyllama') ||
model.name.toLowerCase().includes('cortexso/mistral')
)
})
return (
<Fragment>
<div className="relative">
<Input
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
placeholder="Search..."
prefixIcon={<SearchIcon size={16} />}
/>
<div
className={twMerge(
'absolute left-0 top-10 max-h-[240px] w-full overflow-x-auto rounded-lg border border-[hsla(var(--app-border))] bg-[hsla(var(--app-bg))]',
!searchValue.length ? 'invisible' : 'visible'
)}
>
{!filteredModels.length ? (
<div className="p-3 text-center">
<p className="line-clamp-1 text-[hsla(var(--text-secondary))]">
No Result Found
</p>
</div>
) : (
filteredModels.map((model) => (
<div
className="cursor-pointer p-2 text-left transition-all hover:bg-[hsla(var(--dropdown-menu-hover-bg))]"
key={model.id}
onClick={() => onItemClick(model.name)}
>
<p className="line-clamp-1">
{model.name.replaceAll('cortexso/', '')}
</p>
</div>
))
)}
</div>
</div>
<div className="mb-4 mt-8 flex items-center justify-between">
<h2 className="text-[hsla(var(--text-secondary))]">On-device Models</h2>
<p
className="cursor-pointer text-sm text-[hsla(var(--app-link))]"
onClick={() => {
setFilter('On-device')
setMainViewState(MainViewState.Hub)
}}
>
See All
</p>
</div>
{recommendModels.map((model) => (
<BuiltInModelCard key={model.name} {...model} />
))}
<div className="mb-4 mt-8 flex items-center justify-between">
<h2 className="text-[hsla(var(--text-secondary))]">Cloud Models</h2>
</div>
<div className="flex items-center gap-6">
{Array.from(engineModelMap.entries())
.slice(0, 3)
.map(([engine, models]) => {
const engineLogo: string | undefined = models.find(
(entry) => entry.model?.metadata?.logo != null
)?.model?.metadata?.logo
const apiKeyUrl: string | undefined = models.find(
(entry) => entry.model?.metadata?.api_key_url != null
)?.model?.metadata?.api_key_url
const defaultModel: Model | undefined = models.find(
(entry) => entry.model != null
)?.model
return (
<div
className="flex cursor-pointer flex-col items-center justify-center gap-2"
key={engine as unknown as string}
onClick={() => {
setUpRemoteModelStage(
'SETUP_API_KEY',
engine as unknown as RemoteEngine,
{
logo: engineLogo,
api_key_url: apiKeyUrl,
model: defaultModel,
}
)
}}
>
{engineLogo ? (
<Image
width={48}
height={48}
src={engineLogo}
alt="Engine logo"
className="rounded-full"
/>
) : (
<div className="flex h-12 w-12 items-center justify-center rounded-full border border-[hsla(var(--app-border))] bg-gradient-to-r from-cyan-500 to-blue-500"></div>
)}
<p>{getTitleByCategory(engine as unknown as RemoteEngine)}</p>
</div>
)
})}
<div className="flex flex-col items-center justify-center gap-2">
<div
className="flex h-12 w-12 cursor-pointer items-center justify-center rounded-full border border-dashed border-[hsla(var(--app-border))]"
onClick={() => {
setFilter('Cloud')
setMainViewState(MainViewState.Hub)
}}
>
<PlusIcon className="text-[hsla(var(--text-secondary))]" />
</div>
<p>See All</p>
</div>
</div>
</Fragment>
)
}
export default OnDeviceStarterScreen