remove assistant from web (#6468)

This commit is contained in:
Dinh Long Nguyen 2025-09-15 23:53:59 +07:00 committed by GitHub
parent cf87313f28
commit 491012fa87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 78 additions and 238 deletions

View File

@ -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<Assistant[]> {
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<void> {
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<Assistant>): Promise<void> {
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<void> {
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<Assistant | null> {
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)
}
})
}
}

View File

@ -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'),

View File

@ -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<typeof BaseExtension>
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<AssistantWebModule>
'conversational-web': () => Promise<ConversationalWebModule>
'jan-provider-web': () => Promise<JanProviderWebModule>
'mcp-web': () => Promise<MCPWebModule>

View File

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

View File

@ -46,7 +46,7 @@ const mainMenus = [
title: 'common:assistants',
icon: IconClipboardSmileFilled,
route: route.assistant,
isEnabled: true,
isEnabled: PlatformFeatures[PlatformFeature.ASSISTANTS],
},
{
title: 'common:hub',

View File

@ -50,7 +50,7 @@ export const useAppState = create<AppState>()((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

View File

@ -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 youre 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 dont know or that needs verification\n- Never use tools just because theyre 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<AssistantState>()((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<AssistantState>((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<AssistantState>()((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) })

View File

@ -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<string, object>
)

View File

@ -29,7 +29,7 @@ export const useMessages = create<MessageState>()((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,

View File

@ -49,4 +49,7 @@ export const PlatformFeatures: Record<PlatformFeature, boolean> = {
// Extensions settings page - disabled for web
[PlatformFeature.EXTENSIONS_SETTINGS]: isPlatformTauri(),
// Assistant functionality - disabled for web
[PlatformFeature.ASSISTANTS]: isPlatformTauri(),
}

View File

@ -51,4 +51,7 @@ export enum PlatformFeature {
// Extensions settings page management
EXTENSIONS_SETTINGS = 'extensionsSettings',
// Assistant functionality (creation, editing, management)
ASSISTANTS = 'assistants',
}

View File

@ -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 (
<PlatformGuard feature={PlatformFeature.ASSISTANTS}>
<AssistantContent />
</PlatformGuard>
)
}
function AssistantContent() {
const { t } = useTranslation()
const { assistants, addAssistant, updateAssistant, deleteAssistant } =
useAssistant()

View File

@ -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 (
<div className="flex h-full flex-col flex-justify-center">
<HeaderPage>
<DropdownAssistant />
{PlatformFeatures[PlatformFeature.ASSISTANTS] && <DropdownAssistant />}
</HeaderPage>
<div className="h-full px-4 md:px-8 overflow-y-auto flex flex-col gap-2 justify-center">
<div className="w-full md:w-4/6 mx-auto">

View File

@ -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() {
<div className="flex flex-col h-full">
<HeaderPage>
<div className="flex items-center justify-between w-full pr-2">
<DropdownAssistant />
{PlatformFeatures[PlatformFeature.ASSISTANTS] && <DropdownAssistant />}
</div>
</HeaderPage>
<div className="flex flex-col h-[calc(100%-40px)] ">
<div className="flex flex-col h-[calc(100%-40px)]">
<div
ref={scrollContainerRef}
onScroll={handleScroll}

View File

@ -23,6 +23,7 @@ vi.mock('@/lib/platform/const', () => ({
mcpAutoApproveTools: false,
mcpServersSettings: true,
extensionsSettings: true,
assistants: true,
}
}))