From 7c3ba38f352ba38e2941b8b4d652b8eca5c7cd63 Mon Sep 17 00:00:00 2001 From: qdaxb <4157870+qdaxb@users.noreply.github.com> Date: Sun, 30 Nov 2025 23:59:06 +0800 Subject: [PATCH 1/4] feat(frontend): display model as displayName(modelId) in task model selector - Add displayName field to Model interface - Add getModelDisplayText helper function to format model display - Update trigger button to show displayName(modelId) format - Update dropdown list items to show displayName(modelId) format - Include displayName in search value for better searchability --- .../tasks/components/ModelSelector.tsx | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/frontend/src/features/tasks/components/ModelSelector.tsx b/frontend/src/features/tasks/components/ModelSelector.tsx index 055498e3..7b49d731 100644 --- a/frontend/src/features/tasks/components/ModelSelector.tsx +++ b/frontend/src/features/tasks/components/ModelSelector.tsx @@ -32,6 +32,7 @@ export interface Model { name: string; provider: string; // 'openai' | 'claude' modelId: string; + displayName?: string | null; // Human-readable display name type?: ModelTypeEnum; // 'public' | 'user' - identifies model source } @@ -67,10 +68,17 @@ function unifiedToModel(unified: UnifiedModel): Model { name: unified.name, provider: unified.provider || 'claude', modelId: unified.modelId || '', + displayName: unified.displayName, type: unified.type, }; } +// Helper function to get display text for a model: displayName(modelId) or name(modelId) +function getModelDisplayText(model: Model): string { + const displayName = model.displayName || model.name; + return model.modelId ? `${displayName}(${model.modelId})` : displayName; +} + // Helper function to check if all bots in a team have predefined models function allBotsHavePredefinedModel(team: TeamWithBotDetails | null): boolean { if (!team || !team.bots || team.bots.length === 0) { @@ -262,10 +270,11 @@ export default function ModelSelector({ if (selectedModel.name === DEFAULT_MODEL_NAME) { return t('task_submit.default_model', '默认'); } + const displayText = getModelDisplayText(selectedModel); if (forceOverride && !isMixedTeam) { - return `${selectedModel.name}(${t('task_submit.override_short', '覆盖')})`; + return `${displayText}(${t('task_submit.override_short', '覆盖')})`; } - return selectedModel.name; + return displayText; }; return ( @@ -370,7 +379,7 @@ export default function ModelSelector({ {filteredModels.map(model => ( handleModelSelect(getModelKey(model))} className={cn( 'group cursor-pointer select-none', @@ -396,9 +405,9 @@ export default function ModelSelector({
- {model.name} + {getModelDisplayText(model)} {model.type === 'public' && ( @@ -406,12 +415,6 @@ export default function ModelSelector({ )}
- - {model.modelId} -
From 3d75fa1bd7dd728c69de9492014dbdcbbd548a8e Mon Sep 17 00:00:00 2001 From: qdaxb <4157870+qdaxb@users.noreply.github.com> Date: Mon, 1 Dec 2025 14:33:22 +0800 Subject: [PATCH 2/4] fix(frontend): require model selection for legacy teams without predefined models Legacy teams created before PR#194 may have Bots without agent_config (bind_model). When using these teams, selecting "Default Model" option would cause agent_config to be empty and result in task execution errors. This fix: - Export allBotsHavePredefinedModel function from ModelSelector for reuse - Add isModelSelectionRequired check in ChatArea to disable send button when model selection is required but not fulfilled - Display error-styled "Please select a model" prompt in ModelSelector for legacy teams - Add i18n translations (model_required) for both zh-CN and en locales --- .../features/tasks/components/ChatArea.tsx | 26 ++++++++++++++++--- .../tasks/components/ModelSelector.tsx | 20 +++++++++++--- frontend/src/i18n/locales/en/common.json | 3 ++- frontend/src/i18n/locales/zh-CN/common.json | 3 ++- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/frontend/src/features/tasks/components/ChatArea.tsx b/frontend/src/features/tasks/components/ChatArea.tsx index 2e70c079..18d7ce3d 100644 --- a/frontend/src/features/tasks/components/ChatArea.tsx +++ b/frontend/src/features/tasks/components/ChatArea.tsx @@ -9,7 +9,11 @@ import { Send, CircleStop } from 'lucide-react'; import MessagesArea from './MessagesArea'; import ChatInput from './ChatInput'; import TeamSelector from './TeamSelector'; -import ModelSelector, { Model, DEFAULT_MODEL_NAME } from './ModelSelector'; +import ModelSelector, { + Model, + DEFAULT_MODEL_NAME, + allBotsHavePredefinedModel, +} from './ModelSelector'; import RepositorySelector from './RepositorySelector'; import BranchSelector from './BranchSelector'; import LoadingDots from './LoadingDots'; @@ -172,6 +176,18 @@ export default function ChatArea({ return teamNameLength + modelNameLength > COMPACT_QUOTA_NAME_THRESHOLD; }, [isMobile, selectedTeam?.name, selectedModel?.name]); + // Check if model selection is required but not fulfilled + // For legacy teams without predefined models, user MUST select a model before sending + const isModelSelectionRequired = React.useMemo(() => { + // Skip check if team is not selected, or if team type is 'dify' (external API) + if (!selectedTeam || selectedTeam.agent_type === 'dify') return false; + // If team's bots have predefined models, "Default" option is available, no need to force selection + const hasDefaultOption = allBotsHavePredefinedModel(selectedTeam); + if (hasDefaultOption) return false; + // Model selection is required when no model is selected + return !selectedModel; + }, [selectedTeam, selectedModel]); + const handleTeamChange = (team: Team | null) => { console.log('[ChatArea] handleTeamChange called:', team?.name || 'null', team?.id || 'null'); setSelectedTeam(team); @@ -580,7 +596,9 @@ export default function ChatArea({ size="icon" onClick={handleSendMessage} disabled={ - isLoading || (shouldHideChatInput ? false : !taskInputMessage.trim()) + isLoading || + isModelSelectionRequired || + (shouldHideChatInput ? false : !taskInputMessage.trim()) } className="h-6 w-6 rounded-full hover:bg-primary/10 flex-shrink-0 translate-y-0.5" > @@ -722,7 +740,9 @@ export default function ChatArea({ size="icon" onClick={handleSendMessage} disabled={ - isLoading || (shouldHideChatInput ? false : !taskInputMessage.trim()) + isLoading || + isModelSelectionRequired || + (shouldHideChatInput ? false : !taskInputMessage.trim()) } className="h-6 w-6 rounded-full hover:bg-primary/10 flex-shrink-0 translate-y-0.5" > diff --git a/frontend/src/features/tasks/components/ModelSelector.tsx b/frontend/src/features/tasks/components/ModelSelector.tsx index 055498e3..a0a7e553 100644 --- a/frontend/src/features/tasks/components/ModelSelector.tsx +++ b/frontend/src/features/tasks/components/ModelSelector.tsx @@ -72,7 +72,8 @@ function unifiedToModel(unified: UnifiedModel): Model { } // Helper function to check if all bots in a team have predefined models -function allBotsHavePredefinedModel(team: TeamWithBotDetails | null): boolean { +// Exported for use in ChatArea to determine if model selection is required +export function allBotsHavePredefinedModel(team: TeamWithBotDetails | null): boolean { if (!team || !team.bots || team.bots.length === 0) { return false; } @@ -254,10 +255,20 @@ export default function ModelSelector({ // Determine if selector should be disabled const isDisabled = disabled || externalLoading || isLoading || isMixedTeam; + // Check if model selection is required (for legacy teams without predefined models) + const isModelRequired = !showDefaultOption && !selectedModel; + // Get display text for trigger const getTriggerDisplayText = () => { if (!selectedModel) { - return isLoading ? t('actions.loading') : t('task_submit.select_model', '选择模型'); + if (isLoading) { + return t('actions.loading'); + } + // Show required hint for legacy teams without predefined models + if (isModelRequired) { + return t('task_submit.model_required', '请选择模型'); + } + return t('task_submit.select_model', '选择模型'); } if (selectedModel.name === DEFAULT_MODEL_NAME) { return t('task_submit.default_model', '默认'); @@ -276,7 +287,7 @@ export default function ModelSelector({ style={{ maxWidth: isMobile ? 140 : 180, minWidth: isMobile ? 50 : 70 }} >
@@ -288,7 +299,8 @@ export default function ModelSelector({ disabled={isDisabled} className={cn( 'flex h-9 w-full min-w-0 items-center justify-between rounded-lg text-left', - 'bg-transparent px-0 text-xs text-text-muted', + 'bg-transparent px-0 text-xs', + isModelRequired ? 'text-error' : 'text-text-muted', 'hover:bg-transparent transition-colors', 'focus:outline-none focus:ring-0', 'disabled:cursor-not-allowed disabled:opacity-50' diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json index 4ddfdd14..81292aa2 100644 --- a/frontend/src/i18n/locales/en/common.json +++ b/frontend/src/i18n/locales/en/common.json @@ -535,6 +535,7 @@ "override_short": "Override", "mixed_team_warning": "This team contains multiple executor types, cannot specify a unified model", "default_model": "Default", - "use_bot_model": "Use Bot's predefined model" + "use_bot_model": "Use Bot's predefined model", + "model_required": "Please select a model" } } diff --git a/frontend/src/i18n/locales/zh-CN/common.json b/frontend/src/i18n/locales/zh-CN/common.json index 6d9bebaa..111b1390 100644 --- a/frontend/src/i18n/locales/zh-CN/common.json +++ b/frontend/src/i18n/locales/zh-CN/common.json @@ -536,6 +536,7 @@ "override_short": "覆盖", "mixed_team_warning": "当前团队包含多种执行器类型,无法统一指定模型", "default_model": "默认绑定模型", - "use_bot_model": "使用 Bot 预设模型" + "use_bot_model": "使用 Bot 预设模型", + "model_required": "请选择模型" } } From 55832f2ee21471c6c9ae581b5c35ee49db64c5e0 Mon Sep 17 00:00:00 2001 From: axb Date: Mon, 1 Dec 2025 14:48:56 +0800 Subject: [PATCH 3/4] fix: fix model select --- backend/app/services/adapters/bot_kinds.py | 11 +++++-- .../tasks/components/ModelSelector.tsx | 31 +++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/backend/app/services/adapters/bot_kinds.py b/backend/app/services/adapters/bot_kinds.py index e4ceaa66..10153dfd 100644 --- a/backend/app/services/adapters/bot_kinds.py +++ b/backend/app/services/adapters/bot_kinds.py @@ -603,11 +603,18 @@ def update_with_user( # Update bot's modelRef bot_crd = Bot.model_validate(bot.json) + from app.schemas.kind import ModelRef + if bot_crd.spec.modelRef: bot_crd.spec.modelRef.name = model_name bot_crd.spec.modelRef.namespace = "default" - bot.json = bot_crd.model_dump() - flag_modified(bot, "json") + else: + # Create new modelRef if it doesn't exist + bot_crd.spec.modelRef = ModelRef( + name=model_name, namespace="default" + ) + bot.json = bot_crd.model_dump() + flag_modified(bot, "json") # Only delete old model if it's a user's private custom model (not public or predefined) # A private custom model must satisfy: diff --git a/frontend/src/features/tasks/components/ModelSelector.tsx b/frontend/src/features/tasks/components/ModelSelector.tsx index a0a7e553..2484db56 100644 --- a/frontend/src/features/tasks/components/ModelSelector.tsx +++ b/frontend/src/features/tasks/components/ModelSelector.tsx @@ -170,9 +170,36 @@ export default function ModelSelector({ fetchModels(); }, [fetchModels]); - // Restore last selected model from localStorage or set default + // Track previous team ID to detect team changes and re-validate model selection + const prevTeamIdRef = React.useRef(null); + + // Re-validate model selection when team changes useEffect(() => { - // When team changes or all bots have predefined models, auto-select default + const currentTeamId = selectedTeam?.id ?? null; + const teamChanged = prevTeamIdRef.current !== null && prevTeamIdRef.current !== currentTeamId; + prevTeamIdRef.current = currentTeamId; + + if (!teamChanged) return; + + // Team changed - re-validate model selection based on new team's showDefaultOption + if (showDefaultOption) { + // New team supports default option, set to default + if (!selectedModel || selectedModel.name !== DEFAULT_MODEL_NAME) { + setSelectedModel({ name: DEFAULT_MODEL_NAME, provider: '', modelId: '' }); + } + } else { + // New team does NOT support default option, clear model selection + // User must re-select a compatible model + setSelectedModel(null); + } + }, [selectedTeam?.id, showDefaultOption, selectedModel, setSelectedModel]); + + // Restore last selected model from localStorage or set default (only on initial load) + useEffect(() => { + // Skip if team has changed (handled by the above effect) + // This effect only handles initial load and model list changes + + // When all bots have predefined models, auto-select default if (showDefaultOption) { // If all bots have predefined models, auto-select "Default" if (!selectedModel || selectedModel.name !== DEFAULT_MODEL_NAME) { From 580fe1d1840e60622af7399acee91fca69cd0c4a Mon Sep 17 00:00:00 2001 From: axb Date: Mon, 1 Dec 2025 15:06:18 +0800 Subject: [PATCH 4/4] style: update model selector style --- .../src/features/tasks/components/ModelSelector.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/src/features/tasks/components/ModelSelector.tsx b/frontend/src/features/tasks/components/ModelSelector.tsx index ea5e2e82..912f0d50 100644 --- a/frontend/src/features/tasks/components/ModelSelector.tsx +++ b/frontend/src/features/tasks/components/ModelSelector.tsx @@ -75,8 +75,7 @@ function unifiedToModel(unified: UnifiedModel): Model { // Helper function to get display text for a model: displayName(modelId) or name(modelId) function getModelDisplayText(model: Model): string { - const displayName = model.displayName || model.name; - return model.modelId ? `${displayName}(${model.modelId})` : displayName; + return model.displayName ? `${model.displayName}(${model.name})` : model.name; } // Helper function to check if all bots in a team have predefined models @@ -454,6 +453,14 @@ export default function ModelSelector({ )}
+ {model.modelId && ( + + {model.modelId} + + )}