From 8151ef031343e671b525b0b2f4f93544719ab8b5 Mon Sep 17 00:00:00 2001 From: NamH Date: Wed, 31 Jan 2024 13:23:48 +0700 Subject: [PATCH] feat: add factory reset feature (#1750) * feat(FactoryReset): add factory reset feature Signed-off-by: nam Signed-off-by: James Co-authored-by: Faisal Amir Co-authored-by: James --- core/src/api/index.ts | 2 +- core/src/core.ts | 7 ++ core/src/node/api/routes/fileManager.ts | 2 + electron/handlers/fileManager.ts | 6 +- electron/package.json | 2 +- .../inference-nitro-extension/package.json | 2 +- uikit/package.json | 1 + uikit/src/button/styles.scss | 8 +- uikit/src/checkbox/index.tsx | 29 ++++++ uikit/src/checkbox/styles.scss | 7 ++ uikit/src/index.ts | 1 + uikit/src/main.scss | 1 + web/hooks/useFactoryReset.ts | 59 +++++++++++ web/hooks/useSettings.ts | 7 +- web/screens/LocalServer/index.tsx | 6 +- .../FactoryReset/ModalConfirmReset.tsx | 99 +++++++++++++++++++ .../Settings/Advanced/FactoryReset/index.tsx | 37 +++++++ web/screens/Settings/Advanced/index.tsx | 39 +++----- 18 files changed, 277 insertions(+), 38 deletions(-) create mode 100644 uikit/src/checkbox/index.tsx create mode 100644 uikit/src/checkbox/styles.scss create mode 100644 web/hooks/useFactoryReset.ts create mode 100644 web/screens/Settings/Advanced/FactoryReset/ModalConfirmReset.tsx create mode 100644 web/screens/Settings/Advanced/FactoryReset/index.tsx diff --git a/core/src/api/index.ts b/core/src/api/index.ts index 0adc8b7e2..0d7cc51f7 100644 --- a/core/src/api/index.ts +++ b/core/src/api/index.ts @@ -3,7 +3,6 @@ * @description Enum of all the routes exposed by the app */ export enum AppRoute { - appDataPath = 'appDataPath', openExternalUrl = 'openExternalUrl', openAppDirectory = 'openAppDirectory', openFileExplore = 'openFileExplorer', @@ -62,6 +61,7 @@ export enum FileManagerRoute { syncFile = 'syncFile', getJanDataFolderPath = 'getJanDataFolderPath', getResourcePath = 'getResourcePath', + getUserHomePath = 'getUserHomePath', fileStat = 'fileStat', writeBlob = 'writeBlob', } diff --git a/core/src/core.ts b/core/src/core.ts index 24053e55c..8831c6001 100644 --- a/core/src/core.ts +++ b/core/src/core.ts @@ -83,6 +83,12 @@ const openExternalUrl: (url: string) => Promise = (url) => */ const getResourcePath: () => Promise = () => global.core.api?.getResourcePath() +/** + * Gets the user's home path. + * @returns return user's home path + */ +const getUserHomePath = (): Promise => global.core.api?.getUserHomePath() + /** * Log to file from browser processes. * @@ -127,5 +133,6 @@ export { baseName, log, isSubdirectory, + getUserHomePath, FileStat, } diff --git a/core/src/node/api/routes/fileManager.ts b/core/src/node/api/routes/fileManager.ts index 159c23a0c..66056444e 100644 --- a/core/src/node/api/routes/fileManager.ts +++ b/core/src/node/api/routes/fileManager.ts @@ -8,5 +8,7 @@ export const fsRouter = async (app: HttpServer) => { app.post(`/app/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) => {}) + app.post(`/app/${FileManagerRoute.getUserHomePath}`, async (request: any, reply: any) => {}) + app.post(`/app/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {}) } diff --git a/electron/handlers/fileManager.ts b/electron/handlers/fileManager.ts index 2528aef71..e328cb53b 100644 --- a/electron/handlers/fileManager.ts +++ b/electron/handlers/fileManager.ts @@ -1,4 +1,4 @@ -import { ipcMain } from 'electron' +import { ipcMain, app } from 'electron' // @ts-ignore import reflect from '@alumna/reflect' @@ -38,6 +38,10 @@ export function handleFileMangerIPCs() { getResourcePath() ) + ipcMain.handle(FileManagerRoute.getUserHomePath, async (_event) => + app.getPath('home') + ) + // handle fs is directory here ipcMain.handle( FileManagerRoute.fileStat, diff --git a/electron/package.json b/electron/package.json index 4ee9a19b4..2892fedc6 100644 --- a/electron/package.json +++ b/electron/package.json @@ -86,7 +86,7 @@ "request": "^2.88.2", "request-progress": "^3.0.0", "rimraf": "^5.0.5", - "typescript": "^5.3.3", + "typescript": "^5.2.2", "ulid": "^2.3.0", "use-debounce": "^9.0.4" }, diff --git a/extensions/inference-nitro-extension/package.json b/extensions/inference-nitro-extension/package.json index 44727eb70..8ad516ad9 100644 --- a/extensions/inference-nitro-extension/package.json +++ b/extensions/inference-nitro-extension/package.json @@ -35,7 +35,7 @@ "rollup-plugin-sourcemaps": "^0.6.3", "rollup-plugin-typescript2": "^0.36.0", "run-script-os": "^1.1.6", - "typescript": "^5.3.3" + "typescript": "^5.2.2" }, "dependencies": { "@janhq/core": "file:../../core", diff --git a/uikit/package.json b/uikit/package.json index 43e73dcf2..66f05840b 100644 --- a/uikit/package.json +++ b/uikit/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-context": "^1.0.1", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-icons": "^1.3.0", diff --git a/uikit/src/button/styles.scss b/uikit/src/button/styles.scss index 74585ed1e..003df5b4d 100644 --- a/uikit/src/button/styles.scss +++ b/uikit/src/button/styles.scss @@ -9,7 +9,7 @@ } &-secondary-blue { - @apply bg-blue-200 text-blue-600 hover:bg-blue-500/50; + @apply bg-blue-200 text-blue-600 hover:bg-blue-300/50 dark:hover:bg-blue-200/80; } &-danger { @@ -17,7 +17,7 @@ } &-secondary-danger { - @apply bg-red-200 text-red-600 hover:bg-red-500/50; + @apply bg-red-200 text-red-600 hover:bg-red-300/50 dark:hover:bg-red-200/80; } &-outline { @@ -67,14 +67,18 @@ [type='submit'] { &.btn-primary { @apply bg-primary hover:bg-primary/90; + @apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400; } &.btn-secondary { @apply bg-secondary hover:bg-secondary/80; + @apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400; } &.btn-secondary-blue { @apply bg-blue-200 text-blue-900 hover:bg-blue-200/80; + @apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400; } &.btn-danger { @apply bg-danger hover:bg-danger/90; + @apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400; } } diff --git a/uikit/src/checkbox/index.tsx b/uikit/src/checkbox/index.tsx new file mode 100644 index 000000000..1e78aeafb --- /dev/null +++ b/uikit/src/checkbox/index.tsx @@ -0,0 +1,29 @@ +'use client' + +import * as React from 'react' +import * as CheckboxPrimitive from '@radix-ui/react-checkbox' +import { CheckIcon } from '@radix-ui/react-icons' + +import { twMerge } from 'tailwind-merge' + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/uikit/src/checkbox/styles.scss b/uikit/src/checkbox/styles.scss new file mode 100644 index 000000000..33610f837 --- /dev/null +++ b/uikit/src/checkbox/styles.scss @@ -0,0 +1,7 @@ +.checkbox { + @apply border-border data-[state=checked]:bg-primary h-5 w-5 flex-shrink-0 rounded-md border data-[state=checked]:text-white; + + &--icon { + @apply h-4 w-4; + } +} diff --git a/uikit/src/index.ts b/uikit/src/index.ts index 3d5eaa82a..1b0a26bd1 100644 --- a/uikit/src/index.ts +++ b/uikit/src/index.ts @@ -12,3 +12,4 @@ export * from './command' export * from './textarea' export * from './select' export * from './slider' +export * from './checkbox' diff --git a/uikit/src/main.scss b/uikit/src/main.scss index 546f22811..c1326ba19 100644 --- a/uikit/src/main.scss +++ b/uikit/src/main.scss @@ -16,6 +16,7 @@ @import './textarea/styles.scss'; @import './select/styles.scss'; @import './slider/styles.scss'; +@import './checkbox/styles.scss'; .animate-spin { animation: spin 1s linear infinite; diff --git a/web/hooks/useFactoryReset.ts b/web/hooks/useFactoryReset.ts new file mode 100644 index 000000000..56994d4c4 --- /dev/null +++ b/web/hooks/useFactoryReset.ts @@ -0,0 +1,59 @@ +import { useEffect, useState } from 'react' + +import { fs, AppConfiguration, joinPath, getUserHomePath } from '@janhq/core' + +export default function useFactoryReset() { + const [defaultJanDataFolder, setdefaultJanDataFolder] = useState('') + + useEffect(() => { + async function getDefaultJanDataFolder() { + const homePath = await getUserHomePath() + const defaultJanDataFolder = await joinPath([homePath, 'jan']) + setdefaultJanDataFolder(defaultJanDataFolder) + } + getDefaultJanDataFolder() + }, []) + + const resetAll = async (keepCurrentFolder?: boolean) => { + // read the place of jan data folder + const appConfiguration: AppConfiguration | undefined = + await window.core?.api?.getAppConfigurations() + + if (!appConfiguration) { + console.debug('Failed to get app configuration') + } + + console.debug('appConfiguration: ', appConfiguration) + const janDataFolderPath = appConfiguration!.data_folder + + if (defaultJanDataFolder === janDataFolderPath) { + console.debug('Jan data folder is already at user home') + } else { + // if jan data folder is not at user home, we update the app configuration to point to user home + if (!keepCurrentFolder) { + const configuration: AppConfiguration = { + data_folder: defaultJanDataFolder, + } + await window.core?.api?.updateAppConfiguration(configuration) + } + } + + const modelPath = await joinPath([janDataFolderPath, 'models']) + const threadPath = await joinPath([janDataFolderPath, 'threads']) + + console.debug(`Removing models at ${modelPath}`) + await fs.rmdirSync(modelPath, { recursive: true }) + + console.debug(`Removing threads at ${threadPath}`) + await fs.rmdirSync(threadPath, { recursive: true }) + + // reset the localStorage + localStorage.clear() + await window.core?.api?.relaunch() + } + + return { + defaultJanDataFolder, + resetAll, + } +} diff --git a/web/hooks/useSettings.ts b/web/hooks/useSettings.ts index ef4e08480..168e72489 100644 --- a/web/hooks/useSettings.ts +++ b/web/hooks/useSettings.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { fs, joinPath } from '@janhq/core' import { atom, useAtom } from 'jotai' @@ -32,7 +32,7 @@ export const useSettings = () => { }) } - const readSettings = async () => { + const readSettings = useCallback(async () => { if (!window?.core?.api) { return } @@ -42,7 +42,8 @@ export const useSettings = () => { return typeof settings === 'object' ? settings : JSON.parse(settings) } return {} - } + }, []) + const saveSettings = async ({ runMode, notify, diff --git a/web/screens/LocalServer/index.tsx b/web/screens/LocalServer/index.tsx index d75274f16..7e1ba1fab 100644 --- a/web/screens/LocalServer/index.tsx +++ b/web/screens/LocalServer/index.tsx @@ -91,11 +91,7 @@ const LocalServerScreen = () => { } useEffect(() => { - if ( - localStorage.getItem(FIRST_TIME_VISIT_API_SERVER) === null || - localStorage.getItem(FIRST_TIME_VISIT_API_SERVER) === 'true' - ) { - localStorage.setItem(FIRST_TIME_VISIT_API_SERVER, 'true') + if (localStorage.getItem(FIRST_TIME_VISIT_API_SERVER) == null) { setFirstTimeVisitAPIServer(true) } }, [firstTimeVisitAPIServer]) diff --git a/web/screens/Settings/Advanced/FactoryReset/ModalConfirmReset.tsx b/web/screens/Settings/Advanced/FactoryReset/ModalConfirmReset.tsx new file mode 100644 index 000000000..d8a2321a9 --- /dev/null +++ b/web/screens/Settings/Advanced/FactoryReset/ModalConfirmReset.tsx @@ -0,0 +1,99 @@ +import React, { useCallback, useEffect, useState } from 'react' + +import { fs, AppConfiguration, joinPath, getUserHomePath } from '@janhq/core' + +import { + Modal, + ModalPortal, + ModalContent, + ModalHeader, + ModalTitle, + ModalFooter, + ModalClose, + Button, + Checkbox, + Input, +} from '@janhq/uikit' +import { atom, useAtom } from 'jotai' + +import useFactoryReset from '@/hooks/useFactoryReset' + +export const modalValidationAtom = atom(false) + +const ModalConfirmReset = () => { + const [modalValidation, setModalValidation] = useAtom(modalValidationAtom) + const { resetAll, defaultJanDataFolder } = useFactoryReset() + const [inputValue, setInputValue] = useState('') + const [currentDirectoryChecked, setCurrentDirectoryChecked] = useState(true) + const onFactoryResetClick = useCallback( + () => resetAll(currentDirectoryChecked), + [currentDirectoryChecked, resetAll] + ) + + return ( + setModalValidation(false)} + > + + + + + Are you sure you want to reset to default settings? + + +

+ It will reset the application to its original state, deleting all your + usage data, including model customizations and conversation history. + This action is irreversible. +

+
+

{`To confirm, please enter the word "RESET" below:`}

+ setInputValue(e.target.value)} + /> +
+
+ setCurrentDirectoryChecked(Boolean(e))} + /> +
+ +

