chore: improve models and threads caching (#3744)
* chore: managing and maintaining models and threads in the cache * test: add tests for hooks
This commit is contained in:
parent
87a8bc7359
commit
87e1754e3a
@ -16,14 +16,12 @@ import { mainViewStateAtom, showLeftPanelAtom } from '@/helpers/atoms/App.atom'
|
|||||||
import { editMessageAtom } from '@/helpers/atoms/ChatMessage.atom'
|
import { editMessageAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
|
import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom'
|
||||||
|
|
||||||
|
import { isDownloadALocalModelAtom } from '@/helpers/atoms/Model.atom'
|
||||||
import {
|
import {
|
||||||
reduceTransparentAtom,
|
reduceTransparentAtom,
|
||||||
selectedSettingAtom,
|
selectedSettingAtom,
|
||||||
} from '@/helpers/atoms/Setting.atom'
|
} from '@/helpers/atoms/Setting.atom'
|
||||||
import {
|
import { threadsAtom } from '@/helpers/atoms/Thread.atom'
|
||||||
isDownloadALocalModelAtom,
|
|
||||||
threadsAtom,
|
|
||||||
} from '@/helpers/atoms/Thread.atom'
|
|
||||||
|
|
||||||
export default function RibbonPanel() {
|
export default function RibbonPanel() {
|
||||||
const [mainViewState, setMainViewState] = useAtom(mainViewStateAtom)
|
const [mainViewState, setMainViewState] = useAtom(mainViewStateAtom)
|
||||||
|
|||||||
@ -513,7 +513,7 @@ const ModelDropdown = ({
|
|||||||
const isDownloading = downloadingModels.some(
|
const isDownloading = downloadingModels.some(
|
||||||
(md) => md.id === model.id
|
(md) => md.id === model.id
|
||||||
)
|
)
|
||||||
const isdDownloaded = downloadedModels.some(
|
const isDownloaded = downloadedModels.some(
|
||||||
(c) => c.id === model.id
|
(c) => c.id === model.id
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
@ -528,7 +528,7 @@ const ModelDropdown = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!apiKey && !isLocalEngine(model.engine))
|
if (!apiKey && !isLocalEngine(model.engine))
|
||||||
return null
|
return null
|
||||||
if (isdDownloaded) {
|
if (isDownloaded) {
|
||||||
onClickModelItem(model.id)
|
onClickModelItem(model.id)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -537,7 +537,7 @@ const ModelDropdown = ({
|
|||||||
<p
|
<p
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'line-clamp-1',
|
'line-clamp-1',
|
||||||
!isdDownloaded &&
|
!isDownloaded &&
|
||||||
'text-[hsla(var(--text-secondary))]'
|
'text-[hsla(var(--text-secondary))]'
|
||||||
)}
|
)}
|
||||||
title={model.name}
|
title={model.name}
|
||||||
@ -547,12 +547,12 @@ const ModelDropdown = ({
|
|||||||
<ModelLabel metadata={model.metadata} compact />
|
<ModelLabel metadata={model.metadata} compact />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-[hsla(var(--text-tertiary))]">
|
<div className="flex items-center gap-2 text-[hsla(var(--text-tertiary))]">
|
||||||
{!isdDownloaded && (
|
{!isDownloaded && (
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{toGibibytes(model.metadata.size)}
|
{toGibibytes(model.metadata.size)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!isDownloading && !isdDownloaded ? (
|
{!isDownloading && !isDownloaded ? (
|
||||||
<DownloadCloudIcon
|
<DownloadCloudIcon
|
||||||
size={18}
|
size={18}
|
||||||
className="cursor-pointer text-[hsla(var(--app-link))]"
|
className="cursor-pointer text-[hsla(var(--app-link))]"
|
||||||
|
|||||||
78
web/helpers/atoms/Extension.atom.test.ts
Normal file
78
web/helpers/atoms/Extension.atom.test.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
// Extension.atom.test.ts
|
||||||
|
|
||||||
|
import { act, renderHook } from '@testing-library/react'
|
||||||
|
import * as ExtensionAtoms from './Extension.atom'
|
||||||
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
describe('Extension.atom.ts', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('installingExtensionAtom', () => {
|
||||||
|
it('should initialize as an empty array', () => {
|
||||||
|
const { result } = renderHook(() => useAtomValue(ExtensionAtoms.installingExtensionAtom))
|
||||||
|
expect(result.current).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('setInstallingExtensionAtom', () => {
|
||||||
|
it('should add a new installing extension', () => {
|
||||||
|
const { result: setAtom } = renderHook(() => useSetAtom(ExtensionAtoms.setInstallingExtensionAtom))
|
||||||
|
const { result: getAtom } = renderHook(() => useAtomValue(ExtensionAtoms.installingExtensionAtom))
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
setAtom.current('ext1', { extensionId: 'ext1', percentage: 0 })
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getAtom.current).toEqual([{ extensionId: 'ext1', percentage: 0 }])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update an existing installing extension', () => {
|
||||||
|
const { result: setAtom } = renderHook(() => useSetAtom(ExtensionAtoms.setInstallingExtensionAtom))
|
||||||
|
const { result: getAtom } = renderHook(() => useAtomValue(ExtensionAtoms.installingExtensionAtom))
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
setAtom.current('ext1', { extensionId: 'ext1', percentage: 0 })
|
||||||
|
setAtom.current('ext1', { extensionId: 'ext1', percentage: 50 })
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getAtom.current).toEqual([{ extensionId: 'ext1', percentage: 50 }])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('removeInstallingExtensionAtom', () => {
|
||||||
|
it('should remove an installing extension', () => {
|
||||||
|
const { result: setAtom } = renderHook(() => useSetAtom(ExtensionAtoms.setInstallingExtensionAtom))
|
||||||
|
const { result: removeAtom } = renderHook(() => useSetAtom(ExtensionAtoms.removeInstallingExtensionAtom))
|
||||||
|
const { result: getAtom } = renderHook(() => useAtomValue(ExtensionAtoms.installingExtensionAtom))
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
setAtom.current('ext1', { extensionId: 'ext1', percentage: 0 })
|
||||||
|
setAtom.current('ext2', { extensionId: 'ext2', percentage: 50 })
|
||||||
|
removeAtom.current('ext1')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getAtom.current).toEqual([{ extensionId: 'ext2', percentage: 50 }])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('inActiveEngineProviderAtom', () => {
|
||||||
|
it('should initialize as an empty array', () => {
|
||||||
|
const { result } = renderHook(() => useAtomValue(ExtensionAtoms.inActiveEngineProviderAtom))
|
||||||
|
expect(result.current).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should persist value in storage', () => {
|
||||||
|
const { result } = renderHook(() => useAtom(ExtensionAtoms.inActiveEngineProviderAtom))
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current[1](['provider1', 'provider2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Simulate a re-render to check if the value persists
|
||||||
|
const { result: newResult } = renderHook(() => useAtomValue(ExtensionAtoms.inActiveEngineProviderAtom))
|
||||||
|
expect(newResult.current).toEqual(['provider1', 'provider2'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
import { act, renderHook } from '@testing-library/react'
|
||||||
import * as ModelAtoms from './Model.atom'
|
import * as ModelAtoms from './Model.atom'
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
@ -24,11 +24,6 @@ describe('Model.atom.ts', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('activeAssistantModelAtom', () => {
|
|
||||||
it('should initialize as undefined', () => {
|
|
||||||
expect(ModelAtoms.activeAssistantModelAtom.init).toBeUndefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('selectedModelAtom', () => {
|
describe('selectedModelAtom', () => {
|
||||||
it('should initialize as undefined', () => {
|
it('should initialize as undefined', () => {
|
||||||
|
|||||||
@ -1,8 +1,59 @@
|
|||||||
import { ImportingModel, InferenceEngine, Model, ModelFile } from '@janhq/core'
|
import { ImportingModel, InferenceEngine, Model, ModelFile } from '@janhq/core'
|
||||||
import { atom } from 'jotai'
|
import { atom } from 'jotai'
|
||||||
|
import { atomWithStorage } from 'jotai/utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum for the keys used to store models in the local storage.
|
||||||
|
*/
|
||||||
|
enum ModelStorageAtomKeys {
|
||||||
|
DownloadedModels = 'downloadedModels',
|
||||||
|
AvailableModels = 'availableModels',
|
||||||
|
}
|
||||||
|
//// Models Atom
|
||||||
|
/**
|
||||||
|
* Downloaded Models Atom
|
||||||
|
* This atom stores the list of models that have been downloaded.
|
||||||
|
*/
|
||||||
|
export const downloadedModelsAtom = atomWithStorage<ModelFile[]>(
|
||||||
|
ModelStorageAtomKeys.DownloadedModels,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configured Models Atom
|
||||||
|
* This atom stores the list of models that have been configured and available to download
|
||||||
|
*/
|
||||||
|
export const configuredModelsAtom = atomWithStorage<ModelFile[]>(
|
||||||
|
ModelStorageAtomKeys.AvailableModels,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
export const removeDownloadedModelAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, modelId: string) => {
|
||||||
|
const downloadedModels = get(downloadedModelsAtom)
|
||||||
|
|
||||||
|
set(
|
||||||
|
downloadedModelsAtom,
|
||||||
|
downloadedModels.filter((e) => e.id !== modelId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atom to store the selected model (from ModelDropdown)
|
||||||
|
*/
|
||||||
|
export const selectedModelAtom = atom<ModelFile | undefined>(undefined)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atom to store the expanded engine sections (from ModelDropdown)
|
||||||
|
*/
|
||||||
|
export const showEngineListModelAtom = atom<string[]>([InferenceEngine.nitro])
|
||||||
|
|
||||||
|
/// End Models Atom
|
||||||
|
/// Model Download Atom
|
||||||
|
|
||||||
export const stateModel = atom({ state: 'start', loading: false, model: '' })
|
export const stateModel = atom({ state: 'start', loading: false, model: '' })
|
||||||
export const activeAssistantModelAtom = atom<Model | undefined>(undefined)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the list of models which are being downloaded.
|
* Stores the list of models which are being downloaded.
|
||||||
@ -30,28 +81,20 @@ export const removeDownloadingModelAtom = atom(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const downloadedModelsAtom = atom<ModelFile[]>([])
|
/// End Model Download Atom
|
||||||
|
/// Model Import Atom
|
||||||
export const removeDownloadedModelAtom = atom(
|
|
||||||
null,
|
|
||||||
(get, set, modelId: string) => {
|
|
||||||
const downloadedModels = get(downloadedModelsAtom)
|
|
||||||
|
|
||||||
set(
|
|
||||||
downloadedModelsAtom,
|
|
||||||
downloadedModels.filter((e) => e.id !== modelId)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const configuredModelsAtom = atom<ModelFile[]>([])
|
|
||||||
|
|
||||||
export const defaultModelAtom = atom<Model | undefined>(undefined)
|
|
||||||
|
|
||||||
/// TODO: move this part to another atom
|
/// TODO: move this part to another atom
|
||||||
// store the paths of the models that are being imported
|
// store the paths of the models that are being imported
|
||||||
export const importingModelsAtom = atom<ImportingModel[]>([])
|
export const importingModelsAtom = atom<ImportingModel[]>([])
|
||||||
|
|
||||||
|
// DEPRECATED: Remove when moving to cortex.cpp
|
||||||
|
// Default model template when importing
|
||||||
|
export const defaultModelAtom = atom<Model | undefined>(undefined)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importing progress Atom
|
||||||
|
*/
|
||||||
export const updateImportingModelProgressAtom = atom(
|
export const updateImportingModelProgressAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, importId: string, percentage: number) => {
|
(get, set, importId: string, percentage: number) => {
|
||||||
@ -69,6 +112,9 @@ export const updateImportingModelProgressAtom = atom(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importing error Atom
|
||||||
|
*/
|
||||||
export const setImportingModelErrorAtom = atom(
|
export const setImportingModelErrorAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, importId: string, error: string) => {
|
(get, set, importId: string, error: string) => {
|
||||||
@ -87,6 +133,9 @@ export const setImportingModelErrorAtom = atom(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importing success Atom
|
||||||
|
*/
|
||||||
export const setImportingModelSuccessAtom = atom(
|
export const setImportingModelSuccessAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, importId: string, modelId: string) => {
|
(get, set, importId: string, modelId: string) => {
|
||||||
@ -105,6 +154,9 @@ export const setImportingModelSuccessAtom = atom(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update importing model metadata Atom
|
||||||
|
*/
|
||||||
export const updateImportingModelAtom = atom(
|
export const updateImportingModelAtom = atom(
|
||||||
null,
|
null,
|
||||||
(
|
(
|
||||||
@ -131,6 +183,9 @@ export const updateImportingModelAtom = atom(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const selectedModelAtom = atom<ModelFile | undefined>(undefined)
|
/// End Model Import Atom
|
||||||
|
|
||||||
export const showEngineListModelAtom = atom<string[]>([InferenceEngine.nitro])
|
/// ModelDropdown States Atom
|
||||||
|
export const isDownloadALocalModelAtom = atom<boolean>(false)
|
||||||
|
export const isAnyRemoteModelConfiguredAtom = atom<boolean>(false)
|
||||||
|
/// End ModelDropdown States Atom
|
||||||
|
|||||||
146
web/helpers/atoms/SystemBar.atom.test.ts
Normal file
146
web/helpers/atoms/SystemBar.atom.test.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import { useAtom } from 'jotai'
|
||||||
|
import * as SystemBarAtoms from './SystemBar.atom'
|
||||||
|
|
||||||
|
describe('SystemBar.atom.ts', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('totalRamAtom', () => {
|
||||||
|
it('should initialize as 0', () => {
|
||||||
|
const { result } = renderHook(() => useAtom(SystemBarAtoms.totalRamAtom))
|
||||||
|
expect(result.current[0]).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update correctly', () => {
|
||||||
|
const { result } = renderHook(() => useAtom(SystemBarAtoms.totalRamAtom))
|
||||||
|
act(() => {
|
||||||
|
result.current[1](16384)
|
||||||
|
})
|
||||||
|
expect(result.current[0]).toBe(16384)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('usedRamAtom', () => {
|
||||||
|
it('should initialize as 0', () => {
|
||||||
|
const { result } = renderHook(() => useAtom(SystemBarAtoms.usedRamAtom))
|
||||||
|
expect(result.current[0]).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update correctly', () => {
|
||||||
|
const { result } = renderHook(() => useAtom(SystemBarAtoms.usedRamAtom))
|
||||||
|
act(() => {
|
||||||
|
result.current[1](8192)
|
||||||
|
})
|
||||||
|
expect(result.current[0]).toBe(8192)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('cpuUsageAtom', () => {
|
||||||
|
it('should initialize as 0', () => {
|
||||||
|
const { result } = renderHook(() => useAtom(SystemBarAtoms.cpuUsageAtom))
|
||||||
|
expect(result.current[0]).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update correctly', () => {
|
||||||
|
const { result } = renderHook(() => useAtom(SystemBarAtoms.cpuUsageAtom))
|
||||||
|
act(() => {
|
||||||
|
result.current[1](50)
|
||||||
|
})
|
||||||
|
expect(result.current[0]).toBe(50)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ramUtilitizedAtom', () => {
|
||||||
|
it('should initialize as 0', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useAtom(SystemBarAtoms.ramUtilitizedAtom)
|
||||||
|
)
|
||||||
|
expect(result.current[0]).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update correctly', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useAtom(SystemBarAtoms.ramUtilitizedAtom)
|
||||||
|
)
|
||||||
|
act(() => {
|
||||||
|
result.current[1](75)
|
||||||
|
})
|
||||||
|
expect(result.current[0]).toBe(75)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('gpusAtom', () => {
|
||||||
|
it('should initialize as an empty array', () => {
|
||||||
|
const { result } = renderHook(() => useAtom(SystemBarAtoms.gpusAtom))
|
||||||
|
expect(result.current[0]).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update correctly', () => {
|
||||||
|
const { result } = renderHook(() => useAtom(SystemBarAtoms.gpusAtom))
|
||||||
|
const gpus = [{ id: 'gpu1' }, { id: 'gpu2' }]
|
||||||
|
act(() => {
|
||||||
|
result.current[1](gpus as any)
|
||||||
|
})
|
||||||
|
expect(result.current[0]).toEqual(gpus)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('nvidiaTotalVramAtom', () => {
|
||||||
|
it('should initialize as 0', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useAtom(SystemBarAtoms.nvidiaTotalVramAtom)
|
||||||
|
)
|
||||||
|
expect(result.current[0]).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update correctly', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useAtom(SystemBarAtoms.nvidiaTotalVramAtom)
|
||||||
|
)
|
||||||
|
act(() => {
|
||||||
|
result.current[1](8192)
|
||||||
|
})
|
||||||
|
expect(result.current[0]).toBe(8192)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('availableVramAtom', () => {
|
||||||
|
it('should initialize as 0', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useAtom(SystemBarAtoms.availableVramAtom)
|
||||||
|
)
|
||||||
|
expect(result.current[0]).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update correctly', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useAtom(SystemBarAtoms.availableVramAtom)
|
||||||
|
)
|
||||||
|
act(() => {
|
||||||
|
result.current[1](4096)
|
||||||
|
})
|
||||||
|
expect(result.current[0]).toBe(4096)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('systemMonitorCollapseAtom', () => {
|
||||||
|
it('should initialize as false', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useAtom(SystemBarAtoms.systemMonitorCollapseAtom)
|
||||||
|
)
|
||||||
|
expect(result.current[0]).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update correctly', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useAtom(SystemBarAtoms.systemMonitorCollapseAtom)
|
||||||
|
)
|
||||||
|
act(() => {
|
||||||
|
result.current[1](true)
|
||||||
|
})
|
||||||
|
expect(result.current[0]).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
187
web/helpers/atoms/Thread.atom.test.ts
Normal file
187
web/helpers/atoms/Thread.atom.test.ts
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
// Thread.atom.test.ts
|
||||||
|
|
||||||
|
import { act, renderHook } from '@testing-library/react'
|
||||||
|
import * as ThreadAtoms from './Thread.atom'
|
||||||
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
describe('Thread.atom.ts', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('threadStatesAtom', () => {
|
||||||
|
it('should initialize as an empty object', () => {
|
||||||
|
const { result: threadStatesAtom } = renderHook(() =>
|
||||||
|
useAtom(ThreadAtoms.threadsAtom)
|
||||||
|
)
|
||||||
|
expect(threadStatesAtom.current[0]).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('threadsAtom', () => {
|
||||||
|
it('should initialize as an empty array', () => {
|
||||||
|
const { result: threadsAtom } = renderHook(() =>
|
||||||
|
useAtom(ThreadAtoms.threadsAtom)
|
||||||
|
)
|
||||||
|
expect(threadsAtom.current[0]).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('threadDataReadyAtom', () => {
|
||||||
|
it('should initialize as false', () => {
|
||||||
|
const { result: threadDataReadyAtom } = renderHook(() =>
|
||||||
|
useAtom(ThreadAtoms.threadsAtom)
|
||||||
|
)
|
||||||
|
expect(threadDataReadyAtom.current[0]).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('activeThreadIdAtom', () => {
|
||||||
|
it('should set and get active thread id', () => {
|
||||||
|
const { result: getAtom } = renderHook(() =>
|
||||||
|
useAtomValue(ThreadAtoms.getActiveThreadIdAtom)
|
||||||
|
)
|
||||||
|
const { result: setAtom } = renderHook(() =>
|
||||||
|
useSetAtom(ThreadAtoms.setActiveThreadIdAtom)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(getAtom.current).toBeUndefined()
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
setAtom.current('thread-1')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(getAtom.current).toBe('thread-1')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('activeThreadAtom', () => {
|
||||||
|
it('should return the active thread', () => {
|
||||||
|
const { result: threadsAtom } = renderHook(() =>
|
||||||
|
useAtom(ThreadAtoms.threadsAtom)
|
||||||
|
)
|
||||||
|
const { result: setActiveThreadId } = renderHook(() =>
|
||||||
|
useSetAtom(ThreadAtoms.setActiveThreadIdAtom)
|
||||||
|
)
|
||||||
|
const { result: activeThread } = renderHook(() =>
|
||||||
|
useAtomValue(ThreadAtoms.activeThreadAtom)
|
||||||
|
)
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
threadsAtom.current[1]([
|
||||||
|
{ id: 'thread-1', title: 'Test Thread' },
|
||||||
|
] as any)
|
||||||
|
setActiveThreadId.current('thread-1')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(activeThread.current).toEqual({
|
||||||
|
id: 'thread-1',
|
||||||
|
title: 'Test Thread',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('updateThreadAtom', () => {
|
||||||
|
it('should update an existing thread', () => {
|
||||||
|
const { result: threadsAtom } = renderHook(() =>
|
||||||
|
useAtom(ThreadAtoms.threadsAtom)
|
||||||
|
)
|
||||||
|
const { result: updateThread } = renderHook(() =>
|
||||||
|
useSetAtom(ThreadAtoms.updateThreadAtom)
|
||||||
|
)
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
threadsAtom.current[1]([
|
||||||
|
{
|
||||||
|
id: 'thread-1',
|
||||||
|
title: 'Old Title',
|
||||||
|
updated: new Date('2023-01-01').toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'thread-2',
|
||||||
|
title: 'Thread 2',
|
||||||
|
updated: new Date('2023-01-02').toISOString(),
|
||||||
|
},
|
||||||
|
] as any)
|
||||||
|
})
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
updateThread.current({
|
||||||
|
id: 'thread-1',
|
||||||
|
title: 'New Title',
|
||||||
|
updated: new Date('2023-01-03').toISOString(),
|
||||||
|
} as any)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(threadsAtom.current[0]).toEqual([
|
||||||
|
{
|
||||||
|
id: 'thread-1',
|
||||||
|
title: 'New Title',
|
||||||
|
updated: new Date('2023-01-03').toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'thread-2',
|
||||||
|
title: 'Thread 2',
|
||||||
|
updated: new Date('2023-01-02').toISOString(),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('setThreadModelParamsAtom', () => {
|
||||||
|
it('should set thread model params', () => {
|
||||||
|
const { result: paramsAtom } = renderHook(() =>
|
||||||
|
useAtom(ThreadAtoms.threadModelParamsAtom)
|
||||||
|
)
|
||||||
|
const { result: setParams } = renderHook(() =>
|
||||||
|
useSetAtom(ThreadAtoms.setThreadModelParamsAtom)
|
||||||
|
)
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
setParams.current('thread-1', { modelName: 'gpt-3' } as any)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(paramsAtom.current[0]).toEqual({
|
||||||
|
'thread-1': { modelName: 'gpt-3' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('deleteThreadStateAtom', () => {
|
||||||
|
it('should delete a thread state', () => {
|
||||||
|
const { result: statesAtom } = renderHook(() =>
|
||||||
|
useAtom(ThreadAtoms.threadStatesAtom)
|
||||||
|
)
|
||||||
|
const { result: deleteState } = renderHook(() =>
|
||||||
|
useSetAtom(ThreadAtoms.deleteThreadStateAtom)
|
||||||
|
)
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
statesAtom.current[1]({
|
||||||
|
'thread-1': { lastMessage: 'Hello' },
|
||||||
|
'thread-2': { lastMessage: 'Hi' },
|
||||||
|
} as any)
|
||||||
|
})
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
deleteState.current('thread-1')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(statesAtom.current[0]).toEqual({
|
||||||
|
'thread-2': { lastMessage: 'Hi' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('modalActionThreadAtom', () => {
|
||||||
|
it('should initialize with undefined values', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useAtomValue(ThreadAtoms.modalActionThreadAtom)
|
||||||
|
)
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
showModal: undefined,
|
||||||
|
thread: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,45 +1,91 @@
|
|||||||
import {
|
import { Thread, ThreadContent, ThreadState } from '@janhq/core'
|
||||||
ModelRuntimeParams,
|
|
||||||
ModelSettingParams,
|
|
||||||
Thread,
|
|
||||||
ThreadContent,
|
|
||||||
ThreadState,
|
|
||||||
} from '@janhq/core'
|
|
||||||
|
|
||||||
import { atom } from 'jotai'
|
import { atom } from 'jotai'
|
||||||
import { atomWithStorage } from 'jotai/utils'
|
import { atomWithStorage } from 'jotai/utils'
|
||||||
|
|
||||||
|
import { ModelParams } from '@/types/model'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thread Modal Action Enum
|
||||||
|
*/
|
||||||
export enum ThreadModalAction {
|
export enum ThreadModalAction {
|
||||||
Clean = 'clean',
|
Clean = 'clean',
|
||||||
Delete = 'delete',
|
Delete = 'delete',
|
||||||
EditTitle = 'edit-title',
|
EditTitle = 'edit-title',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const engineParamsUpdateAtom = atom<boolean>(false)
|
const ACTIVE_SETTING_INPUT_BOX = 'activeSettingInputBox'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum for the keys used to store models in the local storage.
|
||||||
|
*/
|
||||||
|
enum ThreadStorageAtomKeys {
|
||||||
|
ThreadStates = 'threadStates',
|
||||||
|
ThreadList = 'threadList',
|
||||||
|
ThreadListReady = 'threadListReady',
|
||||||
|
}
|
||||||
|
|
||||||
|
//// Threads Atom
|
||||||
|
/**
|
||||||
|
* Stores all thread states for the current user
|
||||||
|
*/
|
||||||
|
export const threadStatesAtom = atomWithStorage<Record<string, ThreadState>>(
|
||||||
|
ThreadStorageAtomKeys.ThreadStates,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores all threads for the current user
|
||||||
|
*/
|
||||||
|
export const threadsAtom = atomWithStorage<Thread[]>(
|
||||||
|
ThreadStorageAtomKeys.ThreadList,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether thread data is ready or not
|
||||||
|
* */
|
||||||
|
export const threadDataReadyAtom = atomWithStorage<boolean>(
|
||||||
|
ThreadStorageAtomKeys.ThreadListReady,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store model params at thread level settings
|
||||||
|
*/
|
||||||
|
export const threadModelParamsAtom = atom<Record<string, ModelParams>>({})
|
||||||
|
|
||||||
|
//// End Thread Atom
|
||||||
|
|
||||||
|
/// Active Thread Atom
|
||||||
/**
|
/**
|
||||||
* Stores the current active thread id.
|
* Stores the current active thread id.
|
||||||
*/
|
*/
|
||||||
const activeThreadIdAtom = atom<string | undefined>(undefined)
|
const activeThreadIdAtom = atom<string | undefined>(undefined)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the active thread id
|
||||||
|
*/
|
||||||
export const getActiveThreadIdAtom = atom((get) => get(activeThreadIdAtom))
|
export const getActiveThreadIdAtom = atom((get) => get(activeThreadIdAtom))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the active thread id
|
||||||
|
*/
|
||||||
export const setActiveThreadIdAtom = atom(
|
export const setActiveThreadIdAtom = atom(
|
||||||
null,
|
null,
|
||||||
(_get, set, threadId: string | undefined) => set(activeThreadIdAtom, threadId)
|
(_get, set, threadId: string | undefined) => set(activeThreadIdAtom, threadId)
|
||||||
)
|
)
|
||||||
|
|
||||||
export const waitingToSendMessage = atom<boolean | undefined>(undefined)
|
|
||||||
|
|
||||||
export const isGeneratingResponseAtom = atom<boolean | undefined>(undefined)
|
|
||||||
/**
|
/**
|
||||||
* Stores all thread states for the current user
|
* Get the current active thread metadata
|
||||||
*/
|
*/
|
||||||
export const threadStatesAtom = atom<Record<string, ThreadState>>({})
|
export const activeThreadAtom = atom<Thread | undefined>((get) =>
|
||||||
|
get(threadsAtom).find((c) => c.id === get(getActiveThreadIdAtom))
|
||||||
// Whether thread data is ready or not
|
)
|
||||||
export const threadDataReadyAtom = atom<boolean>(false)
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the active thread state
|
||||||
|
*/
|
||||||
export const activeThreadStateAtom = atom<ThreadState | undefined>((get) => {
|
export const activeThreadStateAtom = atom<ThreadState | undefined>((get) => {
|
||||||
const threadId = get(activeThreadIdAtom)
|
const threadId = get(activeThreadIdAtom)
|
||||||
if (!threadId) {
|
if (!threadId) {
|
||||||
@ -50,6 +96,38 @@ export const activeThreadStateAtom = atom<ThreadState | undefined>((get) => {
|
|||||||
return get(threadStatesAtom)[threadId]
|
return get(threadStatesAtom)[threadId]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the active thread model params
|
||||||
|
*/
|
||||||
|
export const getActiveThreadModelParamsAtom = atom<ModelParams | undefined>(
|
||||||
|
(get) => {
|
||||||
|
const threadId = get(activeThreadIdAtom)
|
||||||
|
if (!threadId) {
|
||||||
|
console.debug('Active thread id is undefined')
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return get(threadModelParamsAtom)[threadId]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
/// End Active Thread Atom
|
||||||
|
|
||||||
|
/// Threads State Atom
|
||||||
|
export const engineParamsUpdateAtom = atom<boolean>(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the thread is waiting to send a message
|
||||||
|
*/
|
||||||
|
export const waitingToSendMessage = atom<boolean | undefined>(undefined)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the thread is generating a response
|
||||||
|
*/
|
||||||
|
export const isGeneratingResponseAtom = atom<boolean | undefined>(undefined)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a thread state from the atom
|
||||||
|
*/
|
||||||
export const deleteThreadStateAtom = atom(
|
export const deleteThreadStateAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, threadId: string) => {
|
(get, set, threadId: string) => {
|
||||||
@ -59,6 +137,9 @@ export const deleteThreadStateAtom = atom(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the thread state with the new state
|
||||||
|
*/
|
||||||
export const updateThreadWaitingForResponseAtom = atom(
|
export const updateThreadWaitingForResponseAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, threadId: string, waitingForResponse: boolean) => {
|
(get, set, threadId: string, waitingForResponse: boolean) => {
|
||||||
@ -71,6 +152,10 @@ export const updateThreadWaitingForResponseAtom = atom(
|
|||||||
set(threadStatesAtom, currentState)
|
set(threadStatesAtom, currentState)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the thread last message
|
||||||
|
*/
|
||||||
export const updateThreadStateLastMessageAtom = atom(
|
export const updateThreadStateLastMessageAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, threadId: string, lastContent?: ThreadContent[]) => {
|
(get, set, threadId: string, lastContent?: ThreadContent[]) => {
|
||||||
@ -84,6 +169,9 @@ export const updateThreadStateLastMessageAtom = atom(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a thread with the new thread metadata
|
||||||
|
*/
|
||||||
export const updateThreadAtom = atom(
|
export const updateThreadAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, updatedThread: Thread) => {
|
(get, set, updatedThread: Thread) => {
|
||||||
@ -103,33 +191,8 @@ export const updateThreadAtom = atom(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores all threads for the current user
|
* Update the thread model params
|
||||||
*/
|
*/
|
||||||
export const threadsAtom = atom<Thread[]>([])
|
|
||||||
|
|
||||||
export const activeThreadAtom = atom<Thread | undefined>((get) =>
|
|
||||||
get(threadsAtom).find((c) => c.id === get(getActiveThreadIdAtom))
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store model params at thread level settings
|
|
||||||
*/
|
|
||||||
export const threadModelParamsAtom = atom<Record<string, ModelParams>>({})
|
|
||||||
|
|
||||||
export type ModelParams = ModelRuntimeParams | ModelSettingParams
|
|
||||||
|
|
||||||
export const getActiveThreadModelParamsAtom = atom<ModelParams | undefined>(
|
|
||||||
(get) => {
|
|
||||||
const threadId = get(activeThreadIdAtom)
|
|
||||||
if (!threadId) {
|
|
||||||
console.debug('Active thread id is undefined')
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return get(threadModelParamsAtom)[threadId]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const setThreadModelParamsAtom = atom(
|
export const setThreadModelParamsAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, threadId: string, params: ModelParams) => {
|
(get, set, threadId: string, params: ModelParams) => {
|
||||||
@ -139,12 +202,17 @@ export const setThreadModelParamsAtom = atom(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const ACTIVE_SETTING_INPUT_BOX = 'activeSettingInputBox'
|
/**
|
||||||
|
* Settings input box active state
|
||||||
|
*/
|
||||||
export const activeSettingInputBoxAtom = atomWithStorage<boolean>(
|
export const activeSettingInputBoxAtom = atomWithStorage<boolean>(
|
||||||
ACTIVE_SETTING_INPUT_BOX,
|
ACTIVE_SETTING_INPUT_BOX,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether thread thread is presenting a Modal or not
|
||||||
|
*/
|
||||||
export const modalActionThreadAtom = atom<{
|
export const modalActionThreadAtom = atom<{
|
||||||
showModal: ThreadModalAction | undefined
|
showModal: ThreadModalAction | undefined
|
||||||
thread: Thread | undefined
|
thread: Thread | undefined
|
||||||
@ -153,5 +221,4 @@ export const modalActionThreadAtom = atom<{
|
|||||||
thread: undefined,
|
thread: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const isDownloadALocalModelAtom = atom<boolean>(false)
|
/// Ebd Threads State Atom
|
||||||
export const isAnyRemoteModelConfiguredAtom = atom<boolean>(false)
|
|
||||||
|
|||||||
95
web/hooks/useAssistant.test.ts
Normal file
95
web/hooks/useAssistant.test.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import { useSetAtom } from 'jotai'
|
||||||
|
import { events, AssistantEvent, ExtensionTypeEnum } from '@janhq/core'
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('jotai', () => ({
|
||||||
|
useAtomValue: jest.fn(),
|
||||||
|
useSetAtom: jest.fn(),
|
||||||
|
useAtom: jest.fn(),
|
||||||
|
atom: jest.fn(),
|
||||||
|
}))
|
||||||
|
jest.mock('@janhq/core')
|
||||||
|
jest.mock('@/extension')
|
||||||
|
|
||||||
|
import useAssistants from './useAssistants'
|
||||||
|
import { extensionManager } from '@/extension'
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const mockAssistants = [
|
||||||
|
{ id: 'assistant-1', name: 'Assistant 1' },
|
||||||
|
{ id: 'assistant-2', name: 'Assistant 2' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockAssistantExtension = {
|
||||||
|
getAssistants: jest.fn().mockResolvedValue(mockAssistants),
|
||||||
|
} as any
|
||||||
|
|
||||||
|
describe('useAssistants', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jest.spyOn(extensionManager, 'get').mockReturnValue(mockAssistantExtension)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fetch and set assistants on mount', async () => {
|
||||||
|
const mockSetAssistants = jest.fn()
|
||||||
|
;(useSetAtom as jest.Mock).mockReturnValue(mockSetAssistants)
|
||||||
|
|
||||||
|
renderHook(() => useAssistants())
|
||||||
|
|
||||||
|
// Wait for useEffect to complete
|
||||||
|
await act(async () => {})
|
||||||
|
|
||||||
|
expect(mockAssistantExtension.getAssistants).toHaveBeenCalled()
|
||||||
|
expect(mockSetAssistants).toHaveBeenCalledWith(mockAssistants)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update assistants when AssistantEvent.OnAssistantsUpdate is emitted', async () => {
|
||||||
|
const mockSetAssistants = jest.fn()
|
||||||
|
;(useSetAtom as jest.Mock).mockReturnValue(mockSetAssistants)
|
||||||
|
|
||||||
|
renderHook(() => useAssistants())
|
||||||
|
|
||||||
|
// Wait for initial useEffect to complete
|
||||||
|
await act(async () => {})
|
||||||
|
|
||||||
|
// Clear previous calls
|
||||||
|
mockSetAssistants.mockClear()
|
||||||
|
|
||||||
|
// Simulate AssistantEvent.OnAssistantsUpdate event
|
||||||
|
await act(async () => {
|
||||||
|
events.emit(AssistantEvent.OnAssistantsUpdate, '')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockAssistantExtension.getAssistants).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should unsubscribe from events on unmount', async () => {
|
||||||
|
const { unmount } = renderHook(() => useAssistants())
|
||||||
|
|
||||||
|
// Wait for useEffect to complete
|
||||||
|
await act(async () => {})
|
||||||
|
|
||||||
|
const offSpy = jest.spyOn(events, 'off')
|
||||||
|
|
||||||
|
unmount()
|
||||||
|
|
||||||
|
expect(offSpy).toHaveBeenCalledWith(
|
||||||
|
AssistantEvent.OnAssistantsUpdate,
|
||||||
|
expect.any(Function)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle case when AssistantExtension is not available', async () => {
|
||||||
|
const mockSetAssistants = jest.fn()
|
||||||
|
;(useSetAtom as jest.Mock).mockReturnValue(mockSetAssistants)
|
||||||
|
;(extensionManager.get as jest.Mock).mockReturnValue(undefined)
|
||||||
|
|
||||||
|
renderHook(() => useAssistants())
|
||||||
|
|
||||||
|
// Wait for useEffect to complete
|
||||||
|
await act(async () => {})
|
||||||
|
|
||||||
|
expect(mockSetAssistants).toHaveBeenCalledWith([])
|
||||||
|
})
|
||||||
|
})
|
||||||
105
web/hooks/useClipboard.test.ts
Normal file
105
web/hooks/useClipboard.test.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import { useClipboard } from './useClipboard'
|
||||||
|
|
||||||
|
describe('useClipboard', () => {
|
||||||
|
let originalClipboard: any
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
originalClipboard = { ...global.navigator.clipboard }
|
||||||
|
const mockClipboard = {
|
||||||
|
writeText: jest.fn(() => Promise.resolve()),
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
global.navigator.clipboard = mockClipboard
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
global.navigator.clipboard = originalClipboard
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllTimers()
|
||||||
|
jest.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should copy text to clipboard', async () => {
|
||||||
|
const { result } = renderHook(() => useClipboard())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.copy('Test text')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('Test text')
|
||||||
|
expect(result.current.copied).toBe(true)
|
||||||
|
expect(result.current.error).toBe(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set copied to false after timeout', async () => {
|
||||||
|
const { result } = renderHook(() => useClipboard({ timeout: 1000 }))
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.copy('Test text')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.copied).toBe(true)
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
jest.advanceTimersByTime(1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.copied).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle clipboard errors', async () => {
|
||||||
|
const mockError = new Error('Clipboard error')
|
||||||
|
// @ts-ignore
|
||||||
|
navigator.clipboard.writeText.mockRejectedValueOnce(mockError)
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useClipboard())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.copy('Test text')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.error).toEqual(mockError)
|
||||||
|
expect(result.current.copied).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reset state', async () => {
|
||||||
|
const { result } = renderHook(() => useClipboard())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.copy('Test text')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.copied).toBe(true)
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.copied).toBe(false)
|
||||||
|
expect(result.current.error).toBe(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle missing clipboard API', () => {
|
||||||
|
// @ts-ignore
|
||||||
|
delete global.navigator.clipboard
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useClipboard())
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.copy('Test text')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.error).toEqual(
|
||||||
|
new Error('useClipboard: navigator.clipboard is not supported')
|
||||||
|
)
|
||||||
|
expect(result.current.copied).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
73
web/hooks/useDeleteModel.test.ts
Normal file
73
web/hooks/useDeleteModel.test.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
|
import useDeleteModel from './useDeleteModel'
|
||||||
|
import { toaster } from '@/containers/Toast'
|
||||||
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
// Mock the dependencies
|
||||||
|
jest.mock('@/extension/ExtensionManager')
|
||||||
|
jest.mock('@/containers/Toast')
|
||||||
|
jest.mock('jotai', () => ({
|
||||||
|
useSetAtom: jest.fn(() => jest.fn()),
|
||||||
|
atom: jest.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('useDeleteModel', () => {
|
||||||
|
const mockModel: any = {
|
||||||
|
id: 'test-model',
|
||||||
|
name: 'Test Model',
|
||||||
|
// Add other required properties of ModelFile
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockDeleteModel = jest.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
;(extensionManager.get as jest.Mock).mockReturnValue({
|
||||||
|
deleteModel: mockDeleteModel,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete a model successfully', async () => {
|
||||||
|
const { result } = renderHook(() => useDeleteModel())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.deleteModel(mockModel)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockDeleteModel).toHaveBeenCalledWith(mockModel)
|
||||||
|
expect(toaster).toHaveBeenCalledWith({
|
||||||
|
title: 'Model Deletion Successful',
|
||||||
|
description: `Model ${mockModel.name} has been successfully deleted.`,
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call removeDownloadedModel with the model id', async () => {
|
||||||
|
const { result } = renderHook(() => useDeleteModel())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.deleteModel(mockModel)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Assuming useSetAtom returns a mock function
|
||||||
|
;(useSetAtom as jest.Mock).mockReturnValue(jest.fn())
|
||||||
|
expect(useSetAtom).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle errors during model deletion', async () => {
|
||||||
|
const error = new Error('Deletion failed')
|
||||||
|
mockDeleteModel.mockRejectedValue(error)
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDeleteModel())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await expect(result.current.deleteModel(mockModel)).rejects.toThrow(
|
||||||
|
'Deletion failed'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockDeleteModel).toHaveBeenCalledWith(mockModel)
|
||||||
|
expect(toaster).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
106
web/hooks/useDeleteThread.test.ts
Normal file
106
web/hooks/useDeleteThread.test.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
import useDeleteThread from './useDeleteThread'
|
||||||
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
|
import { toaster } from '@/containers/Toast'
|
||||||
|
|
||||||
|
// Mock the necessary dependencies
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('jotai', () => ({
|
||||||
|
useAtomValue: jest.fn(),
|
||||||
|
useSetAtom: jest.fn(),
|
||||||
|
useAtom: jest.fn(),
|
||||||
|
atom: jest.fn(),
|
||||||
|
}))
|
||||||
|
jest.mock('@/extension/ExtensionManager')
|
||||||
|
jest.mock('@/containers/Toast')
|
||||||
|
|
||||||
|
describe('useDeleteThread', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete a thread successfully', async () => {
|
||||||
|
const mockThreads = [
|
||||||
|
{ id: 'thread1', title: 'Thread 1' },
|
||||||
|
{ id: 'thread2', title: 'Thread 2' },
|
||||||
|
]
|
||||||
|
const mockSetThreads = jest.fn()
|
||||||
|
;(useAtom as jest.Mock).mockReturnValue([mockThreads, mockSetThreads])
|
||||||
|
|
||||||
|
const mockDeleteThread = jest.fn()
|
||||||
|
extensionManager.get = jest.fn().mockReturnValue({
|
||||||
|
deleteThread: mockDeleteThread,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDeleteThread())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.deleteThread('thread1')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockDeleteThread).toHaveBeenCalledWith('thread1')
|
||||||
|
expect(mockSetThreads).toHaveBeenCalledWith([mockThreads[1]])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should clean a thread successfully', async () => {
|
||||||
|
const mockThreads = [{ id: 'thread1', title: 'Thread 1', metadata: {} }]
|
||||||
|
const mockSetThreads = jest.fn()
|
||||||
|
;(useAtom as jest.Mock).mockReturnValue([mockThreads, mockSetThreads])
|
||||||
|
const mockCleanMessages = jest.fn()
|
||||||
|
;(useSetAtom as jest.Mock).mockReturnValue(() => mockCleanMessages)
|
||||||
|
;(useAtomValue as jest.Mock).mockReturnValue(['thread 1'])
|
||||||
|
|
||||||
|
const mockWriteMessages = jest.fn()
|
||||||
|
const mockSaveThread = jest.fn()
|
||||||
|
extensionManager.get = jest.fn().mockReturnValue({
|
||||||
|
writeMessages: mockWriteMessages,
|
||||||
|
saveThread: mockSaveThread,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDeleteThread())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.cleanThread('thread1')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockWriteMessages).toHaveBeenCalled()
|
||||||
|
expect(mockSaveThread).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'thread1',
|
||||||
|
title: 'New Thread',
|
||||||
|
metadata: expect.objectContaining({ lastMessage: undefined }),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle errors when deleting a thread', async () => {
|
||||||
|
const mockThreads = [{ id: 'thread1', title: 'Thread 1' }]
|
||||||
|
const mockSetThreads = jest.fn()
|
||||||
|
;(useAtom as jest.Mock).mockReturnValue([mockThreads, mockSetThreads])
|
||||||
|
|
||||||
|
const mockDeleteThread = jest
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValue(new Error('Delete error'))
|
||||||
|
extensionManager.get = jest.fn().mockReturnValue({
|
||||||
|
deleteThread: mockDeleteThread,
|
||||||
|
})
|
||||||
|
|
||||||
|
const consoleErrorSpy = jest
|
||||||
|
.spyOn(console, 'error')
|
||||||
|
.mockImplementation(() => {})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDeleteThread())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.deleteThread('thread1')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockDeleteThread).toHaveBeenCalledWith('thread1')
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.any(Error))
|
||||||
|
expect(mockSetThreads).not.toHaveBeenCalled()
|
||||||
|
expect(toaster).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
consoleErrorSpy.mockRestore()
|
||||||
|
})
|
||||||
|
})
|
||||||
98
web/hooks/useDownloadModel.test.ts
Normal file
98
web/hooks/useDownloadModel.test.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import { useAtom, useSetAtom } from 'jotai'
|
||||||
|
import useDownloadModel from './useDownloadModel'
|
||||||
|
import * as core from '@janhq/core'
|
||||||
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
|
|
||||||
|
// Mock the necessary dependencies
|
||||||
|
jest.mock('jotai', () => ({
|
||||||
|
useAtomValue: jest.fn(),
|
||||||
|
useSetAtom: jest.fn(),
|
||||||
|
useAtom: jest.fn(),
|
||||||
|
atom: jest.fn(),
|
||||||
|
}))
|
||||||
|
jest.mock('@janhq/core')
|
||||||
|
jest.mock('@/extension/ExtensionManager')
|
||||||
|
jest.mock('./useGpuSetting', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: () => ({
|
||||||
|
getGpuSettings: jest.fn().mockResolvedValue({ some: 'gpuSettings' }),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('useDownloadModel', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
;(useAtom as jest.Mock).mockReturnValue([false, jest.fn()])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should download a model', async () => {
|
||||||
|
const mockModel: core.Model = {
|
||||||
|
id: 'test-model',
|
||||||
|
sources: [{ filename: 'test.bin' }],
|
||||||
|
} as core.Model
|
||||||
|
|
||||||
|
const mockExtension = {
|
||||||
|
downloadModel: jest.fn().mockResolvedValue(undefined),
|
||||||
|
}
|
||||||
|
;(useSetAtom as jest.Mock).mockReturnValue(() => undefined)
|
||||||
|
;(extensionManager.get as jest.Mock).mockReturnValue(mockExtension)
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDownloadModel())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.downloadModel(mockModel)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockExtension.downloadModel).toHaveBeenCalledWith(
|
||||||
|
mockModel,
|
||||||
|
{ some: 'gpuSettings' },
|
||||||
|
{ ignoreSSL: undefined, proxy: '' }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should abort model download', async () => {
|
||||||
|
const mockModel: core.Model = {
|
||||||
|
id: 'test-model',
|
||||||
|
sources: [{ filename: 'test.bin' }],
|
||||||
|
} as core.Model
|
||||||
|
|
||||||
|
;(core.joinPath as jest.Mock).mockResolvedValue('/path/to/model/test.bin')
|
||||||
|
;(core.abortDownload as jest.Mock).mockResolvedValue(undefined)
|
||||||
|
;(useSetAtom as jest.Mock).mockReturnValue(() => undefined)
|
||||||
|
const { result } = renderHook(() => useDownloadModel())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.abortModelDownload(mockModel)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(core.abortDownload).toHaveBeenCalledWith('/path/to/model/test.bin')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle proxy settings', async () => {
|
||||||
|
const mockModel: core.Model = {
|
||||||
|
id: 'test-model',
|
||||||
|
sources: [{ filename: 'test.bin' }],
|
||||||
|
} as core.Model
|
||||||
|
|
||||||
|
const mockExtension = {
|
||||||
|
downloadModel: jest.fn().mockResolvedValue(undefined),
|
||||||
|
}
|
||||||
|
;(useSetAtom as jest.Mock).mockReturnValue(() => undefined)
|
||||||
|
;(extensionManager.get as jest.Mock).mockReturnValue(mockExtension)
|
||||||
|
;(useAtom as jest.Mock).mockReturnValueOnce([true, jest.fn()]) // proxyEnabled
|
||||||
|
;(useAtom as jest.Mock).mockReturnValueOnce(['http://proxy.com', jest.fn()]) // proxy
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDownloadModel())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.downloadModel(mockModel)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockExtension.downloadModel).toHaveBeenCalledWith(
|
||||||
|
mockModel,
|
||||||
|
expect.objectContaining({ some: 'gpuSettings' }),
|
||||||
|
expect.anything()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
129
web/hooks/useDropModelBinaries.test.ts
Normal file
129
web/hooks/useDropModelBinaries.test.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
// useDropModelBinaries.test.ts
|
||||||
|
|
||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import { useSetAtom } from 'jotai'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import useDropModelBinaries from './useDropModelBinaries'
|
||||||
|
import { getFileInfoFromFile } from '@/utils/file'
|
||||||
|
import { snackbar } from '@/containers/Toast'
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
// Mock the necessary dependencies
|
||||||
|
jest.mock('jotai', () => ({
|
||||||
|
useAtomValue: jest.fn(),
|
||||||
|
useSetAtom: jest.fn(),
|
||||||
|
useAtom: jest.fn(),
|
||||||
|
atom: jest.fn(),
|
||||||
|
}))
|
||||||
|
jest.mock('uuid')
|
||||||
|
jest.mock('@/utils/file')
|
||||||
|
jest.mock('@/containers/Toast')
|
||||||
|
|
||||||
|
describe('useDropModelBinaries', () => {
|
||||||
|
const mockSetImportingModels = jest.fn()
|
||||||
|
const mockSetImportModelStage = jest.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
;(useSetAtom as jest.Mock).mockReturnValueOnce(mockSetImportingModels)
|
||||||
|
;(useSetAtom as jest.Mock).mockReturnValueOnce(mockSetImportModelStage)
|
||||||
|
;(uuidv4 as jest.Mock).mockReturnValue('mock-uuid')
|
||||||
|
;(getFileInfoFromFile as jest.Mock).mockResolvedValue([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle dropping supported files', async () => {
|
||||||
|
const { result } = renderHook(() => useDropModelBinaries())
|
||||||
|
|
||||||
|
const mockFiles = [
|
||||||
|
{ name: 'model1.gguf', path: '/path/to/model1.gguf', size: 1000 },
|
||||||
|
{ name: 'model2.gguf', path: '/path/to/model2.gguf', size: 2000 },
|
||||||
|
]
|
||||||
|
|
||||||
|
;(getFileInfoFromFile as jest.Mock).mockResolvedValue(mockFiles)
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.onDropModels([])
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockSetImportingModels).toHaveBeenCalledWith([
|
||||||
|
{
|
||||||
|
importId: 'mock-uuid',
|
||||||
|
modelId: undefined,
|
||||||
|
name: 'model1',
|
||||||
|
description: '',
|
||||||
|
path: '/path/to/model1.gguf',
|
||||||
|
tags: [],
|
||||||
|
size: 1000,
|
||||||
|
status: 'PREPARING',
|
||||||
|
format: 'gguf',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
importId: 'mock-uuid',
|
||||||
|
modelId: undefined,
|
||||||
|
name: 'model2',
|
||||||
|
description: '',
|
||||||
|
path: '/path/to/model2.gguf',
|
||||||
|
tags: [],
|
||||||
|
size: 2000,
|
||||||
|
status: 'PREPARING',
|
||||||
|
format: 'gguf',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(mockSetImportModelStage).toHaveBeenCalledWith('MODEL_SELECTED')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle dropping unsupported files', async () => {
|
||||||
|
const { result } = renderHook(() => useDropModelBinaries())
|
||||||
|
|
||||||
|
const mockFiles = [
|
||||||
|
{ name: 'unsupported.txt', path: '/path/to/unsupported.txt', size: 500 },
|
||||||
|
]
|
||||||
|
|
||||||
|
;(getFileInfoFromFile as jest.Mock).mockResolvedValue(mockFiles)
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.onDropModels([])
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(snackbar).toHaveBeenCalledWith({
|
||||||
|
description: 'Only files with .gguf extension can be imported.',
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
expect(mockSetImportingModels).not.toHaveBeenCalled()
|
||||||
|
expect(mockSetImportModelStage).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle dropping both supported and unsupported files', async () => {
|
||||||
|
const { result } = renderHook(() => useDropModelBinaries())
|
||||||
|
|
||||||
|
const mockFiles = [
|
||||||
|
{ name: 'model.gguf', path: '/path/to/model.gguf', size: 1000 },
|
||||||
|
{ name: 'unsupported.txt', path: '/path/to/unsupported.txt', size: 500 },
|
||||||
|
]
|
||||||
|
|
||||||
|
;(getFileInfoFromFile as jest.Mock).mockResolvedValue(mockFiles)
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.onDropModels([])
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(snackbar).toHaveBeenCalledWith({
|
||||||
|
description: 'Only files with .gguf extension can be imported.',
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
expect(mockSetImportingModels).toHaveBeenCalledWith([
|
||||||
|
{
|
||||||
|
importId: 'mock-uuid',
|
||||||
|
modelId: undefined,
|
||||||
|
name: 'model',
|
||||||
|
description: '',
|
||||||
|
path: '/path/to/model.gguf',
|
||||||
|
tags: [],
|
||||||
|
size: 1000,
|
||||||
|
status: 'PREPARING',
|
||||||
|
format: 'gguf',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(mockSetImportModelStage).toHaveBeenCalledWith('MODEL_SELECTED')
|
||||||
|
})
|
||||||
|
})
|
||||||
89
web/hooks/useFactoryReset.test.ts
Normal file
89
web/hooks/useFactoryReset.test.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
import useFactoryReset, { FactoryResetState } from './useFactoryReset'
|
||||||
|
import { useActiveModel } from './useActiveModel'
|
||||||
|
import { fs } from '@janhq/core'
|
||||||
|
|
||||||
|
// Mock the dependencies
|
||||||
|
jest.mock('jotai', () => ({
|
||||||
|
atom: jest.fn(),
|
||||||
|
useAtomValue: jest.fn(),
|
||||||
|
useSetAtom: jest.fn(),
|
||||||
|
}))
|
||||||
|
jest.mock('./useActiveModel', () => ({
|
||||||
|
useActiveModel: jest.fn(),
|
||||||
|
}))
|
||||||
|
jest.mock('@janhq/core', () => ({
|
||||||
|
fs: {
|
||||||
|
rm: jest.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('useFactoryReset', () => {
|
||||||
|
const mockStopModel = jest.fn()
|
||||||
|
const mockSetFactoryResetState = jest.fn()
|
||||||
|
const mockGetAppConfigurations = jest.fn()
|
||||||
|
const mockUpdateAppConfiguration = jest.fn()
|
||||||
|
const mockRelaunch = jest.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
;(useAtomValue as jest.Mock).mockReturnValue('/default/jan/data/folder')
|
||||||
|
;(useSetAtom as jest.Mock).mockReturnValue(mockSetFactoryResetState)
|
||||||
|
;(useActiveModel as jest.Mock).mockReturnValue({ stopModel: mockStopModel })
|
||||||
|
global.window ??= Object.create(window)
|
||||||
|
global.window.core = {
|
||||||
|
api: {
|
||||||
|
getAppConfigurations: mockGetAppConfigurations,
|
||||||
|
updateAppConfiguration: mockUpdateAppConfiguration,
|
||||||
|
relaunch: mockRelaunch,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mockGetAppConfigurations.mockResolvedValue({
|
||||||
|
data_folder: '/current/jan/data/folder',
|
||||||
|
quick_ask: false,
|
||||||
|
})
|
||||||
|
jest.spyOn(global, 'setTimeout')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reset all correctly', async () => {
|
||||||
|
const { result } = renderHook(() => useFactoryReset())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.resetAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockSetFactoryResetState).toHaveBeenCalledWith(
|
||||||
|
FactoryResetState.Starting
|
||||||
|
)
|
||||||
|
expect(mockSetFactoryResetState).toHaveBeenCalledWith(
|
||||||
|
FactoryResetState.StoppingModel
|
||||||
|
)
|
||||||
|
expect(mockStopModel).toHaveBeenCalled()
|
||||||
|
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 4000)
|
||||||
|
expect(mockSetFactoryResetState).toHaveBeenCalledWith(
|
||||||
|
FactoryResetState.DeletingData
|
||||||
|
)
|
||||||
|
expect(fs.rm).toHaveBeenCalledWith('/current/jan/data/folder')
|
||||||
|
expect(mockUpdateAppConfiguration).toHaveBeenCalledWith({
|
||||||
|
data_folder: '/default/jan/data/folder',
|
||||||
|
quick_ask: false,
|
||||||
|
})
|
||||||
|
expect(mockSetFactoryResetState).toHaveBeenCalledWith(
|
||||||
|
FactoryResetState.ClearLocalStorage
|
||||||
|
)
|
||||||
|
expect(mockRelaunch).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should keep current folder when specified', async () => {
|
||||||
|
const { result } = renderHook(() => useFactoryReset())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.resetAll(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockUpdateAppConfiguration).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add more tests as needed for error cases, edge cases, etc.
|
||||||
|
})
|
||||||
39
web/hooks/useGetHFRepoData.test.ts
Normal file
39
web/hooks/useGetHFRepoData.test.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import { useGetHFRepoData } from './useGetHFRepoData'
|
||||||
|
import { extensionManager } from '@/extension'
|
||||||
|
|
||||||
|
jest.mock('@/extension', () => ({
|
||||||
|
extensionManager: {
|
||||||
|
get: jest.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('useGetHFRepoData', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fetch HF repo data successfully', async () => {
|
||||||
|
const mockData = { name: 'Test Repo', stars: 100 }
|
||||||
|
const mockFetchHuggingFaceRepoData = jest.fn().mockResolvedValue(mockData)
|
||||||
|
;(extensionManager.get as jest.Mock).mockReturnValue({
|
||||||
|
fetchHuggingFaceRepoData: mockFetchHuggingFaceRepoData,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useGetHFRepoData())
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(false)
|
||||||
|
expect(result.current.error).toBeUndefined()
|
||||||
|
|
||||||
|
let data
|
||||||
|
act(() => {
|
||||||
|
data = result.current.getHfRepoData('test-repo')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(true)
|
||||||
|
|
||||||
|
expect(result.current.error).toBeUndefined()
|
||||||
|
expect(await data).toEqual(mockData)
|
||||||
|
expect(mockFetchHuggingFaceRepoData).toHaveBeenCalledWith('test-repo')
|
||||||
|
})
|
||||||
|
})
|
||||||
103
web/hooks/useGetSystemResources.test.ts
Normal file
103
web/hooks/useGetSystemResources.test.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
// useGetSystemResources.test.ts
|
||||||
|
|
||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import useGetSystemResources from './useGetSystemResources'
|
||||||
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
|
|
||||||
|
// Mock the extensionManager
|
||||||
|
jest.mock('@/extension/ExtensionManager', () => ({
|
||||||
|
extensionManager: {
|
||||||
|
get: jest.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock the necessary dependencies
|
||||||
|
jest.mock('jotai', () => ({
|
||||||
|
useAtomValue: jest.fn(),
|
||||||
|
useSetAtom: () => jest.fn(),
|
||||||
|
useAtom: jest.fn(),
|
||||||
|
atom: jest.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('useGetSystemResources', () => {
|
||||||
|
const mockMonitoringExtension = {
|
||||||
|
getResourcesInfo: jest.fn(),
|
||||||
|
getCurrentLoad: jest.fn(),
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers()
|
||||||
|
;(extensionManager.get as jest.Mock).mockReturnValue(
|
||||||
|
mockMonitoringExtension
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jest.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fetch system resources on initial render', async () => {
|
||||||
|
mockMonitoringExtension.getResourcesInfo.mockResolvedValue({
|
||||||
|
mem: { usedMemory: 4000, totalMemory: 8000 },
|
||||||
|
})
|
||||||
|
mockMonitoringExtension.getCurrentLoad.mockResolvedValue({
|
||||||
|
cpu: { usage: 50 },
|
||||||
|
gpu: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useGetSystemResources())
|
||||||
|
|
||||||
|
expect(mockMonitoringExtension.getResourcesInfo).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should start watching system resources when watch is called', () => {
|
||||||
|
const { result } = renderHook(() => useGetSystemResources())
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.watch()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockMonitoringExtension.getResourcesInfo).toHaveBeenCalled()
|
||||||
|
|
||||||
|
// Fast-forward time by 2 seconds
|
||||||
|
act(() => {
|
||||||
|
jest.advanceTimersByTime(2000)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockMonitoringExtension.getResourcesInfo).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should stop watching when stopWatching is called', () => {
|
||||||
|
const { result } = renderHook(() => useGetSystemResources())
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.watch()
|
||||||
|
})
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.stopWatching()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fast-forward time by 2 seconds
|
||||||
|
act(() => {
|
||||||
|
jest.advanceTimersByTime(2000)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Expect no additional calls after stopping
|
||||||
|
expect(mockMonitoringExtension.getResourcesInfo).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not fetch resources if monitoring extension is not available', async () => {
|
||||||
|
;(extensionManager.get as jest.Mock).mockReturnValue(null)
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useGetSystemResources())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.getSystemResources()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockMonitoringExtension.getResourcesInfo).not.toHaveBeenCalled()
|
||||||
|
expect(mockMonitoringExtension.getCurrentLoad).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
87
web/hooks/useGpuSetting.test.ts
Normal file
87
web/hooks/useGpuSetting.test.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// useGpuSetting.test.ts
|
||||||
|
|
||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import { ExtensionTypeEnum, MonitoringExtension } from '@janhq/core'
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('@/extension')
|
||||||
|
|
||||||
|
import useGpuSetting from './useGpuSetting'
|
||||||
|
import { extensionManager } from '@/extension'
|
||||||
|
|
||||||
|
describe('useGpuSetting', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return GPU settings when available', async () => {
|
||||||
|
const mockGpuSettings = {
|
||||||
|
gpuCount: 2,
|
||||||
|
gpuNames: ['NVIDIA GeForce RTX 3080', 'NVIDIA GeForce RTX 3070'],
|
||||||
|
totalMemory: 20000,
|
||||||
|
freeMemory: 15000,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockMonitoringExtension: Partial<MonitoringExtension> = {
|
||||||
|
getGpuSetting: jest.fn().mockResolvedValue(mockGpuSettings),
|
||||||
|
}
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(extensionManager, 'get')
|
||||||
|
.mockReturnValue(mockMonitoringExtension as MonitoringExtension)
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useGpuSetting())
|
||||||
|
|
||||||
|
let gpuSettings
|
||||||
|
await act(async () => {
|
||||||
|
gpuSettings = await result.current.getGpuSettings()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(gpuSettings).toEqual(mockGpuSettings)
|
||||||
|
expect(extensionManager.get).toHaveBeenCalledWith(
|
||||||
|
ExtensionTypeEnum.SystemMonitoring
|
||||||
|
)
|
||||||
|
expect(mockMonitoringExtension.getGpuSetting).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return undefined when no GPU settings are found', async () => {
|
||||||
|
const mockMonitoringExtension: Partial<MonitoringExtension> = {
|
||||||
|
getGpuSetting: jest.fn().mockResolvedValue(undefined),
|
||||||
|
}
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(extensionManager, 'get')
|
||||||
|
.mockReturnValue(mockMonitoringExtension as MonitoringExtension)
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useGpuSetting())
|
||||||
|
|
||||||
|
let gpuSettings
|
||||||
|
await act(async () => {
|
||||||
|
gpuSettings = await result.current.getGpuSettings()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(gpuSettings).toBeUndefined()
|
||||||
|
expect(extensionManager.get).toHaveBeenCalledWith(
|
||||||
|
ExtensionTypeEnum.SystemMonitoring
|
||||||
|
)
|
||||||
|
expect(mockMonitoringExtension.getGpuSetting).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle missing MonitoringExtension', async () => {
|
||||||
|
jest.spyOn(extensionManager, 'get').mockReturnValue(undefined)
|
||||||
|
jest.spyOn(console, 'debug').mockImplementation(() => {})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useGpuSetting())
|
||||||
|
|
||||||
|
let gpuSettings
|
||||||
|
await act(async () => {
|
||||||
|
gpuSettings = await result.current.getGpuSettings()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(gpuSettings).toBeUndefined()
|
||||||
|
expect(extensionManager.get).toHaveBeenCalledWith(
|
||||||
|
ExtensionTypeEnum.SystemMonitoring
|
||||||
|
)
|
||||||
|
expect(console.debug).toHaveBeenCalledWith('No GPU setting found')
|
||||||
|
})
|
||||||
|
})
|
||||||
70
web/hooks/useImportModel.test.ts
Normal file
70
web/hooks/useImportModel.test.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// useImportModel.test.ts
|
||||||
|
|
||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import { extensionManager } from '@/extension'
|
||||||
|
import useImportModel from './useImportModel'
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('@janhq/core')
|
||||||
|
jest.mock('@/extension')
|
||||||
|
jest.mock('@/containers/Toast')
|
||||||
|
jest.mock('uuid', () => ({ v4: () => 'mocked-uuid' }))
|
||||||
|
|
||||||
|
describe('useImportModel', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should import models successfully', async () => {
|
||||||
|
const mockImportModels = jest.fn().mockResolvedValue(undefined)
|
||||||
|
const mockExtension = {
|
||||||
|
importModels: mockImportModels,
|
||||||
|
} as any
|
||||||
|
|
||||||
|
jest.spyOn(extensionManager, 'get').mockReturnValue(mockExtension)
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useImportModel())
|
||||||
|
|
||||||
|
const models = [
|
||||||
|
{ importId: '1', name: 'Model 1', path: '/path/to/model1' },
|
||||||
|
{ importId: '2', name: 'Model 2', path: '/path/to/model2' },
|
||||||
|
] as any
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.importModels(models, 'local' as any)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockImportModels).toHaveBeenCalledWith(models, 'local')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update model info successfully', async () => {
|
||||||
|
const mockUpdateModelInfo = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ id: 'model-1', name: 'Updated Model' })
|
||||||
|
const mockExtension = {
|
||||||
|
updateModelInfo: mockUpdateModelInfo,
|
||||||
|
} as any
|
||||||
|
|
||||||
|
jest.spyOn(extensionManager, 'get').mockReturnValue(mockExtension)
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useImportModel())
|
||||||
|
|
||||||
|
const modelInfo = { id: 'model-1', name: 'Updated Model' }
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.updateModelInfo(modelInfo)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockUpdateModelInfo).toHaveBeenCalledWith(modelInfo)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty file paths', async () => {
|
||||||
|
const { result } = renderHook(() => useImportModel())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.sanitizeFilePaths([])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Expect no state changes or side effects
|
||||||
|
})
|
||||||
|
})
|
||||||
111
web/hooks/useLoadTheme.test.ts
Normal file
111
web/hooks/useLoadTheme.test.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import { useTheme } from 'next-themes'
|
||||||
|
import { fs, joinPath } from '@janhq/core'
|
||||||
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
|
import { useLoadTheme } from './useLoadTheme'
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('next-themes')
|
||||||
|
jest.mock('@janhq/core')
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('jotai', () => ({
|
||||||
|
useAtomValue: jest.fn(),
|
||||||
|
useSetAtom: jest.fn(),
|
||||||
|
useAtom: jest.fn(),
|
||||||
|
atom: jest.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('useLoadTheme', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockJanDataFolderPath = '/mock/path'
|
||||||
|
const mockThemesPath = '/mock/path/themes'
|
||||||
|
const mockSelectedThemeId = 'joi-light'
|
||||||
|
const mockThemeData = {
|
||||||
|
id: 'joi-light',
|
||||||
|
displayName: 'Joi Light',
|
||||||
|
nativeTheme: 'light',
|
||||||
|
variables: {
|
||||||
|
'--primary-color': '#007bff',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should load theme and set variables', async () => {
|
||||||
|
// Mock Jotai hooks
|
||||||
|
;(useAtomValue as jest.Mock).mockReturnValue(mockJanDataFolderPath)
|
||||||
|
;(useSetAtom as jest.Mock).mockReturnValue(jest.fn())
|
||||||
|
;(useAtom as jest.Mock).mockReturnValue([mockSelectedThemeId, jest.fn()])
|
||||||
|
;(useAtom as jest.Mock).mockReturnValue([mockThemeData, jest.fn()])
|
||||||
|
|
||||||
|
// Mock fs and joinPath
|
||||||
|
;(fs.readdirSync as jest.Mock).mockResolvedValue(['joi-light', 'joi-dark'])
|
||||||
|
;(fs.readFileSync as jest.Mock).mockResolvedValue(
|
||||||
|
JSON.stringify(mockThemeData)
|
||||||
|
)
|
||||||
|
;(joinPath as jest.Mock).mockImplementation((paths) => paths.join('/'))
|
||||||
|
|
||||||
|
// Mock setTheme from next-themes
|
||||||
|
const mockSetTheme = jest.fn()
|
||||||
|
;(useTheme as jest.Mock).mockReturnValue({ setTheme: mockSetTheme })
|
||||||
|
|
||||||
|
// Mock window.electronAPI
|
||||||
|
Object.defineProperty(window, 'electronAPI', {
|
||||||
|
value: {
|
||||||
|
setNativeThemeLight: jest.fn(),
|
||||||
|
setNativeThemeDark: jest.fn(),
|
||||||
|
},
|
||||||
|
writable: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLoadTheme())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current
|
||||||
|
})
|
||||||
|
|
||||||
|
// Assertions
|
||||||
|
expect(fs.readdirSync).toHaveBeenCalledWith(mockThemesPath)
|
||||||
|
expect(fs.readFileSync).toHaveBeenCalledWith(
|
||||||
|
`${mockThemesPath}/${mockSelectedThemeId}/theme.json`,
|
||||||
|
'utf-8'
|
||||||
|
)
|
||||||
|
expect(mockSetTheme).toHaveBeenCalledWith('light')
|
||||||
|
expect(window.electronAPI.setNativeThemeLight).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set default theme if no selected theme', async () => {
|
||||||
|
// Mock Jotai hooks with empty selected theme
|
||||||
|
;(useAtomValue as jest.Mock).mockReturnValue(mockJanDataFolderPath)
|
||||||
|
;(useSetAtom as jest.Mock).mockReturnValue(jest.fn())
|
||||||
|
;(useAtom as jest.Mock).mockReturnValue(['', jest.fn()])
|
||||||
|
;(useAtom as jest.Mock).mockReturnValue([{}, jest.fn()])
|
||||||
|
|
||||||
|
const mockSetSelectedThemeId = jest.fn()
|
||||||
|
;(useAtom as jest.Mock).mockReturnValue(['', mockSetSelectedThemeId])
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLoadTheme())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockSetSelectedThemeId).toHaveBeenCalledWith('joi-light')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle missing janDataFolderPath', async () => {
|
||||||
|
// Mock Jotai hooks with empty janDataFolderPath
|
||||||
|
;(useAtomValue as jest.Mock).mockReturnValue('')
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLoadTheme())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(fs.readdirSync).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
103
web/hooks/useLogs.test.ts
Normal file
103
web/hooks/useLogs.test.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
// useLogs.test.ts
|
||||||
|
|
||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
|
import { fs, joinPath, openFileExplorer } from '@janhq/core'
|
||||||
|
|
||||||
|
import { useLogs } from './useLogs'
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('jotai', () => ({
|
||||||
|
useAtomValue: jest.fn(),
|
||||||
|
atom: jest.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@janhq/core', () => ({
|
||||||
|
fs: {
|
||||||
|
existsSync: jest.fn(),
|
||||||
|
readFileSync: jest.fn(),
|
||||||
|
writeFileSync: jest.fn(),
|
||||||
|
},
|
||||||
|
joinPath: jest.fn(),
|
||||||
|
openFileExplorer: jest.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('useLogs', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
;(useAtomValue as jest.Mock).mockReturnValue('/mock/jan/data/folder')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get logs and sanitize them', async () => {
|
||||||
|
const mockLogs = '/mock/jan/data/folder/some/log/content'
|
||||||
|
const expectedSanitizedLogs = 'jan-data-folder/some/log/content'
|
||||||
|
|
||||||
|
;(joinPath as jest.Mock).mockResolvedValue('file://logs/test.log')
|
||||||
|
;(fs.existsSync as jest.Mock).mockResolvedValue(true)
|
||||||
|
;(fs.readFileSync as jest.Mock).mockResolvedValue(mockLogs)
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLogs())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
const logs = await result.current.getLogs('test')
|
||||||
|
expect(logs).toBe(expectedSanitizedLogs)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(joinPath).toHaveBeenCalledWith(['file://logs', 'test.log'])
|
||||||
|
expect(fs.existsSync).toHaveBeenCalledWith('file://logs/test.log')
|
||||||
|
expect(fs.readFileSync).toHaveBeenCalledWith(
|
||||||
|
'file://logs/test.log',
|
||||||
|
'utf-8'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return empty string if log file does not exist', async () => {
|
||||||
|
;(joinPath as jest.Mock).mockResolvedValue('file://logs/nonexistent.log')
|
||||||
|
;(fs.existsSync as jest.Mock).mockResolvedValue(false)
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLogs())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
const logs = await result.current.getLogs('nonexistent')
|
||||||
|
expect(logs).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(fs.readFileSync).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should open server log', async () => {
|
||||||
|
;(joinPath as jest.Mock).mockResolvedValue(
|
||||||
|
'/mock/jan/data/folder/logs/app.log'
|
||||||
|
)
|
||||||
|
;(openFileExplorer as jest.Mock).mockResolvedValue(undefined)
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLogs())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.openServerLog()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(joinPath).toHaveBeenCalledWith([
|
||||||
|
'/mock/jan/data/folder',
|
||||||
|
'logs',
|
||||||
|
'app.log',
|
||||||
|
])
|
||||||
|
expect(openFileExplorer).toHaveBeenCalledWith(
|
||||||
|
'/mock/jan/data/folder/logs/app.log'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should clear server log', async () => {
|
||||||
|
;(joinPath as jest.Mock).mockResolvedValue('file://logs/app.log')
|
||||||
|
;(fs.writeFileSync as jest.Mock).mockResolvedValue(undefined)
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useLogs())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.clearServerLog()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(joinPath).toHaveBeenCalledWith(['file://logs', 'app.log'])
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalledWith('file://logs/app.log', '')
|
||||||
|
})
|
||||||
|
})
|
||||||
61
web/hooks/useModels.test.ts
Normal file
61
web/hooks/useModels.test.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// useModels.test.ts
|
||||||
|
|
||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import { events, ModelEvent } from '@janhq/core'
|
||||||
|
import { extensionManager } from '@/extension'
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('@janhq/core')
|
||||||
|
jest.mock('@/extension')
|
||||||
|
|
||||||
|
import useModels from './useModels'
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const mockDownloadedModels = [
|
||||||
|
{ id: 'model-1', name: 'Model 1' },
|
||||||
|
{ id: 'model-2', name: 'Model 2' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockConfiguredModels = [
|
||||||
|
{ id: 'model-3', name: 'Model 3' },
|
||||||
|
{ id: 'model-4', name: 'Model 4' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockDefaultModel = { id: 'default-model', name: 'Default Model' }
|
||||||
|
|
||||||
|
describe('useModels', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fetch and set models on mount', async () => {
|
||||||
|
const mockModelExtension = {
|
||||||
|
getDownloadedModels: jest.fn().mockResolvedValue(mockDownloadedModels),
|
||||||
|
getConfiguredModels: jest.fn().mockResolvedValue(mockConfiguredModels),
|
||||||
|
getDefaultModel: jest.fn().mockResolvedValue(mockDefaultModel),
|
||||||
|
} as any
|
||||||
|
|
||||||
|
jest.spyOn(extensionManager, 'get').mockReturnValue(mockModelExtension)
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
renderHook(() => useModels())
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockModelExtension.getDownloadedModels).toHaveBeenCalled()
|
||||||
|
expect(mockModelExtension.getConfiguredModels).toHaveBeenCalled()
|
||||||
|
expect(mockModelExtension.getDefaultModel).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove event listener on unmount', async () => {
|
||||||
|
const removeListenerSpy = jest.spyOn(events, 'off')
|
||||||
|
|
||||||
|
const { unmount } = renderHook(() => useModels())
|
||||||
|
|
||||||
|
unmount()
|
||||||
|
|
||||||
|
expect(removeListenerSpy).toHaveBeenCalledWith(
|
||||||
|
ModelEvent.OnModelsUpdate,
|
||||||
|
expect.any(Function)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -18,6 +18,11 @@ import {
|
|||||||
downloadedModelsAtom,
|
downloadedModelsAtom,
|
||||||
} from '@/helpers/atoms/Model.atom'
|
} from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useModels hook - Handles the state of models
|
||||||
|
* It fetches the downloaded models, configured models and default model from Model Extension
|
||||||
|
* and updates the atoms accordingly.
|
||||||
|
*/
|
||||||
const useModels = () => {
|
const useModels = () => {
|
||||||
const setDownloadedModels = useSetAtom(downloadedModelsAtom)
|
const setDownloadedModels = useSetAtom(downloadedModelsAtom)
|
||||||
const setConfiguredModels = useSetAtom(configuredModelsAtom)
|
const setConfiguredModels = useSetAtom(configuredModelsAtom)
|
||||||
@ -39,6 +44,7 @@ const useModels = () => {
|
|||||||
setDefaultModel(defaultModel)
|
setDefaultModel(defaultModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch all data
|
||||||
Promise.all([
|
Promise.all([
|
||||||
getDownloadedModels(),
|
getDownloadedModels(),
|
||||||
getConfiguredModels(),
|
getConfiguredModels(),
|
||||||
@ -59,16 +65,19 @@ const useModels = () => {
|
|||||||
}, [getData])
|
}, [getData])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Deprecated - Remove when moving to cortex.cpp
|
||||||
const getLocalDefaultModel = async (): Promise<Model | undefined> =>
|
const getLocalDefaultModel = async (): Promise<Model | undefined> =>
|
||||||
extensionManager
|
extensionManager
|
||||||
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
||||||
?.getDefaultModel()
|
?.getDefaultModel()
|
||||||
|
|
||||||
|
// TODO: Deprecated - Remove when moving to cortex.cpp
|
||||||
const getLocalConfiguredModels = async (): Promise<ModelFile[]> =>
|
const getLocalConfiguredModels = async (): Promise<ModelFile[]> =>
|
||||||
extensionManager
|
extensionManager
|
||||||
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
||||||
?.getConfiguredModels() ?? []
|
?.getConfiguredModels() ?? []
|
||||||
|
|
||||||
|
// TODO: Deprecated - Remove when moving to cortex.cpp
|
||||||
const getLocalDownloadedModels = async (): Promise<ModelFile[]> =>
|
const getLocalDownloadedModels = async (): Promise<ModelFile[]> =>
|
||||||
extensionManager
|
extensionManager
|
||||||
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
.get<ModelExtension>(ExtensionTypeEnum.Model)
|
||||||
|
|||||||
@ -8,10 +8,10 @@ import {
|
|||||||
setConvoMessagesAtom,
|
setConvoMessagesAtom,
|
||||||
} from '@/helpers/atoms/ChatMessage.atom'
|
} from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import {
|
import {
|
||||||
ModelParams,
|
|
||||||
setActiveThreadIdAtom,
|
setActiveThreadIdAtom,
|
||||||
setThreadModelParamsAtom,
|
setThreadModelParamsAtom,
|
||||||
} from '@/helpers/atoms/Thread.atom'
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
|
import { ModelParams } from '@/types/model'
|
||||||
|
|
||||||
export default function useSetActiveThread() {
|
export default function useSetActiveThread() {
|
||||||
const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
|
const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
|
||||||
|
|||||||
192
web/hooks/useThread.test.ts
Normal file
192
web/hooks/useThread.test.ts
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
// useThreads.test.ts
|
||||||
|
|
||||||
|
import { renderHook, act } from '@testing-library/react'
|
||||||
|
import { useSetAtom } from 'jotai'
|
||||||
|
import { ExtensionTypeEnum } from '@janhq/core'
|
||||||
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
|
import useThreads from './useThreads'
|
||||||
|
import {
|
||||||
|
threadDataReadyAtom,
|
||||||
|
threadModelParamsAtom,
|
||||||
|
threadsAtom,
|
||||||
|
threadStatesAtom,
|
||||||
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
|
// Mock the necessary dependencies
|
||||||
|
jest.mock('jotai', () => ({
|
||||||
|
useAtomValue: jest.fn(),
|
||||||
|
useSetAtom: jest.fn(),
|
||||||
|
useAtom: jest.fn(),
|
||||||
|
atom: jest.fn(),
|
||||||
|
}))
|
||||||
|
jest.mock('@/extension/ExtensionManager')
|
||||||
|
|
||||||
|
describe('useThreads', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockThreads = [
|
||||||
|
{
|
||||||
|
id: 'thread1',
|
||||||
|
metadata: { lastMessage: 'Hello' },
|
||||||
|
assistants: [
|
||||||
|
{
|
||||||
|
model: {
|
||||||
|
parameters: { param1: 'value1' },
|
||||||
|
settings: { setting1: 'value1' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'thread2',
|
||||||
|
metadata: { lastMessage: 'Hi there' },
|
||||||
|
assistants: [
|
||||||
|
{
|
||||||
|
model: {
|
||||||
|
parameters: { param2: 'value2' },
|
||||||
|
settings: { setting2: 'value2' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
it('should fetch and set threads data', async () => {
|
||||||
|
// Mock Jotai hooks
|
||||||
|
const mockSetThreadStates = jest.fn()
|
||||||
|
const mockSetThreads = jest.fn()
|
||||||
|
const mockSetThreadModelRuntimeParams = jest.fn()
|
||||||
|
const mockSetThreadDataReady = jest.fn()
|
||||||
|
|
||||||
|
;(useSetAtom as jest.Mock).mockImplementation((atom) => {
|
||||||
|
switch (atom) {
|
||||||
|
case threadStatesAtom:
|
||||||
|
return mockSetThreadStates
|
||||||
|
case threadsAtom:
|
||||||
|
return mockSetThreads
|
||||||
|
case threadModelParamsAtom:
|
||||||
|
return mockSetThreadModelRuntimeParams
|
||||||
|
case threadDataReadyAtom:
|
||||||
|
return mockSetThreadDataReady
|
||||||
|
default:
|
||||||
|
return jest.fn()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock extensionManager
|
||||||
|
const mockGetThreads = jest.fn().mockResolvedValue(mockThreads)
|
||||||
|
;(extensionManager.get as jest.Mock).mockReturnValue({
|
||||||
|
getThreads: mockGetThreads,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useThreads())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
// Wait for useEffect to complete
|
||||||
|
})
|
||||||
|
|
||||||
|
// Assertions
|
||||||
|
expect(extensionManager.get).toHaveBeenCalledWith(
|
||||||
|
ExtensionTypeEnum.Conversational
|
||||||
|
)
|
||||||
|
expect(mockGetThreads).toHaveBeenCalled()
|
||||||
|
|
||||||
|
expect(mockSetThreadStates).toHaveBeenCalledWith({
|
||||||
|
thread1: {
|
||||||
|
hasMore: false,
|
||||||
|
waitingForResponse: false,
|
||||||
|
lastMessage: 'Hello',
|
||||||
|
},
|
||||||
|
thread2: {
|
||||||
|
hasMore: false,
|
||||||
|
waitingForResponse: false,
|
||||||
|
lastMessage: 'Hi there',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockSetThreads).toHaveBeenCalledWith(mockThreads)
|
||||||
|
|
||||||
|
expect(mockSetThreadModelRuntimeParams).toHaveBeenCalledWith({
|
||||||
|
thread1: { param1: 'value1', setting1: 'value1' },
|
||||||
|
thread2: { param2: 'value2', setting2: 'value2' },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockSetThreadDataReady).toHaveBeenCalledWith(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty threads', async () => {
|
||||||
|
// Mock empty threads
|
||||||
|
;(extensionManager.get as jest.Mock).mockReturnValue({
|
||||||
|
getThreads: jest.fn().mockResolvedValue([]),
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockSetThreadStates = jest.fn()
|
||||||
|
const mockSetThreads = jest.fn()
|
||||||
|
const mockSetThreadModelRuntimeParams = jest.fn()
|
||||||
|
const mockSetThreadDataReady = jest.fn()
|
||||||
|
|
||||||
|
;(useSetAtom as jest.Mock).mockImplementation((atom) => {
|
||||||
|
switch (atom) {
|
||||||
|
case threadStatesAtom:
|
||||||
|
return mockSetThreadStates
|
||||||
|
case threadsAtom:
|
||||||
|
return mockSetThreads
|
||||||
|
case threadModelParamsAtom:
|
||||||
|
return mockSetThreadModelRuntimeParams
|
||||||
|
case threadDataReadyAtom:
|
||||||
|
return mockSetThreadDataReady
|
||||||
|
default:
|
||||||
|
return jest.fn()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useThreads())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
// Wait for useEffect to complete
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockSetThreadStates).toHaveBeenCalledWith({})
|
||||||
|
expect(mockSetThreads).toHaveBeenCalledWith([])
|
||||||
|
expect(mockSetThreadModelRuntimeParams).toHaveBeenCalledWith({})
|
||||||
|
expect(mockSetThreadDataReady).toHaveBeenCalledWith(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle missing ConversationalExtension', async () => {
|
||||||
|
// Mock missing ConversationalExtension
|
||||||
|
;(extensionManager.get as jest.Mock).mockReturnValue(null)
|
||||||
|
|
||||||
|
const mockSetThreadStates = jest.fn()
|
||||||
|
const mockSetThreads = jest.fn()
|
||||||
|
const mockSetThreadModelRuntimeParams = jest.fn()
|
||||||
|
const mockSetThreadDataReady = jest.fn()
|
||||||
|
|
||||||
|
;(useSetAtom as jest.Mock).mockImplementation((atom) => {
|
||||||
|
switch (atom) {
|
||||||
|
case threadStatesAtom:
|
||||||
|
return mockSetThreadStates
|
||||||
|
case threadsAtom:
|
||||||
|
return mockSetThreads
|
||||||
|
case threadModelParamsAtom:
|
||||||
|
return mockSetThreadModelRuntimeParams
|
||||||
|
case threadDataReadyAtom:
|
||||||
|
return mockSetThreadDataReady
|
||||||
|
default:
|
||||||
|
return jest.fn()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useThreads())
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
// Wait for useEffect to complete
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockSetThreadStates).toHaveBeenCalledWith({})
|
||||||
|
expect(mockSetThreads).toHaveBeenCalledWith([])
|
||||||
|
expect(mockSetThreadModelRuntimeParams).toHaveBeenCalledWith({})
|
||||||
|
expect(mockSetThreadDataReady).toHaveBeenCalledWith(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -11,12 +11,12 @@ import { useSetAtom } from 'jotai'
|
|||||||
|
|
||||||
import { extensionManager } from '@/extension/ExtensionManager'
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
import {
|
import {
|
||||||
ModelParams,
|
|
||||||
threadDataReadyAtom,
|
threadDataReadyAtom,
|
||||||
threadModelParamsAtom,
|
threadModelParamsAtom,
|
||||||
threadStatesAtom,
|
threadStatesAtom,
|
||||||
threadsAtom,
|
threadsAtom,
|
||||||
} from '@/helpers/atoms/Thread.atom'
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
|
import { ModelParams } from '@/types/model'
|
||||||
|
|
||||||
const useThreads = () => {
|
const useThreads = () => {
|
||||||
const setThreadStates = useSetAtom(threadStatesAtom)
|
const setThreadStates = useSetAtom(threadStatesAtom)
|
||||||
|
|||||||
@ -18,10 +18,10 @@ import {
|
|||||||
import { extensionManager } from '@/extension'
|
import { extensionManager } from '@/extension'
|
||||||
import { selectedModelAtom } from '@/helpers/atoms/Model.atom'
|
import { selectedModelAtom } from '@/helpers/atoms/Model.atom'
|
||||||
import {
|
import {
|
||||||
ModelParams,
|
|
||||||
getActiveThreadModelParamsAtom,
|
getActiveThreadModelParamsAtom,
|
||||||
setThreadModelParamsAtom,
|
setThreadModelParamsAtom,
|
||||||
} from '@/helpers/atoms/Thread.atom'
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
|
import { ModelParams } from '@/types/model'
|
||||||
|
|
||||||
export type UpdateModelParameter = {
|
export type UpdateModelParameter = {
|
||||||
params?: ModelParams
|
params?: ModelParams
|
||||||
|
|||||||
4
web/types/model.d.ts
vendored
Normal file
4
web/types/model.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* ModelParams types
|
||||||
|
*/
|
||||||
|
export type ModelParams = ModelRuntimeParams | ModelSettingParams
|
||||||
@ -2,7 +2,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import { ModelRuntimeParams, ModelSettingParams } from '@janhq/core'
|
import { ModelRuntimeParams, ModelSettingParams } from '@janhq/core'
|
||||||
|
|
||||||
import { ModelParams } from '@/helpers/atoms/Thread.atom'
|
import { ModelParams } from '@/types/model'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validation rules for model parameters
|
* Validation rules for model parameters
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user