From ef7abbcb0e95a1a4aa4a970b70195fb04eebc8b9 Mon Sep 17 00:00:00 2001 From: SuYao Date: Thu, 1 May 2025 22:57:04 +0800 Subject: [PATCH] Feat/qwen3 support (#5533) * feat: implement ThinkingPanel for managing reasoning effort and token limits - Added ThinkingPanel component to handle user settings for reasoning effort and thinking budget. - Introduced ThinkingSelect and ThinkingSlider components for selecting reasoning effort and adjusting token limits. - Updated models and hooks to support new reasoning effort and thinking budget features. - Enhanced Inputbar to integrate ThinkingPanel and provide a toggle for enabling thinking features. - Updated translations and styles for new components. * refactor: enhance ThinkingPanel and related components for improved reasoning effort management - Updated ThinkingPanel to streamline token mapping and error messaging. - Refactored ThinkingSelect to utilize a list for better UI interaction. - Enhanced ThinkingSlider with styled components for a more intuitive user experience. - Adjusted model checks in the configuration to support new reasoning models. - Improved translations for clarity and consistency across languages. * feat: close ThinkingPanel on outside click * feat(icons): add new lightbulb icons and update reasoning effort settings in translations - Introduced new SVG icons for lightbulb states (off, on at 10%, 50%, and 90%). - Added "Off" reasoning effort option in English, Japanese, Russian, Simplified Chinese, and Traditional Chinese translations. - Refactored Inputbar and ThinkingButton components to integrate new reasoning effort logic and icon display. * feat(thinking): refactor reasoning effort management and introduce new lightbulb icon - Removed ThinkingPanel, ThinkingSelect, and ThinkingSlider components to streamline reasoning effort management. - Added new MdiLightbulbAutoOutline icon for the auto reasoning effort option. - Updated reasoning effort logic in ThinkingButton to support 'auto' and fallback options. - Enhanced translations for reasoning effort options across multiple languages. - Adjusted model configurations to integrate new reasoning effort settings. * refactor(messageThunk): update multi-model dispatch logic for improved assistant handling - Changed the parameter from baseAssistantId to assistant object for better clarity. - Updated tasksToQueue structure to include assistant configuration instead of just ID and model. - Enhanced the creation of assistant messages to utilize the full assistant object. * chore(messageThunk): remove unused comment for clarity in multi-model response handling * fix(ThinkingButton): update quick panel title for reasoning effort settings * fix(GeminiProvider, OpenAIProvider): apply Math.floor to budget calculations for improved accuracy --------- Co-authored-by: ousugo Co-authored-by: Teo --- src/renderer/src/components/Icons/SVGIcon.tsx | 55 ++++++ src/renderer/src/config/models.ts | 125 ++++++++++--- src/renderer/src/i18n/locales/en-us.json | 19 +- src/renderer/src/i18n/locales/ja-jp.json | 19 +- src/renderer/src/i18n/locales/ru-ru.json | 22 ++- src/renderer/src/i18n/locales/zh-cn.json | 17 +- src/renderer/src/i18n/locales/zh-tw.json | 19 +- src/renderer/src/locales/zh/translation.json | 1 + .../src/pages/home/Inputbar/Inputbar.tsx | 18 +- .../pages/home/Inputbar/ThinkingButton.tsx | 171 ++++++++++++++++++ .../src/pages/home/Tabs/SettingsTab.tsx | 94 +--------- .../AssistantModelSettings.tsx | 30 +-- .../providers/AiProvider/AnthropicProvider.ts | 47 +++-- .../providers/AiProvider/GeminiProvider.ts | 40 ++-- .../providers/AiProvider/OpenAIProvider.ts | 110 ++++++----- src/renderer/src/types/index.ts | 12 +- 16 files changed, 534 insertions(+), 265 deletions(-) create mode 100644 src/renderer/src/locales/zh/translation.json create mode 100644 src/renderer/src/pages/home/Inputbar/ThinkingButton.tsx diff --git a/src/renderer/src/components/Icons/SVGIcon.tsx b/src/renderer/src/components/Icons/SVGIcon.tsx index e6521a97c7..d58eab7ee5 100644 --- a/src/renderer/src/components/Icons/SVGIcon.tsx +++ b/src/renderer/src/components/Icons/SVGIcon.tsx @@ -11,3 +11,58 @@ export const StreamlineGoodHealthAndWellBeing = (props: SVGProps) ) } + +export function MdiLightbulbOffOutline(props: SVGProps) { + return ( + + {/* Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE */} + + + ) +} + +export function MdiLightbulbAutoOutline(props: SVGProps) { + return ( + + {/* Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE */} + + + ) +} + +export function MdiLightbulbOn10(props: SVGProps) { + return ( + + {/* Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE */} + + + ) +} + +export function MdiLightbulbOn50(props: SVGProps) { + return ( + + {/* Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE */} + + + ) +} + +export function MdiLightbulbOn90(props: SVGProps) { + return ( + + {/* Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE */} + + + ) +} diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index c1ed7e84ed..e51a38ab31 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -210,6 +210,7 @@ export const FUNCTION_CALLING_MODELS = [ 'o1(?:-[\\w-]+)?', 'claude', 'qwen', + 'qwen3', 'hunyuan', 'deepseek', 'glm-4(?:-[\\w-]+)?', @@ -2218,30 +2219,40 @@ export function isVisionModel(model: Model): boolean { return VISION_REGEX.test(model.id) || model.type?.includes('vision') || false } -export function isOpenAIoSeries(model: Model): boolean { +export function isOpenAIReasoningModel(model: Model): boolean { return model.id.includes('o1') || model.id.includes('o3') || model.id.includes('o4') } +export function isSupportedReasoningEffortOpenAIModel(model: Model): boolean { + return ( + (model.id.includes('o1') && !(model.id.includes('o1-preview') || model.id.includes('o1-mini'))) || + model.id.includes('o3') || + model.id.includes('o4') + ) +} + export function isOpenAIWebSearch(model: Model): boolean { return model.id.includes('gpt-4o-search-preview') || model.id.includes('gpt-4o-mini-search-preview') } +export function isSupportedThinkingTokenModel(model?: Model): boolean { + if (!model) { + return false + } + + return ( + isSupportedThinkingTokenGeminiModel(model) || + isSupportedThinkingTokenQwenModel(model) || + isSupportedThinkingTokenClaudeModel(model) + ) +} + export function isSupportedReasoningEffortModel(model?: Model): boolean { if (!model) { return false } - if ( - model.id.includes('claude-3-7-sonnet') || - model.id.includes('claude-3.7-sonnet') || - isOpenAIoSeries(model) || - isGrokReasoningModel(model) || - isGemini25ReasoningModel(model) - ) { - return true - } - - return false + return isSupportedReasoningEffortOpenAIModel(model) || isSupportedReasoningEffortGrokModel(model) } export function isGrokModel(model?: Model): boolean { @@ -2263,7 +2274,9 @@ export function isGrokReasoningModel(model?: Model): boolean { return false } -export function isGemini25ReasoningModel(model?: Model): boolean { +export const isSupportedReasoningEffortGrokModel = isGrokReasoningModel + +export function isGeminiReasoningModel(model?: Model): boolean { if (!model) { return false } @@ -2275,6 +2288,51 @@ export function isGemini25ReasoningModel(model?: Model): boolean { return false } +export const isSupportedThinkingTokenGeminiModel = isGeminiReasoningModel + +export function isQwenReasoningModel(model?: Model): boolean { + if (!model) { + return false + } + + if (isSupportedThinkingTokenQwenModel(model)) { + return true + } + + if (model.id.includes('qwq') || model.id.includes('qvq')) { + return true + } + + return false +} + +export function isSupportedThinkingTokenQwenModel(model?: Model): boolean { + if (!model) { + return false + } + + return ( + model.id.includes('qwen3') || + [ + 'qwen-plus-latest', + 'qwen-plus-0428', + 'qwen-plus-2025-04-28', + 'qwen-turbo-latest', + 'qwen-turbo-0428', + 'qwen-turbo-2025-04-28' + ].includes(model.id) + ) +} + +export function isClaudeReasoningModel(model?: Model): boolean { + if (!model) { + return false + } + return model.id.includes('claude-3-7-sonnet') || model.id.includes('claude-3.7-sonnet') +} + +export const isSupportedThinkingTokenClaudeModel = isClaudeReasoningModel + export function isReasoningModel(model?: Model): boolean { if (!model) { return false @@ -2284,15 +2342,14 @@ export function isReasoningModel(model?: Model): boolean { return REASONING_REGEX.test(model.name) || model.type?.includes('reasoning') || false } - if (model.id.includes('claude-3-7-sonnet') || model.id.includes('claude-3.7-sonnet') || isOpenAIoSeries(model)) { - return true - } - - if (isGemini25ReasoningModel(model)) { - return true - } - - if (model.id.includes('glm-z1')) { + if ( + isClaudeReasoningModel(model) || + isOpenAIReasoningModel(model) || + isGeminiReasoningModel(model) || + isQwenReasoningModel(model) || + isGrokReasoningModel(model) || + model.id.includes('glm-z1') + ) { return true } @@ -2487,3 +2544,27 @@ export function groupQwenModels(models: Model[]): Record { {} as Record ) } + +export const THINKING_TOKEN_MAP: Record = { + // Gemini models + 'gemini-.*$': { min: 0, max: 24576 }, + + // Qwen models + 'qwen-plus-.*$': { min: 0, max: 38912 }, + 'qwen-turbo-.*$': { min: 0, max: 38912 }, + 'qwen3-0\\.6b$': { min: 0, max: 30720 }, + 'qwen3-1\\.7b$': { min: 0, max: 30720 }, + 'qwen3-.*$': { min: 0, max: 38912 }, + + // Claude models + 'claude-3[.-]7.*sonnet.*$': { min: 0, max: 64000 } +} + +export const findTokenLimit = (modelId: string): { min: number; max: number } | undefined => { + for (const [pattern, limits] of Object.entries(THINKING_TOKEN_MAP)) { + if (new RegExp(pattern).test(modelId)) { + return limits + } + } + return undefined +} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 8877c8de72..f0b33ba941 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -56,11 +56,11 @@ "settings.preset_messages": "Preset Messages", "settings.prompt": "Prompt Settings", "settings.reasoning_effort": "Reasoning effort", - "settings.reasoning_effort.high": "high", - "settings.reasoning_effort.low": "low", - "settings.reasoning_effort.medium": "medium", - "settings.reasoning_effort.off": "off", - "settings.reasoning_effort.tip": "Only supported by OpenAI o-series, Anthropic, and Grok reasoning models", + "settings.reasoning_effort.off": "Off", + "settings.reasoning_effort.high": "Think harder", + "settings.reasoning_effort.low": "Think less", + "settings.reasoning_effort.medium": "Think normally", + "settings.reasoning_effort.default": "Default", "settings.more": "Assistant Settings" }, "auth": { @@ -250,7 +250,14 @@ "input.upload.upload_from_local": "Upload local file...", "input.web_search.builtin": "Model Built-in", "input.web_search.builtin.enabled_content": "Use the built-in web search function of the model", - "input.web_search.builtin.disabled_content": "The current model does not support web search" + "input.web_search.builtin.disabled_content": "The current model does not support web search", + "input.thinking": "Thinking", + "input.thinking.mode.default": "Default", + "input.thinking.mode.default.tip": "The model will automatically determine the number of tokens to think", + "input.thinking.mode.custom": "Custom", + "input.thinking.mode.custom.tip": "The maximum number of tokens the model can think. Need to consider the context limit of the model, otherwise an error will be reported", + "input.thinking.mode.tokens.tip": "Set the number of thinking tokens to use.", + "input.thinking.budget_exceeds_max": "Thinking budget exceeds the maximum token number" }, "code_block": { "collapse": "Collapse", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 5264cecd30..737856f73a 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -56,11 +56,11 @@ "settings.preset_messages": "プリセットメッセージ", "settings.prompt": "プロンプト設定", "settings.reasoning_effort": "思考連鎖の長さ", - "settings.reasoning_effort.high": "長い", - "settings.reasoning_effort.low": "短い", - "settings.reasoning_effort.medium": "中程度", "settings.reasoning_effort.off": "オフ", - "settings.reasoning_effort.tip": "OpenAI o-series、Anthropic、および Grok の推論モデルのみサポート", + "settings.reasoning_effort.high": "最大限の思考", + "settings.reasoning_effort.low": "少しの思考", + "settings.reasoning_effort.medium": "普通の思考", + "settings.reasoning_effort.default": "デフォルト", "settings.more": "アシスタント設定" }, "auth": { @@ -250,7 +250,14 @@ "input.upload.upload_from_local": "ローカルファイルをアップロード...", "input.web_search.builtin": "モデル内蔵", "input.web_search.builtin.enabled_content": "モデル内蔵のウェブ検索機能を使用", - "input.web_search.builtin.disabled_content": "現在のモデルはウェブ検索をサポートしていません" + "input.web_search.builtin.disabled_content": "現在のモデルはウェブ検索をサポートしていません", + "input.thinking": "思考", + "input.thinking.mode.default": "デフォルト", + "input.thinking.mode.custom": "カスタム", + "input.thinking.mode.custom.tip": "モデルが最大で思考できるトークン数。モデルのコンテキスト制限を考慮する必要があります。そうしないとエラーが発生します", + "input.thinking.mode.default.tip": "モデルが自動的に思考のトークン数を決定します", + "input.thinking.mode.tokens.tip": "思考のトークン数を設定します", + "input.thinking.budget_exceeds_max": "思考予算が最大トークン数を超えました" }, "code_block": { "collapse": "折りたたむ", @@ -1508,7 +1515,7 @@ "title": "プライバシー設定", "enable_privacy_mode": "匿名エラーレポートとデータ統計の送信" }, - "input.show_translate_confirm": "[to be translated]:显示翻译确认对话框" + "input.show_translate_confirm": "翻訳確認ダイアログを表示" }, "translate": { "any.language": "任意の言語", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 68022d75ef..7a5e05947f 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -55,12 +55,11 @@ "settings.model": "Настройки модели", "settings.preset_messages": "Предустановленные сообщения", "settings.prompt": "Настройки промптов", - "settings.reasoning_effort": "Длина цепочки рассуждений", - "settings.reasoning_effort.high": "Длинная", - "settings.reasoning_effort.low": "Короткая", - "settings.reasoning_effort.medium": "Средняя", - "settings.reasoning_effort.off": "Выключено", - "settings.reasoning_effort.tip": "Поддерживается только моделями рассуждений OpenAI o-series, Anthropic и Grok", + "settings.reasoning_effort.off": "Выключить", + "settings.reasoning_effort.high": "Стараюсь думать", + "settings.reasoning_effort.low": "Меньше думать", + "settings.reasoning_effort.medium": "Среднее", + "settings.reasoning_effort.default": "По умолчанию", "settings.more": "Настройки ассистента" }, "auth": { @@ -250,7 +249,14 @@ "input.upload.upload_from_local": "Загрузить локальный файл...", "input.web_search.builtin": "Модель встроена", "input.web_search.builtin.enabled_content": "Используйте встроенную функцию веб-поиска модели", - "input.web_search.builtin.disabled_content": "Текущая модель не поддерживает веб-поиск" + "input.web_search.builtin.disabled_content": "Текущая модель не поддерживает веб-поиск", + "input.thinking": "Мыслим", + "input.thinking.mode.default": "По умолчанию", + "input.thinking.mode.default.tip": "Модель автоматически определяет количество токенов для размышления", + "input.thinking.mode.custom": "Пользовательский", + "input.thinking.mode.custom.tip": "Модель может максимально размышлять количество токенов. Необходимо учитывать ограничение контекста модели, иначе будет ошибка", + "input.thinking.mode.tokens.tip": "Установите количество токенов для размышления", + "input.thinking.budget_exceeds_max": "Бюджет размышления превышает максимальное количество токенов" }, "code_block": { "collapse": "Свернуть", @@ -1508,7 +1514,7 @@ "title": "Настройки приватности", "enable_privacy_mode": "Анонимная отправка отчетов об ошибках и статистики" }, - "input.show_translate_confirm": "[to be translated]:显示翻译确认对话框" + "input.show_translate_confirm": "Показать диалоговое окно подтверждения перевода" }, "translate": { "any.language": "Любой язык", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index ab91f38448..0e8161cf0e 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -56,11 +56,11 @@ "settings.preset_messages": "预设消息", "settings.prompt": "提示词设置", "settings.reasoning_effort": "思维链长度", - "settings.reasoning_effort.high": "长", - "settings.reasoning_effort.low": "短", - "settings.reasoning_effort.medium": "中", - "settings.reasoning_effort.off": "关", - "settings.reasoning_effort.tip": "仅支持 OpenAI o-series、Anthropic、Grok 推理模型", + "settings.reasoning_effort.off": "关闭", + "settings.reasoning_effort.low": "浮想", + "settings.reasoning_effort.medium": "斟酌", + "settings.reasoning_effort.high": "沉思", + "settings.reasoning_effort.default": "默认", "settings.more": "助手设置" }, "auth": { @@ -133,6 +133,13 @@ "input.translating": "翻译中...", "input.send": "发送", "input.settings": "设置", + "input.thinking": "思考", + "input.thinking.mode.default": "默认", + "input.thinking.mode.default.tip": "模型会自动确定思考的 token 数", + "input.thinking.mode.custom": "自定义", + "input.thinking.mode.custom.tip": "模型最多可以思考的 token 数。需要考虑模型的上下文限制,否则会报错", + "input.thinking.mode.tokens.tip": "设置思考的 token 数", + "input.thinking.budget_exceeds_max": "思考预算超过最大 token 数", "input.topics": " 话题 ", "input.translate": "翻译成{{target_language}}", "input.upload": "上传图片或文档", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index b2bf98d40d..63515cdd4c 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -56,11 +56,11 @@ "settings.preset_messages": "預設訊息", "settings.prompt": "提示詞設定", "settings.reasoning_effort": "思維鏈長度", - "settings.reasoning_effort.high": "長", - "settings.reasoning_effort.low": "短", - "settings.reasoning_effort.medium": "中", - "settings.reasoning_effort.off": "關", - "settings.reasoning_effort.tip": "僅支援 OpenAI o-series、Anthropic 和 Grok 推理模型", + "settings.reasoning_effort.off": "關閉", + "settings.reasoning_effort.high": "盡力思考", + "settings.reasoning_effort.low": "稍微思考", + "settings.reasoning_effort.medium": "正常思考", + "settings.reasoning_effort.default": "預設", "settings.more": "助手設定" }, "auth": { @@ -250,7 +250,14 @@ "input.upload.upload_from_local": "上傳本地文件...", "input.web_search.builtin": "模型內置", "input.web_search.builtin.enabled_content": "使用模型內置的網路搜尋功能", - "input.web_search.builtin.disabled_content": "當前模型不支持網路搜尋功能" + "input.web_search.builtin.disabled_content": "當前模型不支持網路搜尋功能", + "input.thinking": "思考", + "input.thinking.mode.default": "預設", + "input.thinking.mode.default.tip": "模型會自動確定思考的 token 數", + "input.thinking.mode.custom": "自定義", + "input.thinking.mode.custom.tip": "模型最多可以思考的 token 數。需要考慮模型的上下文限制,否則會報錯", + "input.thinking.mode.tokens.tip": "設置思考的 token 數", + "input.thinking.budget_exceeds_max": "思考預算超過最大 token 數" }, "code_block": { "collapse": "折疊", diff --git a/src/renderer/src/locales/zh/translation.json b/src/renderer/src/locales/zh/translation.json new file mode 100644 index 0000000000..0519ecba6e --- /dev/null +++ b/src/renderer/src/locales/zh/translation.json @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index af7aca9faf..da17d02bc1 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -1,7 +1,7 @@ import { HolderOutlined } from '@ant-design/icons' import { QuickPanelListItem, QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel' import TranslateButton from '@renderer/components/TranslateButton' -import { isGenerateImageModel, isVisionModel, isWebSearchModel } from '@renderer/config/models' +import { isGenerateImageModel, isReasoningModel, isVisionModel, isWebSearchModel } from '@renderer/config/models' import db from '@renderer/databases' import { useAssistant } from '@renderer/hooks/useAssistant' import { useKnowledgeBases } from '@renderer/hooks/useKnowledge' @@ -65,6 +65,7 @@ import MentionModelsInput from './MentionModelsInput' import NewContextButton from './NewContextButton' import QuickPhrasesButton, { QuickPhrasesButtonRef } from './QuickPhrasesButton' import SendMessageButton from './SendMessageButton' +import ThinkingButton, { ThinkingButtonRef } from './ThinkingButton' import TokenCount from './TokenCount' import WebSearchButton, { WebSearchButtonRef } from './WebSearchButton' @@ -130,7 +131,8 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const knowledgeBaseButtonRef = useRef(null) const mcpToolsButtonRef = useRef(null) const attachmentButtonRef = useRef(null) - const webSearchButtonRef = useRef(null) + const webSearchButtonRef = useRef(null) + const thinkingButtonRef = useRef(null) // eslint-disable-next-line react-hooks/exhaustive-deps const debouncedEstimate = useCallback( @@ -185,6 +187,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const uploadedFiles = await FileManager.uploadFiles(files) const baseUserMessage: MessageInputBaseParams = { assistant, topic, content: text } + console.log('baseUserMessage', baseUserMessage) // getUserMessage() if (uploadedFiles) { @@ -919,6 +922,14 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = setFiles={setFiles} ToolbarButton={ToolbarButton} /> + {isReasoningModel(model) && ( + + )} {showKnowledgeIcon && ( void +} + +interface Props { + ref?: React.RefObject + model: Model + assistant: Assistant + ToolbarButton: any +} + +// 模型类型到支持选项的映射表 +const MODEL_SUPPORTED_OPTIONS: Record = { + default: ['off', 'low', 'medium', 'high'], + grok: ['off', 'low', 'high'], + gemini: ['off', 'low', 'medium', 'high', 'auto'] +} + +// 选项转换映射表:当选项不支持时使用的替代选项 +const OPTION_FALLBACK: Record = { + off: 'off', + low: 'low', + medium: 'high', // medium -> high (for Grok models) + high: 'high', + auto: 'high' // auto -> high (for non-Gemini models) +} + +const ThinkingButton: FC = ({ ref, model, assistant, ToolbarButton }): ReactElement => { + const { t } = useTranslation() + const quickPanel = useQuickPanel() + const { updateAssistantSettings } = useAssistant(assistant.id) + + const isGrokModel = isSupportedReasoningEffortGrokModel(model) + const isGeminiModel = isSupportedThinkingTokenGeminiModel(model) + + const currentReasoningEffort = useMemo(() => { + return assistant.settings?.reasoning_effort || 'off' + }, [assistant.settings?.reasoning_effort]) + + // 确定当前模型支持的选项类型 + const modelType = useMemo(() => { + if (isGeminiModel) return 'gemini' + if (isGrokModel) return 'grok' + return 'default' + }, [isGeminiModel, isGrokModel]) + + // 获取当前模型支持的选项 + const supportedOptions = useMemo(() => { + return MODEL_SUPPORTED_OPTIONS[modelType] + }, [modelType]) + + // 检查当前设置是否与当前模型兼容 + useEffect(() => { + if (currentReasoningEffort && !supportedOptions.includes(currentReasoningEffort)) { + // 使用表中定义的替代选项 + const fallbackOption = OPTION_FALLBACK[currentReasoningEffort as ThinkingOption] + + updateAssistantSettings({ + reasoning_effort: fallbackOption === 'off' ? undefined : fallbackOption + }) + } + }, [currentReasoningEffort, supportedOptions, updateAssistantSettings, model.id]) + + const createThinkingIcon = useCallback((option?: ThinkingOption, isActive: boolean = false) => { + const iconColor = isActive ? 'var(--color-link)' : 'var(--color-icon)' + + switch (true) { + case option === 'low': + return + case option === 'medium': + return + case option === 'high': + return + case option === 'auto': + return + case option === 'off': + return + default: + return + } + }, []) + + const onThinkingChange = useCallback( + (option?: ThinkingOption) => { + const isEnabled = option !== undefined && option !== 'off' + // 然后更新设置 + if (!isEnabled) { + updateAssistantSettings({ + reasoning_effort: undefined + }) + return + } + updateAssistantSettings({ + reasoning_effort: option + }) + return + }, + [updateAssistantSettings] + ) + + const baseOptions = useMemo(() => { + // 使用表中定义的选项创建UI选项 + return supportedOptions.map((option) => ({ + level: option, + label: t(`assistants.settings.reasoning_effort.${option === 'auto' ? 'default' : option}`), + description: '', + icon: createThinkingIcon(option), + isSelected: currentReasoningEffort === option, + action: () => onThinkingChange(option) + })) + }, [t, createThinkingIcon, currentReasoningEffort, supportedOptions, onThinkingChange]) + + const panelItems = baseOptions + + const openQuickPanel = useCallback(() => { + quickPanel.open({ + title: t('assistants.settings.reasoning_effort'), + list: panelItems, + symbol: 'thinking' + }) + }, [quickPanel, panelItems, t]) + + const handleOpenQuickPanel = useCallback(() => { + if (quickPanel.isVisible && quickPanel.symbol === 'thinking') { + quickPanel.close() + } else { + openQuickPanel() + } + }, [openQuickPanel, quickPanel]) + + // 获取当前应显示的图标 + const getThinkingIcon = useCallback(() => { + // 如果当前选项不支持,显示回退选项的图标 + if (currentReasoningEffort && !supportedOptions.includes(currentReasoningEffort)) { + const fallbackOption = OPTION_FALLBACK[currentReasoningEffort as ThinkingOption] + return createThinkingIcon(fallbackOption, true) + } + return createThinkingIcon(currentReasoningEffort, currentReasoningEffort !== 'off') + }, [createThinkingIcon, currentReasoningEffort, supportedOptions]) + + useImperativeHandle(ref, () => ({ + openQuickPanel + })) + + return ( + + + {getThinkingIcon()} + + + ) +} + +export default ThinkingButton diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx index 0bef734c3c..9a21b1c808 100644 --- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx @@ -8,13 +8,11 @@ import { isMac, isWindows } from '@renderer/config/constant' -import { isGrokReasoningModel, isSupportedReasoningEffortModel } from '@renderer/config/models' import { codeThemes } from '@renderer/context/SyntaxHighlighterProvider' import { useAssistant } from '@renderer/hooks/useAssistant' import { useSettings } from '@renderer/hooks/useSettings' import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings' import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' -import { getDefaultModel } from '@renderer/services/AssistantService' import { useAppDispatch } from '@renderer/store' import { SendMessageShortcut, @@ -52,9 +50,9 @@ import { TranslateLanguageVarious } from '@renderer/types' import { modalConfirm } from '@renderer/utils' -import { Button, Col, InputNumber, Row, Segmented, Select, Slider, Switch, Tooltip } from 'antd' +import { Button, Col, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd' import { CircleHelp, RotateCcw, Settings2 } from 'lucide-react' -import { FC, useCallback, useEffect, useState } from 'react' +import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -72,7 +70,6 @@ const SettingsTab: FC = (props) => { const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0) const [fontSizeValue, setFontSizeValue] = useState(fontSize) const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true) - const [reasoningEffort, setReasoningEffort] = useState(assistant?.settings?.reasoning_effort) const { t } = useTranslation() const dispatch = useAppDispatch() @@ -127,17 +124,9 @@ const SettingsTab: FC = (props) => { } } - const onReasoningEffortChange = useCallback( - (value?: 'low' | 'medium' | 'high') => { - updateAssistantSettings({ reasoning_effort: value }) - }, - [updateAssistantSettings] - ) - const onReset = () => { setTemperature(DEFAULT_TEMPERATURE) setContextCount(DEFAULT_CONTEXTCOUNT) - setReasoningEffort(undefined) updateAssistant({ ...assistant, settings: { @@ -148,7 +137,6 @@ const SettingsTab: FC = (props) => { maxTokens: DEFAULT_MAX_TOKENS, streamOutput: true, hideMessages: false, - reasoning_effort: undefined, customParameters: [] } }) @@ -160,25 +148,8 @@ const SettingsTab: FC = (props) => { setEnableMaxTokens(assistant?.settings?.enableMaxTokens ?? false) setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS) setStreamOutput(assistant?.settings?.streamOutput ?? true) - setReasoningEffort(assistant?.settings?.reasoning_effort) }, [assistant]) - useEffect(() => { - // 当是Grok模型时,处理reasoning_effort的设置 - // For Grok models, only 'low' and 'high' reasoning efforts are supported. - // This ensures compatibility with the model's capabilities and avoids unsupported configurations. - if (isGrokReasoningModel(assistant?.model || getDefaultModel())) { - const currentEffort = assistant?.settings?.reasoning_effort - if (!currentEffort || currentEffort === 'low') { - setReasoningEffort('low') // Default to 'low' if no effort is set or if it's already 'low'. - onReasoningEffortChange('low') - } else if (currentEffort === 'medium' || currentEffort === 'high') { - setReasoningEffort('high') // Force 'high' for 'medium' or 'high' to simplify the configuration. - onReasoningEffortChange('high') - } - } - }, [assistant?.model, assistant?.settings?.reasoning_effort, onReasoningEffortChange]) - const formatSliderTooltip = (value?: number) => { if (value === undefined) return '' return value === 20 ? '∞' : value.toString() @@ -294,46 +265,6 @@ const SettingsTab: FC = (props) => { )} - {isSupportedReasoningEffortModel(assistant?.model || getDefaultModel()) && ( - <> - - - - - - - - - - - { - const typedValue = value === 'off' ? undefined : (value as 'low' | 'medium' | 'high') - setReasoningEffort(typedValue) - onReasoningEffortChange(typedValue) - }} - options={ - isGrokReasoningModel(assistant?.model || getDefaultModel()) - ? [ - { value: 'low', label: t('assistants.settings.reasoning_effort.low') }, - { value: 'high', label: t('assistants.settings.reasoning_effort.high') } - ] - : [ - { value: 'low', label: t('assistants.settings.reasoning_effort.low') }, - { value: 'medium', label: t('assistants.settings.reasoning_effort.medium') }, - { value: 'high', label: t('assistants.settings.reasoning_effort.high') }, - { value: 'off', label: t('assistants.settings.reasoning_effort.off') } - ] - } - name="group" - block - /> - - - - - )} {t('settings.messages.title')} @@ -706,27 +637,6 @@ export const SettingGroup = styled.div<{ theme?: ThemeMode }>` margin-bottom: 10px; ` -// Define the styled component with hover state styling -const SegmentedContainer = styled.div` - margin-top: 5px; - .ant-segmented-item { - font-size: 12px; - } - .ant-segmented-item-selected { - background-color: var(--color-primary) !important; - color: white !important; - } - - .ant-segmented-item:hover:not(.ant-segmented-item-selected) { - background-color: var(--color-primary-bg) !important; - color: var(--color-primary) !important; - } - - .ant-segmented-thumb { - background-color: var(--color-primary) !important; - } -` - const StyledSelect = styled(Select)` .ant-select-selector { border-radius: 15px !important; diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx index d7604ed79f..24b6025a12 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantModelSettings.tsx @@ -6,7 +6,7 @@ import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/cons import { SettingRow } from '@renderer/pages/settings' import { Assistant, AssistantSettingCustomParameters, AssistantSettings } from '@renderer/types' import { modalConfirm } from '@renderer/utils' -import { Button, Col, Divider, Input, InputNumber, Radio, Row, Select, Slider, Switch, Tooltip } from 'antd' +import { Button, Col, Divider, Input, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd' import { isNull } from 'lodash' import { FC, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -23,7 +23,6 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT) const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false) const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0) - const [reasoningEffort, setReasoningEffort] = useState(assistant?.settings?.reasoning_effort) const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true) const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel) const [topP, setTopP] = useState(assistant?.settings?.topP ?? 1) @@ -43,10 +42,6 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA } } - const onReasoningEffortChange = (value) => { - updateAssistantSettings({ reasoning_effort: value }) - } - const onContextCountChange = (value) => { if (!isNaN(value as number)) { updateAssistantSettings({ contextCount: value }) @@ -153,7 +148,6 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA setMaxTokens(0) setStreamOutput(true) setTopP(1) - setReasoningEffort(undefined) setCustomParameters([]) updateAssistantSettings({ temperature: DEFAULT_TEMPERATURE, @@ -162,7 +156,6 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA maxTokens: 0, streamOutput: true, topP: 1, - reasoning_effort: undefined, customParameters: [] }) } @@ -383,27 +376,6 @@ const AssistantModelSettings: FC = ({ assistant, updateAssistant, updateA /> - - - { - setReasoningEffort(e.target.value) - onReasoningEffortChange(e.target.value) - }}> - {t('assistants.settings.reasoning_effort.low')} - {t('assistants.settings.reasoning_effort.medium')} - {t('assistants.settings.reasoning_effort.high')} - {t('assistants.settings.reasoning_effort.off')} - - -