mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-29 05:51:26 +08:00
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.
This commit is contained in:
parent
69e9b9855e
commit
8747974359
@ -1,16 +1,17 @@
|
||||
import { isSupportedReasoningEffortGrokModel } from '@renderer/config/models'
|
||||
import { Assistant, Model } from '@renderer/types'
|
||||
import { Select } from 'antd'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { List } from 'antd'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { ReasoningEffortOptions } from './index'
|
||||
|
||||
interface ThinkingSelectProps {
|
||||
model: Model
|
||||
assistant: Assistant
|
||||
value?: ReasoningEffortOptions | null
|
||||
onChange?: (value: ReasoningEffortOptions) => void
|
||||
value: ReasoningEffortOptions
|
||||
onChange: (value: ReasoningEffortOptions) => void
|
||||
}
|
||||
|
||||
interface OptionType {
|
||||
@ -18,9 +19,8 @@ interface OptionType {
|
||||
value: ReasoningEffortOptions
|
||||
}
|
||||
|
||||
export default function ThinkingSelect({ model, assistant, value, onChange }: ThinkingSelectProps) {
|
||||
export default function ThinkingSelect({ model, value, onChange }: ThinkingSelectProps) {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const baseOptions = useMemo(
|
||||
() =>
|
||||
@ -41,24 +41,41 @@ export default function ThinkingSelect({ model, assistant, value, onChange }: Th
|
||||
[model, baseOptions]
|
||||
)
|
||||
|
||||
const currentValue = value ?? assistant.settings?.reasoning_effort ?? null
|
||||
|
||||
const handleChange = (newValue: ReasoningEffortOptions) => {
|
||||
onChange?.(newValue)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
placement="topRight"
|
||||
options={options}
|
||||
value={currentValue}
|
||||
onChange={handleChange}
|
||||
style={{ minWidth: 120 }}
|
||||
open={open}
|
||||
onDropdownVisibleChange={setOpen}
|
||||
/>
|
||||
</>
|
||||
<List
|
||||
dataSource={options}
|
||||
renderItem={(option) => (
|
||||
<StyledListItem $isSelected={value === option.value} onClick={() => onChange(option.value)}>
|
||||
<ReasoningEffortLabel>{option.label}</ReasoningEffortLabel>
|
||||
</StyledListItem>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ReasoningEffortLabel = styled.div`
|
||||
font-size: 16px;
|
||||
font-family: Ubuntu;
|
||||
`
|
||||
|
||||
const StyledListItem = styled(List.Item)<{ $isSelected: boolean }>`
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
margin: 4px 0;
|
||||
font-family: Ubuntu;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
transition: all 0.3s;
|
||||
background-color: ${(props) => (props.$isSelected ? 'var(--color-background-soft)' : 'transparent')};
|
||||
|
||||
.ant-list-item {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Col, InputNumber, Radio, Row, Slider, Space, Tooltip } from 'antd'
|
||||
import { Button, InputNumber, Slider, Tooltip } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { isSupportedThinkingTokenGeminiModel } from '../../config/models'
|
||||
|
||||
@ -14,11 +16,9 @@ interface ThinkingSliderProps {
|
||||
}
|
||||
|
||||
export default function ThinkingSlider({ model, value, min, max, onChange }: ThinkingSliderProps) {
|
||||
// 使用null表示"Default"模式,使用数字表示"Custom"模式
|
||||
const [mode, setMode] = useState<'default' | 'custom'>(value === null ? 'default' : 'custom')
|
||||
const [customValue, setCustomValue] = useState<number>(value === null ? 0 : value)
|
||||
|
||||
// 当外部value变化时更新内部状态
|
||||
const { t } = useTranslation()
|
||||
useEffect(() => {
|
||||
if (value === null) {
|
||||
setMode('default')
|
||||
@ -28,18 +28,15 @@ export default function ThinkingSlider({ model, value, min, max, onChange }: Thi
|
||||
}
|
||||
}, [value])
|
||||
|
||||
// 处理模式切换
|
||||
const handleModeChange = (e: any) => {
|
||||
const newMode = e.target.value
|
||||
const handleModeChange = (newMode: 'default' | 'custom') => {
|
||||
setMode(newMode)
|
||||
if (newMode === 'default') {
|
||||
onChange(null) // 传递null表示使用默认行为
|
||||
onChange(null)
|
||||
} else {
|
||||
onChange(customValue) // 传递当前自定义值
|
||||
onChange(customValue)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理自定义值变化
|
||||
const handleCustomValueChange = (newValue: number | null) => {
|
||||
if (newValue !== null) {
|
||||
setCustomValue(newValue)
|
||||
@ -48,45 +45,128 @@ export default function ThinkingSlider({ model, value, min, max, onChange }: Thi
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Container>
|
||||
{isSupportedThinkingTokenGeminiModel(model) && (
|
||||
<Radio.Group value={mode} onChange={handleModeChange}>
|
||||
<Radio.Button value="default">Default (Model's default behavior)</Radio.Button>
|
||||
<Radio.Button value="custom">Custom</Radio.Button>
|
||||
</Radio.Group>
|
||||
<ButtonGroup>
|
||||
<Tooltip title={t('chat.input.thinking.mode.default.tip')}>
|
||||
<ModeButton type={mode === 'default' ? 'primary' : 'text'} onClick={() => handleModeChange('default')}>
|
||||
{t('chat.input.thinking.mode.default')}
|
||||
</ModeButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('chat.input.thinking.mode.custom.tip')}>
|
||||
<ModeButton type={mode === 'custom' ? 'primary' : 'text'} onClick={() => handleModeChange('custom')}>
|
||||
{t('chat.input.thinking.mode.custom')}
|
||||
</ModeButton>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
|
||||
{mode === 'custom' && (
|
||||
<Row align="middle">
|
||||
<Col span={12}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Slider
|
||||
min={min}
|
||||
max={max}
|
||||
onChange={handleCustomValueChange}
|
||||
value={customValue}
|
||||
marks={{
|
||||
0: { label: '0' },
|
||||
[max]: { label: `${max.toLocaleString()}` }
|
||||
}}
|
||||
/>
|
||||
<Tooltip title="Set the number of thinking tokens to use. Set to 0 to explicitly use no thinking tokens.">
|
||||
<InfoCircleOutlined style={{ marginLeft: 8, color: 'var(--color-text-secondary)' }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<InputNumber
|
||||
<CustomControls>
|
||||
<SliderContainer>
|
||||
<Slider
|
||||
min={min}
|
||||
max={max}
|
||||
style={{ margin: '0 16px' }}
|
||||
value={customValue}
|
||||
onChange={handleCustomValueChange}
|
||||
addonAfter="tokens"
|
||||
tooltip={{ formatter: null }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<SliderMarks>
|
||||
<span>0</span>
|
||||
<span>{max.toLocaleString()}</span>
|
||||
</SliderMarks>
|
||||
</SliderContainer>
|
||||
|
||||
<InputContainer>
|
||||
<StyledInputNumber
|
||||
min={min}
|
||||
max={max}
|
||||
value={customValue}
|
||||
onChange={(value) => handleCustomValueChange(Number(value))}
|
||||
controls={false}
|
||||
/>
|
||||
<Tooltip title={t('chat.input.thinking.mode.tokens.tip')}>
|
||||
<InfoCircleOutlined style={{ color: 'var(--color-text-2)' }} />
|
||||
</Tooltip>
|
||||
</InputContainer>
|
||||
</CustomControls>
|
||||
)}
|
||||
</Space>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
min-width: 320px;
|
||||
padding: 4px;
|
||||
`
|
||||
|
||||
const ButtonGroup = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin-bottom: 4px;
|
||||
`
|
||||
|
||||
const ModeButton = styled(Button)`
|
||||
min-width: 90px;
|
||||
height: 28px;
|
||||
border-radius: 14px;
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
|
||||
&.ant-btn-primary {
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const CustomControls = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
`
|
||||
|
||||
const SliderContainer = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 180px;
|
||||
`
|
||||
|
||||
const SliderMarks = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: var(--color-text-2);
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
const InputContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const StyledInputNumber = styled(InputNumber)`
|
||||
width: 70px;
|
||||
|
||||
.ant-input-number-input {
|
||||
height: 28px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
`
|
||||
|
||||
@ -14,19 +14,17 @@ import ThinkingSlider from './ThinkingSlider'
|
||||
|
||||
const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> = {
|
||||
// Gemini models
|
||||
'^gemini-.*$': { min: 0, max: 24576 },
|
||||
'gemini-.*$': { min: 0, max: 24576 },
|
||||
|
||||
// Qwen models
|
||||
'^qwen-plus-.*$': { min: 0, max: 38912 },
|
||||
'^qwen-turbo-.*$': { min: 0, max: 38912 },
|
||||
'^qwq-.*$': { min: 0, max: 32768 },
|
||||
'^qvq-.*$': { min: 0, max: 16384 },
|
||||
'^qwen3-0\\.6b$': { min: 0, max: 30720 },
|
||||
'^qwen3-1\\.7b$': { min: 0, max: 30720 },
|
||||
'^qwen3-.*$': { min: 0, max: 38912 },
|
||||
'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.*sonnet$': { min: 0, max: 64000 }
|
||||
'claude-3[.-]7.*sonnet.*$': { min: 0, max: 64000 }
|
||||
}
|
||||
|
||||
export type ReasoningEffortOptions = 'low' | 'medium' | 'high'
|
||||
@ -67,10 +65,9 @@ export default function ThinkingPanel({ model, assistant }: ThinkingPanelProps)
|
||||
return currentThinkingBudget > maxTokens
|
||||
}, [currentThinkingBudget, maxTokens])
|
||||
|
||||
// 使用useEffect显示错误消息
|
||||
useEffect(() => {
|
||||
if (isBudgetExceedingMax && isSupportedThinkingTokenClaudeModel(model)) {
|
||||
window.message.error(t('chat.input.thinking_budget_exceeds_max'))
|
||||
window.message.error(t('chat.input.thinking.budget_exceeds_max'))
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isBudgetExceedingMax, model])
|
||||
@ -109,13 +106,11 @@ export default function ThinkingPanel({ model, assistant }: ThinkingPanelProps)
|
||||
}
|
||||
|
||||
if (isSupportedReasoningEffort) {
|
||||
const currentReasoningEffort = assistant.settings?.reasoning_effort
|
||||
|
||||
return (
|
||||
<ThinkingSelect
|
||||
model={model}
|
||||
assistant={assistant}
|
||||
value={currentReasoningEffort}
|
||||
model={model}
|
||||
value={assistant.settings?.reasoning_effort || 'medium'}
|
||||
onChange={onReasoningEffortChange}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -210,6 +210,7 @@ export const FUNCTION_CALLING_MODELS = [
|
||||
'o1(?:-[\\w-]+)?',
|
||||
'claude',
|
||||
'qwen',
|
||||
'qwen3',
|
||||
'hunyuan',
|
||||
'deepseek',
|
||||
'glm-4(?:-[\\w-]+)?',
|
||||
@ -2298,7 +2299,7 @@ export function isQwenReasoningModel(model?: Model): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
if (model.id.includes('qwq')) {
|
||||
if (model.id.includes('qwq') || model.id.includes('qvq')) {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -2312,8 +2313,6 @@ export function isSupportedThinkingTokenQwenModel(model?: Model): boolean {
|
||||
|
||||
return (
|
||||
model.id.includes('qwen3') ||
|
||||
model.id.includes('qwq') ||
|
||||
model.id.includes('qvq') ||
|
||||
[
|
||||
'qwen-plus-latest',
|
||||
'qwen-plus-0428',
|
||||
|
||||
@ -56,11 +56,9 @@
|
||||
"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.high": "Think harder",
|
||||
"settings.reasoning_effort.low": "Think less",
|
||||
"settings.reasoning_effort.medium": "Think normally",
|
||||
"settings.more": "Assistant Settings"
|
||||
},
|
||||
"auth": {
|
||||
@ -185,7 +183,6 @@
|
||||
"settings.top_p": "Top-P",
|
||||
"settings.top_p.tip": "Default value is 1, the smaller the value, the less variety in the answers, the easier to understand, the larger the value, the larger the range of the AI's vocabulary, the more diverse",
|
||||
"suggestions.title": "Suggested Questions",
|
||||
"thinking": "Thinking",
|
||||
"topics.auto_rename": "Auto Rename",
|
||||
"topics.clear.title": "Clear Messages",
|
||||
"topics.copy.image": "Copy as image",
|
||||
@ -250,7 +247,15 @@
|
||||
"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.",
|
||||
"thinking": "Thinking",
|
||||
"input.thinking.budget_exceeds_max": "Thinking budget exceeds the maximum token number"
|
||||
},
|
||||
"code_block": {
|
||||
"collapse": "Collapse",
|
||||
|
||||
@ -56,11 +56,9 @@
|
||||
"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.more": "アシスタント設定"
|
||||
},
|
||||
"auth": {
|
||||
@ -250,7 +248,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": "折りたたむ",
|
||||
@ -1507,7 +1512,7 @@
|
||||
"title": "プライバシー設定",
|
||||
"enable_privacy_mode": "匿名エラーレポートとデータ統計の送信"
|
||||
},
|
||||
"input.show_translate_confirm": "[to be translated]:显示翻译确认对话框"
|
||||
"input.show_translate_confirm": "翻訳確認ダイアログを表示"
|
||||
},
|
||||
"translate": {
|
||||
"any.language": "任意の言語",
|
||||
|
||||
@ -55,12 +55,9 @@
|
||||
"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.high": "Стараюсь думать",
|
||||
"settings.reasoning_effort.low": "Меньше думать",
|
||||
"settings.reasoning_effort.medium": "Среднее",
|
||||
"settings.more": "Настройки ассистента"
|
||||
},
|
||||
"auth": {
|
||||
@ -250,7 +247,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": "Свернуть",
|
||||
@ -1507,7 +1511,7 @@
|
||||
"title": "Настройки приватности",
|
||||
"enable_privacy_mode": "Анонимная отправка отчетов об ошибках и статистики"
|
||||
},
|
||||
"input.show_translate_confirm": "[to be translated]:显示翻译确认对话框"
|
||||
"input.show_translate_confirm": "Показать диалоговое окно подтверждения перевода"
|
||||
},
|
||||
"translate": {
|
||||
"any.language": "Любой язык",
|
||||
|
||||
@ -56,11 +56,9 @@
|
||||
"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.low": "稍微思考",
|
||||
"settings.reasoning_effort.medium": "正常思考",
|
||||
"settings.reasoning_effort.high": "尽力思考",
|
||||
"settings.more": "助手设置"
|
||||
},
|
||||
"auth": {
|
||||
@ -133,6 +131,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": "上传图片或文档",
|
||||
|
||||
@ -56,11 +56,9 @@
|
||||
"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.more": "助手設定"
|
||||
},
|
||||
"auth": {
|
||||
@ -250,7 +248,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": "折疊",
|
||||
|
||||
@ -799,13 +799,17 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
const newEnableThinking = !assistant.enableThinking
|
||||
updateAssistant({ ...assistant, enableThinking: newEnableThinking })
|
||||
|
||||
if (newEnableThinking && (isSupportedThinkingToken || isSupportedReasoningEffort)) {
|
||||
setThinkingPanelVisible(true)
|
||||
} else {
|
||||
if (!newEnableThinking) {
|
||||
setThinkingPanelVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onTogglePanel = () => {
|
||||
if (isSupportedThinkingToken || isSupportedReasoningEffort) {
|
||||
setThinkingPanelVisible((prev) => !prev)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWebSearchModel(model) && assistant.enableWebSearch) {
|
||||
updateAssistant({ ...assistant, enableWebSearch: false })
|
||||
@ -952,6 +956,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
assistant={assistant}
|
||||
ToolbarButton={ToolbarButton}
|
||||
onToggleThinking={onToggleThinking}
|
||||
onTogglePanel={onTogglePanel}
|
||||
showPanel={thinkingPanelVisible}
|
||||
/>
|
||||
</ThinkingButtonContainer>
|
||||
<WebSearchButton ref={webSearchButtonRef} assistant={assistant} ToolbarButton={ToolbarButton} />
|
||||
@ -1139,7 +1145,8 @@ const ToolbarButton = styled(Button)`
|
||||
&.active {
|
||||
background-color: var(--color-primary) !important;
|
||||
.anticon,
|
||||
.iconfont {
|
||||
.iconfont,
|
||||
.chevron-icon {
|
||||
color: var(--color-white-soft);
|
||||
}
|
||||
&:hover {
|
||||
@ -1155,31 +1162,18 @@ const ThinkingPanelContainer = styled.div`
|
||||
transform: translateX(-50%);
|
||||
background-color: var(--color-background);
|
||||
border: 0.5px solid var(--color-border);
|
||||
border-radius: 8px 8px 0 0;
|
||||
border-radius: 15px;
|
||||
padding: 10px;
|
||||
margin-bottom: 5px;
|
||||
z-index: 10;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
|
||||
min-width: 250px;
|
||||
|
||||
/* Add a small arrow pointing down to the ThinkingButton */
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 5px solid var(--color-border);
|
||||
}
|
||||
`
|
||||
|
||||
const ThinkingButtonContainer = styled.div`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export default Inputbar
|
||||
|
||||
@ -1,31 +1,72 @@
|
||||
import { isReasoningModel } from '@renderer/config/models'
|
||||
import {
|
||||
isReasoningModel,
|
||||
isSupportedReasoningEffortModel,
|
||||
isSupportedThinkingTokenModel
|
||||
} from '@renderer/config/models'
|
||||
import { Assistant, Model } from '@renderer/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { Brain } from 'lucide-react'
|
||||
import { Atom, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
model: Model
|
||||
assistant: Assistant
|
||||
ToolbarButton: any
|
||||
onToggleThinking: () => void
|
||||
onTogglePanel: () => void
|
||||
showPanel: boolean
|
||||
}
|
||||
|
||||
const ThinkingButton: FC<Props> = ({ model, assistant, ToolbarButton, onToggleThinking }) => {
|
||||
const ThinkingButton: FC<Props> = ({ model, assistant, ToolbarButton, onToggleThinking, onTogglePanel, showPanel }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!isReasoningModel(model)) {
|
||||
return null
|
||||
}
|
||||
const isSupportedThinkingToken = isSupportedThinkingTokenModel(model)
|
||||
const isSupportedReasoningEffort = isSupportedReasoningEffortModel(model)
|
||||
|
||||
return (
|
||||
<Tooltip placement="top" title={t('chat.input.thinking')} arrow>
|
||||
<ToolbarButton type="text" disabled={!isReasoningModel(model)} onClick={onToggleThinking}>
|
||||
<Brain size={18} color={assistant.enableThinking ? 'var(--color-link)' : 'var(--color-icon)'} />
|
||||
</ToolbarButton>
|
||||
<ButtonContainer>
|
||||
<ToolbarButton type="text" disabled={!isReasoningModel(model)} onClick={onToggleThinking}>
|
||||
<Atom size={18} color={assistant.enableThinking ? 'var(--color-link)' : 'var(--color-icon)'} />
|
||||
</ToolbarButton>
|
||||
<ChevronButton onClick={onTogglePanel} disabled={!isSupportedThinkingToken && !isSupportedReasoningEffort}>
|
||||
{showPanel ? (
|
||||
<ChevronUp size={18} color={assistant.enableThinking ? 'var(--color-link)' : 'var(--color-icon)'} />
|
||||
) : (
|
||||
<ChevronDown size={18} color={assistant.enableThinking ? 'var(--color-link)' : 'var(--color-icon)'} />
|
||||
)}
|
||||
</ChevronButton>
|
||||
</ButtonContainer>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@ -23,8 +23,6 @@ import OpenAI from 'openai'
|
||||
import { CompletionsParams } from '.'
|
||||
import BaseProvider from './BaseProvider'
|
||||
|
||||
type ReasoningEffort = 'high' | 'medium' | 'low'
|
||||
|
||||
interface ReasoningConfig {
|
||||
type: 'enabled' | 'disabled'
|
||||
budget_tokens?: number
|
||||
@ -124,32 +122,17 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
* @param model - The model
|
||||
* @returns The reasoning effort
|
||||
*/
|
||||
private getReasoningEffort(assistant: Assistant, model: Model): ReasoningConfig | undefined {
|
||||
private getBudgetToken(assistant: Assistant, model: Model): ReasoningConfig | undefined {
|
||||
if (!isReasoningModel(model)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const effortRatios: Record<ReasoningEffort, number> = {
|
||||
high: 0.8,
|
||||
medium: 0.5,
|
||||
low: 0.2
|
||||
}
|
||||
|
||||
const effort = assistant?.settings?.reasoning_effort as ReasoningEffort
|
||||
const effortRatio = effortRatios[effort]
|
||||
|
||||
if (!effortRatio) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const isClaude37Sonnet = model.id.includes('claude-3-7-sonnet') || model.id.includes('claude-3.7-sonnet')
|
||||
|
||||
if (!isClaude37Sonnet) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const maxTokens = assistant?.settings?.maxTokens || DEFAULT_MAX_TOKENS
|
||||
const budgetTokens = Math.trunc(Math.max(Math.min(maxTokens * effortRatio, 32000), 1024))
|
||||
const budgetTokens = assistant?.settings?.thinking_budget || maxTokens
|
||||
|
||||
if (budgetTokens > maxTokens) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'enabled',
|
||||
@ -200,7 +183,7 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
top_p: this.getTopP(assistant, model),
|
||||
system: systemPrompt,
|
||||
// @ts-ignore thinking
|
||||
thinking: this.getReasoningEffort(assistant, model),
|
||||
thinking: this.getBudgetToken(assistant, model),
|
||||
...this.getCustomParameters(assistant)
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import {
|
||||
getOpenAIWebSearchParams,
|
||||
isGrokReasoningModel,
|
||||
isHunyuanSearchModel,
|
||||
isOpenAIWebSearch,
|
||||
isReasoningModel,
|
||||
isSupportedModel,
|
||||
isSupportedReasoningEffortGrokModel,
|
||||
isSupportedReasoningEffortModel,
|
||||
isSupportedReasoningEffortOpenAIModel,
|
||||
isSupportedThinkingTokenClaudeModel,
|
||||
@ -264,11 +264,15 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
if (model.provider === 'openrouter') {
|
||||
if (isSupportedReasoningEffortModel(model)) {
|
||||
return {
|
||||
reasoning_effort: assistant?.settings?.reasoning_effort
|
||||
reasoning: {
|
||||
effort: assistant?.settings?.reasoning_effort
|
||||
}
|
||||
}
|
||||
} else if (isSupportedThinkingTokenModel(model)) {
|
||||
return {
|
||||
max_tokens: assistant?.settings?.thinking_budget
|
||||
reasoning: {
|
||||
max_tokens: assistant?.settings?.thinking_budget
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -286,23 +290,39 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
}
|
||||
}
|
||||
|
||||
if (isGrokReasoningModel(model)) {
|
||||
return {
|
||||
reasoning_effort: assistant?.settings?.reasoning_effort
|
||||
if (isSupportedReasoningEffortGrokModel(model)) {
|
||||
if (enableThinking) {
|
||||
return {
|
||||
reasoning_effort: assistant?.settings?.reasoning_effort
|
||||
}
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSupportedReasoningEffortOpenAIModel(model)) {
|
||||
return {
|
||||
reasoning_effort: assistant?.settings?.reasoning_effort
|
||||
if (enableThinking) {
|
||||
return {
|
||||
reasoning_effort: assistant?.settings?.reasoning_effort
|
||||
}
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSupportedThinkingTokenClaudeModel(model)) {
|
||||
return {
|
||||
thinking: {
|
||||
type: 'enabled',
|
||||
budget_tokens: assistant?.settings?.thinking_budget
|
||||
if (enableThinking) {
|
||||
return {
|
||||
thinking: {
|
||||
type: 'enabled',
|
||||
budget_tokens: assistant?.settings?.thinking_budget
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
thinking: {
|
||||
type: 'disabled'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user