Просмотр исходного кода

WIP: 在模型旁添加快速模式闪电按钮

[gstack-context]
Decisions: 独立 bolt 快捷按钮紧邻模型;开启蓝色;aria-pressed;无活动会话时禁用;失败由 store 回滚
Remaining: 假 9119 RPC 记录、端到端协议验证、全量回归与重载
Skill: /frontend-skill
[/gstack-context]
samien 2 месяцев назад
Родитель
Сommit
e66fb636be

+ 2 - 0
src/client/components/common/AppIcon.vue

@@ -16,6 +16,7 @@ type IconName =
   | 'brain'
   | 'settings'
   | 'model'
+  | 'bolt'
   | 'arrow-up'
   | 'stop'
   | 'tools'
@@ -86,6 +87,7 @@ withDefaults(defineProps<{ name: IconName; size?: number }>(), { size: 18 })
     <template v-else-if="name === 'brain'"><path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-5-.1 2.5 2.5 0 0 1-3-3 3 3 0 0 1 0-5.8A2.5 2.5 0 0 1 7 6a2.5 2.5 0 0 1 2.5-4Z" /><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 5-.1 2.5 2.5 0 0 0 3-3 3 3 0 0 0 0-5.8A2.5 2.5 0 0 0 17 6a2.5 2.5 0 0 0-2.5-4Z" /></template>
     <template v-else-if="name === 'settings'"><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5v.2h-4v-.2a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1-2.8-2.8.1-.1A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.5-1H3v-4h.1A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.3-1.8l-.1-.1 2.8-2.8.1.1a1.7 1.7 0 0 0 1.8.3 1.7 1.7 0 0 0 1-1.5V3h4v.2a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1 2.8 2.8-.1.1A1.7 1.7 0 0 0 19.4 9a1.7 1.7 0 0 0 1.5 1h.1v4h-.1a1.7 1.7 0 0 0-1.5 1Z" /></template>
     <template v-else-if="name === 'model'"><circle cx="12" cy="12" r="3" /><path d="M12 2v4M12 18v4M2 12h4M18 12h4M5 5l3 3M16 16l3 3M5 19l3-3M16 8l3-3" /></template>
+    <template v-else-if="name === 'bolt'"><path d="M13.3 2 4.5 13h6.2L9.8 22l9.7-12h-6.2z" fill="currentColor" stroke="none" /></template>
     <template v-else-if="name === 'arrow-up'"><path d="M12 19V5m-7 7 7-7 7 7" stroke-width="2.2" /></template>
     <template v-else-if="name === 'stop'"><rect x="6" y="6" width="12" height="12" rx="2.5" fill="currentColor" stroke="none" /></template>
     <template v-else-if="name === 'tools'"><path d="M14.7 6.3a4 4 0 0 0-5-5L12 3.6 8.4 7.2 6.1 4.9a4 4 0 0 0 5 5l-7.4 7.4a2.1 2.1 0 0 0 3 3l7.4-7.4a4 4 0 0 0 5-5l-2.3 2.3-3.6-3.6z" /></template>

+ 21 - 0
src/client/components/composer/ComposerShell.vue

