From b986e3854e88322e0c3e9d1b4ca530652b458898 Mon Sep 17 00:00:00 2001 From: xlx1212 Date: Wed, 5 Aug 2026 05:12:20 +0800 Subject: [PATCH] feat(model-selector): add thinking-level quick selector in chat dropdown Add a compact reasoning effort selector at the bottom of the Flow Chat model selector dropdown. When the currently selected model has thinking enabled, the dropdown shows a row of effort-level buttons (None, Low, Medium, High, Max, etc.) that the user can click to instantly change the model's reasoning_effort and persist it to config. Closes #1545 --- .../flow_chat/components/ModelSelector.scss | 56 ++++++++++++++++++ .../flow_chat/components/ModelSelector.tsx | 59 ++++++++++++++++++- .../infrastructure/config/utils/reasoning.ts | 53 +++++++++++++++++ src/web-ui/src/locales/en-US/flow-chat.json | 11 +++- src/web-ui/src/locales/zh-CN/flow-chat.json | 11 +++- src/web-ui/src/locales/zh-TW/flow-chat.json | 11 +++- 6 files changed, 194 insertions(+), 7 deletions(-) diff --git a/src/web-ui/src/flow_chat/components/ModelSelector.scss b/src/web-ui/src/flow_chat/components/ModelSelector.scss index 025af00a61..47cfb5db20 100644 --- a/src/web-ui/src/flow_chat/components/ModelSelector.scss +++ b/src/web-ui/src/flow_chat/components/ModelSelector.scss @@ -359,6 +359,57 @@ flex-shrink: 0; } + // ==================== Effort selector ==================== + + &__effort-section { + border-top: 1px solid var(--bf-appearance-token-color-overlay-white-08); + padding: 6px 8px 8px; + } + + &__effort-header { + font-size: var(--bf-appearance-token-flowchat-font-size-xxs); + font-weight: 500; + color: var(--bf-appearance-token-color-text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 0 4px 6px; + } + + &__effort-options { + display: flex; + flex-wrap: wrap; + gap: 4px; + } + + &__effort-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 3px 8px; + border-radius: 4px; + border: 1px solid var(--bf-appearance-token-border-subtle); + background: transparent; + color: var(--bf-appearance-token-color-text-secondary); + font-size: var(--bf-appearance-token-flowchat-font-size-2xs); + font-weight: 500; + cursor: pointer; + transition: + background-color 140ms ease, + color 140ms ease, + border-color 140ms ease; + + &:hover { + background: var(--bf-appearance-token-element-bg-medium); + color: var(--bf-appearance-token-color-text-primary); + } + + &--active { + background: color-mix(in srgb, var(--bf-appearance-token-color-accent-500) 15%, transparent); + border-color: color-mix(in srgb, var(--bf-appearance-token-color-accent-500) 40%, transparent); + color: color-mix(in srgb, var(--bf-appearance-token-color-accent-500) 90%, var(--bf-appearance-token-color-text-primary)); + } + } + :root[data-bf-appearance-mode="light"] &, :root[data-bf-appearance-mode="light"] &, .light & { @@ -384,6 +435,10 @@ background: var(--bf-appearance-token-color-overlay-slate-12); } } + + &__effort-section { + border-top-color: var(--bf-appearance-token-border-subtle); + } } } @@ -407,6 +462,7 @@ @media (prefers-reduced-motion: reduce) { .bitfun-model-selector__trigger, .bitfun-model-selector__option, + .bitfun-model-selector__effort-btn, .bitfun-model-selector__chevron { transition-duration: 0ms; } diff --git a/src/web-ui/src/flow_chat/components/ModelSelector.tsx b/src/web-ui/src/flow_chat/components/ModelSelector.tsx index 7cca92d7ba..806f54d80b 100644 --- a/src/web-ui/src/flow_chat/components/ModelSelector.tsx +++ b/src/web-ui/src/flow_chat/components/ModelSelector.tsx @@ -17,7 +17,7 @@ import { configManager } from '@/infrastructure/config/services/ConfigManager'; import { agentAPI } from '@/infrastructure/api/service-api/AgentAPI'; import { ACPClientAPI, type AcpSessionOptions } from '@/infrastructure/api/service-api/ACPClientAPI'; import { getProviderDisplayName } from '@/infrastructure/config/services/modelConfigs'; -import { getEffectiveReasoningMode, isReasoningVisiblyEnabled } from '@/infrastructure/config/utils/reasoning'; +import { getEffectiveReasoningMode, isReasoningVisiblyEnabled, getReasoningEffortOptionsForModel } from '@/infrastructure/config/utils/reasoning'; import { globalEventBus } from '@/infrastructure/event-bus'; import type { AIModelConfig, AgentModelDefaultsConfig, DefaultModelsConfig } from '@/infrastructure/config/types'; import { Switch, Tooltip } from '@/component-library'; @@ -707,6 +707,37 @@ export const ModelSelector: React.FC = ({ sessionId, ]); + const handleSetReasoningEffort = useCallback(async (effort: string) => { + const modelId = getCurrentModelId(); + if (isSpecialModel(modelId)) return; + + const targetModel = allModels.find(m => m.id === modelId); + if (!targetModel) return; + + const updatedModels = allModels.map(m => + m.id === modelId ? { ...m, reasoning_effort: effort } : m, + ); + + try { + await configManager.setConfig('ai.models', updatedModels); + setAllModels(updatedModels); + globalEventBus.emit('mode:config:updated'); + log.info('Reasoning effort updated', { modelId, effort }); + } catch (error) { + log.error('Failed to update reasoning effort', error); + notificationService.error(t('modelSelector.thinkingLevelUpdateFailed')); + } + }, [allModels, getCurrentModelId, t]); + + const currentModelEffortOptions = useMemo(() => { + const modelId = getCurrentModelId(); + if (isSpecialModel(modelId)) return null; + const targetModel = allModels.find(m => m.id === modelId); + if (!targetModel) return null; + if (!isReasoningVisiblyEnabled(getEffectiveReasoningMode(targetModel))) return null; + return getReasoningEffortOptionsForModel(targetModel); + }, [allModels, getCurrentModelId]); + const handleTriggerKeyDown = useCallback((event: React.KeyboardEvent) => { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); @@ -1233,6 +1264,32 @@ export const ModelSelector: React.FC = ({ ); })} + + {currentModelEffortOptions && currentModelEffortOptions.length > 0 && ( +
+
+ {t('reasoningEffort.title')} +
+
+ {currentModelEffortOptions.map(option => { + const isSelected = currentModel?.reasoningEffort === option.value; + return ( + + ); + })} +
+
+ )} , getAppearanceOverlayHost() )} diff --git a/src/web-ui/src/infrastructure/config/utils/reasoning.ts b/src/web-ui/src/infrastructure/config/utils/reasoning.ts index 1cde229ee0..4907789f52 100644 --- a/src/web-ui/src/infrastructure/config/utils/reasoning.ts +++ b/src/web-ui/src/infrastructure/config/utils/reasoning.ts @@ -56,3 +56,56 @@ export function supportsDeepSeekReasoningEffort( model_name: config?.model_name, }) === 'deepseek'; } + +export interface ReasoningEffortOption { + label: string; + value: string; +} + +const RESPONSES_EFFORT_OPTIONS: ReasoningEffortOption[] = [ + { label: 'None', value: 'none' }, + { label: 'Minimal', value: 'minimal' }, + { label: 'Low', value: 'low' }, + { label: 'Medium', value: 'medium' }, + { label: 'High', value: 'high' }, + { label: 'Extra High', value: 'xhigh' }, +]; + +const ANTHROPIC_EFFORT_OPTIONS: ReasoningEffortOption[] = [ + { label: 'Low', value: 'low' }, + { label: 'Medium', value: 'medium' }, + { label: 'High', value: 'high' }, + { label: 'Max', value: 'max' }, +]; + +const DEEPSEEK_EFFORT_OPTIONS: ReasoningEffortOption[] = [ + { label: 'High', value: 'high' }, + { label: 'Max', value: 'max' }, +]; + +/** + * Returns the reasoning effort options available for a given model config, + * or null when the model has no provider-specific effort selector. + */ +export function getReasoningEffortOptionsForModel( + config?: Partial> | null, +): ReasoningEffortOption[] | null { + if (!config) return null; + + if (supportsDeepSeekReasoningEffort(config)) { + return DEEPSEEK_EFFORT_OPTIONS; + } + + if (supportsResponsesReasoning(config.provider)) { + return RESPONSES_EFFORT_OPTIONS; + } + + if (supportsAnthropicReasoning(config.provider)) { + // Adaptive mode uses the effort selector; manual thinking uses budget tokens. + if (supportsAnthropicAdaptive(config.model_name) || config.reasoning_mode === 'adaptive') { + return ANTHROPIC_EFFORT_OPTIONS; + } + } + + return null; +} diff --git a/src/web-ui/src/locales/en-US/flow-chat.json b/src/web-ui/src/locales/en-US/flow-chat.json index 9d08bbc0c0..f7fa670bfc 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -2371,6 +2371,7 @@ "fastModeDescription": "1.5x speed with higher credit usage", "switchFailed": "Failed to switch model", "modelNotConfigured": "Not Configured", + "thinkingLevelUpdateFailed": "Failed to update thinking level", "contextUsage": { "agentPrompt": "Last request prompt: {{usage}}", "acpContext": "ACP reported context: {{usage}}", @@ -2379,8 +2380,12 @@ }, "reasoningEffort": { "title": "Reasoning Effort", - "tooltip": "Adjust model reasoning depth (Responses API only)", + "tooltip": "Adjust model reasoning depth", "default": "Default", + "none": "None", + "noneDesc": "No reasoning", + "minimal": "Minimal", + "minimalDesc": "Minimal reasoning", "low": "Low", "lowDesc": "Fast responses, light reasoning", "medium": "Medium", @@ -2388,7 +2393,9 @@ "high": "High", "highDesc": "Deep reasoning", "xhigh": "Extra High", - "xhighDesc": "Maximum reasoning depth (slower)" + "xhighDesc": "Maximum reasoning depth (slower)", + "max": "Max", + "maxDesc": "Maximum reasoning depth" }, "coworkScope": { "title": "Choose a Cowork workspace", diff --git a/src/web-ui/src/locales/zh-CN/flow-chat.json b/src/web-ui/src/locales/zh-CN/flow-chat.json index c45ae4f672..15635d0c53 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -2371,6 +2371,7 @@ "fastModeDescription": "1.5 倍速度,消耗更多额度", "switchFailed": "切换模型失败", "modelNotConfigured": "未配置", + "thinkingLevelUpdateFailed": "更新思考层级失败", "contextUsage": { "agentPrompt": "上次请求输入上下文: {{usage}}", "acpContext": "ACP 上报上下文: {{usage}}", @@ -2379,8 +2380,12 @@ }, "reasoningEffort": { "title": "推理深度", - "tooltip": "调整模型推理深度(仅 Responses API)", + "tooltip": "调整模型推理深度", "default": "默认", + "none": "None", + "noneDesc": "不启用推理", + "minimal": "Minimal", + "minimalDesc": "极简推理", "low": "Low", "lowDesc": "快速响应,轻度推理", "medium": "Medium", @@ -2388,7 +2393,9 @@ "high": "High", "highDesc": "深度推理", "xhigh": "Extra High", - "xhighDesc": "最大推理深度(较慢)" + "xhighDesc": "最大推理深度(较慢)", + "max": "Max", + "maxDesc": "最大推理深度" }, "coworkScope": { "title": "选择 Cowork 工作区", diff --git a/src/web-ui/src/locales/zh-TW/flow-chat.json b/src/web-ui/src/locales/zh-TW/flow-chat.json index 16c4b9b490..ea193ee449 100644 --- a/src/web-ui/src/locales/zh-TW/flow-chat.json +++ b/src/web-ui/src/locales/zh-TW/flow-chat.json @@ -2371,6 +2371,7 @@ "fastModeDescription": "1.5 倍速度,消耗更多額度", "switchFailed": "切換模型失敗", "modelNotConfigured": "未設定", + "thinkingLevelUpdateFailed": "更新思考層級失敗", "contextUsage": { "agentPrompt": "上次請求輸入上下文: {{usage}}", "acpContext": "ACP 回報上下文: {{usage}}", @@ -2379,8 +2380,12 @@ }, "reasoningEffort": { "title": "推理深度", - "tooltip": "調整模型推理深度(僅 Responses API)", + "tooltip": "調整模型推理深度", "default": "默認", + "none": "None", + "noneDesc": "不啟用推理", + "minimal": "Minimal", + "minimalDesc": "極簡推理", "low": "Low", "lowDesc": "快速響應,輕度推理", "medium": "Medium", @@ -2388,7 +2393,9 @@ "high": "High", "highDesc": "深度推理", "xhigh": "Extra High", - "xhighDesc": "最大推理深度(較慢)" + "xhighDesc": "最大推理深度(較慢)", + "max": "Max", + "maxDesc": "最大推理深度" }, "coworkScope": { "title": "選擇 Cowork 工作區",