diff --git a/electron/main.ts b/electron/main.ts index 2affe9a55..9e50ae228 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -212,6 +212,15 @@ function handleIPCs() { return app.getVersion(); }); + /** + * Handles the "openAppDirectory" IPC message by opening the app's user data directory. + * The `shell.openPath` method is used to open the directory in the user's default file explorer. + * @param _event - The IPC event object. + */ + ipcMain.handle("openAppDirectory", async (_event) => { + shell.openPath(app.getPath("userData")); + }); + /** * Opens a URL in the user's default browser. * @param _event - The IPC event object. diff --git a/electron/preload.ts b/electron/preload.ts index 14815bb67..84fd0d69b 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -28,6 +28,8 @@ contextBridge.exposeInMainWorld("electronAPI", { relaunch: () => ipcRenderer.invoke("relaunch"), + openAppDirectory: () => ipcRenderer.invoke("openAppDirectory"), + deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath), installRemotePlugin: (pluginName: string) => diff --git a/electron/tests/navigation.e2e.spec.ts b/electron/tests/navigation.e2e.spec.ts index 90220d5db..104333650 100644 --- a/electron/tests/navigation.e2e.spec.ts +++ b/electron/tests/navigation.e2e.spec.ts @@ -40,8 +40,12 @@ test("renders left navigation panel", async () => { expect(chatSection).toBe(false); // Home actions - const botBtn = await page.getByTestId("Bot").first().isEnabled(); + /* Disable unstable feature tests + ** const botBtn = await page.getByTestId("Bot").first().isEnabled(); + ** Enable back when it is whitelisted + */ + const myModelsBtn = await page.getByTestId("My Models").first().isEnabled(); const settingsBtn = await page.getByTestId("Settings").first().isEnabled(); - expect([botBtn, myModelsBtn, settingsBtn].filter((e) => !e).length).toBe(0); + expect([myModelsBtn, settingsBtn].filter((e) => !e).length).toBe(0); }); diff --git a/web/app/_components/LeftHeaderAction/index.tsx b/web/app/_components/LeftHeaderAction/index.tsx index e8e978bc5..f24f41122 100644 --- a/web/app/_components/LeftHeaderAction/index.tsx +++ b/web/app/_components/LeftHeaderAction/index.tsx @@ -1,6 +1,6 @@ 'use client' -import React from 'react' +import React, { useContext } from 'react' import SecondaryButton from '../SecondaryButton' import { useSetAtom, useAtomValue } from 'jotai' import { @@ -13,6 +13,9 @@ import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels' import { Button } from '@uikit' import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom' import { showingModalNoActiveModel } from '@helpers/atoms/Modal.atom' +import { + FeatureToggleContext, +} from '@helpers/FeatureToggleWrapper' const LeftHeaderAction: React.FC = () => { const setMainView = useSetAtom(setMainViewStateAtom) @@ -20,6 +23,7 @@ const LeftHeaderAction: React.FC = () => { const activeModel = useAtomValue(activeAssistantModelAtom) const { requestCreateConvo } = useCreateConversation() const setShowModalNoActiveModel = useSetAtom(showingModalNoActiveModel) + const { experimentalFeatureEnabed } = useContext(FeatureToggleContext) const onExploreClick = () => { setMainView(MainViewState.ExploreModel) @@ -50,12 +54,14 @@ const LeftHeaderAction: React.FC = () => { className="w-full flex-1" icon={} /> - } - /> + {experimentalFeatureEnabed && ( + } + /> + )} - {isActive ? ( - - ) : null} - - ) - })} + {menu.icon} + + {menu.name} + + + {isActive ? ( + + ) : null} + + ) + })} {
+
+ + )} + + ) +} + +export default Advanced diff --git a/web/screens/Settings/CorePlugins/PluginsCatalog.tsx b/web/screens/Settings/CorePlugins/PluginsCatalog.tsx index c5c094252..48b64bb72 100644 --- a/web/screens/Settings/CorePlugins/PluginsCatalog.tsx +++ b/web/screens/Settings/CorePlugins/PluginsCatalog.tsx @@ -1,24 +1,22 @@ 'use client' -import React, { useState, useEffect, useRef } from 'react' +import React, { useState, useEffect, useRef, useContext } from 'react' import { Button, Switch } from '@uikit' import Loader from '@containers/Loader' import { formatPluginsName } from '@utils/converter' import { plugins, extensionPoints } from '@plugin' -import { executeSerial } from '@services/pluginService' -import { DataService } from '@janhq/core' import useGetAppVersion from '@hooks/useGetAppVersion' +import { FeatureToggleContext } from '@helpers/FeatureToggleWrapper' const PluginCatalog = () => { - // const [search, setSearch] = useState('') - // const [fileName, setFileName] = useState('') const [activePlugins, setActivePlugins] = useState([]) const [pluginCatalog, setPluginCatalog] = useState([]) const [isLoading, setIsLoading] = useState(false) const experimentRef = useRef(null) const fileInputRef = useRef(null) const { version } = useGetAppVersion() + const { experimentalFeatureEnabed } = useContext(FeatureToggleContext) /** * Loads the plugin catalog module from a CDN and sets it as the plugin catalog state. */ @@ -28,33 +26,13 @@ const PluginCatalog = () => { } if (!version) return - // Load plugin manifest from plugin if any - if (extensionPoints.get(DataService.GetPluginManifest)) { - executeSerial(DataService.GetPluginManifest).then((data: any) => { - setPluginCatalog( - data.filter( - (e: any) => - !e.requiredVersion || - e.requiredVersion.replace(/[.^]/g, '') <= - version.replaceAll('.', '') - ) - ) - }) - } else { - // Fallback to app default manifest - import( - /* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}` - ).then((data) => - setPluginCatalog( - data.default.filter( - (e: any) => - !e.requiredVersion || - e.requiredVersion.replace(/[.^]/g, '') <= - version.replaceAll('.', '') - ) - ) - ) - } + // Get plugin manifest + import(/* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}`).then( + (data) => { + if (Array.isArray(data.default) && experimentalFeatureEnabed) + setPluginCatalog(data.default) + } + ) }, [version]) /** @@ -67,7 +45,7 @@ const PluginCatalog = () => { useEffect(() => { const getActivePlugins = async () => { const plgs = await plugins.getActive() - setActivePlugins(plgs) + if (Array.isArray(plgs)) setActivePlugins(plgs) if (extensionPoints.get('experimentComponent')) { const components = await Promise.all( @@ -111,20 +89,6 @@ const PluginCatalog = () => { if (res) window.coreAPI?.relaunch() } - /** - * Updates a plugin by calling the `window.pluggableElectronIpc.update` function with the plugin name. - * If the update is successful, the application is relaunched using the `window.coreAPI.relaunch` function. - * TODO: should update using window.coreAPI rather than pluggableElectronIpc (Plugin Manager Facades) - * @param plugin - The name of the plugin to update. - */ - const update = async (plugin: string) => { - if (typeof window !== 'undefined') { - // @ts-ignore - await window.pluggableElectronIpc.update([plugin], true) - window.coreAPI?.relaunch() - } - } - /** * Downloads a remote plugin tarball and installs it using the `plugins.install` function. * If the installation is successful, the application is relaunched using the `coreAPI.relaunch` function. @@ -152,9 +116,9 @@ const PluginCatalog = () => { return (
- {(pluginCatalog ?? []) + {pluginCatalog .concat( - activePlugins?.filter( + activePlugins.filter( (e) => !(pluginCatalog ?? []).some((p) => p.name === e.name) ) ?? [] ) diff --git a/web/screens/Settings/index.tsx b/web/screens/Settings/index.tsx index 1f0c23e5f..eb800ca19 100644 --- a/web/screens/Settings/index.tsx +++ b/web/screens/Settings/index.tsx @@ -14,17 +14,23 @@ import { twMerge } from 'tailwind-merge' import { formatPluginsName } from '@utils/converter' import { extensionPoints } from '@plugin' - -const staticMenu = ['Appearance'] - -if (typeof window !== "undefined" && window.electronAPI) { - staticMenu.push('Core Plugins'); -} +import Advanced from './Advanced' const SettingsScreen = () => { const [activeStaticMenu, setActiveStaticMenu] = useState('Appearance') const [preferenceItems, setPreferenceItems] = useState([]) const [preferenceValues, setPreferenceValues] = useState([]) + const [menus, setMenus] = useState([]) + + useEffect(() => { + const menu = ['Appearance'] + + if (typeof window !== 'undefined' && window.electronAPI) { + menu.push('Core Plugins') + } + menu.push('Advanced') + setMenus(menu) + }, []) /** * Fetches the active plugins and their preferences from the `plugins` and `preferences` modules. @@ -72,6 +78,9 @@ const SettingsScreen = () => { case 'Appearance': return + case 'Advanced': + return + default: return ( { Options
- {staticMenu.map((menu, i) => { + {menus.map((menu, i) => { const isActive = activeStaticMenu === menu return (