feat: add blank chat hero and limit quick menu actions

This commit is contained in:
JOJO 2025-12-30 10:32:09 +08:00
parent 1efd6aa0d3
commit 099de1e922
3 changed files with 176 additions and 49 deletions

View File

@ -127,6 +127,11 @@
/> />
<VirtualMonitorSurface v-show="chatDisplayMode === 'monitor'" /> <VirtualMonitorSurface v-show="chatDisplayMode === 'monitor'" />
<div v-if="blankHeroActive" class="blank-hero-overlay">
<span class="icon icon-lg" :style="iconStyle('bot')" aria-hidden="true"></span>
<p class="blank-hero-text">{{ blankWelcomeText }}</p>
</div>
<div <div
v-if="chatDisplayMode === 'chat'" v-if="chatDisplayMode === 'chat'"
class="scroll-lock-toggle" class="scroll-lock-toggle"
@ -150,46 +155,48 @@
</button> </button>
</div> </div>
<InputComposer <div class="composer-container" :class="{ 'blank-hero-mode': composerHeroActive }">
ref="inputComposer" <InputComposer
:input-message="inputMessage" ref="inputComposer"
:input-is-multiline="inputIsMultiline" :input-message="inputMessage"
:input-is-focused="inputIsFocused" :input-is-multiline="inputIsMultiline"
:is-connected="isConnected" :input-is-focused="inputIsFocused"
:streaming-message="composerBusy" :is-connected="isConnected"
:input-locked="displayLockEngaged" :streaming-message="composerBusy"
:uploading="uploading" :input-locked="displayLockEngaged"
:thinking-mode="thinkingMode" :uploading="uploading"
:run-mode="resolvedRunMode" :thinking-mode="thinkingMode"
:quick-menu-open="quickMenuOpen" :run-mode="resolvedRunMode"
:tool-menu-open="toolMenuOpen" :quick-menu-open="quickMenuOpen"
:mode-menu-open="modeMenuOpen" :tool-menu-open="toolMenuOpen"
:tool-settings="toolSettings" :mode-menu-open="modeMenuOpen"
:tool-settings-loading="toolSettingsLoading" :tool-settings="toolSettings"
:settings-open="settingsOpen" :tool-settings-loading="toolSettingsLoading"
:compressing="compressing" :settings-open="settingsOpen"
:current-conversation-id="currentConversationId" :compressing="compressing"
:icon-style="iconStyle" :current-conversation-id="currentConversationId"
:tool-category-icon="toolCategoryIcon" :icon-style="iconStyle"
@update:input-message="inputSetMessage" :tool-category-icon="toolCategoryIcon"
@input-change="handleInputChange" @update:input-message="inputSetMessage"
@input-focus="handleInputFocus" @input-change="handleInputChange"
@input-blur="handleInputBlur" @input-focus="handleInputFocus"
@toggle-quick-menu="toggleQuickMenu" @input-blur="handleInputBlur"
@send-message="sendMessage" @toggle-quick-menu="toggleQuickMenu"
@send-or-stop="handleSendOrStop" @send-message="sendMessage"
@quick-upload="handleQuickUpload" @send-or-stop="handleSendOrStop"
@toggle-tool-menu="toggleToolMenu" @quick-upload="handleQuickUpload"
@toggle-mode-menu="toggleModeMenu" @toggle-tool-menu="toggleToolMenu"
@select-run-mode="handleModeSelect" @toggle-mode-menu="toggleModeMenu"
@toggle-settings="toggleSettings" @select-run-mode="handleModeSelect"
@update-tool-category="updateToolCategory" @toggle-settings="toggleSettings"
@realtime-terminal="handleRealtimeTerminalClick" @update-tool-category="updateToolCategory"
@toggle-focus-panel="handleFocusPanelToggleClick" @realtime-terminal="handleRealtimeTerminalClick"
@toggle-token-panel="handleTokenPanelToggleClick" @toggle-focus-panel="handleFocusPanelToggleClick"
@compress-conversation="handleCompressConversationClick" @toggle-token-panel="handleTokenPanelToggleClick"
@file-selected="handleFileSelected" @compress-conversation="handleCompressConversationClick"
/> @file-selected="handleFileSelected"
/>
</div>
</main> </main>
<div v-if="!isMobileViewport" class="resize-handle" @mousedown="startResize('right', $event)"></div> <div v-if="!isMobileViewport" class="resize-handle" @mousedown="startResize('right', $event)"></div>

View File

