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,
|
reduceTransparentAtom,
|
||||||
selectedSettingAtom,
|
selectedSettingAtom,
|
||||||
} from '@/helpers/atoms/Setting.atom'
|
} from '@/helpers/atoms/Setting.atom'
|
||||||
|
import { threadsAtom } from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
export default function RibbonPanel() {
|
export default function RibbonPanel() {
|
||||||
const [mainViewState, setMainViewState] = useAtom(mainViewStateAtom)
|
const [mainViewState, setMainViewState] = useAtom(mainViewStateAtom)
|
||||||
@ -32,6 +33,7 @@ export default function RibbonPanel() {
|
|||||||
const reduceTransparent = useAtomValue(reduceTransparentAtom)
|
const reduceTransparent = useAtomValue(reduceTransparentAtom)
|
||||||
const setSelectedSetting = useSetAtom(selectedSettingAtom)
|
const setSelectedSetting = useSetAtom(selectedSettingAtom)
|
||||||
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
const downloadedModels = useAtomValue(downloadedModelsAtom)
|
||||||
|
const threads = useAtomValue(threadsAtom)
|
||||||
|
|
||||||
const onMenuClick = (state: MainViewState) => {
|
const onMenuClick = (state: MainViewState) => {
|
||||||
if (mainViewState === state) return
|
if (mainViewState === state) return
|
||||||
@ -88,6 +90,7 @@ export default function RibbonPanel() {
|
|||||||
reduceTransparent && ' bg-[hsla(var(--ribbon-panel-bg))]',
|
reduceTransparent && ' bg-[hsla(var(--ribbon-panel-bg))]',
|
||||||
mainViewState === MainViewState.Thread &&
|
mainViewState === MainViewState.Thread &&
|
||||||
!isDownloadALocalModel &&
|
!isDownloadALocalModel &&
|
||||||
|
!threads.length &&
|
||||||
'border-none'
|
'border-none'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -7,9 +7,21 @@ const createJestConfig = nextJest({})
|
|||||||
const config = {
|
const config = {
|
||||||
coverageProvider: 'v8',
|
coverageProvider: 'v8',
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: 'jsdom',
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(ts|tsx)$': 'ts-jest',
|
||||||
|
'^.+\\.(js|jsx)$': 'babel-jest',
|
||||||
|
},
|
||||||
|
moduleNameMapper: {
|
||||||
|
// ...
|
||||||
|
'^@/(.*)$': '<rootDir>/$1',
|
||||||
|
},
|
||||||
// Add more setup options before each test is run
|
// Add more setup options before each test is run
|
||||||
// setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
// setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||||
}
|
}
|
||||||
|
|
||||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
// https://stackoverflow.com/a/72926763/5078746
|
||||||
module.exports = createJestConfig(config)
|
// 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 { InferenceEngine } from '@janhq/core'
|
||||||
import { Button, Input, Progress, ScrollArea } from '@janhq/joi'
|
import { Button, Input, Progress, ScrollArea } from '@janhq/joi'
|
||||||
|
import { useClickOutside } from '@janhq/joi'
|
||||||
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { SearchIcon, DownloadCloudIcon } from 'lucide-react'
|
import { SearchIcon, DownloadCloudIcon } from 'lucide-react'
|
||||||
@ -48,6 +49,7 @@ type Props = {
|
|||||||
|
|
||||||
const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
|
const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
|
||||||
const [searchValue, setSearchValue] = useState('')
|
const [searchValue, setSearchValue] = useState('')
|
||||||
|
const [isOpen, setIsOpen] = useState(Boolean(searchValue.length))
|
||||||
const downloadingModels = useAtomValue(getDownloadingModelAtom)
|
const downloadingModels = useAtomValue(getDownloadingModelAtom)
|
||||||
const { downloadModel } = useDownloadModel()
|
const { downloadModel } = useDownloadModel()
|
||||||
const downloadStates = useAtomValue(modelDownloadStateAtom)
|
const downloadStates = useAtomValue(modelDownloadStateAtom)
|
||||||
@ -56,8 +58,8 @@ const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
|
|||||||
const configuredModels = useAtomValue(configuredModelsAtom)
|
const configuredModels = useAtomValue(configuredModelsAtom)
|
||||||
const setMainViewState = useSetAtom(mainViewStateAtom)
|
const setMainViewState = useSetAtom(mainViewStateAtom)
|
||||||
|
|
||||||
const featuredModel = configuredModels.filter((x) =>
|
const featuredModel = configuredModels.filter(
|
||||||
x.metadata.tags.includes('Featured')
|
(x) => x.metadata.tags.includes('Featured') && x.metadata.size < 5000000000
|
||||||
)
|
)
|
||||||
|
|
||||||
const remoteModel = configuredModels.filter(
|
const remoteModel = configuredModels.filter(
|
||||||
@ -72,6 +74,7 @@ const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const remoteModelEngine = remoteModel.map((x) => x.engine)
|
const remoteModelEngine = remoteModel.map((x) => x.engine)
|
||||||
|
|
||||||
const groupByEngine = remoteModelEngine.filter(function (item, index) {
|
const groupByEngine = remoteModelEngine.filter(function (item, index) {
|
||||||
if (remoteModelEngine.indexOf(item) === index) return item
|
if (remoteModelEngine.indexOf(item) === index) return item
|
||||||
})
|
})
|
||||||
@ -88,6 +91,8 @@ const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
|
|||||||
|
|
||||||
const rows = getRows(groupByEngine, itemsPerRow)
|
const rows = getRows(groupByEngine, itemsPerRow)
|
||||||
|
|
||||||
|
const refDropdown = useClickOutside(() => setIsOpen(false))
|
||||||
|
|
||||||
const [visibleRows, setVisibleRows] = useState(1)
|
const [visibleRows, setVisibleRows] = useState(1)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -101,19 +106,22 @@ const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
|
|||||||
height={48}
|
height={48}
|
||||||
/>
|
/>
|
||||||
<h1 className="text-base font-semibold">Select a model to start</h1>
|
<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>
|
<Fragment>
|
||||||
<div className="relative">
|
<div className="relative" ref={refDropdown}>
|
||||||
<Input
|
<Input
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={(e) => setSearchValue(e.target.value)}
|
onFocus={() => setIsOpen(true)}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchValue(e.target.value)
|
||||||
|
}}
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
prefixIcon={<SearchIcon size={16} />}
|
prefixIcon={<SearchIcon size={16} />}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
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))]',
|
'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 ? (
|
{!filteredModels.length ? (
|
||||||
@ -201,7 +209,7 @@ const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
|
|||||||
>
|
>
|
||||||
<div className="w-full text-left">
|
<div className="w-full text-left">
|
||||||
<h6>{featModel.name}</h6>
|
<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}
|
{featModel.metadata.author}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -232,6 +240,7 @@ const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="flex flex-col items-end justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
theme="ghost"
|
theme="ghost"
|
||||||
className="!bg-[hsla(var(--secondary-bg))]"
|
className="!bg-[hsla(var(--secondary-bg))]"
|
||||||
@ -239,6 +248,10 @@ const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
|
|||||||
>
|
>
|
||||||
Download
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
|
<span className="font-medium text-[hsla(var(--text-secondary))]">
|
||||||
|
{toGibibytes(featModel.metadata.size)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -255,7 +268,7 @@ const OnDeviceStarterScreen = ({ extensionHasSettings }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={rowIndex}
|
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) => {
|
{row.map((remoteEngine) => {
|
||||||
const engineLogo = getLogoEngine(
|
const engineLogo = getLogoEngine(
|
||||||
@ -7,7 +7,7 @@ import ThreadLeftPanel from '@/screens/Thread/ThreadLeftPanel'
|
|||||||
import { localEngines } from '@/utils/modelEngine'
|
import { localEngines } from '@/utils/modelEngine'
|
||||||
|
|
||||||
import ThreadCenterPanel from './ThreadCenterPanel'
|
import ThreadCenterPanel from './ThreadCenterPanel'
|
||||||
import OnDeviceStarterScreen from './ThreadCenterPanel/ChatBody/EmptyModel'
|
import OnDeviceStarterScreen from './ThreadCenterPanel/ChatBody/OnDeviceStarterScreen'
|
||||||
import ModalCleanThread from './ThreadLeftPanel/ModalCleanThread'
|
import ModalCleanThread from './ThreadLeftPanel/ModalCleanThread'
|
||||||
import ModalDeleteThread from './ThreadLeftPanel/ModalDeleteThread'
|
import ModalDeleteThread from './ThreadLeftPanel/ModalDeleteThread'
|
||||||
import ModalEditTitleThread from './ThreadLeftPanel/ModalEditTitleThread'
|
import ModalEditTitleThread from './ThreadLeftPanel/ModalEditTitleThread'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user