diff --git a/src/web-ui/src/component-library/components/Select/Select.scss b/src/web-ui/src/component-library/components/Select/Select.scss index 94e19ae8..8a32efcd 100644 --- a/src/web-ui/src/component-library/components/Select/Select.scss +++ b/src/web-ui/src/component-library/components/Select/Select.scss @@ -286,7 +286,10 @@ border-radius: 3px; color: var(--color-text-primary, #e8e8e8); font-size: var(--font-size-sm, 14px); - outline: none; + outline: none !important; + box-shadow: none !important; + appearance: none; + -webkit-appearance: none; transition: all var(--motion-fast, 0.15s) var(--easing-standard, cubic-bezier(0.4, 0, 0.2, 1)); &::placeholder { @@ -297,9 +300,11 @@ background: var(--element-bg-soft, rgba(255, 255, 255, 0.08)); } - &:focus { + &:focus, + &:focus-visible { background: var(--element-bg-soft, rgba(255, 255, 255, 0.08)); - box-shadow: inset 0 0 0 1px var(--color-accent-500, #60a5fa); + outline: none !important; + box-shadow: none !important; } } diff --git a/src/web-ui/src/infrastructure/config/components/AIModelConfig.scss b/src/web-ui/src/infrastructure/config/components/AIModelConfig.scss index c2c7f842..774c7e76 100644 --- a/src/web-ui/src/infrastructure/config/components/AIModelConfig.scss +++ b/src/web-ui/src/infrastructure/config/components/AIModelConfig.scss @@ -136,6 +136,13 @@ } } + &__provider-group-actions { + display: inline-flex; + align-items: center; + gap: $size-gap-1; + flex-shrink: 0; + } + &__meta-tag { flex-shrink: 0; font-size: $font-size-xs; diff --git a/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx b/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx index 62336e69..b7a9ea99 100644 --- a/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx @@ -166,6 +166,12 @@ const AIModelConfig: React.FC = () => { ], [] ); + const requestFormatLabelMap = useMemo( + () => Object.fromEntries( + requestFormatOptions.map(option => [String(option.value), option.label]) + ) as Record, + [requestFormatOptions] + ); const reasoningEffortOptions = useMemo( () => [ @@ -541,7 +547,7 @@ const AIModelConfig: React.FC = () => { setIsEditing(true); }; - const handleAddModelToExistingProvider = (config: AIModelConfigType) => { + const handleEditProvider = (config: AIModelConfigType) => { resetRemoteModelDiscovery(); setManualModelInput(''); setShowApiKey(false); @@ -583,6 +589,10 @@ const AIModelConfig: React.FC = () => { setIsEditing(true); }; + const handleAddModelToExistingProvider = (config: AIModelConfigType) => { + handleEditProvider(config); + }; + const handleEdit = (config: AIModelConfigType) => { resetRemoteModelDiscovery(); @@ -980,6 +990,7 @@ const AIModelConfig: React.FC = () => { const renderEditingForm = () => { if (!isEditing || !editingConfig) return null; const isFromTemplate = !editingConfig.id && !!currentTemplate; + const isProviderScopedEditing = !editingConfig.id; const currentProviderLabel = (editingConfig.name || currentTemplate?.name || t('providerSelection.customTitle')).trim() || t('providerSelection.customTitle'); const configuredProviderModels = getConfiguredModelsForProvider(currentProviderLabel); const configuredProviderModelOptions: SelectOption[] = configuredProviderModels.map(model => ({ @@ -1119,7 +1130,7 @@ const AIModelConfig: React.FC = () => { <>
{isFromTemplate ? ( @@ -1259,69 +1270,73 @@ const AIModelConfig: React.FC = () => { ) : ( <> - - setEditingConfig(prev => ({ ...prev, name: e.target.value }))} placeholder={t('form.configNamePlaceholder')} inputSize="small" /> - - -
- { - resetRemoteModelDiscovery(); - setEditingConfig(prev => ({ - ...prev, - base_url: e.target.value, - request_url: resolveRequestUrl(e.target.value, prev?.provider || 'openai', prev?.model_name || '') - })); - }} - onFocus={(e) => e.target.select()} - placeholder={'https://open.bigmodel.cn/api/paas/v4/chat/completions'} - inputSize="small" - /> - {editingConfig.base_url && ( -
+ {isProviderScopedEditing && ( + <> + + setEditingConfig(prev => ({ ...prev, name: e.target.value }))} placeholder={t('form.configNamePlaceholder')} inputSize="small" /> + + +
{ + resetRemoteModelDiscovery(); + setEditingConfig(prev => ({ + ...prev, + base_url: e.target.value, + request_url: resolveRequestUrl(e.target.value, prev?.provider || 'openai', prev?.model_name || '') + })); + }} onFocus={(e) => e.target.select()} + placeholder={'https://open.bigmodel.cn/api/paas/v4/chat/completions'} inputSize="small" - className="bitfun-ai-model-config__resolved-url-input" /> + {editingConfig.base_url && ( +
+ e.target.select()} + inputSize="small" + className="bitfun-ai-model-config__resolved-url-input" + /> +
+ )}
- )} -
- - - { - resetRemoteModelDiscovery(); - setEditingConfig(prev => ({ ...prev, api_key: e.target.value })); - }} - placeholder={t('form.apiKeyPlaceholder')} - inputSize="small" - suffix={apiKeySuffix} - /> - + + + { + resetRemoteModelDiscovery(); + setEditingConfig(prev => ({ ...prev, api_key: e.target.value })); + }} + placeholder={t('form.apiKeyPlaceholder')} + inputSize="small" + suffix={apiKeySuffix} + /> + + + { - const provider = value as string; - resetRemoteModelDiscovery(); - setEditingConfig(prev => ({ - ...prev, - provider, - request_url: resolveRequestUrl(prev?.base_url || '', provider, prev?.model_name || ''), - reasoning_effort: isResponsesProvider(provider) ? (prev?.reasoning_effort || 'medium') : undefined, - })); - }} placeholder={t('form.providerPlaceholder')} options={requestFormatOptions} size="small" /> -
@@ -1470,9 +1485,6 @@ const AIModelConfig: React.FC = () => { {t(`category.${config.category}`)} - - {config.provider} - {testResult && ( { {t('details.modelName')} {config.model_name}
-
- {t('details.provider')} - {config.provider} -
{t('details.contextWindow')} {config.context_window?.toLocaleString() || '128,000'} @@ -1657,15 +1665,28 @@ const AIModelConfig: React.FC = () => {
{group.providerName} {group.models.length} + + {requestFormatLabelMap[group.models[0]?.provider || 'openai'] || (group.models[0]?.provider || 'openai')} + +
+
+ handleEditProvider(group.models[0])} + tooltip={t('actions.edit')} + > + + + handleAddModelToExistingProvider(group.models[0])} + tooltip={t('actions.addModel')} + > + +
- handleAddModelToExistingProvider(group.models[0])} - tooltip={t('actions.addModel')} - > - -
{group.models.map(config => renderModelCollectionItem(config))} @@ -1733,7 +1754,9 @@ const AIModelConfig: React.FC = () => { onClose={closeEditingModal} title={editingConfig?.id ? t('editModel') - : (currentTemplate ? `${t('newModel')} - ${currentTemplate.name}` : t('newModel'))} + : (selectedModelDrafts.some(draft => !!draft.configId) + ? t('editProvider') + : (currentTemplate ? `${t('newProvider')} - ${currentTemplate.name}` : t('newProvider')))} size="xlarge" > {renderEditingForm()} diff --git a/src/web-ui/src/infrastructure/config/components/DefaultModelConfig.tsx b/src/web-ui/src/infrastructure/config/components/DefaultModelConfig.tsx index e85248b6..f36d09ec 100644 --- a/src/web-ui/src/infrastructure/config/components/DefaultModelConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/DefaultModelConfig.tsx @@ -52,16 +52,10 @@ export const DefaultModelConfig: React.FC = () => { const [defaultModels, setDefaultModels] = useState({ primary: null, fast: null }); const [optionalCapabilities, setOptionalCapabilities] = useState({}); - - useEffect(() => { - loadData(); - }, []); - - const loadData = async () => { + const loadData = useCallback(async () => { try { setLoading(true); - const [allModels, defaultModelsConfig] = await Promise.all([ configManager.getConfig('ai.models') || [], configManager.getConfig('ai.default_models') || {}, @@ -69,13 +63,11 @@ export const DefaultModelConfig: React.FC = () => { setModels(allModels); - setDefaultModels({ primary: defaultModelsConfig?.primary || null, fast: defaultModelsConfig?.fast || null, }); - setOptionalCapabilities({ image_understanding: defaultModelsConfig?.image_understanding, image_generation: defaultModelsConfig?.image_generation, @@ -87,7 +79,23 @@ export const DefaultModelConfig: React.FC = () => { } finally { setLoading(false); } - }; + }, [t]); + + useEffect(() => { + void loadData(); + + const unsubscribeModels = configManager.watch('ai.models', () => { + void loadData(); + }); + const unsubscribeDefaultModels = configManager.watch('ai.default_models', () => { + void loadData(); + }); + + return () => { + unsubscribeModels(); + unsubscribeDefaultModels(); + }; + }, [loadData]); const getModelName = useCallback((modelId: string | null | undefined): string | undefined => { diff --git a/src/web-ui/src/locales/en-US/settings/ai-model.json b/src/web-ui/src/locales/en-US/settings/ai-model.json index 3792afc4..11225c5a 100644 --- a/src/web-ui/src/locales/en-US/settings/ai-model.json +++ b/src/web-ui/src/locales/en-US/settings/ai-model.json @@ -1,8 +1,11 @@ { "title": "Model Configuration", "subtitle": "Configure and manage AI model providers", + "editProvider": "Edit Provider Configuration", + "newProvider": "New Provider Configuration", "editModel": "Edit Model Configuration", "newModel": "New Model Configuration", + "editProviderSubtitle": "Basic Provider Parameters", "editSubtitle": "Basic Model Parameters", "confirmDelete": "Are you sure you want to delete this configuration?", "providerSelection": { diff --git a/src/web-ui/src/locales/zh-CN/settings/ai-model.json b/src/web-ui/src/locales/zh-CN/settings/ai-model.json index 6d5dc446..7fa341f1 100644 --- a/src/web-ui/src/locales/zh-CN/settings/ai-model.json +++ b/src/web-ui/src/locales/zh-CN/settings/ai-model.json @@ -1,8 +1,11 @@ { "title": "模型配置", "subtitle": "配置和管理 AI 模型提供商", + "editProvider": "编辑服务商配置", + "newProvider": "新建服务商配置", "editModel": "编辑模型配置", "newModel": "新建模型配置", + "editProviderSubtitle": "基础服务商参数配置", "editSubtitle": "基础模型参数配置", "confirmDelete": "确定删除此配置?", "providerSelection": {