diff --git a/api/core/entities/model_entities.py b/api/core/entities/model_entities.py index 663a8164c6..12431976f0 100644 --- a/api/core/entities/model_entities.py +++ b/api/core/entities/model_entities.py @@ -29,6 +29,7 @@ class SimpleModelProviderEntity(BaseModel): provider: str label: I18nObject icon_small: I18nObject | None = None + icon_small_dark: I18nObject | None = None icon_large: I18nObject | None = None supported_model_types: list[ModelType] @@ -42,6 +43,7 @@ class SimpleModelProviderEntity(BaseModel): provider=provider_entity.provider, label=provider_entity.label, icon_small=provider_entity.icon_small, + icon_small_dark=provider_entity.icon_small_dark, icon_large=provider_entity.icon_large, supported_model_types=provider_entity.supported_model_types, ) diff --git a/api/core/model_runtime/entities/provider_entities.py b/api/core/model_runtime/entities/provider_entities.py index 0508116962..648b209ef1 100644 --- a/api/core/model_runtime/entities/provider_entities.py +++ b/api/core/model_runtime/entities/provider_entities.py @@ -99,6 +99,7 @@ class SimpleProviderEntity(BaseModel): provider: str label: I18nObject icon_small: I18nObject | None = None + icon_small_dark: I18nObject | None = None icon_large: I18nObject | None = None supported_model_types: Sequence[ModelType] models: list[AIModelEntity] = [] @@ -124,7 +125,6 @@ class ProviderEntity(BaseModel): icon_small: I18nObject | None = None icon_large: I18nObject | None = None icon_small_dark: I18nObject | None = None - icon_large_dark: I18nObject | None = None background: str | None = None help: ProviderHelpEntity | None = None supported_model_types: Sequence[ModelType] diff --git a/api/core/model_runtime/model_providers/model_provider_factory.py b/api/core/model_runtime/model_providers/model_provider_factory.py index e1afc41bee..b8704ef4ed 100644 --- a/api/core/model_runtime/model_providers/model_provider_factory.py +++ b/api/core/model_runtime/model_providers/model_provider_factory.py @@ -300,6 +300,14 @@ class ModelProviderFactory: file_name = provider_schema.icon_small.zh_Hans else: file_name = provider_schema.icon_small.en_US + elif icon_type.lower() == "icon_small_dark": + if not provider_schema.icon_small_dark: + raise ValueError(f"Provider {provider} does not have small dark icon.") + + if lang.lower() == "zh_hans": + file_name = provider_schema.icon_small_dark.zh_Hans + else: + file_name = provider_schema.icon_small_dark.en_US else: if not provider_schema.icon_large: raise ValueError(f"Provider {provider} does not have large icon.") diff --git a/api/services/entities/model_provider_entities.py b/api/services/entities/model_provider_entities.py index d07badefa7..f405546909 100644 --- a/api/services/entities/model_provider_entities.py +++ b/api/services/entities/model_provider_entities.py @@ -69,6 +69,7 @@ class ProviderResponse(BaseModel): label: I18nObject description: I18nObject | None = None icon_small: I18nObject | None = None + icon_small_dark: I18nObject | None = None icon_large: I18nObject | None = None background: str | None = None help: ProviderHelpEntity | None = None @@ -92,6 +93,11 @@ class ProviderResponse(BaseModel): self.icon_small = I18nObject( en_US=f"{url_prefix}/icon_small/en_US", zh_Hans=f"{url_prefix}/icon_small/zh_Hans" ) + if self.icon_small_dark is not None: + self.icon_small_dark = I18nObject( + en_US=f"{url_prefix}/icon_small_dark/en_US", + zh_Hans=f"{url_prefix}/icon_small_dark/zh_Hans", + ) if self.icon_large is not None: self.icon_large = I18nObject( @@ -109,6 +115,7 @@ class ProviderWithModelsResponse(BaseModel): provider: str label: I18nObject icon_small: I18nObject | None = None + icon_small_dark: I18nObject | None = None icon_large: I18nObject | None = None status: CustomConfigurationStatus models: list[ProviderModelWithStatusEntity] @@ -123,6 +130,11 @@ class ProviderWithModelsResponse(BaseModel): en_US=f"{url_prefix}/icon_small/en_US", zh_Hans=f"{url_prefix}/icon_small/zh_Hans" ) + if self.icon_small_dark is not None: + self.icon_small_dark = I18nObject( + en_US=f"{url_prefix}/icon_small_dark/en_US", zh_Hans=f"{url_prefix}/icon_small_dark/zh_Hans" + ) + if self.icon_large is not None: self.icon_large = I18nObject( en_US=f"{url_prefix}/icon_large/en_US", zh_Hans=f"{url_prefix}/icon_large/zh_Hans" @@ -147,6 +159,11 @@ class SimpleProviderEntityResponse(SimpleProviderEntity): en_US=f"{url_prefix}/icon_small/en_US", zh_Hans=f"{url_prefix}/icon_small/zh_Hans" ) + if self.icon_small_dark is not None: + self.icon_small_dark = I18nObject( + en_US=f"{url_prefix}/icon_small_dark/en_US", zh_Hans=f"{url_prefix}/icon_small_dark/zh_Hans" + ) + if self.icon_large is not None: self.icon_large = I18nObject( en_US=f"{url_prefix}/icon_large/en_US", zh_Hans=f"{url_prefix}/icon_large/zh_Hans" diff --git a/api/services/model_provider_service.py b/api/services/model_provider_service.py index 50ddbbf681..a9e2c72534 100644 --- a/api/services/model_provider_service.py +++ b/api/services/model_provider_service.py @@ -79,6 +79,7 @@ class ModelProviderService: label=provider_configuration.provider.label, description=provider_configuration.provider.description, icon_small=provider_configuration.provider.icon_small, + icon_small_dark=provider_configuration.provider.icon_small_dark, icon_large=provider_configuration.provider.icon_large, background=provider_configuration.provider.background, help=provider_configuration.provider.help, @@ -402,6 +403,7 @@ class ModelProviderService: provider=provider, label=first_model.provider.label, icon_small=first_model.provider.icon_small, + icon_small_dark=first_model.provider.icon_small_dark, icon_large=first_model.provider.icon_large, status=CustomConfigurationStatus.ACTIVE, models=[ diff --git a/api/tests/test_containers_integration_tests/services/test_model_provider_service.py b/api/tests/test_containers_integration_tests/services/test_model_provider_service.py index 8cb3572c47..612210ef86 100644 --- a/api/tests/test_containers_integration_tests/services/test_model_provider_service.py +++ b/api/tests/test_containers_integration_tests/services/test_model_provider_service.py @@ -227,6 +227,7 @@ class TestModelProviderService: mock_provider_entity.label = {"en_US": "OpenAI", "zh_Hans": "OpenAI"} mock_provider_entity.description = {"en_US": "OpenAI provider", "zh_Hans": "OpenAI 提供商"} mock_provider_entity.icon_small = {"en_US": "icon_small.png", "zh_Hans": "icon_small.png"} + mock_provider_entity.icon_small_dark = None mock_provider_entity.icon_large = {"en_US": "icon_large.png", "zh_Hans": "icon_large.png"} mock_provider_entity.background = "#FF6B6B" mock_provider_entity.help = None @@ -300,6 +301,7 @@ class TestModelProviderService: mock_provider_entity_llm.label = {"en_US": "OpenAI", "zh_Hans": "OpenAI"} mock_provider_entity_llm.description = {"en_US": "OpenAI provider", "zh_Hans": "OpenAI 提供商"} mock_provider_entity_llm.icon_small = {"en_US": "icon_small.png", "zh_Hans": "icon_small.png"} + mock_provider_entity_llm.icon_small_dark = None mock_provider_entity_llm.icon_large = {"en_US": "icon_large.png", "zh_Hans": "icon_large.png"} mock_provider_entity_llm.background = "#FF6B6B" mock_provider_entity_llm.help = None @@ -313,6 +315,7 @@ class TestModelProviderService: mock_provider_entity_embedding.label = {"en_US": "Cohere", "zh_Hans": "Cohere"} mock_provider_entity_embedding.description = {"en_US": "Cohere provider", "zh_Hans": "Cohere 提供商"} mock_provider_entity_embedding.icon_small = {"en_US": "icon_small.png", "zh_Hans": "icon_small.png"} + mock_provider_entity_embedding.icon_small_dark = None mock_provider_entity_embedding.icon_large = {"en_US": "icon_large.png", "zh_Hans": "icon_large.png"} mock_provider_entity_embedding.background = "#4ECDC4" mock_provider_entity_embedding.help = None @@ -1023,6 +1026,7 @@ class TestModelProviderService: provider="openai", label={"en_US": "OpenAI", "zh_Hans": "OpenAI"}, icon_small={"en_US": "icon_small.png", "zh_Hans": "icon_small.png"}, + icon_small_dark=None, icon_large={"en_US": "icon_large.png", "zh_Hans": "icon_large.png"}, ), model="gpt-3.5-turbo", @@ -1040,6 +1044,7 @@ class TestModelProviderService: provider="openai", label={"en_US": "OpenAI", "zh_Hans": "OpenAI"}, icon_small={"en_US": "icon_small.png", "zh_Hans": "icon_small.png"}, + icon_small_dark=None, icon_large={"en_US": "icon_large.png", "zh_Hans": "icon_large.png"}, ), model="gpt-4", diff --git a/web/app/components/header/account-setting/model-provider-page/declarations.ts b/web/app/components/header/account-setting/model-provider-page/declarations.ts index 134df7b3e8..9a3c45cace 100644 --- a/web/app/components/header/account-setting/model-provider-page/declarations.ts +++ b/web/app/components/header/account-setting/model-provider-page/declarations.ts @@ -217,6 +217,7 @@ export type ModelProvider = { url: TypeWithI18N } icon_small: TypeWithI18N + icon_small_dark?: TypeWithI18N icon_large: TypeWithI18N background?: string supported_model_types: ModelTypeEnum[] @@ -255,6 +256,7 @@ export type Model = { provider: string icon_large: TypeWithI18N icon_small: TypeWithI18N + icon_small_dark?: TypeWithI18N label: TypeWithI18N models: ModelItem[] status: ModelStatusEnum diff --git a/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx index 02c7c404ab..af9cac7fb8 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-icon/index.tsx @@ -6,8 +6,10 @@ import type { import { useLanguage } from '../hooks' import { Group } from '@/app/components/base/icons/src/vender/other' import { OpenaiBlue, OpenaiTeal, OpenaiViolet, OpenaiYellow } from '@/app/components/base/icons/src/public/llm' -import cn from '@/utils/classnames' import { renderI18nObject } from '@/i18n-config' +import { Theme } from '@/types/app' +import cn from '@/utils/classnames' +import useTheme from '@/hooks/use-theme' type ModelIconProps = { provider?: Model | ModelProvider @@ -23,6 +25,7 @@ const ModelIcon: FC = ({ iconClassName, isDeprecated = false, }) => { + const { theme } = useTheme() const language = useLanguage() if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('o')) return
@@ -36,7 +39,16 @@ const ModelIcon: FC = ({ if (provider?.icon_small) { return (
- model-icon + model-icon
) } diff --git a/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx b/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx index 220c43c9da..6192f1d3ed 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx @@ -40,7 +40,12 @@ const ProviderIcon: FC = ({
provider-icon
diff --git a/web/app/components/plugins/card/index.tsx b/web/app/components/plugins/card/index.tsx index e20aef6220..a820a6cef8 100644 --- a/web/app/components/plugins/card/index.tsx +++ b/web/app/components/plugins/card/index.tsx @@ -6,6 +6,8 @@ import { getLanguage } from '@/i18n-config/language' import cn from '@/utils/classnames' import { RiAlertFill } from '@remixicon/react' import React from 'react' +import useTheme from '@/hooks/use-theme' +import { Theme } from '@/types/app' import Partner from '../base/badges/partner' import Verified from '../base/badges/verified' import Icon from '../card/base/card-icon' @@ -50,7 +52,9 @@ const Card = ({ const locale = localeFromProps ? getLanguage(localeFromProps) : defaultLocale const { t } = useMixedTranslation(localeFromProps) const { categoriesMap } = useCategories(t, true) - const { category, type, name, org, label, brief, icon, verified, badges = [] } = payload + const { category, type, name, org, label, brief, icon, icon_dark, verified, badges = [] } = payload + const { theme } = useTheme() + const iconSrc = theme === Theme.dark && icon_dark ? icon_dark : icon const getLocalizedText = (obj: Record | undefined) => obj ? renderI18nObject(obj, locale) : '' const isPartner = badges.includes('partner') @@ -71,7 +75,7 @@ const Card = ({ {!hideCornerMark && } {/* Header */}
- +
diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx index e8e6cf84b1..6cf55ac044 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx @@ -64,10 +64,12 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({ uniqueIdentifier, } = result const icon = await getIconUrl(manifest!.icon) + const iconDark = manifest.icon_dark ? await getIconUrl(manifest.icon_dark) : undefined setUniqueIdentifier(uniqueIdentifier) setManifest({ ...manifest, icon, + icon_dark: iconDark, }) setStep(InstallStep.readyToInstall) }, [getIconUrl]) diff --git a/web/app/components/plugins/install-plugin/utils.ts b/web/app/components/plugins/install-plugin/utils.ts index 79c6d7b031..afbe0f18af 100644 --- a/web/app/components/plugins/install-plugin/utils.ts +++ b/web/app/components/plugins/install-plugin/utils.ts @@ -17,6 +17,7 @@ export const pluginManifestToCardPluginProps = (pluginManifest: PluginDeclaratio brief: pluginManifest.description, description: pluginManifest.description, icon: pluginManifest.icon, + icon_dark: pluginManifest.icon_dark, verified: pluginManifest.verified, introduction: '', repository: '', diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx index 555280268f..197f2e2a92 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx @@ -28,9 +28,9 @@ import { RiHardDrive3Line, } from '@remixicon/react' import { useBoolean } from 'ahooks' -import { useTheme } from 'next-themes' import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import useTheme from '@/hooks/use-theme' import Verified from '../base/badges/verified' import { AutoUpdateLine } from '../../base/icons/src/vender/system' import DeprecationNotice from '../base/deprecation-notice' @@ -86,7 +86,7 @@ const DetailHeader = ({ alternative_plugin_id, } = detail - const { author, category, name, label, description, icon, verified, tool } = detail.declaration || detail + const { author, category, name, label, description, icon, icon_dark, verified, tool } = detail.declaration || detail const isTool = category === PluginCategoryEnum.tool const providerBriefInfo = tool?.identity const providerKey = `${plugin_id}/${providerBriefInfo?.name}` @@ -109,6 +109,11 @@ const DetailHeader = ({ return false }, [isFromMarketplace, latest_version, version]) + const iconFileName = theme === 'dark' && icon_dark ? icon_dark : icon + const iconSrc = iconFileName + ? (iconFileName.startsWith('http') ? iconFileName : `${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${tenant_id}&filename=${iconFileName}`) + : '' + const detailUrl = useMemo(() => { if (isFromGitHub) return `https://github.com/${meta!.repo}` @@ -214,7 +219,7 @@ const DetailHeader = ({ <div className={cn('shrink-0 border-b border-divider-subtle bg-components-panel-bg p-4 pb-3', isReadmeView && 'border-b-0 bg-transparent p-0')}> <div className="flex"> <div className={cn('overflow-hidden rounded-xl border border-components-panel-border-subtle', isReadmeView && 'bg-components-panel-bg')}> - <Icon src={icon.startsWith('http') ? icon : `${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${tenant_id}&filename=${icon}`} /> + <Icon src={iconSrc} /> </div> <div className="ml-3 w-0 grow"> <div className="flex h-5 items-center"> diff --git a/web/app/components/plugins/plugin-item/index.tsx b/web/app/components/plugins/plugin-item/index.tsx index 92a67b6e22..51a72d1e5a 100644 --- a/web/app/components/plugins/plugin-item/index.tsx +++ b/web/app/components/plugins/plugin-item/index.tsx @@ -14,11 +14,11 @@ import { RiHardDrive3Line, RiLoginCircleLine, } from '@remixicon/react' -import { useTheme } from 'next-themes' import type { FC } from 'react' import React, { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { gte } from 'semver' +import useTheme from '@/hooks/use-theme' import Verified from '../base/badges/verified' import Badge from '../../base/badge' import { Github } from '../../base/icons/src/public/common' @@ -58,7 +58,7 @@ const PluginItem: FC<Props> = ({ status, deprecated_reason, } = plugin - const { category, author, name, label, description, icon, verified, meta: declarationMeta } = plugin.declaration + const { category, author, name, label, description, icon, icon_dark, verified, meta: declarationMeta } = plugin.declaration const orgName = useMemo(() => { return [PluginSource.github, PluginSource.marketplace].includes(source) ? author : '' @@ -84,6 +84,10 @@ const PluginItem: FC<Props> = ({ const title = getValueFromI18nObject(label) const descriptionText = getValueFromI18nObject(description) const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) + const iconFileName = theme === 'dark' && icon_dark ? icon_dark : icon + const iconSrc = iconFileName + ? (iconFileName.startsWith('http') ? iconFileName : `${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${tenant_id}&filename=${iconFileName}`) + : '' return ( <div @@ -105,7 +109,7 @@ const PluginItem: FC<Props> = ({ <div className='flex h-10 w-10 items-center justify-center overflow-hidden rounded-xl border-[1px] border-components-panel-border-subtle'> <img className='h-full w-full' - src={`${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${tenant_id}&filename=${icon}`} + src={iconSrc} alt={`plugin-${plugin_unique_identifier}-logo`} /> </div> diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index d9659df3ad..667e2ed668 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -71,6 +71,7 @@ export type PluginDeclaration = { version: string author: string icon: string + icon_dark?: string name: string category: PluginCategoryEnum label: Record<Locale, string> @@ -248,7 +249,7 @@ export type PluginInfoFromMarketPlace = { } export type Plugin = { - type: 'plugin' | 'bundle' | 'model' | 'extension' | 'tool' | 'agent_strategy' + type: 'plugin' | 'bundle' | 'model' | 'extension' | 'tool' | 'agent_strategy' | 'datasource' | 'trigger' org: string author?: string name: string @@ -257,6 +258,7 @@ export type Plugin = { latest_version: string latest_package_identifier: string icon: string + icon_dark?: string verified: boolean label: Record<Locale, string> brief: Record<Locale, string> diff --git a/web/app/components/tools/types.ts b/web/app/components/tools/types.ts index 499a07342d..e20061a899 100644 --- a/web/app/components/tools/types.ts +++ b/web/app/components/tools/types.ts @@ -49,6 +49,7 @@ export type Collection = { author: string description: TypeWithI18N icon: string | Emoji + icon_dark?: string | Emoji label: TypeWithI18N type: CollectionType | string team_credentials: Record<string, any> diff --git a/web/app/components/workflow/block-selector/tool/action-item.tsx b/web/app/components/workflow/block-selector/tool/action-item.tsx index 01c319327a..1ca61b3039 100644 --- a/web/app/components/workflow/block-selector/tool/action-item.tsx +++ b/web/app/components/workflow/block-selector/tool/action-item.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React from 'react' +import React, { useMemo } from 'react' import type { ToolWithProvider } from '../../types' import { BlockEnum } from '../../types' import type { ToolDefaultValue } from '../types' @@ -10,9 +10,13 @@ import { useGetLanguage } from '@/context/i18n' import BlockIcon from '../../block-icon' import cn from '@/utils/classnames' import { useTranslation } from 'react-i18next' +import useTheme from '@/hooks/use-theme' +import { Theme } from '@/types/app' import { basePath } from '@/utils/var' -const normalizeProviderIcon = (icon: ToolWithProvider['icon']) => { +const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => { + if (!icon) + return icon if (typeof icon === 'string' && basePath && icon.startsWith('/') && !icon.startsWith(`${basePath}/`)) return `${basePath}${icon}` return icon @@ -36,6 +40,20 @@ const ToolItem: FC<Props> = ({ const { t } = useTranslation() const language = useGetLanguage() + const { theme } = useTheme() + const normalizedIcon = useMemo<ToolWithProvider['icon']>(() => { + return normalizeProviderIcon(provider.icon) ?? provider.icon + }, [provider.icon]) + const normalizedIconDark = useMemo(() => { + if (!provider.icon_dark) + return undefined + return normalizeProviderIcon(provider.icon_dark) ?? provider.icon_dark + }, [provider.icon_dark]) + const providerIcon = useMemo(() => { + if (theme === Theme.dark && normalizedIconDark) + return normalizedIconDark + return normalizedIcon + }, [theme, normalizedIcon, normalizedIconDark]) return ( <Tooltip @@ -49,7 +67,7 @@ const ToolItem: FC<Props> = ({ size='md' className='mb-2' type={BlockEnum.Tool} - toolIcon={provider.icon} + toolIcon={providerIcon} /> <div className='mb-1 text-sm leading-5 text-text-primary'>{payload.label[language]}</div> <div className='text-xs leading-[18px] text-text-secondary'>{payload.description[language]}</div> @@ -73,7 +91,8 @@ const ToolItem: FC<Props> = ({ provider_name: provider.name, plugin_id: provider.plugin_id, plugin_unique_identifier: provider.plugin_unique_identifier, - provider_icon: normalizeProviderIcon(provider.icon), + provider_icon: normalizedIcon, + provider_icon_dark: normalizedIconDark, tool_name: payload.name, tool_label: payload.label[language], tool_description: payload.description[language], diff --git a/web/app/components/workflow/block-selector/tool/tool.tsx b/web/app/components/workflow/block-selector/tool/tool.tsx index 38be8d19d6..2ce8f8130e 100644 --- a/web/app/components/workflow/block-selector/tool/tool.tsx +++ b/web/app/components/workflow/block-selector/tool/tool.tsx @@ -14,11 +14,15 @@ import ActionItem from './action-item' import BlockIcon from '../../block-icon' import { useTranslation } from 'react-i18next' import { useHover } from 'ahooks' +import useTheme from '@/hooks/use-theme' +import { Theme } from '@/types/app' import McpToolNotSupportTooltip from '../../nodes/_base/components/mcp-tool-not-support-tooltip' import { Mcp } from '@/app/components/base/icons/src/vender/other' import { basePath } from '@/utils/var' -const normalizeProviderIcon = (icon: ToolWithProvider['icon']) => { +const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => { + if (!icon) + return icon if (typeof icon === 'string' && basePath && icon.startsWith('/') && !icon.startsWith(`${basePath}/`)) return `${basePath}${icon}` return icon @@ -59,6 +63,20 @@ const Tool: FC<Props> = ({ const isHovering = useHover(ref) const isMCPTool = payload.type === CollectionType.mcp const isShowCanNotChooseMCPTip = !canChooseMCPTool && isMCPTool + const { theme } = useTheme() + const normalizedIcon = useMemo<ToolWithProvider['icon']>(() => { + return normalizeProviderIcon(payload.icon) ?? payload.icon + }, [payload.icon]) + const normalizedIconDark = useMemo(() => { + if (!payload.icon_dark) + return undefined + return normalizeProviderIcon(payload.icon_dark) ?? payload.icon_dark + }, [payload.icon_dark]) + const providerIcon = useMemo<ToolWithProvider['icon']>(() => { + if (theme === Theme.dark && normalizedIconDark) + return normalizedIconDark + return normalizedIcon + }, [theme, normalizedIcon, normalizedIconDark]) const getIsDisabled = useCallback((tool: ToolType) => { if (!selectedTools || !selectedTools.length) return false return selectedTools.some(selectedTool => (selectedTool.provider_name === payload.name || selectedTool.provider_name === payload.id) && selectedTool.tool_name === tool.name) @@ -95,7 +113,8 @@ const Tool: FC<Props> = ({ provider_name: payload.name, plugin_id: payload.plugin_id, plugin_unique_identifier: payload.plugin_unique_identifier, - provider_icon: normalizeProviderIcon(payload.icon), + provider_icon: normalizedIcon, + provider_icon_dark: normalizedIconDark, tool_name: tool.name, tool_label: tool.label[language], tool_description: tool.description[language], @@ -177,7 +196,8 @@ const Tool: FC<Props> = ({ provider_name: payload.name, plugin_id: payload.plugin_id, plugin_unique_identifier: payload.plugin_unique_identifier, - provider_icon: normalizeProviderIcon(payload.icon), + provider_icon: normalizedIcon, + provider_icon_dark: normalizedIconDark, tool_name: tool.name, tool_label: tool.label[language], tool_description: tool.description[language], @@ -192,7 +212,7 @@ const Tool: FC<Props> = ({ <BlockIcon className='shrink-0' type={BlockEnum.Tool} - toolIcon={payload.icon} + toolIcon={providerIcon} /> <div className='ml-2 flex w-0 grow items-center text-sm text-text-primary'> <span className='max-w-[250px] truncate'>{notShowProvider ? actions[0]?.label[language] : payload.label[language]}</span> diff --git a/web/app/components/workflow/block-selector/trigger-plugin/item.tsx b/web/app/components/workflow/block-selector/trigger-plugin/item.tsx index 702d3603fb..49db8c6c3e 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/item.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/item.tsx @@ -10,6 +10,17 @@ import BlockIcon from '@/app/components/workflow/block-icon' import { BlockEnum } from '@/app/components/workflow/types' import type { TriggerDefaultValue, TriggerWithProvider } from '@/app/components/workflow/block-selector/types' import TriggerPluginActionItem from './action-item' +import { Theme } from '@/types/app' +import useTheme from '@/hooks/use-theme' +import { basePath } from '@/utils/var' + +const normalizeProviderIcon = (icon?: TriggerWithProvider['icon']) => { + if (!icon) + return icon + if (typeof icon === 'string' && basePath && icon.startsWith('/') && !icon.startsWith(`${basePath}/`)) + return `${basePath}${icon}` + return icon +} type Props = { className?: string @@ -26,6 +37,7 @@ const TriggerPluginItem: FC<Props> = ({ }) => { const { t } = useTranslation() const language = useGetLanguage() + const { theme } = useTheme() const notShowProvider = payload.type === CollectionType.workflow const actions = payload.events const hasAction = !notShowProvider @@ -55,6 +67,23 @@ const TriggerPluginItem: FC<Props> = ({ return payload.author || '' }, [payload.author, payload.type, t]) + const normalizedIcon = useMemo<TriggerWithProvider['icon']>(() => { + return normalizeProviderIcon(payload.icon) ?? payload.icon + }, [payload.icon]) + const normalizedIconDark = useMemo(() => { + if (!payload.icon_dark) + return undefined + return normalizeProviderIcon(payload.icon_dark) ?? payload.icon_dark + }, [payload.icon_dark]) + const providerIcon = useMemo<TriggerWithProvider['icon']>(() => { + if (theme === Theme.dark && normalizedIconDark) + return normalizedIconDark + return normalizedIcon + }, [normalizedIcon, normalizedIconDark, theme]) + const providerWithResolvedIcon = useMemo(() => ({ + ...payload, + icon: providerIcon, + }), [payload, providerIcon]) return ( <div @@ -99,7 +128,7 @@ const TriggerPluginItem: FC<Props> = ({ <BlockIcon className='shrink-0' type={BlockEnum.TriggerPlugin} - toolIcon={payload.icon} + toolIcon={providerIcon} /> <div className='ml-2 flex min-w-0 flex-1 items-center text-sm text-text-primary'> <span className='max-w-[200px] truncate'>{notShowProvider ? actions[0]?.label[language] : payload.label[language]}</span> @@ -118,7 +147,7 @@ const TriggerPluginItem: FC<Props> = ({ actions.map(action => ( <TriggerPluginActionItem key={action.name} - provider={payload} + provider={providerWithResolvedIcon} payload={action} onSelect={onSelect} disabled={false} diff --git a/web/app/components/workflow/block-selector/types.ts b/web/app/components/workflow/block-selector/types.ts index b69453e937..1e5acbbeb3 100644 --- a/web/app/components/workflow/block-selector/types.ts +++ b/web/app/components/workflow/block-selector/types.ts @@ -59,6 +59,7 @@ export type ToolDefaultValue = PluginCommonDefaultValue & { meta?: PluginMeta plugin_id?: string provider_icon?: Collection['icon'] + provider_icon_dark?: Collection['icon'] plugin_unique_identifier?: string } diff --git a/web/app/components/workflow/hooks/use-tool-icon.ts b/web/app/components/workflow/hooks/use-tool-icon.ts index 8276989ee3..faf962d450 100644 --- a/web/app/components/workflow/hooks/use-tool-icon.ts +++ b/web/app/components/workflow/hooks/use-tool-icon.ts @@ -15,6 +15,7 @@ import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types' import type { ToolNodeType } from '../nodes/tool/types' import type { DataSourceNodeType } from '../nodes/data-source/types' import type { TriggerWithProvider } from '../block-selector/types' +import useTheme from '@/hooks/use-theme' const isTriggerPluginNode = (data: Node['data']): data is PluginTriggerNodeType => data.type === BlockEnum.TriggerPlugin @@ -22,17 +23,30 @@ const isToolNode = (data: Node['data']): data is ToolNodeType => data.type === B const isDataSourceNode = (data: Node['data']): data is DataSourceNodeType => data.type === BlockEnum.DataSource +type IconValue = ToolWithProvider['icon'] + +const resolveIconByTheme = ( + currentTheme: string | undefined, + icon?: IconValue, + iconDark?: IconValue, +) => { + if (currentTheme === 'dark' && iconDark) + return iconDark + return icon +} + const findTriggerPluginIcon = ( identifiers: (string | undefined)[], triggers: TriggerWithProvider[] | undefined, + currentTheme?: string, ) => { const targetTriggers = triggers || [] for (const identifier of identifiers) { if (!identifier) continue const matched = targetTriggers.find(trigger => trigger.id === identifier || canFindTool(trigger.id, identifier)) - if (matched?.icon) - return matched.icon + if (matched) + return resolveIconByTheme(currentTheme, matched.icon, matched.icon_dark) } return undefined } @@ -44,6 +58,7 @@ export const useToolIcon = (data?: Node['data']) => { const { data: mcpTools } = useAllMCPTools() const dataSourceList = useStore(s => s.dataSourceList) const { data: triggerPlugins } = useAllTriggerPlugins() + const { theme } = useTheme() const toolIcon = useMemo(() => { if (!data) @@ -57,6 +72,7 @@ export const useToolIcon = (data?: Node['data']) => { data.provider_name, ], triggerPlugins, + theme, ) if (icon) return icon @@ -100,12 +116,16 @@ export const useToolIcon = (data?: Node['data']) => { return true return data.provider_name === toolWithProvider.name }) - if (matched?.icon) - return matched.icon + if (matched) { + const icon = resolveIconByTheme(theme, matched.icon, matched.icon_dark) + if (icon) + return icon + } } - if (data.provider_icon) - return data.provider_icon + const fallbackIcon = resolveIconByTheme(theme, data.provider_icon, data.provider_icon_dark) + if (fallbackIcon) + return fallbackIcon return '' } @@ -114,7 +134,7 @@ export const useToolIcon = (data?: Node['data']) => { return dataSourceList?.find(toolWithProvider => toolWithProvider.plugin_id === data.plugin_id)?.icon || '' return '' - }, [data, dataSourceList, buildInTools, customTools, workflowTools, mcpTools, triggerPlugins]) + }, [data, dataSourceList, buildInTools, customTools, workflowTools, mcpTools, triggerPlugins, theme]) return toolIcon } @@ -126,6 +146,7 @@ export const useGetToolIcon = () => { const { data: mcpTools } = useAllMCPTools() const { data: triggerPlugins } = useAllTriggerPlugins() const workflowStore = useWorkflowStore() + const { theme } = useTheme() const getToolIcon = useCallback((data: Node['data']) => { const { @@ -144,6 +165,7 @@ export const useGetToolIcon = () => { data.provider_name, ], triggerPlugins, + theme, ) } @@ -182,12 +204,16 @@ export const useGetToolIcon = () => { return true return data.provider_name === toolWithProvider.name }) - if (matched?.icon) - return matched.icon + if (matched) { + const icon = resolveIconByTheme(theme, matched.icon, matched.icon_dark) + if (icon) + return icon + } } - if (data.provider_icon) - return data.provider_icon + const fallbackIcon = resolveIconByTheme(theme, data.provider_icon, data.provider_icon_dark) + if (fallbackIcon) + return fallbackIcon return undefined } @@ -196,7 +222,7 @@ export const useGetToolIcon = () => { return dataSourceList?.find(toolWithProvider => toolWithProvider.plugin_id === data.plugin_id)?.icon return undefined - }, [workflowStore, triggerPlugins, buildInTools, customTools, workflowTools, mcpTools]) + }, [workflowStore, triggerPlugins, buildInTools, customTools, workflowTools, mcpTools, theme]) return getToolIcon } diff --git a/web/app/components/workflow/nodes/tool/types.ts b/web/app/components/workflow/nodes/tool/types.ts index 6e6ef858dc..da3b7f7b31 100644 --- a/web/app/components/workflow/nodes/tool/types.ts +++ b/web/app/components/workflow/nodes/tool/types.ts @@ -22,5 +22,6 @@ export type ToolNodeType = CommonNodeType & { params?: Record<string, any> plugin_id?: string provider_icon?: Collection['icon'] + provider_icon_dark?: Collection['icon_dark'] plugin_unique_identifier?: string } diff --git a/web/service/use-triggers.ts b/web/service/use-triggers.ts index cfb786e4a9..67522d2e55 100644 --- a/web/service/use-triggers.ts +++ b/web/service/use-triggers.ts @@ -25,6 +25,7 @@ const convertToTriggerWithProvider = (provider: TriggerProviderApiEntity): Trigg author: provider.author, description: provider.description, icon: provider.icon || '', + icon_dark: provider.icon_dark || '', label: provider.label, type: CollectionType.trigger, team_credentials: {},