From f33c2c205a1804b77c77fe9769f94da4c5415994 Mon Sep 17 00:00:00 2001 From: Dinh Long Nguyen Date: Tue, 30 Sep 2025 21:39:08 +0700 Subject: [PATCH] feat: web add search button for extension (#6671) * add search button for web extension * change button color and behavior * Update extensions-web/src/mcp-web/components/WebSearchButton.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core/package.json | 5 + core/rolldown.config.mjs | 2 +- core/src/browser/extensions/mcp.ts | 9 +- core/src/types/mcp/mcpEntity.ts | 14 ++ extensions-web/package.json | 5 + .../mcp-web/components/WebSearchButton.tsx | 54 ++++++++ .../src/mcp-web/components/index.ts | 1 + extensions-web/src/mcp-web/index.ts | 12 +- extensions-web/tsconfig.json | 1 + extensions-web/vite.config.ts | 2 +- web-app/src/containers/ChatInput.tsx | 121 ++++++++++-------- .../src/containers/McpExtensionToolLoader.tsx | 61 +++++++++ yarn.lock | 45 ++++--- 13 files changed, 263 insertions(+), 69 deletions(-) create mode 100644 extensions-web/src/mcp-web/components/WebSearchButton.tsx create mode 100644 extensions-web/src/mcp-web/components/index.ts create mode 100644 web-app/src/containers/McpExtensionToolLoader.tsx diff --git a/core/package.json b/core/package.json index eec56a733..203eaf293 100644 --- a/core/package.json +++ b/core/package.json @@ -27,11 +27,13 @@ "devDependencies": { "@npmcli/arborist": "^7.1.0", "@types/node": "^22.10.0", + "@types/react": "19.1.2", "@vitest/coverage-v8": "^2.1.8", "@vitest/ui": "^2.1.8", "eslint": "8.57.0", "happy-dom": "^15.11.6", "pacote": "^21.0.0", + "react": "19.0.0", "request": "^2.88.2", "request-progress": "^3.0.0", "rimraf": "^6.0.1", @@ -44,5 +46,8 @@ "rxjs": "^7.8.1", "ulidx": "^2.3.0" }, + "peerDependencies": { + "react": "19.0.0" + }, "packageManager": "yarn@4.5.3" } diff --git a/core/rolldown.config.mjs b/core/rolldown.config.mjs index fd3329ee0..fbb2bd351 100644 --- a/core/rolldown.config.mjs +++ b/core/rolldown.config.mjs @@ -10,7 +10,7 @@ export default defineConfig([ sourcemap: true, }, platform: 'browser', - external: ['path'], + external: ['path', 'react', 'react-dom', 'react/jsx-runtime'], define: { NODE: JSON.stringify(`${pkgJson.name}/${pkgJson.node}`), VERSION: JSON.stringify(pkgJson.version), diff --git a/core/src/browser/extensions/mcp.ts b/core/src/browser/extensions/mcp.ts index 7f30a5428..d34e0e298 100644 --- a/core/src/browser/extensions/mcp.ts +++ b/core/src/browser/extensions/mcp.ts @@ -1,5 +1,6 @@ -import { MCPInterface, MCPTool, MCPToolCallResult } from '../../types' +import { MCPInterface, MCPTool, MCPToolCallResult, MCPToolComponentProps } from '../../types' import { BaseExtension, ExtensionTypeEnum } from '../extension' +import type { ComponentType } from 'react' /** * MCP (Model Context Protocol) extension for managing tools and server communication. @@ -18,4 +19,10 @@ export abstract class MCPExtension extends BaseExtension implements MCPInterface abstract getConnectedServers(): Promise abstract refreshTools(): Promise abstract isHealthy(): Promise + + /** + * Optional method to provide a custom UI component for tools + * @returns A React component or null if no custom component is provided + */ + getToolComponent?(): ComponentType | null } \ No newline at end of file diff --git a/core/src/types/mcp/mcpEntity.ts b/core/src/types/mcp/mcpEntity.ts index a2259e52e..a1d3c5bd3 100644 --- a/core/src/types/mcp/mcpEntity.ts +++ b/core/src/types/mcp/mcpEntity.ts @@ -21,4 +21,18 @@ export interface MCPServerInfo { name: string connected: boolean tools?: MCPTool[] +} + +/** + * Props for MCP tool UI components + */ +export interface MCPToolComponentProps { + /** List of available MCP tools */ + tools: MCPTool[] + + /** Function to check if a specific tool is currently enabled */ + isToolEnabled: (toolName: string) => boolean + + /** Function to toggle a tool's enabled/disabled state */ + onToolToggle: (toolName: string, enabled: boolean) => void } \ No newline at end of file diff --git a/extensions-web/package.json b/extensions-web/package.json index 232ba13fa..aa536e9fe 100644 --- a/extensions-web/package.json +++ b/extensions-web/package.json @@ -22,6 +22,9 @@ }, "devDependencies": { "@janhq/core": "workspace:*", + "@tabler/icons-react": "^3.34.0", + "@types/react": "19.1.2", + "react": "19.0.0", "typescript": "5.9.2", "vite": "5.4.20", "vitest": "2.1.9", @@ -29,6 +32,8 @@ }, "peerDependencies": { "@janhq/core": "*", + "@tabler/icons-react": "*", + "react": "19.0.0", "zustand": "5.0.3" }, "dependencies": { diff --git a/extensions-web/src/mcp-web/components/WebSearchButton.tsx b/extensions-web/src/mcp-web/components/WebSearchButton.tsx new file mode 100644 index 000000000..47744de94 --- /dev/null +++ b/extensions-web/src/mcp-web/components/WebSearchButton.tsx @@ -0,0 +1,54 @@ +import { useMemo, useCallback } from 'react' +import { IconWorld } from '@tabler/icons-react' +import { MCPToolComponentProps } from '@janhq/core' + +// List of tool names considered as web search tools +const WEB_SEARCH_TOOL_NAMES = ['google_search', 'scrape']; + +export const WebSearchButton = ({ + tools, + isToolEnabled, + onToolToggle, +}: MCPToolComponentProps) => { + const webSearchTools = useMemo( + () => tools.filter((tool) => WEB_SEARCH_TOOL_NAMES.includes(tool.name)), + [tools] + ) + + // Early return if no web search tools available + if (webSearchTools.length === 0) { + return null + } + + // Check if all web search tools are enabled + const isEnabled = useMemo( + () => webSearchTools.every((tool) => isToolEnabled(tool.name)), + [webSearchTools, isToolEnabled] + ) + + const handleToggle = useCallback(() => { + // Toggle all web search tools at once + const newState = !isEnabled + webSearchTools.forEach((tool) => { + onToolToggle(tool.name, newState) + }) + }, [isEnabled, webSearchTools, onToolToggle]) + + return ( + + ) +} \ No newline at end of file diff --git a/extensions-web/src/mcp-web/components/index.ts b/extensions-web/src/mcp-web/components/index.ts new file mode 100644 index 000000000..9b22072aa --- /dev/null +++ b/extensions-web/src/mcp-web/components/index.ts @@ -0,0 +1 @@ +export { WebSearchButton } from './WebSearchButton' \ No newline at end of file diff --git a/extensions-web/src/mcp-web/index.ts b/extensions-web/src/mcp-web/index.ts index 5e13846a7..2400e74c0 100644 --- a/extensions-web/src/mcp-web/index.ts +++ b/extensions-web/src/mcp-web/index.ts @@ -4,11 +4,13 @@ * Uses official MCP TypeScript SDK with proper session handling */ -import { MCPExtension, MCPTool, MCPToolCallResult } from '@janhq/core' +import { MCPExtension, MCPTool, MCPToolCallResult, MCPToolComponentProps } from '@janhq/core' import { getSharedAuthService, JanAuthService } from '../shared' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import { JanMCPOAuthProvider } from './oauth-provider' +import { WebSearchButton } from './components' +import type { ComponentType } from 'react' // JAN_API_BASE is defined in vite.config.ts (defaults to 'https://api-dev.jan.ai/jan/v1') declare const JAN_API_BASE: string @@ -232,4 +234,12 @@ export default class MCPExtensionWeb extends MCPExtension { throw error } } + + /** + * Provides a custom UI component for web search tools + * @returns The WebSearchButton component + */ + getToolComponent(): ComponentType | null { + return WebSearchButton + } } \ No newline at end of file diff --git a/extensions-web/tsconfig.json b/extensions-web/tsconfig.json index e90dd4997..b39b50ee5 100644 --- a/extensions-web/tsconfig.json +++ b/extensions-web/tsconfig.json @@ -3,6 +3,7 @@ "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", + "jsx": "react-jsx", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "strict": true, diff --git a/extensions-web/vite.config.ts b/extensions-web/vite.config.ts index 89cfb7d0e..79ae4310d 100644 --- a/extensions-web/vite.config.ts +++ b/extensions-web/vite.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ fileName: 'index' }, rollupOptions: { - external: ['@janhq/core', 'zustand'] + external: ['@janhq/core', 'zustand', 'react', 'react-dom', 'react/jsx-runtime', '@tabler/icons-react'] }, emptyOutDir: false // Don't clean the output directory }, diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index cba580ebd..6e314a0f5 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -38,6 +38,9 @@ import { useTools } from '@/hooks/useTools' import { TokenCounter } from '@/components/TokenCounter' import { useMessages } from '@/hooks/useMessages' import { useShallow } from 'zustand/react/shallow' +import { McpExtensionToolLoader } from './McpExtensionToolLoader' +import { ExtensionTypeEnum, MCPExtension } from '@janhq/core' +import { ExtensionManager } from '@/lib/extension' type ChatInputProps = { className?: string @@ -171,6 +174,12 @@ const ChatInput = ({ // Check if there are active MCP servers const hasActiveMCPServers = connectedServers.length > 0 || tools.length > 0 + // Get MCP extension and its custom component + const extensionManager = ExtensionManager.getInstance() + const mcpExtension = extensionManager.get(ExtensionTypeEnum.MCP) + const MCPToolComponent = mcpExtension?.getToolComponent?.() + + const handleSendMesage = (prompt: string) => { if (!selectedModel) { setMessage('Please select a model to start chatting.') @@ -719,60 +728,72 @@ const ChatInput = ({ {selectedModel?.capabilities?.includes('tools') && hasActiveMCPServers && ( - - - + ) : ( + // Use default tools dropdown + + -
{ - setDropdownToolsAvailable(false) - e.stopPropagation() - }} + - { - setDropdownToolsAvailable(isOpen) - if (isOpen) { - setTooltipToolsAvailable(false) - } +
{ + setDropdownToolsAvailable(false) + e.stopPropagation() }} > - {(isOpen, toolsCount) => { - return ( -
- - {toolsCount > 0 && ( -
- - {toolsCount > 99 ? '99+' : toolsCount} - -
- )} -
- ) - }} - -
-
- -

