diff --git a/extensions-web/src/assistant-web/index.ts b/extensions-web/src/assistant-web/index.ts deleted file mode 100644 index 0a800d36d..000000000 --- a/extensions-web/src/assistant-web/index.ts +++ /dev/null @@ -1,198 +0,0 @@ -/** - * Web Assistant Extension - * Implements assistant management using IndexedDB - */ - -import { Assistant, AssistantExtension } from '@janhq/core' -import { getSharedDB } from '../shared/db' - -export default class AssistantExtensionWeb extends AssistantExtension { - private db: IDBDatabase | null = null - - private defaultAssistant: Assistant = { - avatar: '👋', - thread_location: undefined, - id: 'jan', - object: 'assistant', - created_at: Date.now() / 1000, - name: 'Jan', - 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: - 'You are a helpful AI assistant. Your primary goal is to assist users with their questions and tasks to the best of your abilities.\n\n' + - 'When responding:\n' + - '- Answer directly from your knowledge when you can\n' + - '- Be concise, clear, and helpful\n' + - '- Admit when you\'re unsure rather than making things up\n\n' + - 'If tools are available to you:\n' + - '- Only use tools when they add real value to your response\n' + - '- Use tools when the user explicitly asks (e.g., "search for...", "calculate...", "run this code")\n' + - '- Use tools for information you don\'t know or that needs verification\n' + - '- Never use tools just because they\'re available\n\n' + - 'When using tools:\n' + - '- Use one tool at a time and wait for results\n' + - '- Use actual values as arguments, not variable names\n' + - '- Learn from each result before deciding next steps\n' + - '- Avoid repeating the same tool call with identical parameters\n\n' + - 'Remember: Most questions can be answered without tools. Think first whether you need them.\n\n' + - 'Current date: {{current_date}}', - tools: [ - { - type: 'retrieval', - enabled: false, - useTimeWeightedRetriever: false, - settings: { - top_k: 2, - chunk_size: 1024, - chunk_overlap: 64, - retrieval_template: `Use the following pieces of context to answer the question at the end. -{context} -Question: {question} -Helpful Answer:`, - }, - }, - ], - file_ids: [], - metadata: undefined, - } - - async onLoad() { - console.log('Loading Web Assistant Extension') - this.db = await getSharedDB() - - // Create default assistant if none exist - const assistants = await this.getAssistants() - if (assistants.length === 0) { - await this.createAssistant(this.defaultAssistant) - } - } - - onUnload() { - // Don't close shared DB, other extensions might be using it - this.db = null - } - - private ensureDB(): void { - if (!this.db) { - throw new Error('Database not initialized. Call onLoad() first.') - } - } - - async getAssistants(): Promise { - this.ensureDB() - - return new Promise((resolve, reject) => { - const transaction = this.db!.transaction(['assistants'], 'readonly') - const store = transaction.objectStore('assistants') - const request = store.getAll() - - request.onsuccess = () => { - resolve(request.result || []) - } - - request.onerror = () => { - reject(request.error) - } - }) - } - - async createAssistant(assistant: Assistant): Promise { - this.ensureDB() - - return new Promise((resolve, reject) => { - const transaction = this.db!.transaction(['assistants'], 'readwrite') - const store = transaction.objectStore('assistants') - - const assistantToStore = { - ...assistant, - created_at: assistant.created_at || Date.now() / 1000, - } - - const request = store.add(assistantToStore) - - request.onsuccess = () => { - console.log('Assistant created:', assistant.id) - resolve() - } - - request.onerror = () => { - console.error('Failed to create assistant:', request.error) - reject(request.error) - } - }) - } - - async updateAssistant(id: string, assistant: Partial): Promise { - this.ensureDB() - - return new Promise((resolve, reject) => { - const transaction = this.db!.transaction(['assistants'], 'readwrite') - const store = transaction.objectStore('assistants') - - // First get the existing assistant - const getRequest = store.get(id) - - getRequest.onsuccess = () => { - const existingAssistant = getRequest.result - if (!existingAssistant) { - reject(new Error(`Assistant with id ${id} not found`)) - return - } - - const updatedAssistant = { - ...existingAssistant, - ...assistant, - id, // Ensure ID doesn't change - } - - const putRequest = store.put(updatedAssistant) - - putRequest.onsuccess = () => resolve() - putRequest.onerror = () => reject(putRequest.error) - } - - getRequest.onerror = () => { - reject(getRequest.error) - } - }) - } - - async deleteAssistant(assistant: Assistant): Promise { - this.ensureDB() - - return new Promise((resolve, reject) => { - const transaction = this.db!.transaction(['assistants'], 'readwrite') - const store = transaction.objectStore('assistants') - const request = store.delete(assistant.id) - - request.onsuccess = () => { - console.log('Assistant deleted:', assistant.id) - resolve() - } - - request.onerror = () => { - console.error('Failed to delete assistant:', request.error) - reject(request.error) - } - }) - } - - async getAssistant(id: string): Promise { - this.ensureDB() - - return new Promise((resolve, reject) => { - const transaction = this.db!.transaction(['assistants'], 'readonly') - const store = transaction.objectStore('assistants') - const request = store.get(id) - - request.onsuccess = () => { - resolve(request.result || null) - } - - request.onerror = () => { - reject(request.error) - } - }) - } -} \ No newline at end of file diff --git a/extensions-web/src/index.ts b/extensions-web/src/index.ts index e45c2d71c..9e7f3aab3 100644 --- a/extensions-web/src/index.ts +++ b/extensions-web/src/index.ts @@ -5,18 +5,16 @@ import type { WebExtensionRegistry } from './types' -export { default as AssistantExtensionWeb } from './assistant-web' export { default as ConversationalExtensionWeb } from './conversational-web' export { default as JanProviderWeb } from './jan-provider-web' export { default as MCPExtensionWeb } from './mcp-web' // Re-export types -export type { - WebExtensionRegistry, +export type { + WebExtensionRegistry, WebExtensionModule, WebExtensionName, WebExtensionLoader, - AssistantWebModule, ConversationalWebModule, JanProviderWebModule, MCPWebModule @@ -24,7 +22,6 @@ export type { // Extension registry for dynamic loading export const WEB_EXTENSIONS: WebExtensionRegistry = { - 'assistant-web': () => import('./assistant-web'), 'conversational-web': () => import('./conversational-web'), 'jan-provider-web': () => import('./jan-provider-web'), 'mcp-web': () => import('./mcp-web'), diff --git a/extensions-web/src/types.ts b/extensions-web/src/types.ts index f98d761cc..47ef0be71 100644 --- a/extensions-web/src/types.ts +++ b/extensions-web/src/types.ts @@ -2,14 +2,10 @@ * Web Extension Types */ -import type { AssistantExtension, ConversationalExtension, BaseExtension, AIEngine, MCPExtension } from '@janhq/core' +import type { ConversationalExtension, BaseExtension, AIEngine, MCPExtension } from '@janhq/core' type ExtensionConstructorParams = ConstructorParameters -export interface AssistantWebModule { - default: new (...args: ExtensionConstructorParams) => AssistantExtension -} - export interface ConversationalWebModule { default: new (...args: ExtensionConstructorParams) => ConversationalExtension } @@ -22,10 +18,9 @@ export interface MCPWebModule { default: new (...args: ExtensionConstructorParams) => MCPExtension } -export type WebExtensionModule = AssistantWebModule | ConversationalWebModule | JanProviderWebModule | MCPWebModule +export type WebExtensionModule = ConversationalWebModule | JanProviderWebModule | MCPWebModule export interface WebExtensionRegistry { - 'assistant-web': () => Promise 'conversational-web': () => Promise 'jan-provider-web': () => Promise 'mcp-web': () => Promise diff --git a/web-app/src/containers/DropdownAssistant.tsx b/web-app/src/containers/DropdownAssistant.tsx index 44a24e5e6..a75925002 100644 --- a/web-app/src/containers/DropdownAssistant.tsx +++ b/web-app/src/containers/DropdownAssistant.tsx @@ -28,7 +28,7 @@ const DropdownAssistant = () => { ) const selectedAssistant = - assistants.find((a) => a.id === currentAssistant.id) || assistants[0] + assistants.find((a) => a.id === currentAssistant?.id) || assistants[0] return ( <> diff --git a/web-app/src/containers/LeftPanel.tsx b/web-app/src/containers/LeftPanel.tsx index 2f4ce9ecc..8ddfdbd36 100644 --- a/web-app/src/containers/LeftPanel.tsx +++ b/web-app/src/containers/LeftPanel.tsx @@ -46,7 +46,7 @@ const mainMenus = [ title: 'common:assistants', icon: IconClipboardSmileFilled, route: route.assistant, - isEnabled: true, + isEnabled: PlatformFeatures[PlatformFeature.ASSISTANTS], }, { title: 'common:hub', diff --git a/web-app/src/hooks/useAppState.ts b/web-app/src/hooks/useAppState.ts index fe885e043..837ed8c38 100644 --- a/web-app/src/hooks/useAppState.ts +++ b/web-app/src/hooks/useAppState.ts @@ -50,7 +50,7 @@ export const useAppState = create()((set) => ({ const currentAssistant = useAssistant.getState().currentAssistant const selectedAssistant = - assistants.find((a) => a.id === currentAssistant.id) || assistants[0] + assistants.find((a) => a.id === currentAssistant?.id) || assistants[0] set(() => ({ streamingContent: content diff --git a/web-app/src/hooks/useAssistant.ts b/web-app/src/hooks/useAssistant.ts index eab1fffc9..577ff1283 100644 --- a/web-app/src/hooks/useAssistant.ts +++ b/web-app/src/hooks/useAssistant.ts @@ -2,10 +2,12 @@ import { getServiceHub } from '@/hooks/useServiceHub' import { Assistant as CoreAssistant } from '@janhq/core' import { create } from 'zustand' import { localStorageKey } from '@/constants/localStorage' +import { PlatformFeatures } from '@/lib/platform/const' +import { PlatformFeature } from '@/lib/platform/types' interface AssistantState { assistants: Assistant[] - currentAssistant: Assistant + currentAssistant: Assistant | null addAssistant: (assistant: Assistant) => void updateAssistant: (assistant: Assistant) => void deleteAssistant: (id: string) => void @@ -46,14 +48,31 @@ export const defaultAssistant: Assistant = { 'You are a helpful AI assistant. Your primary goal is to assist users with their questions and tasks to the best of your abilities.\n\nWhen responding:\n- Answer directly from your knowledge when you can\n- Be concise, clear, and helpful\n- Admit when you’re unsure rather than making things up\n\nIf tools are available to you:\n- Only use tools when they add real value to your response\n- Use tools when the user explicitly asks (e.g., "search for...", "calculate...", "run this code")\n- Use tools for information you don’t know or that needs verification\n- Never use tools just because they’re available\n\nWhen using tools:\n- Use one tool at a time and wait for results\n- Use actual values as arguments, not variable names\n- Learn from each result before deciding next steps\n- Avoid repeating the same tool call with identical parameters\n\nRemember: Most questions can be answered without tools. Think first whether you need them.\n\nCurrent date: {{current_date}}', } -export const useAssistant = create()((set, get) => ({ - assistants: [defaultAssistant], - currentAssistant: defaultAssistant, +// Platform-aware initial state +const getInitialAssistantState = () => { + if (PlatformFeatures[PlatformFeature.ASSISTANTS]) { + return { + assistants: [defaultAssistant], + currentAssistant: defaultAssistant, + } + } else { + return { + assistants: [], + currentAssistant: null, + } + } +} + +export const useAssistant = create((set, get) => ({ + ...getInitialAssistantState(), addAssistant: (assistant) => { set({ assistants: [...get().assistants, assistant] }) - getServiceHub().assistants().createAssistant(assistant as unknown as CoreAssistant).catch((error) => { - console.error('Failed to create assistant:', error) - }) + getServiceHub() + .assistants() + .createAssistant(assistant as unknown as CoreAssistant) + .catch((error) => { + console.error('Failed to create assistant:', error) + }) }, updateAssistant: (assistant) => { const state = get() @@ -63,25 +82,31 @@ export const useAssistant = create()((set, get) => ({ ), // Update currentAssistant if it's the same assistant being updated currentAssistant: - state.currentAssistant.id === assistant.id + state.currentAssistant?.id === assistant.id ? assistant : state.currentAssistant, }) // Create assistant already cover update logic - getServiceHub().assistants().createAssistant(assistant as unknown as CoreAssistant).catch((error) => { - console.error('Failed to update assistant:', error) - }) + getServiceHub() + .assistants() + .createAssistant(assistant as unknown as CoreAssistant) + .catch((error) => { + console.error('Failed to update assistant:', error) + }) }, deleteAssistant: (id) => { const state = get() - getServiceHub().assistants().deleteAssistant( - state.assistants.find((e) => e.id === id) as unknown as CoreAssistant - ).catch((error) => { - console.error('Failed to delete assistant:', error) - }) + getServiceHub() + .assistants() + .deleteAssistant( + state.assistants.find((e) => e.id === id) as unknown as CoreAssistant + ) + .catch((error) => { + console.error('Failed to delete assistant:', error) + }) // Check if we're deleting the current assistant - const wasCurrentAssistant = state.currentAssistant.id === id + const wasCurrentAssistant = state.currentAssistant?.id === id set({ assistants: state.assistants.filter((a) => a.id !== id) }) diff --git a/web-app/src/hooks/useChat.ts b/web-app/src/hooks/useChat.ts index 029dfe722..f56a650b6 100644 --- a/web-app/src/hooks/useChat.ts +++ b/web-app/src/hooks/useChat.ts @@ -73,7 +73,7 @@ export const useChat = () => { }, [provider, selectedProvider]) const selectedAssistant = - assistants.find((a) => a.id === currentAssistant.id) || assistants[0] + assistants.find((a) => a.id === currentAssistant?.id) || assistants[0] const getCurrentThread = useCallback(async () => { let currentThread = retrieveThread() @@ -237,7 +237,7 @@ export const useChat = () => { const builder = new CompletionMessagesBuilder( messages, - renderInstructions(currentAssistant?.instructions) + currentAssistant ? renderInstructions(currentAssistant.instructions) : undefined ) if (troubleshooting) builder.addUserMessage(message, attachments) @@ -284,10 +284,10 @@ export const useChat = () => { builder.getMessages(), abortController, availableTools, - currentAssistant.parameters?.stream === false ? false : true, + currentAssistant?.parameters?.stream === false ? false : true, { ...modelSettings, - ...currentAssistant.parameters, + ...(currentAssistant?.parameters || {}), } as unknown as Record ) diff --git a/web-app/src/hooks/useMessages.ts b/web-app/src/hooks/useMessages.ts index 8dba73b9b..fc9dcf793 100644 --- a/web-app/src/hooks/useMessages.ts +++ b/web-app/src/hooks/useMessages.ts @@ -29,7 +29,7 @@ export const useMessages = create()((set, get) => ({ const currentAssistant = useAssistant.getState().currentAssistant const selectedAssistant = - assistants.find((a) => a.id === currentAssistant.id) || assistants[0] + assistants.find((a) => a.id === currentAssistant?.id) || assistants[0] const newMessage = { ...message, diff --git a/web-app/src/lib/platform/const.ts b/web-app/src/lib/platform/const.ts index 5192a6d1e..c8beccf94 100644 --- a/web-app/src/lib/platform/const.ts +++ b/web-app/src/lib/platform/const.ts @@ -49,4 +49,7 @@ export const PlatformFeatures: Record = { // Extensions settings page - disabled for web [PlatformFeature.EXTENSIONS_SETTINGS]: isPlatformTauri(), + + // Assistant functionality - disabled for web + [PlatformFeature.ASSISTANTS]: isPlatformTauri(), } \ No newline at end of file diff --git a/web-app/src/lib/platform/types.ts b/web-app/src/lib/platform/types.ts index 48d917cab..64a8a2367 100644 --- a/web-app/src/lib/platform/types.ts +++ b/web-app/src/lib/platform/types.ts @@ -51,4 +51,7 @@ export enum PlatformFeature { // Extensions settings page management EXTENSIONS_SETTINGS = 'extensionsSettings', + + // Assistant functionality (creation, editing, management) + ASSISTANTS = 'assistants', } diff --git a/web-app/src/routes/assistant.tsx b/web-app/src/routes/assistant.tsx index 22e913445..bf4fd928c 100644 --- a/web-app/src/routes/assistant.tsx +++ b/web-app/src/routes/assistant.tsx @@ -10,6 +10,8 @@ import AddEditAssistant from '@/containers/dialogs/AddEditAssistant' import { DeleteAssistantDialog } from '@/containers/dialogs' import { AvatarEmoji } from '@/containers/AvatarEmoji' import { useTranslation } from '@/i18n/react-i18next-compat' +import { PlatformGuard } from '@/lib/platform/PlatformGuard' +import { PlatformFeature } from '@/lib/platform/types' // eslint-disable-next-line @typescript-eslint/no-explicit-any export const Route = createFileRoute(route.assistant as any)({ @@ -17,6 +19,14 @@ export const Route = createFileRoute(route.assistant as any)({ }) function Assistant() { + return ( + + + + ) +} + +function AssistantContent() { const { t } = useTranslation() const { assistants, addAssistant, updateAssistant, deleteAssistant } = useAssistant() diff --git a/web-app/src/routes/index.tsx b/web-app/src/routes/index.tsx index 4ff643356..a23b29de4 100644 --- a/web-app/src/routes/index.tsx +++ b/web-app/src/routes/index.tsx @@ -18,6 +18,8 @@ type SearchParams = { import DropdownAssistant from '@/containers/DropdownAssistant' import { useEffect } from 'react' import { useThreads } from '@/hooks/useThreads' +import { PlatformFeatures } from '@/lib/platform/const' +import { PlatformFeature } from '@/lib/platform/types' export const Route = createFileRoute(route.home as any)({ component: Index, @@ -54,7 +56,7 @@ function Index() { return (
- + {PlatformFeatures[PlatformFeature.ASSISTANTS] && }
diff --git a/web-app/src/routes/threads/$threadId.tsx b/web-app/src/routes/threads/$threadId.tsx index 6f2a83de8..a7c62c807 100644 --- a/web-app/src/routes/threads/$threadId.tsx +++ b/web-app/src/routes/threads/$threadId.tsx @@ -24,6 +24,8 @@ import { useTranslation } from '@/i18n/react-i18next-compat' import { useChat } from '@/hooks/useChat' import { useSmallScreen } from '@/hooks/useMediaQuery' import { useTools } from '@/hooks/useTools' +import { PlatformFeatures } from '@/lib/platform/const' +import { PlatformFeature } from '@/lib/platform/types' // as route.threadsDetail export const Route = createFileRoute('/threads/$threadId')({ @@ -300,10 +302,10 @@ function ThreadDetail() {
- + {PlatformFeatures[PlatformFeature.ASSISTANTS] && }
-
+
({ mcpAutoApproveTools: false, mcpServersSettings: true, extensionsSettings: true, + assistants: true, } }))