@@ -11,6 +11,8 @@ const props = withDefaults(defineProps<{
   streaming?: boolean
   sending?: boolean
   modelLabel?: string
+  fastMode?: boolean
+  fastModeDisabled?: boolean
   reasoningEffort?: string
   reasoningValue?: string
   reasoningOptions?: ComposerOption[]
@@ -30,6 +32,8 @@ const props = withDefaults(defineProps<{
   streaming: false,
   sending: false,
   modelLabel: '选择模型',
+  fastMode: false,
+  fastModeDisabled: false,
   reasoningEffort: '默认',
   reasoningValue: '',
   reasoningOptions: () => [],
@@ -47,6 +51,7 @@ const emit = defineEmits<{
   send: [payload: ComposerSubmit]
   stop: []
   modelClick: []
+  fastModeToggle: [enabled: boolean]
   reasoningChange: [value: string]
   settingsClick: []
   queueToggle: []
@@ -470,6 +475,19 @@ defineExpose({
           <button v-if="mode === 'chat'" class="composer-tool composer-tool--model" type="button" :disabled="disabled" :title="modelLabel" @click="emit('modelClick')">
             <AppIcon name="model" :size="15" /><span>{{ compactModel }}</span><AppIcon name="chevron-down" :size="11" />
           </button>
+          <button
+            v-if="mode === 'chat'"
+            class="composer-tool composer-tool--icon composer-fast-mode"
+            :class="{ active: fastMode }"
+            type="button"
+            :disabled="disabled || fastModeDisabled"
+            :aria-pressed="fastMode"
+            :aria-label="`快速模式:${fastMode ? '已开启' : '已关闭'}`"
+            title="快速模式:响应更快,消耗更多额度"
+            @click="emit('fastModeToggle', !fastMode)"
+          >
+            <AppIcon name="bolt" :size="15" />
+          </button>
           <span v-else class="composer-mention-hint"><b>@</b><span>提及 Agent</span></span>
         </div>
         <div class="composer-actions">
@@ -551,6 +569,9 @@ defineExpose({
 .composer-tool span { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
 .composer-tool--icon { width: 28px; padding: 0; }
 .composer-tool--model { min-width: 0; }
+.composer-fast-mode.active { background: color-mix(in srgb, #1677ff 12%, transparent); color: #1677ff; }
+.composer-fast-mode.active:hover { background: color-mix(in srgb, #1677ff 18%, transparent); color: #1677ff; }
+.dark .composer-fast-mode.active { color: #4c9aff; }
 .composer-mention-hint { display: inline-flex; align-items: center; gap: 4px; color: var(--text-muted); font-size: 11px; }
 .composer-mention-hint b { display: grid; place-items: center; width: 21px; height: 21px; border-radius: 7px; background: var(--surface-soft); color: var(--text-secondary); font-size: 12px; }
 .composer-popover { position: absolute; z-index: 30; bottom: calc(100% + 10px); left: 0; width: min(320px, calc(100vw - 48px)); padding: 12px; border: 1px solid var(--line); border-radius: 13px; background: var(--surface-raised); box-shadow: var(--shadow-float); }

+ 7 - 0
src/client/views/ChatView.vue

@@ -204,6 +204,10 @@ function selectReasoning(id: string) {
   chat.reasoningEffort = id || undefined
 }
 
+async function toggleFastMode(enabled: boolean) {
+  try { await chat.setFastMode(enabled) } catch { /* store restores the acknowledged state */ }
+}
+
 function restoreShowThinking(profile = auth.activeProfile?.name || 'default') {
   showThinking.value = readAgentShowThinking(auth.user?.id ?? 'local', profile)
 }
@@ -414,6 +418,8 @@ watch(() => chat.activeSessionId, async id => {
         :streaming="chat.isStreaming"
         :sending="chat.isSending"
         :model-label="chat.selectedModel?.name || '选择模型'"
+        :fast-mode="chat.fastMode"
+        :fast-mode-disabled="!chat.activeSessionId"
         :reasoning-effort="reasoningLabel"
         :reasoning-value="chat.reasoningEffort || ''"
         :reasoning-options="reasoningComposerOptions"
@@ -430,6 +436,7 @@ watch(() => chat.activeSessionId, async id => {
         @send="send"
         @stop="chat.interrupt"
         @model-click="modelDialog = true"
+        @fast-mode-toggle="toggleFastMode"
         @reasoning-change="selectReasoning"
         @settings-click="toggleShowThinking"
         @queue-toggle="queueMode = !queueMode"

+ 11 - 0
tests/client/composer.test.ts

@@ -14,4 +14,15 @@ describe('composer availability', () => {
     expect(wrapper.find('[aria-label="停止生成"]').exists()).toBe(true)
     expect(wrapper.find('[aria-label="发送消息"]').exists()).toBe(false)
   })
+
+  it('places an accessible blue fast-mode toggle beside the model control', () => {
+    const wrapper = mount(ComposerShell, { props: { mode: 'chat', modelLabel: 'gpt-5.6-terra', fastMode: true } })
+    const model = wrapper.get('.composer-tool--model')
+    const fast = wrapper.get('.composer-fast-mode')
+    expect(model.element.nextElementSibling).toBe(fast.element)
+    expect(fast.attributes('aria-pressed')).toBe('true')
+    expect(fast.classes()).toContain('active')
+    fast.trigger('click')
+    expect(wrapper.emitted('fastModeToggle')?.[0]).toEqual([false])
+  })
 })