Skip to content

Commit

Permalink
fix: issue responsive and missing size on starter screen (#3644)
Browse files Browse the repository at this point in the history
* fix: issue responisve and missing size on starter screen

* chore: fix click outside

* chore: mock function useclickoutside element

* chore: update web jest config directory

* chore: remove dir setup jest web

* chore: remove baseUrl tsconfig web

* chore: change how we shod featured model

* chore: remove min size
  • Loading branch information
urmauur authored Sep 13, 2024
1 parent ba1ba89 commit 0cce4a0
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 18 deletions.
3 changes: 3 additions & 0 deletions web/containers/Layout/RibbonPanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
reduceTransparentAtom,
selectedSettingAtom,
} from '@/helpers/atoms/Setting.atom'
import { threadsAtom } from '@/helpers/atoms/Thread.atom'

export default function RibbonPanel() {
const [mainViewState, setMainViewState] = useAtom(mainViewStateAtom)
Expand All @@ -32,6 +33,7 @@ export default function RibbonPanel() {
const reduceTransparent = useAtomValue(reduceTransparentAtom)
const setSelectedSetting = useSetAtom(selectedSettingAtom)
const downloadedModels = useAtomValue(downloadedModelsAtom)
const threads = useAtomValue(threadsAtom)

const onMenuClick = (state: MainViewState) => {
if (mainViewState === state) return
Expand Down Expand Up @@ -88,6 +90,7 @@ export default function RibbonPanel() {
reduceTransparent && ' bg-[hsla(var(--ribbon-panel-bg))]',
mainViewState === MainViewState.Thread &&
!isDownloadALocalModel &&
!threads.length &&
'border-none'
)}
>
Expand Down
16 changes: 14 additions & 2 deletions web/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,21 @@ const createJestConfig = nextJest({})
const config = {
coverageProvider: 'v8',
testEnvironment: 'jsdom',
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
'^.+\\.(js|jsx)$': 'babel-jest',
},
moduleNameMapper: {
// ...
'^@/(.*)$': '<rootDir>/$1',
},
// Add more setup options before each test is run
// setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
}

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(config)
// https://stackoverflow.com/a/72926763/5078746
// module.exports = createJestConfig(config)
module.exports = async () => ({
...(await createJestConfig(config)()),
transformIgnorePatterns: ['/node_modules/(?!(layerr)/)'],
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import { Provider } from 'jotai'
import OnDeviceStarterScreen from './index'
import * as jotai from 'jotai'
import '@testing-library/jest-dom'

jest.mock('jotai', () => ({
...jest.requireActual('jotai'),
useAtomValue: jest.fn(),
useSetAtom: jest.fn(),
}))

jest.mock('next/image', () => ({
__esModule: true,
default: (props: any) => <img {...props} />,
}))

jest.mock('@janhq/joi', () => ({
Button: (props: any) => <button {...props} />,
Input: ({ prefixIcon, ...props }: any) => (
<div>
{prefixIcon}
<input {...props} />
</div>
),
Progress: () => <div data-testid="progress" />,
ScrollArea: ({ children }: any) => <div>{children}</div>,
useClickOutside: jest.fn(),
}))

jest.mock('@/containers/Brand/Logo/Mark', () => () => (
<div data-testid="logo-mark" />
))
jest.mock('@/containers/CenterPanelContainer', () => ({ children }: any) => (
<div>{children}</div>
))
jest.mock('@/containers/Loader/ProgressCircle', () => () => (
<div data-testid="progress-circle" />
))
jest.mock('@/containers/ModelLabel', () => () => (
<div data-testid="model-label" />
))

jest.mock('@/hooks/useDownloadModel', () => ({
__esModule: true,
default: () => ({ downloadModel: jest.fn() }),
}))

// Mock the necessary atoms
const mockAtomValue = jest.spyOn(jotai, 'useAtomValue')
const mockSetAtom = jest.spyOn(jotai, 'useSetAtom')

describe('OnDeviceStarterScreen', () => {
const mockExtensionHasSettings = [
{
name: 'Test Extension',
setting: 'test-setting',
apiKey: 'test-key',
provider: 'test-provider',
},
]

beforeEach(() => {
mockAtomValue.mockImplementation(() => [])
mockSetAtom.mockImplementation(() => jest.fn())
})

it('renders the component', () => {
render(
<Provider>
<OnDeviceStarterScreen
extensionHasSettings={mockExtensionHasSettings}
/>
</Provider>
)

expect(screen.getByText('Select a model to start')).toBeInTheDocument()
expect(screen.getByTestId('logo-mark')).toBeInTheDocument()
})

it('handles search input', () => {
render(
<Provider>
<OnDeviceStarterScreen
extensionHasSettings={mockExtensionHasSettings}
/>
</Provider>
)

const searchInput = screen.getByPlaceholderText('Search...')
fireEvent.change(searchInput, { target: { value: 'test model' } })

expect(searchInput).toHaveValue('test model')
})

it('displays "No Result Found" when no models match the search', () => {
mockAtomValue.mockImplementation(() => [])

render(
<Provider>
<OnDeviceStarterScreen
extensionHasSettings={mockExtensionHasSettings}
/>
</Provider>
)

const searchInput = screen.getByPlaceholderText('Search...')
fireEvent.change(searchInput, { target: { value: 'nonexistent model' } })

expect(screen.getByText('No Result Found')).toBeInTheDocument()
})

it('renders featured models', () => {
const mockConfiguredModels = [
{
id: 'gemma-2-9b-it',
name: 'Gemma 2B',
metadata: {
tags: ['Featured'],
author: 'Test Author',
size: 3000000000,
},
},
{
id: 'llama3.1-8b-instruct',
name: 'Llama 3.1',
metadata: { tags: [], author: 'Test Author', size: 2000000000 },
},
]

mockAtomValue.mockImplementation((atom) => {
return mockConfiguredModels
})

render(
<Provider>
<OnDeviceStarterScreen
extensionHasSettings={mockExtensionHasSettings}
/>
</Provider>
)

expect(screen.getByText('Gemma 2B')).toBeInTheDocument()
expect(screen.queryByText('Llama 3.1')).not.toBeInTheDocument()
})

it('renders cloud models', () => {
const mockRemoteModels = [
{ id: 'remote-model-1', name: 'Remote Model 1', engine: 'openai' },
{ id: 'remote-model-2', name: 'Remote Model 2', engine: 'anthropic' },
]

mockAtomValue.mockImplementation((atom) => {
if (atom === jotai.atom([])) {
return mockRemoteModels
}
return []
})

render(
<Provider>
<OnDeviceStarterScreen
extensionHasSettings={mockExtensionHasSettings}
/>
</Provider>
)

expect(screen.getByText('Cloud Models')).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Image from 'next/image'

import { InferenceEngine } from '@janhq/core'
import { Button, Input, Progress, ScrollArea } from '@janhq/joi'
import { useClickOutside } from '@janhq/joi'

import { useAtomValue, useSetAtom } from 'jotai'
import { SearchIcon, DownloadCloudIcon } from 'lucide-react'
Expand Down Expand Up @@ -48,6 +49,7 @@ type Props = {

const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
const [searchValue, setSearchValue] = useState('')
const [isOpen, setIsOpen] = useState(Boolean(searchValue.length))
const downloadingModels = useAtomValue(getDownloadingModelAtom)
const { downloadModel } = useDownloadModel()
const downloadStates = useAtomValue(modelDownloadStateAtom)
Expand All @@ -56,8 +58,8 @@ const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
const configuredModels = useAtomValue(configuredModelsAtom)
const setMainViewState = useSetAtom(mainViewStateAtom)

const featuredModel = configuredModels.filter((x) =>
x.metadata.tags.includes('Featured')
const featuredModel = configuredModels.filter(
(x) => x.metadata.tags.includes('Featured') && x.metadata.size < 5000000000
)

const remoteModel = configuredModels.filter(
Expand All @@ -72,6 +74,7 @@ const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
})

const remoteModelEngine = remoteModel.map((x) => x.engine)

const groupByEngine = remoteModelEngine.filter(function (item, index) {
if (remoteModelEngine.indexOf(item) === index) return item
})
Expand All @@ -88,6 +91,8 @@ const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {

const rows = getRows(groupByEngine, itemsPerRow)

const refDropdown = useClickOutside(() => setIsOpen(false))

const [visibleRows, setVisibleRows] = useState(1)

return (
Expand All @@ -101,19 +106,22 @@ const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
height={48}
/>
<h1 className="text-base font-semibold">Select a model to start</h1>
<div className="mt-6 w-full lg:w-1/2">
<div className="mt-6 w-[320px] md:w-[400px]">
<Fragment>
<div className="relative">
<div className="relative" ref={refDropdown}>
<Input
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onFocus={() => setIsOpen(true)}
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'
!isOpen ? 'invisible' : 'visible'
)}
>
{!filteredModels.length ? (
Expand Down Expand Up @@ -201,7 +209,7 @@ const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
>
<div className="w-full text-left">
<h6>{featModel.name}</h6>
<p className="mt-1 text-[hsla(var(--text-secondary))]">
<p className="mt-4 text-[hsla(var(--text-secondary))]">
{featModel.metadata.author}
</p>
</div>
Expand Down Expand Up @@ -232,13 +240,18 @@ const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
))}
</div>
) : (
<Button
theme="ghost"
className="!bg-[hsla(var(--secondary-bg))]"
onClick={() => downloadModel(featModel)}
>
Download
</Button>
<div className="flex flex-col items-end justify-end gap-2">
<Button
theme="ghost"
className="!bg-[hsla(var(--secondary-bg))]"
onClick={() => downloadModel(featModel)}
>
Download
</Button>
<span className="font-medium text-[hsla(var(--text-secondary))]">
{toGibibytes(featModel.metadata.size)}
</span>
</div>
)}
</div>
)
Expand All @@ -255,7 +268,7 @@ const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
return (
<div
key={rowIndex}
className="my-2 flex items-center justify-normal gap-10"
className="my-2 flex items-center justify-center gap-4 md:gap-10"
>
{row.map((remoteEngine) => {
const engineLogo = getLogoEngine(
Expand Down
2 changes: 1 addition & 1 deletion web/screens/Thread/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import ThreadLeftPanel from '@/screens/Thread/ThreadLeftPanel'
import { localEngines } from '@/utils/modelEngine'

import ThreadCenterPanel from './ThreadCenterPanel'
import OnDeviceStarterScreen from './ThreadCenterPanel/ChatBody/EmptyModel'
import OnDeviceStarterScreen from './ThreadCenterPanel/ChatBody/OnDeviceStarterScreen'
import ModalCleanThread from './ThreadLeftPanel/ModalCleanThread'
import ModalDeleteThread from './ThreadLeftPanel/ModalDeleteThread'
import ModalEditTitleThread from './ThreadLeftPanel/ModalEditTitleThread'
Expand Down

0 comments on commit 0cce4a0

Please sign in to comment.