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}
+
+ )}