Files
cherry-studio/src/renderer/src/pages/home/Inputbar/tools/components/WebSearchQuickPanelManager.tsx
SuYao a6182eaf85 Refactor/inputbar (#10332)
* Refactor inputbar system with configurable scope-based architecture

- **Implement scope-based configuration** for chat, agent sessions, and mini-window with feature toggles
- **Add tool registry system** with dependency injection for modular inputbar tools
- **Create shared state management** via InputbarToolsProvider for consistent state handling
- **Migrate existing tools** to registry-based definitions with proper scope filtering

The changes introduce a flexible inputbar architecture that supports different use cases through scope-based configuration while maintaining feature parity and improving code organization.

* Remove unused import and refactor tool rendering

- Delete obsolete '@renderer/pages/home/Inputbar/tools' import from Inputbar.tsx
- Extract ToolButton component to render tools outside useMemo dependency cycle
- Store tool definitions in config for deferred rendering with current context
- Fix potential stale closure issues in tool rendering by rebuilding context on each render

* Wrap ToolButton in React.memo and optimize quick panel menu updates

- Memoize ToolButton component to prevent unnecessary re-renders when tool key remains unchanged
- Replace direct menu state updates with version-based triggering to batch registry changes
- Add useEffect to consolidate menu updates and reduce redundant flat operations

* chore style

* refactor(InputbarToolsProvider): simplify quick panel menu update logic

* Improve QuickPanel behavior and input handling

- Default select first item when panel symbol changes to enhance user experience
- Add Tab key support for selecting template variables in input field
- Refactor QuickPanel trigger logic with better symbol tracking and boundary checks
- Fix typo in translation key for model selection menu item

* Refactor import statements to use type-only imports

- Convert inline type imports to explicit type imports in Inputbar.tsx and types.ts
- Replace combined type/value imports with separate type imports in InputbarToolsProvider and tools
- Remove unnecessary menu version state and effect in InputbarToolsProvider

* Refactor InputbarTools context to separate state and dispatch concerns

- Split single context into separate state and dispatch contexts to optimize re-renders
- Introduce derived state for `couldMentionNotVisionModel` based on file types
- Encapsulate Quick Panel API in stable object with memoized functions
- Add internal dispatch context for Inputbar-specific state setters

* Refactor Inputbar to use split context hooks and optimize QuickPanel

- Replace monolithic `useInputbarTools` with separate state, dispatch, and internal dispatch hooks
- Move text state from context to local component state in InputbarInner
- Optimize QuickPanel trigger registration to use ref pattern, avoiding frequent re-registrations

* Refactor QuickPanel API to separate concerns between tools and inputbar

- Split QuickPanel API into `toolsRegistry` for tool registration and `triggers` for inputbar triggering
- Remove unused QuickPanel state variables and clean up dependencies
- Update tool context to use new API structure with proper type safety

* Optimize the state management of QuickPanel and Inputbar, add text update functionality, and improve the tool registration logic.

* chore

* Add reusable React hooks and InputbarCore component for chat input

- Create `useInputText`, `useKeyboardHandler`, and `useTextareaResize` hooks for text management, keyboard shortcuts, and auto-resizing
- Implement `InputbarCore` component with modular toolbar sections, drag-drop support, and textarea customization
- Add `useFileDragDrop` and `usePasteHandler` hooks for file uploads and paste handling with type filtering

* Refactor Inputbar to use custom hooks for text and textarea management

- Replace manual text state with useInputText hook for text management and empty state
- Replace textarea resize logic with useTextareaResize hook for automatic height adjustment
- Add comprehensive refactoring documentation with usage examples and guidelines

* Refactor inputbar drag-drop and paste handling into custom hooks

- Extract paste handling logic into usePasteHandler hook
- Extract drag-drop file handling into useFileDragDrop hook
- Remove inline drag-drop state and handlers, use hook interfaces
- Clean up dependencies and callback optimizations

* Refactor Inputbar component to use InputbarCore composition

- Extract complex UI logic into InputbarCore component for better separation of concerns
- Remove intermediate wrapper component and action ref forwarding pattern
- Consolidate focus/blur handlers and simplify component structure

* Refactor Inputbar to expose actions via ref for external control

- Extract action handlers into ProviderActionHandlers interface and expose via ref
- Split component into Inputbar wrapper and InputbarInner implementation
- Update useEffect to sync inner component actions with ref for external access

* feat: inputbar core

* refactor: Update QuickPanel integration across various tools

* refactor: migrate to antd

* chore: format

* fix: clean code

* clean code

* fix i18n

* fix: i18n

* relative path

* model type

* 🤖 Weekly Automated Update: Nov 09, 2025 (#11209)

feat(bot): Weekly automated script run

Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com>
Co-authored-by: SuYao <sy20010504@gmail.com>

* format

* fix

* fix: format

* use ripgrep

* update with input

* add common filters

* fix build issue

* format

* fix error

* smooth change

* adjust

* support listing dir

* keep list files when focus and blur

* support draft save

* Optimize the rendering logic of session messages and input bars, and simplify conditional judgments.

* Upgrade to agentId

* format

* 🐛 fix: force quick triggers for agent sessions

* revert

* fix migrate

* fix: filter

* fix: trigger

* chore packages

* feat: 添加过滤和排序功能,支持自定义函数

* fix cursor bug

* fix format

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: beyondkmp <beyondkmp@gmail.com>
Co-authored-by: kangfenmao <kangfenmao@qq.com>
2025-11-12 20:04:58 +08:00

235 lines
8.0 KiB
TypeScript

import { BaiduOutlined, GoogleOutlined } from '@ant-design/icons'
import { loggerService } from '@logger'
import { BingLogo, BochaLogo, ExaLogo, SearXNGLogo, TavilyLogo, ZhipuLogo } from '@renderer/components/Icons'
import type { QuickPanelListItem } from '@renderer/components/QuickPanel'
import { QuickPanelReservedSymbol } from '@renderer/components/QuickPanel'
import {
isGeminiModel,
isGPT5SeriesReasoningModel,
isOpenAIWebSearchModel,
isWebSearchModel
} from '@renderer/config/models'
import { isGeminiWebSearchProvider } from '@renderer/config/providers'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useTimer } from '@renderer/hooks/useTimer'
import { useWebSearchProviders } from '@renderer/hooks/useWebSearchProviders'
import type { ToolQuickPanelController, ToolRenderContext } from '@renderer/pages/home/Inputbar/types'
import { getProviderByModel } from '@renderer/services/AssistantService'
import WebSearchService from '@renderer/services/WebSearchService'
import type { WebSearchProvider, WebSearchProviderId } from '@renderer/types'
import { hasObjectKey } from '@renderer/utils'
import { isToolUseModeFunction } from '@renderer/utils/assistant'
import { Globe } from 'lucide-react'
import { useCallback, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
const logger = loggerService.withContext('WebSearchQuickPanel')
export const WebSearchProviderIcon = ({
pid,
size = 18,
color
}: {
pid?: WebSearchProviderId
size?: number
color?: string
}) => {
switch (pid) {
case 'bocha':
return <BochaLogo className="icon" width={size} height={size} color={color} />
case 'exa':
return <ExaLogo className="icon" width={size - 2} height={size} color={color} />
case 'tavily':
return <TavilyLogo className="icon" width={size} height={size} color={color} />
case 'zhipu':
return <ZhipuLogo className="icon" width={size} height={size} color={color} />
case 'searxng':
return <SearXNGLogo className="icon" width={size} height={size} color={color} />
case 'local-baidu':
return <BaiduOutlined size={size} style={{ color, fontSize: size }} />
case 'local-bing':
return <BingLogo className="icon" width={size} height={size} color={color} />
case 'local-google':
return <GoogleOutlined size={size} style={{ color, fontSize: size }} />
default:
return <Globe className="icon" size={size} style={{ color, fontSize: size }} />
}
}
export const useWebSearchPanelController = (assistantId: string, quickPanelController: ToolQuickPanelController) => {
const { t } = useTranslation()
const { assistant, updateAssistant } = useAssistant(assistantId)
const { providers } = useWebSearchProviders()
const { setTimeoutTimer } = useTimer()
const enableWebSearch = assistant?.webSearchProviderId || assistant.enableWebSearch
const updateWebSearchProvider = useCallback(
async (providerId?: WebSearchProvider['id']) => {
setTimeoutTimer('updateWebSearchProvider', () => {
updateAssistant({
...assistant,
webSearchProviderId: providerId,
enableWebSearch: false
})
})
},
[assistant, setTimeoutTimer, updateAssistant]
)
const updateQuickPanelItem = useCallback(
async (providerId?: WebSearchProvider['id']) => {
if (providerId === assistant.webSearchProviderId) {
updateWebSearchProvider(undefined)
} else {
updateWebSearchProvider(providerId)
}
},
[assistant.webSearchProviderId, updateWebSearchProvider]
)
const updateToModelBuiltinWebSearch = useCallback(async () => {
const update = {
...assistant,
webSearchProviderId: undefined,
enableWebSearch: !assistant.enableWebSearch
}
const model = assistant.model
const provider = getProviderByModel(model)
if (!model) {
logger.error('Model does not exist.')
window.toast.error(t('error.model.not_exists'))
return
}
if (
isGeminiWebSearchProvider(provider) &&
isGeminiModel(model) &&
isToolUseModeFunction(assistant) &&
update.enableWebSearch &&
assistant.mcpServers &&
assistant.mcpServers.length > 0
) {
update.enableWebSearch = false
window.toast.warning(t('chat.mcp.warning.gemini_web_search'))
}
if (
isOpenAIWebSearchModel(model) &&
isGPT5SeriesReasoningModel(model) &&
update.enableWebSearch &&
assistant.settings?.reasoning_effort === 'minimal'
) {
update.enableWebSearch = false
window.toast.warning(t('chat.web_search.warning.openai'))
}
setTimeoutTimer('updateSelectedWebSearchBuiltin', () => updateAssistant(update), 200)
}, [assistant, setTimeoutTimer, t, updateAssistant])
const providerItems = useMemo<QuickPanelListItem[]>(() => {
const isWebSearchModelEnabled = assistant.model && isWebSearchModel(assistant.model)
const items: QuickPanelListItem[] = providers
.map((p) => ({
label: p.name,
description: WebSearchService.isWebSearchEnabled(p.id)
? hasObjectKey(p, 'apiKey')
? t('settings.tool.websearch.apikey')
: t('settings.tool.websearch.free')
: t('chat.input.web_search.enable_content'),
icon: <WebSearchProviderIcon size={13} pid={p.id} />,
isSelected: p.id === assistant?.webSearchProviderId,
disabled: !WebSearchService.isWebSearchEnabled(p.id),
action: () => updateQuickPanelItem(p.id)
}))
.filter((item) => !item.disabled)
if (isWebSearchModelEnabled) {
items.unshift({
label: t('chat.input.web_search.builtin.label'),
description: isWebSearchModelEnabled
? t('chat.input.web_search.builtin.enabled_content')
: t('chat.input.web_search.builtin.disabled_content'),
icon: <Globe />,
isSelected: assistant.enableWebSearch,
disabled: !isWebSearchModelEnabled,
action: () => updateToModelBuiltinWebSearch()
})
}
return items
}, [
assistant.enableWebSearch,
assistant.model,
assistant?.webSearchProviderId,
providers,
t,
updateQuickPanelItem,
updateToModelBuiltinWebSearch
])
const openQuickPanel = useCallback(() => {
quickPanelController.open({
title: t('chat.input.web_search.label'),
list: providerItems,
symbol: QuickPanelReservedSymbol.WebSearch,
pageSize: 9
})
}, [providerItems, quickPanelController, t])
const toggleQuickPanel = useCallback(() => {
if (quickPanelController.isVisible && quickPanelController.symbol === QuickPanelReservedSymbol.WebSearch) {
quickPanelController.close()
} else {
openQuickPanel()
}
}, [openQuickPanel, quickPanelController])
return {
enableWebSearch,
providerItems,
openQuickPanel,
toggleQuickPanel,
updateWebSearchProvider,
updateToModelBuiltinWebSearch,
selectedProviderId: assistant.webSearchProviderId
}
}
interface ManagerProps {
context: ToolRenderContext<any, any>
}
const WebSearchQuickPanelManager = ({ context }: ManagerProps) => {
const { assistant, quickPanel, quickPanelController, t } = context
const { providerItems, openQuickPanel } = useWebSearchPanelController(assistant.id, quickPanelController)
const { registerRootMenu, registerTrigger } = quickPanel
const { updateList, isVisible, symbol } = quickPanelController
useEffect(() => {
if (isVisible && symbol === QuickPanelReservedSymbol.WebSearch) {
updateList(providerItems)
}
}, [isVisible, providerItems, symbol, updateList])
useEffect(() => {
const disposeMenu = registerRootMenu([
{
label: t('chat.input.web_search.label'),
description: '',
icon: <Globe size={18} />,
isMenu: true,
action: () => openQuickPanel()
}
])
const disposeTrigger = registerTrigger(QuickPanelReservedSymbol.WebSearch, () => openQuickPanel())
return () => {
disposeMenu()
disposeTrigger()
}
}, [openQuickPanel, registerRootMenu, registerTrigger, t])
return null
}
export default WebSearchQuickPanelManager