diff --git a/src/renderer/src/components/Icons/SVGIcon.tsx b/src/renderer/src/components/Icons/SVGIcon.tsx index e6521a97c7..d57b41bc83 100644 --- a/src/renderer/src/components/Icons/SVGIcon.tsx +++ b/src/renderer/src/components/Icons/SVGIcon.tsx @@ -11,3 +11,47 @@ 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 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/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 22c9391fbb..435f1aedf6 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -56,6 +56,7 @@ "settings.preset_messages": "Preset Messages", "settings.prompt": "Prompt Settings", "settings.reasoning_effort": "Reasoning effort", + "settings.reasoning_effort.off": "Off", "settings.reasoning_effort.high": "Think harder", "settings.reasoning_effort.low": "Think less", "settings.reasoning_effort.medium": "Think normally", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index bc15a32b7d..ef20c7b7b2 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -56,6 +56,7 @@ "settings.preset_messages": "プリセットメッセージ", "settings.prompt": "プロンプト設定", "settings.reasoning_effort": "思考連鎖の長さ", + "settings.reasoning_effort.off": "オフ", "settings.reasoning_effort.high": "最大限の思考", "settings.reasoning_effort.low": "少しの思考", "settings.reasoning_effort.medium": "普通の思考", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index bb9e43a950..3a49541db2 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -55,6 +55,7 @@ "settings.model": "Настройки модели", "settings.preset_messages": "Предустановленные сообщения", "settings.prompt": "Настройки промптов", + "settings.reasoning_effort.off": "Выключить", "settings.reasoning_effort.high": "Стараюсь думать", "settings.reasoning_effort.low": "Меньше думать", "settings.reasoning_effort.medium": "Среднее", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index cf788b540c..214502c2dd 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -56,9 +56,10 @@ "settings.preset_messages": "预设消息", "settings.prompt": "提示词设置", "settings.reasoning_effort": "思维链长度", - "settings.reasoning_effort.low": "稍微思考", - "settings.reasoning_effort.medium": "正常思考", - "settings.reasoning_effort.high": "尽力思考", + "settings.reasoning_effort.off": "关闭", + "settings.reasoning_effort.low": "浮想", + "settings.reasoning_effort.medium": "斟酌", + "settings.reasoning_effort.high": "沉思", "settings.more": "助手设置" }, "auth": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index a11d29b0ff..6d159409a6 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -56,6 +56,7 @@ "settings.preset_messages": "預設訊息", "settings.prompt": "提示詞設定", "settings.reasoning_effort": "思維鏈長度", + "settings.reasoning_effort.off": "關閉", "settings.reasoning_effort.high": "盡力思考", "settings.reasoning_effort.low": "稍微思考", "settings.reasoning_effort.medium": "正常思考", diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index c296ebcb15..1305b815c3 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -1,13 +1,7 @@ import { HolderOutlined } from '@ant-design/icons' import { QuickPanelListItem, QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel' -import ThinkingPanel from '@renderer/components/ThinkingPanel' import TranslateButton from '@renderer/components/TranslateButton' -import { - isGenerateImageModel, - isSupportedReasoningEffortModel, - 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' @@ -59,7 +53,6 @@ import React, { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useS import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import { isSupportedThinkingTokenModel } from '../../../config/models' import NarrowLayout from '../Messages/NarrowLayout' import AttachmentButton, { AttachmentButtonRef } from './AttachmentButton' import AttachmentPreview from './AttachmentPreview' @@ -72,7 +65,7 @@ import MentionModelsInput from './MentionModelsInput' import NewContextButton from './NewContextButton' import QuickPhrasesButton, { QuickPhrasesButtonRef } from './QuickPhrasesButton' import SendMessageButton from './SendMessageButton' -import ThinkingButton from './ThinkingButton' +import ThinkingButton, { ThinkingButtonRef } from './ThinkingButton' import TokenCount from './TokenCount' import WebSearchButton, { WebSearchButtonRef } from './WebSearchButton' @@ -118,7 +111,6 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState([]) const [mentionModels, setMentionModels] = useState([]) const [isDragging, setIsDragging] = useState(false) - const [thinkingPanelVisible, setThinkingPanelVisible] = useState(false) const [textareaHeight, setTextareaHeight] = useState() const startDragY = useRef(0) const startHeight = useRef(0) @@ -139,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( @@ -770,9 +763,6 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1 - const isSupportedReasoningEffort = isSupportedReasoningEffortModel(model) - const isSupportedThinkingToken = isSupportedThinkingTokenModel(model) - const handleKnowledgeBaseSelect = (bases?: KnowledgeBase[]) => { updateAssistant({ ...assistant, knowledge_bases: bases }) setSelectedKnowledgeBases(bases ?? []) @@ -795,21 +785,6 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = updateAssistant({ ...assistant, enableGenerateImage: !assistant.enableGenerateImage }) } - const onToggleThinking = () => { - const newEnableThinking = !assistant.enableThinking - updateAssistant({ ...assistant, enableThinking: newEnableThinking }) - - if (!newEnableThinking) { - setThinkingPanelVisible(false) - } - } - - const onTogglePanel = () => { - if (isSupportedThinkingToken || isSupportedReasoningEffort) { - setThinkingPanelVisible((prev) => !prev) - } - } - useEffect(() => { if (!isWebSearchModel(model) && assistant.enableWebSearch) { updateAssistant({ ...assistant, enableWebSearch: false }) @@ -945,21 +920,14 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = setFiles={setFiles} ToolbarButton={ToolbarButton} /> - - {thinkingPanelVisible && ( - - - - )} + {isReasoningModel(model) && ( - + )} {showKnowledgeIcon && ( = { + // 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 } +} + +// Helper function to find matching token limit +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 +} + +// 根据模型和选择的思考档位计算thinking_budget值 +const calculateThinkingBudget = (model: Model, option: ReasoningEffortOptions | null): number | undefined => { + if (!option || !isSupportedThinkingTokenModel(model)) { + return undefined + } + + const tokenLimits = findTokenLimit(model.id) + if (!tokenLimits) return undefined + + const { min, max } = tokenLimits + + switch (option) { + case 'low': + return Math.floor(min + (max - min) * 0.25) + case 'medium': + return Math.floor(min + (max - min) * 0.5) + case 'high': + return Math.floor(min + (max - min) * 0.75) + default: + return undefined + } +} + +export interface ThinkingButtonRef { + openQuickPanel: () => void +} interface Props { + ref?: React.RefObject model: Model assistant: Assistant ToolbarButton: any - onToggleThinking: () => void - onTogglePanel: () => void - showPanel: boolean } -const ThinkingButton: FC = ({ model, assistant, ToolbarButton, onToggleThinking, onTogglePanel, showPanel }) => { +const ThinkingButton: FC = ({ ref, model, assistant, ToolbarButton }): ReactElement => { const { t } = useTranslation() + const quickPanel = useQuickPanel() + const { updateAssistantSettings } = useAssistant(assistant.id) + + const supportedThinkingToken = isSupportedThinkingTokenModel(model) + const supportedReasoningEffort = isSupportedReasoningEffortModel(model) + const isGrokModel = isSupportedReasoningEffortGrokModel(model) + + // 根据thinking_budget逆推思考档位 + const inferReasoningEffortFromBudget = useCallback( + (model: Model, budget: number | undefined): ReasoningEffortOptions | null => { + if (!budget || !supportedThinkingToken) return null + + const tokenLimits = findTokenLimit(model.id) + if (!tokenLimits) return null + + const { min, max } = tokenLimits + const range = max - min + + // 计算预算在范围内的百分比 + const normalizedBudget = (budget - min) / range + + // 根据百分比确定档位 + if (normalizedBudget <= 0.33) return 'low' + if (normalizedBudget <= 0.66) return 'medium' + return 'high' + }, + [supportedThinkingToken] + ) + + const currentReasoningEffort = useMemo(() => { + // 优先使用显式设置的reasoning_effort + if (assistant.settings?.reasoning_effort) { + return assistant.settings.reasoning_effort + } + + // 如果有thinking_budget但没有reasoning_effort,则推导档位 + if (assistant.settings?.thinking_budget) { + return inferReasoningEffortFromBudget(model, assistant.settings.thinking_budget) + } - if (!isReasoningModel(model)) { return null - } - const isSupportedThinkingToken = isSupportedThinkingTokenModel(model) - const isSupportedReasoningEffort = isSupportedReasoningEffortModel(model) + }, [assistant.settings?.reasoning_effort, assistant.settings?.thinking_budget, inferReasoningEffortFromBudget, model]) + + const createThinkingIcon = useCallback((option: ReasoningEffortOptions | null, 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 + default: + return + } + }, []) + + const onThinkingChange = useCallback( + (option: ReasoningEffortOptions | null) => { + if (!option) { + // 禁用思考 + updateAssistantSettings({ + reasoning_effort: undefined, + thinking_budget: undefined + }) + return + } + + // 启用思考 + if (supportedReasoningEffort) { + updateAssistantSettings({ + reasoning_effort: option + }) + } + + if (supportedThinkingToken) { + const budget = calculateThinkingBudget(model, option) + updateAssistantSettings({ + reasoning_effort: option, + thinking_budget: budget + }) + } + }, + [model, supportedReasoningEffort, supportedThinkingToken, updateAssistantSettings] + ) + + const baseOptions = useMemo( + () => [ + { + level: null, + label: t('assistants.settings.reasoning_effort.off'), + description: '', + icon: createThinkingIcon(null), + isSelected: currentReasoningEffort === null, + action: () => onThinkingChange(null) + }, + { + level: 'low', + label: t('assistants.settings.reasoning_effort.low'), + description: '', + icon: createThinkingIcon('low'), + isSelected: currentReasoningEffort === 'low', + action: () => onThinkingChange('low') + }, + { + level: 'medium', + label: t('assistants.settings.reasoning_effort.medium'), + description: '', + icon: createThinkingIcon('medium'), + isSelected: currentReasoningEffort === 'medium', + action: () => onThinkingChange('medium') + }, + { + level: 'high', + label: t('assistants.settings.reasoning_effort.high'), + description: '', + icon: createThinkingIcon('high'), + isSelected: currentReasoningEffort === 'high', + action: () => onThinkingChange('high') + } + ], + [currentReasoningEffort, onThinkingChange, t, createThinkingIcon] + ) + + const panelItems = useMemo(() => { + return isGrokModel ? baseOptions.filter((option) => option.level === 'low' || option.level === 'high') : baseOptions + }, [baseOptions, isGrokModel]) + + const openQuickPanel = useCallback(() => { + quickPanel.open({ + title: t('chat.input.thinking'), + 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(() => { + return createThinkingIcon(currentReasoningEffort, currentReasoningEffort !== null) + }, [createThinkingIcon, currentReasoningEffort]) + + useImperativeHandle(ref, () => ({ + openQuickPanel + })) return ( - - - - - - - {showPanel ? ( - - ) : ( - - )} - - + + + {getThinkingIcon()} + ) } -const ButtonContainer = styled.div` - display: flex; - align-items: center; - gap: 0px; -` - -const ChevronButton = styled.button` - background: none; - border: none; - padding: 2px; - cursor: pointer; - color: var(--color-icon); - display: flex; - align-items: center; - justify-content: center; - margin-left: -8px; - transition: all 0.3s; - - &:hover { - color: var(--color-text-1); - } -` - export default ThinkingButton