feat(CustomHeaderPopup): add custom headers management for providers

- Introduced a new CustomHeaderPopup component for managing extra headers for providers.
- Integrated the popup into the ProviderSetting component, allowing users to edit headers via a modal.
- Refactored ApiKeyListPopup to use a styled container for improved layout.
This commit is contained in:
kangfenmao 2025-07-09 18:15:32 +08:00
parent e273ddcfb0
commit f7fa665f3a
3 changed files with 123 additions and 45 deletions

View File

@ -10,6 +10,7 @@ import { Button, Card, Flex, List, Popconfirm, Space, Tooltip, Typography } from
import { Trash } from 'lucide-react'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { isLlmProvider, useApiKeys } from './hook'
import ApiKeyItem from './item'
@ -87,7 +88,7 @@ export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, prov
: keys
return (
<>
<ListContainer>
{/* Keys 列表 */}
<Card
size="small"
@ -122,7 +123,7 @@ export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, prov
)}
</Card>
<Flex align="center" justify="space-between" style={{ marginTop: '0.5rem' }}>
<Flex dir="row" align="center" justify="space-between" style={{ marginTop: 15 }}>
{/* 帮助文本 */}
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
@ -166,7 +167,7 @@ export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, prov
</Button>
</Space>
</Flex>
</>
</ListContainer>
)
}
@ -222,3 +223,8 @@ export const DocPreprocessApiKeyList: FC<SpecificApiKeyListProps> = ({
/>
)
}
const ListContainer = styled.div`
padding-top: 15px;
padding-bottom: 15px;
`

View File

@ -0,0 +1,102 @@
import CodeEditor from '@renderer/components/CodeEditor'
import { TopView } from '@renderer/components/TopView'
import { useProvider } from '@renderer/hooks/useProvider'
import { Provider } from '@renderer/types'
import { Modal, Space } from 'antd'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SettingHelpText } from '..'
interface ShowParams {
provider: Provider
}
interface Props extends ShowParams {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const { updateProvider } = useProvider(provider.id)
const [headerText, setHeaderText] = useState<string>(JSON.stringify(provider.extra_headers || {}, null, 2))
const onUpdateHeaders = useCallback(() => {
try {
const headers = headerText.trim() ? JSON.parse(headerText) : {}
updateProvider({ ...provider, extra_headers: headers })
window.message.success({ content: t('message.save.success.title') })
} catch (error) {
window.message.error({ content: t('settings.provider.copilot.invalid_json') })
}
}, [headerText, provider, updateProvider, t])
const onOk = () => {
onUpdateHeaders()
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
CustomHeaderPopup.hide = onCancel
return (
<Modal
title={t('settings.provider.copilot.custom_headers')}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
transitionName="animation-move-down"
centered>
<Space.Compact direction="vertical" style={{ width: '100%', marginTop: 5 }}>
<SettingHelpText>{t('settings.provider.copilot.headers_description')}</SettingHelpText>
<CodeEditor
value={headerText}
language="json"
onChange={(value) => setHeaderText(value)}
placeholder={`{\n "Header-Name": "Header-Value"\n}`}
options={{
lint: true,
collapsible: false,
wrappable: true,
lineNumbers: true,
foldGutter: true,
highlightActiveLine: true,
keymap: true
}}
/>
</Space.Compact>
</Modal>
)
}
const TopViewKey = 'CustomHeaderPopup'
export default class CustomHeaderPopup {
static topviewId = 0
static hide() {
TopView.hide(TopViewKey)
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
})
}
}

View File

@ -1,7 +1,6 @@
import { CheckOutlined, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons'
import { isOpenAIProvider } from '@renderer/aiCore/clients/ApiClientFactory'
import OpenAIAlert from '@renderer/components/Alert/OpenAIAlert'
import CodeEditor from '@renderer/components/CodeEditor'
import { StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons/SVGIcon'
import { HStack } from '@renderer/components/Layout'
import { ApiKeyConnectivity, ApiKeyListPopup } from '@renderer/components/Popups/ApiKeyListPopup'
@ -19,7 +18,7 @@ import { lightbulbVariants } from '@renderer/utils/motionVariants'
import { Button, Divider, Flex, Input, Space, Switch, Tooltip } from 'antd'
import Link from 'antd/es/typography/Link'
import { debounce, isEmpty } from 'lodash'
import { List, Settings2, SquareArrowOutUpRight } from 'lucide-react'
import { Settings2, SquareArrowOutUpRight } from 'lucide-react'
import { motion } from 'motion/react'
import { FC, useCallback, useDeferredValue, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -33,6 +32,7 @@ import {
SettingSubtitle,
SettingTitle
} from '..'
import CustomHeaderPopup from './CustomHeaderPopup'
import DMXAPISettings from './DMXAPISettings'
import GithubCopilotSettings from './GithubCopilotSettings'
import GPUStackSettings from './GPUStackSettings'
@ -80,8 +80,6 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
checking: false
})
const [headerText, setHeaderText] = useState<string>(JSON.stringify(provider.extra_headers || {}, null, 2))
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedUpdateApiKey = useCallback(
debounce((value) => {
@ -311,16 +309,6 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
setApiHost(provider.apiHost)
}, [provider.apiHost, provider.id])
const onUpdateHeaders = useCallback(() => {
try {
const headers = headerText.trim() ? JSON.parse(headerText) : {}
updateProvider({ ...provider, extra_headers: headers })
window.message.success({ content: t('message.save.success.title') })
} catch (error) {
window.message.error({ content: t('settings.provider.copilot.invalid_json') })
}
}, [headerText, provider, updateProvider, t])
return (
<SettingContainer theme={theme} style={{ background: 'var(--color-background)' }}>
<SettingTitle>
@ -367,7 +355,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
{t('settings.provider.api_key')}
{provider.id !== 'copilot' && (
<Tooltip title={t('settings.provider.api.key.list.open')} mouseEnterDelay={0.5}>
<Button type="text" size="small" onClick={openApiKeyList} icon={<List size={14} />} />
<Button type="text" size="small" onClick={openApiKeyList} icon={<Settings2 size={14} />} />
</Tooltip>
)}
</SettingSubtitle>
@ -410,7 +398,15 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
)}
{!isDmxapi && (
<>
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
<SettingSubtitle style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{t('settings.provider.api_host')}
<Button
type="text"
size="small"
onClick={() => CustomHeaderPopup.show({ provider })}
icon={<Settings2 size={14} />}
/>
</SettingSubtitle>
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input
value={apiHost}
@ -437,32 +433,6 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
)}
</>
)}
{provider.id !== 'copilot' && (
<>
<SettingSubtitle style={{ marginTop: 5 }}>
{t('settings.provider.copilot.custom_headers')}
</SettingSubtitle>
<Space.Compact direction="vertical" style={{ width: '100%', marginTop: 5 }}>
<SettingHelpText>{t('settings.provider.copilot.headers_description')}</SettingHelpText>
<CodeEditor
value={headerText}
language="json"
onChange={(value) => setHeaderText(value)}
onBlur={onUpdateHeaders}
placeholder={`{\n "Header-Name": "Header-Value"\n}`}
options={{
lint: true,
collapsible: false,
wrappable: true,
lineNumbers: true,
foldGutter: true,
highlightActiveLine: true,
keymap: true
}}
/>
</Space.Compact>
</>
)}
</>
)}
{isAzureOpenAI && (