diff --git a/core/src/browser/fs.ts b/core/src/browser/fs.ts index 1bbaeb32b..0a05d4c56 100644 --- a/core/src/browser/fs.ts +++ b/core/src/browser/fs.ts @@ -4,7 +4,7 @@ import { FileStat } from '../types' * Writes data to a file at the specified path. * @returns {Promise} A Promise that resolves when the file is written successfully. */ -const writeFileSync = (...args: any[]) => globalThis.core.api?.writeFileSync(...args) +const writeFileSync = (...args: any[]) => globalThis.core.api?.writeFileSync({ args }) /** * Writes blob data to a file at the specified path. diff --git a/extensions/assistant-extension/src/index.ts b/extensions/assistant-extension/src/index.ts index bb253bd7f..9fc518c36 100644 --- a/extensions/assistant-extension/src/index.ts +++ b/extensions/assistant-extension/src/index.ts @@ -1,7 +1,14 @@ -import { Assistant, AssistantExtension } from '@janhq/core' - +import { Assistant, AssistantExtension, fs, joinPath } from '@janhq/core' export default class JanAssistantExtension extends AssistantExtension { - async onLoad() {} + async onLoad() { + if (!(await fs.existsSync('file://assistants'))) { + await fs.mkdir('file://assistants') + } + const assistants = await this.getAssistants() + if (assistants.length === 0) { + await this.createAssistant(this.defaultAssistant) + } + } /** * Called when the extension is unloaded. @@ -9,23 +16,66 @@ export default class JanAssistantExtension extends AssistantExtension { onUnload(): void {} async getAssistants(): Promise { - return [this.defaultAssistant] + if (!(await fs.existsSync('file://assistants'))) + return [this.defaultAssistant] + const assistants = await fs.readdirSync('file://assistants') + const assistantsData: Assistant[] = [] + for (const assistant of assistants) { + const assistantPath = await joinPath([ + 'file://assistants', + assistant, + 'assistant.json', + ]) + if (!(await fs.existsSync(assistantPath))) { + console.warn(`Assistant file not found: ${assistantPath}`) + continue + } + try { + const assistantData = JSON.parse(await fs.readFileSync(assistantPath)) + assistantsData.push(assistantData as Assistant) + } catch (error) { + console.error(`Failed to read assistant ${assistant}:`, error) + } + } + return assistantsData } - /** DEPRECATED */ - async createAssistant(assistant: Assistant): Promise {} - async deleteAssistant(assistant: Assistant): Promise {} + async createAssistant(assistant: Assistant): Promise { + const assistantPath = await joinPath([ + 'file://assistants', + assistant.id, + 'assistant.json', + ]) + const assistantFolder = await joinPath(['file://assistants', assistant.id]) + if (!(await fs.existsSync(assistantFolder))) { + await fs.mkdir(assistantFolder) + } + await fs.writeFileSync(assistantPath, JSON.stringify(assistant, null, 2)) + } + + async deleteAssistant(assistant: Assistant): Promise { + const assistantPath = await joinPath([ + 'file://assistants', + assistant.id, + 'assistant.json', + ]) + if (await fs.existsSync(assistantPath)) { + await fs.unlinkSync(assistantPath) + } + } private defaultAssistant: Assistant = { - avatar: '', + avatar: '👋', thread_location: undefined, id: 'jan', object: 'assistant', created_at: Date.now() / 1000, name: 'Jan', - description: 'A default assistant that can use all downloaded models', + description: + 'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf.', model: '*', - instructions: '', + instructions: + 'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf. Respond naturally and concisely, take actions when needed, and guide the user toward their goals.', tools: [ { type: 'retrieval', diff --git a/extensions/inference-cortex-extension/resources/default_settings.json b/extensions/inference-cortex-extension/resources/default_settings.json index 5574128e5..f9f1bf651 100644 --- a/extensions/inference-cortex-extension/resources/default_settings.json +++ b/extensions/inference-cortex-extension/resources/default_settings.json @@ -52,7 +52,6 @@ "value": true } }, - { "key": "caching_enabled", "title": "Caching", diff --git a/src-tauri/src/core/cmd.rs b/src-tauri/src/core/cmd.rs index a52c75684..7c8470d7d 100644 --- a/src-tauri/src/core/cmd.rs +++ b/src-tauri/src/core/cmd.rs @@ -156,7 +156,6 @@ pub fn get_configuration_file_path(app_handle: tauri::AppHandle) }); let package_name = env!("CARGO_PKG_NAME"); - log::debug!("Package name: {}", package_name); #[cfg(target_os = "linux")] let old_data_dir = { if let Some(config_path) = dirs::config_dir() { diff --git a/src-tauri/src/core/fs.rs b/src-tauri/src/core/fs.rs index 7b3afa8c6..4aa8e65dc 100644 --- a/src-tauri/src/core/fs.rs +++ b/src-tauri/src/core/fs.rs @@ -97,6 +97,20 @@ pub fn read_file_sync( fs::read_to_string(&path).map_err(|e| e.to_string()) } +#[tauri::command] +pub fn write_file_sync( + app_handle: tauri::AppHandle, + args: Vec, +) -> Result<(), String> { + if args.len() < 2 || args[0].is_empty() || args[1].is_empty() { + return Err("write_file_sync error: Invalid argument".to_string()); + } + + let path = resolve_path(app_handle, &args[0]); + let content = &args[1]; + fs::write(&path, content).map_err(|e| e.to_string()) +} + #[tauri::command] pub fn readdir_sync( app_handle: tauri::AppHandle, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c34353d22..e6f21a6c8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -30,6 +30,7 @@ pub fn run() { core::fs::read_file_sync, core::fs::rm, core::fs::file_stat, + core::fs::write_file_sync, // App commands core::cmd::get_themes, core::cmd::get_app_configurations, diff --git a/web-app/src/hooks/useAssistant.ts b/web-app/src/hooks/useAssistant.ts index 559f5530a..12feacdee 100644 --- a/web-app/src/hooks/useAssistant.ts +++ b/web-app/src/hooks/useAssistant.ts @@ -1,6 +1,6 @@ -import { localStorageKey } from '@/constants/localStorage' +import { createAssistant, deleteAssistant } from '@/services/assistants' +import { Assistant as CoreAssistant } from '@janhq/core' import { create } from 'zustand' -import { persist } from 'zustand/middleware' interface AssistantState { assistants: Assistant[] @@ -9,46 +9,59 @@ interface AssistantState { updateAssistant: (assistant: Assistant) => void deleteAssistant: (id: string) => void setCurrentAssistant: (assistant: Assistant) => void + setAssistants: (assistants: Assistant[]) => void } export const defaultAssistant: Assistant = { - avatar: '👋', id: 'jan', name: 'Jan', created_at: 1747029866.542, - description: 'A default assistant that can use all downloaded models.', - instructions: '', parameters: {}, + avatar: '👋', + description: + 'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf.', + instructions: + 'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf. Respond naturally and concisely, take actions when needed, and guide the user toward their goals.', } -export const useAssistant = create()( - persist( - (set, get) => ({ - assistants: [defaultAssistant], - currentAssistant: defaultAssistant, - addAssistant: (assistant) => - set({ assistants: [...get().assistants, assistant] }), - updateAssistant: (assistant) => { - const state = get() - set({ - assistants: state.assistants.map((a) => - a.id === assistant.id ? assistant : a - ), - // Update currentAssistant if it's the same assistant being updated - currentAssistant: - state.currentAssistant.id === assistant.id - ? assistant - : state.currentAssistant, - }) - }, - deleteAssistant: (id) => - set({ assistants: get().assistants.filter((a) => a.id !== id) }), - setCurrentAssistant: (assistant) => { - set({ currentAssistant: assistant }) - }, - }), - { - name: localStorageKey.assistant, - } - ) -) +export const useAssistant = create()((set, get) => ({ + assistants: [defaultAssistant], + currentAssistant: defaultAssistant, + addAssistant: (assistant) => { + set({ assistants: [...get().assistants, assistant] }) + createAssistant(assistant as unknown as CoreAssistant).catch((error) => { + console.error('Failed to create assistant:', error) + }) + }, + updateAssistant: (assistant) => { + const state = get() + set({ + assistants: state.assistants.map((a) => + a.id === assistant.id ? assistant : a + ), + // Update currentAssistant if it's the same assistant being updated + currentAssistant: + state.currentAssistant.id === assistant.id + ? assistant + : state.currentAssistant, + }) + // Create assistant already cover update logic + createAssistant(assistant as unknown as CoreAssistant).catch((error) => { + console.error('Failed to update assistant:', error) + }) + }, + deleteAssistant: (id) => { + deleteAssistant( + get().assistants.find((e) => e.id === id) as unknown as CoreAssistant + ).catch((error) => { + console.error('Failed to delete assistant:', error) + }) + set({ assistants: get().assistants.filter((a) => a.id !== id) }) + }, + setCurrentAssistant: (assistant) => { + set({ currentAssistant: assistant }) + }, + setAssistants: (assistants) => { + set({ assistants }) + }, +})) diff --git a/web-app/src/providers/DataProvider.tsx b/web-app/src/providers/DataProvider.tsx index 73333c5a2..f31a9ad64 100644 --- a/web-app/src/providers/DataProvider.tsx +++ b/web-app/src/providers/DataProvider.tsx @@ -10,6 +10,8 @@ import { ModelManager } from '@janhq/core' import { useEffect } from 'react' import { useMCPServers } from '@/hooks/useMCPServers' import { getMCPConfig } from '@/services/mcp' +import { useAssistant } from '@/hooks/useAssistant' +import { getAssistants } from '@/services/assistants' export function DataProvider() { const { setProviders } = useModelProvider() @@ -17,6 +19,7 @@ export function DataProvider() { const { setMessages } = useMessages() const { checkForUpdate } = useAppUpdater() const { setServers } = useMCPServers() + const { setAssistants } = useAssistant() useEffect(() => { fetchModels().then((models) => { @@ -24,6 +27,9 @@ export function DataProvider() { getProviders().then(setProviders) }) getMCPConfig().then((data) => setServers(data.mcpServers ?? [])) + getAssistants().then((data) => + setAssistants((data as unknown as Assistant[]) ?? []) + ) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) diff --git a/web-app/src/services/assistants.ts b/web-app/src/services/assistants.ts new file mode 100644 index 000000000..6c5b7c97a --- /dev/null +++ b/web-app/src/services/assistants.ts @@ -0,0 +1,32 @@ +import { ExtensionManager } from '@/lib/extension' +import { Assistant, AssistantExtension, ExtensionTypeEnum } from '@janhq/core' + +/** + * Fetches all available assistants. + * @returns A promise that resolves to the assistants. + */ +export const getAssistants = async () => { + return ExtensionManager.getInstance() + .get(ExtensionTypeEnum.Assistant) + ?.getAssistants() +} + +/** + * Creates a new assistant. + * @param assistant The assistant to create. + */ +export const createAssistant = async (assistant: Assistant) => { + return ExtensionManager.getInstance() + .get(ExtensionTypeEnum.Assistant) + ?.createAssistant(assistant) +} +/** + * Deletes an existing assistant. + * @param assistant The assistant to delete. + * @return A promise that resolves when the assistant is deleted. + */ +export const deleteAssistant = async (assistant: Assistant) => { + return ExtensionManager.getInstance() + .get(ExtensionTypeEnum.Assistant) + ?.deleteAssistant(assistant) +} diff --git a/web-app/src/services/models.ts b/web-app/src/services/models.ts index 1d9405b9c..7fa8dcb85 100644 --- a/web-app/src/services/models.ts +++ b/web-app/src/services/models.ts @@ -161,7 +161,12 @@ export const deleteModel = async (id: string) => { if (!extension) throw new Error('Model extension not found') try { - return await extension.deleteModel(id) + return await extension.deleteModel(id).then(() => { + // TODO: This should be removed when we integrate new llama.cpp extension + if (id.includes(':')) { + extension.addSource(`cortexso/${id.split(':')[0]}`) + } + }) } catch (error) { console.error('Failed to delete model:', error) throw error