From df7fd26907b65e78dc7143ea3fb2faa4a8f9dade Mon Sep 17 00:00:00 2001 From: Yuhang <190720896+YuhangHere@users.noreply.github.com> Date: Tue, 2 Sep 2025 00:50:54 +0800 Subject: [PATCH] fix: complete PoeLogo rendering support across provider UI components (#9756) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: PoeLogo in ProviderLogoPicker * fix: PoeLogo in AddProviderPopup 该文件缺少足够参数匹配‘poe’,鉴于目前只有poe用的‘svg’,简便起见所以只匹配‘svg’而不匹配‘poe’,以后可能需要重构代码 * fix: PoeLogo in ProviderList * refactor: provider logo rendering logic 1.三个地方渲染头像统一命名成getProviderLogo 2.鉴于目前PROVIDER_LOGO_MAP中只有poe的是‘svg’,故对于判断简化处理,以后有其它提供商改成‘svg’时再重构代码 3.优化内置头像选择器组件部分代码 * Update index.tsx * refactor(provider-logo): 将获取provider logo的逻辑统一到useProviderLogo钩子中 将原本分散在各处的获取provider logo的逻辑集中到新创建的useProviderLogo钩子中 移除config/providers.ts中的getProviderLogo函数 修改所有使用getProviderLogo的地方改为使用新钩子 * refactor(useProvider): 移除未使用的getProviderLogo函数 * refactor(ProviderAvatar): 重构提供者头像组件为更灵活的API设计 将原有的getProviderAvatar函数重构为ProviderAvatar组件,提供更灵活的属性和样式控制 统一所有使用提供者头像的地方调用新组件,移除重复的样式代码 * refactor(ImageStorage): 使用put替代add方法更新数据库 将db.settings.add方法替换为db.settings.put,以提高数据更新的准确性和一致性 * feat(llm): 添加logos状态管理及相关操作 添加logos状态字段及setLogos、setLogo、removeLogo操作方法,用于管理不同LLM提供商的logo * refactor(hooks): 使用 useCallback 优化 useTimer 的性能 将 setTimeoutTimer 和 setIntervalTimer 函数用 useCallback 包裹以避免不必要的重新创建 * refactor(provider-logo): 重构提供商logo管理逻辑,使用redux存储logo状态 - 将logo管理逻辑从组件中抽离到useProviderLogo hook - 使用redux存储和更新logo状态 - 优化logo的加载和保存流程 - 统一处理系统提供商和自定义提供商的logo显示 - 移除冗余的ImageStorage直接调用 * test(api): 修复ApiService测试中的mock数据缺失 * fix(store): 更新持久化存储版本至144并添加迁移逻辑 添加版本144的迁移逻辑,初始化llm.logos状态以修复潜在问题 --------- Co-authored-by: icarus --- .../components/ProviderLogoPicker/index.tsx | 27 +- src/renderer/src/config/providers.ts | 4 - src/renderer/src/hooks/useProviderLogo.tsx | 112 +++++++ src/renderer/src/hooks/useTimer.ts | 10 +- .../src/pages/paintings/AihubmixPage.tsx | 18 +- .../src/pages/paintings/DmxapiPage.tsx | 18 +- .../src/pages/paintings/NewApiPage.tsx | 18 +- .../src/pages/paintings/TokenFluxPage.tsx | 13 +- .../src/pages/paintings/ZhipuPage.tsx | 18 +- .../ProviderSettings/AddProviderPopup.tsx | 104 ++----- .../ProviderSettings/ProviderList.tsx | 281 +++++++----------- src/renderer/src/services/ImageStorage.ts | 4 +- .../src/services/__tests__/ApiService.test.ts | 3 +- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/llm.ts | 20 +- src/renderer/src/store/migrate.ts | 9 + 16 files changed, 324 insertions(+), 337 deletions(-) create mode 100644 src/renderer/src/hooks/useProviderLogo.tsx diff --git a/src/renderer/src/components/ProviderLogoPicker/index.tsx b/src/renderer/src/components/ProviderLogoPicker/index.tsx index b18b2b47d7..619c41ebec 100644 --- a/src/renderer/src/components/ProviderLogoPicker/index.tsx +++ b/src/renderer/src/components/ProviderLogoPicker/index.tsx @@ -1,6 +1,9 @@ import { SearchOutlined } from '@ant-design/icons' import { PROVIDER_LOGO_MAP } from '@renderer/config/providers' +import { useProviderAvatar } from '@renderer/hooks/useProviderLogo' import { getProviderLabel } from '@renderer/i18n/label' +import { useAppSelector } from '@renderer/store' +import { isSystemProvider } from '@renderer/types' import { Input, Tooltip } from 'antd' import { FC, useMemo, useState } from 'react' import styled from 'styled-components' @@ -12,19 +15,21 @@ interface Props { // 用于选择内置头像的提供商Logo选择器组件 const ProviderLogoPicker: FC = ({ onProviderClick }) => { const [searchText, setSearchText] = useState('') + const providers = useAppSelector((state) => state.llm.providers) + const { ProviderAvatar } = useProviderAvatar() const filteredProviders = useMemo(() => { - const providers = Object.entries(PROVIDER_LOGO_MAP).map(([id, logo]) => ({ - id, - logo, - name: getProviderLabel(id) + const _providers = providers.filter(isSystemProvider).map((p) => ({ + id: p.id, + name: p.name, + label: getProviderLabel(p.id), + logo: PROVIDER_LOGO_MAP[p.id] })) - - if (!searchText) return providers + if (!searchText) return _providers const searchLower = searchText.toLowerCase() - return providers.filter((p) => p.name.toLowerCase().includes(searchLower)) - }, [searchText]) + return _providers.filter((p) => `${p.name} ${p.id} ${p.label}`.toLowerCase().includes(searchLower)) + }, [providers, searchText]) const handleProviderClick = (event: React.MouseEvent, providerId: string) => { event.stopPropagation() @@ -48,10 +53,10 @@ const ProviderLogoPicker: FC = ({ onProviderClick }) => { /> - {filteredProviders.map(({ id, logo, name }) => ( - + {filteredProviders.map(({ id, label }) => ( + handleProviderClick(e, id)}> - {name} + ))} diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 194d4c20b7..af678ed765 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -664,10 +664,6 @@ export const PROVIDER_LOGO_MAP: AtLeast = { poe: 'svg' // use svg icon component } as const -export function getProviderLogo(providerId: string) { - return PROVIDER_LOGO_MAP[providerId as keyof typeof PROVIDER_LOGO_MAP] -} - // export const SUPPORTED_REANK_PROVIDERS = ['silicon', 'jina', 'voyageai', 'dashscope', 'aihubmix'] export const NOT_SUPPORTED_RERANK_PROVIDERS = ['ollama', 'lmstudio'] as const satisfies SystemProviderId[] export const ONLY_SUPPORTED_DIMENSION_PROVIDERS = ['ollama', 'infini'] as const satisfies SystemProviderId[] diff --git a/src/renderer/src/hooks/useProviderLogo.tsx b/src/renderer/src/hooks/useProviderLogo.tsx new file mode 100644 index 0000000000..49d48e9ebd --- /dev/null +++ b/src/renderer/src/hooks/useProviderLogo.tsx @@ -0,0 +1,112 @@ +import { loggerService } from '@logger' +import { PoeLogo } from '@renderer/components/Icons' +import { PROVIDER_LOGO_MAP } from '@renderer/config/providers' +import ImageStorage from '@renderer/services/ImageStorage' +import { getProviderById } from '@renderer/services/ProviderService' +import { useAppSelector } from '@renderer/store' +import { removeLogo, setLogo, setLogos } from '@renderer/store/llm' +import { isSystemProviderId, Provider } from '@renderer/types' +import { generateColorFromChar, getFirstCharacter, getForegroundColor } from '@renderer/utils' +import { Avatar, AvatarProps } from 'antd' +import { AvatarSize } from 'antd/es/avatar/AvatarContext' +import { isEmpty } from 'lodash' +import { useCallback, useEffect } from 'react' +import { useDispatch } from 'react-redux' +import styled, { CSSProperties } from 'styled-components' + +const logger = loggerService.withContext('useProviderLogo') + +export const useProviderAvatar = () => { + const providers = useAppSelector((state) => state.llm.providers) + const logos = useAppSelector((state) => state.llm.logos) + const dispatch = useDispatch() + + const saveLogo = useCallback( + async (logo: string, providerId: string) => { + ImageStorage.set(`provider-${providerId}`, logo) + dispatch(setLogo({ id: providerId, logo })) + }, + [dispatch] + ) + + const deleteLogo = useCallback( + async (id: string) => { + ImageStorage.remove(id).catch((e) => logger.error('Falied to remove image.', e as Error)) + dispatch(removeLogo(id)) + }, + [dispatch] + ) + + useEffect(() => { + const getLogos = async () => { + const _logos = {} + for (const p of providers) { + _logos[p.id] = await ImageStorage.get(`provider-${p.id}`) + } + dispatch(setLogos(_logos)) + } + getLogos() + }, [dispatch, providers]) + + const ProviderAvatar = useCallback( + ({ + pid, + name, + size, + src, + style, + ...rest + }: { + pid?: string + name?: string + size?: number + src?: string + } & AvatarProps) => { + if (src) { + return + } + let provider: Provider | undefined + if (pid) { + // 特殊处理一下svg格式 + if (isSystemProviderId(pid)) { + const logoSrc = PROVIDER_LOGO_MAP[pid] + switch (pid) { + case 'poe': + return + default: + return + } + } + + const customLogo = logos[pid] + if (customLogo) { + return + } + if (!name) { + // generate a avatar for custom provider + provider = getProviderById(pid) + if (!provider) { + return null + } + } + } + return + }, + [logos] + ) + + function GeneratedAvatar({ name, size, style }: { name: string; size?: AvatarSize; style?: CSSProperties }) { + const backgroundColor = generateColorFromChar(name) + const color = name ? getForegroundColor(backgroundColor) : 'white' + return ( + + {getFirstCharacter(!isEmpty(name) ? name : 'P')} + + ) + } + return { ProviderAvatar, GeneratedAvatar, saveLogo, deleteLogo, logos } +} + +const ProviderLogo = styled(Avatar)` + border: 0.5px solid var(--color-border); +` diff --git a/src/renderer/src/hooks/useTimer.ts b/src/renderer/src/hooks/useTimer.ts index 139c3e7143..556ff64567 100644 --- a/src/renderer/src/hooks/useTimer.ts +++ b/src/renderer/src/hooks/useTimer.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react' +import { useCallback, useEffect, useRef } from 'react' /** * 定时器管理 Hook,用于管理 setTimeout 和 setInterval 定时器,支持通过 key 来标识不同的定时器 @@ -65,12 +65,12 @@ export const useTimer = () => { * cleanup(); * ``` */ - const setTimeoutTimer = (key: string, ...args: Parameters) => { + const setTimeoutTimer = useCallback((key: string, ...args: Parameters) => { clearTimeout(timeoutMapRef.current.get(key)) const timer = setTimeout(...args) timeoutMapRef.current.set(key, timer) return () => clearTimeoutTimer(key) - } + }, []) /** * 设置一个 setInterval 定时器 @@ -89,12 +89,12 @@ export const useTimer = () => { * cleanup(); * ``` */ - const setIntervalTimer = (key: string, ...args: Parameters) => { + const setIntervalTimer = useCallback((key: string, ...args: Parameters) => { clearInterval(intervalMapRef.current.get(key)) const timer = setInterval(...args) intervalMapRef.current.set(key, timer) return () => clearIntervalTimer(key) - } + }, []) /** * 清除指定 key 的 setTimeout 定时器 diff --git a/src/renderer/src/pages/paintings/AihubmixPage.tsx b/src/renderer/src/pages/paintings/AihubmixPage.tsx index 6c9236e7dd..3b2ea14d5c 100644 --- a/src/renderer/src/pages/paintings/AihubmixPage.tsx +++ b/src/renderer/src/pages/paintings/AihubmixPage.tsx @@ -7,11 +7,11 @@ import { HStack } from '@renderer/components/Layout' import Scrollbar from '@renderer/components/Scrollbar' import TranslateButton from '@renderer/components/TranslateButton' import { isMac } from '@renderer/config/constant' -import { getProviderLogo } from '@renderer/config/providers' import { LanguagesEnum } from '@renderer/config/translate' import { useTheme } from '@renderer/context/ThemeProvider' import { usePaintings } from '@renderer/hooks/usePaintings' import { useAllProviders } from '@renderer/hooks/useProvider' +import { useProviderAvatar } from '@renderer/hooks/useProviderLogo' import { useRuntime } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' import { getProviderLabel } from '@renderer/i18n/label' @@ -22,7 +22,7 @@ import { setGenerating } from '@renderer/store/runtime' import type { FileMetadata } from '@renderer/types' import type { PaintingAction, PaintingsState } from '@renderer/types' import { getErrorMessage, uuid } from '@renderer/utils' -import { Avatar, Button, Input, InputNumber, Radio, Segmented, Select, Slider, Switch, Tooltip, Upload } from 'antd' +import { Button, Input, InputNumber, Radio, Segmented, Select, Slider, Switch, Tooltip, Upload } from 'antd' import TextArea from 'antd/es/input/TextArea' import { Info } from 'lucide-react' import type { FC } from 'react' @@ -54,6 +54,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { aihubmix_image_edit, aihubmix_image_upscale } = usePaintings() + const { ProviderAvatar } = useProviderAvatar() const paintings = useMemo(() => { return { @@ -847,12 +848,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { {t('common.provider')} {t('paintings.learn_more')} - + @@ -860,7 +856,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { {providerOptions.map((provider) => ( - + {provider.label} @@ -1029,10 +1025,6 @@ const StyledInputNumber = styled(InputNumber)` width: 70px; ` -const ProviderLogo = styled(Avatar)` - border: 0.5px solid var(--color-border); -` - // 添加新的样式组件 const ModeSegmentedContainer = styled.div` display: flex; diff --git a/src/renderer/src/pages/paintings/DmxapiPage.tsx b/src/renderer/src/pages/paintings/DmxapiPage.tsx index 21a784397f..9212bb262c 100644 --- a/src/renderer/src/pages/paintings/DmxapiPage.tsx +++ b/src/renderer/src/pages/paintings/DmxapiPage.tsx @@ -4,9 +4,9 @@ import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navb import { HStack } from '@renderer/components/Layout' import Scrollbar from '@renderer/components/Scrollbar' import { isMac } from '@renderer/config/constant' -import { getProviderLogo } from '@renderer/config/providers' import { usePaintings } from '@renderer/hooks/usePaintings' import { useAllProviders } from '@renderer/hooks/useProvider' +import { useProviderAvatar } from '@renderer/hooks/useProviderLogo' import { useRuntime } from '@renderer/hooks/useRuntime' import { getProviderLabel } from '@renderer/i18n/label' import FileManager from '@renderer/services/FileManager' @@ -15,7 +15,7 @@ import { setGenerating } from '@renderer/store/runtime' import type { FileMetadata } from '@renderer/types' import { convertToBase64, uuid } from '@renderer/utils' import { DmxapiPainting } from '@types' -import { Avatar, Button, Input, InputNumber, Segmented, Select, Switch, Tooltip } from 'antd' +import { Button, Input, InputNumber, Segmented, Select, Switch, Tooltip } from 'antd' import TextArea from 'antd/es/input/TextArea' import { Info } from 'lucide-react' import React, { FC, useEffect, useRef, useState } from 'react' @@ -46,6 +46,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { const [painting, setPainting] = useState(dmxapi_paintings?.[0] || DEFAULT_PAINTING) const { t } = useTranslation() const providers = useAllProviders() + const { ProviderAvatar } = useProviderAvatar() const providerOptions = Options.map((option) => { const provider = providers.find((p) => p.id === option) if (provider) { @@ -814,19 +815,15 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { {t('paintings.top_up')} - + {providerOptions.map((provider) => ( - + {provider.label} @@ -581,8 +577,4 @@ const SelectOptionContainer = styled.div` gap: 8px; ` -const ProviderLogo = styled(Avatar)` - border-radius: 4px; -` - export default ZhipuPage diff --git a/src/renderer/src/pages/settings/ProviderSettings/AddProviderPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/AddProviderPopup.tsx index 586271e902..821c0ee7bc 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/AddProviderPopup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/AddProviderPopup.tsx @@ -1,18 +1,18 @@ -import { loggerService } from '@logger' import { Center, VStack } from '@renderer/components/Layout' import ProviderLogoPicker from '@renderer/components/ProviderLogoPicker' import { TopView } from '@renderer/components/TopView' import { PROVIDER_LOGO_MAP } from '@renderer/config/providers' +import { useProviderAvatar } from '@renderer/hooks/useProviderLogo' import ImageStorage from '@renderer/services/ImageStorage' import { Provider, ProviderType } from '@renderer/types' -import { compressImage, generateColorFromChar, getForegroundColor } from '@renderer/utils' +import { compressImage } from '@renderer/utils' import { Divider, Dropdown, Form, Input, Modal, Popover, Select, Upload } from 'antd' import { ItemType } from 'antd/es/menu/interface' import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -const logger = loggerService.withContext('AddProviderPopup') +// const logger = loggerService.withContext('AddProviderPopup') interface Props { provider?: Provider @@ -23,27 +23,20 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { const [open, setOpen] = useState(true) const [name, setName] = useState(provider?.name || '') const [type, setType] = useState(provider?.type || 'openai') - const [logo, setLogo] = useState(null) const [logoPickerOpen, setLogoPickerOpen] = useState(false) const [dropdownOpen, setDropdownOpen] = useState(false) const { t } = useTranslation() const uploadRef = useRef(null) + const [logo, setLogo] = useState() + + const { ProviderAvatar, logos, saveLogo } = useProviderAvatar() useEffect(() => { - if (provider?.id) { - const loadLogo = async () => { - try { - const logoData = await ImageStorage.get(`provider-${provider.id}`) - if (logoData) { - setLogo(logoData) - } - } catch (error) { - logger.error('Failed to load logo', error as Error) - } - } - loadLogo() + if (provider) { + const logo = logos[provider.id] + setLogo(logo) } - }, [provider]) + }, [provider, logos, setLogo]) const onOk = async () => { setOpen(false) @@ -74,12 +67,9 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { const logoUrl = PROVIDER_LOGO_MAP[providerId] if (provider?.id) { - await ImageStorage.set(`provider-${provider.id}`, logoUrl) - const savedLogo = await ImageStorage.get(`provider-${provider.id}`) - setLogo(savedLogo) - } else { - setLogo(logoUrl) + saveLogo(logoUrl, provider.id) } + setLogo(logoUrl) setLogoPickerOpen(false) } catch (error: any) { @@ -89,10 +79,9 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { const handleReset = async () => { try { - setLogo(null) - if (provider?.id) { - await ImageStorage.set(`provider-${provider.id}`, '') + saveLogo('', provider.id) + ImageStorage.set(`provider-${provider.id}`, '') } setDropdownOpen(false) @@ -101,10 +90,6 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { } } - const getInitials = () => { - return name.charAt(0) || 'P' - } - const items = [ { key: 'upload', @@ -133,7 +118,7 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { await ImageStorage.set(`provider-${provider.id}`, logoData) } const savedLogo = await ImageStorage.get(`provider-${provider.id}`) - setLogo(savedLogo) + saveLogo(savedLogo, provider.id) } else { // 临时保存在内存中,等创建 provider 后会在调用方保存 const tempUrl = await new Promise((resolve) => { @@ -171,10 +156,6 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { } ] satisfies ItemType[] - // for logo - const backgroundColor = generateColorFromChar(name) - const color = name ? getForegroundColor(backgroundColor) : 'white' - return ( = ({ provider, resolve }) => { } }} placement="bottom"> - {logo ? ( - - ) : ( - - {getInitials()} - - )} + + + @@ -258,39 +235,6 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { ) } -const ProviderLogo = styled.img` - cursor: pointer; - width: 60px; - height: 60px; - border-radius: 12px; - object-fit: contain; - transition: opacity 0.3s ease; - background-color: var(--color-background-soft); - padding: 5px; - border: 0.5px solid var(--color-border); - &:hover { - opacity: 0.8; - } -` - -const ProviderInitialsLogo = styled.div` - cursor: pointer; - width: 60px; - height: 60px; - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; - font-size: 30px; - font-weight: 500; - transition: opacity 0.3s ease; - background-color: var(--color-background-soft); - border: 0.5px solid var(--color-border); - &:hover { - opacity: 0.8; - } -` - const MenuItem = styled.div` width: 100%; text-align: center; @@ -321,3 +265,15 @@ export default class AddProviderPopup { }) } } + +const ProviderLogo = styled.div` + cursor: pointer; + object-fit: contain; + border-radius: 12px; + transition: opacity 0.3s ease; + background-color: var(--color-background-soft); + border: 0.5px solid var(--color-border); + &:hover { + opacity: 0.8; + } +` diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx index a5f2b0139e..dd6b4a5489 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx @@ -5,22 +5,13 @@ import { type DraggableVirtualListRef, useDraggableReorder } from '@renderer/components/DraggableList' -import { DeleteIcon, EditIcon, PoeLogo } from '@renderer/components/Icons' -import { getProviderLogo } from '@renderer/config/providers' +import { DeleteIcon, EditIcon } from '@renderer/components/Icons' import { useAllProviders, useProviders } from '@renderer/hooks/useProvider' +import { useProviderAvatar } from '@renderer/hooks/useProviderLogo' import { useTimer } from '@renderer/hooks/useTimer' -import ImageStorage from '@renderer/services/ImageStorage' import { isSystemProvider, Provider, ProviderType } from '@renderer/types' -import { - generateColorFromChar, - getFancyProviderName, - getFirstCharacter, - getForegroundColor, - matchKeywordsInModel, - matchKeywordsInProvider, - uuid -} from '@renderer/utils' -import { Avatar, Button, Dropdown, Input, MenuProps, Tag } from 'antd' +import { getFancyProviderName, matchKeywordsInModel, matchKeywordsInProvider, uuid } from '@renderer/utils' +import { Button, Dropdown, Input, MenuProps, Tag } from 'antd' import { GripVertical, PlusIcon, Search, UserPen } from 'lucide-react' import { FC, startTransition, useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -45,34 +36,13 @@ const ProviderList: FC = () => { const { t } = useTranslation() const [searchText, setSearchText] = useState('') const [dragging, setDragging] = useState(false) - const [providerLogos, setProviderLogos] = useState>({}) const listRef = useRef(null) + const { ProviderAvatar, saveLogo, deleteLogo } = useProviderAvatar() const setSelectedProvider = useCallback((provider: Provider) => { startTransition(() => _setSelectedProvider(provider)) }, []) - useEffect(() => { - const loadAllLogos = async () => { - const logos: Record = {} - for (const provider of providers) { - if (provider.id) { - try { - const logoData = await ImageStorage.get(`provider-${provider.id}`) - if (logoData) { - logos[provider.id] = logoData - } - } catch (error) { - logger.error(`Failed to load logo for provider ${provider.id}`, error as Error) - } - } - } - setProviderLogos(logos) - } - - loadAllLogos() - }, [providers]) - useEffect(() => { if (searchParams.get('id')) { const providerId = searchParams.get('id') @@ -162,17 +132,10 @@ const ProviderList: FC = () => { models: [], enabled: true, isSystem: false - } as Provider - - let updatedLogos = { ...providerLogos } + } satisfies Provider if (logo) { try { - await ImageStorage.set(`provider-${provider.id}`, logo) - updatedLogos = { - ...updatedLogos, - [provider.id]: logo - } - setProviderLogos(updatedLogos) + saveLogo(logo, provider.id) } catch (error) { logger.error('Failed to save logo', error as Error) window.message.error('保存Provider Logo失败') @@ -183,132 +146,91 @@ const ProviderList: FC = () => { setSelectedProvider(provider) } - const getDropdownMenus = (provider: Provider): MenuProps['items'] => { - const noteMenu = { - label: t('settings.provider.notes.title'), - key: 'notes', - icon: , - onClick: () => ModelNotesPopup.show({ provider }) - } + const getDropdownMenus = useCallback( + (provider: Provider): MenuProps['items'] => { + const noteMenu = { + label: t('settings.provider.notes.title'), + key: 'notes', + icon: , + onClick: () => ModelNotesPopup.show({ provider }) + } - const editMenu = { - label: t('common.edit'), - key: 'edit', - icon: , - async onClick() { - const { name, type, logoFile, logo } = await AddProviderPopup.show(provider) + const editMenu = { + label: t('common.edit'), + key: 'edit', + icon: , + async onClick() { + const { name, type, logoFile, logo } = await AddProviderPopup.show(provider) - if (name) { - updateProvider({ ...provider, name, type }) - if (provider.id) { - if (logo) { - try { - await ImageStorage.set(`provider-${provider.id}`, logo) - setProviderLogos((prev) => ({ - ...prev, - [provider.id]: logo - })) - } catch (error) { - logger.error('Failed to save logo', error as Error) - window.message.error('更新Provider Logo失败') - } - } else if (logo === undefined && logoFile === undefined) { - try { - await ImageStorage.set(`provider-${provider.id}`, '') - setProviderLogos((prev) => { - const newLogos = { ...prev } - delete newLogos[provider.id] - return newLogos - }) - } catch (error) { - logger.error('Failed to reset logo', error as Error) + if (name) { + updateProvider({ ...provider, name, type }) + if (provider.id) { + if (logo) { + try { + saveLogo(logo, provider.id) + } catch (error) { + logger.error('Failed to save logo', error as Error) + window.message.error('更新Provider Logo失败') + } + } else if (logo === undefined && logoFile === undefined) { + try { + deleteLogo(provider.id) + } catch (error) { + logger.error('Failed to reset logo', error as Error) + } } } } } } - } - const deleteMenu = { - label: t('common.delete'), - key: 'delete', - icon: , - danger: true, - async onClick() { - window.modal.confirm({ - title: t('settings.provider.delete.title'), - content: t('settings.provider.delete.content'), - okButtonProps: { danger: true }, - okText: t('common.delete'), - centered: true, - onOk: async () => { - // 删除provider前先清理其logo - if (provider.id) { - try { - await ImageStorage.remove(`provider-${provider.id}`) - setProviderLogos((prev) => { - const newLogos = { ...prev } - delete newLogos[provider.id] - return newLogos - }) - } catch (error) { - logger.error('Failed to delete logo', error as Error) + const deleteMenu = { + label: t('common.delete'), + key: 'delete', + icon: , + danger: true, + async onClick() { + window.modal.confirm({ + title: t('settings.provider.delete.title'), + content: t('settings.provider.delete.content'), + okButtonProps: { danger: true }, + okText: t('common.delete'), + centered: true, + onOk: async () => { + // 删除provider前先清理其logo + if (provider.id) { + try { + deleteLogo(provider.id) + } catch (error) { + logger.error('Failed to delete logo', error as Error) + } } + + setSelectedProvider(providers.filter((p) => isSystemProvider(p))[0]) + removeProvider(provider) } - - setSelectedProvider(providers.filter((p) => isSystemProvider(p))[0]) - removeProvider(provider) - } - }) + }) + } } - } - const menus = [editMenu, noteMenu, deleteMenu] + const menus = [editMenu, noteMenu, deleteMenu] - if (providers.filter((p) => p.id === provider.id).length > 1) { - return menus - } - - if (isSystemProvider(provider)) { - return [noteMenu] - } else if (provider.isSystem) { - // 这里是处理数据中存在新版本删掉的系统提供商的情况 - // 未来期望能重构一下,不要依赖isSystem字段 - return [noteMenu, deleteMenu] - } else { - return menus - } - } - - const getProviderAvatar = (provider: Provider, size: number = 25) => { - // 特殊处理一下svg格式 - if (isSystemProvider(provider)) { - switch (provider.id) { - case 'poe': - return + if (providers.filter((p) => p.id === provider.id).length > 1) { + return menus } - } - const logoSrc = getProviderLogo(provider.id) - if (logoSrc) { - return - } - - const customLogo = providerLogos[provider.id] - if (customLogo) { - return - } - - // generate color for custom provider - const backgroundColor = generateColorFromChar(provider.name) - const color = provider.name ? getForegroundColor(backgroundColor) : 'white' - - return ( - - {getFirstCharacter(provider.name)} - - ) - } + if (isSystemProvider(provider)) { + return [noteMenu] + } else if (provider.isSystem) { + // 这里是处理数据中存在新版本删掉的系统提供商的情况 + // 未来期望能重构一下,不要依赖isSystem字段 + return [noteMenu, deleteMenu] + } else { + return menus + } + }, + [providers, deleteLogo, removeProvider, saveLogo, setSelectedProvider, t, updateProvider] + ) const filteredProviders = providers.filter((provider) => { const keywords = searchText.toLowerCase().split(/\s+/).filter(Boolean) @@ -336,6 +258,31 @@ const ProviderList: FC = () => { [handleReorder] ) + const providerRender = useCallback( + (provider: Provider) => { + return ( + + setSelectedProvider(provider)}> + + + + + {getFancyProviderName(provider)} + {provider.enabled && ( + + ON + + )} + + + ) + }, + [ProviderAvatar, getDropdownMenus, selectedProvider?.id, setSelectedProvider] + ) + return ( @@ -373,25 +320,7 @@ const ProviderList: FC = () => { paddingRight: 5 }} itemContainerStyle={{ paddingBottom: 5 }}> - {(provider) => ( - - setSelectedProvider(provider)}> - - - - {getProviderAvatar(provider)} - {getFancyProviderName(provider)} - {provider.enabled && ( - - ON - - )} - - - )} + {providerRender}