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

View File

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

View File

@ -260,6 +260,61 @@
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 {
top: auto;
bottom: 0;