@ -148,6 +148,19 @@ const appOptions = {
historyLoading: false, historyLoading: false,
historyLoadingFor: null, historyLoadingFor: null,
historyLoadSeq: 0, historyLoadSeq: 0,
blankHeroActive: false,
blankHeroExiting: false,
blankWelcomeText: '',
blankWelcomePool: [
'有什么可以帮忙的?',
'想了解些热点吗?',
'要我帮你完成作业吗?',
'整点代码?',
'随便聊点什么?',
'想让我帮你整理一下思路吗?',
'要不要我帮你写个小工具?',
'发我一句话,我来接着做。'
],
mobileViewportQuery: null, mobileViewportQuery: null,
modeMenuOpen: false, modeMenuOpen: false,
conversationListRequestSeq: 0, conversationListRequestSeq: 0,
@ -318,6 +331,9 @@ const appOptions = {
composerBusy() { composerBusy() {
const monitorLock = this.monitorIsLocked && this.chatDisplayMode === 'monitor'; const monitorLock = this.monitorIsLocked && this.chatDisplayMode === 'monitor';
return this.streamingUi || this.taskInProgress || monitorLock || this.stopRequested; return this.streamingUi || this.taskInProgress || monitorLock || this.stopRequested;
},
composerHeroActive() {
return this.blankHeroActive || this.blankHeroExiting;
} }
}, },
@ -342,6 +358,12 @@ const appOptions = {
inputMessage() { inputMessage() {
this.autoResizeInput(); this.autoResizeInput();
}, },
messages: {
deep: true,
handler() {
this.refreshBlankHeroState();
}
},
currentConversationId: { currentConversationId: {
immediate: false, immediate: false,
handler(newValue, oldValue) { handler(newValue, oldValue) {
@ -354,6 +376,7 @@ const appOptions = {
historyLoadingFor: this.historyLoadingFor, historyLoadingFor: this.historyLoadingFor,
historyLoadSeq: this.historyLoadSeq historyLoadSeq: this.historyLoadSeq
}); });
this.refreshBlankHeroState();
this.logMessageState('watch:currentConversationId', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload }); this.logMessageState('watch:currentConversationId', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload });
if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) { if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) {
return; return;
@ -1397,6 +1420,7 @@ const appOptions = {
const targetConversationId = this.currentConversationId; const targetConversationId = this.currentConversationId;
if (!targetConversationId || targetConversationId.startsWith('temp_')) { if (!targetConversationId || targetConversationId.startsWith('temp_')) {
debugLog('没有当前对话ID跳过历史加载'); debugLog('没有当前对话ID跳过历史加载');
this.refreshBlankHeroState();
return; return;
} }
@ -1481,16 +1505,17 @@ const appOptions = {
debugLog('尝试不显示错误弹窗,仅在控制台记录'); debugLog('尝试不显示错误弹窗,仅在控制台记录');
// 不显示alert避免打断用户体验 // 不显示alert避免打断用户体验
this.logMessageState('fetchAndDisplayHistory:error-clear', { error: error?.message || String(error) }); this.logMessageState('fetchAndDisplayHistory:error-clear', { error: error?.message || String(error) });
this.messages = []; this.messages = [];
this.logMessageState('fetchAndDisplayHistory:error-cleared'); this.logMessageState('fetchAndDisplayHistory:error-cleared');
}
} finally {
// 仅在本次加载仍是最新请求时清除 loading 状态
if (loadSeq === this.historyLoadSeq) {
this.historyLoading = false;
this.historyLoadingFor = null;
}
this.refreshBlankHeroState();
} }
} finally {
// 仅在本次加载仍是最新请求时清除 loading 状态
if (loadSeq === this.historyLoadSeq) {
this.historyLoading = false;
this.historyLoadingFor = null;
}
}
}, },
// ========================================== // ==========================================
@ -2020,6 +2045,16 @@ const appOptions = {
return; return;
} }
const wasBlank = this.isConversationBlank();
if (wasBlank) {
this.blankHeroExiting = true;
this.blankHeroActive = true;
setTimeout(() => {
this.blankHeroExiting = false;
this.blankHeroActive = false;
}, 320);
}
// 标记任务进行中,直到任务完成或用户手动停止 // 标记任务进行中,直到任务完成或用户手动停止
this.taskInProgress = true; this.taskInProgress = true;
this.chatAddUserMessage(message); this.chatAddUserMessage(message);
@ -2373,6 +2408,36 @@ const appOptions = {
this.uiSetPanelMenuOpen(false); this.uiSetPanelMenuOpen(false);
}, },
isConversationBlank() {
if (!Array.isArray(this.messages) || !this.messages.length) return true;
return !this.messages.some(
(msg) => msg && (msg.role === 'user' || msg.role === 'assistant')
);
},
pickWelcomeText() {
const pool = this.blankWelcomePool;
if (!Array.isArray(pool) || !pool.length) {
this.blankWelcomeText = '有什么可以帮忙的?';
return;
}
const idx = Math.floor(Math.random() * pool.length);
this.blankWelcomeText = pool[idx];
},
refreshBlankHeroState() {
const isBlank = this.isConversationBlank();
if (isBlank) {
if (!this.blankHeroExiting) {
this.pickWelcomeText();
}
this.blankHeroActive = true;
} else {
this.blankHeroActive = false;
this.blankHeroExiting = false;
}
},
applyToolSettingsSnapshot(categories) { applyToolSettingsSnapshot(categories) {
if (!Array.isArray(categories)) { if (!Array.isArray(categories)) {
console.warn('[ToolSettings] Snapshot skipped: categories not array', categories); console.warn('[ToolSettings] Snapshot skipped: categories not array', categories);

View File

@ -260,6 +260,61 @@
gap: 6px; gap: 6px;
} }
/* Blank conversation hero */
.chat-container {
position: relative;
}
.blank-hero-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
pointer-events: none;
z-index: 1;
gap: 10px;
padding-bottom: 140px;
}
.blank-hero-text {
font-size: 32px;
color: #2f3a4a;
font-weight: 600;
}
.blank-hero-overlay .icon-lg {
width: 48px;
height: 48px;
}
.composer-container {
position: relative;
transition: transform 0.3s ease;
z-index: 2;
}
.composer-container.blank-hero-mode {
transform: translateY(-38vh);
}
@media (max-width: 768px) {
.blank-hero-text {
font-size: 28px;
}
.blank-hero-overlay .icon-lg {
width: 44px;
height: 44px;
}
.composer-container.blank-hero-mode {
transform: translateY(-32vh);
}
.blank-hero-overlay {
padding-bottom: 120px;
}
}
.quick-submenu.tool-submenu { .quick-submenu.tool-submenu {
top: auto; top: auto;
bottom: 0; bottom: 0;