From 76c025d53b164b9fcb06e91b0e5ec9d37ff1ded2 Mon Sep 17 00:00:00 2001 From: Yuhang <190720896+YuhangHere@users.noreply.github.com> Date: Fri, 22 Aug 2025 09:42:24 +0800 Subject: [PATCH] Feat/add built-in provider avatar options when adding a provider (#9350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add 'builtin avatar' option to avatar dropdown -Introduces a new 'builtin avatar' option to the avatar selection dropdown in AddProviderPopup. -Updates i18n translation files for all supported languages to include the 'builtin' avatar label. Signed-off-by: Yuhang <190720896+YuhangHere@users.noreply.github.com> * Add provider logo picker for builtin avatar selection -Introduces a ProviderLogoPicker component for selecting a builtin provider logo as an avatar in AddProviderPopup. -Updates provider logo handling in ProviderSettings.(If deleting the logoFile caused any issues, I sincerely apologize.) Signed-off-by: Yuhang <190720896+YuhangHere@users.noreply.github.com> * Adjust ProviderLogoPicker layout dimensions and grid Signed-off-by: Yuhang <190720896+YuhangHere@users.noreply.github.com> * Fix ProviderLogoPicker popover trigger behavior Signed-off-by: Yuhang <190720896+YuhangHere@users.noreply.github.com> * Merge branch 'main' into feat/add-builtin-provider-avatars * Update index.tsx --------- Signed-off-by: Yuhang <190720896+YuhangHere@users.noreply.github.com> --- .../components/ProviderLogoPicker/index.tsx | 113 ++++++++++++++++++ src/renderer/src/config/providers.ts | 2 +- src/renderer/src/i18n/locales/en-us.json | 3 +- src/renderer/src/i18n/locales/ja-jp.json | 1 + src/renderer/src/i18n/locales/ru-ru.json | 1 + src/renderer/src/i18n/locales/zh-cn.json | 1 + src/renderer/src/i18n/locales/zh-tw.json | 1 + .../ProviderSettings/AddProviderPopup.tsx | 69 +++++++++-- .../pages/settings/ProviderSettings/index.tsx | 2 +- 9 files changed, 181 insertions(+), 12 deletions(-) create mode 100644 src/renderer/src/components/ProviderLogoPicker/index.tsx diff --git a/src/renderer/src/components/ProviderLogoPicker/index.tsx b/src/renderer/src/components/ProviderLogoPicker/index.tsx new file mode 100644 index 0000000000..b18b2b47d7 --- /dev/null +++ b/src/renderer/src/components/ProviderLogoPicker/index.tsx @@ -0,0 +1,113 @@ +import { SearchOutlined } from '@ant-design/icons' +import { PROVIDER_LOGO_MAP } from '@renderer/config/providers' +import { getProviderLabel } from '@renderer/i18n/label' +import { Input, Tooltip } from 'antd' +import { FC, useMemo, useState } from 'react' +import styled from 'styled-components' + +interface Props { + onProviderClick: (providerId: string) => void +} + +// 用于选择内置头像的提供商Logo选择器组件 +const ProviderLogoPicker: FC = ({ onProviderClick }) => { + const [searchText, setSearchText] = useState('') + + const filteredProviders = useMemo(() => { + const providers = Object.entries(PROVIDER_LOGO_MAP).map(([id, logo]) => ({ + id, + logo, + name: getProviderLabel(id) + })) + + if (!searchText) return providers + + const searchLower = searchText.toLowerCase() + return providers.filter((p) => p.name.toLowerCase().includes(searchLower)) + }, [searchText]) + + const handleProviderClick = (event: React.MouseEvent, providerId: string) => { + event.stopPropagation() + onProviderClick(providerId) + } + + return ( + + + } + value={searchText} + onChange={(e) => setSearchText(e.target.value)} + size="small" + allowClear + style={{ + borderRadius: 'var(--list-item-border-radius)', + background: 'var(--color-background-soft)' + }} + /> + + + {filteredProviders.map(({ id, logo, name }) => ( + + handleProviderClick(e, id)}> + {name} + + + ))} + + + ) +} + +const Container = styled.div` + width: 350px; + max-height: 300px; + display: flex; + flex-direction: column; + padding: 12px; + background: var(--color-background); + border-radius: 8px; +` + +const SearchContainer = styled.div` + margin-bottom: 12px; +` + +const LogoGrid = styled.div` + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 8px; + overflow-y: auto; + flex: 1; + padding: 4px; +` + +const LogoItem = styled.div` + width: 52px; + height: 52px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 8px; + transition: all 0.2s ease; + background: var(--color-background-soft); + border: 0.5px solid var(--color-border); + + &:hover { + background: var(--color-background-mute); + transform: scale(1.05); + border-color: var(--color-primary); + } + + img { + width: 32px; + height: 32px; + object-fit: contain; + user-select: none; + -webkit-user-drag: none; + } +` + +export default ProviderLogoPicker diff --git a/src/renderer/src/config/providers.ts b/src/renderer/src/config/providers.ts index 2e9bb483f8..4b429dd1c0 100644 --- a/src/renderer/src/config/providers.ts +++ b/src/renderer/src/config/providers.ts @@ -594,7 +594,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record = export const SYSTEM_PROVIDERS: SystemProvider[] = Object.values(SYSTEM_PROVIDERS_CONFIG) -const PROVIDER_LOGO_MAP: AtLeast = { +export const PROVIDER_LOGO_MAP: AtLeast = { ph8: Ph8ProviderLogo, '302ai': Ai302ProviderLogo, openai: OpenAiProviderLogo, diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 8f05b3d2d9..a7a52d1ac3 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -2679,7 +2679,8 @@ "title": "Auto Update" }, "avatar": { - "reset": "Reset Avatar" + "builtin": "Builtin avatar", + "reset": "Reset avatar" }, "backup": { "button": "Backup", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 9f5f782ea7..953213fdbf 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -2679,6 +2679,7 @@ "title": "自動更新" }, "avatar": { + "builtin": "内蔵アバター", "reset": "アバターをリセット" }, "backup": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 4896b85fee..a251854c34 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -2679,6 +2679,7 @@ "title": "Автоматическое обновление" }, "avatar": { + "builtin": "Встроенный аватар", "reset": "Сброс аватара" }, "backup": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 87f50debf0..8b55525a9c 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -2679,6 +2679,7 @@ "title": "自动更新" }, "avatar": { + "builtin": "内置头像", "reset": "重置头像" }, "backup": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index f92c27f973..2e2babab9c 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -2679,6 +2679,7 @@ "title": "自動更新" }, "avatar": { + "builtin": "內置頭像", "reset": "重設頭像" }, "backup": { diff --git a/src/renderer/src/pages/settings/ProviderSettings/AddProviderPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/AddProviderPopup.tsx index 4a845521d9..16060ba7c1 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/AddProviderPopup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/AddProviderPopup.tsx @@ -1,10 +1,12 @@ 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 ImageStorage from '@renderer/services/ImageStorage' import { Provider, ProviderType } from '@renderer/types' import { compressImage, generateColorFromChar } from '@renderer/utils' -import { Divider, Dropdown, Form, Input, Modal, Select, Upload } from 'antd' +import { Divider, Dropdown, Form, Input, Modal, Popover, Select, Upload } from 'antd' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -21,6 +23,7 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { 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() @@ -63,6 +66,25 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { const buttonDisabled = name.length === 0 + // 处理内置头像的点击事件 + const handleProviderLogoClick = async (providerId: string) => { + try { + 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) + } + + setLogoPickerOpen(false) + } catch (error: any) { + window.message.error(error.message) + } + } + const handleReset = async () => { try { setLogo(null) @@ -131,6 +153,20 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { ) }, + { + key: 'builtin', + label: ( +
{ + e.stopPropagation() + setDropdownOpen(false) + setLogoPickerOpen(true) + }}> + {t('settings.general.avatar.builtin')} +
+ ) + }, { key: 'reset', label: ( @@ -170,15 +206,30 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { placement="bottom" onOpenChange={(visible) => { setDropdownOpen(visible) + if (visible) { + setLogoPickerOpen(false) + } }}> - {logo ? ( - - ) : ( - - {getInitials()} - - )} + } + trigger="click" + open={logoPickerOpen} + onOpenChange={(visible) => { + setLogoPickerOpen(visible) + if (visible) { + setDropdownOpen(false) + } + }} + placement="bottom"> + {logo ? ( + + ) : ( + + {getInitials()} + + )} + diff --git a/src/renderer/src/pages/settings/ProviderSettings/index.tsx b/src/renderer/src/pages/settings/ProviderSettings/index.tsx index 5fdfaecd64..86207ccc57 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/index.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/index.tsx @@ -327,7 +327,7 @@ const ProvidersList: FC = () => { if (name) { updateProvider({ ...provider, name, type }) if (provider.id) { - if (logoFile && logo) { + if (logo) { try { await ImageStorage.set(`provider-${provider.id}`, logo) setProviderLogos((prev) => ({