feat: update UI allow user change folder (#1738)

* feat: wip ui jan folder setting

* change input disabled

* finished change directory jan folder

* fix overlap value input current path folder

* make app reload to latest page

* fix: add experimental feature toggle til the next release

---------

Co-authored-by: Louis <louis@jan.ai>
This commit is contained in:
Faisal Amir 2024-01-24 22:13:58 +07:00 committed by GitHub
parent 1b794b5337
commit 6ba48bc1e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 398 additions and 145 deletions

View File

@ -14,6 +14,7 @@ const buttonVariants = cva('btn', {
outline: 'btn-outline',
secondary: 'btn-secondary',
secondaryBlue: 'btn-secondary-blue',
secondaryDanger: 'btn-secondary-danger',
ghost: 'btn-ghost',
success: 'btn-success',
},

View File

@ -9,13 +9,17 @@
}
&-secondary-blue {
@apply bg-blue-200 text-blue-600 hover:bg-blue-500/80;
@apply bg-blue-200 text-blue-600 hover:bg-blue-500/50;
}
&-danger {
@apply bg-danger text-danger-foreground hover:bg-danger/90;
}
&-secondary-danger {
@apply bg-red-200 text-red-600 hover:bg-red-500/50;
}
&-outline {
@apply border-input border bg-transparent;
}

View File

@ -1,6 +1,6 @@
.input {
@apply border-border placeholder:text-muted-foreground flex h-9 w-full rounded-lg border bg-transparent px-3 py-1 transition-colors;
@apply disabled:cursor-not-allowed disabled:opacity-50;
@apply disabled:cursor-not-allowed disabled:bg-zinc-100;
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
@apply file:border-0 file:bg-transparent file:font-medium;
}

View File

@ -9,11 +9,14 @@ import RibbonNav from '@/containers/Layout/Ribbon'
import TopBar from '@/containers/Layout/TopBar'
import { MainViewState } from '@/constants/screens'
import { useMainViewState } from '@/hooks/useMainViewState'
import { SUCCESS_SET_NEW_DESTINATION } from '@/hooks/useVaultDirectory'
const BaseLayout = (props: PropsWithChildren) => {
const { children } = props
const { mainViewState } = useMainViewState()
const { mainViewState, setMainViewState } = useMainViewState()
const { theme, setTheme } = useTheme()
@ -21,6 +24,12 @@ const BaseLayout = (props: PropsWithChildren) => {
setTheme(theme as string)
}, [setTheme, theme])
useEffect(() => {
if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') {
setMainViewState(MainViewState.Settings)
}
}, [])
return (
<div className="flex h-screen w-screen flex-1 overflow-hidden">
<RibbonNav />

View File

@ -50,10 +50,12 @@ const availableShortcuts = [
const ShortcutModal: React.FC = () => (
<Modal>
<ModalTrigger asChild>
<Button size="sm" themes="secondary">
Show
</Button>
<ModalTrigger>
<div>
<Button size="sm" themes="secondaryBlue">
Show
</Button>
</div>
</ModalTrigger>
<ModalContent className="max-w-2xl">
<ModalHeader>

View File

@ -0,0 +1,105 @@
import { useEffect } from 'react'
import { fs, AppConfiguration } from '@janhq/core'
import { atom, useAtom } from 'jotai'
import { useMainViewState } from './useMainViewState'
const isSameDirectoryAtom = atom(false)
const isDirectoryConfirmAtom = atom(false)
const isErrorSetNewDestAtom = atom(false)
const currentPathAtom = atom('')
const newDestinationPathAtom = atom('')
export const SUCCESS_SET_NEW_DESTINATION = 'successSetNewDestination'
export function useVaultDirectory() {
const [isSameDirectory, setIsSameDirectory] = useAtom(isSameDirectoryAtom)
const { setMainViewState } = useMainViewState()
const [isDirectoryConfirm, setIsDirectoryConfirm] = useAtom(
isDirectoryConfirmAtom
)
const [isErrorSetNewDest, setIsErrorSetNewDest] = useAtom(
isErrorSetNewDestAtom
)
const [currentPath, setCurrentPath] = useAtom(currentPathAtom)
const [newDestinationPath, setNewDestinationPath] = useAtom(
newDestinationPathAtom
)
useEffect(() => {
window.core?.api
?.getAppConfigurations()
?.then((appConfig: AppConfiguration) => {
setCurrentPath(appConfig.data_folder)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const setNewDestination = async () => {
const destFolder = await window.core?.api?.selectDirectory()
setNewDestinationPath(destFolder)
if (destFolder) {
console.debug(`Destination folder selected: ${destFolder}`)
try {
const appConfiguration: AppConfiguration =
await window.core?.api?.getAppConfigurations()
const currentJanDataFolder = appConfiguration.data_folder
if (currentJanDataFolder === destFolder) {
console.debug(
`Destination folder is the same as current folder. Ignore..`
)
setIsSameDirectory(true)
setIsDirectoryConfirm(false)
return
} else {
setIsSameDirectory(false)
setIsDirectoryConfirm(true)
}
setIsErrorSetNewDest(false)
} catch (e) {
console.error(`Error: ${e}`)
setIsErrorSetNewDest(true)
}
}
}
const applyNewDestination = async () => {
try {
const appConfiguration: AppConfiguration =
await window.core?.api?.getAppConfigurations()
const currentJanDataFolder = appConfiguration.data_folder
appConfiguration.data_folder = newDestinationPath
await fs.syncFile(currentJanDataFolder, newDestinationPath)
await window.core?.api?.updateAppConfiguration(appConfiguration)
console.debug(
`File sync finished from ${currentPath} to ${newDestinationPath}`
)
setIsErrorSetNewDest(false)
localStorage.setItem(SUCCESS_SET_NEW_DESTINATION, 'true')
await window.core?.api?.relaunch()
} catch (e) {
console.error(`Error: ${e}`)
setIsErrorSetNewDest(true)
}
}
return {
setNewDestination,
newDestinationPath,
applyNewDestination,
isSameDirectory,
setIsDirectoryConfirm,
isDirectoryConfirm,
setIsSameDirectory,
currentPath,
isErrorSetNewDest,
setIsErrorSetNewDest,
}
}

View File

@ -0,0 +1,57 @@
import React from 'react'
import {
Modal,
ModalPortal,
ModalContent,
ModalHeader,
ModalTitle,
ModalFooter,
ModalClose,
Button,
} from '@janhq/uikit'
import { useVaultDirectory } from '@/hooks/useVaultDirectory'
const ModalChangeDirectory = () => {
const {
isDirectoryConfirm,
setIsDirectoryConfirm,
applyNewDestination,
newDestinationPath,
} = useVaultDirectory()
return (
<Modal
open={isDirectoryConfirm}
onOpenChange={() => setIsDirectoryConfirm(false)}
>
<ModalPortal />
<ModalContent>
<ModalHeader>
<ModalTitle>Relocate Jan Data Folder</ModalTitle>
</ModalHeader>
<p className="text-muted-foreground">
Are you sure you want to relocate Jan data folder to{' '}
<span className="font-medium text-foreground">
{newDestinationPath}
</span>
? A restart will be required afterward.
</p>
<ModalFooter>
<div className="flex gap-x-2">
<ModalClose asChild onClick={() => setIsDirectoryConfirm(false)}>
<Button themes="ghost">Cancel</Button>
</ModalClose>
<ModalClose asChild>
<Button onClick={applyNewDestination} autoFocus>
Yes, Proceed
</Button>
</ModalClose>
</div>
</ModalFooter>
</ModalContent>
</Modal>
)
}
export default ModalChangeDirectory

View File

@ -0,0 +1,44 @@
import React from 'react'
import {
Modal,
ModalPortal,
ModalContent,
ModalHeader,
ModalTitle,
ModalFooter,
ModalClose,
Button,
} from '@janhq/uikit'
import { useVaultDirectory } from '@/hooks/useVaultDirectory'
const ModalErrorSetDestGlobal = () => {
const { isErrorSetNewDest, setIsErrorSetNewDest } = useVaultDirectory()
return (
<Modal
open={isErrorSetNewDest}
onOpenChange={() => setIsErrorSetNewDest(false)}
>
<ModalPortal />
<ModalContent>
<ModalHeader>
<ModalTitle>Error Occurred</ModalTitle>
</ModalHeader>
<p className="text-muted-foreground">
Oops! Something went wrong. Jan data folder remains the same. Please
try again.
</p>
<ModalFooter>
<div className="flex gap-x-2">
<ModalClose asChild onClick={() => setIsErrorSetNewDest(false)}>
<Button themes="danger">Got it</Button>
</ModalClose>
</div>
</ModalFooter>
</ModalContent>
</Modal>
)
}
export default ModalErrorSetDestGlobal

View File

@ -0,0 +1,49 @@
import React from 'react'
import {
Modal,
ModalPortal,
ModalContent,
ModalHeader,
ModalTitle,
ModalFooter,
ModalClose,
Button,
} from '@janhq/uikit'
import { useVaultDirectory } from '@/hooks/useVaultDirectory'
const ModalSameDirectory = () => {
const { isSameDirectory, setIsSameDirectory, setNewDestination } =
useVaultDirectory()
return (
<Modal
open={isSameDirectory}
onOpenChange={() => setIsSameDirectory(false)}
>
<ModalPortal />
<ModalContent>
<ModalHeader>
<ModalTitle>Unable to move files</ModalTitle>
</ModalHeader>
<p className="text-muted-foreground">
{`It seems like the folder you've chosen same with current directory`}
</p>
<ModalFooter>
<div className="flex gap-x-2">
<ModalClose asChild onClick={() => setIsSameDirectory(false)}>
<Button themes="ghost">Cancel</Button>
</ModalClose>
<ModalClose asChild>
<Button themes="danger" onClick={setNewDestination} autoFocus>
Choose a different folder
</Button>
</ModalClose>
</div>
</ModalFooter>
</ModalContent>
</Modal>
)
}
export default ModalSameDirectory

View File

@ -0,0 +1,52 @@
import { Button, Input } from '@janhq/uikit'
import { PencilIcon, FolderOpenIcon } from 'lucide-react'
import { useVaultDirectory } from '@/hooks/useVaultDirectory'
import ModalChangeDirectory from './ModalChangeDirectory'
import ModalErrorSetDestGlobal from './ModalErrorSetDestGlobal'
import ModalSameDirectory from './ModalSameDirectory'
const DataFolder = () => {
const { currentPath, setNewDestination } = useVaultDirectory()
return (
<>
<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">
<h6 className="text-sm font-semibold capitalize">
Jan Data Folder
</h6>
</div>
<p className="leading-relaxed">
Where messages, model configurations, and other user data are
placed.
</p>
</div>
<div className="flex items-center gap-x-3">
<div className="relative">
<Input value={currentPath} className="w-[240px] pr-8" disabled />
<FolderOpenIcon
size={16}
className="absolute right-2 top-1/2 -translate-y-1/2"
/>
</div>
<Button
size="sm"
themes="outline"
className="h-9 w-9 p-0"
onClick={setNewDestination}
>
<PencilIcon size={16} />
</Button>
</div>
</div>
<ModalSameDirectory />
<ModalChangeDirectory />
<ModalErrorSetDestGlobal />
</>
)
}
export default DataFolder

View File

@ -9,7 +9,7 @@ import {
ChangeEvent,
} from 'react'
import { fs, AppConfiguration } from '@janhq/core'
import { fs } from '@janhq/core'
import { Switch, Button, Input } from '@janhq/uikit'
import ShortcutModal from '@/containers/ShortcutModal'
@ -20,6 +20,8 @@ import { FeatureToggleContext } from '@/context/FeatureToggle'
import { useSettings } from '@/hooks/useSettings'
import DataFolder from './DataFolder'
const Advanced = () => {
const {
experimentalFeature,
@ -31,6 +33,7 @@ const Advanced = () => {
} = useContext(FeatureToggleContext)
const [partialProxy, setPartialProxy] = useState<string>(proxy)
const [gpuEnabled, setGpuEnabled] = useState<boolean>(false)
const { readSettings, saveSettings, validateSettings, setShowNotification } =
useSettings()
const onProxyChange = useCallback(
@ -46,17 +49,6 @@ const Advanced = () => {
[setPartialProxy, setProxy]
)
// TODO: remove me later.
const [currentPath, setCurrentPath] = useState('')
useEffect(() => {
window.core?.api
?.getAppConfigurations()
?.then((appConfig: AppConfiguration) => {
setCurrentPath(appConfig.data_folder)
})
}, [])
useEffect(() => {
readSettings().then((settings) => {
setGpuEnabled(settings.run_mode === 'gpu')
@ -73,45 +65,55 @@ const Advanced = () => {
})
}
const onJanVaultDirectoryClick = async () => {
const destFolder = await window.core?.api?.selectDirectory()
if (destFolder) {
console.debug(`Destination folder selected: ${destFolder}`)
try {
const appConfiguration: AppConfiguration =
await window.core?.api?.getAppConfigurations()
const currentJanDataFolder = appConfiguration.data_folder
if (currentJanDataFolder === destFolder) {
console.debug(
`Destination folder is the same as current folder. Ignore..`
)
return
}
appConfiguration.data_folder = destFolder
await fs.syncFile(currentJanDataFolder, destFolder)
await window.core?.api?.updateAppConfiguration(appConfiguration)
console.debug(
`File sync finished from ${currentJanDataFolder} to ${destFolder}`
)
await window.core?.api?.relaunch()
} catch (e) {
console.error(`Error: ${e}`)
}
}
}
return (
<div className="block w-full">
{/* Keyboard shortcut */}
<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">
<h6 className="text-sm font-semibold capitalize">
Keyboard Shortcuts
</h6>
</div>
<p className="leading-relaxed">
Shortcuts that you might find useful in Jan app.
</p>
</div>
<ShortcutModal />
</div>
{/* Experimental */}
<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">
<h6 className="text-sm font-semibold capitalize">
Experimental Mode
</h6>
</div>
<p className="leading-relaxed">
Enable experimental features that may be unstable tested.
</p>
</div>
<Switch
checked={experimentalFeature}
onCheckedChange={(e) => {
if (e === true) {
setExperimentalFeature(true)
} else {
setExperimentalFeature(false)
}
}}
/>
</div>
{/* CPU / GPU switching */}
{!isMac && (
<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-shrink-0 space-y-1.5">
<div className="flex gap-x-2">
<h6 className="text-sm font-semibold capitalize">NVidia GPU</h6>
</div>
<p className="whitespace-pre-wrap leading-relaxed">
<p className="leading-relaxed">
Enable GPU acceleration for NVidia GPUs.
</p>
</div>
@ -133,36 +135,17 @@ const Advanced = () => {
/>
</div>
)}
{/* Experimental */}
<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">
Experimental Mode
</h6>
</div>
<p className="whitespace-pre-wrap leading-relaxed">
Enable experimental features that may be unstable tested.
</p>
</div>
<Switch
checked={experimentalFeature}
onCheckedChange={(e) => {
if (e === true) {
setExperimentalFeature(true)
} else {
setExperimentalFeature(false)
}
}}
/>
</div>
{/* Directory */}
{experimentalFeature && <DataFolder />}
{/* Proxy */}
<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-shrink-0 space-y-1.5">
<div className="flex gap-x-2">
<h6 className="text-sm font-semibold capitalize">HTTPS Proxy</h6>
</div>
<p className="whitespace-pre-wrap leading-relaxed">
<p className="leading-relaxed">
Specify the HTTPS proxy or leave blank (proxy auto-configuration and
SOCKS not supported).
</p>
@ -173,15 +156,16 @@ const Advanced = () => {
/>
</div>
</div>
{/* Ignore SSL certificates */}
<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-shrink-0 space-y-1.5">
<div className="flex gap-x-2">
<h6 className="text-sm font-semibold capitalize">
Ignore SSL certificates
</h6>
</div>
<p className="whitespace-pre-wrap leading-relaxed">
<p className="leading-relaxed">
Allow self-signed or unverified certificates - may be required for
certain proxies.
</p>
@ -197,79 +181,19 @@ const Advanced = () => {
}}
/>
</div>
{window.electronAPI && (
<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">
Open App Directory
</h6>
</div>
<p className="whitespace-pre-wrap leading-relaxed">
Open the directory where your app data, like conversation history
and model configurations, is located.
</p>
</div>
<Button
size="sm"
themes="secondary"
onClick={() => window.electronAPI.openAppDirectory()}
>
Open
</Button>
</div>
)}
{/* Claer log */}
<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-shrink-0 space-y-1.5">
<div className="flex gap-x-2">
<h6 className="text-sm font-semibold capitalize">Clear logs</h6>
</div>
<p className="whitespace-pre-wrap leading-relaxed">
Clear all logs from Jan app.
</p>
<p className="leading-relaxed">Clear all logs from Jan app.</p>
</div>
<Button size="sm" themes="secondary" onClick={clearLogs}>
<Button size="sm" themes="secondaryDanger" onClick={clearLogs}>
Clear
</Button>
</div>
{experimentalFeature && (
<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">
Jan Data Folder
</h6>
</div>
<p className="whitespace-pre-wrap leading-relaxed">
Where messages, model configurations, and other user data is
placed.
</p>
<p className="whitespace-pre-wrap leading-relaxed text-gray-500">
{`${currentPath}`}
</p>
</div>
<Button
size="sm"
themes="secondary"
onClick={onJanVaultDirectoryClick}
>
Select
</Button>
</div>
)}
<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">
Keyboard Shortcuts
</h6>
</div>
<p className="whitespace-pre-wrap leading-relaxed">
Shortcuts that you might find useful in Jan app.
</p>
</div>
<ShortcutModal />
</div>
</div>
)
}

View File

@ -111,7 +111,7 @@ const ExtensionCatalog = () => {
onChange={handleFileChange}
/>
<Button
themes="secondary"
themes="secondaryBlue"
size="sm"
onClick={() => fileInputRef.current?.click()}
>

View File

@ -7,14 +7,14 @@ import { motion as m } from 'framer-motion'
import { twMerge } from 'tailwind-merge'
import { SUCCESS_SET_NEW_DESTINATION } from '@/hooks/useVaultDirectory'
import Advanced from '@/screens/Settings/Advanced'
import AppearanceOptions from '@/screens/Settings/Appearance'
import ExtensionCatalog from '@/screens/Settings/CoreExtensions'
import Models from '@/screens/Settings/Models'
import { formatExtensionsName } from '@/utils/converter'
const SettingsScreen = () => {
const [activeStaticMenu, setActiveStaticMenu] = useState('My Models')
const [menus, setMenus] = useState<any[]>([])
@ -46,6 +46,12 @@ const SettingsScreen = () => {
}
}
useEffect(() => {
if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') {
setActiveStaticMenu('Advanced Settings')
}
}, [])
return (
<div className="flex h-full bg-background">
<div className="flex h-full w-64 flex-shrink-0 flex-col overflow-y-auto border-r border-border">