mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +08:00
feat(provider): add agent support filter for provider list
- Add filter dropdown in provider search to filter by agent support - Create AnthropicProviderListPopover component for showing supported providers - Add filter=agent URL parameter support in ProviderList - Update AgentModal and CodeToolsPage to use the new popover component - Add getAnthropicSupportedProviders utility function 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d41229c69b
commit
ddfbce071b
148
src/renderer/src/components/AnthropicProviderListPopover.tsx
Normal file
148
src/renderer/src/components/AnthropicProviderListPopover.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import { ProviderAvatar } from '@renderer/components/ProviderAvatar'
|
||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||
import ImageStorage from '@renderer/services/ImageStorage'
|
||||
import type { Provider } from '@renderer/types'
|
||||
import { getFancyProviderName } from '@renderer/utils'
|
||||
import { getClaudeSupportedProviders } from '@renderer/utils/provider'
|
||||
import type { PopoverProps } from 'antd'
|
||||
import { Popover } from 'antd'
|
||||
import { ArrowUpRight, HelpCircle } from 'lucide-react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface AnthropicProviderListPopoverProps {
|
||||
/** Callback when provider is clicked */
|
||||
onProviderClick?: () => void
|
||||
/** Use window.navigate instead of Link (for non-router context like TopView) */
|
||||
useWindowNavigate?: boolean
|
||||
/** Custom trigger element, defaults to HelpCircle icon */
|
||||
children?: ReactNode
|
||||
/** Popover placement */
|
||||
placement?: PopoverProps['placement']
|
||||
/** Custom filter function for providers, defaults to getClaudeSupportedProviders */
|
||||
filterProviders?: (providers: Provider[]) => Provider[]
|
||||
}
|
||||
|
||||
const AnthropicProviderListPopover: FC<AnthropicProviderListPopoverProps> = ({
|
||||
onProviderClick,
|
||||
useWindowNavigate = false,
|
||||
children,
|
||||
placement = 'right',
|
||||
filterProviders = getClaudeSupportedProviders
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const allProviders = useAllProviders()
|
||||
const providers = filterProviders(allProviders)
|
||||
const [providerLogos, setProviderLogos] = useState<Record<string, string>>({})
|
||||
|
||||
useEffect(() => {
|
||||
const loadAllLogos = async () => {
|
||||
const logos: Record<string, string> = {}
|
||||
for (const provider of providers) {
|
||||
if (provider.id) {
|
||||
try {
|
||||
const logoData = await ImageStorage.get(`provider-${provider.id}`)
|
||||
if (logoData) {
|
||||
logos[provider.id] = logoData
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors loading logos
|
||||
}
|
||||
}
|
||||
}
|
||||
setProviderLogos(logos)
|
||||
}
|
||||
|
||||
loadAllLogos()
|
||||
}, [providers])
|
||||
|
||||
const handleClick = (providerId: string) => {
|
||||
onProviderClick?.()
|
||||
if (useWindowNavigate) {
|
||||
window.navigate(`/settings/provider?id=${providerId}`)
|
||||
}
|
||||
}
|
||||
|
||||
const content = (
|
||||
<PopoverContent>
|
||||
<PopoverTitle>{t('code.supported_providers')}</PopoverTitle>
|
||||
<ProviderListContainer>
|
||||
{providers.map((provider) =>
|
||||
useWindowNavigate ? (
|
||||
<ProviderItem key={provider.id} onClick={() => handleClick(provider.id)}>
|
||||
<ProviderAvatar
|
||||
provider={provider}
|
||||
customLogos={providerLogos}
|
||||
size={20}
|
||||
style={{ width: 20, height: 20 }}
|
||||
/>
|
||||
{getFancyProviderName(provider)}
|
||||
<ArrowUpRight size={14} />
|
||||
</ProviderItem>
|
||||
) : (
|
||||
<ProviderLink
|
||||
key={provider.id}
|
||||
href={`/settings/provider?id=${provider.id}`}
|
||||
onClick={() => handleClick(provider.id)}>
|
||||
<ProviderAvatar
|
||||
provider={provider}
|
||||
customLogos={providerLogos}
|
||||
size={20}
|
||||
style={{ width: 20, height: 20 }}
|
||||
/>
|
||||
{getFancyProviderName(provider)}
|
||||
<ArrowUpRight size={14} />
|
||||
</ProviderLink>
|
||||
)
|
||||
)}
|
||||
</ProviderListContainer>
|
||||
</PopoverContent>
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover content={content} trigger="hover" placement={placement}>
|
||||
{children || <HelpCircle size={14} style={{ color: 'var(--color-text-3)', cursor: 'pointer' }} />}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const PopoverContent = styled.div`
|
||||
width: 200px;
|
||||
`
|
||||
|
||||
const PopoverTitle = styled.div`
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const ProviderListContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const ProviderItem = styled.div`
|
||||
color: var(--color-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: var(--color-link);
|
||||
}
|
||||
`
|
||||
|
||||
const ProviderLink = styled.a`
|
||||
color: var(--color-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
color: var(--color-link);
|
||||
}
|
||||
`
|
||||
|
||||
export default AnthropicProviderListPopover
|
||||
@ -1,6 +1,6 @@
|
||||
import { loggerService } from '@logger'
|
||||
import AnthropicProviderListPopover from '@renderer/components/AnthropicProviderListPopover'
|
||||
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
|
||||
import { HelpTooltip } from '@renderer/components/TooltipIcons'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { permissionModeCards } from '@renderer/config/agent'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
@ -16,6 +16,7 @@ import type {
|
||||
UpdateAgentForm
|
||||
} from '@renderer/types'
|
||||
import { AgentConfigurationSchema, isAgentType } from '@renderer/types'
|
||||
import { getAnthropicSupportedProviders } from '@renderer/utils/provider'
|
||||
import { Alert, Button, Input, Modal, Select } from 'antd'
|
||||
import { AlertTriangleIcon } from 'lucide-react'
|
||||
import type { ChangeEvent, FormEvent } from 'react'
|
||||
@ -420,7 +421,14 @@ const PopupContainer: React.FC<Props> = ({ agent, afterSubmit, resolve }) => {
|
||||
<Label>
|
||||
{t('common.model')} <RequiredMark>*</RequiredMark>
|
||||
</Label>
|
||||
<HelpTooltip title={t('agent.add.model.tooltip')} />
|
||||
<AnthropicProviderListPopover
|
||||
useWindowNavigate
|
||||
filterProviders={getAnthropicSupportedProviders}
|
||||
onProviderClick={() => {
|
||||
setOpen(false)
|
||||
resolve(undefined)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<SelectAgentBaseModelButton
|
||||
agentBase={tempAgentBase}
|
||||
|
||||
@ -7,7 +7,9 @@
|
||||
"invalid_agent": "Invalid Agent"
|
||||
},
|
||||
"model": {
|
||||
"tooltip": "Currently, only models that support Anthropic endpoints are available for the Agent feature."
|
||||
"supported_providers": "Supported Providers",
|
||||
"tooltip": "Currently, only models that support Anthropic endpoints are available for the Agent feature.",
|
||||
"view_providers": "View supported providers"
|
||||
},
|
||||
"title": "Add Agent",
|
||||
"type": {
|
||||
@ -4508,6 +4510,11 @@
|
||||
},
|
||||
"docs_check": "Check",
|
||||
"docs_more_details": "for more details",
|
||||
"filter": {
|
||||
"agent": "Agent Supported",
|
||||
"all": "All Providers"
|
||||
},
|
||||
"filter_agent": "Filter Agent Supported Providers",
|
||||
"get_api_key": "Get API Key",
|
||||
"misc": "Other",
|
||||
"no_models_for_check": "No models available for checking (e.g. chat models)",
|
||||
|
||||
@ -7,7 +7,9 @@
|
||||
"invalid_agent": "无效的 Agent"
|
||||
},
|
||||
"model": {
|
||||
"tooltip": "目前,只有支持 Anthropic 端点的模型可用于 Agent 功能。"
|
||||
"supported_providers": "支持的服务商",
|
||||
"tooltip": "目前,只有支持 Anthropic 端点的模型可用于 Agent 功能。",
|
||||
"view_providers": "查看支持的服务商"
|
||||
},
|
||||
"title": "添加 Agent",
|
||||
"type": {
|
||||
@ -4508,6 +4510,11 @@
|
||||
},
|
||||
"docs_check": "查看",
|
||||
"docs_more_details": "获取更多详情",
|
||||
"filter": {
|
||||
"agent": "支持 Agent",
|
||||
"all": "全部服务商"
|
||||
},
|
||||
"filter_agent": "筛选支持 Agent 的服务商",
|
||||
"get_api_key": "点击这里获取密钥",
|
||||
"misc": "其他",
|
||||
"no_models_for_check": "没有可以被检测的模型(例如对话模型)",
|
||||
|
||||
@ -7,7 +7,9 @@
|
||||
"invalid_agent": "無效的 Agent"
|
||||
},
|
||||
"model": {
|
||||
"tooltip": "目前,僅支援 Anthropic 端點的模型可供代理功能使用。"
|
||||
"supported_providers": "支援的服務商",
|
||||
"tooltip": "目前,僅支援 Anthropic 端點的模型可供代理功能使用。",
|
||||
"view_providers": "檢視支援的服務商"
|
||||
},
|
||||
"title": "新增 Agent",
|
||||
"type": {
|
||||
@ -4508,6 +4510,11 @@
|
||||
},
|
||||
"docs_check": "檢查",
|
||||
"docs_more_details": "檢視更多細節",
|
||||
"filter": {
|
||||
"agent": "支援 Agent",
|
||||
"all": "全部服務商"
|
||||
},
|
||||
"filter_agent": "篩選支援 Agent 的服務商",
|
||||
"get_api_key": "點選這裡取得金鑰",
|
||||
"misc": "其他",
|
||||
"no_models_for_check": "沒有可以被檢查的模型(例如對話模型)",
|
||||
|
||||
@ -7,7 +7,9 @@
|
||||
"invalid_agent": "Ungültiger Agent"
|
||||
},
|
||||
"model": {
|
||||
"tooltip": "Derzeit sind für die Agent-Funktion nur Modelle verfügbar, die Anthropic-Endpunkte unterstützen."
|
||||
"supported_providers": "Unterstützte Anbieter",
|
||||
"tooltip": "Derzeit sind für die Agent-Funktion nur Modelle verfügbar, die Anthropic-Endpunkte unterstützen.",
|
||||
"view_providers": "Unterstützte Anbieter anzeigen"
|
||||
},
|
||||
"title": "Agent hinzufügen",
|
||||
"type": {
|
||||
@ -4508,6 +4510,11 @@
|
||||
},
|
||||
"docs_check": "Anzeigen",
|
||||
"docs_more_details": "Weitere Details anzeigen",
|
||||
"filter": {
|
||||
"agent": "Vom Agenten unterstützt",
|
||||
"all": "Alle Anbieter"
|
||||
},
|
||||
"filter_agent": "Von Filter-Agent unterstützte Anbieter",
|
||||
"get_api_key": "Hier klicken um Schlüssel zu erhalten",
|
||||
"misc": "Sonstige",
|
||||
"no_models_for_check": "Keine testbaren Modelle (z.B. Chat-Modelle)",
|
||||
|
||||
@ -7,7 +7,9 @@
|
||||
"invalid_agent": "Μη έγκυρος Agent"
|
||||
},
|
||||
"model": {
|
||||
"tooltip": "Προς το παρόν, μόνο μοντέλα που υποστηρίζουν τελικά σημεία Anthropic είναι διαθέσιμα για τη λειτουργία Agent."
|
||||
"supported_providers": "Υποστηριζόμενοι Πάροχοι",
|
||||
"tooltip": "Προς το παρόν, μόνο μοντέλα που υποστηρίζουν τελικά σημεία Anthropic είναι διαθέσιμα για τη λειτουργία Agent.",
|
||||
"view_providers": "Δείτε τους υποστηριζόμενους παρόχους"
|
||||
},
|
||||
"title": "Προσθήκη Agent",
|
||||
"type": {
|
||||
@ -4508,6 +4510,11 @@
|
||||
},
|
||||
"docs_check": "Άνοιγμα",
|
||||
"docs_more_details": "Λάβετε περισσότερες λεπτομέρειες",
|
||||
"filter": {
|
||||
"agent": "Υποστηριζόμενος από πράκτορα",
|
||||
"all": "Όλοι οι Πάροχοι"
|
||||
},
|
||||
"filter_agent": "Υποστηριζόμενοι Πάροχοι του Φίλτρου Πράκτορα",
|
||||
"get_api_key": "Κάντε κλικ εδώ για να πάρετε κλειδί",
|
||||
"misc": "Άλλο",
|
||||
"no_models_for_check": "Δεν υπάρχουν μοντέλα για έλεγχο (π.χ. μοντέλα συνομιλίας)",
|
||||
|
||||
@ -7,7 +7,9 @@
|
||||
"invalid_agent": "Agent inválido"
|
||||
},
|
||||
"model": {
|
||||
"tooltip": "Actualmente, solo los modelos que admiten puntos finales de Anthropic están disponibles para la función Agente."
|
||||
"supported_providers": "Proveedores Admitidos",
|
||||
"tooltip": "Actualmente, solo los modelos que admiten puntos finales de Anthropic están disponibles para la función Agente.",
|
||||
"view_providers": "Ver proveedores compatibles"
|
||||
},
|
||||
"title": "Agregar Agente",
|
||||
"type": {
|
||||
@ -4508,6 +4510,11 @@
|
||||
},
|
||||
"docs_check": "Ver",
|
||||
"docs_more_details": "Obtener más detalles",
|
||||
"filter": {
|
||||
"agent": "Soporte de Agente",
|
||||
"all": "Todos los Proveedores"
|
||||
},
|
||||
"filter_agent": "Proveedores compatibles con el agente de filtrado",
|
||||
"get_api_key": "Haga clic aquí para obtener la clave",
|
||||
"misc": "otro",
|
||||
"no_models_for_check": "No hay modelos disponibles para revisar (por ejemplo, modelos de conversación)",
|
||||
|
||||
@ -7,7 +7,9 @@
|
||||
"invalid_agent": "Agent invalide"
|
||||
},
|
||||
"model": {
|
||||
"tooltip": "Actuellement, seuls les modèles qui prennent en charge les points de terminaison Anthropic sont disponibles pour la fonctionnalité Agent."
|
||||
"supported_providers": "Fournisseurs pris en charge",
|
||||
"tooltip": "Actuellement, seuls les modèles qui prennent en charge les points de terminaison Anthropic sont disponibles pour la fonctionnalité Agent.",
|
||||
"view_providers": "Afficher les fournisseurs pris en charge"
|
||||
},
|
||||
"title": "Ajouter un agent",
|
||||
"type": {
|
||||
@ -4508,6 +4510,11 @@
|
||||
},
|
||||
"docs_check": "Voir",
|
||||
"docs_more_details": "Obtenir plus de détails",
|
||||
"filter": {
|
||||
"agent": "Prise en charge par un agent",
|
||||
"all": "Tous les fournisseurs"
|
||||
},
|
||||
"filter_agent": "Fournisseurs pris en charge par l'agent de filtrage",
|
||||
"get_api_key": "Cliquez ici pour obtenir une clé",
|
||||
"misc": "autre",
|
||||
"no_models_for_check": "Aucun modèle détectable (par exemple, modèle de chat)",
|
||||
|
||||
@ -7,7 +7,9 @@
|
||||
"invalid_agent": "無効なエージェント"
|
||||
},
|
||||
"model": {
|
||||
"tooltip": "現在、エージェント機能では、Anthropicエンドポイントをサポートするモデルのみが利用可能です。"
|
||||
"supported_providers": "サポートされているプロバイダー",
|
||||
"tooltip": "現在、エージェント機能では、Anthropicエンドポイントをサポートするモデルのみが利用可能です。",
|
||||
"view_providers": "サポートされているプロバイダーを表示"
|
||||
},
|
||||
"title": "エージェントを追加",
|
||||
"type": {
|
||||
@ -4508,6 +4510,11 @@
|
||||
},
|
||||
"docs_check": "チェック",
|
||||
"docs_more_details": "詳細を確認",
|
||||
"filter": {
|
||||
"agent": "エージェントサポート",
|
||||
"all": "すべてのプロバイダー"
|
||||
},
|
||||
"filter_agent": "フィルターエージェント対応プロバイダー",
|
||||
"get_api_key": "APIキーを取得",
|
||||
"misc": "その他",
|
||||
"no_models_for_check": "チェックするモデルがありません(例:会話モデル)",
|
||||
|
||||
@ -7,7 +7,9 @@
|
||||
"invalid_agent": "Agent inválido"
|
||||
},
|
||||
"model": {
|
||||
"tooltip": "Atualmente, apenas modelos que suportam endpoints da Anthropic estão disponíveis para o recurso Agente."
|
||||
"supported_providers": "Provedores Suportados",
|
||||
"tooltip": "Atualmente, apenas modelos que suportam endpoints da Anthropic estão disponíveis para o recurso Agente.",
|
||||
"view_providers": "Ver provedores suportados"
|
||||
},
|
||||
"title": "Adicionar Agente",
|
||||
"type": {
|
||||
@ -4508,6 +4510,11 @@
|
||||
},
|
||||
"docs_check": "Verificar",
|
||||
"docs_more_details": "Obter mais detalhes",
|
||||
"filter": {
|
||||
"agent": "Suportado por Agente",
|
||||
"all": "Todos os Provedores"
|
||||
},
|
||||
"filter_agent": "Agente de Filtro - Provedores Suportados",
|
||||
"get_api_key": "Clique aqui para obter a chave",
|
||||
"misc": "outro",
|
||||
"no_models_for_check": "Não há modelos disponíveis para verificação (por exemplo, modelos de conversa)",
|
||||
|
||||
@ -7,7 +7,9 @@
|
||||
"invalid_agent": "Недействительный агент"
|
||||
},
|
||||
"model": {
|
||||
"tooltip": "В настоящее время для функции агента доступны только модели, поддерживающие конечные точки Anthropic."
|
||||
"supported_providers": "Поддерживаемые поставщики",
|
||||
"tooltip": "В настоящее время для функции агента доступны только модели, поддерживающие конечные точки Anthropic.",
|
||||
"view_providers": "Просмотреть поддерживаемых поставщиков"
|
||||
},
|
||||
"title": "Добавить агента",
|
||||
"type": {
|
||||
@ -4508,6 +4510,11 @@
|
||||
},
|
||||
"docs_check": "Проверить",
|
||||
"docs_more_details": "для получения дополнительной информации",
|
||||
"filter": {
|
||||
"agent": "Поддержка агента",
|
||||
"all": "Все поставщики"
|
||||
},
|
||||
"filter_agent": "Поддерживаемые поставщики агента фильтрации",
|
||||
"get_api_key": "Получить ключ API",
|
||||
"misc": "другие",
|
||||
"no_models_for_check": "Нет моделей для проверки (например, диалоговые модели)",
|
||||
|
||||
@ -1,29 +1,26 @@
|
||||
import AiProvider from '@renderer/aiCore'
|
||||
import AnthropicProviderListPopover from '@renderer/components/AnthropicProviderListPopover'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import ModelSelector from '@renderer/components/ModelSelector'
|
||||
import { isMac, isWin } from '@renderer/config/constant'
|
||||
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
|
||||
import { getProviderLogo } from '@renderer/config/providers'
|
||||
import { useCodeTools } from '@renderer/hooks/useCodeTools'
|
||||
import { useAllProviders, useProviders } from '@renderer/hooks/useProvider'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setIsBunInstalled } from '@renderer/store/mcp'
|
||||
import type { EndpointType, Model } from '@renderer/types'
|
||||
import { getClaudeSupportedProviders } from '@renderer/utils/provider'
|
||||
import type { TerminalConfig } from '@shared/config/constant'
|
||||
import { codeTools, terminalApps } from '@shared/config/constant'
|
||||
import { isSiliconAnthropicCompatibleModel } from '@shared/config/providers'
|
||||
import { Alert, Avatar, Button, Checkbox, Input, Popover, Select, Space, Tooltip } from 'antd'
|
||||
import { ArrowUpRight, Download, FolderOpen, HelpCircle, Terminal, X } from 'lucide-react'
|
||||
import { Alert, Button, Checkbox, Input, Select, Space, Tooltip } from 'antd'
|
||||
import { Download, FolderOpen, Terminal, X } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import {
|
||||
@ -40,7 +37,6 @@ const logger = loggerService.withContext('CodeToolsPage')
|
||||
const CodeToolsPage: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { providers } = useProviders()
|
||||
const allProviders = useAllProviders()
|
||||
const dispatch = useAppDispatch()
|
||||
const isBunInstalled = useAppSelector((state) => state.mcp.isBunInstalled)
|
||||
const {
|
||||
@ -372,48 +368,7 @@ const CodeToolsPage: FC = () => {
|
||||
<SettingsItem>
|
||||
<div className="settings-label">
|
||||
{t('code.model')}
|
||||
{selectedCliTool === 'claude-code' && (
|
||||
<Popover
|
||||
content={
|
||||
<div style={{ width: 200 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>{t('code.supported_providers')}</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8
|
||||
}}>
|
||||
{getClaudeSupportedProviders(allProviders).map((provider) => {
|
||||
return (
|
||||
<Link
|
||||
key={provider.id}
|
||||
style={{
|
||||
color: 'var(--color-text)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4
|
||||
}}
|
||||
to={`/settings/provider?id=${provider.id}`}>
|
||||
<ProviderLogo shape="square" src={getProviderLogo(provider.id)} size={20} />
|
||||
{getProviderLabel(provider.id)}
|
||||
<ArrowUpRight size={14} />
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
trigger="hover"
|
||||
placement="right">
|
||||
<HelpCircle
|
||||
size={14}
|
||||
style={{
|
||||
color: 'var(--color-text-3)',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
)}
|
||||
{selectedCliTool === 'claude-code' && <AnthropicProviderListPopover />}
|
||||
</div>
|
||||
<ModelSelector
|
||||
providers={availableProviders}
|
||||
@ -621,8 +576,4 @@ const BunInstallAlert = styled.div`
|
||||
margin-bottom: 24px;
|
||||
`
|
||||
|
||||
const ProviderLogo = styled(Avatar)`
|
||||
border-radius: 4px;
|
||||
`
|
||||
|
||||
export default CodeToolsPage
|
||||
|
||||
@ -15,7 +15,7 @@ import { isSystemProvider } from '@renderer/types'
|
||||
import { getFancyProviderName, matchKeywordsInModel, matchKeywordsInProvider, uuid } from '@renderer/utils'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Button, Dropdown, Input, Tag } from 'antd'
|
||||
import { GripVertical, PlusIcon, Search, UserPen } from 'lucide-react'
|
||||
import { Check, Filter, GripVertical, PlusIcon, Search, UserPen } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { startTransition, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -42,6 +42,7 @@ const ProviderList: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState<string>('')
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const [agentFilterEnabled, setAgentFilterEnabled] = useState(false)
|
||||
const [providerLogos, setProviderLogos] = useState<Record<string, string>>({})
|
||||
const listRef = useRef<DraggableVirtualListRef>(null)
|
||||
|
||||
@ -71,7 +72,16 @@ const ProviderList: FC = () => {
|
||||
}, [providers])
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get('id')) {
|
||||
let shouldUpdate = false
|
||||
const hasFilterParam = searchParams.get('filter') === 'agent'
|
||||
|
||||
// Handle filter param first - when filter is enabled, ignore id param
|
||||
if (hasFilterParam) {
|
||||
setAgentFilterEnabled(true)
|
||||
searchParams.delete('filter')
|
||||
searchParams.delete('id') // Clear id param when filter is enabled
|
||||
shouldUpdate = true
|
||||
} else if (searchParams.get('id')) {
|
||||
const providerId = searchParams.get('id')
|
||||
const provider = providers.find((p) => p.id === providerId)
|
||||
if (provider) {
|
||||
@ -89,6 +99,10 @@ const ProviderList: FC = () => {
|
||||
setSelectedProvider(providers[0])
|
||||
}
|
||||
searchParams.delete('id')
|
||||
shouldUpdate = true
|
||||
}
|
||||
|
||||
if (shouldUpdate) {
|
||||
setSearchParams(searchParams)
|
||||
}
|
||||
}, [providers, searchParams, setSearchParams, setSelectedProvider, setTimeoutTimer])
|
||||
@ -282,6 +296,11 @@ const ProviderList: FC = () => {
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter by agent support
|
||||
if (agentFilterEnabled && !provider.anthropicApiHost) {
|
||||
return false
|
||||
}
|
||||
|
||||
const keywords = searchText.toLowerCase().split(/\s+/).filter(Boolean)
|
||||
const isProviderMatch = matchKeywordsInProvider(keywords, provider)
|
||||
const isModelMatch = provider.models.some((model) => matchKeywordsInModel(keywords, model))
|
||||
@ -316,7 +335,34 @@ const ProviderList: FC = () => {
|
||||
placeholder={t('settings.provider.search')}
|
||||
value={searchText}
|
||||
style={{ borderRadius: 'var(--list-item-border-radius)', height: 35 }}
|
||||
suffix={<Search size={14} />}
|
||||
prefix={<Search size={14} />}
|
||||
suffix={
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: t('settings.provider.filter.all'),
|
||||
key: 'all',
|
||||
icon: agentFilterEnabled ? <CheckPlaceholder /> : <Check size={14} />,
|
||||
onClick: () => setAgentFilterEnabled(false)
|
||||
},
|
||||
{
|
||||
label: t('settings.provider.filter.agent'),
|
||||
key: 'agent',
|
||||
icon: agentFilterEnabled ? <Check size={14} /> : <CheckPlaceholder />,
|
||||
onClick: () => setAgentFilterEnabled(true)
|
||||
}
|
||||
]
|
||||
}}
|
||||
trigger={['click']}>
|
||||
<FilterButton>
|
||||
<Filter
|
||||
size={14}
|
||||
className={agentFilterEnabled ? 'text-[var(--color-primary)]' : 'text-[var(--color-text-3)]'}
|
||||
/>
|
||||
</FilterButton>
|
||||
</Dropdown>
|
||||
}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
@ -457,4 +503,20 @@ const AddButtonWrapper = styled.div`
|
||||
padding: 10px 8px;
|
||||
`
|
||||
|
||||
const FilterButton = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const CheckPlaceholder = styled.span`
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
`
|
||||
|
||||
export default ProviderList
|
||||
|
||||
@ -2,9 +2,11 @@ import { type AzureOpenAIProvider, type Provider, SystemProviderIds } from '@ren
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
getAnthropicSupportedProviders,
|
||||
getClaudeSupportedProviders,
|
||||
isAIGatewayProvider,
|
||||
isAnthropicProvider,
|
||||
isAnthropicSupportedProvider,
|
||||
isAzureOpenAIProvider,
|
||||
isCherryAIProvider,
|
||||
isGeminiProvider,
|
||||
@ -67,6 +69,26 @@ describe('provider utils', () => {
|
||||
expect(getClaudeSupportedProviders(providers)).toEqual(providers.slice(0, 3))
|
||||
})
|
||||
|
||||
it('filters Anthropic supported providers', () => {
|
||||
const providers = [
|
||||
createProvider({ id: 'anthropic-official', type: 'anthropic' }),
|
||||
createProvider({ id: 'custom-host', anthropicApiHost: 'https://anthropic.local' }),
|
||||
createProvider({ id: 'aihubmix' }),
|
||||
createProvider({ id: 'other' })
|
||||
]
|
||||
|
||||
expect(getAnthropicSupportedProviders(providers)).toEqual(providers.slice(0, 2))
|
||||
})
|
||||
|
||||
it('checks Anthropic supported provider', () => {
|
||||
expect(isAnthropicSupportedProvider(createProvider({ id: 'anthropic-official', type: 'anthropic' }))).toBe(true)
|
||||
expect(
|
||||
isAnthropicSupportedProvider(createProvider({ id: 'custom-host', anthropicApiHost: 'https://anthropic.local' }))
|
||||
).toBe(true)
|
||||
expect(isAnthropicSupportedProvider(createProvider({ id: 'aihubmix' }))).toBe(false)
|
||||
expect(isAnthropicSupportedProvider(createProvider({ id: 'other' }))).toBe(false)
|
||||
})
|
||||
|
||||
it('evaluates message array content support', () => {
|
||||
expect(isSupportArrayContentProvider(createProvider())).toBe(true)
|
||||
|
||||
|
||||
@ -12,6 +12,14 @@ export const getClaudeSupportedProviders = (providers: Provider[]) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const getAnthropicSupportedProviders = (providers: Provider[]) => {
|
||||
return providers.filter(isAnthropicSupportedProvider)
|
||||
}
|
||||
|
||||
export const isAnthropicSupportedProvider = (provider: Provider) => {
|
||||
return provider.type === 'anthropic' || !!provider.anthropicApiHost
|
||||
}
|
||||
|
||||
const NOT_SUPPORT_ARRAY_CONTENT_PROVIDERS = [
|
||||
'deepseek',
|
||||
'baichuan',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user