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:
parent
5e58f67abd
commit
8151ef0313
@ -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',
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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) => {})
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
29
uikit/src/checkbox/index.tsx
Normal file
29
uikit/src/checkbox/index.tsx
Normal 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 }
|
||||
7
uikit/src/checkbox/styles.scss
Normal file
7
uikit/src/checkbox/styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -12,3 +12,4 @@ export * from './command'
|
||||
export * from './textarea'
|
||||
export * from './select'
|
||||
export * from './slider'
|
||||
export * from './checkbox'
|
||||
|
||||
@ -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;
|
||||
|
||||
59
web/hooks/useFactoryReset.ts
Normal file
59
web/hooks/useFactoryReset.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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
|
||||
37
web/screens/Settings/Advanced/FactoryReset/index.tsx
Normal file
37
web/screens/Settings/Advanced/FactoryReset/index.tsx
Normal 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
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user