1 line
17 KiB
JSON
1 line
17 KiB
JSON
{"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"}}}} |