diff --git a/core/src/browser/fs.ts b/core/src/browser/fs.ts index d73e01915..6bdd193b8 100644 --- a/core/src/browser/fs.ts +++ b/core/src/browser/fs.ts @@ -19,7 +19,7 @@ const writeBlob: (path: string, data: string) => Promise = (path, data) => * Reads the contents of a file at the specified path. * @returns {Promise} A Promise that resolves with the contents of the file. */ -const readFileSync = (...args: any[]) => globalThis.core.api?.readFileSync(...args) +const readFileSync = (...args: any[]) => globalThis.core.api?.readFileSync({ args }) /** * Check whether the file exists * @param {string} path @@ -30,12 +30,12 @@ const existsSync = (...args: any[]) => globalThis.core.api?.existsSync({ args }) * List the directory files * @returns {Promise} A Promise that resolves with the contents of the directory. */ -const readdirSync = (...args: any[]) => globalThis.core.api?.readdirSync(...args) +const readdirSync = (...args: any[]) => globalThis.core.api?.readdirSync({ args }) /** * Creates a directory at the specified path. * @returns {Promise} A Promise that resolves when the directory is created successfully. */ -const mkdir = (...args: any[]) => globalThis.core.api?.mkdir(...args) +const mkdir = (...args: any[]) => globalThis.core.api?.mkdir({ args }) /** * Removes a directory at the specified path. diff --git a/core/src/node/api/processors/app.ts b/core/src/node/api/processors/app.ts index d86e6265c..2ee83e63f 100644 --- a/core/src/node/api/processors/app.ts +++ b/core/src/node/api/processors/app.ts @@ -8,6 +8,7 @@ import { normalizeFilePath, getJanDataFolderPath, } from '../../helper' +import { readdirSync } from 'fs' export class App implements Processor { observer?: Function @@ -69,10 +70,22 @@ export class App implements Processor { writeLog(args) } + /** + * Get app configurations. + */ getAppConfigurations() { return appConfiguration() } + /** + * Get themes from the app data folder. + * @returns + */ + getThemes() { + const themesPath = join(getJanDataFolderPath(), 'themes') + return readdirSync(themesPath) + } + async updateAppConfiguration(args: any) { await updateAppConfiguration(args) } diff --git a/core/src/types/api/index.ts b/core/src/types/api/index.ts index 2f33b72e4..5bae92d8e 100644 --- a/core/src/types/api/index.ts +++ b/core/src/types/api/index.ts @@ -40,7 +40,7 @@ export enum NativeRoute { /** * App Route APIs * @description Enum of all the routes exposed by the app - */ +*/ export enum AppRoute { getAppConfigurations = 'getAppConfigurations', updateAppConfiguration = 'updateAppConfiguration', @@ -51,6 +51,7 @@ export enum AppRoute { log = 'log', systemInformation = 'systemInformation', showToast = 'showToast', + getThemes = 'getThemes', } export enum AppEvent { diff --git a/extensions/assistant-extension/src/index.ts b/extensions/assistant-extension/src/index.ts index 621d8e216..0f9b52808 100644 --- a/extensions/assistant-extension/src/index.ts +++ b/extensions/assistant-extension/src/index.ts @@ -1,38 +1,11 @@ -import { - fs, - Assistant, - events, - joinPath, - AssistantExtension, - AssistantEvent, - ToolManager, -} from '@janhq/core' +import { Assistant, AssistantExtension, ToolManager } from '@janhq/core' import { RetrievalTool } from './tools/retrieval' export default class JanAssistantExtension extends AssistantExtension { - private static readonly _homeDir = 'file://assistants' async onLoad() { // Register the retrieval tool ToolManager.instance().register(new RetrievalTool()) - - // making the assistant directory - const assistantDirExist = await fs.existsSync( - JanAssistantExtension._homeDir - ) - if ( - localStorage.getItem(`${this.name}-version`) !== VERSION || - !assistantDirExist - ) { - if (!assistantDirExist) await fs.mkdir(JanAssistantExtension._homeDir) - - // Write assistant metadata - await this.createJanAssistant() - // Finished migration - localStorage.setItem(`${this.name}-version`, VERSION) - // Update the assistant list - events.emit(AssistantEvent.OnAssistantsUpdate, {}) - } } /** @@ -40,87 +13,13 @@ export default class JanAssistantExtension extends AssistantExtension { */ onUnload(): void {} - async createAssistant(assistant: Assistant): Promise { - const assistantDir = await joinPath([ - JanAssistantExtension._homeDir, - assistant.id, - ]) - if (!(await fs.existsSync(assistantDir))) await fs.mkdir(assistantDir) - - // store the assistant metadata json - const assistantMetadataPath = await joinPath([ - assistantDir, - 'assistant.json', - ]) - try { - await fs.writeFileSync( - assistantMetadataPath, - JSON.stringify(assistant, null, 2) - ) - } catch (err) { - console.error(err) - } - } - async getAssistants(): Promise { - try { - // get all the assistant directories - // get all the assistant metadata json - const results: Assistant[] = [] - - const allFileName: string[] = await fs.readdirSync( - JanAssistantExtension._homeDir - ) - - for (const fileName of allFileName) { - const filePath = await joinPath([ - JanAssistantExtension._homeDir, - fileName, - ]) - - if (!(await fs.fileStat(filePath))?.isDirectory) continue - const jsonFiles: string[] = (await fs.readdirSync(filePath)).filter( - (file: string) => file === 'assistant.json' - ) - - if (jsonFiles.length !== 1) { - // has more than one assistant file -> ignore - continue - } - - const content = await fs.readFileSync( - await joinPath([filePath, jsonFiles[0]]), - 'utf-8' - ) - const assistant: Assistant = - typeof content === 'object' ? content : JSON.parse(content) - - results.push(assistant) - } - - return results - } catch (err) { - console.debug(err) - return [this.defaultAssistant] - } + return [this.defaultAssistant] } - async deleteAssistant(assistant: Assistant): Promise { - if (assistant.id === 'jan') { - return Promise.reject('Cannot delete Jan Assistant') - } - - // remove the directory - const assistantDir = await joinPath([ - JanAssistantExtension._homeDir, - assistant.id, - ]) - return fs.rm(assistantDir) - } - - private async createJanAssistant(): Promise { - await this.createAssistant(this.defaultAssistant) - } + /** DEPRECATED */ + async createAssistant(assistant: Assistant): Promise {} + async deleteAssistant(assistant: Assistant): Promise {} private defaultAssistant: Assistant = { avatar: '', diff --git a/src-tauri/src/handlers/cmd.rs b/src-tauri/src/handlers/cmd.rs index c8ed5fbe5..e9e980a99 100644 --- a/src-tauri/src/handlers/cmd.rs +++ b/src-tauri/src/handlers/cmd.rs @@ -106,6 +106,23 @@ pub fn get_jan_extensions_path(app_handle: tauri::AppHandle) -> PathBuf { get_jan_data_folder_path(app_handle).join("extensions") } +#[tauri::command] +pub fn get_themes(app_handle: tauri::AppHandle) -> Vec { + let mut themes = vec![]; + let themes_path = get_jan_data_folder_path(app_handle).join("themes"); + if themes_path.exists() { + for entry in fs::read_dir(themes_path).unwrap() { + let entry = entry.unwrap(); + if entry.path().is_dir() { + if let Some(name) = entry.file_name().to_str() { + themes.push(name.to_string()); + } + } + } + } + themes +} + #[tauri::command] pub fn get_configuration_file_path(app_handle: tauri::AppHandle) -> PathBuf { let app_path = app_handle.path().app_data_dir().unwrap_or_else(|err| { @@ -274,6 +291,10 @@ pub fn install_extensions(app: tauri::AppHandle) -> Result<(), String> { .as_ref() .and_then(|manifest| manifest["version"].as_str()) .unwrap_or(""), + "productName": extension_manifest + .as_ref() + .and_then(|manifest| manifest["productName"].as_str()) + .unwrap_or(""), }); extensions_list.push(new_extension); diff --git a/src-tauri/src/handlers/fs.rs b/src-tauri/src/handlers/fs.rs index 9ea2de1b8..9b841474c 100644 --- a/src-tauri/src/handlers/fs.rs +++ b/src-tauri/src/handlers/fs.rs @@ -41,6 +41,38 @@ pub fn exists_sync(app_handle: tauri::AppHandle, args: Vec) -> Result, +) -> Result { + if args.is_empty() || args[0].is_empty() { + return Err("read_file_sync error: Invalid argument".to_string()); + } + + let path = resolve_path(app_handle, &args[0]); + fs::read_to_string(&path).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn readdir_sync( + app_handle: tauri::AppHandle, + args: Vec, +) -> Result, String> { + if args.is_empty() || args[0].is_empty() { + return Err("read_dir_sync error: Invalid argument".to_string()); + } + + let path = resolve_path(app_handle, &args[0]); + println!("Reading directory: {:?}", path); + let entries = fs::read_dir(&path).map_err(|e| e.to_string())?; + let paths: Vec = entries + .filter_map(|entry| entry.ok()) + .map(|entry| entry.path().to_string_lossy().to_string()) + .collect(); + Ok(paths) +} + fn normalize_file_path(path: &str) -> String { path.replace("file:/", "").replace("file:\\", "") } @@ -48,7 +80,8 @@ fn normalize_file_path(path: &str) -> String { fn resolve_path(app_handle: tauri::AppHandle, path: &str) -> PathBuf { let path = if path.starts_with("file:/") || path.starts_with("file:\\") { let normalized = normalize_file_path(path); - get_jan_data_folder_path(app_handle).join(normalized) + let relative_normalized = normalized.strip_prefix("/").unwrap_or(&normalized); + get_jan_data_folder_path(app_handle).join(relative_normalized) } else { PathBuf::from(path) }; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index aeea4bfe1..ede050b65 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -47,10 +47,14 @@ pub fn run() { .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_shell::init()) .invoke_handler(tauri::generate_handler![ - // handlers::fs::join_path, - // handlers::fs::mkdir, - // handlers::fs::exists_sync, - // handlers::fs::rm, + handlers::fs::join_path, + handlers::fs::mkdir, + handlers::fs::exists_sync, + handlers::fs::readdir_sync, + handlers::fs::read_file_sync, + handlers::fs::rm, + // App commands + handlers::cmd::get_themes, handlers::cmd::get_app_configurations, handlers::cmd::get_active_extensions, handlers::cmd::get_user_home_path, @@ -111,13 +115,17 @@ pub fn run() { eprintln!("Failed to install extensions: {}", e); } - // Copy binaries to app_data + // Copy engine binaries to app_data let app_data_dir = app.app_handle().path().app_data_dir().unwrap(); let binaries_dir = app.app_handle().path().resource_dir().unwrap().join("binaries"); + let themes_dir = app.app_handle().path().resource_dir().unwrap().join("resources"); - if let Err(e) = copy_dir_all(binaries_dir, app_data_dir) { + if let Err(e) = copy_dir_all(binaries_dir, app_data_dir.clone()) { eprintln!("Failed to copy binaries: {}", e); } + if let Err(e) = copy_dir_all(themes_dir, app_data_dir.clone()) { + eprintln!("Failed to copy themes: {}", e); + } Ok(()) }) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ca42a1511..b3f91d609 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -51,7 +51,8 @@ "icons/icon.ico" ], "resources": [ - "binaries/engines/**/*" + "binaries/engines/**/*", + "resources/themes/**/*" ], "externalBin": ["binaries/cortex-server"] } diff --git a/web/helpers/atoms/Setting.atom.ts b/web/helpers/atoms/Setting.atom.ts index d8ad38a6d..1fbb3d2a6 100644 --- a/web/helpers/atoms/Setting.atom.ts +++ b/web/helpers/atoms/Setting.atom.ts @@ -24,7 +24,7 @@ export const themesOptionsAtom = atomWithStorage< export const selectedThemeIdAtom = atomWithStorage( THEME, - '', + 'joi-light', undefined, { getOnInit: true } ) diff --git a/web/hooks/useLoadTheme.ts b/web/hooks/useLoadTheme.ts index 4d37c3126..d292803f6 100644 --- a/web/hooks/useLoadTheme.ts +++ b/web/hooks/useLoadTheme.ts @@ -4,13 +4,10 @@ import { useTheme } from 'next-themes' import { fs, joinPath } from '@janhq/core' -import { useAtom, useAtomValue } from 'jotai' +import { useAtom } from 'jotai' import cssVars from '@/utils/jsonToCssVariables' -import themeData from '@/../../public/theme.json' with { type: 'json' } - -import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom' import { selectedThemeIdAtom, themeDataAtom, @@ -20,7 +17,6 @@ import { type NativeThemeProps = 'light' | 'dark' export const useLoadTheme = () => { - const janDataFolderPath = useAtomValue(janDataFolderPathAtom) const [themeOptions, setThemeOptions] = useAtom(themesOptionsAtom) const [themeData, setThemeData] = useAtom(themeDataAtom) const [selectedIdTheme, setSelectedIdTheme] = useAtom(selectedThemeIdAtom) @@ -51,48 +47,28 @@ export const useLoadTheme = () => { } const getThemes = useCallback(async () => { - if (!janDataFolderPath.length) return - const folderPath = await joinPath([janDataFolderPath, 'themes']) - const installedThemes = await fs.readdirSync(folderPath) + const installedThemes = await window.core.api.getThemes() - const themesOptions: { name: string; value: string }[] = installedThemes - .filter((x: string) => x !== '.DS_Store') - .map(async (x: string) => { - const y = await joinPath([`${folderPath}/${x}`, `theme.json`]) - const c: Theme = JSON.parse(await fs.readFileSync(y, 'utf-8')) - return { name: c?.displayName, value: c.id } - }) - Promise.all(themesOptions).then((results) => { - setThemeOptions(results) - }) + const themesOptions: { name: string; value: string }[] = + installedThemes.map((x: string) => ({ + name: x.replace(/-/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()), + value: x, + })) + setThemeOptions(themesOptions) - if (janDataFolderPath.length > 0) { - if (!selectedIdTheme.length) return setSelectedIdTheme('joi-light') - const filePath = await joinPath([ - `${folderPath}/${selectedIdTheme}`, - `theme.json`, - ]) - const theme: Theme = JSON.parse(await fs.readFileSync(filePath, 'utf-8')) + if (!selectedIdTheme.length) return setSelectedIdTheme('joi-light') + const filePath = await joinPath([ + 'file://themes', + selectedIdTheme, + 'theme.json', + ]) - setThemeData(theme) - setNativeTheme(theme.nativeTheme) - applyTheme(theme) - } else { - // Apply default bundled theme - const theme: Theme | undefined = themeData - if (theme) { - setThemeData(theme) - applyTheme(theme) - } - } - }, [ - janDataFolderPath, - selectedIdTheme, - setNativeTheme, - setSelectedIdTheme, - setThemeData, - setThemeOptions, - ]) + const theme: Theme = JSON.parse(await fs.readFileSync(filePath, 'utf-8')) + + setThemeData(theme) + setNativeTheme(theme.nativeTheme) + applyTheme(theme) + }, []) const configureTheme = useCallback(async () => { if (!themeData || !themeOptions) { @@ -105,11 +81,9 @@ export const useLoadTheme = () => { useEffect(() => { configureTheme() - }, [ - configureTheme, - selectedIdTheme, - setNativeTheme, - setSelectedIdTheme, - themeData?.nativeTheme, - ]) + }, [themeData]) + + useEffect(() => { + getThemes() + }, []) } diff --git a/web/screens/Settings/SettingLeftPanel/index.tsx b/web/screens/Settings/SettingLeftPanel/index.tsx index 499cf9f8e..1b410865b 100644 --- a/web/screens/Settings/SettingLeftPanel/index.tsx +++ b/web/screens/Settings/SettingLeftPanel/index.tsx @@ -149,7 +149,7 @@ const SettingLeftPanel = () => { {extensionHasSettings .sort((a, b) => String(a.name).localeCompare(String(b.name))) - .filter((e) => !e.name?.includes('Cortex')) + .filter((e) => !e.name?.toLowerCase().includes('cortex')) .map((item) => (