Skip to content

Commit

Permalink
feat: new starter screen (#3217)
Browse files Browse the repository at this point in the history
* feat: starter screen

* chore: update flow starter screen

* fix CI

Signed-off-by: James <[email protected]>

* chore: update variable name

* chore: fix CI

* update download cortex binary

Signed-off-by: James <[email protected]>

---------

Signed-off-by: James <[email protected]>
Co-authored-by: James <[email protected]>
  • Loading branch information
urmauur and namchuai authored Jul 31, 2024
1 parent 5369da7 commit e8ee694
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 33 deletions.
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion electron/download.bat
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions web/helpers/atoms/SetupRemoteModel.atom.ts
Original file line number Diff line number Diff line change
@@ -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<
Expand Down
8 changes: 6 additions & 2 deletions web/screens/HubScreen2/components/SetUpApiKeyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ 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'
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(
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
37 changes: 15 additions & 22 deletions web/screens/Thread/ThreadCenterPanel/ChatBody/EmptyModel/index.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
Expand Down
33 changes: 29 additions & 4 deletions web/screens/Thread/index.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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>
Expand Down

0 comments on commit e8ee694

Please sign in to comment.