新增: F-01阶段6 AiChat模型下拉+override穿透

This commit is contained in:
2026-06-17 00:50:47 +08:00
parent 63bff8bf96
commit 4c84fbc1b6
7 changed files with 256 additions and 16 deletions

View File

@@ -211,6 +211,42 @@
{{ $t('aiChat.pendingApprovalCount', { n: pendingApprovalCount }) }}
</div>
</div>
<!-- F-01 阶段6: 模型选择器(自动/指定)
自动模式=路由器选(现状零变化);指定模式=用户从下拉选 model_id,override 穿透主对话
provider model_configs(空池):下拉空 + 强制自动(toggle 禁用)
仅展示 active provider enabled model_configs -->
<div class="ai-model-picker" :title="modelPickerHint">
<div class="ai-model-mode">
<button
type="button"
class="ai-model-mode-btn"
:class="{ 'ai-model-mode-btn--active': !isSpecifyMode }"
:disabled="!enabledModels.length"
@click="setAutoMode"
>{{ $t('aiChat.autoMode') }}</button>
<button
type="button"
class="ai-model-mode-btn"
:class="{ 'ai-model-mode-btn--active': isSpecifyMode }"
:disabled="!enabledModels.length"
:title="enabledModels.length ? $t('aiChat.specifyModeHint') : $t('aiChat.noModels')"
@click="setSpecifyMode"
>{{ $t('aiChat.specifyMode') }}</button>
</div>
<select
v-if="enabledModels.length"
class="ai-model-select"
:value="selectedModelId"
:disabled="!isSpecifyMode"
:title="isSpecifyMode ? '' : $t('aiChat.autoModeHint')"
@change="onModelSelect($event)"
>
<option v-for="m in enabledModels" :key="m.model_id" :value="m.model_id">
{{ modelOptionLabel(m) }}
</option>
</select>
<span v-else class="ai-model-empty">{{ $t('aiChat.noModels') }}</span>
</div>
<div class="ai-header-actions">
<button class="ai-btn-icon" @click="confirmNewConversation" :title="$t('aiChat.newConversation')">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
@@ -978,8 +1014,11 @@ function detectMentionTrigger() {
}
// UX-09:切换对话/新对话时取消编辑态(编辑态绑定末条 user,切走后无效)
// F-01 阶段6: 同步清 modelOverride(后端 switchConversation/new 已清 session.model_override,
// 前端 ref 不清则会与后端不一致——override 是单对话级 UI 选择,不跨对话持久化)。
watch(() => store.state.activeConversationId, () => {
if (editingMsgId.value) cancelEdit()
store.modelOverride.value = null
})
function selectSkill(s: SkillInfo) {
@@ -1196,6 +1235,54 @@ const activeProviderName = computed(() => {
return active?.name || store.state.providers[0]?.name || t('aiChat.notConfigured')
})
// ── F-01 阶段6: 顶部模型选择器(自动/指定) ──
// active provider 的 model_configs(用户在 Settings 拉取过的,仅 enabled 项)。
// 空池(provider 未拉取/无 enabled 模型)时下拉为空 + 强制自动模式。
const activeProviderRecord = computed(() =>
store.state.providers.find(p => p.id === store.state.activeProvider)
|| store.state.providers.find(p => p.is_default)
|| store.state.providers[0]
|| null,
)
const enabledModels = computed(() => {
const p = activeProviderRecord.value
if (!p?.model_configs) return []
return p.model_configs.filter(m => m.enabled)
})
// 指定模式 = store.modelOverride 非 null(模块级 ref,发送链路读它透传后端)。
const isSpecifyMode = computed(() => store.modelOverride.value !== null)
const selectedModelId = computed(() => store.modelOverride.value || '')
/** 当前选中模型的 model_id(指定模式);自动模式为空 */
const modelPickerHint = computed(() => {
if (!enabledModels.value.length) return t('aiChat.noModels')
return isSpecifyMode.value
? t('aiChat.specifyModeHint')
: t('aiChat.autoModeHint')
})
/** 下拉选项 label:model_id + 4 维度简标(智力/价格),复用 settings i18n 标签字面 */
function modelOptionLabel(m: import('../api/types').ModelConfig): string {
const name = m.label || m.model_id
const intel = t('settings.tagIntel.' + m.intelligence)
const cost = t('settings.tagCost.' + m.cost_tier)
return `${name} · ${intel} · ${cost}`
}
function setAutoMode() {
store.modelOverride.value = null
}
function setSpecifyMode() {
if (!enabledModels.value.length) return
// 进入指定模式默认选第一个 enabled 模型(若已非空且在池中则保留)
const cur = store.modelOverride.value
const inPool = cur && enabledModels.value.some(m => m.model_id === cur)
if (!inPool) {
store.modelOverride.value = enabledModels.value[0]?.model_id || null
}
}
function onModelSelect(e: Event) {
const v = (e.target as HTMLSelectElement).value
store.modelOverride.value = v || null
}
// 当前视图是否正在生成(切走后台生成时光标不显示)
const isViewingGenerating = computed(() =>
store.state.streaming && store.state.generatingConvId === store.state.activeConversationId
@@ -2397,6 +2484,69 @@ body.ai-sidebar-resizing * {
.ai-approval-badge:hover {
opacity: 0.85;
}
/* ═══ F-01 阶段6: 模型选择器(自动/指定 toggle + 下拉) ═══ */
/* 紧凑内联控件,header 中间区。空池时下拉隐藏 + toggle 禁用,强制自动。 */
.ai-model-picker {
display: flex;
align-items: center;
gap: 4px;
margin-left: auto;
margin-right: 4px;
padding-left: 8px;
min-width: 0;
}
.ai-model-mode {
display: inline-flex;
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius-sm);
overflow: hidden;
flex-shrink: 0;
}
.ai-model-mode-btn {
border: none;
background: transparent;
color: var(--df-text-dim);
font-size: 10px;
padding: 3px 8px;
cursor: pointer;
transition: background 0.15s, color 0.15s;
white-space: nowrap;
}
.ai-model-mode-btn:hover:not(:disabled) {
background: var(--df-bg-card);
color: var(--df-text);
}
.ai-model-mode-btn--active {
background: var(--df-accent-bg);
color: var(--df-accent);
}
.ai-model-mode-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.ai-model-select {
min-width: 0;
max-width: 180px;
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius-sm);
background: var(--df-bg);
color: var(--df-text);
font-size: 10px;
font-family: var(--df-font-mono);
padding: 3px 6px;
outline: none;
cursor: pointer;
}
.ai-model-select:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.ai-model-empty {
font-size: 10px;
color: var(--df-text-dim);
white-space: nowrap;
}
.ai-header-actions {
display: flex;
gap: 4px;