feat: move model and mode pickers to header bar
This commit is contained in:
parent
7890926c3d
commit
462b0ed6f3
@ -111,8 +111,71 @@
|
||||
:format-rate="formatRate"
|
||||
/>
|
||||
|
||||
<div v-if="titleRibbonVisible" class="conversation-ribbon">
|
||||
<div
|
||||
v-if="titleRibbonVisible"
|
||||
class="conversation-ribbon"
|
||||
ref="titleRibbon"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="conversation-ribbon__selector"
|
||||
:class="{ open: headerMenuOpen }"
|
||||
@click.stop="toggleHeaderMenu"
|
||||
:disabled="!isConnected"
|
||||
>
|
||||
<span class="selector-label">
|
||||
<span class="selector-model">{{ currentModelLabel }}</span>
|
||||
<span class="selector-sep">·</span>
|
||||
<span class="selector-mode">{{ headerRunModeLabel }}</span>
|
||||
</span>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" class="selector-caret">
|
||||
<path d="M7 10l5 5 5-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<span class="conversation-ribbon__text">{{ titleTypingText || currentConversationTitle }}</span>
|
||||
|
||||
<transition name="header-menu">
|
||||
<div
|
||||
v-if="headerMenuOpen"
|
||||
class="model-mode-dropdown"
|
||||
ref="headerMenu"
|
||||
>
|
||||
<div class="dropdown-column">
|
||||
<div class="dropdown-title">模型</div>
|
||||
<button
|
||||
v-for="option in modelOptions"
|
||||
:key="option.key"
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
:class="{ active: option.key === currentModelKey, disabled: option.disabled }"
|
||||
@click.stop="handleHeaderModelSelect(option.key, option.disabled)"
|
||||
:disabled="streamingMessage || !isConnected || option.disabled"
|
||||
>
|
||||
<div class="item-label">{{ option.label }}</div>
|
||||
<div class="item-desc">{{ option.description }}</div>
|
||||
<span v-if="option.key === currentModelKey" class="item-check">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="dropdown-column">
|
||||
<div class="dropdown-title">运行模式</div>
|
||||
<button
|
||||
v-for="option in headerRunModeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
:class="{ active: option.value === resolvedRunMode }"
|
||||
@click.stop="handleHeaderRunModeSelect(option.value)"
|
||||
:disabled="streamingMessage || !isConnected"
|
||||
>
|
||||
<div class="item-label">{{ option.label }}</div>
|
||||
<div class="item-desc">{{ option.desc }}</div>
|
||||
<span v-if="option.value === resolvedRunMode" class="item-check">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<ChatArea
|
||||
|
||||
@ -269,6 +269,7 @@ const appOptions = {
|
||||
titleTypingTimer: null,
|
||||
titleReady: false,
|
||||
suppressTitleTyping: false,
|
||||
headerMenuOpen: false,
|
||||
blankWelcomePool: [
|
||||
'有什么可以帮忙的?',
|
||||
'想了解些热点吗?',
|
||||
@ -346,6 +347,7 @@ const appOptions = {
|
||||
|
||||
document.addEventListener('click', this.handleClickOutsideQuickMenu);
|
||||
document.addEventListener('click', this.handleClickOutsidePanelMenu);
|
||||
document.addEventListener('click', this.handleClickOutsideHeaderMenu);
|
||||
document.addEventListener('click', this.handleClickOutsideMobileMenu);
|
||||
window.addEventListener('popstate', this.handlePopState);
|
||||
window.addEventListener('keydown', this.handleMobileOverlayEscape);
|
||||
@ -436,6 +438,21 @@ const appOptions = {
|
||||
}
|
||||
return this.thinkingMode ? 'thinking' : 'fast';
|
||||
},
|
||||
headerRunModeOptions() {
|
||||
return [
|
||||
{ value: 'fast', label: '快速模式', desc: '低思考,响应更快' },
|
||||
{ value: 'thinking', label: '思考模式', desc: '更长思考,综合回答' },
|
||||
{ value: 'deep', label: '深度思考', desc: '持续推理,适合复杂任务' }
|
||||
];
|
||||
},
|
||||
headerRunModeLabel() {
|
||||
const current = this.headerRunModeOptions.find((o) => o.value === this.resolvedRunMode);
|
||||
return current ? current.label : '快速模式';
|
||||
},
|
||||
currentModelLabel() {
|
||||
const modelStore = useModelStore();
|
||||
return modelStore.currentModel?.label || 'Kimi-k2.5';
|
||||
},
|
||||
policyUiBlocks() {
|
||||
const store = usePolicyStore();
|
||||
return store.uiBlocks || {};
|
||||
@ -502,13 +519,14 @@ const appOptions = {
|
||||
}
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('click', this.handleClickOutsideQuickMenu);
|
||||
document.removeEventListener('click', this.handleClickOutsidePanelMenu);
|
||||
document.removeEventListener('click', this.handleClickOutsideMobileMenu);
|
||||
window.removeEventListener('popstate', this.handlePopState);
|
||||
window.removeEventListener('keydown', this.handleMobileOverlayEscape);
|
||||
this.teardownMobileViewportWatcher();
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('click', this.handleClickOutsideQuickMenu);
|
||||
document.removeEventListener('click', this.handleClickOutsidePanelMenu);
|
||||
document.removeEventListener('click', this.handleClickOutsideHeaderMenu);
|
||||
document.removeEventListener('click', this.handleClickOutsideMobileMenu);
|
||||
window.removeEventListener('popstate', this.handlePopState);
|
||||
window.removeEventListener('keydown', this.handleMobileOverlayEscape);
|
||||
this.teardownMobileViewportWatcher();
|
||||
this.subAgentStopPolling();
|
||||
this.resourceStopContainerStatsPolling();
|
||||
this.resourceStopProjectStoragePolling();
|
||||
@ -2780,6 +2798,16 @@ const appOptions = {
|
||||
}
|
||||
},
|
||||
|
||||
toggleHeaderMenu() {
|
||||
if (!this.isConnected) return;
|
||||
this.headerMenuOpen = !this.headerMenuOpen;
|
||||
if (this.headerMenuOpen) {
|
||||
this.closeQuickMenu();
|
||||
this.inputCloseMenus();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
async handleModeSelect(mode) {
|
||||
if (!this.isConnected || this.streamingMessage) {
|
||||
return;
|
||||
@ -2787,6 +2815,11 @@ const appOptions = {
|
||||
await this.setRunMode(mode);
|
||||
},
|
||||
|
||||
async handleHeaderRunModeSelect(mode) {
|
||||
await this.handleModeSelect(mode);
|
||||
this.closeHeaderMenu();
|
||||
},
|
||||
|
||||
async handleModelSelect(key) {
|
||||
if (!this.isConnected || this.streamingMessage) {
|
||||
return;
|
||||
@ -2863,6 +2896,12 @@ const appOptions = {
|
||||
}
|
||||
},
|
||||
|
||||
async handleHeaderModelSelect(key, disabled) {
|
||||
if (disabled) return;
|
||||
await this.handleModelSelect(key);
|
||||
this.closeHeaderMenu();
|
||||
},
|
||||
|
||||
async handleCycleRunMode() {
|
||||
const modes: Array<'fast' | 'thinking' | 'deep'> = ['fast', 'thinking', 'deep'];
|
||||
const currentMode = this.resolvedRunMode;
|
||||
@ -3024,6 +3063,10 @@ const appOptions = {
|
||||
this.closeQuickMenu();
|
||||
},
|
||||
|
||||
closeHeaderMenu() {
|
||||
this.headerMenuOpen = false;
|
||||
},
|
||||
|
||||
handleReviewSelect(id) {
|
||||
if (id === this.currentConversationId) {
|
||||
this.uiPushToast({
|
||||
@ -3208,6 +3251,16 @@ const appOptions = {
|
||||
this.closeQuickMenu();
|
||||
},
|
||||
|
||||
handleClickOutsideHeaderMenu(event) {
|
||||
if (!this.headerMenuOpen) return;
|
||||
const ribbon = this.$refs.titleRibbon as HTMLElement | undefined;
|
||||
const menu = this.$refs.headerMenu as HTMLElement | undefined;
|
||||
if ((ribbon && ribbon.contains(event.target)) || (menu && menu.contains(event.target))) {
|
||||
return;
|
||||
}
|
||||
this.closeHeaderMenu();
|
||||
},
|
||||
|
||||
handleClickOutsidePanelMenu(event) {
|
||||
if (!this.panelMenuOpen) {
|
||||
return;
|
||||
|
||||
@ -26,24 +26,6 @@
|
||||
>
|
||||
发送图片
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="menu-entry has-submenu"
|
||||
@click.stop="$emit('toggle-mode-menu')"
|
||||
:disabled="!isConnected || streamingMessage"
|
||||
>
|
||||
<span>运行模式</span>
|
||||
<span class="entry-arrow">{{ runModeLabel }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="menu-entry has-submenu"
|
||||
@click.stop="$emit('toggle-model-menu')"
|
||||
:disabled="!isConnected"
|
||||
>
|
||||
<span>切换模型</span>
|
||||
<span class="entry-arrow">{{ currentModelLabel }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="menu-entry has-submenu"
|
||||
@ -63,45 +45,6 @@
|
||||
<span class="entry-arrow">›</span>
|
||||
</button>
|
||||
|
||||
<transition name="submenu-slide">
|
||||
<div class="quick-submenu mode-submenu" v-if="modeMenuOpen">
|
||||
<div class="submenu-list">
|
||||
<button
|
||||
v-for="option in runModeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="menu-entry submenu-entry"
|
||||
:class="{ active: option.value === resolvedRunMode }"
|
||||
@click.stop="$emit('select-run-mode', option.value)"
|
||||
:disabled="streamingMessage || !isConnected"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<transition name="submenu-slide">
|
||||
<div class="quick-submenu model-submenu" v-if="modelMenuOpen">
|
||||
<div class="submenu-list">
|
||||
<button
|
||||
v-for="option in modelOptions"
|
||||
:key="option.key"
|
||||
type="button"
|
||||
class="menu-entry submenu-entry"
|
||||
:class="{ active: option.key === currentModelKey }"
|
||||
@click.stop="$emit('select-model', option.key)"
|
||||
:disabled="streamingMessage || !isConnected"
|
||||
>
|
||||
<span class="submenu-label">
|
||||
<span>{{ option.label }}</span>
|
||||
<span class="submenu-desc">{{ option.description }}</span>
|
||||
</span>
|
||||
<span v-if="option.key === currentModelKey" class="entry-arrow">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<transition name="submenu-slide">
|
||||
<div class="quick-submenu tool-submenu" v-if="toolMenuOpen">
|
||||
|
||||
@ -15,18 +15,19 @@
|
||||
.conversation-ribbon {
|
||||
position: absolute;
|
||||
inset: 0 0 auto 0;
|
||||
height: 38px;
|
||||
height: 44px;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
justify-content: space-between;
|
||||
color: #0f172a;
|
||||
background: var(--chat-surface-color); /* 与对话区域保持一致且不透明 */
|
||||
box-shadow: none;
|
||||
pointer-events: none;
|
||||
pointer-events: auto;
|
||||
user-select: none;
|
||||
backdrop-filter: none;
|
||||
z-index: 40;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.conversation-ribbon::after {
|
||||
@ -53,10 +54,12 @@
|
||||
color: #0f172a;
|
||||
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chat-container.has-title-ribbon .messages-area {
|
||||
padding-top: 42px;
|
||||
padding-top: 54px;
|
||||
}
|
||||
|
||||
.chat-container--monitor .virtual-monitor-surface {
|
||||
@ -129,6 +132,146 @@
|
||||
padding-top: 68px;
|
||||
}
|
||||
|
||||
.conversation-ribbon__selector {
|
||||
pointer-events: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.conversation-ribbon__selector:hover {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.conversation-ribbon__selector:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.conversation-ribbon__selector.open {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.selector-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.selector-model {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.selector-sep {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.selector-mode {
|
||||
font-weight: 500;
|
||||
color: rgba(15, 23, 42, 0.8);
|
||||
}
|
||||
|
||||
.selector-caret {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
.model-mode-dropdown {
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
left: 16px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18);
|
||||
backdrop-filter: blur(8px);
|
||||
pointer-events: auto;
|
||||
min-width: 480px;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.dropdown-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dropdown-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(15, 23, 42, 0.65);
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.18s ease;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.dropdown-item:hover:not(.disabled) {
|
||||
background: #f6f7fb;
|
||||
}
|
||||
|
||||
.dropdown-item.disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
grid-column: 1 / 2;
|
||||
font-size: 12px;
|
||||
color: rgba(15, 23, 42, 0.65);
|
||||
}
|
||||
|
||||
.item-check {
|
||||
align-self: center;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.header-menu-enter-active,
|
||||
.header-menu-leave-active {
|
||||
transition: all 0.16s ease, opacity 0.16s ease;
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.header-menu-enter-from,
|
||||
.header-menu-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px) scale(0.98);
|
||||
}
|
||||
|
||||
.scroll-lock-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user