feat: add factory reset feature (#1750)

* feat(FactoryReset): add factory reset feature

Signed-off-by: nam <namnh0122@gmail.com>
Signed-off-by: James <james@jan.ai>
Co-authored-by: Faisal Amir <urmauur@gmail.com>
Co-authored-by: James <james@jan.ai>
This commit is contained in:
NamH 2024-01-31 13:23:48 +07:00 committed by GitHub
parent 5e58f67abd
commit 8151ef0313
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 277 additions and 38 deletions

View File

@ -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',
}

View File

@ -83,6 +83,12 @@ const openExternalUrl: (url: string) => Promise<any> = (url) =>
*/
const getResourcePath: () => Promise<string> = () => global.core.api?.getResourcePath()
/**
* Gets the user's home path.
* @returns return user's home path
*/
const getUserHomePath = (): Promise<string> => global.core.api?.getUserHomePath()
/**
* Log to file from browser processes.
*
@ -127,5 +133,6 @@ export {
baseName,
log,
isSubdirectory,
getUserHomePath,
FileStat,
}

View File

@ -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) => {})
}

View File

@ -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,

View File

@ -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"
},

View File

@ -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",

View File

@ -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",

View File

@ -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;
}
}

View File

@ -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<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={twMerge('checkbox', className)}
{...props}
>
<CheckboxPrimitive.Indicator
className={twMerge(
'flex flex-shrink-0 items-center justify-center text-current'
)}
>
<CheckIcon className="checkbox--icon" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -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;
}
}

View File

@ -12,3 +12,4 @@ export * from './command'
export * from './textarea'
export * from './select'
export * from './slider'
export * from './checkbox'

View File

@ -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;

View File

@ -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,
}
}

View File

@ -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,

View File

@ -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])

View File

@ -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 (
<Modal
open={modalValidation}
onOpenChange={() => setModalValidation(false)}
>
<ModalPortal />
<ModalContent>
<ModalHeader>
<ModalTitle>
Are you sure you want to reset to default settings?
</ModalTitle>
</ModalHeader>
<p className="text-muted-foreground">
It will reset the application to its original state, deleting all your
usage data, including model customizations and conversation history.
This action is irreversible.
</p>
<div>
<p className="mb-2 mt-1 text-muted-foreground">{`To confirm, please enter the word "RESET" below:`}</p>
<Input
placeholder='Enter "RESET"'
onChange={(e) => setInputValue(e.target.value)}
/>
</div>
<div className="flex flex-shrink-0 items-start space-x-2">
<Checkbox
id="currentDirectory"
checked={currentDirectoryChecked}
onCheckedChange={(e) => setCurrentDirectoryChecked(Boolean(e))}
/>
<div className="mt-0.5 flex flex-col">
<label
htmlFor="currentDirectory"
className="cursor-pointer text-sm font-medium leading-none"
>
Keep the current app data location
</label>
<p className="mt-2 leading-relaxed">
Otherwise it will reset back to its original location at:
{/* TODO should be from system */}
<span className="font-medium">{defaultJanDataFolder}</span>
</p>
</div>
</div>
<ModalFooter>
<div className="flex gap-x-2">
<ModalClose asChild onClick={() => setModalValidation(false)}>
<Button themes="outline">Cancel</Button>
</ModalClose>
<ModalClose asChild>
<Button
autoFocus
themes="danger"
disabled={inputValue !== 'RESET'}
onClick={onFactoryResetClick}
>
Reset Now
</Button>
</ModalClose>
</div>
</ModalFooter>
</ModalContent>
</Modal>
)
}
export default ModalConfirmReset

View File

@ -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 (
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
<div className="w-4/5 flex-shrink-0 space-y-1.5">
<div className="flex gap-x-2">
<h6 className="text-sm font-semibold capitalize">
Reset to Factory Default
</h6>
</div>
<p className="whitespace-pre-wrap leading-relaxed">
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.
</p>
</div>
<Button
size="sm"
themes="secondaryDanger"
onClick={() => setModalValidation(true)}
>
Reset
</Button>
<ModalValidation />
</div>
)
}
export default FactoryReset

View File

@ -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<HTMLInputElement>) => {
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 = () => {
</div>
<Switch
checked={experimentalFeature}
onCheckedChange={(e) => {
if (e === true) {
setExperimentalFeature(true)
} else {
setExperimentalFeature(false)
}
}}
onCheckedChange={setExperimentalFeature}
/>
</div>
@ -119,7 +116,7 @@ const Advanced = () => {
</div>
<Switch
checked={gpuEnabled}
onCheckedChange={(e: boolean) => {
onCheckedChange={(e) => {
if (e === true) {
saveSettings({ runMode: 'gpu' })
setGpuEnabled(true)
@ -137,7 +134,7 @@ const Advanced = () => {
)}
{/* Directory */}
<DataFolder />
{experimentalFeature && <DataFolder />}
{/* Proxy */}
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
@ -170,16 +167,7 @@ const Advanced = () => {
certain proxies.
</p>
</div>
<Switch
checked={ignoreSSL}
onCheckedChange={(e) => {
if (e === true) {
setIgnoreSSL(true)
} else {
setIgnoreSSL(false)
}
}}
/>
<Switch checked={ignoreSSL} onCheckedChange={(e) => setIgnoreSSL(e)} />
</div>
{/* Open app directory */}
@ -206,7 +194,7 @@ const Advanced = () => {
</div>
)}
{/* Claer log */}
{/* Clear log */}
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
<div className="flex-shrink-0 space-y-1.5">
<div className="flex gap-x-2">
@ -218,6 +206,9 @@ const Advanced = () => {
Clear
</Button>
</div>
{/* Factory Reset */}
<FactoryReset />
</div>
)
}