chore: add relocate jan data folder function to new FE (#5043)

* chore: typo

* fix: linter issues

* chore: fix linter

* chore: fix linter

* chore: add relocate data folder
This commit is contained in:
Louis 2025-05-21 10:48:10 +07:00 committed by GitHub
parent e4168a4c17
commit 6676e0ced8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 106 additions and 37 deletions

View File

@ -19,7 +19,7 @@ export abstract class HardwareManagementExtension extends BaseExtension {
/** /**
* @returns A Promise that resolves to an object of set active gpus. * @returns A Promise that resolves to an object of set active gpus.
*/ */
abstract setAvtiveGpu(data: { gpus: number[] }): Promise<{ abstract setActiveGpu(data: { gpus: number[] }): Promise<{
message: string message: string
activated_gpus: number[] activated_gpus: number[]
}> }>

View File

@ -51,7 +51,7 @@ export default class JSONHardwareManagementExtension extends HardwareManagementE
/** /**
* @returns A Promise that resolves to an object of set gpu activate. * @returns A Promise that resolves to an object of set gpu activate.
*/ */
async setAvtiveGpu(data: { gpus: number[] }): Promise<{ async setActiveGpu(data: { gpus: number[] }): Promise<{
message: string message: string
activated_gpus: number[] activated_gpus: number[]
}> { }> {

View File

@ -12,7 +12,7 @@
] ]
}, },
"scripts": { "scripts": {
"lint": "yarn workspace jan lint && yarn workspace @janhq/web lint", "lint": "yarn workspace @janhq/web-app lint",
"test:unit": "jest", "test:unit": "jest",
"test:coverage": "yarn workspace @janhq/web-app test", "test:coverage": "yarn workspace @janhq/web-app test",
"test": "yarn workspace @janhq/web-app test", "test": "yarn workspace @janhq/web-app test",

View File

@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf, io};
use tauri::{AppHandle, Manager, Runtime, State}; use tauri::{AppHandle, Manager, Runtime, State};
use super::{server, setup, state::AppState}; use super::{server, setup, state::AppState};
@ -267,6 +267,65 @@ pub fn get_user_home_path(app: AppHandle) -> String {
return get_app_configurations(app.clone()).data_folder; return get_app_configurations(app.clone()).data_folder;
} }
/// Recursively copy a directory from src to dst
fn copy_dir_recursive(src: &PathBuf, dst: &PathBuf) -> Result<(), io::Error> {
if !dst.exists() {
fs::create_dir_all(dst)?;
}
for entry in fs::read_dir(src)? {
let entry = entry?;
let file_type = entry.file_type()?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if file_type.is_dir() {
copy_dir_recursive(&src_path, &dst_path)?;
} else {
fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}
#[tauri::command]
pub fn change_app_data_folder(
app_handle: tauri::AppHandle,
new_data_folder: String,
) -> Result<(), String> {
// Get current data folder path
let current_data_folder = get_jan_data_folder_path(app_handle.clone());
let new_data_folder_path = PathBuf::from(&new_data_folder);
// Create the new data folder if it doesn't exist
if !new_data_folder_path.exists() {
fs::create_dir_all(&new_data_folder_path)
.map_err(|e| format!("Failed to create new data folder: {}", e))?;
}
// Copy all files from the old folder to the new one
if current_data_folder.exists() {
log::info!(
"Copying data from {:?} to {:?}",
current_data_folder,
new_data_folder_path
);
copy_dir_recursive(&current_data_folder, &new_data_folder_path)
.map_err(|e| format!("Failed to copy data to new folder: {}", e))?;
} else {
log::info!("Current data folder does not exist, nothing to copy");
}
// Update the configuration to point to the new folder
let mut configuration = get_app_configurations(app_handle.clone());
configuration.data_folder = new_data_folder;
// Save the updated configuration
update_app_configuration(app_handle, configuration)
}
#[tauri::command] #[tauri::command]
pub fn app_token(state: State<'_, AppState>) -> Option<String> { pub fn app_token(state: State<'_, AppState>) -> Option<String> {
state.app_token.clone() state.app_token.clone()

View File

@ -46,6 +46,7 @@ pub fn run() {
core::cmd::start_server, core::cmd::start_server,
core::cmd::stop_server, core::cmd::stop_server,
core::cmd::read_logs, core::cmd::read_logs,
core::cmd::change_app_data_folder,
// MCP commands // MCP commands
core::mcp::get_tools, core::mcp::get_tools,
core::mcp::call_tool, core::mcp::call_tool,

View File

@ -20,7 +20,6 @@ export default function LanguageSwitcher() {
const changeLanguage = (lng: string) => { const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng) i18n.changeLanguage(lng)
// @ts-ignore
setCurrentLanguage(lng as Language) setCurrentLanguage(lng as Language)
} }

View File

@ -41,7 +41,6 @@ export function ModelSetting({ model, provider }: ModelSettingProps) {
[key]: { [key]: {
...(model.settings?.[key] != null ? model.settings?.[key] : {}), ...(model.settings?.[key] != null ? model.settings?.[key] : {}),
controller_props: { controller_props: {
// @ts-ignore
...(model.settings?.[key]?.controller_props ?? {}), ...(model.settings?.[key]?.controller_props ?? {}),
value: value, value: value,
}, },
@ -67,7 +66,6 @@ export function ModelSetting({ model, provider }: ModelSettingProps) {
updateModel({ updateModel({
id: model.id, id: model.id,
settings: Object.entries(updatedModel.settings).map(([key, value]) => ({ settings: Object.entries(updatedModel.settings).map(([key, value]) => ({
// @ts-ignore
[key]: value.controller_props?.value, [key]: value.controller_props?.value,
})) as ModelSettingParams, })) as ModelSettingParams,
}) })

View File

@ -8,8 +8,8 @@ import {
// Dropdown component // Dropdown component
type DropdownControlProps = { type DropdownControlProps = {
value: string value: string
options?: Array<{ value: string; name: string }> options?: Array<{ value: number | string; name: string }>
onChange: (value: string) => void onChange: (value: number | string) => void
} }
export function DropdownControl({ export function DropdownControl({

View File

@ -10,12 +10,12 @@ type DynamicControllerProps = {
title?: string title?: string
className?: string className?: string
description?: string description?: string
controllerType: 'input' | 'checkbox' | 'dropdown' | 'textarea' | 'slider' controllerType: 'input' | 'checkbox' | 'dropdown' | 'textarea' | 'slider' | string
controllerProps: { controllerProps: {
value?: string | boolean | number value?: string | boolean | number
placeholder?: string placeholder?: string
type?: string type?: string
options?: Array<{ value: string; name: string }> options?: Array<{ value: number | string; name: string }>
input_actions?: string[] input_actions?: string[]
rows?: number rows?: number
min?: number min?: number

View File

@ -3,11 +3,9 @@ import { persist, createJSONStorage } from 'zustand/middleware'
import { localStorageKey } from '@/constants/localStorage' import { localStorageKey } from '@/constants/localStorage'
type LeftPanelStoreState = { type LeftPanelStoreState = {
// @ts-ignore
currentLanguage: Language currentLanguage: Language
spellCheckChatInput: boolean spellCheckChatInput: boolean
setSpellCheckChatInput: (value: boolean) => void setSpellCheckChatInput: (value: boolean) => void
// @ts-ignore
setCurrentLanguage: (value: Language) => void setCurrentLanguage: (value: Language) => void
} }

View File

@ -182,7 +182,7 @@ export const useHardware = create<HardwareStore>()(
} }
} }
setActiveGpus({ setActiveGpus({
gpus: newGPUs.filter((e) => e.activated).map((e) => e.id as unknown as number), gpus: newGPUs.filter((e) => e.activated).map((e) => parseInt(e.id)),
}) })
return { return {
hardwareData: { hardwareData: {

View File

@ -283,8 +283,7 @@ export const postMessageProcessing = async (
? JSON.parse(toolCall.function.arguments) ? JSON.parse(toolCall.function.arguments)
: {}, : {},
}) })
// @ts-ignore if ('error' in result && result.error) break
if (result.error) break
message.metadata = { message.metadata = {
...(message.metadata ?? {}), ...(message.metadata ?? {}),
@ -300,7 +299,6 @@ export const postMessageProcessing = async (
}, },
], ],
} }
// @ts-ignore
builder.addToolMessage(result.content[0]?.text ?? '', toolCall.id) builder.addToolMessage(result.content[0]?.text ?? '', toolCall.id)
// update message metadata // update message metadata
return message return message

View File

@ -27,6 +27,7 @@ export const AppRoutes = [
'restartMcpServers', 'restartMcpServers',
'getConnectedServers', 'getConnectedServers',
'readLogs', 'readLogs',
'changeAppDataFolder',
] ]
// Define API routes based on different route types // Define API routes based on different route types
export const Routes = [...CoreRoutes, ...APIRoutes, ...AppRoutes].map((r) => ({ export const Routes = [...CoreRoutes, ...APIRoutes, ...AppRoutes].map((r) => ({

View File

@ -21,7 +21,11 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { factoryReset, getJanDataFolder } from '@/services/app' import {
factoryReset,
getJanDataFolder,
relocateJanDataFolder,
} from '@/services/app'
import { IconFolder } from '@tabler/icons-react' import { IconFolder } from '@tabler/icons-react'
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -115,6 +119,8 @@ function General() {
if (selectedPath === janDataFolder) return if (selectedPath === janDataFolder) return
if (selectedPath !== null) { if (selectedPath !== null) {
setJanDataFolder(selectedPath) setJanDataFolder(selectedPath)
await relocateJanDataFolder(selectedPath)
window.core?.api?.relaunch()
// TODO: we need function to move everything into new folder selectedPath // TODO: we need function to move everything into new folder selectedPath
// eg like this // eg like this
// await window.core?.api?.moveDataFolder(selectedPath) // await window.core?.api?.moveDataFolder(selectedPath)

View File

@ -64,3 +64,12 @@ export const getJanDataFolder = async (): Promise<string | undefined> => {
return undefined return undefined
} }
} }
/**
* @description This function is used to relocate the Jan data folder.
* It will change the app data folder to the specified path.
* @param path The new path for the Jan data folder
*/
export const relocateJanDataFolder = async (path: string) => {
window.core?.api?.changeAppDataFolder({ newDataFolder: path })
}

View File

@ -36,7 +36,7 @@ export const setActiveGpus = async (data: { gpus: number[] }) => {
} }
try { try {
const response = await extension.setAvtiveGpu(data) const response = await extension.setActiveGpu(data)
return response return response
} catch (error) { } catch (error) {
console.error('Failed to install engine variant:', error) console.error('Failed to install engine variant:', error)

View File

@ -21,7 +21,7 @@ export const getTools = (): Promise<MCPTool[]> => {
/** /**
* @description This function gets connected MCP servers. * @description This function gets connected MCP servers.
* @returns {Promise<string[]>} The MCP names * @returns {Promise<string[]>} The MCP names
* @returns * @returns
*/ */
export const getConnectedServers = (): Promise<string[]> => { export const getConnectedServers = (): Promise<string[]> => {
return window.core?.api?.getConnectedServers() return window.core?.api?.getConnectedServers()
@ -36,6 +36,6 @@ export const getConnectedServers = (): Promise<string[]> => {
export const callTool = (args: { export const callTool = (args: {
toolName: string toolName: string
arguments: object arguments: object
}): Promise<unknown> => { }): Promise<{ error: string; content: { text: string }[] }> => {
return window.core?.api?.callTool(args) return window.core?.api?.callTool(args)
} }

View File

@ -77,7 +77,7 @@ export const getProviders = async (): Promise<ModelProvider[]> => {
? (model.capabilities as string[]) ? (model.capabilities as string[])
: [ModelCapabilities.COMPLETION], : [ModelCapabilities.COMPLETION],
provider: providerName, provider: providerName,
settings: modelSettings, settings: modelSettings ,
})), })),
} }
runtimeProviders.push(provider) runtimeProviders.push(provider)

1
web-app/src/types/app.d.ts vendored Normal file
View File

@ -0,0 +1 @@
type Language = 'en' | 'id' | 'vn'

View File

@ -1,23 +1,22 @@
import { ExtensionManager } from '@/lib/extension' export {}
type Language = 'en' | 'id' | 'vn'
declare module 'react-syntax-highlighter-virtualized-renderer' declare module 'react-syntax-highlighter-virtualized-renderer'
type AppCore = { type AppCore = {
api: APIs api: APIs
extensionManager: ExtensionManager | undefined extensionManager: ExtensionManager | undefined
} }
declare global { declare global {
declare const IS_TAURI: boolean
declare const IS_MACOS: boolean
declare const IS_WINDOWS: boolean
declare const IS_LINUX: boolean
declare const IS_IOS: boolean
declare const IS_ANDROID: boolean
declare const PLATFORM: string
declare const VERSION: string
interface Window { interface Window {
core: AppCore | undefined core: AppCore | undefined
} }
}
let IS_TAURI: boolean
let IS_MACOS: boolean
let IS_WINDOWS: boolean
let IS_LINUX: boolean
let IS_IOS: boolean
let IS_ANDROID: boolean
let PLATFORM: string
let VERSION: string
}

View File

@ -5,7 +5,7 @@ type ControllerProps = {
value?: string | boolean | number value?: string | boolean | number
placeholder?: string placeholder?: string
type?: string type?: string
options?: Array<{ value: string; name: string }> options?: Array<{ value: number | string; name: string }>
input_actions?: string[] input_actions?: string[]
} }
@ -16,7 +16,7 @@ type ProviderSetting = {
key: string key: string
title: string title: string
description: string description: string
controller_type: 'input' | 'checkbox' | 'dropdown' | 'slider' controller_type: 'input' | 'checkbox' | 'dropdown' | 'slider' | string
controller_props: ControllerProps controller_props: ControllerProps
} }
@ -31,7 +31,7 @@ type Model = {
description?: string description?: string
format?: string format?: string
capabilities?: string[] capabilities?: string[]
settings?: Record<string, unknown> settings?: Record<string, ProviderSetting>
} }
/** /**