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:
parent
5369da78f5
commit
e8ee694abd
8
Makefile
8
Makefile
@ -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:
|
||||
|
||||
@ -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
|
||||
@ -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<
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user