{t('tools')}

-
- - + { + setDropdownToolsAvailable(isOpen) + if (isOpen) { + setTooltipToolsAvailable(false) + } + }} + > + {(isOpen, toolsCount) => { + return ( +
+ + {toolsCount > 0 && ( +
+ + {toolsCount > 99 ? '99+' : toolsCount} + +
+ )} +
+ ) + }} +
+
+
+ +

{t('tools')}

+
+
+
+ ) )} {selectedModel?.capabilities?.includes('web_search') && ( diff --git a/web-app/src/containers/McpExtensionToolLoader.tsx b/web-app/src/containers/McpExtensionToolLoader.tsx new file mode 100644 index 000000000..b4a80d82a --- /dev/null +++ b/web-app/src/containers/McpExtensionToolLoader.tsx @@ -0,0 +1,61 @@ +import { ComponentType } from 'react' +import { MCPTool, MCPToolComponentProps } from '@janhq/core' +import { useToolAvailable } from '@/hooks/useToolAvailable' +import { useThreads } from '@/hooks/useThreads' + +interface McpExtensionToolLoaderProps { + tools: MCPTool[] + hasActiveMCPServers: boolean + selectedModelHasTools: boolean + initialMessage?: boolean + MCPToolComponent?: ComponentType | null +} + +export const McpExtensionToolLoader = ({ + tools, + hasActiveMCPServers, + selectedModelHasTools, + initialMessage, + MCPToolComponent, +}: McpExtensionToolLoaderProps) => { + // Get tool management hooks + const { isToolDisabled, setToolDisabledForThread, setDefaultDisabledTools, getDefaultDisabledTools } = useToolAvailable() + const { getCurrentThread } = useThreads() + const currentThread = getCurrentThread() + + // Handle tool toggle for custom component + const handleToolToggle = (toolName: string, enabled: boolean) => { + if (initialMessage) { + const currentDefaults = getDefaultDisabledTools() + if (enabled) { + setDefaultDisabledTools(currentDefaults.filter((name) => name !== toolName)) + } else { + setDefaultDisabledTools([...currentDefaults, toolName]) + } + } else if (currentThread?.id) { + setToolDisabledForThread(currentThread.id, toolName, enabled) + } + } + + const isToolEnabled = (toolName: string): boolean => { + if (initialMessage) { + return !getDefaultDisabledTools().includes(toolName) + } else if (currentThread?.id) { + return !isToolDisabled(currentThread.id, toolName) + } + return false + } + + // Only render if we have the custom MCP component and conditions are met + if (!selectedModelHasTools || !hasActiveMCPServers || !MCPToolComponent) { + return null + } + + return ( + + ) +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2cb509689..ce900004b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3453,45 +3453,40 @@ __metadata: languageName: node linkType: hard -"@jan/extensions-web@link:../extensions-web::locator=%40janhq%2Fweb-app%40workspace%3Aweb-app": - version: 0.0.0-use.local - resolution: "@jan/extensions-web@link:../extensions-web::locator=%40janhq%2Fweb-app%40workspace%3Aweb-app" - languageName: node - linkType: soft - -"@jan/extensions-web@workspace:extensions-web": +"@jan/extensions-web@workspace:*, @jan/extensions-web@workspace:extensions-web": version: 0.0.0-use.local resolution: "@jan/extensions-web@workspace:extensions-web" dependencies: "@janhq/core": "workspace:*" "@modelcontextprotocol/sdk": "npm:1.17.5" + "@tabler/icons-react": "npm:^3.34.0" + "@types/react": "npm:19.1.2" + react: "npm:19.0.0" typescript: "npm:5.9.2" vite: "npm:5.4.20" vitest: "npm:2.1.9" zustand: "npm:5.0.8" peerDependencies: "@janhq/core": "*" + "@tabler/icons-react": "*" + react: 19.0.0 zustand: 5.0.3 languageName: unknown linkType: soft -"@janhq/core@link:../core::locator=%40janhq%2Fweb-app%40workspace%3Aweb-app": - version: 0.0.0-use.local - resolution: "@janhq/core@link:../core::locator=%40janhq%2Fweb-app%40workspace%3Aweb-app" - languageName: node - linkType: soft - "@janhq/core@workspace:*, @janhq/core@workspace:core": version: 0.0.0-use.local resolution: "@janhq/core@workspace:core" dependencies: "@npmcli/arborist": "npm:^7.1.0" "@types/node": "npm:^22.10.0" + "@types/react": "npm:19.1.2" "@vitest/coverage-v8": "npm:^2.1.8" "@vitest/ui": "npm:^2.1.8" eslint: "npm:8.57.0" happy-dom: "npm:^15.11.6" pacote: "npm:^21.0.0" + react: "npm:19.0.0" request: "npm:^2.88.2" request-progress: "npm:^3.0.0" rimraf: "npm:^6.0.1" @@ -3501,6 +3496,8 @@ __metadata: typescript: "npm:^5.8.3" ulidx: "npm:^2.3.0" vitest: "npm:^2.1.8" + peerDependencies: + react: 19.0.0 languageName: unknown linkType: soft @@ -3512,8 +3509,8 @@ __metadata: "@dnd-kit/modifiers": "npm:9.0.0" "@dnd-kit/sortable": "npm:10.0.0" "@eslint/js": "npm:8.57.0" - "@jan/extensions-web": "link:../extensions-web" - "@janhq/core": "link:../core" + "@jan/extensions-web": "workspace:*" + "@janhq/core": "workspace:*" "@radix-ui/react-accordion": "npm:1.2.11" "@radix-ui/react-avatar": "npm:1.1.10" "@radix-ui/react-dialog": "npm:1.1.15" @@ -7022,6 +7019,17 @@ __metadata: languageName: node linkType: hard +"@tabler/icons-react@npm:^3.34.0": + version: 3.35.0 + resolution: "@tabler/icons-react@npm:3.35.0" + dependencies: + "@tabler/icons": "npm:3.35.0" + peerDependencies: + react: ">= 16" + checksum: 10c0/8d280fcdae00916b001142ba0800ea05d8fa2acdcbd82f88a299b4141fb941237be2e826b86b1af710e038b4f8bb6f76f452c3309c29fd62398b4d5789c2b3e0 + languageName: node + linkType: hard + "@tabler/icons@npm:3.34.0": version: 3.34.0 resolution: "@tabler/icons@npm:3.34.0" @@ -7029,6 +7037,13 @@ __metadata: languageName: node linkType: hard +"@tabler/icons@npm:3.35.0": + version: 3.35.0 + resolution: "@tabler/icons@npm:3.35.0" + checksum: 10c0/93098828128ffed2cf412b39bd78992f93f25b22349a4e04523d2a018b7fe376ddeff105babcc3efedd707aa00b705425c7d9f598d6987552a563c62125795a2 + languageName: node + linkType: hard + "@tailwindcss/node@npm:4.1.4": version: 4.1.4 resolution: "@tailwindcss/node@npm:4.1.4"