Files
cherry-studio/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx
T
Phantom bef0180e4c feat: web search icons (#9147)
* feat(类型): 添加WebSearchProviderIds常量并更新WebSearchProvider类型

* refactor(web-search): 重构网络搜索提供商配置和logo获取逻辑

将webSearchProviders.ts中的提供商logo获取函数移动到使用组件中
并优化提供商配置的类型定义

* feat(WebSearchButton): 添加不同搜索引擎的图标支持

为WebSearchButton组件添加多个搜索引擎的图标支持,包括Baidu、Google、Bing等

* feat(types): 添加预处理和网页搜索提供者的类型校验函数

添加 PreprocessProviderId 和 WebSearchProviderId 的类型校验函数 isPreprocessProviderId 和 isWebSearchProviderId,用于验证字符串是否为有效的提供者 ID

* refactor(types): 重命名ApiProviderUnion并添加更新函数类型

添加用于更新不同类型API提供者的函数类型,提高类型安全性

* refactor(websearch): 将搜索提供商配置提取到单独文件

将websearch store中的搜索提供商配置提取到单独的配置文件,提高代码可维护性

* refactor(PreprocessSettings): 移除未使用的 system 选项禁用逻辑

由于 system 字段实际未使用,移除相关代码以简化逻辑

* refactor(api-key-popup): 移除providerKind参数,改用providerId判断类型

* refactor(preprocessProviders): 使用类型定义优化预处理提供者配置

将 providerId 参数类型从 string 改为 PreprocessProviderId
为 PREPROCESS_PROVIDER_CONFIG 添加类型定义

* refactor(hooks): 使用PreprocessProviderId类型替换字符串类型参数

* refactor(hooks): 使用 WebSearchProviderId 类型替换字符串类型参数

将 useWebSearchProvider 钩子的 id 参数类型从 string 改为 WebSearchProviderId,提高类型安全性

* refactor(knowledge): 将providerId类型改为PreprocessProviderId

* refactor(PreprocessSettings): 移除未使用的options相关代码

清理PreprocessSettings组件中已被注释掉的options状态和相关逻辑,简化代码结构

* refactor(WebSearchProviderSetting): 将providerId类型从string改为WebSearchProviderId

* refactor(websearch): 移除WebSearchProvider类型中不必要的id字段约束

* style(WebSearchButton): 调整图标大小和样式以保持视觉一致性

* fix(ApiKeyListPopup): 修正LLM提供者判断逻辑

使用'models'属性检查替代原有逻辑,更准确地判断是否为LLM provider

* fix(ApiKeyListPopup): 修复预处理provider判断逻辑

处理mistral同时提供预处理和llm服务的情况,避免误判
2025-08-14 23:19:17 +08:00

175 lines
6.0 KiB
TypeScript

import { BaiduOutlined, GoogleOutlined } from '@ant-design/icons'
import { BingLogo, BochaLogo, ExaLogo, SearXNGLogo, TavilyLogo } from '@renderer/components/Icons'
import { QuickPanelListItem, useQuickPanel } from '@renderer/components/QuickPanel'
import { isWebSearchModel } from '@renderer/config/models'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useWebSearchProviders } from '@renderer/hooks/useWebSearchProviders'
import WebSearchService from '@renderer/services/WebSearchService'
import { Assistant, WebSearchProvider, WebSearchProviderId } from '@renderer/types'
import { hasObjectKey } from '@renderer/utils'
import { Tooltip } from 'antd'
import { Globe } from 'lucide-react'
import { FC, memo, startTransition, useCallback, useImperativeHandle, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
export interface WebSearchButtonRef {
openQuickPanel: () => void
}
interface Props {
ref?: React.RefObject<WebSearchButtonRef | null>
assistant: Assistant
ToolbarButton: any
}
const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
const { t } = useTranslation()
const quickPanel = useQuickPanel()
const { providers } = useWebSearchProviders()
const { updateAssistant } = useAssistant(assistant.id)
const enableWebSearch = assistant?.webSearchProviderId || assistant.enableWebSearch
const WebSearchIcon = useCallback(
({ pid, size = 18 }: { pid?: WebSearchProviderId; size?: number }) => {
const iconColor = enableWebSearch ? 'var(--color-primary)' : 'var(--color-icon)'
switch (pid) {
case 'bocha':
return <BochaLogo width={size} height={size} color={iconColor} />
case 'exa':
// size微调,视觉上和其他图标平衡一些
return <ExaLogo width={size - 2} height={size} color={iconColor} />
case 'tavily':
return <TavilyLogo width={size} height={size} color={iconColor} />
case 'searxng':
return <SearXNGLogo width={size} height={size} color={iconColor} />
case 'local-baidu':
return <BaiduOutlined size={size} style={{ color: iconColor, fontSize: size }} />
case 'local-bing':
return <BingLogo width={size} height={size} color={iconColor} />
case 'local-google':
return <GoogleOutlined size={size} style={{ color: iconColor, fontSize: size }} />
default:
return <Globe size={size} style={{ color: iconColor, fontSize: size }} />
}
},
[enableWebSearch]
)
const updateSelectedWebSearchProvider = useCallback(
async (providerId?: WebSearchProvider['id']) => {
// TODO: updateAssistant有性能问题,会导致关闭快捷面板卡顿
const currentWebSearchProviderId = assistant.webSearchProviderId
const newWebSearchProviderId = currentWebSearchProviderId === providerId ? undefined : providerId
startTransition(() => {
updateAssistant({ ...assistant, webSearchProviderId: newWebSearchProviderId, enableWebSearch: false })
})
},
[assistant, updateAssistant]
)
const updateSelectedWebSearchBuiltin = useCallback(async () => {
// TODO: updateAssistant有性能问题,会导致关闭快捷面板卡顿
startTransition(() => {
updateAssistant({ ...assistant, webSearchProviderId: undefined, enableWebSearch: !assistant.enableWebSearch })
})
}, [assistant, 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: <WebSearchIcon size={13} pid={p.id} />,
isSelected: p.id === assistant?.webSearchProviderId,
disabled: !WebSearchService.isWebSearchEnabled(p.id),
action: () => updateSelectedWebSearchProvider(p.id)
}))
.filter((o) => !o.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: () => updateSelectedWebSearchBuiltin()
})
}
return items
}, [
WebSearchIcon,
assistant.enableWebSearch,
assistant.model,
assistant?.webSearchProviderId,
providers,
t,
updateSelectedWebSearchBuiltin,
updateSelectedWebSearchProvider
])
const openQuickPanel = useCallback(() => {
if (assistant.webSearchProviderId) {
updateSelectedWebSearchProvider(undefined)
return
}
if (assistant.enableWebSearch) {
updateSelectedWebSearchBuiltin()
return
}
quickPanel.open({
title: t('chat.input.web_search.label'),
list: providerItems,
symbol: '?',
pageSize: 9
})
}, [
assistant.webSearchProviderId,
assistant.enableWebSearch,
quickPanel,
t,
providerItems,
updateSelectedWebSearchProvider,
updateSelectedWebSearchBuiltin
])
const handleOpenQuickPanel = useCallback(() => {
if (quickPanel.isVisible && quickPanel.symbol === '?') {
quickPanel.close()
} else {
openQuickPanel()
}
}, [openQuickPanel, quickPanel])
useImperativeHandle(ref, () => ({
openQuickPanel
}))
return (
<Tooltip
placement="top"
title={enableWebSearch ? t('common.close') : t('chat.input.web_search.label')}
mouseLeaveDelay={0}
arrow>
<ToolbarButton type="text" onClick={handleOpenQuickPanel}>
<WebSearchIcon pid={assistant.webSearchProviderId} />
</ToolbarButton>
</Tooltip>
)
}
export default memo(WebSearchButton)