From e8ee694abd33b34112d2c7d09f8c03370c2d22cc Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Wed, 31 Jul 2024 16:41:39 +0700 Subject: [PATCH] feat: new starter screen (#3217) * feat: starter screen * chore: update flow starter screen * fix CI Signed-off-by: James * chore: update variable name * chore: fix CI * update download cortex binary Signed-off-by: James --------- Signed-off-by: James Co-authored-by: James --- Makefile | 8 +- electron/download.bat | 5 +- web/helpers/atoms/SetupRemoteModel.atom.ts | 6 + .../components/SetUpApiKeyModal.tsx | 8 +- .../EmptyModel/OnDeviceListStarter.tsx | 188 ++++++++++++++++++ .../ChatBody/EmptyModel/index.tsx | 37 ++-- web/screens/Thread/index.tsx | 33 ++- 7 files changed, 252 insertions(+), 33 deletions(-) create mode 100644 web/screens/Thread/ThreadCenterPanel/ChatBody/EmptyModel/OnDeviceListStarter.tsx diff --git a/Makefile b/Makefile index 09df90bbf..e2c6a4a2a 100644 --- a/Makefile +++ b/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: diff --git a/electron/download.bat b/electron/download.bat index dae26683b..817ceb979 100644 --- a/electron/download.bat +++ b/electron/download.bat @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/web/helpers/atoms/SetupRemoteModel.atom.ts b/web/helpers/atoms/SetupRemoteModel.atom.ts index a3a07eda8..798e6d02f 100644 --- a/web/helpers/atoms/SetupRemoteModel.atom.ts +++ b/web/helpers/atoms/SetupRemoteModel.atom.ts @@ -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('NONE') const engineBeingSetUpAtom = atom(undefined) const remoteEngineBeingSetUpMetadataAtom = atom< diff --git a/web/screens/HubScreen2/components/SetUpApiKeyModal.tsx b/web/screens/HubScreen2/components/SetUpApiKeyModal.tsx index 5e95e8e51..052e80393 100644 --- a/web/screens/HubScreen2/components/SetUpApiKeyModal.tsx +++ b/web/screens/HubScreen2/components/SetUpApiKeyModal.tsx @@ -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) diff --git a/web/screens/Thread/ThreadCenterPanel/ChatBody/EmptyModel/OnDeviceListStarter.tsx b/web/screens/Thread/ThreadCenterPanel/ChatBody/EmptyModel/OnDeviceListStarter.tsx new file mode 100644 index 000000000..90ca94991 --- /dev/null +++ b/web/screens/Thread/ThreadCenterPanel/ChatBody/EmptyModel/OnDeviceListStarter.tsx @@ -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 + + const builtInModels: HfModelEntry[] = + data.modelCategories.get('BuiltInModels') || [] + const huggingFaceModels: HfModelEntry[] = + data.modelCategories.get('HuggingFace') || [] + + const engineModelMap = new Map() + 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 ( + +
+ setSearchValue(e.target.value)} + placeholder="Search..." + prefixIcon={} + /> +
+ {!filteredModels.length ? ( +
+

+ No Result Found +

+
+ ) : ( + filteredModels.map((model) => ( +
onItemClick(model.name)} + > +

+ {model.name.replaceAll('cortexso/', '')} +

+
+ )) + )} +
+
+
+

On-device Models

+

{ + setFilter('On-device') + setMainViewState(MainViewState.Hub) + }} + > + See All +

+
+ {recommendModels.map((model) => ( + + ))} + +
+

Cloud Models

+
+ +
+ {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 ( +
{ + setUpRemoteModelStage( + 'SETUP_API_KEY', + engine as unknown as RemoteEngine, + { + logo: engineLogo, + api_key_url: apiKeyUrl, + model: defaultModel, + } + ) + }} + > + {engineLogo ? ( + Engine logo + ) : ( +
+ )} +

{getTitleByCategory(engine as unknown as RemoteEngine)}

+
+ ) + })} + +
+
{ + setFilter('Cloud') + setMainViewState(MainViewState.Hub) + }} + > + +
+

See All

+
+
+
+ ) +} + +export default OnDeviceStarterScreen diff --git a/web/screens/Thread/ThreadCenterPanel/ChatBody/EmptyModel/index.tsx b/web/screens/Thread/ThreadCenterPanel/ChatBody/EmptyModel/index.tsx index 1b3cb39a3..2dcfd8041 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatBody/EmptyModel/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatBody/EmptyModel/index.tsx @@ -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 ( -
- -

Welcome!

-

- You need to download your first model -

- +
+
+
+ +

Select a model to start

+
+ +
+
+
) diff --git a/web/screens/Thread/index.tsx b/web/screens/Thread/index.tsx index ca5ed44e6..f35d8e05b 100644 --- a/web/screens/Thread/index.tsx +++ b/web/screens/Thread/index.tsx @@ -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 (
- {!downloadedModels.length ? ( + {!downloadedModels.length && !isAnyRemoteModelConfigured ? ( ) : ( - <> + - + )}