fix: issue responsive and missing size on starter screen (#3644)
* 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
This commit is contained in:
parent
ba1ba89fa3
commit
0cce4a0c83
@ -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)
|
||||
@ -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
|
||||
@ -88,6 +90,7 @@ export default function RibbonPanel() {
|
||||
reduceTransparent && ' bg-[hsla(var(--ribbon-panel-bg))]',
|
||||
mainViewState === MainViewState.Thread &&
|
||||
!isDownloadALocalModel &&
|
||||
!threads.length &&
|
||||
'border-none'
|
||||
)}
|
||||
>
|
||||
|
||||
@ -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)/)'],
|
||||
})
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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'
|
||||
@ -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)
|
||||
@ -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(
|
||||
@ -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
|
||||
})
|
||||
@ -88,6 +91,8 @@ const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
|
||||
|
||||
const rows = getRows(groupByEngine, itemsPerRow)
|
||||
|
||||
const refDropdown = useClickOutside(() => setIsOpen(false))
|
||||
|
||||
const [visibleRows, setVisibleRows] = useState(1)
|
||||
|
||||
return (
|
||||
@ -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 ? (
|
||||
@ -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>
|
||||
@ -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>
|
||||
)
|
||||
@ -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(
|
||||
@ -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'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user