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"
|
: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>
|
<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>
|
</div>
|
||||||
|
|
||||||
<ChatArea
|
<ChatArea
|
||||||
|
|||||||
@ -269,6 +269,7 @@ const appOptions = {
|
|||||||
titleTypingTimer: null,
|
titleTypingTimer: null,
|
||||||
titleReady: false,
|
titleReady: false,
|
||||||
suppressTitleTyping: false,
|
suppressTitleTyping: false,
|
||||||
|
headerMenuOpen: false,
|
||||||
blankWelcomePool: [
|
blankWelcomePool: [
|
||||||
'有什么可以帮忙的?',
|
'有什么可以帮忙的?',
|
||||||
'想了解些热点吗?',
|
'想了解些热点吗?',
|
||||||
@ -346,6 +347,7 @@ const appOptions = {
|
|||||||
|
|
||||||
document.addEventListener('click', this.handleClickOutsideQuickMenu);
|
document.addEventListener('click', this.handleClickOutsideQuickMenu);
|
||||||
document.addEventListener('click', this.handleClickOutsidePanelMenu);
|
document.addEventListener('click', this.handleClickOutsidePanelMenu);
|
||||||
|
document.addEventListener('click', this.handleClickOutsideHeaderMenu);
|
||||||
document.addEventListener('click', this.handleClickOutsideMobileMenu);
|
document.addEventListener('click', this.handleClickOutsideMobileMenu);
|
||||||
window.addEventListener('popstate', this.handlePopState);
|
window.addEventListener('popstate', this.handlePopState);
|
||||||
window.addEventListener('keydown', this.handleMobileOverlayEscape);
|
window.addEventListener('keydown', this.handleMobileOverlayEscape);
|
||||||
@ -436,6 +438,21 @@ const appOptions = {
|
|||||||
}
|
}
|
||||||
return this.thinkingMode ? 'thinking' : 'fast';
|
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() {
|
policyUiBlocks() {
|
||||||
const store = usePolicyStore();
|
const store = usePolicyStore();
|
||||||
return store.uiBlocks || {};
|
return store.uiBlocks || {};
|
||||||
@ -505,6 +522,7 @@ const appOptions = {
|
|||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
document.removeEventListener('click', this.handleClickOutsideQuickMenu);
|
document.removeEventListener('click', this.handleClickOutsideQuickMenu);
|
||||||
document.removeEventListener('click', this.handleClickOutsidePanelMenu);
|
document.removeEventListener('click', this.handleClickOutsidePanelMenu);
|
||||||
|
document.removeEventListener('click', this.handleClickOutsideHeaderMenu);
|
||||||
document.removeEventListener('click', this.handleClickOutsideMobileMenu);
|
document.removeEventListener('click', this.handleClickOutsideMobileMenu);
|
||||||
window.removeEventListener('popstate', this.handlePopState);
|
window.removeEventListener('popstate', this.handlePopState);
|
||||||
window.removeEventListener('keydown', this.handleMobileOverlayEscape);
|
window.removeEventListener('keydown', this.handleMobileOverlayEscape);
|
||||||
@ -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) {
|
async handleModeSelect(mode) {
|
||||||
if (!this.isConnected || this.streamingMessage) {
|
if (!this.isConnected || this.streamingMessage) {
|
||||||
return;
|
return;
|
||||||
@ -2787,6 +2815,11 @@ const appOptions = {
|
|||||||
await this.setRunMode(mode);
|
await this.setRunMode(mode);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async handleHeaderRunModeSelect(mode) {
|
||||||
|
await this.handleModeSelect(mode);
|
||||||
|
this.closeHeaderMenu();
|
||||||
|
},
|
||||||
|
|
||||||
async handleModelSelect(key) {
|
async handleModelSelect(key) {
|
||||||
if (!this.isConnected || this.streamingMessage) {
|
if (!this.isConnected || this.streamingMessage) {
|
||||||
return;
|
return;
|
||||||
@ -2863,6 +2896,12 @@ const appOptions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async handleHeaderModelSelect(key, disabled) {
|
||||||
|
if (disabled) return;
|
||||||
|
await this.handleModelSelect(key);
|
||||||
|
this.closeHeaderMenu();
|
||||||
|
},
|
||||||
|
|
||||||
async handleCycleRunMode() {
|
async handleCycleRunMode() {
|
||||||
const modes: Array<'fast' | 'thinking' | 'deep'> = ['fast', 'thinking', 'deep'];
|
const modes: Array<'fast' | 'thinking' | 'deep'> = ['fast', 'thinking', 'deep'];
|
||||||
const currentMode = this.resolvedRunMode;
|
const currentMode = this.resolvedRunMode;
|
||||||
@ -3024,6 +3063,10 @@ const appOptions = {
|
|||||||
this.closeQuickMenu();
|
this.closeQuickMenu();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
closeHeaderMenu() {
|
||||||
|
this.headerMenuOpen = false;
|
||||||
|
},
|
||||||
|
|
||||||
handleReviewSelect(id) {
|
handleReviewSelect(id) {
|
||||||
if (id === this.currentConversationId) {
|
if (id === this.currentConversationId) {
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
@ -3208,6 +3251,16 @@ const appOptions = {
|
|||||||
this.closeQuickMenu();
|
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) {
|
handleClickOutsidePanelMenu(event) {
|
||||||
if (!this.panelMenuOpen) {
|
if (!this.panelMenuOpen) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -26,24 +26,6 @@
|
|||||||
>
|
>
|
||||||
发送图片
|
发送图片
|
||||||
</button>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="menu-entry has-submenu"
|
class="menu-entry has-submenu"
|
||||||
@ -63,45 +45,6 @@
|
|||||||
<span class="entry-arrow">›</span>
|
<span class="entry-arrow">›</span>
|
||||||
</button>
|
</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">
|
<transition name="submenu-slide">
|
||||||
<div class="quick-submenu tool-submenu" v-if="toolMenuOpen">
|
<div class="quick-submenu tool-submenu" v-if="toolMenuOpen">
|
||||||
|
|||||||
@ -15,18 +15,19 @@
|
|||||||
.conversation-ribbon {
|
.conversation-ribbon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0 0 auto 0;
|
inset: 0 0 auto 0;
|
||||||
height: 38px;
|
height: 44px;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
background: var(--chat-surface-color); /* 与对话区域保持一致且不透明 */
|
background: var(--chat-surface-color); /* 与对话区域保持一致且不透明 */
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
pointer-events: none;
|
pointer-events: auto;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
backdrop-filter: none;
|
backdrop-filter: none;
|
||||||
z-index: 40;
|
z-index: 40;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation-ribbon::after {
|
.conversation-ribbon::after {
|
||||||
@ -53,10 +54,12 @@
|
|||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);
|
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-container.has-title-ribbon .messages-area {
|
.chat-container.has-title-ribbon .messages-area {
|
||||||
padding-top: 42px;
|
padding-top: 54px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-container--monitor .virtual-monitor-surface {
|
.chat-container--monitor .virtual-monitor-surface {
|
||||||
@ -129,6 +132,146 @@
|
|||||||
padding-top: 68px;
|
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 {
|
.scroll-lock-btn {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user