Files

1 line
17 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "ces-ltd/titanx", "files": {"src/renderer/pages/settings/components/EditModeModal.tsx": {"1d14f5f46b6c46334a54ae6aee3fc57c6ffd6d9a": {"t": 1785595450.3600233, "n": 17034, "h": "df8ea16de4f2d42d", "c": "import type { IProvider } from '@/common/config/storage';\nimport ModalHOC from '@/renderer/utils/ui/ModalHOC';\nimport { Form, Input, Message, Select } from '@arco-design/web-react';\nimport React, { useEffect, useMemo } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport AionModal from '@/renderer/components/base/AionModal';\nimport { LinkCloud } from '@icon-park/react';\nimport { ipcBridge } from '@/common';\nimport useModeModeList from '@renderer/hooks/agent/useModeModeList';\n\n// Provider Logo imports\nimport GeminiLogo from '@/renderer/assets/logos/ai-major/gemini.svg';\nimport OpenAILogo from '@/renderer/assets/logos/ai-major/openai.svg';\nimport AnthropicLogo from '@/renderer/assets/logos/ai-major/anthropic.svg';\nimport BedrockLogo from '@/renderer/assets/logos/ai-cloud/bedrock.svg';\nimport DeepSeekLogo from '@/renderer/assets/logos/ai-major/deepseek.svg';\nimport OpenRouterLogo from '@/renderer/assets/logos/ai-cloud/openrouter.svg';\nimport SiliconFlowLogo from '@/renderer/assets/logos/ai-cloud/siliconflow.png';\nimport QwenLogo from '@/renderer/assets/logos/ai-china/qwen.svg';\nimport KimiLogo from '@/renderer/assets/logos/ai-china/kimi.svg';\nimport ZhipuLogo from '@/renderer/assets/logos/ai-china/zhipu.svg';\nimport XaiLogo from '@/renderer/assets/logos/ai-major/xai.svg';\nimport VolcengineLogo from '@/renderer/assets/logos/ai-china/volcengine.svg';\nimport BaiduLogo from '@/renderer/assets/logos/ai-china/baidu.svg';\nimport TencentLogo from '@/renderer/assets/logos/ai-china/tencent.svg';\nimport LingyiLogo from '@/renderer/assets/logos/ai-china/lingyiwanwu.svg';\nimport PoeLogo from '@/renderer/assets/logos/ai-cloud/poe.svg';\nimport ModelScopeLogo from '@/renderer/assets/logos/ai-cloud/modelscope.svg';\nimport InfiniAILogo from '@/renderer/assets/logos/ai-cloud/infiniai.svg';\nimport CtyunLogo from '@/renderer/assets/logos/ai-cloud/ctyun.svg';\nimport StepFunLogo from '@/renderer/assets/logos/ai-china/stepfun.svg';\nimport NewApiLogo from '@/renderer/assets/logos/ai-cloud/newapi.svg';\n\n/**\n * 供应商配置(包含名称、URL、Logo)\n * Provider config (includes name, URL, logo)\n */\nconst PROVIDER_CONFIGS = [\n { name: 'Gemini', url: '', logo: GeminiLogo, platform: 'gemini' },\n { name: 'Gemini (Vertex AI)', url: '', logo: GeminiLogo, platform: 'gemini-vertex-ai' },\n { name: 'New API', url: '', logo: NewApiLogo, platform: 'new-api' },\n { name: 'OpenAI', url: 'https://api.openai.com/v1', logo: OpenAILogo },\n { name: 'Anthropic', url: 'https://api.anthropic.com/v1', logo: AnthropicLogo },\n { name: 'AWS Bedrock', url: '', logo: BedrockLogo, platform: 'bedrock' },\n { name: 'DeepSeek', url: 'https://api.deepseek.com', logo: DeepSeekLogo },\n { name: 'OpenRouter', url: 'https://openrouter.ai/api/v1', logo: OpenRouterLogo },\n { name: 'SiliconFlow-CN', url: 'https://api.siliconflow.cn/v1', logo: SiliconFlowLogo },\n { name: 'SiliconFlow', url: 'https://api.siliconflow.com/v1', logo: SiliconFlowLogo },\n { name: 'Dashscope', url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', logo: QwenLogo },\n { name: 'Moonshot (China)', url: 'https://api.moonshot.cn/v1', logo: KimiLogo },\n { name: 'Moonshot (Global)', url: 'https://api.moonshot.ai/v1', logo: KimiLogo },\n { name: 'Zhipu', url: 'https://open.bigmodel.cn/api/paas/v4', logo: ZhipuLogo },\n { name: 'xAI', url: 'https://api.x.ai/v1', logo: XaiLogo },\n { name: 'Ark', url: 'https://ark.cn-beijing.volces.com/api/v3', logo: VolcengineLogo },\n { name: 'Qianfan', url: 'https://qianfan.baidubce.com/v2', logo: BaiduLogo },\n { name: 'Hunyuan', url: 'https://api.hunyuan.cloud.tencent.com/v1', logo: TencentLogo },\n { name: 'Lingyi', url: 'https://api.lingyiwanwu.com/v1', logo: LingyiLogo },\n { name: 'Poe', url: 'https://api.poe.com/v1', logo: PoeLogo },\n { name: 'ModelScope', url: 'https://api-inference.modelscope.cn/v1', logo: ModelScopeLogo },\n { name: 'InfiniAI', url: 'https://cloud.infini-ai.com/maas/v1', logo: InfiniAILogo },\n { name: 'Ctyun', url: 'https://wishub-x1.ctyun.cn/v1', logo: CtyunLogo },\n { name: 'StepFun', url: 'https://api.stepfun.com/v1', logo: StepFunLogo },\n];\n\n/**\n * 根据名称或 URL 获取供应商 Logo\n * Get provider logo by name or URL\n */\nconst getProviderLogo = (name?: string, baseUrl?: string, platform?: string): string | null => {\n if (!name && !baseUrl && !platform) return null;\n\n // 优先按 platform 匹配(Gemini 系列)\n if (platform) {\n const byPlatform = PROVIDER_CONFIGS.find((p) => p.platform === platform);\n if (byPlatform) return byPlatform.logo;\n }\n\n // 按名称精确匹配\n const byName = PROVIDER_CONFIGS.find((p) => p.name === name);\n if (byName) return byName.logo;\n\n // 按名称模糊匹配(忽略大小写)\n const byNameLower = PROVIDER_CONFIGS.find((p) => p.name.toLowerCase() === name?.toLowerCase());\n if (byNameLower) return byNameLower.logo;\n\n // 按 URL 匹配\n if (baseUrl) {\n const byUrl = PROVIDER_CONFIGS.find((p) => p.url && baseUrl.includes(p.url.replace('https://', '').split('/')[0]));\n if (byUrl) return byUrl.logo;\n }\n\n return null;\n};\n\n/**\n * 供应商 Logo 组件\n * Provider Logo Component\n */\nconst ProviderLogo: React.FC<{ logo: string | null; name: string; size?: number }> = ({ logo, name, size = 20 }) => {\n if (logo) {\n return <img src={logo} alt={name} className='object-contain shrink-0' style={{ width: size, height: size }} />;\n }\n return <LinkCloud theme='outline' size={size} className='text-t-secondary flex shrink-0' />;\n};\n\nconst EditModeModal = ModalHOC<{ data?: IProvider; onChange(data: IProvider): void }>(\n ({ modalProps, modalCtrl, ...props }) => {\n const { t } = useTranslation();\n const { data } = props;\n const [form] = Form.useForm();\n const [message, messageContext] = Message.useMessage();\n\n // Watch bedrockAuthMethod only for UI conditional rendering (not for auto-refresh)\n const bedrockAuthMethod = Form.useWatch('bedrockAuthMethod', form);\n const isBedrock = data?.platform === 'bedrock';\n\n // 获取供应商 Logo / Get provider logo\n const providerLogo = useMemo(() => {\n return getProviderLogo(data?.name, data?.baseUrl, data?.platform);\n }, [data?.name, data?.baseUrl, data?.platform]);\n\n // For Bedrock, don't pass bedrockConfig to avoid auto-refresh on input changes\n // We'll build it dynamically in onFocus\n const modelListState = useModeModeList(data?.platform || 'gemini', data?.baseUrl, data?.apiKey, true, undefined);\n\n useEffect(() => {\n if (data) {\n form.setFieldsValue({\n ...data,\n model:\n data.model && data.model.length > 0 ? (data.model.length === 1 ? data.model[0] : data.model) : undefined,\n bedrockAuthMethod: data.bedrockConfig?.authMethod || 'accessKey',\n bedrockRegion: data.bedrockConfig?.region || 'us-east-1',\n bedrockAccessKeyId: data.bedrockConfig?.accessKeyId || '',\n bedrockSecretAccessKey: data.bedrockConfig?.secretAccessKey || '',\n bedrockProfile: data.bedrockConfig?.profile || '',\n });\n }\n }, [data, form]);\n\n return (\n <AionModal\n visible={modalProps.visible}\n onCancel={modalCtrl.close}\n header={{ title: t('settings.editModel'), showClose: true }}\n style={{ minHeight: '400px', maxHeight: '90vh', borderRadius: 16 }}\n contentStyle={{ background: 'var(--bg-1)', borderRadius: 16, padding: '20px 24px 16px', overflow: 'auto' }}\n onOk={async () => {\n try {\n const values = await form.validate();\n const updatedProvider: IProvider = {\n ...data,\n ...values,\n // Ensure model is always an array\n model: Array.isArray(values.model) ? values.model : [values.model],\n };\n\n // Add Bedrock configuration if platform is Bedrock\n if (isBedrock) {\n updatedProvider.bedrockConfig = {\n authMethod: values.bedrockAuthMethod,\n region: values.bedrockRegion,\n ...(values.bedrockAuthMethod === 'accessKey'\n ? {\n accessKeyId: values.bedrockAccessKeyId,\n secretAccessKey: values.bedrockSecretAccessKey,\n }\n : {\n profile: values.bedrockProfile,\n }),\n };\n }\n\n props.onChange(updatedProvider);\n modalCtrl.close();\n } catch {\n // Validation failed — Arco Form highlights invalid fields automatically\n }\n }}\n okText={t('common.save')}\n cancelText={t('common.cancel')}\n >\n {messageContext}\n <div className='py-20px'>\n <Form form={form} layout='vertical'>\n {/* 模型供应商名称(可编辑,带 Logo)/ Model Provider name (editable, with Logo) */}\n <Form.Item\n label={\n <div className='flex items-center gap-6px'>\n <ProviderLogo logo={providerLogo} name={data?.name || ''} size={16} />\n <span>{t('settings.modelProvider')}</span>\n </div>\n }\n field='name'\n required\n rules={[{ required: true }]}\n >\n <Input placeholder={t('settings.modelProvider')} />\n </Form.Item>\n\n {/* Base URL - 仅 Gemini 平台显示(用于自定义代理)/ Base URL - only for Gemini platform (for custom proxy) */}\n <Form.Item\n hidden={isBedrock}\n label={t('settings.baseUrl')}\n required={data?.platform !== 'gemini' && data?.platform !== 'gemini-vertex-ai' && !isBedrock}\n rules={[{ required: data?.platform !== 'gemini' && data?.platform !== 'gemini-vertex-ai' && !isBedrock }]}\n field={'baseUrl'}\n disabled\n >\n <Input></Input>\n </Form.Item>\n\n <Form.Item\n hidden={isBedrock}\n label={t('settings.apiKey')}\n required={!isBedrock}\n rules={[{ required: !isBedrock }]}\n field={'apiKey'}\n extra={<div className='text-11px text-t-secondary mt-2'>💡 {t('settings.multiApiKeyEditTip')}</div>}\n >\n <Input.TextArea rows={4} placeholder={t('settings.apiKeyPlaceholder')} />\n </Form.Item>\n\n {/* AWS Bedrock Authentication Method */}\n <Form.Item\n hidden={!isBedrock}\n label={t('settings.bedrock.authMethod')}\n field={'bedrockAuthMethod'}\n required={isBedrock}\n rules={[{ required: isBedrock }]}\n >\n <Select>\n <Select.Option value='accessKey'>{t('settings.bedrock.authMethodAccessKey')}</Select.Option>\n <Select.Option value='profile'>{t('settings.bedrock.authMethodProfile')}</Select.Option>\n </Select>\n </Form.Item>\n\n {/* AWS Region */}\n <Form.Item\n hidden={!isBedrock}\n label={t('settings.bedrock.region')}\n field={'bedrockRegion'}\n required={isBedrock}\n rules={[{ required: isBedrock }]}\n extra={t('settings.bedrock.regionHint')}\n >\n <Select showSearch>\n <Select.Option value='us-east-1'>US East (N. Virginia)</Select.Option>\n <Select.Option value='us-west-2'>US West (Oregon)</Select.Option>\n <Select.Option value='eu-west-1'>Europe (Ireland)</Select.Option>\n <Select.Option value='eu-central-1'>Europe (Frankfurt)</Select.Option>\n <Select.Option value='ap-southeast-1'>Asia Pacific (Singapore)</Select.Option>\n <Select.Option value='ap-northeast-1'>Asia Pacific (Tokyo)</Select.Option>\n <Select.Option value='ap-southeast-2'>Asia Pacific (Sydney)</Select.Option>\n <Select.Option value='ca-central-1'>Canada (Central)</Select.Option>\n </Select>\n </Form.Item>\n\n {/* Access Key ID */}\n <Form.Item\n hidden={!isBedrock || bedrockAuthMethod !== 'accessKey'}\n label={t('settings.bedrock.accessKeyId')}\n field={'bedrockAccessKeyId'}\n required={isBedrock && bedrockAuthMethod === 'accessKey'}\n rules={[{ required: isBedrock && bedrockAuthMethod === 'accessKey' }]}\n >\n <Input.Password placeholder='AKIA...' visibilityToggle />\n </Form.Item>\n\n {/* Secret Access Key */}\n <Form.Item\n hidden={!isBedrock || bedrockAuthMethod !== 'accessKey'}\n label={t('settings.bedrock.secretAccessKey')}\n field={'bedrockSecretAccessKey'}\n required={isBedrock && bedrockAuthMethod === 'accessKey'}\n rules={[{ required: isBedrock && bedrockAuthMethod === 'accessKey' }]}\n >\n <Input.Password visibilityToggle />\n </Form.Item>\n\n {/* AWS Profile */}\n <Form.Item\n hidden={!isBedrock || bedrockAuthMethod !== 'profile'}\n label={t('settings.bedrock.profile')}\n field={'bedrockProfile'}\n required={isBedrock && bedrockAuthMethod === 'profile'}\n rules={[{ required: isBedrock && bedrockAuthMethod === 'profile' }]}\n extra={t('settings.bedrock.profileHint')}\n >\n <Input placeholder='default' />\n </Form.Item>\n\n {/* Model Selection */}\n <Form.Item\n label={t('settings.modelName')}\n field={'model'}\n required\n rules={[{ required: true }]}\n validateStatus={modelListState.error ? 'error' : undefined}\n help={modelListState.error}\n >\n <Select\n loading={modelListState.isLoading}\n showSearch\n allowCreate\n mode={data?.model && data.model.length > 1 ? 'multiple' : undefined}\n onFocus={async () => {\n // For Bedrock, build bedrockConfig from current form values and fetch models\n if (isBedrock) {\n const values = form.getFields();\n if (!values.bedrockAuthMethod || !values.bedrockRegion) {\n message.error(t('settings.bedrock.fillRequiredFields'));\n return;\n }\n if (\n values.bedrockAuthMethod === 'accessKey' &&\n (!values.bedrockAccessKeyId || !values.bedrockSecretAccessKey)\n ) {\n message.error(t('settings.bedrock.fillRequiredFields'));\n return;\n }\n if (values.bedrockAuthMethod === 'profile' && !values.bedrockProfile) {\n message.error(t('settings.bedrock.fillRequiredFields'));\n return;\n }\n // Build bedrockConfig and fetch models manually\n const bedrockConfig = {\n authMethod: values.bedrockAuthMethod,\n region: values.bedrockRegion,\n ...(values.bedrockAuthMethod === 'accessKey'\n ? {\n accessKeyId: values.bedrockAccessKeyId,\n secretAccessKey: values.bedrockSecretAccessKey,\n }\n : {\n profile: values.bedrockProfile,\n }),\n };\n try {\n const res = await ipcBridge.mode.fetchModelList.invoke({\n platform: data?.platform || 'bedrock',\n api_key: '',\n bedrockConfig,\n });\n if (res.success) {\n const models =\n res.data?.mode.map((v: any) => {\n if (typeof v === 'string') {\n return { label: v, value: v };\n } else {\n return { label: v.name, value: v.id };\n }\n }) || [];\n // Update the model list state manually\n void modelListState.mutate({ models }, false);\n } else {\n message.error(res.msg || 'Failed to fetch models');\n }\n } catch (error: any) {\n message.error(error.message || 'Failed to fetch models');\n }\n return;\n }\n void modelListState.mutate();\n }}\n options={modelListState.data?.models || []}\n />\n </Form.Item>\n </Form>\n </div>\n </AionModal>\n );\n }\n);\n\nexport default EditModeModal;\n"}}}}