chore: simplify themes and assistants

This commit is contained in:
Louis 2025-03-26 11:43:00 +07:00
parent 27beb46801
commit 24b8a1b66a
No known key found for this signature in database
GPG Key ID: 44FA9F4D33C37DE2
11 changed files with 121 additions and 171 deletions

View File

@ -19,7 +19,7 @@ const writeBlob: (path: string, data: string) => Promise<any> = (path, data) =>
* Reads the contents of a file at the specified path.
* @returns {Promise<any>} 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<any>} 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<any>} 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.

View File

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

View File

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

View File

@ -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<void> {
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<Assistant[]> {
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]
}
}
async deleteAssistant(assistant: Assistant): Promise<void> {
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<void> {
await this.createAssistant(this.defaultAssistant)
}
/** DEPRECATED */
async createAssistant(assistant: Assistant): Promise<void> {}
async deleteAssistant(assistant: Assistant): Promise<void> {}
private defaultAssistant: Assistant = {
avatar: '',

View File

@ -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<String> {
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);

View File

@ -41,6 +41,38 @@ pub fn exists_sync(app_handle: tauri::AppHandle, args: Vec<String>) -> Result<bo
Ok(path.exists())
}
#[tauri::command]
pub fn read_file_sync(
app_handle: tauri::AppHandle,
args: Vec<String>,
) -> Result<String, String> {
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<String>,
) -> Result<Vec<String>, 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<String> = 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)
};

View File

@ -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(())
})

View File

@ -51,7 +51,8 @@
"icons/icon.ico"
],
"resources": [
"binaries/engines/**/*"
"binaries/engines/**/*",
"resources/themes/**/*"
],
"externalBin": ["binaries/cortex-server"]
}

View File

@ -24,7 +24,7 @@ export const themesOptionsAtom = atomWithStorage<
export const selectedThemeIdAtom = atomWithStorage<string>(
THEME,
'',
'joi-light',
undefined,
{ getOnInit: true }
)

View File

@ -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`,
'file://themes',
selectedIdTheme,
'theme.json',
])
const theme: Theme = JSON.parse(await fs.readFileSync(filePath, 'utf-8'))
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 configureTheme = useCallback(async () => {
if (!themeData || !themeOptions) {
@ -105,11 +81,9 @@ export const useLoadTheme = () => {
useEffect(() => {
configureTheme()
}, [
configureTheme,
selectedIdTheme,
setNativeTheme,
setSelectedIdTheme,
themeData?.nativeTheme,
])
}, [themeData])
useEffect(() => {
getThemes()
}, [])
}

View File

@ -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) => (
<SettingItem
key={item.name}