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>
This commit is contained in:
parent
b3fc64049a
commit
f33c2c205a
@ -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"
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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<string[]>
|
||||
abstract refreshTools(): Promise<void>
|
||||
abstract isHealthy(): Promise<boolean>
|
||||
|
||||
/**
|
||||
* Optional method to provide a custom UI component for tools
|
||||
* @returns A React component or null if no custom component is provided
|
||||
*/
|
||||
getToolComponent?(): ComponentType<MCPToolComponentProps> | null
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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": {
|
||||
|
||||
54
extensions-web/src/mcp-web/components/WebSearchButton.tsx
Normal file
54
extensions-web/src/mcp-web/components/WebSearchButton.tsx
Normal file
@ -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 (
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
className={`h-7 px-2 py-1 flex items-center justify-center rounded-md transition-all duration-200 ease-in-out gap-1 cursor-pointer ml-0.5 border-0 ${
|
||||
isEnabled
|
||||
? 'bg-accent/20 text-accent'
|
||||
: 'bg-transparent text-main-view-fg/70 hover:bg-main-view-fg/5'
|
||||
}`}
|
||||
title={isEnabled ? 'Disable Web Search' : 'Enable Web Search'}
|
||||
>
|
||||
<IconWorld
|
||||
size={16}
|
||||
className={isEnabled ? 'text-accent' : 'text-main-view-fg/70'}
|
||||
/>
|
||||
<span className={`text-sm font-medium ${isEnabled ? 'text-accent' : ''}`}>Search</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
1
extensions-web/src/mcp-web/components/index.ts
Normal file
1
extensions-web/src/mcp-web/components/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { WebSearchButton } from './WebSearchButton'
|
||||
@ -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<MCPToolComponentProps> | null {
|
||||
return WebSearchButton
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
|
||||
@ -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
|
||||
},
|
||||
|
||||
@ -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<MCPExtension>(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 && (
|
||||
<TooltipProvider>
|
||||
<Tooltip
|
||||
open={tooltipToolsAvailable}
|
||||
onOpenChange={setTooltipToolsAvailable}
|
||||
>
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
disabled={dropdownToolsAvailable}
|
||||
MCPToolComponent ? (
|
||||
// Use custom MCP component
|
||||
<McpExtensionToolLoader
|
||||
tools={tools}
|
||||
hasActiveMCPServers={hasActiveMCPServers}
|
||||
selectedModelHasTools={selectedModel?.capabilities?.includes('tools') ?? false}
|
||||
initialMessage={initialMessage}
|
||||
MCPToolComponent={MCPToolComponent}
|
||||
/>
|
||||
) : (
|
||||
// Use default tools dropdown
|
||||
<TooltipProvider>
|
||||
<Tooltip
|
||||
open={tooltipToolsAvailable}
|
||||
onOpenChange={setTooltipToolsAvailable}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
setDropdownToolsAvailable(false)
|
||||
e.stopPropagation()
|
||||
}}
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
disabled={dropdownToolsAvailable}
|
||||
>
|
||||
<DropdownToolsAvailable
|
||||
initialMessage={initialMessage}
|
||||
onOpenChange={(isOpen) => {
|
||||
setDropdownToolsAvailable(isOpen)
|
||||
if (isOpen) {
|
||||
setTooltipToolsAvailable(false)
|
||||
}
|
||||
<div
|
||||
onClick={(e) => {
|
||||
setDropdownToolsAvailable(false)
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
{(isOpen, toolsCount) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'h-7 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1 cursor-pointer relative',
|
||||
isOpen && 'bg-main-view-fg/10'
|
||||
)}
|
||||
>
|
||||
<IconTool
|
||||
size={18}
|
||||
className="text-main-view-fg/50"
|
||||
/>
|
||||
{toolsCount > 0 && (
|
||||
<div className="absolute -top-2 -right-2 bg-accent text-accent-fg text-xs rounded-full size-5 flex items-center justify-center font-medium">
|
||||
<span className="leading-0 text-xs">
|
||||
{toolsCount > 99 ? '99+' : toolsCount}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</DropdownToolsAvailable>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('tools')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<DropdownToolsAvailable
|
||||
initialMessage={initialMessage}
|
||||
onOpenChange={(isOpen) => {
|
||||
setDropdownToolsAvailable(isOpen)
|
||||
if (isOpen) {
|
||||
setTooltipToolsAvailable(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(isOpen, toolsCount) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'h-7 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1 cursor-pointer relative',
|
||||
isOpen && 'bg-main-view-fg/10'
|
||||
)}
|
||||
>
|
||||
<IconTool
|
||||
size={18}
|
||||
className="text-main-view-fg/50"
|
||||
/>
|
||||
{toolsCount > 0 && (
|
||||
<div className="absolute -top-2 -right-2 bg-accent text-accent-fg text-xs rounded-full size-5 flex items-center justify-center font-medium">
|
||||
<span className="leading-0 text-xs">
|
||||
{toolsCount > 99 ? '99+' : toolsCount}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</DropdownToolsAvailable>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('tools')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
)}
|
||||
{selectedModel?.capabilities?.includes('web_search') && (
|
||||
<TooltipProvider>
|
||||
|
||||
61
web-app/src/containers/McpExtensionToolLoader.tsx
Normal file
61
web-app/src/containers/McpExtensionToolLoader.tsx
Normal file
@ -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<MCPToolComponentProps> | 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 (
|
||||
<MCPToolComponent
|
||||
tools={tools}
|
||||
isToolEnabled={isToolEnabled}
|
||||
onToolToggle={handleToolToggle}
|
||||
/>
|
||||
)
|
||||
}
|
||||
45
yarn.lock
45
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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user