+ Otherwise it will reset back to its original location at: + {/* TODO should be from system */} + {defaultJanDataFolder} +

+
+
+ +
+ setModalValidation(false)}> + + + + + +
+
+
+
+ ) +} + +export default ModalConfirmReset diff --git a/web/screens/Settings/Advanced/FactoryReset/index.tsx b/web/screens/Settings/Advanced/FactoryReset/index.tsx new file mode 100644 index 000000000..e7b1e2995 --- /dev/null +++ b/web/screens/Settings/Advanced/FactoryReset/index.tsx @@ -0,0 +1,37 @@ +import { Button } from '@janhq/uikit' + +import { useSetAtom } from 'jotai' + +import ModalValidation, { modalValidationAtom } from './ModalConfirmReset' + +const FactoryReset = () => { + const setModalValidation = useSetAtom(modalValidationAtom) + + return ( +
+
+
+
+ Reset to Factory Default +
+
+

+ Reset the application to its original state, deleting all your usage + data, including model customizations and conversation history. This + action is irreversible and recommended only if the application is in a + corrupted state. +

+
+ + +
+ ) +} + +export default FactoryReset diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 5c85a0e1e..df92afdd4 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-hooks/exhaustive-deps */ 'use client' import { @@ -21,6 +20,7 @@ import { FeatureToggleContext } from '@/context/FeatureToggle' import { useSettings } from '@/hooks/useSettings' import DataFolder from './DataFolder' +import FactoryReset from './FactoryReset' const Advanced = () => { const { @@ -36,6 +36,7 @@ const Advanced = () => { const { readSettings, saveSettings, validateSettings, setShowNotification } = useSettings() + const onProxyChange = useCallback( (event: ChangeEvent) => { const value = event.target.value || '' @@ -50,10 +51,12 @@ const Advanced = () => { ) useEffect(() => { - readSettings().then((settings) => { + const setUseGpuIfPossible = async () => { + const settings = await readSettings() setGpuEnabled(settings.run_mode === 'gpu') - }) - }, []) + } + setUseGpuIfPossible() + }, [readSettings]) const clearLogs = async () => { if (await fs.existsSync(`file://logs`)) { @@ -96,13 +99,7 @@ const Advanced = () => { { - if (e === true) { - setExperimentalFeature(true) - } else { - setExperimentalFeature(false) - } - }} + onCheckedChange={setExperimentalFeature} /> @@ -119,7 +116,7 @@ const Advanced = () => { { + onCheckedChange={(e) => { if (e === true) { saveSettings({ runMode: 'gpu' }) setGpuEnabled(true) @@ -137,7 +134,7 @@ const Advanced = () => { )} {/* Directory */} - + {experimentalFeature && } {/* Proxy */}
@@ -170,16 +167,7 @@ const Advanced = () => { certain proxies.

- { - if (e === true) { - setIgnoreSSL(true) - } else { - setIgnoreSSL(false) - } - }} - /> + setIgnoreSSL(e)} /> {/* Open app directory */} @@ -206,7 +194,7 @@ const Advanced = () => { )} - {/* Claer log */} + {/* Clear log */}
@@ -218,6 +206,9 @@ const Advanced = () => { Clear
+ + {/* Factory Reset */} +
) }