Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 36 additions & 22 deletions docs/contracts/generated/chat-event-graph.json
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,12 @@
"modules/ui-system/appearance-studio.js",
"modules/ui-system/ask-nova-modal.js",
"modules/ui-system/avatar-picker.js",
"modules/ui-system/chat-back-to-bottom.js",
"modules/ui-system/chat-composer-inset.js",
"modules/ui-system/component-manifest.js",
"modules/ui-system/component-showcase.js",
"modules/ui-system/contribution-registry.js",
"modules/ui-system/conversation-turn-navigator.js",
"modules/ui-system/interactive-chat-app.js",
"modules/ui-system/lifecycle-inspector.js",
"modules/ui-system/lifecycle-scope.js",
Expand Down Expand Up @@ -3034,6 +3037,11 @@
}
],
"consumers": [
{
"file": "renderer.js",
"line": 662,
"kind": "custom-event-listener"
},
{
"file": "modules/event-listeners.js",
"line": 586,
Expand Down Expand Up @@ -3061,6 +3069,12 @@
}
],
"evidence": [
{
"file": "renderer.js",
"line": 662,
"kind": "custom-event-listener",
"match": "addEventListener('global-settings-updated'"
},
{
"file": "modules/event-listeners.js",
"line": 586,
Expand Down Expand Up @@ -4509,7 +4523,7 @@
"producers": [
{
"file": "modules/ui-helpers.js",
"line": 599,
"line": 639,
"kind": "custom-event-dispatch"
}
],
Expand Down Expand Up @@ -4567,7 +4581,7 @@
},
{
"file": "modules/ui-helpers.js",
"line": 599,
"line": 639,
"kind": "custom-event-dispatch",
"match": "dispatchEvent(new CustomEvent('modal-ready'"
},
Expand All @@ -4584,12 +4598,12 @@
"producers": [
{
"file": "modules/ui-helpers.js",
"line": 608,
"line": 648,
"kind": "custom-event-dispatch"
},
{
"file": "modules/ui-helpers.js",
"line": 630,
"line": 670,
"kind": "custom-event-dispatch"
}
],
Expand All @@ -4613,13 +4627,13 @@
"evidence": [
{
"file": "modules/ui-helpers.js",
"line": 608,
"line": 648,
"kind": "custom-event-dispatch",
"match": "dispatchEvent(new CustomEvent('modal-visibility-changed'"
},
{
"file": "modules/ui-helpers.js",
"line": 630,
"line": 670,
"kind": "custom-event-dispatch",
"match": "dispatchEvent(new CustomEvent('modal-visibility-changed'"
},
Expand Down Expand Up @@ -5124,7 +5138,7 @@
"consumers": [
{
"file": "renderer.js",
"line": 602,
"line": 606,
"kind": "preload-subscription"
},
{
Expand All @@ -5136,7 +5150,7 @@
"evidence": [
{
"file": "renderer.js",
"line": 602,
"line": 606,
"kind": "preload-subscription",
"match": "onStreamingEventConsumer("
},
Expand Down Expand Up @@ -5914,14 +5928,14 @@
"consumers": [
{
"file": "modules/ui-helpers.js",
"line": 347,
"line": 359,
"kind": "custom-event-listener"
}
],
"evidence": [
{
"file": "modules/ui-helpers.js",
"line": 347,
"line": 359,
"kind": "custom-event-listener",
"match": "addEventListener('pointercancel'"
}
Expand Down Expand Up @@ -7476,14 +7490,14 @@
"consumers": [
{
"file": "modules/ui-helpers.js",
"line": 335,
"line": 347,
"kind": "custom-event-listener"
}
],
"evidence": [
{
"file": "modules/ui-helpers.js",
"line": 335,
"line": 347,
"kind": "custom-event-listener",
"match": "addEventListener('touchcancel'"
}
Expand Down Expand Up @@ -7635,7 +7649,7 @@
"producers": [
{
"file": "renderer.js",
"line": 261,
"line": 265,
"kind": "custom-event-dispatch"
}
],
Expand All @@ -7649,7 +7663,7 @@
"evidence": [
{
"file": "renderer.js",
"line": 261,
"line": 265,
"kind": "custom-event-dispatch",
"match": "dispatchEvent(new CustomEvent('vcp-appearance-studio-configure'"
},
Expand All @@ -7666,7 +7680,7 @@
"producers": [
{
"file": "renderer.js",
"line": 251,
"line": 255,
"kind": "custom-event-dispatch"
}
],
Expand All @@ -7680,7 +7694,7 @@
"evidence": [
{
"file": "renderer.js",
"line": 251,
"line": 255,
"kind": "custom-event-dispatch",
"match": "dispatchEvent(new CustomEvent('vcp-appearance-studio-dispose'"
},
Expand Down Expand Up @@ -7716,15 +7730,15 @@
"producers": [
{
"file": "renderer.js",
"line": 571,
"line": 575,
"kind": "custom-event-dispatch"
}
],
"consumers": [],
"evidence": [
{
"file": "renderer.js",
"line": 571,
"line": 575,
"kind": "custom-event-dispatch",
"match": "dispatchEvent(new CustomEvent('vcp-chat-stream-terminal'"
}
Expand Down Expand Up @@ -8014,7 +8028,7 @@
"producers": [
{
"file": "renderer.js",
"line": 802,
"line": 837,
"kind": "custom-event-dispatch"
}
],
Expand All @@ -8028,7 +8042,7 @@
"evidence": [
{
"file": "renderer.js",
"line": 802,
"line": 837,
"kind": "custom-event-dispatch",
"match": "dispatchEvent(new CustomEvent('vcp-main-chat-commands-configure'"
},
Expand All @@ -8045,15 +8059,15 @@
"producers": [
{
"file": "renderer.js",
"line": 981,
"line": 1016,
"kind": "custom-event-dispatch"
}
],
"consumers": [],
"evidence": [
{
"file": "renderer.js",
"line": 981,
"line": 1016,
"kind": "custom-event-dispatch",
"match": "dispatchEvent(new CustomEvent('vcp-renderer-ready'"
}
Expand Down
3 changes: 3 additions & 0 deletions main.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
<link rel="stylesheet" href="vendor/fontawesome-v6/fontawesome-v6.css">
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="styles/compact-sidebar.css">
<link rel="stylesheet" href="styles/ui-system/turn-navigator.css">
<link rel="stylesheet" href="styles/ui-system/chat-back-to-bottom.css">
<link rel="stylesheet" href="styles/ui-system/chat-composer-inset.css">
<script src="modules/ui-system/lifecycle-scope.js"></script>
<script src="modules/ui-system/performance-recorder.js"></script>
<script src="modules/ui-system/task-handle.js"></script>
Expand Down
2 changes: 2 additions & 0 deletions modules/searchManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,8 @@ const searchManager = {

const messageEl = document.querySelector(`.message-item[data-message-id='${message.id}']`);
if (messageEl) {
// 切话题会重新打开粘底跟随;不先释放,长话题里平滑滚动途中消息增高,会被拽回底部。
this.uiHelper.releaseChatScrollFollow?.();
messageEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
messageEl.classList.add('message-highlight');
setTimeout(() => {
Expand Down
7 changes: 7 additions & 0 deletions modules/settings/schema/appearance-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,13 @@ export const appearanceSettingsSection = section('appearance-settings', '界面
when: ['chatPresentationModeBubble', 'enableUserChatBubbleUi'],
save: { present: true },
}),
switchField('enableTurnNavigator', {
label: '显示提问导航条',
hint: '聊天区左侧每条提问一根短横条,悬停预览、点击跳转。聊天区宽度不足 864px 时自动隐藏。',
hintInsideWrapper: true,
checked: true,
save: { present: true },
}),
numberCells('chatBubbleWideWidth', {
label: '宽屏模式自定义宽度(%)',
hint: '可设置范围为 50% - 98%。标准模式沿用系统默认宽度,这里只调整气泡模式的宽屏布局。',
Expand Down
54 changes: 47 additions & 7 deletions modules/ui-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
const textareaResizeStates = new WeakMap();
const chatScrollStates = new WeakMap();
const CHAT_BOTTOM_THRESHOLD_PX = 50;
const CHAT_FOLLOW_CHANGE_EVENT = 'vcp-chat-follow-change';
const REGEX_CACHE_MAX_ENTRIES = 512;
const regexCompileCache = new Map();
const filePreviewIconMarkup = `
Expand Down Expand Up @@ -272,6 +273,17 @@
return getDistanceFromChatBottom(container) <= threshold;
}

// 跟随状态每次真正翻转都在滚动容器上派发 CHAT_FOLLOW_CHANGE_EVENT;
// 「回到底部」按钮这类界面只订阅事件,不直接读写内部状态。
function setChatFollowBottom(container, state, followBottom) {
if (state.followBottom === followBottom) return;
state.followBottom = followBottom;
const EventCtor = container.ownerDocument?.defaultView?.CustomEvent;
if (typeof EventCtor === 'function') {
container.dispatchEvent(new EventCtor(CHAT_FOLLOW_CHANGE_EVENT, { detail: { followBottom } }));
}
}

function getChatScrollState(container) {
let state = chatScrollStates.get(container);
if (state) return state;
Expand Down Expand Up @@ -313,7 +325,7 @@
if (!container.isConnected || state.generation !== expectedGeneration) return;
const nearBottom = isChatNearBottom(container);
// 用户主动回到底部时重新授权持续跟随;离开底部则保持关闭。
state.followBottom = nearBottom;
setChatFollowBottom(container, state, nearBottom);
state.userScrollActive = false;
});
};
Expand All @@ -324,7 +336,7 @@
// 向上滚轮是明确的解锁意图。保持 userScrollActive,直到后续
// 向下滚动结算;这样该滚轮默认行为产生的近底部 scroll 事件
// 不会在同一轮事件中立刻把 followBottom 改回 true。
state.followBottom = false;
setChatFollowBottom(container, state, false);
} else {
settleUserScrollIntent();
}
Expand Down Expand Up @@ -356,7 +368,7 @@
// 更早到达的向上滚轮解锁意图。重新开启统一交给向下滚轮、
// touchend 或滚动条 pointerup 的代际保护结算。
if (!isChatNearBottom(container)) {
state.followBottom = false;
setChatFollowBottom(container, state, false);
}
return;
}
Expand All @@ -365,7 +377,7 @@
// 只有真正到达底部(而非落入 50px 追踪阈值)才重新开启跟随。
// 这同时防止小幅向上滚轮产生的 scroll 事件发生近底部竞态。
if (getDistanceFromChatBottom(container) <= 1) {
state.followBottom = true;
setChatFollowBottom(container, state, true);
}
}, { passive: true });

Expand Down Expand Up @@ -425,7 +437,7 @@
if (!container) return;
const state = getChatScrollState(container);
state.generation += 1;
state.followBottom = true;
setChatFollowBottom(container, state, true);
state.programmatic = false;
state.userScrollActive = false;
state.requestedGeneration = null;
Expand All @@ -439,6 +451,34 @@
}
};

/**
* Releases bottom-follow before a programmatic jump (search result, turn
* navigator). The jump itself is not user input, so follow would stay on and
* content-visibility messages that grow while the jump passes them would let
* the ResizeObserver pull the view back to the bottom. Reaching the bottom
* again re-enables follow as usual.
*/
uiHelperFunctions.releaseChatScrollFollow = function() {
const container = getChatScrollContainer();
if (!container) return;
const state = getChatScrollState(container);
state.generation += 1;
state.programmatic = false;
state.userScrollActive = false;
state.requestedGeneration = null;
if (state.frameId) {
cancelAnimationFrame(state.frameId);
state.frameId = 0;
}
if (state.layoutFrameId) {
cancelAnimationFrame(state.layoutFrameId);
state.layoutFrameId = 0;
}
setChatFollowBottom(container, state, false);
};

uiHelperFunctions.CHAT_FOLLOW_CHANGE_EVENT = CHAT_FOLLOW_CHANGE_EVENT;

/**
* Scrolls the chat Surface to the bottom when bottom-follow is active.
* Calls in one paint frame are normally coalesced. `force` bypasses the
Expand All @@ -465,7 +505,7 @@
return false;
}

state.followBottom = true;
setChatFollowBottom(container, state, true);
state.requestedGeneration = expectedGeneration ?? state.generation;

const commitScroll = () => {
Expand All @@ -487,7 +527,7 @@
// 之间完成解码并再次撑高内容;只要用户没有产生新滚动意图,
// ResizeObserver 会继续补偿到新的底部。
if (container.isConnected && isChatNearBottom(container)) {
state.followBottom = true;
setChatFollowBottom(container, state, true);
}
});
};
Expand Down
Loading
Loading