新增: F-01阶段6 AiChat模型下拉+override穿透
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user