From a6f2de922c566f68f56cc60e790d3df1f2f88923 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 18 Nov 2024 16:10:57 +0700 Subject: [PATCH 1/6] chore: refactor rerender components --- electron/managers/window.ts | 6 +++++ web/containers/Providers/DataLoader.tsx | 16 ++++++++++-- web/hooks/useModels.ts | 34 +++++++++++++++---------- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/electron/managers/window.ts b/electron/managers/window.ts index c9c43ea77..c89e1d7c4 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,11 @@ class WindowManager { windowManager.hideMainWindow() } }) + + windowManager.mainWindow?.on('ready-to-show', function () { + // Feature Toggle for Quick Ask + windowManager.mainWindow?.show() + }) } createQuickAskWindow(preloadPath: string, startUrl: string): void { diff --git a/web/containers/Providers/DataLoader.tsx b/web/containers/Providers/DataLoader.tsx index 4319c5eed..fdd7caae2 100644 --- a/web/containers/Providers/DataLoader.tsx +++ b/web/containers/Providers/DataLoader.tsx @@ -2,7 +2,12 @@ import { Fragment, ReactNode, useEffect } from 'react' -import { AppConfiguration, getUserHomePath } from '@janhq/core' +import { + AppConfiguration, + events, + getUserHomePath, + ModelEvent, +} from '@janhq/core' import { useSetAtom } from 'jotai' import useAssistants from '@/hooks/useAssistants' @@ -30,12 +35,19 @@ const DataLoader: React.FC = ({ children }) => { const setJanDefaultDataFolder = useSetAtom(defaultJanDataFolderAtom) const setJanSettingScreen = useSetAtom(janSettingScreenAtom) - useModels() useThreads() useAssistants() useGetSystemResources() useLoadTheme() + const { loadDataModel, isUpdated } = useModels() + useEffect(() => { + // Listen for model updates + if (isUpdated) { + loadDataModel() + } + }, [isUpdated, loadDataModel]) + useEffect(() => { window.core?.api ?.getAppConfigurations() diff --git a/web/hooks/useModels.ts b/web/hooks/useModels.ts index 400e02793..3f67faaeb 100644 --- a/web/hooks/useModels.ts +++ b/web/hooks/useModels.ts @@ -29,6 +29,7 @@ import { const useModels = () => { const setDownloadedModels = useSetAtom(downloadedModelsAtom) const setExtensionModels = useSetAtom(configuredModelsAtom) + let isUpdated = false const getData = useCallback(() => { const getDownloadedModels = async () => { @@ -52,10 +53,10 @@ 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 +76,28 @@ const useModels = () => { const reloadData = useDebouncedCallback(() => getData(), 300) + const getModels = async (): Promise => + extensionManager + .get(ExtensionTypeEnum.Model) + ?.getModels() ?? [] + useEffect(() => { // Try get data on mount - reloadData() - - // Listen for model updates - events.on(ModelEvent.OnModelsUpdate, async () => reloadData()) - return () => { - // Remove listener on unmount - events.off(ModelEvent.OnModelsUpdate, async () => {}) + if (isUpdated) { + reloadData() + // Listen for model updates + events.on(ModelEvent.OnModelsUpdate, async () => reloadData()) + return () => { + // Remove listener on unmount + events.off(ModelEvent.OnModelsUpdate, async () => {}) + } } - }, [getData, reloadData]) + }, [getData, isUpdated, reloadData]) + + return { + loadDataModel: getData, + isUpdated: isUpdated, + } } -const getModels = async (): Promise => - extensionManager.get(ExtensionTypeEnum.Model)?.getModels() ?? - [] - export default useModels From 18e9a2e4d8dd5c6c300c2ee0fbb9e507270067b9 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 18 Nov 2024 16:17:31 +0700 Subject: [PATCH 2/6] chore: remove get data on mount --- web/containers/Providers/DataLoader.tsx | 7 +------ web/hooks/useModels.ts | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/web/containers/Providers/DataLoader.tsx b/web/containers/Providers/DataLoader.tsx index fdd7caae2..8faa87865 100644 --- a/web/containers/Providers/DataLoader.tsx +++ b/web/containers/Providers/DataLoader.tsx @@ -2,12 +2,7 @@ import { Fragment, ReactNode, useEffect } from 'react' -import { - AppConfiguration, - events, - getUserHomePath, - ModelEvent, -} from '@janhq/core' +import { AppConfiguration, getUserHomePath } from '@janhq/core' import { useSetAtom } from 'jotai' import useAssistants from '@/hooks/useAssistants' diff --git a/web/hooks/useModels.ts b/web/hooks/useModels.ts index 3f67faaeb..684ac2729 100644 --- a/web/hooks/useModels.ts +++ b/web/hooks/useModels.ts @@ -84,7 +84,6 @@ const useModels = () => { useEffect(() => { // Try get data on mount if (isUpdated) { - reloadData() // Listen for model updates events.on(ModelEvent.OnModelsUpdate, async () => reloadData()) return () => { From 5c512ae7b4a022de15f0a9a05a3d9f5468080a64 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 18 Nov 2024 16:31:50 +0700 Subject: [PATCH 3/6] chore: remove commented code --- electron/managers/window.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/electron/managers/window.ts b/electron/managers/window.ts index c89e1d7c4..918036365 100644 --- a/electron/managers/window.ts +++ b/electron/managers/window.ts @@ -81,7 +81,6 @@ class WindowManager { }) windowManager.mainWindow?.on('ready-to-show', function () { - // Feature Toggle for Quick Ask windowManager.mainWindow?.show() }) } From b8e521164b0e18dc01eda24dc34c418393bf7641 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 18 Nov 2024 16:48:27 +0700 Subject: [PATCH 4/6] chore: remove conditional on dataloader --- web/containers/Providers/DataLoader.tsx | 4 +--- web/hooks/useModels.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/web/containers/Providers/DataLoader.tsx b/web/containers/Providers/DataLoader.tsx index 8faa87865..ed4c07ec3 100644 --- a/web/containers/Providers/DataLoader.tsx +++ b/web/containers/Providers/DataLoader.tsx @@ -38,9 +38,7 @@ const DataLoader: React.FC = ({ children }) => { const { loadDataModel, isUpdated } = useModels() useEffect(() => { // Listen for model updates - if (isUpdated) { - loadDataModel() - } + loadDataModel() }, [isUpdated, loadDataModel]) useEffect(() => { diff --git a/web/hooks/useModels.ts b/web/hooks/useModels.ts index 684ac2729..0aed91ed2 100644 --- a/web/hooks/useModels.ts +++ b/web/hooks/useModels.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react' +import { useCallback, useEffect, useRef } from 'react' import { ExtensionTypeEnum, @@ -29,10 +29,15 @@ import { const useModels = () => { const setDownloadedModels = useSetAtom(downloadedModelsAtom) const setExtensionModels = useSetAtom(configuredModelsAtom) + const hasFetchedDownloadedModels = useRef(false) // Track whether the function has been executed + let isUpdated = false const getData = useCallback(() => { + if (hasFetchedDownloadedModels.current) return + const getDownloadedModels = async () => { + hasFetchedDownloadedModels.current = true const localModels = (await getModels()).map((e) => ({ ...e, name: ModelManager.instance().models.get(e.id)?.name ?? e.id, @@ -72,7 +77,7 @@ const useModels = () => { // Fetch all data getExtensionModels() getDownloadedModels() - }, [setDownloadedModels, setExtensionModels]) + }, []) const reloadData = useDebouncedCallback(() => getData(), 300) @@ -91,7 +96,7 @@ const useModels = () => { events.off(ModelEvent.OnModelsUpdate, async () => {}) } } - }, [getData, isUpdated, reloadData]) + }, [isUpdated, reloadData]) return { loadDataModel: getData, From e9fd7f4554f88dadd6d662150138158184894ee7 Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 18 Nov 2024 18:22:09 +0700 Subject: [PATCH 5/6] fix: models load --- core/src/browser/extension.ts | 1 - .../inference-cortex-extension/src/index.ts | 4 +- extensions/model-extension/src/index.ts | 33 ++++++------- web/containers/Providers/DataLoader.tsx | 7 +-- web/containers/Providers/EventListener.tsx | 2 +- .../Providers/ModelImportListener.tsx | 2 +- web/hooks/useModels.ts | 47 +++++++++++-------- 7 files changed, 53 insertions(+), 43 deletions(-) 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/extensions/inference-cortex-extension/src/index.ts b/extensions/inference-cortex-extension/src/index.ts index e83a17561..34a376ac8 100644 --- a/extensions/inference-cortex-extension/src/index.ts +++ b/extensions/inference-cortex-extension/src/index.ts @@ -215,7 +215,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 ed4c07ec3..d3d747d02 100644 --- a/web/containers/Providers/DataLoader.tsx +++ b/web/containers/Providers/DataLoader.tsx @@ -29,17 +29,18 @@ const DataLoader: React.FC = ({ children }) => { const setQuickAskEnabled = useSetAtom(quickAskEnabledAtom) const setJanDefaultDataFolder = useSetAtom(defaultJanDataFolderAtom) const setJanSettingScreen = useSetAtom(janSettingScreenAtom) + const { loadDataModel } = useModels() useThreads() useAssistants() useGetSystemResources() useLoadTheme() - const { loadDataModel, isUpdated } = useModels() useEffect(() => { - // Listen for model updates + // Load data once loadDataModel() - }, [isUpdated, loadDataModel]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) useEffect(() => { window.core?.api 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.ts b/web/hooks/useModels.ts index 0aed91ed2..d2b05779f 100644 --- a/web/hooks/useModels.ts +++ b/web/hooks/useModels.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from 'react' +import { useCallback, useEffect } from 'react' import { ExtensionTypeEnum, @@ -9,7 +9,7 @@ import { ModelManager, } from '@janhq/core' -import { useSetAtom } from 'jotai' +import { useSetAtom, useAtom } from 'jotai' import { useDebouncedCallback } from 'use-debounce' @@ -27,17 +27,11 @@ import { * and updates the atoms accordingly. */ const useModels = () => { - const setDownloadedModels = useSetAtom(downloadedModelsAtom) + const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelsAtom) const setExtensionModels = useSetAtom(configuredModelsAtom) - const hasFetchedDownloadedModels = useRef(false) // Track whether the function has been executed - - let isUpdated = false const getData = useCallback(() => { - if (hasFetchedDownloadedModels.current) return - const getDownloadedModels = async () => { - hasFetchedDownloadedModels.current = true const localModels = (await getModels()).map((e) => ({ ...e, name: ModelManager.instance().models.get(e.id)?.name ?? e.id, @@ -58,6 +52,8 @@ const useModels = () => { setDownloadedModels(toUpdate) + let isUpdated = false + toUpdate.forEach((model) => { if (!ModelManager.instance().models.has(model.id)) { ModelManager.instance().models.set(model.id, model) @@ -77,30 +73,41 @@ const useModels = () => { // Fetch all data getExtensionModels() getDownloadedModels() - }, []) + }, [setDownloadedModels, setExtensionModels]) const reloadData = useDebouncedCallback(() => getData(), 300) + 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(() => { - // Try get data on mount - if (isUpdated) { - // Listen for model updates - events.on(ModelEvent.OnModelsUpdate, async () => reloadData()) - return () => { - // Remove listener on unmount - events.off(ModelEvent.OnModelsUpdate, async () => {}) - } + // Listen for model updates + events.on(ModelEvent.OnModelsUpdate, async (data: { fetch?: boolean }) => { + if (data.fetch) reloadData() + else updateStates() + }) + return () => { + // Remove listener on unmount + events.off(ModelEvent.OnModelsUpdate, async () => {}) } - }, [isUpdated, reloadData]) + }, [reloadData, updateStates]) return { loadDataModel: getData, - isUpdated: isUpdated, } } From 5c5239097de9ffa03cd785e72887bc2a1711781f Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 18 Nov 2024 19:51:49 +0700 Subject: [PATCH 6/6] chore: update test --- web/hooks/useModels.test.ts | 5 +++-- web/hooks/usePath.ts | 34 ---------------------------------- 2 files changed, 3 insertions(+), 36 deletions(-) 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/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, }