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>
This commit is contained in:
Faisal Amir 2024-07-31 16:41:39 +07:00 committed by GitHub
parent 5369da78f5
commit e8ee694abd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 252 additions and 33 deletions

View File

@ -34,11 +34,11 @@ else
@tgz_count=$$(find pre-install -type f -name "*.tgz" | wc -l); dir_count=$$(find extensions -mindepth 1 -maxdepth 1 -type d -exec test -e '{}/package.json' \; -print | wc -l); if [ $$tgz_count -ne $$dir_count ]; then echo "Number of .tgz files in pre-install ($$tgz_count) does not match the number of subdirectories in extension ($$dir_count)"; exit 1; else echo "Extension build successful"; fi
endif
dev: check-file-counts
dev: install-and-build
yarn dev
# Linting
lint: check-file-counts
lint: install-and-build
yarn lint
update-playwright-config:
@ -106,11 +106,11 @@ test: lint
yarn test
# Builds and publishes the app
build-and-publish: check-file-counts
build-and-publish: install-and-build
yarn build:publish
# Build
build: check-file-counts
build: install-and-build
yarn build
clean:

View File

@ -1,3 +1,6 @@
@echo off
set /p CORTEX_VERSION=<./resources/version.txt
.\node_modules\.bin\download https://github.com/janhq/cortex/releases/download/v%CORTEX_VERSION%/cortex-%CORTEX_VERSION%-amd64-windows.tar.gz -e -s 1 -o ./resources/win
set DOWNLOAD_URL=https://github.com/janhq/cortex/releases/download/v%CORTEX_VERSION%/cortex-%CORTEX_VERSION%-amd64-windows.tar.gz
echo Downloading from %DOWNLOAD_URL%
.\node_modules\.bin\download %DOWNLOAD_URL% -e -o ./resources/win

View File

@ -1,8 +1,14 @@
import { RemoteEngine } from '@janhq/core'
import { atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
export type SetupRemoteModelStage = 'NONE' | 'SETUP_INTRO' | 'SETUP_API_KEY'
const IS_ANY_REMOTE_MODEL_CONFIGURED = 'isAnyRemoteModelConfigured'
export const isAnyRemoteModelConfiguredAtom = atomWithStorage(
IS_ANY_REMOTE_MODEL_CONFIGURED,
false
)
const remoteModelSetUpStageAtom = atom<SetupRemoteModelStage>('NONE')
const engineBeingSetUpAtom = atom<RemoteEngine | undefined>(undefined)
const remoteEngineBeingSetUpMetadataAtom = atom<

View File

@ -3,7 +3,7 @@ import { Fragment, useCallback, useEffect, useState } from 'react'
import Image from 'next/image'
import { Button, Input, Modal } from '@janhq/joi'
import { useAtom } from 'jotai'
import { useAtom, useSetAtom } from 'jotai'
import { ArrowUpRight } from 'lucide-react'
import useEngineMutation from '@/hooks/useEngineMutation'
@ -11,10 +11,13 @@ import useEngineQuery from '@/hooks/useEngineQuery'
import { getTitleByCategory } from '@/utils/model-engine'
import { isAnyRemoteModelConfiguredAtom } from '@/helpers/atoms/SetupRemoteModel.atom'
import { setUpRemoteModelStageAtom } from '@/helpers/atoms/SetupRemoteModel.atom'
const SetUpApiKeyModal: React.FC = () => {
const updateEngineConfig = useEngineMutation()
const isAnyRemoteModelConfigured = useSetAtom(isAnyRemoteModelConfiguredAtom)
const { data: engineData } = useEngineQuery()
const [{ stage, remoteEngine, metadata }, setUpRemoteModelStage] = useAtom(
@ -42,7 +45,8 @@ const SetUpApiKeyModal: React.FC = () => {
value: apiKey,
},
})
}, [updateEngineConfig, apiKey, remoteEngine])
isAnyRemoteModelConfigured(true)
}, [remoteEngine, updateEngineConfig, apiKey, isAnyRemoteModelConfigured])
const onDismiss = useCallback(() => {
setUpRemoteModelStage('NONE', undefined)

View File

@ -0,0 +1,188 @@
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

View File

@ -1,35 +1,28 @@
import { memo } from 'react'
import { Button } from '@janhq/joi'
import { useSetAtom } from 'jotai'
import LogoMark from '@/containers/Brand/Logo/Mark'
import CenterPanelContainer from '@/containers/CenterPanelContainer'
import { MainViewState, mainViewStateAtom } from '@/helpers/atoms/App.atom'
import OnDeviceStarterScreen from './OnDeviceListStarter'
const EmptyModel = () => {
const setMainViewState = useSetAtom(mainViewStateAtom)
return (
<CenterPanelContainer>
<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={48}
height={48}
/>
<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 className="flex h-full w-full items-center overflow-x-hidden">
<div className="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 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-10 w-full lg:w-1/2">
<OnDeviceStarterScreen />
</div>
</div>
</div>
</div>
</CenterPanelContainer>
)

View File

@ -1,4 +1,10 @@
import { useAtomValue } from 'jotai'
import { Fragment, useEffect } from 'react'
import { Model } from '@janhq/core'
import { useAtom, useAtomValue } from 'jotai'
import useCortex from '@/hooks/useCortex'
import useModels from '@/hooks/useModels'
import ThreadLeftPanel from '@/screens/Thread/ThreadLeftPanel'
@ -7,19 +13,38 @@ import EmptyModel from './ThreadCenterPanel/ChatBody/EmptyModel'
import ThreadRightPanel from './ThreadRightPanel'
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
import {
isAnyRemoteModelConfiguredAtom,
setUpRemoteModelStageAtom,
} from '@/helpers/atoms/SetupRemoteModel.atom'
const ThreadScreen = () => {
const downloadedModels = useAtomValue(downloadedModelsAtom)
const isAnyRemoteModelConfigured = useAtomValue(
isAnyRemoteModelConfiguredAtom
)
const { createModel } = useCortex()
const { getModels } = useModels()
const [{ metadata }] = useAtom(setUpRemoteModelStageAtom)
useEffect(() => {
if (isAnyRemoteModelConfigured) {
createModel(metadata?.model as Model)
getModels()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAnyRemoteModelConfigured])
return (
<div className="relative flex h-full w-full flex-1 overflow-x-hidden">
{!downloadedModels.length ? (
{!downloadedModels.length && !isAnyRemoteModelConfigured ? (
<EmptyModel />
) : (
<>
<Fragment>
<ThreadLeftPanel />
<ThreadCenterPanel />
</>
</Fragment>
)}
<ThreadRightPanel />
</div>