fix: refine title ribbon visuals and typing
This commit is contained in:
parent
d34fbe963a
commit
e903c99ca4
@ -88,7 +88,8 @@
|
||||
:class="{
|
||||
'chat-container--immersive': workspaceCollapsed,
|
||||
'chat-container--mobile': isMobileViewport,
|
||||
'chat-container--monitor': chatDisplayMode === 'monitor'
|
||||
'chat-container--monitor': chatDisplayMode === 'monitor',
|
||||
'has-title-ribbon': titleRibbonVisible
|
||||
}"
|
||||
>
|
||||
<TokenDrawer
|
||||
@ -108,6 +109,10 @@
|
||||
:format-rate="formatRate"
|
||||
/>
|
||||
|
||||
<div v-if="titleRibbonVisible" class="conversation-ribbon">
|
||||
<span class="conversation-ribbon__text">{{ titleTypingText || currentConversationTitle }}</span>
|
||||
</div>
|
||||
|
||||
<ChatArea
|
||||
v-show="chatDisplayMode === 'chat'"
|
||||
ref="messagesArea"
|
||||
|
||||
@ -257,6 +257,12 @@ const appOptions = {
|
||||
blankHeroExiting: false,
|
||||
blankWelcomeText: '',
|
||||
lastBlankConversationId: null,
|
||||
// 对话标题打字效果
|
||||
titleTypingText: '',
|
||||
titleTypingTarget: '',
|
||||
titleTypingTimer: null,
|
||||
titleReady: false,
|
||||
suppressTitleTyping: false,
|
||||
blankWelcomePool: [
|
||||
'有什么可以帮忙的?',
|
||||
'想了解些热点吗?',
|
||||
@ -405,6 +411,9 @@ const appOptions = {
|
||||
}
|
||||
return this.thinkingMode ? 'thinking' : 'fast';
|
||||
},
|
||||
titleRibbonVisible() {
|
||||
return !this.isMobileViewport && this.chatDisplayMode === 'chat';
|
||||
},
|
||||
...mapWritableState(useToolStore, [
|
||||
'preparingTools',
|
||||
'activeTools',
|
||||
@ -459,6 +468,10 @@ const appOptions = {
|
||||
this.resourceStopProjectStoragePolling();
|
||||
this.resourceStopUsageQuotaPolling();
|
||||
teardownShowImageObserver();
|
||||
if (this.titleTypingTimer) {
|
||||
clearInterval(this.titleTypingTimer);
|
||||
this.titleTypingTimer = null;
|
||||
}
|
||||
const cleanup = this.destroyEasterEggEffect(true);
|
||||
if (cleanup && typeof cleanup.catch === 'function') {
|
||||
cleanup.catch(() => {});
|
||||
@ -466,18 +479,31 @@ const appOptions = {
|
||||
},
|
||||
|
||||
watch: {
|
||||
inputMessage() {
|
||||
this.autoResizeInput();
|
||||
},
|
||||
messages: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.refreshBlankHeroState();
|
||||
}
|
||||
},
|
||||
currentConversationId: {
|
||||
immediate: false,
|
||||
handler(newValue, oldValue) {
|
||||
inputMessage() {
|
||||
this.autoResizeInput();
|
||||
},
|
||||
messages: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.refreshBlankHeroState();
|
||||
}
|
||||
},
|
||||
currentConversationTitle(newVal, oldVal) {
|
||||
const target = (newVal && newVal.trim()) || '';
|
||||
if (this.suppressTitleTyping) {
|
||||
this.titleTypingText = target;
|
||||
this.titleTypingTarget = target;
|
||||
return;
|
||||
}
|
||||
const previous = (oldVal && oldVal.trim()) || (this.titleTypingText && this.titleTypingText.trim()) || '';
|
||||
const placeholderPrev = !previous || previous === '新对话';
|
||||
const placeholderTarget = !target || target === '新对话';
|
||||
const animate = placeholderPrev && !placeholderTarget; // 仅从空/占位切换到真实标题时动画
|
||||
this.startTitleTyping(target, { animate });
|
||||
},
|
||||
currentConversationId: {
|
||||
immediate: false,
|
||||
handler(newValue, oldValue) {
|
||||
debugLog('currentConversationId 变化', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload });
|
||||
traceLog('watch:currentConversationId', {
|
||||
oldValue,
|
||||
@ -971,10 +997,18 @@ const appOptions = {
|
||||
},
|
||||
|
||||
async bootstrapRoute() {
|
||||
// 在路由解析期间抑制标题动画,避免预置“新对话”闪烁
|
||||
this.suppressTitleTyping = true;
|
||||
this.titleReady = false;
|
||||
this.currentConversationTitle = '';
|
||||
this.titleTypingText = '';
|
||||
const path = window.location.pathname.replace(/^\/+/, '');
|
||||
if (!path || path === 'new') {
|
||||
this.currentConversationId = null;
|
||||
this.currentConversationTitle = '新对话';
|
||||
this.titleReady = true;
|
||||
this.suppressTitleTyping = false;
|
||||
this.startTitleTyping('新对话', { animate: false });
|
||||
this.initialRouteResolved = true;
|
||||
return;
|
||||
}
|
||||
@ -985,18 +1019,27 @@ const appOptions = {
|
||||
const result = await resp.json();
|
||||
if (result.success) {
|
||||
this.currentConversationId = convId;
|
||||
this.currentConversationTitle = result.title || '对话';
|
||||
this.currentConversationTitle = result.title || '';
|
||||
this.titleReady = true;
|
||||
this.suppressTitleTyping = false;
|
||||
this.startTitleTyping(this.currentConversationTitle, { animate: false });
|
||||
history.replaceState({ conversationId: convId }, '', `/${this.stripConversationPrefix(convId)}`);
|
||||
} else {
|
||||
history.replaceState({}, '', '/new');
|
||||
this.currentConversationId = null;
|
||||
this.currentConversationTitle = '新对话';
|
||||
this.titleReady = true;
|
||||
this.suppressTitleTyping = false;
|
||||
this.startTitleTyping('新对话', { animate: false });
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('初始化路由失败:', error);
|
||||
history.replaceState({}, '', '/new');
|
||||
this.currentConversationId = null;
|
||||
this.currentConversationTitle = '新对话';
|
||||
this.titleReady = true;
|
||||
this.suppressTitleTyping = false;
|
||||
this.startTitleTyping('新对话', { animate: false });
|
||||
} finally {
|
||||
this.initialRouteResolved = true;
|
||||
}
|
||||
@ -1313,6 +1356,10 @@ const appOptions = {
|
||||
this.skipConversationHistoryReload = true;
|
||||
// 首次从状态恢复对话时,避免 socket 的 conversation_loaded 再次触发历史加载
|
||||
this.skipConversationLoadedEvent = true;
|
||||
this.suppressTitleTyping = true;
|
||||
this.titleReady = false;
|
||||
this.currentConversationTitle = '';
|
||||
this.titleTypingText = '';
|
||||
this.currentConversationId = statusConversationId;
|
||||
|
||||
// 如果有当前对话,尝试获取标题和历史
|
||||
@ -1321,6 +1368,15 @@ const appOptions = {
|
||||
const convData = await convResponse.json();
|
||||
if (convData.success && convData.data) {
|
||||
this.currentConversationTitle = convData.data.title;
|
||||
this.titleReady = true;
|
||||
this.suppressTitleTyping = false;
|
||||
this.startTitleTyping(this.currentConversationTitle, { animate: false });
|
||||
} else {
|
||||
this.titleReady = true;
|
||||
this.suppressTitleTyping = false;
|
||||
const fallbackTitle = this.currentConversationTitle || '新对话';
|
||||
this.currentConversationTitle = fallbackTitle;
|
||||
this.startTitleTyping(fallbackTitle, { animate: false });
|
||||
}
|
||||
// 初始化时调用一次,因为 skipConversationHistoryReload 会阻止 watch 触发
|
||||
if (
|
||||
@ -1335,6 +1391,9 @@ const appOptions = {
|
||||
this.updateCurrentContextTokens();
|
||||
} catch (e) {
|
||||
console.warn('获取当前对话标题失败:', e);
|
||||
this.titleReady = true;
|
||||
this.suppressTitleTyping = false;
|
||||
this.startTitleTyping(this.currentConversationTitle || '新对话', '');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1482,10 +1541,16 @@ const appOptions = {
|
||||
debugLog('加载对话:', conversationId);
|
||||
traceLog('loadConversation:start', { conversationId, currentConversationId: this.currentConversationId, force });
|
||||
this.logMessageState('loadConversation:start', { conversationId, force });
|
||||
this.suppressTitleTyping = true;
|
||||
this.titleReady = false;
|
||||
this.currentConversationTitle = '';
|
||||
this.titleTypingText = '';
|
||||
|
||||
if (!force && conversationId === this.currentConversationId) {
|
||||
debugLog('已是当前对话,跳过加载');
|
||||
traceLog('loadConversation:skip-same', { conversationId });
|
||||
this.suppressTitleTyping = false;
|
||||
this.titleReady = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1504,6 +1569,9 @@ const appOptions = {
|
||||
this.skipConversationHistoryReload = true;
|
||||
this.currentConversationId = conversationId;
|
||||
this.currentConversationTitle = result.title;
|
||||
this.titleReady = true;
|
||||
this.suppressTitleTyping = false;
|
||||
this.startTitleTyping(this.currentConversationTitle, { animate: false });
|
||||
this.promoteConversationToTop(conversationId);
|
||||
history.pushState({ conversationId }, '', `/${this.stripConversationPrefix(conversationId)}`);
|
||||
this.skipConversationLoadedEvent = true;
|
||||
@ -1524,6 +1592,8 @@ const appOptions = {
|
||||
|
||||
} else {
|
||||
console.error('对话加载失败:', result.message);
|
||||
this.suppressTitleTyping = false;
|
||||
this.titleReady = true;
|
||||
this.uiPushToast({
|
||||
title: '加载对话失败',
|
||||
message: result.message || '服务器未返回成功状态',
|
||||
@ -1533,6 +1603,8 @@ const appOptions = {
|
||||
} catch (error) {
|
||||
console.error('加载对话异常:', error);
|
||||
traceLog('loadConversation:error', { conversationId, error: error?.message || String(error) });
|
||||
this.suppressTitleTyping = false;
|
||||
this.titleReady = true;
|
||||
this.uiPushToast({
|
||||
title: '加载对话异常',
|
||||
message: error.message || String(error),
|
||||
@ -2723,6 +2795,47 @@ const appOptions = {
|
||||
this.$forceUpdate();
|
||||
this.conditionalScrollToBottom();
|
||||
},
|
||||
startTitleTyping(title: string, options: { animate?: boolean } = {}) {
|
||||
if (this.titleTypingTimer) {
|
||||
clearInterval(this.titleTypingTimer);
|
||||
this.titleTypingTimer = null;
|
||||
}
|
||||
const target = (title || '').trim();
|
||||
const animate = options.animate ?? true;
|
||||
if (!animate) {
|
||||
this.titleTypingTarget = target;
|
||||
this.titleTypingText = target;
|
||||
return;
|
||||
}
|
||||
const previous = (this.titleTypingText || '').trim();
|
||||
if (previous === target) {
|
||||
this.titleTypingTarget = target;
|
||||
this.titleTypingText = target;
|
||||
return;
|
||||
}
|
||||
this.titleTypingTarget = target;
|
||||
this.titleTypingText = previous;
|
||||
|
||||
const frames: string[] = [];
|
||||
for (let i = previous.length; i >= 0; i--) {
|
||||
frames.push(previous.slice(0, i));
|
||||
}
|
||||
for (let j = 1; j <= target.length; j++) {
|
||||
frames.push(target.slice(0, j));
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
this.titleTypingTimer = window.setInterval(() => {
|
||||
if (index >= frames.length) {
|
||||
clearInterval(this.titleTypingTimer!);
|
||||
this.titleTypingTimer = null;
|
||||
this.titleTypingText = target;
|
||||
return;
|
||||
}
|
||||
this.titleTypingText = frames[index];
|
||||
index += 1;
|
||||
}, 32);
|
||||
},
|
||||
toggleBlock(blockId) {
|
||||
if (!blockId) {
|
||||
return;
|
||||
|
||||
@ -176,11 +176,13 @@
|
||||
:get-tool-icon="getToolIcon"
|
||||
:get-tool-status-text="getToolStatusText"
|
||||
:get-tool-description="getToolDescription"
|
||||
:format-search-topic="formatSearchTopic"
|
||||
:format-search-time="formatSearchTime"
|
||||
:streaming-message="streamingMessage"
|
||||
@toggle="toggleBlock(group.action.blockId || `${index}-tool-${group.actionIndex}`)"
|
||||
/>
|
||||
:format-search-topic="formatSearchTopic"
|
||||
:format-search-time="formatSearchTime"
|
||||
:streaming-message="streamingMessage"
|
||||
:register-collapse-content="registerCollapseContent"
|
||||
:collapse-key="group.action.blockId || `${index}-tool-${group.actionIndex}`"
|
||||
@toggle="toggleBlock(group.action.blockId || `${index}-tool-${group.actionIndex}`)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@ -311,6 +313,8 @@
|
||||
:format-search-topic="formatSearchTopic"
|
||||
:format-search-time="formatSearchTime"
|
||||
:streaming-message="streamingMessage"
|
||||
:register-collapse-content="registerCollapseContent"
|
||||
:collapse-key="action.blockId || `${index}-tool-${actionIndex}`"
|
||||
@toggle="toggleBlock(action.blockId || `${index}-tool-${actionIndex}`)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -28,7 +28,7 @@ export const useConversationStore = defineStore('conversation', {
|
||||
hasMoreConversations: false,
|
||||
loadingMoreConversations: false,
|
||||
currentConversationId: null,
|
||||
currentConversationTitle: '当前对话',
|
||||
currentConversationTitle: '',
|
||||
searchQuery: '',
|
||||
searchTimer: null,
|
||||
conversationsOffset: 0,
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
/* 聊天容器整体布局,保证聊天区可见并支持上下滚动 */
|
||||
.chat-container {
|
||||
--chat-surface-color: var(--theme-surface-strong, #ffffff);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
background: var(--chat-surface-color);
|
||||
min-width: 0;
|
||||
height: var(--app-viewport, 100vh);
|
||||
overflow: hidden;
|
||||
@ -11,6 +12,53 @@
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.conversation-ribbon {
|
||||
position: absolute;
|
||||
inset: 0 0 auto 0;
|
||||
height: 38px;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
color: #0f172a;
|
||||
background: var(--chat-surface-color); /* 与对话区域保持一致且不透明 */
|
||||
box-shadow: none;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
backdrop-filter: none;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.conversation-ribbon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -8px;
|
||||
height: 16px;
|
||||
background: var(--chat-surface-color);
|
||||
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.6) 55%, rgba(0, 0, 0, 0) 100%);
|
||||
-webkit-mask-image: linear-gradient(180deg, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.6) 55%, rgba(0, 0, 0, 0) 100%);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.conversation-ribbon__text {
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
line-height: 1.4;
|
||||
letter-spacing: 0.02em;
|
||||
max-width: 60%;
|
||||
color: #0f172a;
|
||||
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-container.has-title-ribbon .messages-area {
|
||||
padding-top: 42px;
|
||||
}
|
||||
|
||||
.chat-container--monitor .virtual-monitor-surface {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user