diff --git a/core/src/browser/extension.ts b/core/src/browser/extension.ts index d934e1c06..b7a9fca4e 100644 --- a/core/src/browser/extension.ts +++ b/core/src/browser/extension.ts @@ -113,7 +113,6 @@ export abstract class BaseExtension implements ExtensionType { for (const model of models) { ModelManager.instance().register(model) } - events.emit(ModelEvent.OnModelsUpdate, {}) } /** diff --git a/electron/managers/window.ts b/electron/managers/window.ts index c9c43ea77..918036365 100644 --- a/electron/managers/window.ts +++ b/electron/managers/window.ts @@ -28,6 +28,7 @@ class WindowManager { ...mainWindowConfig, width: bounds.width, height: bounds.height, + show: false, x: bounds.x, y: bounds.y, webPreferences: { @@ -78,6 +79,10 @@ class WindowManager { windowManager.hideMainWindow() } }) + + windowManager.mainWindow?.on('ready-to-show', function () { + windowManager.mainWindow?.show() + }) } createQuickAskWindow(preloadPath: string, startUrl: string): void { diff --git a/extensions/inference-cortex-extension/src/index.ts b/extensions/inference-cortex-extension/src/index.ts index 6bd3c468e..e24b5413f 100644 --- a/extensions/inference-cortex-extension/src/index.ts +++ b/extensions/inference-cortex-extension/src/index.ts @@ -228,7 +228,9 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine { // Delay for the state update from cortex.cpp // Just to be sure setTimeout(() => { - events.emit(ModelEvent.OnModelsUpdate, {}) + events.emit(ModelEvent.OnModelsUpdate, { + fetch: true, + }) }, 500) } }) diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts index b3ad2a012..63f505bd6 100644 --- a/extensions/model-extension/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -40,11 +40,6 @@ export default class JanModelExtension extends ModelExtension { async onLoad() { this.registerSettings(SETTINGS) - // Try get models from cortex.cpp - this.getModels().then((models) => { - this.registerModels(models) - }) - // Listen to app download events this.handleDesktopEvents() } @@ -163,19 +158,27 @@ export default class JanModelExtension extends ModelExtension { (e) => e.engine === InferenceEngine.nitro ) - await this.cortexAPI.getModels().then((models) => { - const existingIds = models.map((e) => e.id) - toImportModels = toImportModels.filter( - (e: Model) => !existingIds.includes(e.id) && !e.settings?.vision_model - ) - }) + /** + * Fetch models from cortex.cpp + */ + var fetchedModels = await this.cortexAPI.getModels().catch(() => []) + + // Checking if there are models to import + const existingIds = fetchedModels.map((e) => e.id) + toImportModels = toImportModels.filter( + (e: Model) => !existingIds.includes(e.id) && !e.settings?.vision_model + ) + + /** + * There is no model to import + * just return fetched models + */ + if (!toImportModels.length) return fetchedModels console.log('To import models:', toImportModels.length) /** * There are models to import - * do not return models from cortex.cpp yet - * otherwise it will reset the app cache - * */ + */ if (toImportModels.length > 0) { // Import models await Promise.all( @@ -202,8 +205,6 @@ export default class JanModelExtension extends ModelExtension { }) }) ) - - return currentModels } /** diff --git a/web/containers/Providers/DataLoader.tsx b/web/containers/Providers/DataLoader.tsx index 4319c5eed..d3d747d02 100644 --- a/web/containers/Providers/DataLoader.tsx +++ b/web/containers/Providers/DataLoader.tsx @@ -29,13 +29,19 @@ const DataLoader: React.FC = ({ children }) => { const setQuickAskEnabled = useSetAtom(quickAskEnabledAtom) const setJanDefaultDataFolder = useSetAtom(defaultJanDataFolderAtom) const setJanSettingScreen = useSetAtom(janSettingScreenAtom) + const { loadDataModel } = useModels() - useModels() useThreads() useAssistants() useGetSystemResources() useLoadTheme() + useEffect(() => { + // Load data once + loadDataModel() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + useEffect(() => { window.core?.api ?.getAppConfigurations() diff --git a/web/containers/Providers/EventListener.tsx b/web/containers/Providers/EventListener.tsx index 5cb0debab..c1dcf7c40 100644 --- a/web/containers/Providers/EventListener.tsx +++ b/web/containers/Providers/EventListener.tsx @@ -112,8 +112,8 @@ const EventListenerWrapper = ({ children }: PropsWithChildren) => { state.downloadState = 'end' setDownloadState(state) removeDownloadingModel(state.modelId) + events.emit(ModelEvent.OnModelsUpdate, { fetch: true }) } - events.emit(ModelEvent.OnModelsUpdate, {}) }, [removeDownloadingModel, setDownloadState] ) diff --git a/web/containers/Providers/ModelImportListener.tsx b/web/containers/Providers/ModelImportListener.tsx index f1ca2a768..a60b7be80 100644 --- a/web/containers/Providers/ModelImportListener.tsx +++ b/web/containers/Providers/ModelImportListener.tsx @@ -43,7 +43,7 @@ const ModelImportListener = ({ children }: PropsWithChildren) => { const onImportModelSuccess = useCallback( (state: ImportingModel) => { if (!state.modelId) return - events.emit(ModelEvent.OnModelsUpdate, {}) + events.emit(ModelEvent.OnModelsUpdate, { fetch: true }) setImportingModelSuccess(state.importId, state.modelId) }, [setImportingModelSuccess] diff --git a/web/hooks/useModels.test.ts b/web/hooks/useModels.test.ts index 9b6b898ad..f9c3b04b4 100644 --- a/web/hooks/useModels.test.ts +++ b/web/hooks/useModels.test.ts @@ -42,8 +42,9 @@ describe('useModels', () => { jest.spyOn(extensionManager, 'get').mockReturnValue(mockModelExtension) - act(() => { - renderHook(() => useModels()) + const { result } = renderHook(() => useModels()) + await act(() => { + result.current?.loadDataModel() }) expect(mockModelExtension.getModels).toHaveBeenCalled() diff --git a/web/hooks/useModels.ts b/web/hooks/useModels.ts index 400e02793..d2b05779f 100644 --- a/web/hooks/useModels.ts +++ b/web/hooks/useModels.ts @@ -9,7 +9,7 @@ import { ModelManager, } from '@janhq/core' -import { useSetAtom } from 'jotai' +import { useSetAtom, useAtom } from 'jotai' import { useDebouncedCallback } from 'use-debounce' @@ -27,7 +27,7 @@ import { * and updates the atoms accordingly. */ const useModels = () => { - const setDownloadedModels = useSetAtom(downloadedModelsAtom) + const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom) const setExtensionModels = useSetAtom(configuredModelsAtom) const getData = useCallback(() => { @@ -53,9 +53,11 @@ const useModels = () => { setDownloadedModels(toUpdate) let isUpdated = false + toUpdate.forEach((model) => { if (!ModelManager.instance().models.has(model.id)) { ModelManager.instance().models.set(model.id, model) + // eslint-disable-next-line react-hooks/exhaustive-deps isUpdated = true } }) @@ -75,21 +77,38 @@ const useModels = () => { const reloadData = useDebouncedCallback(() => getData(), 300) - useEffect(() => { - // Try get data on mount - reloadData() + const updateStates = useCallback(() => { + const cachedModels = ModelManager.instance().models.values().toArray() + const toUpdate = [ + ...downloadedModels, + ...cachedModels.filter( + (e: Model) => !downloadedModels.some((g: Model) => g.id === e.id) + ), + ] + setDownloadedModels(toUpdate) + }, [downloadedModels, setDownloadedModels]) + + const getModels = async (): Promise => + extensionManager + .get(ExtensionTypeEnum.Model) + ?.getModels() ?? [] + + useEffect(() => { // Listen for model updates - events.on(ModelEvent.OnModelsUpdate, async () => reloadData()) + events.on(ModelEvent.OnModelsUpdate, async (data: { fetch?: boolean }) => { + if (data.fetch) reloadData() + else updateStates() + }) return () => { // Remove listener on unmount events.off(ModelEvent.OnModelsUpdate, async () => {}) } - }, [getData, reloadData]) + }, [reloadData, updateStates]) + + return { + loadDataModel: getData, + } } -const getModels = async (): Promise => - extensionManager.get(ExtensionTypeEnum.Model)?.getModels() ?? - [] - export default useModels diff --git a/web/hooks/usePath.ts b/web/hooks/usePath.ts index 98e3009b4..b732926a6 100644 --- a/web/hooks/usePath.ts +++ b/web/hooks/usePath.ts @@ -42,39 +42,6 @@ export const usePath = () => { openFileExplorer(fullPath) } - const onViewJson = async (type: string) => { - // TODO: this logic should be refactored. - if (type !== 'Model' && !activeThread) return - - let filePath = undefined - const assistantId = activeThread?.assistants[0]?.assistant_id - switch (type) { - case 'Engine': - case 'Thread': - filePath = await joinPath([ - 'threads', - activeThread?.id ?? '', - 'thread.json', - ]) - break - case 'Model': - if (!selectedModel) return - filePath = await joinPath(['models', selectedModel.id, 'model.json']) - break - case 'Assistant': - case 'Tools': - if (!assistantId) return - filePath = await joinPath(['assistants', assistantId, 'assistant.json']) - break - default: - break - } - - if (!filePath) return - const fullPath = await joinPath([janDataFolderPath, filePath]) - openFileExplorer(fullPath) - } - const onViewFile = async (id: string) => { if (!activeThread) return @@ -99,7 +66,6 @@ export const usePath = () => { return { onRevealInFinder, - onViewJson, onViewFile, onViewFileContainer, }