// @ts-nocheck import katex from 'katex'; import DOMPurify from 'dompurify'; let showImageRenderSeq = 0; let showImageDebugLogCount = 0; const SHOW_IMAGE_DEBUG_VERSION = 'v2-lite-throttled'; const SHOW_IMAGE_DEBUG_MAX_LOGS = 200; let showHtmlDebugLogCount = 0; const SHOW_HTML_DEBUG_MAX_LOGS = 500; function getShowImageDebugMode(): 'off' | 'lite' | 'verbose' { if (typeof window === 'undefined') return 'off'; try { const explicitFlag = (window as any).__SHOW_IMAGE_DEBUG__; if ( explicitFlag === false || explicitFlag === '0' || explicitFlag === 'off' || explicitFlag === 'false' ) { return 'off'; } if (explicitFlag === 'verbose') return 'verbose'; if (explicitFlag === true || explicitFlag === '1' || explicitFlag === 'lite') return 'lite'; const localFlag = window.localStorage?.getItem('showImageDebug'); if (localFlag === '0' || localFlag === 'off' || localFlag === 'false' || localFlag === 'none') { return 'off'; } if (localFlag === 'verbose') return 'verbose'; if (localFlag === '1' || localFlag === 'true' || localFlag === 'lite') return 'lite'; return 'off'; } catch { return 'off'; } } function debugShowImageLog(event: string, payload: Record = {}) { const mode = getShowImageDebugMode(); if (mode === 'off') return; if ( mode === 'lite' && !['observer:setup', 'observer:show-image-mutation', 'render:start', 'render:end'].includes( event ) ) { return; } if (showImageDebugLogCount >= SHOW_IMAGE_DEBUG_MAX_LOGS) return; showImageDebugLogCount += 1; if (showImageDebugLogCount === SHOW_IMAGE_DEBUG_MAX_LOGS) { console.warn('[SHOW_IMAGE_DEBUG]', 'log-limit-reached', { max: SHOW_IMAGE_DEBUG_MAX_LOGS, version: SHOW_IMAGE_DEBUG_VERSION }); return; } console.log('[SHOW_IMAGE_DEBUG]', event, payload); } function isShowHtmlDebugEnabled() { if (typeof window === 'undefined') return true; try { const explicitFlag = (window as any).__SHOW_HTML_DEBUG__; if (explicitFlag === false || explicitFlag === '0') return false; if (explicitFlag === true || explicitFlag === '1') return true; const localFlag = window.localStorage?.getItem('showHtmlDebug'); if (localFlag === '0' || localFlag === 'false') return false; if (localFlag === '1' || localFlag === 'true') return true; return true; } catch { return true; } } function debugShowHtmlLog(event: string, payload: Record = {}) { if (!isShowHtmlDebugEnabled()) return; if (showHtmlDebugLogCount >= SHOW_HTML_DEBUG_MAX_LOGS) return; showHtmlDebugLogCount += 1; if (showHtmlDebugLogCount === SHOW_HTML_DEBUG_MAX_LOGS) { console.warn('[SHOW_HTML_DEBUG]', 'log-limit-reached', { max: SHOW_HTML_DEBUG_MAX_LOGS }); return; } console.log('[SHOW_HTML_DEBUG]', event, payload); } function markShowTagDrawingActive(holdMs = 1600) { if (typeof window === 'undefined') return; try { (window as any).__SHOW_TAG_DRAWING_UNTIL__ = Date.now() + holdMs; } catch { // ignore } } function summarizeNodeForDebug(node: Node | null) { if (!node) return 'null'; if (node.nodeType === Node.TEXT_NODE) { const text = (node.textContent || '').replace(/\s+/g, ' ').trim(); return `TEXT(${text.slice(0, 80)})`; } if (node.nodeType === Node.ELEMENT_NODE) { const el = node as Element; const tag = el.tagName.toLowerCase(); if (tag === 'show_image' || tag === 'show-image') { return `SHOW_IMAGE(src=${el.getAttribute('src') || ''})`; } return `${tag.toUpperCase()}`; } return `NODE(${node.nodeType})`; } function collectShowImageOrder(root: ParentNode | null = document) { if (!root || !(root instanceof Node)) return []; const walker = document.createTreeWalker(root, NodeFilter.SHOW_ALL); const tokens: string[] = []; let current = walker.currentNode; while (current) { if (current.nodeType === Node.TEXT_NODE) { const text = (current.textContent || '').replace(/\s+/g, ' ').trim(); if (text) tokens.push(`TEXT(${text.slice(0, 40)})`); } else if (current.nodeType === Node.ELEMENT_NODE) { const el = current as Element; const tag = el.tagName.toLowerCase(); if (tag === 'show_image' || tag === 'show-image') { tokens.push(`SHOW_IMAGE(${el.getAttribute('src') || ''})`); } else if (tag === 'show_html' || tag === 'show-html') { tokens.push(`SHOW_HTML(ratio=${readShowHtmlRatio(el)},js=${readShowHtmlJsMode(el)})`); } } current = walker.nextNode(); } return tokens.slice(0, 200); } function normalizeShowImageSrc(src: string) { if (!src) return ''; const trimmed = src.trim(); if (/^https?:\/\//i.test(trimmed)) return trimmed; if (trimmed.startsWith('/user_upload/')) return trimmed; // 兼容容器内部路径:/workspace/.../user_upload/xxx.png 或 /workspace/user_upload/xxx const idx = trimmed.toLowerCase().indexOf('/user_upload/'); if (idx >= 0) { return '/user_upload/' + trimmed.slice(idx + '/user_upload/'.length); } if (trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../')) { return trimmed; } return ''; } function parsePixelSize(raw: string | null, fallback: number) { if (!raw) return fallback; const n = Number.parseInt(raw.trim(), 10); if (!Number.isFinite(n) || n <= 0) return fallback; return Math.min(4096, n); } const SHOW_HTML_ALLOWED_RATIOS = ['1:1', '16:9', '9:16', '4:3', '3:4'] as const; const SHOW_HTML_RATIO_VALUES: Record = { '1:1': 1, '16:9': 16 / 9, '9:16': 9 / 16, '4:3': 4 / 3, '3:4': 3 / 4 }; // show_html 统一按比例对应预设基准像素渲染(前端仍会按可用空间自适应) const SHOW_HTML_RATIO_BASE_SIZE: Record = { '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'; const cleaned = raw.trim().toLowerCase().replace(/[x/]/g, ':').replace(/\s+/g, ''); if ((SHOW_HTML_ALLOWED_RATIOS as readonly string[]).includes(cleaned)) return cleaned; return '1:1'; } function pickNearestAllowedRatio(width: number, height: number) { if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { return '1:1'; } const target = width / height; let best = '1:1'; let bestDiff = Number.POSITIVE_INFINITY; for (const ratio of SHOW_HTML_ALLOWED_RATIOS) { const diff = Math.abs(SHOW_HTML_RATIO_VALUES[ratio] - target); if (diff < bestDiff) { bestDiff = diff; best = ratio; } } return best; } function readShowHtmlRatio(node: Element) { const ratioAttr = node.getAttribute('ratio'); if (ratioAttr) return normalizeShowHtmlRatio(ratioAttr); // 兼容旧格式:模型输出了 width/height 时,自动映射到允许的比例 const width = parsePixelSize(node.getAttribute('width'), 0); const height = parsePixelSize(node.getAttribute('height'), 0); if (width > 0 && height > 0) return pickNearestAllowedRatio(width, height); return '1:1'; } function normalizeShowHtmlJsMode(raw: string | null) { if (!raw) return 'off'; const cleaned = raw.trim().toLowerCase(); if (['on', '1', 'true', 'yes', 'enabled', 'enable'].includes(cleaned)) { return 'on'; } return 'off'; } function readShowHtmlJsMode(node: Element) { return normalizeShowHtmlJsMode(node.getAttribute('js')); } 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(); const flowRect = node.closest('.messages-flow')?.getBoundingClientRect() || node.closest('.message-text')?.getBoundingClientRect() || node.closest('.text-content')?.getBoundingClientRect() || node.closest('.messages-area')?.getBoundingClientRect() || null; const availableWidth = Math.max( 220, Math.floor(Math.min(flowRect?.width || windowW, parentRect?.width || windowW) - 8) ); // 移除“二次约束”后:窗口/容器足够大时,严格使用 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); const widthPx = Math.max(220, Math.round(base.width * scale)); const heightPx = Math.round(widthPx / ratio); return { widthPx, heightPx, ratio }; } function decodeBase64Utf8(input: string) { if (!input) return ''; try { return decodeURIComponent(escape(atob(input))); } catch { return ''; } } function sanitizeInlineCss(css: string) { if (!css) return ''; return css .replace(/expression\s*\([^)]*\)/gi, '') .replace(/url\s*\(\s*['"]?\s*javascript:[^)]+\)/gi, 'url(about:blank)') .replace(/@import[^;]+;/gi, ''); } 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, 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) { const purified = DOMPurify.sanitize(rawHtml || '', { USE_PROFILES: { html: true, svg: true, svgFilters: true, mathMl: true }, FORBID_TAGS: ['script', 'noscript', 'iframe', 'object', 'embed', 'link'], ALLOW_DATA_ATTR: false, ALLOW_ARIA_ATTR: true }) as string; const parser = new DOMParser(); const doc = parser.parseFromString(`${purified}`, 'text/html'); const elements = Array.from(doc.body.querySelectorAll('*')); elements.forEach((el) => { Array.from(el.attributes).forEach((attr) => { const name = attr.name.toLowerCase(); const value = attr.value || ''; if (name.startsWith('on')) { el.removeAttribute(attr.name); return; } if ( (name === 'src' || name === 'href' || name === 'xlink:href') && /^\s*javascript:/i.test(value) ) { el.removeAttribute(attr.name); return; } if (name === 'style') { el.setAttribute('style', sanitizeInlineCss(value)); } }); }); doc.querySelectorAll('style').forEach((styleEl) => { styleEl.textContent = rewriteCssSelectorsForShadowDom( sanitizeInlineCss(styleEl.textContent || '') ); }); return doc.body.innerHTML; } function buildShowHtmlIframeSrcdoc(rawHtml: string) { const content = (rawHtml || '').trim(); if (!content) { return ''; } // 若模型已经输出完整 HTML 文档,直接使用;否则包裹成最小可运行文档。 if (/]/i.test(content)) { return content; } return ` ${content} `; } function escapeHtml(input: string) { return input .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function renderShowImages(root: ParentNode | null = document) { if (!root) return; const debugMode = getShowImageDebugMode(); const verbose = debugMode === 'verbose'; const renderId = ++showImageRenderSeq; const imageNodes = Array.from( root.querySelectorAll('show_image:not([data-rendered]), show-image:not([data-rendered])') ); const htmlNodes = Array.from( root.querySelectorAll('show_html:not([data-rendered]), show-html:not([data-rendered])') ); if (htmlNodes.length > 0) { markShowTagDrawingActive(); } if (!verbose && imageNodes.length === 0 && htmlNodes.length === 0) return; const rootNode = root as Node; debugShowImageLog('render:start', { renderId, rootType: rootNode?.nodeType === Node.ELEMENT_NODE ? (rootNode as Element).tagName.toLowerCase() : `nodeType:${rootNode?.nodeType}`, pendingImageCount: imageNodes.length, pendingHtmlCount: htmlNodes.length, orderBefore: collectShowImageOrder(root) }); if (verbose) { debugShowImageLog('render:pending-show-images', { renderId, count: imageNodes.length, nodes: imageNodes.map((n) => n.getAttribute('src') || '') }); } imageNodes.forEach((node) => { if (!node.isConnected || node.hasAttribute('data-rendered')) return; if (verbose) { debugShowImageLog('render:node-start', { renderId, src: node.getAttribute('src') || '', parent: summarizeNodeForDebug(node.parentNode), prevSibling: summarizeNodeForDebug(node.previousSibling), nextSibling: summarizeNodeForDebug(node.nextSibling), childCount: node.childNodes.length, childPreview: Array.from(node.childNodes) .slice(0, 6) .map((child) => summarizeNodeForDebug(child)) }); } // 将 show_image 内误被包裹的内容移动到当前节点之后,保持原有顺序 if (node.parentNode && node.firstChild) { const parent = node.parentNode; const ref = node.nextSibling; // 可能为 null,insertBefore 会当 append const children = Array.from(node.childNodes); if (verbose) { debugShowImageLog('render:unwrap-children', { renderId, src: node.getAttribute('src') || '', moveCount: children.length, ref: summarizeNodeForDebug(ref), children: children.slice(0, 10).map((child) => summarizeNodeForDebug(child)) }); } children.forEach((child) => parent.insertBefore(child, ref)); } const rawSrc = node.getAttribute('src') || ''; const mappedSrc = normalizeShowImageSrc(rawSrc); if (!mappedSrc) { node.setAttribute('data-rendered', '1'); node.setAttribute('data-rendered-error', 'invalid-src'); if (verbose) debugShowImageLog('render:invalid-src', { renderId, rawSrc }); return; } const alt = node.getAttribute('alt') || ''; const safeAlt = escapeHtml(alt.trim()); const figure = document.createElement('figure'); figure.className = 'chat-inline-card chat-inline-card--image chat-inline-image'; const img = document.createElement('img'); img.loading = 'lazy'; img.src = mappedSrc; img.alt = safeAlt; img.onerror = () => { figure.classList.add('chat-inline-card--error', 'chat-inline-image--error'); const tip = document.createElement('div'); tip.className = 'chat-inline-card__error chat-inline-image__error'; tip.textContent = '图片加载失败'; figure.appendChild(tip); }; figure.appendChild(img); if (safeAlt) { const caption = document.createElement('figcaption'); caption.className = 'chat-inline-card__caption'; caption.innerHTML = safeAlt; figure.appendChild(caption); } node.replaceChildren(figure); node.setAttribute('data-rendered', '1'); if (verbose) { debugShowImageLog('render:node-done', { renderId, rawSrc, mappedSrc, alt }); } }); htmlNodes.forEach((node) => { markShowTagDrawingActive(); 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 jsMode = readShowHtmlJsMode(node); 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) { // 修复:流式初期若 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, widthPx, heightPx }); } } else { showHtmlStreamingSizeLockByPath.delete(pathKey); } // streaming 阶段 v-html 会反复重建 show_html 节点,这里先锁定占位尺寸,避免高度掉 0 node.style.display = 'block'; node.style.width = `${widthPx}px`; node.style.maxWidth = '100%'; node.style.minHeight = `${heightPx}px`; node.style.margin = '14px auto'; node.style.boxSizing = 'border-box'; node.style.overflow = 'hidden'; debugShowHtmlLog('render:node-seen', { renderId, inStreaming, pathKey, hasRendered: node.hasAttribute('data-rendered'), ratioAttr: node.getAttribute('ratio') || '', jsMode, ratioKey, partial: node.getAttribute('data-partial') || '', encodedLength: encoded.length, effectiveEncodedLength: effectiveEncoded.length, innerLength: (node.innerHTML || '').length, reservedWidth: `${widthPx}px`, reservedMinHeight: `${heightPx}px` }); // js="on":在闭合前只显示占位,不做实时渲染,避免 iframe/srcdoc 反复重建与闪烁 if (jsMode === 'on' && isPartial) { let pending = showHtmlJsPendingRenderByPath.get(pathKey); if (!pending) { const wrapper = document.createElement('div'); wrapper.className = 'chat-inline-card chat-inline-card--html chat-inline-card--pending chat-inline-html chat-inline-html--pending'; const host = document.createElement('div'); host.className = 'chat-inline-card__body chat-inline-html__host chat-inline-html__host--pending'; const tip = document.createElement('div'); tip.className = 'chat-inline-card__pending-tip chat-inline-html__pending-tip'; tip.textContent = '正在渲染内容...'; host.appendChild(tip); wrapper.appendChild(host); pending = { wrapper, host, tip, widthPx: 0, heightPx: 0, ratioKey: '1:1' }; showHtmlJsPendingRenderByPath.set(pathKey, pending); } pending.wrapper.style.width = `${widthPx}px`; pending.host.style.height = `${heightPx}px`; pending.widthPx = widthPx; pending.heightPx = heightPx; pending.ratioKey = ratioKey; node.replaceChildren(pending.wrapper); node.setAttribute('data-rendered', '1'); debugShowHtmlLog('render:node-js-pending', { renderId, pathKey, ratioKey, widthPx, heightPx }); return; } // js="on":闭合后使用 sandbox iframe 隔离执行(不依赖 shadow root 直渲染)。 if (jsMode === 'on') { const rawHtml = effectiveEncoded ? decodeBase64Utf8(effectiveEncoded) : node.innerHTML || ''; const srcdoc = buildShowHtmlIframeSrcdoc(rawHtml); let persistent = showHtmlJsIframeRenderByPath.get(pathKey); if (!persistent) { const wrapper = document.createElement('div'); wrapper.className = 'chat-inline-card chat-inline-card--html chat-inline-card--iframe chat-inline-html chat-inline-html--iframe'; const iframe = document.createElement('iframe'); iframe.className = 'chat-inline-html__iframe'; iframe.setAttribute('sandbox', 'allow-scripts'); iframe.setAttribute('referrerpolicy', 'no-referrer'); iframe.setAttribute('loading', 'lazy'); wrapper.appendChild(iframe); persistent = { encoded: '', ratioKey: '1:1', widthPx: 0, heightPx: 0, srcdoc: '', wrapper, iframe }; showHtmlJsIframeRenderByPath.set(pathKey, persistent); } persistent.wrapper.style.width = `${widthPx}px`; persistent.iframe.style.height = `${heightPx}px`; const needsUpdate = persistent.encoded !== effectiveEncoded || persistent.widthPx !== widthPx || persistent.heightPx !== heightPx || persistent.ratioKey !== ratioKey || persistent.srcdoc !== srcdoc; if (needsUpdate) { persistent.iframe.srcdoc = srcdoc; persistent.encoded = effectiveEncoded; persistent.widthPx = widthPx; persistent.heightPx = heightPx; persistent.ratioKey = ratioKey; persistent.srcdoc = srcdoc; } if (!inStreaming || isPartial || needsUpdate || !node.hasAttribute('data-rendered')) { node.replaceChildren(persistent.wrapper); } node.setAttribute('data-rendered', '1'); showHtmlJsPendingRenderByPath.delete(pathKey); debugShowHtmlLog('render:node-js-iframe', { renderId, pathKey, ratioKey, widthPx, heightPx, encodedLength: effectiveEncoded.length, srcdocLength: srcdoc.length, needsUpdate }); return; } if (inStreaming) { const now = Date.now(); const lastTs = showHtmlStreamingRenderTsByPath.get(pathKey) || 0; if (now - lastTs < SHOW_HTML_STREAMING_RENDER_INTERVAL_MS) { const persistent = showHtmlPersistentRenderByPath.get(pathKey); if (persistent) { persistent.wrapper.style.width = `${widthPx}px`; persistent.host.style.height = `${heightPx}px`; node.replaceChildren(persistent.wrapper); node.setAttribute('data-rendered', '1'); debugShowHtmlLog('render:node-throttled-rehydrate', { renderId, pathKey, ratioKey, widthPx, heightPx }); } debugShowHtmlLog('render:node-throttled', { renderId, pathKey, now, lastTs, interval: SHOW_HTML_STREAMING_RENDER_INTERVAL_MS }); return; } showHtmlStreamingRenderTsByPath.set(pathKey, now); } else { showHtmlStreamingRenderTsByPath.delete(pathKey); } const rawHtml = effectiveEncoded ? decodeBase64Utf8(effectiveEncoded) : node.innerHTML || ''; const safeHtml = sanitizeShowHtmlContent(rawHtml); debugShowHtmlLog('render:node-content', { renderId, pathKey, jsMode, encodedLength: encoded.length, effectiveEncodedLength: effectiveEncoded.length, rawLength: rawHtml.length, safeLength: safeHtml.length, ratioKey, widthPx, heightPx }); let persistent = showHtmlPersistentRenderByPath.get(pathKey); if (!persistent && effectiveEncoded && inStreaming) { // 精确匹配 let ep = showHtmlPersistentByEncoded.get(effectiveEncoded); // 前缀匹配:多条渲染源可能输出不同阶段的 encoded,用最长前缀匹配找到 COMPLETE 的 wrapper if (!ep) { let bestLen = 0; for (const [key, val] of showHtmlPersistentByEncoded) { if (key.startsWith(effectiveEncoded) && key.length > bestLen) { ep = val; bestLen = key.length; } } } if (ep) { persistent = { encoded: ep.encoded, ratioKey: ep.ratioKey, widthPx: ep.widthPx, heightPx: ep.heightPx, wrapper: ep.wrapper, host: ep.host, root: ep.root }; showHtmlPersistentRenderByPath.set(pathKey, persistent); } } if (!persistent) { const wrapper = document.createElement('div'); wrapper.className = 'chat-inline-card chat-inline-card--html chat-inline-html'; const host = document.createElement('div'); host.className = 'chat-inline-card__body chat-inline-html__host'; const shadow = host.attachShadow({ mode: 'open' }); const style = document.createElement('style'); style.textContent = ` :host { display: block; box-sizing: border-box; width: 100%; height: 100%; overflow: auto; background: transparent; color: inherit; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; line-height: 1.5; } *, *::before, *::after { box-sizing: border-box; } .show-html-root { width: 100%; height: 100%; min-height: 100%; overflow: auto; background: transparent; scrollbar-width: thin; scrollbar-color: rgba(121, 109, 94, 0.48) transparent; } .show-html-root::-webkit-scrollbar { width: 8px; height: 8px; } .show-html-root::-webkit-scrollbar-track { background: transparent; } .show-html-root::-webkit-scrollbar-thumb { background: rgba(121, 109, 94, 0.48); border-radius: 8px; } .show-html-root::-webkit-scrollbar-thumb:hover { background: rgba(121, 109, 94, 0.62); } `; const root = document.createElement('div'); root.className = 'show-html-root'; shadow.appendChild(style); shadow.appendChild(root); wrapper.appendChild(host); persistent = { encoded: '', ratioKey: '1:1', widthPx: 0, heightPx: 0, wrapper, host, root }; showHtmlPersistentRenderByPath.set(pathKey, persistent); } persistent.wrapper.style.width = `${widthPx}px`; persistent.host.style.height = `${heightPx}px`; const needsUpdate = persistent.encoded !== effectiveEncoded || persistent.widthPx !== widthPx || persistent.heightPx !== heightPx || persistent.ratioKey !== ratioKey; if (needsUpdate) { persistent.root.innerHTML = safeHtml; persistent.encoded = effectiveEncoded; persistent.widthPx = widthPx; persistent.heightPx = heightPx; persistent.ratioKey = ratioKey; if (effectiveEncoded && inStreaming) { showHtmlPersistentByEncoded.set(effectiveEncoded, { wrapper: persistent.wrapper, host: persistent.host, root: persistent.root, encoded: effectiveEncoded, ratioKey, widthPx, heightPx }); } debugShowHtmlLog('render:node-persistent-update', { renderId, pathKey, encodedLength: effectiveEncoded.length, ratioKey, widthPx, heightPx }); } else { debugShowHtmlLog('render:node-persistent-reuse', { renderId, pathKey, encodedLength: effectiveEncoded.length }); } if (!inStreaming || isPartial || needsUpdate || !node.hasAttribute('data-rendered')) { node.replaceChildren(persistent.wrapper); } node.setAttribute('data-rendered', '1'); const rect = persistent.wrapper.getBoundingClientRect(); debugShowHtmlLog('render:node-done', { renderId, pathKey, jsMode, ratioKey, widthPx, heightPx, rect: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) }, partial: node.getAttribute('data-partial') || '', inStreaming }); if (verbose) { debugShowImageLog('render:show-html-done', { renderId, ratioKey, widthPx, heightPx, contentLength: rawHtml.length }); } }); debugShowImageLog('render:end', { renderId, orderAfter: collectShowImageOrder(root) }); } let showImageObserver: MutationObserver | null = null; let showContainerObserver: MutationObserver | null = null; let showTagRenderScheduled = false; let showTagBindRetryTimer: number | null = null; let showTagObservedContainer: Element | null = null; const showHtmlStreamingRenderTsByPath = new Map(); const SHOW_HTML_STREAMING_RENDER_INTERVAL_MS = 180; const showHtmlCompletedSnapshotByPath = new Map< string, { encoded: string, ratioKey: string } >(); const showHtmlStreamingSizeLockByPath = new Map< string, { ratioKey: string, widthPx: number, heightPx: number } >(); const showHtmlPersistentRenderByPath = new Map< string, { encoded: string, ratioKey: string, widthPx: number, heightPx: number, wrapper: HTMLElement, host: HTMLElement, root: HTMLElement } >(); const showHtmlPersistentByEncoded = new Map(); const showHtmlJsPendingRenderByPath = new Map< string, { wrapper: HTMLElement, host: HTMLElement, tip: HTMLElement, widthPx: number, heightPx: number, ratioKey: string } >(); const showHtmlJsIframeRenderByPath = new Map< string, { encoded: string, ratioKey: string, widthPx: number, heightPx: number, srcdoc: string, wrapper: HTMLElement, iframe: HTMLIFrameElement } >(); let layoutDebugObserver: MutationObserver | null = null; let layoutDebugResizeObserver: ResizeObserver | null = null; let layoutDebugStarted = false; let layoutDebugBindRetryTimer: number | null = null; let layoutDebugCount = 0; const LAYOUT_DEBUG_MAX = 300; const layoutDebugLastTsByKey = new Map(); function scheduleShowTagRender(container: Element) { if (showTagRenderScheduled) return; showTagRenderScheduled = true; const run = () => { showTagRenderScheduled = false; renderShowImages(container); }; if (typeof queueMicrotask === 'function') { queueMicrotask(run); return; } Promise.resolve().then(run); } function getShowTagContainer() { return document.querySelector('.messages-area'); } function isLayoutDebugEnabled() { if (typeof window === 'undefined') return true; try { const explicitFlag = (window as any).__LAYOUT_DEBUG__; if (explicitFlag === false || explicitFlag === '0') return false; if (explicitFlag === true || explicitFlag === '1') return true; const localFlag = window.localStorage?.getItem('layoutDebug'); if (localFlag === '0' || localFlag === 'false') return false; if (localFlag === '1' || localFlag === 'true') return true; return true; } catch { return true; } } function layoutDebugLog(event: string, payload: Record = {}, key = event) { if (!isLayoutDebugEnabled()) return; if (layoutDebugCount >= LAYOUT_DEBUG_MAX) return; const now = Date.now(); const lastTs = layoutDebugLastTsByKey.get(key) || 0; if (now - lastTs < 120) return; layoutDebugLastTsByKey.set(key, now); layoutDebugCount += 1; if (layoutDebugCount === LAYOUT_DEBUG_MAX) { console.warn('[LAYOUT_DEBUG]', 'log-limit-reached', { max: LAYOUT_DEBUG_MAX }); return; } console.log('[LAYOUT_DEBUG]', event, payload); } function elementRectSummary(el: Element | null) { if (!el) return null; const rect = el.getBoundingClientRect(); return { className: (el as HTMLElement).className || '', x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) }; } function setupLayoutDebugObservers() { if (layoutDebugStarted || !isLayoutDebugEnabled()) return; const trackedSelectors = [ '.main-container', '.workspace-region', '.workspace-panel', '.chat-container', '.messages-area' ]; const tracked = trackedSelectors .map((selector) => document.querySelector(selector)) .filter((el): el is Element => !!el); if (tracked.length === 0) { layoutDebugLog('layout:setup-wait', { reason: 'targets-not-mounted' }, 'layout:setup-wait'); if (layoutDebugBindRetryTimer) { window.clearTimeout(layoutDebugBindRetryTimer); } layoutDebugBindRetryTimer = window.setTimeout(() => { layoutDebugBindRetryTimer = null; setupLayoutDebugObservers(); }, 250); return; } layoutDebugStarted = true; layoutDebugLog('layout:setup', { trackedCount: tracked.length, tracked: tracked.map((el) => (el as HTMLElement).className || el.tagName.toLowerCase()) }); const snapshot = () => { layoutDebugLog( 'layout:snapshot', { window: { innerWidth: window.innerWidth, innerHeight: window.innerHeight }, main: elementRectSummary(document.querySelector('.main-container')), workspace: elementRectSummary(document.querySelector('.workspace-region')), panel: elementRectSummary(document.querySelector('.workspace-panel')), chat: elementRectSummary(document.querySelector('.chat-container')), messages: elementRectSummary(document.querySelector('.messages-area')) }, 'layout:snapshot' ); }; snapshot(); layoutDebugObserver = new MutationObserver((mutations) => { const interesting = mutations.filter((m) => { if (!(m.target instanceof Element)) return false; const className = (m.target as HTMLElement).className || ''; return ( m.type === 'attributes' && (tracked.some((el) => el === m.target) || className.includes('chat-container') || className.includes('workspace-panel') || className.includes('main-container')) ); }); if (!interesting.length) return; layoutDebugLog('layout:mutation', { count: interesting.length, items: interesting.slice(0, 8).map((m) => ({ attr: m.attributeName, target: summarizeNodeForDebug(m.target) })) }); snapshot(); }); layoutDebugObserver.observe(document.body, { subtree: true, attributes: true, attributeFilter: ['class', 'style'] }); layoutDebugResizeObserver = new ResizeObserver((entries) => { if (!entries.length) return; layoutDebugLog('layout:resize', { items: entries.slice(0, 8).map((entry) => ({ target: summarizeNodeForDebug(entry.target), width: Math.round(entry.contentRect.width), height: Math.round(entry.contentRect.height) })) }); snapshot(); }); tracked.forEach((el) => layoutDebugResizeObserver?.observe(el)); window.addEventListener('resize', snapshot); } function buildNodePathKey(node: Element) { const getIndexWithin = (el: Element, selector: string) => { const list = Array.from(document.querySelectorAll(selector)); const idx = list.indexOf(el); return idx >= 0 ? idx : -1; }; const parts: string[] = []; const messageBlock = node.closest('.message-block'); if (messageBlock) { parts.push(`msg:${getIndexWithin(messageBlock, '.message-block')}`); } const actionItem = node.closest('.action-item'); if (actionItem) { parts.push(`action:${getIndexWithin(actionItem, '.action-item')}`); } const textContent = node.closest('.text-content'); if (textContent) { parts.push(`text:${getIndexWithin(textContent, '.text-content')}`); } let cur: Element | null = node; while (cur && cur !== document.body && parts.length < 12) { const parent = cur.parentElement; if (!parent) break; const index = Array.prototype.indexOf.call(parent.children, cur); parts.push(`${cur.tagName.toLowerCase()}:${index}`); if (cur.classList.contains('text-content')) break; cur = parent; } return parts.reverse().join('/'); } export function setupShowImageObserver() { if (showImageObserver || showContainerObserver) return; setupLayoutDebugObservers(); const bind = () => { const container = getShowTagContainer(); if (!container) { if (showTagBindRetryTimer) { window.clearTimeout(showTagBindRetryTimer); } showTagBindRetryTimer = window.setTimeout(bind, 250); return; } if (showTagBindRetryTimer) { window.clearTimeout(showTagBindRetryTimer); showTagBindRetryTimer = null; } if (showTagObservedContainer === container && showImageObserver) return; if (showImageObserver) { showImageObserver.disconnect(); showImageObserver = null; } showTagObservedContainer = container; debugShowImageLog('observer:setup', { container: summarizeNodeForDebug(container), debugMode: getShowImageDebugMode(), version: SHOW_IMAGE_DEBUG_VERSION }); renderShowImages(container); showImageObserver = new MutationObserver((mutations) => { const debugMode = getShowImageDebugMode(); const verbose = debugMode === 'verbose'; const hasShowImageRelatedMutation = mutations.some((mutation) => { const targetIsShowImage = mutation.target instanceof Element && ['show_image', 'show_html', 'show-image', 'show-html'].includes( mutation.target.tagName.toLowerCase() ); const addedHasShowTag = Array.from(mutation.addedNodes).some( (n) => n instanceof Element && (['show_image', 'show_html', 'show-image', 'show-html'].includes( n.tagName.toLowerCase() ) || !!n.querySelector?.('show_image,show_html,show-image,show-html')) ); return targetIsShowImage || addedHasShowTag; }); if (!hasShowImageRelatedMutation) return; debugShowImageLog('observer:show-image-mutation', { mutationCount: mutations.length, mutations: verbose ? mutations.slice(0, 20).map((m) => ({ type: m.type, target: summarizeNodeForDebug(m.target), added: Array.from(m.addedNodes) .slice(0, 6) .map((n) => summarizeNodeForDebug(n)), removed: Array.from(m.removedNodes) .slice(0, 6) .map((n) => summarizeNodeForDebug(n)) })) : undefined }); scheduleShowTagRender(container); }); showImageObserver.observe(container, { childList: true, subtree: true }); }; bind(); showContainerObserver = new MutationObserver(() => { const latest = getShowTagContainer(); if (!latest) return; if (latest !== showTagObservedContainer) { bind(); } }); showContainerObserver.observe(document.body, { childList: true, subtree: true }); } export function teardownShowImageObserver() { if (showTagBindRetryTimer) { window.clearTimeout(showTagBindRetryTimer); showTagBindRetryTimer = null; } if (showImageObserver) { showImageObserver.disconnect(); showImageObserver = null; } if (showContainerObserver) { showContainerObserver.disconnect(); showContainerObserver = null; } if (layoutDebugObserver) { layoutDebugObserver.disconnect(); layoutDebugObserver = null; } if (layoutDebugResizeObserver) { layoutDebugResizeObserver.disconnect(); layoutDebugResizeObserver = null; } if (layoutDebugBindRetryTimer) { window.clearTimeout(layoutDebugBindRetryTimer); layoutDebugBindRetryTimer = null; } layoutDebugStarted = false; layoutDebugCount = 0; layoutDebugLastTsByKey.clear(); showHtmlStreamingRenderTsByPath.clear(); showHtmlCompletedSnapshotByPath.clear(); showHtmlStreamingSizeLockByPath.clear(); showHtmlPersistentRenderByPath.clear(); showHtmlPersistentByEncoded.clear(); showHtmlJsPendingRenderByPath.clear(); showHtmlJsIframeRenderByPath.clear(); showTagObservedContainer = null; } function updateViewportHeightVar() { const docEl = document.documentElement; const visualViewport = window.visualViewport; if (visualViewport) { const vh = visualViewport.height; const bottomInset = Math.max( 0, (window.innerHeight || docEl.clientHeight || vh) - visualViewport.height - visualViewport.offsetTop ); docEl.style.setProperty('--app-viewport', `${vh}px`); docEl.style.setProperty('--app-bottom-inset', `${bottomInset}px`); } else { const height = window.innerHeight || docEl.clientHeight; if (height) { docEl.style.setProperty('--app-viewport', `${height}px`); } docEl.style.setProperty('--app-bottom-inset', 'env(safe-area-inset-bottom, 0px)'); } } if (typeof window !== 'undefined') { window.katex = katex; updateViewportHeightVar(); window.addEventListener('resize', updateViewportHeightVar); window.addEventListener('orientationchange', updateViewportHeightVar); window.addEventListener('pageshow', updateViewportHeightVar); if (window.visualViewport) { window.visualViewport.addEventListener('resize', updateViewportHeightVar); window.visualViewport.addEventListener('scroll', updateViewportHeightVar); } }