forked from janhq/jan
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: starter screen * chore: fix unused import * chore: update see all color
- Loading branch information
Showing
4 changed files
with
394 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
321 changes: 303 additions & 18 deletions
321
web/screens/Thread/ThreadCenterPanel/ChatBody/EmptyModel/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,317 @@ | ||
import { memo } from 'react' | ||
import React, { Fragment, useState } from 'react' | ||
|
||
import { Button } from '@janhq/joi' | ||
import { useSetAtom } from 'jotai' | ||
import Image from 'next/image' | ||
|
||
import { InferenceEngine } from '@janhq/core/.' | ||
import { Button, Input, Progress, ScrollArea } from '@janhq/joi' | ||
|
||
import { useAtomValue, useSetAtom } from 'jotai' | ||
import { SearchIcon, DownloadCloudIcon } from 'lucide-react' | ||
|
||
import { twMerge } from 'tailwind-merge' | ||
|
||
import LogoMark from '@/containers/Brand/Logo/Mark' | ||
import CenterPanelContainer from '@/containers/CenterPanelContainer' | ||
|
||
import ProgressCircle from '@/containers/Loader/ProgressCircle' | ||
|
||
import ModelLabel from '@/containers/ModelLabel' | ||
|
||
import { MainViewState } from '@/constants/screens' | ||
|
||
import useDownloadModel from '@/hooks/useDownloadModel' | ||
|
||
import { modelDownloadStateAtom } from '@/hooks/useDownloadState' | ||
|
||
import { formatDownloadPercentage, toGibibytes } from '@/utils/converter' | ||
import { | ||
getLogoEngine, | ||
getTitleByEngine, | ||
localEngines, | ||
} from '@/utils/modelEngine' | ||
|
||
import { mainViewStateAtom } from '@/helpers/atoms/App.atom' | ||
import { | ||
configuredModelsAtom, | ||
getDownloadingModelAtom, | ||
} from '@/helpers/atoms/Model.atom' | ||
import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom' | ||
|
||
type Props = { | ||
extensionHasSettings: { | ||
name?: string | ||
setting: string | ||
apiKey: string | ||
provider: string | ||
}[] | ||
} | ||
|
||
const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => { | ||
const [searchValue, setSearchValue] = useState('') | ||
const downloadingModels = useAtomValue(getDownloadingModelAtom) | ||
const { downloadModel } = useDownloadModel() | ||
const downloadStates = useAtomValue(modelDownloadStateAtom) | ||
const setSelectedSetting = useSetAtom(selectedSettingAtom) | ||
|
||
const EmptyModel = () => { | ||
const configuredModels = useAtomValue(configuredModelsAtom) | ||
const setMainViewState = useSetAtom(mainViewStateAtom) | ||
|
||
const featuredModel = configuredModels.filter((x) => | ||
x.metadata.tags.includes('Featured') | ||
) | ||
|
||
const remoteModel = configuredModels.filter( | ||
(x) => !localEngines.includes(x.engine) | ||
) | ||
|
||
const filteredModels = configuredModels.filter((model) => { | ||
return ( | ||
localEngines.includes(model.engine) && | ||
model.name.toLowerCase().includes(searchValue.toLowerCase()) | ||
) | ||
}) | ||
|
||
const remoteModelEngine = remoteModel.map((x) => x.engine) | ||
const groupByEngine = remoteModelEngine.filter(function (item, index) { | ||
if (remoteModelEngine.indexOf(item) === index) return item | ||
}) | ||
|
||
const itemsPerRow = 5 | ||
|
||
const getRows = (array: string[], itemsPerRow: number) => { | ||
const rows = [] | ||
for (let i = 0; i < array.length; i += itemsPerRow) { | ||
rows.push(array.slice(i, i + itemsPerRow)) | ||
} | ||
return rows | ||
} | ||
|
||
const rows = getRows(groupByEngine, itemsPerRow) | ||
|
||
const [visibleRows, setVisibleRows] = useState(1) | ||
|
||
return ( | ||
<div className="mx-auto flex h-full w-3/4 flex-col items-center justify-center text-center"> | ||
<LogoMark className="mx-auto mb-4 animate-wave" width={56} height={56} /> | ||
<h1 className="text-base font-semibold">Welcome!</h1> | ||
<p className="mt-1 text-[hsla(var(--text-secondary))]"> | ||
You need to download your first model | ||
</p> | ||
<Button | ||
className="mt-4" | ||
onClick={() => setMainViewState(MainViewState.Hub)} | ||
> | ||
Explore The Hub | ||
</Button> | ||
</div> | ||
<CenterPanelContainer> | ||
<ScrollArea className="flex h-full w-full items-center"> | ||
<div className="relative mt-4 flex h-full w-full flex-col items-center justify-center"> | ||
<div className="mx-auto flex h-full w-3/4 flex-col items-center justify-center py-16 text-center"> | ||
<LogoMark | ||
className="mx-auto mb-4 animate-wave" | ||
width={48} | ||
height={48} | ||
/> | ||
<h1 className="text-base font-semibold">Select a model to start</h1> | ||
<div className="mt-6 w-full lg:w-1/2"> | ||
<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) => { | ||
const isDownloading = downloadingModels.some( | ||
(md) => md.id === model.id | ||
) | ||
return ( | ||
<div | ||
key={model.id} | ||
className="flex items-center justify-between gap-4 px-3 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]" | ||
> | ||
<div className="flex items-center gap-2"> | ||
<p | ||
className={twMerge('line-clamp-1')} | ||
title={model.name} | ||
> | ||
{model.name} | ||
</p> | ||
<ModelLabel metadata={model.metadata} compact /> | ||
</div> | ||
<div className="flex items-center gap-2 text-[hsla(var(--text-tertiary))]"> | ||
<span className="font-medium"> | ||
{toGibibytes(model.metadata.size)} | ||
</span> | ||
{!isDownloading ? ( | ||
<DownloadCloudIcon | ||
size={18} | ||
className="cursor-pointer text-[hsla(var(--app-link))]" | ||
onClick={() => downloadModel(model)} | ||
/> | ||
) : ( | ||
Object.values(downloadStates) | ||
.filter((x) => x.modelId === model.id) | ||
.map((item) => ( | ||
<ProgressCircle | ||
key={item.modelId} | ||
percentage={ | ||
formatDownloadPercentage( | ||
item?.percent, | ||
{ | ||
hidePercentage: true, | ||
} | ||
) as number | ||
} | ||
size={100} | ||
/> | ||
)) | ||
)} | ||
</div> | ||
</div> | ||
) | ||
}) | ||
)} | ||
</div> | ||
</div> | ||
<div className="mt-6 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(--text-secondary))]" | ||
onClick={() => { | ||
setMainViewState(MainViewState.Hub) | ||
}} | ||
> | ||
See All | ||
</p> | ||
</div> | ||
|
||
{featuredModel.slice(0, 2).map((featModel) => { | ||
const isDownloading = downloadingModels.some( | ||
(md) => md.id === featModel.id | ||
) | ||
return ( | ||
<div | ||
key={featModel.id} | ||
className="my-2 flex items-center justify-between gap-2 border-b border-[hsla(var(--app-border))] py-4 last:border-none" | ||
> | ||
<div className="w-full text-left"> | ||
<h6>{featModel.name}</h6> | ||
<p className="mt-1 text-[hsla(var(--text-secondary))]"> | ||
{featModel.metadata.author} | ||
</p> | ||
</div> | ||
|
||
{isDownloading ? ( | ||
<div className="flex w-full items-center gap-2"> | ||
{Object.values(downloadStates).map((item, i) => ( | ||
<div | ||
className="flex w-full items-center gap-2" | ||
key={i} | ||
> | ||
<Progress | ||
className="w-full" | ||
value={ | ||
formatDownloadPercentage(item?.percent, { | ||
hidePercentage: true, | ||
}) as number | ||
} | ||
/> | ||
<div className="flex items-center justify-between gap-x-2"> | ||
<div className="flex gap-x-2"> | ||
<span className="font-medium text-[hsla(var(--primary-bg))]"> | ||
{formatDownloadPercentage(item?.percent)} | ||
</span> | ||
</div> | ||
</div> | ||
</div> | ||
))} | ||
</div> | ||
) : ( | ||
<Button | ||
theme="ghost" | ||
className="!bg-[hsla(var(--secondary-bg))]" | ||
onClick={() => downloadModel(featModel)} | ||
> | ||
Download | ||
</Button> | ||
)} | ||
</div> | ||
) | ||
})} | ||
|
||
<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 flex-col justify-center gap-6"> | ||
{rows.slice(0, visibleRows).map((row, rowIndex) => { | ||
return ( | ||
<div | ||
key={rowIndex} | ||
className="my-2 flex items-center justify-normal gap-10" | ||
> | ||
{row.map((remoteEngine) => { | ||
const engineLogo = getLogoEngine( | ||
remoteEngine as InferenceEngine | ||
) | ||
|
||
return ( | ||
<div | ||
className="flex cursor-pointer flex-col items-center justify-center gap-4" | ||
key={remoteEngine} | ||
onClick={() => { | ||
setMainViewState(MainViewState.Settings) | ||
setSelectedSetting( | ||
extensionHasSettings.find((x) => | ||
x.name?.toLowerCase().includes(remoteEngine) | ||
)?.setting as string | ||
) | ||
}} | ||
> | ||
{engineLogo && ( | ||
<Image | ||
width={48} | ||
height={48} | ||
src={engineLogo} | ||
alt="Engine logo" | ||
className="h-10 w-10 flex-shrink-0" | ||
/> | ||
)} | ||
|
||
<p> | ||
{getTitleByEngine( | ||
remoteEngine as InferenceEngine | ||
)} | ||
</p> | ||
</div> | ||
) | ||
})} | ||
</div> | ||
) | ||
})} | ||
</div> | ||
{visibleRows < rows.length && ( | ||
<button | ||
onClick={() => setVisibleRows(visibleRows + 1)} | ||
className="mt-4 text-[hsla(var(--text-secondary))]" | ||
> | ||
See More | ||
</button> | ||
)} | ||
</Fragment> | ||
</div> | ||
</div> | ||
</div> | ||
</ScrollArea> | ||
</CenterPanelContainer> | ||
) | ||
} | ||
|
||
export default memo(EmptyModel) | ||
export default OnDeviceStarterScreen |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.