mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-22 08:40:08 +08:00
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:
parent
e273ddcfb0
commit
f7fa665f3a
@ -10,6 +10,7 @@ import { Button, Card, Flex, List, Popconfirm, Space, Tooltip, Typography } from
|
|||||||
import { Trash } from 'lucide-react'
|
import { Trash } from 'lucide-react'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { isLlmProvider, useApiKeys } from './hook'
|
import { isLlmProvider, useApiKeys } from './hook'
|
||||||
import ApiKeyItem from './item'
|
import ApiKeyItem from './item'
|
||||||
@ -87,7 +88,7 @@ export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, prov
|
|||||||
: keys
|
: keys
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ListContainer>
|
||||||
{/* Keys 列表 */}
|
{/* Keys 列表 */}
|
||||||
<Card
|
<Card
|
||||||
size="small"
|
size="small"
|
||||||
@ -122,7 +123,7 @@ export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, prov
|
|||||||
)}
|
)}
|
||||||
</Card>
|
</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>
|
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
|
||||||
|
|
||||||
@ -166,7 +167,7 @@ export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, prov
|
|||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</ListContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,3 +223,8 @@ export const DocPreprocessApiKeyList: FC<SpecificApiKeyListProps> = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ListContainer = styled.div`
|
||||||
|
padding-top: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
`
|
||||||
|
|||||||
@ -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
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { CheckOutlined, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons'
|
import { CheckOutlined, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons'
|
||||||
import { isOpenAIProvider } from '@renderer/aiCore/clients/ApiClientFactory'
|
import { isOpenAIProvider } from '@renderer/aiCore/clients/ApiClientFactory'
|
||||||
import OpenAIAlert from '@renderer/components/Alert/OpenAIAlert'
|
import OpenAIAlert from '@renderer/components/Alert/OpenAIAlert'
|
||||||
import CodeEditor from '@renderer/components/CodeEditor'
|
|
||||||
import { StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons/SVGIcon'
|
import { StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons/SVGIcon'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import { ApiKeyConnectivity, ApiKeyListPopup } from '@renderer/components/Popups/ApiKeyListPopup'
|
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 { Button, Divider, Flex, Input, Space, Switch, Tooltip } from 'antd'
|
||||||
import Link from 'antd/es/typography/Link'
|
import Link from 'antd/es/typography/Link'
|
||||||
import { debounce, isEmpty } from 'lodash'
|
import { debounce, isEmpty } from 'lodash'
|
||||||
import { List, Settings2, SquareArrowOutUpRight } from 'lucide-react'
|
import { Settings2, SquareArrowOutUpRight } from 'lucide-react'
|
||||||
import { motion } from 'motion/react'
|
import { motion } from 'motion/react'
|
||||||
import { FC, useCallback, useDeferredValue, useEffect, useMemo, useState } from 'react'
|
import { FC, useCallback, useDeferredValue, useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -33,6 +32,7 @@ import {
|
|||||||
SettingSubtitle,
|
SettingSubtitle,
|
||||||
SettingTitle
|
SettingTitle
|
||||||
} from '..'
|
} from '..'
|
||||||
|
import CustomHeaderPopup from './CustomHeaderPopup'
|
||||||
import DMXAPISettings from './DMXAPISettings'
|
import DMXAPISettings from './DMXAPISettings'
|
||||||
import GithubCopilotSettings from './GithubCopilotSettings'
|
import GithubCopilotSettings from './GithubCopilotSettings'
|
||||||
import GPUStackSettings from './GPUStackSettings'
|
import GPUStackSettings from './GPUStackSettings'
|
||||||
@ -80,8 +80,6 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
|||||||
checking: false
|
checking: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const [headerText, setHeaderText] = useState<string>(JSON.stringify(provider.extra_headers || {}, null, 2))
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const debouncedUpdateApiKey = useCallback(
|
const debouncedUpdateApiKey = useCallback(
|
||||||
debounce((value) => {
|
debounce((value) => {
|
||||||
@ -311,16 +309,6 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
|||||||
setApiHost(provider.apiHost)
|
setApiHost(provider.apiHost)
|
||||||
}, [provider.apiHost, provider.id])
|
}, [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 (
|
return (
|
||||||
<SettingContainer theme={theme} style={{ background: 'var(--color-background)' }}>
|
<SettingContainer theme={theme} style={{ background: 'var(--color-background)' }}>
|
||||||
<SettingTitle>
|
<SettingTitle>
|
||||||
@ -367,7 +355,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
|||||||
{t('settings.provider.api_key')}
|
{t('settings.provider.api_key')}
|
||||||
{provider.id !== 'copilot' && (
|
{provider.id !== 'copilot' && (
|
||||||
<Tooltip title={t('settings.provider.api.key.list.open')} mouseEnterDelay={0.5}>
|
<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>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</SettingSubtitle>
|
</SettingSubtitle>
|
||||||
@ -410,7 +398,15 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
|||||||
)}
|
)}
|
||||||
{!isDmxapi && (
|
{!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 }}>
|
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||||
<Input
|
<Input
|
||||||
value={apiHost}
|
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 && (
|
{isAzureOpenAI && (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user