Revert "fix: complete PoeLogo rendering support across provider UI components (#9756)"

This reverts commit df7fd26907.
This commit is contained in:
kangfenmao 2025-09-02 02:48:43 +08:00
parent 80f49aecd7
commit 6fb878d3b6
16 changed files with 338 additions and 325 deletions

View File

@ -1,9 +1,6 @@
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'
@ -15,21 +12,19 @@ interface Props {
// 用于选择内置头像的提供商Logo选择器组件
const ProviderLogoPicker: FC<Props> = ({ onProviderClick }) => {
const [searchText, setSearchText] = useState('')
const providers = useAppSelector((state) => state.llm.providers)
const { ProviderAvatar } = useProviderAvatar()
const filteredProviders = useMemo(() => {
const _providers = providers.filter(isSystemProvider).map((p) => ({
id: p.id,
name: p.name,
label: getProviderLabel(p.id),
logo: PROVIDER_LOGO_MAP[p.id]
const providers = Object.entries(PROVIDER_LOGO_MAP).map(([id, logo]) => ({
id,
logo,
name: getProviderLabel(id)
}))
if (!searchText) return _providers
if (!searchText) return providers
const searchLower = searchText.toLowerCase()
return _providers.filter((p) => `${p.name} ${p.id} ${p.label}`.toLowerCase().includes(searchLower))
}, [providers, searchText])
return providers.filter((p) => p.name.toLowerCase().includes(searchLower))
}, [searchText])
const handleProviderClick = (event: React.MouseEvent, providerId: string) => {
event.stopPropagation()
@ -53,10 +48,10 @@ const ProviderLogoPicker: FC<Props> = ({ onProviderClick }) => {
/>
</SearchContainer>
<LogoGrid>
{filteredProviders.map(({ id, label }) => (
<Tooltip key={id} title={label} placement="top" mouseLeaveDelay={0}>
{filteredProviders.map(({ id, logo, name }) => (
<Tooltip key={id} title={name} placement="top" mouseLeaveDelay={0}>
<LogoItem onClick={(e) => handleProviderClick(e, id)}>
<ProviderAvatar pid={id} size={32} />
<img src={logo} alt={name} draggable={false} />
</LogoItem>
</Tooltip>
))}

View File

@ -664,6 +664,10 @@ export const PROVIDER_LOGO_MAP: AtLeast<SystemProviderId, string> = {
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[]

View File

@ -1,112 +0,0 @@
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 <ProviderLogo draggable="false" shape="square" src={src} size={size} style={style} {...rest} />
}
let provider: Provider | undefined
if (pid) {
// 特殊处理一下svg格式
if (isSystemProviderId(pid)) {
const logoSrc = PROVIDER_LOGO_MAP[pid]
switch (pid) {
case 'poe':
return <PoeLogo fontSize={typeof size === 'number' ? size : 18} style={style} />
default:
return <ProviderLogo draggable="false" shape="square" src={logoSrc} size={size} style={style} {...rest} />
}
}
const customLogo = logos[pid]
if (customLogo) {
return <ProviderLogo draggable="false" shape="square" src={customLogo} size={size} style={style} {...rest} />
}
if (!name) {
// generate a avatar for custom provider
provider = getProviderById(pid)
if (!provider) {
return null
}
}
}
return <GeneratedAvatar name={name ?? provider?.name ?? 'P'} size={size} style={style} />
},
[logos]
)
function GeneratedAvatar({ name, size, style }: { name: string; size?: AvatarSize; style?: CSSProperties }) {
const backgroundColor = generateColorFromChar(name)
const color = name ? getForegroundColor(backgroundColor) : 'white'
return (
<ProviderLogo size={size} shape="square" style={{ backgroundColor, color, ...style }}>
{getFirstCharacter(!isEmpty(name) ? name : 'P')}
</ProviderLogo>
)
}
return { ProviderAvatar, GeneratedAvatar, saveLogo, deleteLogo, logos }
}
const ProviderLogo = styled(Avatar)`
border: 0.5px solid var(--color-border);
`

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef } from 'react'
import { useEffect, useRef } from 'react'
/**
* Hook setTimeout setInterval key
@ -65,12 +65,12 @@ export const useTimer = () => {
* cleanup();
* ```
*/
const setTimeoutTimer = useCallback((key: string, ...args: Parameters<typeof setTimeout>) => {
const setTimeoutTimer = (key: string, ...args: Parameters<typeof setTimeout>) => {
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 = useCallback((key: string, ...args: Parameters<typeof setInterval>) => {
const setIntervalTimer = (key: string, ...args: Parameters<typeof setInterval>) => {
clearInterval(intervalMapRef.current.get(key))
const timer = setInterval(...args)
intervalMapRef.current.set(key, timer)
return () => clearIntervalTimer(key)
}, [])
}
/**
* key setTimeout

View File

@ -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 { Button, Input, InputNumber, Radio, Segmented, Select, Slider, Switch, Tooltip, Upload } from 'antd'
import { Avatar, 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,7 +54,6 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
aihubmix_image_edit,
aihubmix_image_upscale
} = usePaintings()
const { ProviderAvatar } = useProviderAvatar()
const paintings = useMemo(() => {
return {
@ -848,7 +847,12 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
<SettingHelpLink target="_blank" href={aihubmixProvider.apiHost}>
{t('paintings.learn_more')}
<ProviderAvatar pid={aihubmixProvider.id} size={16} style={{ marginLeft: 5 }} />
<ProviderLogo
shape="square"
src={getProviderLogo(aihubmixProvider.id)}
size={16}
style={{ marginLeft: 5 }}
/>
</SettingHelpLink>
</ProviderTitleContainer>
@ -856,7 +860,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
{providerOptions.map((provider) => (
<Select.Option value={provider.value} key={provider.value}>
<SelectOptionContainer>
<ProviderAvatar pid={provider.value ?? ''} size={16} />
<ProviderLogo shape="square" src={getProviderLogo(provider.value || '')} size={16} />
{provider.label}
</SelectOptionContainer>
</Select.Option>
@ -1025,6 +1029,10 @@ const StyledInputNumber = styled(InputNumber)`
width: 70px;
`
const ProviderLogo = styled(Avatar)`
border: 0.5px solid var(--color-border);
`
// 添加新的样式组件
const ModeSegmentedContainer = styled.div`
display: flex;

View File

@ -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 { Button, Input, InputNumber, Segmented, Select, Switch, Tooltip } from 'antd'
import { Avatar, 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,7 +46,6 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
const [painting, setPainting] = useState<DmxapiPainting>(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) {
@ -815,15 +814,19 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
<SettingHelpLink target="_blank" href={TOP_UP_URL}>
{t('paintings.top_up')}
</SettingHelpLink>
<ProviderAvatar pid={dmxapiProvider.id} size={16} style={{ marginLeft: 5 }} />
<ProviderLogo
shape="square"
src={getProviderLogo(dmxapiProvider.id)}
size={16}
style={{ marginLeft: 5 }}
/>
</div>
</ProviderTitleContainer>
<Select value={providerOptions[3].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
{providerOptions.map((provider) => (
<Select.Option value={provider.value} key={provider.value}>
<SelectOptionContainer>
<ProviderAvatar pid={provider.value || ''} size={16} />
<ProviderLogo shape="square" src={getProviderLogo(provider.value || '')} size={16} />
{provider.label}
</SelectOptionContainer>
</Select.Option>
@ -1035,6 +1038,9 @@ const ProviderTitleContainer = styled.div`
margin-bottom: 5px;
`
const ProviderLogo = styled(Avatar)`
border: 0.5px solid var(--color-border);
`
const SelectOptionContainer = styled.div`
display: flex;
align-items: center;

View File

@ -6,11 +6,11 @@ import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navb
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 {
@ -29,7 +29,7 @@ import { setGenerating } from '@renderer/store/runtime'
import type { PaintingAction, PaintingsState } from '@renderer/types'
import { FileMetadata } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { Button, Empty, InputNumber, Segmented, Select, Upload } from 'antd'
import { Avatar, Button, Empty, InputNumber, Segmented, Select, Upload } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import React, { FC } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@ -47,7 +47,6 @@ const logger = loggerService.withContext('NewApiPage')
const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
const [mode, setMode] = useState<keyof PaintingsState>('openai_image_generate')
const { addPainting, removePainting, updatePainting, openai_image_generate, openai_image_edit } = usePaintings()
const { ProviderAvatar } = useProviderAvatar()
const newApiPaintings = useMemo(() => {
return {
@ -510,7 +509,12 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
<SettingHelpLink target="_blank" href={'https://docs.newapi.pro/apps/cherry-studio/'}>
{t('paintings.learn_more')}
<ProviderAvatar pid={newApiProvider.id} size={16} style={{ marginLeft: 5 }} />
<ProviderLogo
shape="square"
src={getProviderLogo(newApiProvider.id)}
size={16}
style={{ marginLeft: 5 }}
/>
</SettingHelpLink>
</ProviderTitleContainer>
@ -521,7 +525,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => {
{providerOptions.map((provider) => (
<Select.Option value={provider.value} key={provider.value}>
<SelectOptionContainer>
<ProviderAvatar pid={provider.value} size={16} />
<ProviderLogo shape="square" src={getProviderLogo(provider.value || '')} size={16} />
{provider.label}
</SelectOptionContainer>
</Select.Option>
@ -790,6 +794,10 @@ const ToolbarMenu = styled.div`
gap: 6px;
`
const ProviderLogo = styled(Avatar)`
border: 0.5px solid var(--color-border);
`
// 添加新的样式组件
const ModeSegmentedContainer = styled.div`
display: flex;

View File

@ -4,10 +4,10 @@ import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navb
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 { 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'
@ -17,7 +17,7 @@ import { useAppDispatch } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import type { TokenFluxPainting } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { Button, Select, Tooltip } from 'antd'
import { Avatar, Button, Select, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { Info } from 'lucide-react'
import type { FC } from 'react'
@ -50,7 +50,6 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
const { t, i18n } = useTranslation()
const providers = useAllProviders()
const { addPainting, removePainting, updatePainting, tokenflux_paintings } = usePaintings()
const { ProviderAvatar } = useProviderAvatar()
const tokenFluxPaintings = tokenflux_paintings
const [painting, setPainting] = useState<TokenFluxPainting>(
tokenFluxPaintings[0] || { ...DEFAULT_TOKENFLUX_PAINTING, id: uuid() }
@ -384,7 +383,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
<SettingTitle style={{ marginBottom: 8 }}>{t('common.provider')}</SettingTitle>
<SettingHelpLink target="_blank" href="https://tokenflux.ai">
{t('paintings.learn_more')}
<ProviderAvatar pid={'tokenflux'} size={16} style={{ marginLeft: 5 }} />
<ProviderLogo shape="square" src={getProviderLogo('tokenflux')} size={16} style={{ marginLeft: 5 }} />
</SettingHelpLink>
</ProviderTitleContainer>
@ -395,7 +394,7 @@ const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
{providerOptions.map((provider) => (
<Select.Option value={provider.value} key={provider.value}>
<SelectOptionContainer>
<ProviderAvatar pid={provider.value} size={16} />
<ProviderLogo shape="square" src={getProviderLogo(provider.value || '')} size={16} />
{provider.label}
</SelectOptionContainer>
</Select.Option>
@ -766,6 +765,10 @@ const InfoIcon = styled(Info)`
}
`
const ProviderLogo = styled(Avatar)`
border: 0.5px solid var(--color-border);
`
const ProviderTitleContainer = styled.div`
display: flex;
justify-content: space-between;

View File

@ -4,16 +4,16 @@ 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'
import { useAppDispatch } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import { getErrorMessage, uuid } from '@renderer/utils'
import { Button, InputNumber, Radio, Select } from 'antd'
import { Avatar, Button, InputNumber, Radio, Select } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -39,7 +39,6 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
const [painting, setPainting] = useState<any>(zhipu_paintings?.[0] || DEFAULT_PAINTING)
const { t } = useTranslation()
const providers = useAllProviders()
const { ProviderAvatar } = useProviderAvatar()
// 确保painting使用智谱的cogview系列模型
useEffect(() => {
@ -369,14 +368,19 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => {
<SettingHelpLink target="_blank" href={COURSE_URL}>
{t('paintings.paint_course')}
</SettingHelpLink>
<ProviderAvatar pid={zhipuProvider.id} size={16} style={{ marginLeft: 5 }} />
<ProviderLogo
shape="square"
src={getProviderLogo(zhipuProvider.id)}
size={16}
style={{ marginLeft: 5 }}
/>
</div>
</ProviderTitleContainer>
<Select value={providerOptions[0].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
{providerOptions.map((provider) => (
<Select.Option value={provider.value} key={provider.value}>
<SelectOptionContainer>
<ProviderAvatar pid={provider.value} size={16} />
<ProviderLogo shape="square" src={getProviderLogo(provider.value || '')} size={16} />
{provider.label}
</SelectOptionContainer>
</Select.Option>
@ -577,4 +581,8 @@ const SelectOptionContainer = styled.div`
gap: 8px;
`
const ProviderLogo = styled(Avatar)`
border-radius: 4px;
`
export default ZhipuPage

View File

@ -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 } from '@renderer/utils'
import { compressImage, generateColorFromChar, getForegroundColor } 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,20 +23,27 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
const [open, setOpen] = useState(true)
const [name, setName] = useState(provider?.name || '')
const [type, setType] = useState<ProviderType>(provider?.type || 'openai')
const [logo, setLogo] = useState<string | null>(null)
const [logoPickerOpen, setLogoPickerOpen] = useState(false)
const [dropdownOpen, setDropdownOpen] = useState(false)
const { t } = useTranslation()
const uploadRef = useRef<HTMLDivElement>(null)
const [logo, setLogo] = useState<string>()
const { ProviderAvatar, logos, saveLogo } = useProviderAvatar()
useEffect(() => {
if (provider) {
const logo = logos[provider.id]
setLogo(logo)
if (provider?.id) {
const loadLogo = async () => {
try {
const logoData = await ImageStorage.get(`provider-${provider.id}`)
if (logoData) {
setLogo(logoData)
}
}, [provider, logos, setLogo])
} catch (error) {
logger.error('Failed to load logo', error as Error)
}
}
loadLogo()
}
}, [provider])
const onOk = async () => {
setOpen(false)
@ -67,9 +74,12 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
const logoUrl = PROVIDER_LOGO_MAP[providerId]
if (provider?.id) {
saveLogo(logoUrl, provider.id)
}
await ImageStorage.set(`provider-${provider.id}`, logoUrl)
const savedLogo = await ImageStorage.get(`provider-${provider.id}`)
setLogo(savedLogo)
} else {
setLogo(logoUrl)
}
setLogoPickerOpen(false)
} catch (error: any) {
@ -79,9 +89,10 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
const handleReset = async () => {
try {
setLogo(null)
if (provider?.id) {
saveLogo('', provider.id)
ImageStorage.set(`provider-${provider.id}`, '')
await ImageStorage.set(`provider-${provider.id}`, '')
}
setDropdownOpen(false)
@ -90,6 +101,10 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
}
}
const getInitials = () => {
return name.charAt(0) || 'P'
}
const items = [
{
key: 'upload',
@ -118,7 +133,7 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
await ImageStorage.set(`provider-${provider.id}`, logoData)
}
const savedLogo = await ImageStorage.get(`provider-${provider.id}`)
saveLogo(savedLogo, provider.id)
setLogo(savedLogo)
} else {
// 临时保存在内存中,等创建 provider 后会在调用方保存
const tempUrl = await new Promise<string>((resolve) => {
@ -156,6 +171,10 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
}
] satisfies ItemType[]
// for logo
const backgroundColor = generateColorFromChar(name)
const color = name ? getForegroundColor(backgroundColor) : 'white'
return (
<Modal
open={open}
@ -195,9 +214,13 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
}
}}
placement="bottom">
<ProviderLogo>
<ProviderAvatar pid={provider?.id} name={name} src={logo} size={60} style={{ fontSize: 32 }} />
</ProviderLogo>
{logo ? (
<ProviderLogo src={logo} />
) : (
<ProviderInitialsLogo style={name ? { backgroundColor, color } : undefined}>
{getInitials()}
</ProviderInitialsLogo>
)}
</Popover>
</Dropdown>
</VStack>
@ -235,6 +258,39 @@ const PopupContainer: React.FC<Props> = ({ 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;
@ -265,15 +321,3 @@ 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;
}
`

View File

@ -5,13 +5,22 @@ import {
type DraggableVirtualListRef,
useDraggableReorder
} from '@renderer/components/DraggableList'
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
import { DeleteIcon, EditIcon, PoeLogo } from '@renderer/components/Icons'
import { getProviderLogo } from '@renderer/config/providers'
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 { getFancyProviderName, matchKeywordsInModel, matchKeywordsInProvider, uuid } from '@renderer/utils'
import { Button, Dropdown, Input, MenuProps, Tag } from 'antd'
import {
generateColorFromChar,
getFancyProviderName,
getFirstCharacter,
getForegroundColor,
matchKeywordsInModel,
matchKeywordsInProvider,
uuid
} from '@renderer/utils'
import { Avatar, 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'
@ -36,13 +45,34 @@ const ProviderList: FC = () => {
const { t } = useTranslation()
const [searchText, setSearchText] = useState<string>('')
const [dragging, setDragging] = useState(false)
const [providerLogos, setProviderLogos] = useState<Record<string, string>>({})
const listRef = useRef<DraggableVirtualListRef>(null)
const { ProviderAvatar, saveLogo, deleteLogo } = useProviderAvatar()
const setSelectedProvider = useCallback((provider: Provider) => {
startTransition(() => _setSelectedProvider(provider))
}, [])
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 (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')
@ -132,10 +162,17 @@ const ProviderList: FC = () => {
models: [],
enabled: true,
isSystem: false
} satisfies Provider
} as Provider
let updatedLogos = { ...providerLogos }
if (logo) {
try {
saveLogo(logo, provider.id)
await ImageStorage.set(`provider-${provider.id}`, logo)
updatedLogos = {
...updatedLogos,
[provider.id]: logo
}
setProviderLogos(updatedLogos)
} catch (error) {
logger.error('Failed to save logo', error as Error)
window.message.error('保存Provider Logo失败')
@ -146,8 +183,7 @@ const ProviderList: FC = () => {
setSelectedProvider(provider)
}
const getDropdownMenus = useCallback(
(provider: Provider): MenuProps['items'] => {
const getDropdownMenus = (provider: Provider): MenuProps['items'] => {
const noteMenu = {
label: t('settings.provider.notes.title'),
key: 'notes',
@ -167,14 +203,23 @@ const ProviderList: FC = () => {
if (provider.id) {
if (logo) {
try {
saveLogo(logo, provider.id)
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 {
deleteLogo(provider.id)
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)
}
@ -200,7 +245,12 @@ const ProviderList: FC = () => {
// 删除provider前先清理其logo
if (provider.id) {
try {
deleteLogo(provider.id)
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)
}
@ -228,9 +278,37 @@ const ProviderList: FC = () => {
} else {
return menus
}
},
[providers, deleteLogo, removeProvider, saveLogo, setSelectedProvider, t, updateProvider]
}
const getProviderAvatar = (provider: Provider, size: number = 25) => {
// 特殊处理一下svg格式
if (isSystemProvider(provider)) {
switch (provider.id) {
case 'poe':
return <PoeLogo fontSize={size} />
}
}
const logoSrc = getProviderLogo(provider.id)
if (logoSrc) {
return <ProviderLogo draggable="false" shape="circle" src={logoSrc} size={size} />
}
const customLogo = providerLogos[provider.id]
if (customLogo) {
return <ProviderLogo draggable="false" shape="square" src={customLogo} size={size} />
}
// generate color for custom provider
const backgroundColor = generateColorFromChar(provider.name)
const color = provider.name ? getForegroundColor(backgroundColor) : 'white'
return (
<ProviderLogo size={size} shape="square" style={{ backgroundColor, color, minWidth: size }}>
{getFirstCharacter(provider.name)}
</ProviderLogo>
)
}
const filteredProviders = providers.filter((provider) => {
const keywords = searchText.toLowerCase().split(/\s+/).filter(Boolean)
@ -258,31 +336,6 @@ const ProviderList: FC = () => {
[handleReorder]
)
const providerRender = useCallback(
(provider: Provider) => {
return (
<Dropdown menu={{ items: getDropdownMenus(provider) }} trigger={['contextMenu']}>
<ProviderListItem
key={provider.id}
className={provider.id === selectedProvider?.id ? 'active' : ''}
onClick={() => setSelectedProvider(provider)}>
<DragHandle>
<GripVertical size={12} />
</DragHandle>
<ProviderAvatar pid={provider.id} name={provider.name} size={25} />
<ProviderItemName className="text-nowrap">{getFancyProviderName(provider)}</ProviderItemName>
{provider.enabled && (
<Tag color="green" style={{ marginLeft: 'auto', marginRight: 0, borderRadius: 16 }}>
ON
</Tag>
)}
</ProviderListItem>
</Dropdown>
)
},
[ProviderAvatar, getDropdownMenus, selectedProvider?.id, setSelectedProvider]
)
return (
<Container className="selectable">
<ProviderListContainer>
@ -320,7 +373,25 @@ const ProviderList: FC = () => {
paddingRight: 5
}}
itemContainerStyle={{ paddingBottom: 5 }}>
{providerRender}
{(provider) => (
<Dropdown menu={{ items: getDropdownMenus(provider) }} trigger={['contextMenu']}>
<ProviderListItem
key={provider.id}
className={provider.id === selectedProvider?.id ? 'active' : ''}
onClick={() => setSelectedProvider(provider)}>
<DragHandle>
<GripVertical size={12} />
</DragHandle>
{getProviderAvatar(provider)}
<ProviderItemName className="text-nowrap">{getFancyProviderName(provider)}</ProviderItemName>
{provider.enabled && (
<Tag color="green" style={{ marginLeft: 'auto', marginRight: 0, borderRadius: 16 }}>
ON
</Tag>
)}
</ProviderListItem>
</Dropdown>
)}
</DraggableVirtualList>
<AddButtonWrapper>
<Button
@ -395,6 +466,10 @@ const DragHandle = styled.div`
}
`
const ProviderLogo = styled(Avatar)`
border: 0.5px solid var(--color-border);
`
const ProviderItemName = styled.div`
margin-left: 10px;
font-weight: 500;

View File

@ -16,7 +16,7 @@ export default class ImageStorage {
db.settings.update(id, { value })
return
}
await db.settings.put({ id, value })
await db.settings.add({ id, value })
} else {
// file image
const base64Image = await convertToBase64(value)
@ -25,7 +25,7 @@ export default class ImageStorage {
db.settings.update(id, { value: base64Image })
return
}
await db.settings.put({ id, value: base64Image })
await db.settings.add({ id, value: base64Image })
}
}
} catch (error) {

View File

@ -233,8 +233,7 @@ vi.mock('@renderer/store/llm.ts', () => {
secretAccessKey: '',
region: ''
}
},
logos: {}
}
} satisfies LlmState
const mockReducer = (state = mockInitialState) => {

View File

@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 144,
version: 143,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
migrate
},

View File

@ -39,7 +39,6 @@ export interface LlmState {
translateModel: Model
quickAssistantId: string
settings: LlmSettings
logos: Record<string, string>
}
export const initialState: LlmState = {
@ -72,8 +71,7 @@ export const initialState: LlmState = {
secretAccessKey: '',
region: ''
}
},
logos: {}
}
}
// 由于 isLocalAi 目前总是为false该函数暂未被使用
@ -221,17 +219,6 @@ const llmSlice = createSlice({
provider.models[modelIndex] = action.payload.model
}
}
},
setLogos: (state, action: PayloadAction<Record<string, string>>) => {
state.logos = action.payload
},
setLogo: (state, action: PayloadAction<{ id: string; logo: string }>) => {
const { id, logo } = action.payload
state.logos[id] = logo
},
removeLogo: (state, action: PayloadAction<string>) => {
const id = action.payload
delete state.logos[id]
}
}
})
@ -257,10 +244,7 @@ export const {
setAwsBedrockAccessKeyId,
setAwsBedrockSecretAccessKey,
setAwsBedrockRegion,
updateModel,
setLogos,
setLogo,
removeLogo
updateModel
} = llmSlice.actions
export default llmSlice.reducer

View File

@ -2322,15 +2322,6 @@ const migrateConfig = {
} catch (error) {
return state
}
},
// after 1.5.8
'144': (state: RootState) => {
try {
state.llm.logos = {}
return state
} catch (error) {
return state
}
}
}