fix: stabilize show_html rendering and restore conversation mode state
This commit is contained in:
parent
33d270ff63
commit
383e68188c
@ -229,6 +229,9 @@ class WebTerminal(MainTerminal):
|
||||
"conversation_id": conversation_id,
|
||||
"title": conversation_data.get("title", "未知对话"),
|
||||
"messages_count": len(self.context_manager.conversation_history),
|
||||
"run_mode": self.run_mode,
|
||||
"thinking_mode": self.thinking_mode,
|
||||
"model_key": getattr(self, "model_key", None),
|
||||
"message": f"对话已加载: {conversation_id}"
|
||||
}
|
||||
else:
|
||||
|
||||
@ -53,9 +53,18 @@
|
||||
|
||||
如需在界面直接渲染 HTML 预览,使用 `<show_html ratio="比例">...</show_html>`。
|
||||
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
||||
- 各比例对应的前端基准渲染尺寸(宽 x 高)如下,请按这些尺寸去设计元素大小与间距:
|
||||
- `1:1` → `620x620`
|
||||
- `16:9` → `620x349`
|
||||
- `9:16` → `620x1102`
|
||||
- `4:3` → `620x465`
|
||||
- `3:4` → `620x827`
|
||||
- 说明:以上为基准像素,最终仍会按窗口与容器可用空间自适应裁剪
|
||||
- **禁止**输出 `width` / `height` 属性,最终像素尺寸由前端根据窗口自适应
|
||||
- 实际展示给用户的渲染后内容长宽都很小,可能只有几百 px;需主动使用合适的间距调整、合适的元素大小和距离(如 `padding`/`gap`/`line-height`)保证内容可读、不过度留白
|
||||
- 标签内容只写 **HTML + CSS**,不要写 JavaScript
|
||||
- 禁止输出 `<script>`、`onclick` 等事件脚本;前端会禁用脚本执行
|
||||
- 当用户说“给我画一个xxx”这类可视化诉求时,优先使用 `<show_html>` 标签直接实现,而不是先写一个 html 文件(除非用户明确要求落盘)
|
||||
|
||||
简单示例(正确格式):
|
||||
<show_html ratio="16:9">
|
||||
|
||||
@ -53,9 +53,18 @@
|
||||
|
||||
如需在界面直接渲染 HTML 预览,使用 `<show_html ratio="比例">...</show_html>`。
|
||||
- `ratio` 只能从以下五种里选:`1:1`、`16:9`、`9:16`、`4:3`、`3:4`
|
||||
- 各比例对应的前端基准渲染尺寸(宽 x 高)如下,请按这些尺寸去设计元素大小与间距:
|
||||
- `1:1` → `620x620`
|
||||
- `16:9` → `620x349`
|
||||
- `9:16` → `620x1102`
|
||||
- `4:3` → `620x465`
|
||||
- `3:4` → `620x827`
|
||||
- 说明:以上为基准像素,最终仍会按窗口与容器可用空间自适应裁剪
|
||||
- **禁止**输出 `width` / `height` 属性,最终像素尺寸由前端根据窗口自适应
|
||||
- 实际展示给用户的渲染后内容长宽都很小,可能只有几百 px;需主动使用合适的间距调整、合适的元素大小和距离(如 `padding`/`gap`/`line-height`)保证内容可读、不过度留白
|
||||
- 标签内容只写 **HTML + CSS**,不要写 JavaScript
|
||||
- 禁止输出 `<script>`、`onclick` 等事件脚本;前端会禁用脚本执行
|
||||
- 当用户说“给我画一个xxx”这类可视化诉求时,优先使用 `<show_html>` 标签直接实现,而不是先写一个 html 文件(除非用户明确要求落盘)
|
||||
|
||||
简单示例(正确格式):
|
||||
<show_html ratio="16:9">
|
||||
|
||||
@ -211,6 +211,7 @@ def get_user_resources(username: Optional[str] = None, workspace_id: Optional[st
|
||||
if has_request_context():
|
||||
session['run_mode'] = terminal.run_mode
|
||||
session['thinking_mode'] = terminal.thinking_mode
|
||||
session['model_key'] = getattr(terminal, "model_key", None)
|
||||
if is_api_user:
|
||||
session['workspace_id'] = getattr(workspace, "workspace_id", None)
|
||||
else:
|
||||
@ -424,6 +425,7 @@ def ensure_conversation_loaded(
|
||||
if has_request_context():
|
||||
session['run_mode'] = terminal.run_mode
|
||||
session['thinking_mode'] = terminal.thinking_mode
|
||||
session['model_key'] = getattr(terminal, "model_key", None)
|
||||
except Exception:
|
||||
pass
|
||||
# 应用对话级自定义 prompt / personalization(仅 API)。
|
||||
|
||||
@ -356,6 +356,9 @@ def load_conversation(conversation_id, terminal: WebTerminal, workspace: UserWor
|
||||
result = terminal.load_conversation(conversation_id)
|
||||
|
||||
if result["success"]:
|
||||
session['run_mode'] = terminal.run_mode
|
||||
session['thinking_mode'] = terminal.thinking_mode
|
||||
session['model_key'] = getattr(terminal, "model_key", None)
|
||||
normalized_id = _normalize_conv_id(conversation_id)
|
||||
if _is_host_mode_request(username):
|
||||
try:
|
||||
|
||||
@ -59,7 +59,9 @@ class TaskRecord:
|
||||
self.updated_at = self.created_at
|
||||
self.message = message
|
||||
self.conversation_id = conversation_id
|
||||
self.events: deque[Dict[str, Any]] = deque(maxlen=1000)
|
||||
# 刷新恢复时前端会从事件流重建进行中的输出,1000 在长流式回复下会过早截断,
|
||||
# 导致“只恢复最后几个字符”。这里提高缓冲上限,优先保证重建完整性。
|
||||
self.events: deque[Dict[str, Any]] = deque(maxlen=20000)
|
||||
self.thread: Optional[threading.Thread] = None
|
||||
self.error: Optional[str] = None
|
||||
self.model_key = model_key
|
||||
|
||||
@ -166,6 +166,14 @@ const SHOW_HTML_RATIO_VALUES: Record<string, number> = {
|
||||
'4:3': 4 / 3,
|
||||
'3:4': 3 / 4
|
||||
};
|
||||
// show_html 统一按比例对应预设基准像素渲染(前端仍会按可用空间自适应)
|
||||
const SHOW_HTML_RATIO_BASE_SIZE: Record<string, { width: number; height: number }> = {
|
||||
'1:1': { width: 620, height: 620 },
|
||||
'16:9': { width: 620, height: 349 },
|
||||
'9:16': { width: 620, height: 1102 },
|
||||
'4:3': { width: 620, height: 465 },
|
||||
'3:4': { width: 620, height: 827 }
|
||||
};
|
||||
|
||||
function normalizeShowHtmlRatio(raw: string | null) {
|
||||
if (!raw) return '1:1';
|
||||
@ -203,6 +211,7 @@ function readShowHtmlRatio(node: Element) {
|
||||
|
||||
function computeAdaptiveShowHtmlSize(node: Element, ratioKey: string) {
|
||||
const ratio = SHOW_HTML_RATIO_VALUES[ratioKey] || 1;
|
||||
const base = SHOW_HTML_RATIO_BASE_SIZE[ratioKey] || SHOW_HTML_RATIO_BASE_SIZE['1:1'];
|
||||
const windowW = Math.max(320, window.innerWidth || document.documentElement.clientWidth || 320);
|
||||
const windowH = Math.max(360, window.innerHeight || document.documentElement.clientHeight || 360);
|
||||
const parentRect = node.parentElement?.getBoundingClientRect();
|
||||
@ -216,28 +225,16 @@ function computeAdaptiveShowHtmlSize(node: Element, ratioKey: string) {
|
||||
220,
|
||||
Math.floor(Math.min(flowRect?.width || windowW, parentRect?.width || windowW) - 8)
|
||||
);
|
||||
const maxWidth = Math.max(220, Math.min(availableWidth, Math.floor(windowW * 0.88), 920));
|
||||
const minWidth = Math.min(maxWidth, Math.max(220, Math.floor(windowW * 0.34)));
|
||||
let widthPx = Math.max(minWidth, maxWidth);
|
||||
// 移除“二次约束”后:窗口/容器足够大时,严格使用 prompt 约定的基准像素;
|
||||
// 仅在物理空间不足时按比例等比缩小(不再做额外的最小/最大高度钳制)。
|
||||
// 最大卡片约束:上限按 1200x900(窗口不足时仍按可用空间缩放)
|
||||
const limitWidth = Math.max(220, Math.min(availableWidth, Math.floor(windowW * 0.96), 1200));
|
||||
const limitHeight = Math.max(180, Math.min(Math.floor(windowH * 0.9), 900));
|
||||
const widthScale = limitWidth / base.width;
|
||||
const heightScale = limitHeight / base.height;
|
||||
const scale = Math.min(1, widthScale, heightScale);
|
||||
let widthPx = Math.max(220, Math.round(base.width * scale));
|
||||
let heightPx = Math.round(widthPx / ratio);
|
||||
const maxHeight = Math.max(200, Math.min(900, Math.floor(windowH * 0.68)));
|
||||
const minHeight = Math.max(180, Math.min(280, Math.floor(windowH * 0.32)));
|
||||
if (heightPx > maxHeight) {
|
||||
heightPx = maxHeight;
|
||||
widthPx = Math.round(heightPx * ratio);
|
||||
}
|
||||
if (heightPx < minHeight) {
|
||||
heightPx = minHeight;
|
||||
widthPx = Math.round(heightPx * ratio);
|
||||
}
|
||||
if (widthPx > maxWidth) {
|
||||
widthPx = maxWidth;
|
||||
heightPx = Math.round(widthPx / ratio);
|
||||
}
|
||||
if (widthPx < minWidth) {
|
||||
widthPx = minWidth;
|
||||
heightPx = Math.round(widthPx / ratio);
|
||||
}
|
||||
return { widthPx, heightPx, ratio };
|
||||
}
|
||||
|
||||
@ -260,11 +257,16 @@ function sanitizeInlineCss(css: string) {
|
||||
|
||||
function rewriteCssSelectorsForShadowDom(css: string) {
|
||||
if (!css) return '';
|
||||
// 注意:不能直接把 html/body 连续替换成 .show-html-root,
|
||||
// 否则后续 /\bhtml\b/ 会再次命中 ".show-html-root" 里的 "html",产生
|
||||
// 类似 ".show-.show-html-root-root" 的错误选择器,导致布局样式失效(元素偏移)。
|
||||
const placeholder = '__SHOW_HTML_ROOT_SELECTOR__';
|
||||
return css
|
||||
.replace(/\bhtml\s*,\s*body\b/gi, '.show-html-root')
|
||||
.replace(/\bbody\s*,\s*html\b/gi, '.show-html-root')
|
||||
.replace(/\bhtml\b/gi, '.show-html-root')
|
||||
.replace(/\bbody\b/gi, '.show-html-root');
|
||||
.replace(/\bhtml\s*,\s*body\b/gi, placeholder)
|
||||
.replace(/\bbody\s*,\s*html\b/gi, placeholder)
|
||||
.replace(/\bhtml\b/gi, placeholder)
|
||||
.replace(/\bbody\b/gi, placeholder)
|
||||
.replace(new RegExp(placeholder, 'g'), '.show-html-root');
|
||||
}
|
||||
|
||||
function sanitizeShowHtmlContent(rawHtml: string) {
|
||||
@ -437,15 +439,51 @@ function renderShowImages(root: ParentNode | null = document) {
|
||||
if (!node.isConnected) return;
|
||||
const inStreaming = !!node.closest('.streaming-text');
|
||||
const pathKey = buildNodePathKey(node);
|
||||
const isPartial = node.getAttribute('data-partial') === '1';
|
||||
const encoded = node.getAttribute('data-encoded') || '';
|
||||
const computedRatioKey = readShowHtmlRatio(node);
|
||||
let ratioKey = computedRatioKey;
|
||||
let { widthPx, heightPx } = computeAdaptiveShowHtmlSize(node, ratioKey);
|
||||
|
||||
// 流式阶段:一旦 show_html 已经出现完整闭合(非 partial),冻结该 path 的已完成快照;
|
||||
// 后续即使继续输出其他普通文本,也复用该快照,避免已完成 show_html 反复刷新。
|
||||
if (inStreaming && !isPartial && encoded) {
|
||||
const frozen = showHtmlCompletedSnapshotByPath.get(pathKey);
|
||||
if (!frozen || frozen.encoded !== encoded || frozen.ratioKey !== ratioKey) {
|
||||
showHtmlCompletedSnapshotByPath.set(pathKey, {
|
||||
encoded,
|
||||
ratioKey
|
||||
});
|
||||
}
|
||||
} else if (!inStreaming) {
|
||||
showHtmlCompletedSnapshotByPath.delete(pathKey);
|
||||
}
|
||||
const frozenSnapshot = inStreaming ? showHtmlCompletedSnapshotByPath.get(pathKey) : null;
|
||||
const effectiveEncoded = frozenSnapshot?.encoded || encoded;
|
||||
if (frozenSnapshot?.ratioKey) {
|
||||
ratioKey = frozenSnapshot.ratioKey;
|
||||
({ widthPx, heightPx } = computeAdaptiveShowHtmlSize(node, ratioKey));
|
||||
}
|
||||
|
||||
if (inStreaming) {
|
||||
const locked = showHtmlStreamingSizeLockByPath.get(pathKey);
|
||||
if (locked) {
|
||||
ratioKey = locked.ratioKey;
|
||||
widthPx = locked.widthPx;
|
||||
heightPx = locked.heightPx;
|
||||
// 修复:流式初期若 ratio 解析不完整会先落到 1:1,后续必须允许升级到真实比例
|
||||
const shouldUpgradeRatio =
|
||||
ratioKey !== locked.ratioKey && (locked.ratioKey === '1:1' || ratioKey !== '1:1');
|
||||
if (shouldUpgradeRatio) {
|
||||
ratioKey = computedRatioKey;
|
||||
({ widthPx, heightPx } = computeAdaptiveShowHtmlSize(node, ratioKey));
|
||||
showHtmlStreamingSizeLockByPath.set(pathKey, {
|
||||
ratioKey,
|
||||
widthPx,
|
||||
heightPx
|
||||
});
|
||||
} else {
|
||||
ratioKey = locked.ratioKey;
|
||||
widthPx = locked.widthPx;
|
||||
heightPx = locked.heightPx;
|
||||
}
|
||||
} else {
|
||||
showHtmlStreamingSizeLockByPath.set(pathKey, {
|
||||
ratioKey,
|
||||
@ -474,7 +512,8 @@ function renderShowImages(root: ParentNode | null = document) {
|
||||
ratioAttr: node.getAttribute('ratio') || '',
|
||||
ratioKey,
|
||||
partial: node.getAttribute('data-partial') || '',
|
||||
encodedLength: (node.getAttribute('data-encoded') || '').length,
|
||||
encodedLength: encoded.length,
|
||||
effectiveEncodedLength: effectiveEncoded.length,
|
||||
innerLength: (node.innerHTML || '').length,
|
||||
reservedWidth: `${widthPx}px`,
|
||||
reservedMinHeight: `${heightPx}px`
|
||||
@ -511,13 +550,13 @@ function renderShowImages(root: ParentNode | null = document) {
|
||||
showHtmlStreamingRenderTsByPath.delete(pathKey);
|
||||
}
|
||||
|
||||
const encoded = node.getAttribute('data-encoded') || '';
|
||||
const rawHtml = encoded ? decodeBase64Utf8(encoded) : node.innerHTML || '';
|
||||
const rawHtml = effectiveEncoded ? decodeBase64Utf8(effectiveEncoded) : node.innerHTML || '';
|
||||
const safeHtml = sanitizeShowHtmlContent(rawHtml);
|
||||
debugShowHtmlLog('render:node-content', {
|
||||
renderId,
|
||||
pathKey,
|
||||
encodedLength: encoded.length,
|
||||
effectiveEncodedLength: effectiveEncoded.length,
|
||||
rawLength: rawHtml.length,
|
||||
safeLength: safeHtml.length,
|
||||
ratioKey,
|
||||
@ -539,7 +578,7 @@ function renderShowImages(root: ParentNode | null = document) {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
@ -548,13 +587,12 @@ function renderShowImages(root: ParentNode | null = document) {
|
||||
}
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
}
|
||||
.show-html-root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
background: transparent;
|
||||
}
|
||||
`;
|
||||
@ -578,20 +616,20 @@ function renderShowImages(root: ParentNode | null = document) {
|
||||
persistent.wrapper.style.width = `${widthPx}px`;
|
||||
persistent.host.style.height = `${heightPx}px`;
|
||||
if (
|
||||
persistent.encoded !== encoded ||
|
||||
persistent.encoded !== effectiveEncoded ||
|
||||
persistent.widthPx !== widthPx ||
|
||||
persistent.heightPx !== heightPx ||
|
||||
persistent.ratioKey !== ratioKey
|
||||
) {
|
||||
persistent.root.innerHTML = safeHtml;
|
||||
persistent.encoded = encoded;
|
||||
persistent.encoded = effectiveEncoded;
|
||||
persistent.widthPx = widthPx;
|
||||
persistent.heightPx = heightPx;
|
||||
persistent.ratioKey = ratioKey;
|
||||
debugShowHtmlLog('render:node-persistent-update', {
|
||||
renderId,
|
||||
pathKey,
|
||||
encodedLength: encoded.length,
|
||||
encodedLength: effectiveEncoded.length,
|
||||
ratioKey,
|
||||
widthPx,
|
||||
heightPx
|
||||
@ -600,7 +638,7 @@ function renderShowImages(root: ParentNode | null = document) {
|
||||
debugShowHtmlLog('render:node-persistent-reuse', {
|
||||
renderId,
|
||||
pathKey,
|
||||
encodedLength: encoded.length
|
||||
encodedLength: effectiveEncoded.length
|
||||
});
|
||||
}
|
||||
|
||||
@ -608,12 +646,12 @@ function renderShowImages(root: ParentNode | null = document) {
|
||||
node.setAttribute('data-rendered', '1');
|
||||
const rect = persistent.wrapper.getBoundingClientRect();
|
||||
debugShowHtmlLog('render:node-done', {
|
||||
renderId,
|
||||
pathKey,
|
||||
ratioKey,
|
||||
widthPx,
|
||||
heightPx,
|
||||
rect: {
|
||||
renderId,
|
||||
pathKey,
|
||||
ratioKey,
|
||||
widthPx,
|
||||
heightPx,
|
||||
rect: {
|
||||
x: Math.round(rect.x),
|
||||
y: Math.round(rect.y),
|
||||
w: Math.round(rect.width),
|
||||
@ -647,6 +685,13 @@ let showTagBindRetryTimer: number | null = null;
|
||||
let showTagObservedContainer: Element | null = null;
|
||||
const showHtmlStreamingRenderTsByPath = new Map<string, number>();
|
||||
const SHOW_HTML_STREAMING_RENDER_INTERVAL_MS = 180;
|
||||
const showHtmlCompletedSnapshotByPath = new Map<
|
||||
string,
|
||||
{
|
||||
encoded: string,
|
||||
ratioKey: string
|
||||
}
|
||||
>();
|
||||
const showHtmlStreamingSizeLockByPath = new Map<
|
||||
string,
|
||||
{
|
||||
@ -963,6 +1008,7 @@ export function teardownShowImageObserver() {
|
||||
layoutDebugCount = 0;
|
||||
layoutDebugLastTsByKey.clear();
|
||||
showHtmlStreamingRenderTsByPath.clear();
|
||||
showHtmlCompletedSnapshotByPath.clear();
|
||||
showHtmlStreamingSizeLockByPath.clear();
|
||||
showHtmlPersistentRenderByPath.clear();
|
||||
showTagObservedContainer = null;
|
||||
|
||||
@ -261,34 +261,7 @@ export const conversationMethods = {
|
||||
}
|
||||
}
|
||||
|
||||
// 应用个性化设置中的默认模型和思考模式
|
||||
try {
|
||||
const personalizationStore = usePersonalizationStore();
|
||||
|
||||
if (personalizationStore.loaded) {
|
||||
const defaultRunMode = personalizationStore.form.default_run_mode;
|
||||
const defaultModel = personalizationStore.form.default_model;
|
||||
|
||||
if (defaultRunMode) {
|
||||
this.runMode = defaultRunMode;
|
||||
debugLog('应用默认运行模式:', defaultRunMode);
|
||||
}
|
||||
|
||||
if (defaultModel) {
|
||||
this.currentModelKey = defaultModel;
|
||||
debugLog('应用默认模型:', defaultModel);
|
||||
}
|
||||
|
||||
// 根据默认运行模式设置思考模式
|
||||
if (defaultRunMode === 'thinking') {
|
||||
this.thinkingMode = true;
|
||||
} else if (defaultRunMode === 'fast') {
|
||||
this.thinkingMode = false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('应用个性化默认设置失败:', error);
|
||||
}
|
||||
// 注意:加载已有对话时必须保留该对话自身的模型/模式,不能套用用户默认值。
|
||||
|
||||
// 有任务或后台子智能体运行时,提示用户确认切换
|
||||
try {
|
||||
@ -343,6 +316,19 @@ export const conversationMethods = {
|
||||
if (result.success) {
|
||||
debugLog('对话加载API成功:', result);
|
||||
traceLog('loadConversation:api-success', { conversationId, title: result.title });
|
||||
if (typeof result.run_mode === 'string') {
|
||||
this.runMode = result.run_mode;
|
||||
this.thinkingMode =
|
||||
typeof result.thinking_mode === 'boolean'
|
||||
? result.thinking_mode
|
||||
: result.run_mode !== 'fast';
|
||||
} else if (typeof result.thinking_mode === 'boolean') {
|
||||
this.thinkingMode = result.thinking_mode;
|
||||
this.runMode = result.thinking_mode ? 'thinking' : 'fast';
|
||||
}
|
||||
if (typeof result.model_key === 'string' && result.model_key) {
|
||||
this.modelSet(result.model_key);
|
||||
}
|
||||
|
||||
// 2. 更新当前对话信息
|
||||
this.skipConversationHistoryReload = true;
|
||||
|
||||
@ -1601,6 +1601,19 @@ export const uiMethods = {
|
||||
const resp = await fetch(`/api/conversations/${convId}/load`, { method: 'PUT' });
|
||||
const result = await resp.json();
|
||||
if (result.success) {
|
||||
if (typeof result.run_mode === 'string') {
|
||||
this.runMode = result.run_mode;
|
||||
this.thinkingMode =
|
||||
typeof result.thinking_mode === 'boolean'
|
||||
? result.thinking_mode
|
||||
: result.run_mode !== 'fast';
|
||||
} else if (typeof result.thinking_mode === 'boolean') {
|
||||
this.thinkingMode = result.thinking_mode;
|
||||
this.runMode = result.thinking_mode ? 'thinking' : 'fast';
|
||||
}
|
||||
if (typeof result.model_key === 'string' && result.model_key) {
|
||||
this.modelSet(result.model_key);
|
||||
}
|
||||
this.currentConversationId = convId;
|
||||
this.currentConversationTitle = result.title || '';
|
||||
this.titleReady = true;
|
||||
@ -1858,8 +1871,14 @@ export const uiMethods = {
|
||||
});
|
||||
}
|
||||
|
||||
// 应用个性化设置中的默认模型和思考模式
|
||||
if (personalizationStore.loaded) {
|
||||
// 仅在“无当前对话”的新会话入口应用默认模型/模式;
|
||||
// 对于已存在对话(含刷新恢复/URL指定对话),必须保留对话自身设置。
|
||||
const statusConversationIdForDefaults = statusData?.conversation?.current_id;
|
||||
const hasActiveConversation =
|
||||
typeof statusConversationIdForDefaults === 'string' &&
|
||||
statusConversationIdForDefaults.length > 0 &&
|
||||
!statusConversationIdForDefaults.startsWith('temp_');
|
||||
if (personalizationStore.loaded && !hasActiveConversation && !this.currentConversationId) {
|
||||
const defaultRunMode = personalizationStore.form.default_run_mode;
|
||||
const defaultModel = personalizationStore.form.default_model;
|
||||
|
||||
|
||||
@ -281,6 +281,9 @@ export function scrollToBottom(ctx: ScrollContext, options?: ScrollOptions) {
|
||||
|
||||
let settleCount = 0;
|
||||
let lastHeight = -1;
|
||||
const settleStartTs = Date.now();
|
||||
const settleMaxCount = drawingActive ? 30 : 12;
|
||||
const settleMaxDurationMs = drawingActive ? 3200 : 1200;
|
||||
const settleScroll = () => {
|
||||
settleCount += 1;
|
||||
messagesArea.scrollTop =
|
||||
@ -290,11 +293,15 @@ export function scrollToBottom(ctx: ScrollContext, options?: ScrollOptions) {
|
||||
const remain = currentHeight - messagesArea.scrollTop - messagesArea.clientHeight;
|
||||
const heightStable = Math.abs(currentHeight - lastHeight) <= 2;
|
||||
const atBottom = Math.abs(remain) <= 2;
|
||||
const settleElapsed = Date.now() - settleStartTs;
|
||||
const shouldContinue =
|
||||
!atBottom && settleCount < settleMaxCount && settleElapsed < settleMaxDurationMs;
|
||||
scrollDebugLog(
|
||||
'scrollToBottom:settle',
|
||||
{
|
||||
traceId,
|
||||
settleCount,
|
||||
settleElapsed,
|
||||
currentHeight,
|
||||
lastHeight,
|
||||
remain,
|
||||
@ -307,7 +314,7 @@ export function scrollToBottom(ctx: ScrollContext, options?: ScrollOptions) {
|
||||
80
|
||||
);
|
||||
lastHeight = currentHeight;
|
||||
if (settleCount < 7 && (!heightStable || !atBottom)) {
|
||||
if (shouldContinue) {
|
||||
if (
|
||||
(drawingActive || options?.force) &&
|
||||
(settleCount === 1 || settleCount === 4 || settleCount === 6)
|
||||
|
||||
@ -937,7 +937,7 @@ body[data-theme='dark'] .more-icon {
|
||||
margin: 14px auto;
|
||||
border: 1px solid var(--claude-border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
background: transparent;
|
||||
box-shadow: var(--claude-shadow);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user