From bc3f76dd732cb70cecbf728de94f654a359c8590 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 9 Jul 2026 12:17:40 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(ai-chat):=20=E6=80=9D=E8=80=83?= =?UTF-8?q?=E5=BC=BA=E5=BA=A6=E7=A7=BB=E5=85=A5=E8=81=8A=E5=A4=A9=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E5=B9=B6=E6=8C=89=E6=AC=A1=E8=AF=B7=E6=B1=82=E4=B8=8B?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 面板底部新增思考强度选择,按服务商 profile 动态选项 - ChatSendOptions 支持 thinkingIntensity,仅本次发送生效 - 从供应商表单移除思考强度配置,避免与面板控制重复 - 补齐多语言文案与相关前后端测试 --- .../components/ai/AIChatInput.i18n.test.tsx | 4 + .../components/ai/AIChatInput.notice.test.tsx | 10 +++ frontend/src/components/ai/AIChatInput.tsx | 48 +++++++++--- .../ai/AIChatThinkingIntensitySelect.tsx | 74 +++++++++++++++++++ .../ai/AISettingsProvidersSection.tsx | 60 --------------- .../components/ai/aiChatPayloadDispatch.ts | 57 ++++++++++++++ frontend/src/utils/aiProviderEditorState.ts | 1 - frontend/wailsjs/go/aiservice/Service.d.ts | 2 + frontend/wailsjs/go/aiservice/Service.js | 4 + frontend/wailsjs/go/models.ts | 2 + internal/ai/service/service.go | 13 +++- internal/ai/service/service_test.go | 19 +++++ internal/ai/types.go | 7 +- shared/i18n/de-DE.json | 16 +++- shared/i18n/en-US.json | 16 +++- shared/i18n/ja-JP.json | 16 +++- shared/i18n/ru-RU.json | 16 +++- shared/i18n/zh-CN.json | 16 +++- shared/i18n/zh-TW.json | 16 +++- 19 files changed, 314 insertions(+), 83 deletions(-) create mode 100644 frontend/src/components/ai/AIChatThinkingIntensitySelect.tsx diff --git a/frontend/src/components/ai/AIChatInput.i18n.test.tsx b/frontend/src/components/ai/AIChatInput.i18n.test.tsx index 14679407..6a5779af 100644 --- a/frontend/src/components/ai/AIChatInput.i18n.test.tsx +++ b/frontend/src/components/ai/AIChatInput.i18n.test.tsx @@ -95,6 +95,8 @@ const renderAIChatInput = ( onComposerAction={() => {}} onModelChange={() => {}} onFetchModels={() => {}} + thinkingIntensity="medium" + onThinkingIntensityChange={() => {}} textareaRef={React.createRef()} darkMode={false} textColor="#162033" @@ -128,6 +130,8 @@ const renderAIChatInputWithoutProvider = ( onComposerAction={() => {}} onModelChange={() => {}} onFetchModels={() => {}} + thinkingIntensity="medium" + onThinkingIntensityChange={() => {}} textareaRef={React.createRef()} darkMode={false} textColor="#162033" diff --git a/frontend/src/components/ai/AIChatInput.notice.test.tsx b/frontend/src/components/ai/AIChatInput.notice.test.tsx index 89ba5fc8..d798e352 100644 --- a/frontend/src/components/ai/AIChatInput.notice.test.tsx +++ b/frontend/src/components/ai/AIChatInput.notice.test.tsx @@ -70,6 +70,8 @@ const buildAIChatInput = (overrides: Partial {}} onModelChange={() => {}} onFetchModels={() => {}} + thinkingIntensity="medium" + onThinkingIntensityChange={() => {}} textareaRef={React.createRef()} darkMode={false} textColor="#162033" @@ -113,6 +115,8 @@ describe('AIChatInput notice layout', () => { onComposerAction={() => {}} onModelChange={() => {}} onFetchModels={() => {}} + thinkingIntensity="medium" + onThinkingIntensityChange={() => {}} textareaRef={React.createRef()} darkMode={false} textColor="#162033" @@ -152,6 +156,8 @@ describe('AIChatInput notice layout', () => { onComposerAction={() => {}} onModelChange={() => {}} onFetchModels={() => {}} + thinkingIntensity="medium" + onThinkingIntensityChange={() => {}} textareaRef={React.createRef()} darkMode={false} textColor="#162033" @@ -185,6 +191,8 @@ describe('AIChatInput notice layout', () => { onComposerAction={() => {}} onModelChange={() => {}} onFetchModels={() => {}} + thinkingIntensity="medium" + onThinkingIntensityChange={() => {}} textareaRef={React.createRef()} darkMode={false} textColor="#162033" @@ -195,6 +203,8 @@ describe('AIChatInput notice layout', () => { ); expect(markup).toContain('gn-v2-ai-model-select'); + expect(markup).toContain('gn-v2-ai-thinking-select'); + expect(markup).toContain('gn-v2-ai-model-bar'); expect(markup).not.toContain('ant-select-filled'); expect(markup).not.toContain('ant-select-show-search'); }); diff --git a/frontend/src/components/ai/AIChatInput.tsx b/frontend/src/components/ai/AIChatInput.tsx index 47ef546b..453eb705 100644 --- a/frontend/src/components/ai/AIChatInput.tsx +++ b/frontend/src/components/ai/AIChatInput.tsx @@ -15,7 +15,9 @@ import AIChatComposerStatus from './AIChatComposerStatus'; import AIChatComposerActions from './AIChatComposerActions'; import AIChatAttachmentStrip from './AIChatAttachmentStrip'; import AIChatContextPreview from './AIChatContextPreview'; +import { noAutoCapInputProps } from '../../utils/inputAutoCap'; import AIChatProviderModelSelect from './AIChatProviderModelSelect'; +import AIChatThinkingIntensitySelect from './AIChatThinkingIntensitySelect'; import { buildAIChatReadinessSnapshot } from './aiChatReadiness'; import { useAIChatContextBinding } from './useAIChatContextBinding'; import { useAIChatDraftAttachments } from './useAIChatDraftAttachments'; @@ -42,6 +44,8 @@ interface AIChatInputProps { onComposerAction?: (actionKey: AIComposerNoticeAction) => void; onModelChange: (val: string) => void; onFetchModels: () => void; + thinkingIntensity: string; + onThinkingIntensityChange: (val: string) => void; textareaRef: React.RefObject; darkMode: boolean; textColor: string; @@ -56,7 +60,8 @@ export const AIChatInput: React.FC = ({ input, setInput, draftAttachments, setDraftAttachments, sending, onSend, onStop, handleKeyDown, activeConnName, activeContext, activeProvider, dynamicModels, loadingModels, sendShortcutBinding, shortcutPlatform = 'windows', composerNotice, onComposerAction, - onModelChange, onFetchModels, textareaRef, darkMode, textColor, mutedColor, overlayTheme, + onModelChange, onFetchModels, thinkingIntensity, onThinkingIntensityChange, + textareaRef, darkMode, textColor, mutedColor, overlayTheme, contextUsageChars, maxContextChars, isV2Ui = false }) => { const i18n = useOptionalI18n(); @@ -248,7 +253,9 @@ export const AIChatInput: React.FC = ({ onKeyDown={handleKeyDown as any} placeholder={t('ai_chat.input.placeholder', { shortcut: sendShortcutLabel })} variant="borderless" - autoSize={{ minRows: 1, maxRows: 8 }} + autoSize={{ minRows: 3, maxRows: 8 }} + autoComplete="off" + {...noAutoCapInputProps} style={{ color: textColor, width: '100%', padding: 0, resize: 'none' }} /> @@ -260,10 +267,11 @@ export const AIChatInput: React.FC = ({ display: 'flex', alignItems: 'center', gap: 4, fontSize: 11, padding: '2px 8px', borderRadius: 12, background: darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)', - color: overlayTheme.mutedText, cursor: 'default' + color: overlayTheme.mutedText, cursor: 'default', + minWidth: 96, maxWidth: 200, overflow: 'hidden', flex: '1 1 auto', }}> - - + + {activeConnName}{activeContext?.dbName ? ` / ${activeContext.dbName}` : ''} @@ -279,6 +287,13 @@ export const AIChatInput: React.FC = ({ onFetchModels={onFetchModels} /> + + {contextUsageChars !== undefined && maxContextChars !== undefined && (
= ({ onKeyDown={handleKeyDown as any} placeholder={t('ai_chat.input.placeholder_compact', { shortcut: sendShortcutLabel })} variant="borderless" - autoSize={{ minRows: 1, maxRows: 8 }} + autoSize={{ minRows: 3, maxRows: 8 }} + autoComplete="off" + {...noAutoCapInputProps} style={{ color: textColor, width: '100%', padding: 0, resize: 'none' }} /> = ({
+ {/* 单行紧凑:连接 | 模型 | 思考 | 用量;模型固定宽,不撑满 */} {activeConnName && (
- {activeConnName}{activeContext?.dbName ? ` / ${activeContext.dbName}` : ''} + + {activeConnName}{activeContext?.dbName ? ` / ${activeContext.dbName}` : ''} +
)} - = ({ onModelChange={onModelChange} onFetchModels={onFetchModels} /> - -
- + {contextUsageChars !== undefined && maxContextChars !== undefined && (
maxContextChars * 0.8 ? ' is-warn' : ''}`}>
)} diff --git a/frontend/src/components/ai/AIChatThinkingIntensitySelect.tsx b/frontend/src/components/ai/AIChatThinkingIntensitySelect.tsx new file mode 100644 index 00000000..1cbe30b9 --- /dev/null +++ b/frontend/src/components/ai/AIChatThinkingIntensitySelect.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { Select } from 'antd'; +import { DownOutlined } from '@ant-design/icons'; + +import { t as catalogTranslate } from '../../i18n/catalog'; +import { useOptionalI18n } from '../../i18n/provider'; +import type { AIProviderConfig } from '../../types'; +import { + resolveThinkingIntensityOptions, + resolveThinkingIntensityProfile, +} from '../../utils/aiThinkingIntensity'; + +interface AIChatThinkingIntensitySelectProps { + activeProvider?: AIProviderConfig | null; + value: string; + variant: 'legacy' | 'v2'; + onChange: (value: string) => void; +} + +const AIChatThinkingIntensitySelect: React.FC = ({ + activeProvider, + value, + variant, + onChange, +}) => { + const i18n = useOptionalI18n(); + const t = i18n?.t ?? ((key: string, params?: Record) => + catalogTranslate('en-US', key, params)); + + if (!activeProvider) { + return null; + } + + const profile = resolveThinkingIntensityProfile({ + type: activeProvider.type, + apiFormat: activeProvider.apiFormat, + baseUrl: activeProvider.baseUrl, + model: activeProvider.model, + }); + const options = resolveThinkingIntensityOptions(profile).map((item) => ({ + value: item.value, + label: t(item.labelKey), + })); + + if (variant === 'legacy') { + return ( + } + /> + ); +}; + +export default AIChatThinkingIntensitySelect; diff --git a/frontend/src/components/ai/AISettingsProvidersSection.tsx b/frontend/src/components/ai/AISettingsProvidersSection.tsx index c8ed7fd2..4a70f517 100644 --- a/frontend/src/components/ai/AISettingsProvidersSection.tsx +++ b/frontend/src/components/ai/AISettingsProvidersSection.tsx @@ -14,12 +14,6 @@ import { PROVIDER_PRESET_GRID_STYLE, PROVIDER_PRESET_CARD_TITLE_STYLE, } from '../../utils/aiSettingsPresetLayout'; -import { - coerceThinkingIntensityForProfile, - resolveThinkingIntensityHintKey, - resolveThinkingIntensityOptions, - resolveThinkingIntensityProfile, -} from '../../utils/aiThinkingIntensity'; export interface AISettingsProviderPresetOption { key: string; @@ -126,44 +120,8 @@ const AISettingsProvidersSection: React.FC = ({ const currentFieldLabelStyle = fieldLabelStyle(sectionLabelColor); const watchedModel = Form.useWatch('model', form); const watchedModels = Form.useWatch('models', form); - const watchedBaseUrl = Form.useWatch('baseUrl', form); - const watchedType = Form.useWatch('type', form); - const watchedThinkingIntensity = Form.useWatch('thinkingIntensity', form); const watchedInlineCompletionModel = Form.useWatch('inlineCompletionModel', form); const presetFromForm = providerPresets.find((preset) => preset.key === presetKeyFromForm); - const thinkingProfile = React.useMemo(() => resolveThinkingIntensityProfile({ - type: watchedType || editingProvider?.type, - apiFormat: watchedApiFormat || editingProvider?.apiFormat, - baseUrl: watchedBaseUrl || editingProvider?.baseUrl || presetFromForm?.defaultBaseUrl, - model: watchedModel || editingProvider?.model || presetFromForm?.defaultModel, - }), [ - editingProvider?.apiFormat, - editingProvider?.baseUrl, - editingProvider?.model, - editingProvider?.type, - presetFromForm?.defaultBaseUrl, - presetFromForm?.defaultModel, - watchedApiFormat, - watchedBaseUrl, - watchedModel, - watchedType, - ]); - const thinkingIntensityOptions = React.useMemo( - () => resolveThinkingIntensityOptions(thinkingProfile).map((item) => ({ - value: item.value, - label: copy(item.labelKey), - })), - // copy 依赖 i18n 语言,label 随语言刷新即可 - // eslint-disable-next-line react-hooks/exhaustive-deps - [copy, thinkingProfile, i18n?.language], - ); - React.useEffect(() => { - if (!isEditing) return; - const next = coerceThinkingIntensityForProfile(watchedThinkingIntensity, thinkingProfile); - if (next && next !== String(watchedThinkingIntensity || '').trim()) { - form.setFieldsValue({ thinkingIntensity: next }); - } - }, [form, isEditing, thinkingProfile, watchedThinkingIntensity]); const inlineCompletionModelOptions = React.useMemo(() => { const values = [ watchedModel, @@ -412,24 +370,6 @@ const AISettingsProvidersSection: React.FC = ({ -
-
- {copy('ai_settings.form.section.thinking')} -
- {copy('ai_settings.form.thinking_intensity')}} - name="thinkingIntensity" - extra={copy(resolveThinkingIntensityHintKey(thinkingProfile))} - style={{ marginBottom: 0 }} - > -