diff --git a/app/static/public/js/chat.js b/app/static/public/js/chat.js index b83133c..ecf159a 100644 --- a/app/static/public/js/chat.js +++ b/app/static/public/js/chat.js @@ -138,7 +138,10 @@ const entry = createMessage(msg.role, text); if (entry && msg.role === 'assistant') { entry.sources = msg.sources || null; + entry.rendering = msg.rendering || null; updateMessage(entry, text, true); + } else if (entry && msg.role === 'user') { + renderUserMessage(entry, text, []); } } if (activeStreamInfo && activeStreamInfo.sessionId === session.id && activeStreamInfo.entry.row) { @@ -594,7 +597,175 @@ .map((item) => normalizeSourceText(item.label || item.hostname || item.href || '')) .filter(Boolean); const titleAttr = titles.length ? ` title="${escapeHtml(titles.join('\n'))}"` : ''; - return `${label}${extraCount > 0 ? `+${extraCount}` : ''}`; + if (extraCount <= 0) { + return `${label}`; + } + const payload = escapeHtml(encodeURIComponent(JSON.stringify(items.map((item) => ({ + href: String(item && item.href || '').trim(), + hostname: normalizeSourceText(item && item.hostname || ''), + label: normalizeSourceText(item && item.label || '') + }))))); + return `${label}+${extraCount}`; + } + + function expandInlineCitationCluster(cluster) { + if (!(cluster instanceof HTMLElement)) return; + if (cluster.dataset.expanded === '1') return; + const raw = cluster.dataset.citationLinks || ''; + if (!raw) return; + try { + const links = JSON.parse(decodeURIComponent(raw)); + if (!Array.isArray(links) || !links.length) return; + const expanded = links + .map((item) => buildInlineCitationChip([item])) + .filter(Boolean) + .join(''); + if (!expanded) return; + const wrapper = document.createElement('span'); + wrapper.className = 'inline print-hidden inline-citation-cluster-expanded'; + wrapper.innerHTML = expanded; + cluster.replaceWith(wrapper); + } catch (e) { + // ignore malformed payload + } + } + + function parseRenderingCards(rendering) { + const rawModelResponse = rendering && rendering.rawModelResponse && typeof rendering.rawModelResponse === 'object' + ? rendering.rawModelResponse + : null; + const rawCards = Array.isArray(rawModelResponse && rawModelResponse.cardAttachmentsJson) + ? rawModelResponse.cardAttachmentsJson + : []; + const cardMap = new Map(); + rawCards.forEach((raw) => { + if (typeof raw !== 'string' || !raw.trim()) return; + try { + const card = JSON.parse(raw); + if (!card || typeof card !== 'object' || !card.id) return; + cardMap.set(String(card.id), card); + } catch (e) { + // ignore malformed cards + } + }); + return cardMap; + } + + function buildRenderedImageMarkdown(card) { + const image = card && card.image && typeof card.image === 'object' ? card.image : {}; + const original = String(image.original || image.link || '').trim(); + if (!original) return ''; + const title = normalizeSourceText(image.title || '') || 'image'; + return `\n![${title}](${original})\n`; + } + + function normalizeRenderedMarkdownLayout(text) { + let output = String(text || ''); + output = output.replace(/([^\n])\s*(#{2,6}\s+)/g, '$1\n\n$2'); + output = output.replace(/(<\/span><\/span>)\s*(#{2,6}\s+)/g, '$1\n\n$2'); + output = output.replace(/(<\/span><\/span>)\s*(\d+\.\s+)/g, '$1\n\n$2'); + output = output.replace(/(<\/span><\/span>)\s*([*-]\s+)/g, '$1\n\n$2'); + output = output.replace(/(<\/span><\/span>)\s*(\*\*[^*]+\*\*:)/g, '$1\n\n$2'); + output = output.replace(/([。!?;])\s*(\d+\.\s+)/g, '$1\n\n$2'); + output = output.replace(/([。!?;])\s*([*-]\s+)/g, '$1\n\n$2'); + output = output.replace(/([^\n])\s*(?:[-*•]\s+\*\*[^*]+\*\*:)/g, (match, prefix) => `${prefix}\n\n${match.slice(prefix.length).trimStart()}`); + output = output.replace(/\n{3,}/g, '\n\n'); + return output; + } + + function preserveRenderBoundary(match, replacement) { + const trailingWhitespaceMatch = String(match || '').match(/((?:\s| |\u00a0|\u2060)*)$/); + const trailingWhitespace = trailingWhitespaceMatch ? trailingWhitespaceMatch[1] : ''; + const normalizedTrailing = trailingWhitespace + .replace(/ |\u00a0|\u2060/g, ' ') + .replace(/[ \t]+\n/g, '\n'); + + if (!normalizedTrailing) return replacement; + if (normalizedTrailing.includes('\n\n')) return `${replacement}\n\n`; + if (normalizedTrailing.includes('\n')) return `${replacement}\n`; + return `${replacement} `; + } + + function renderExactGrokCards(rawMessage, rendering) { + const message = String(rawMessage || ''); + if (!message || !rendering || typeof rendering !== 'object') return message; + const cardMap = parseRenderingCards(rendering); + if (!cardMap.size) return message; + + let rendered = message.replace( + /(?:]*card_id="[^"]+"[^>]*>[\s\S]*?<\/grok:render>(?:\s| |\u00a0|\u2060)*)+/g, + (match) => { + const ids = Array.from(match.matchAll(/card_id="([^"]+)"/g)) + .map((part) => String(part[1] || '').trim()) + .filter(Boolean); + if (!ids.length) return ''; + + const output = []; + let pendingCitations = []; + const flushCitations = () => { + if (!pendingCitations.length) return; + output.push(buildInlineCitationChip(pendingCitations)); + pendingCitations = []; + }; + + ids.forEach((id) => { + const card = cardMap.get(id); + if (!card) return; + if (String(card.type || '') === 'render_inline_citation' && card.url) { + pendingCitations.push({ + href: String(card.url).trim(), + hostname: getSourceHostname(card.url), + label: normalizeSourceText(card.title || '') || getSourceHostname(card.url) || String(card.url).trim() + }); + return; + } + flushCitations(); + if (String(card.type || '') === 'render_searched_image') { + output.push(buildRenderedImageMarkdown(card)); + } + }); + + flushCitations(); + return preserveRenderBoundary(match, output.join('')); + } + ); + + const extraImages = Array.isArray(rendering.extraImages) ? rendering.extraImages : []; + if (extraImages.length) { + const appended = extraImages + .map((url) => String(url || '').trim()) + .filter(Boolean) + .map((url) => `\n![image](${url})\n`) + .join(''); + if (appended) rendered += appended; + } + + return normalizeRenderedMarkdownLayout(rendered); + } + + function extractThinkMarkup(raw) { + const source = String(raw || ''); + if (!source.includes('')) return ''; + const matches = source.match(/[\s\S]*?<\/think>|[\s\S]*$/g) || []; + return matches.join('\n'); + } + + function getRenderableAssistantText(entry) { + if (!entry || entry.role !== 'assistant') { + return entry && entry.raw ? entry.raw : ''; + } + const rendering = entry.rendering && typeof entry.rendering === 'object' ? entry.rendering : null; + const rawModelResponse = rendering && rendering.rawModelResponse && typeof rendering.rawModelResponse === 'object' + ? rendering.rawModelResponse + : null; + const rawMessage = rawModelResponse && typeof rawModelResponse.message === 'string' + ? rawModelResponse.message + : ''; + if (!rawMessage) return entry.raw || ''; + const renderedAnswer = renderExactGrokCards(rawMessage, rendering); + const thinkMarkup = extractThinkMarkup(entry.raw || ''); + if (!thinkMarkup) return renderedAnswer; + return `${thinkMarkup}\n\n${renderedAnswer}`.trim(); } function renderInlineCitationTokens(value, htmlLinks) { @@ -612,134 +783,17 @@ links.push(item); }); if (!links.length) return ''; - const grouped = []; - let currentGroup = []; - let currentKey = ''; - links.forEach((item) => { - const key = normalizeSourceText(item.hostname || item.href || ''); - if (!currentGroup.length || key === currentKey) { - currentGroup.push(item); - currentKey = key; - return; - } - grouped.push(currentGroup); - currentGroup = [item]; - currentKey = key; - }); - if (currentGroup.length) grouped.push(currentGroup); - return grouped.map((group) => buildInlineCitationChip(group)).join(''); - }); - } - - function buildStructuredSourceCandidates(sources) { - const candidates = []; - const seen = new Set(); - const citations = Array.isArray(sources && sources.citations) ? sources.citations : []; - const groups = Array.isArray(sources && sources.groups) ? sources.groups : []; - - const pushCandidate = (item) => { - const href = String(item && item.url || '').trim(); - if (!href || seen.has(href)) return; - seen.add(href); - const hostname = getSourceHostname(href); - const label = normalizeSourceText(item && item.title || '') || hostname || href; - const preview = normalizeSourceText(item && item.preview || ''); - candidates.push({ href, hostname, label, preview }); - }; - - citations.forEach(pushCandidate); - groups.forEach((group) => { - const results = Array.isArray(group && group.results) ? group.results : []; - results.forEach(pushCandidate); - }); - - return candidates; - } - - function extractCitationTerms(value) { - return (String(value || '').toLowerCase().match(/[a-z0-9][a-z0-9.-]{2,}|[\u4e00-\u9fff]{2,}/g) || []) - .map((term) => term.trim()) - .filter((term) => term && !/^(模型|图片|图像|生成|支持|能力|介绍|版本|官方|页面|blog)$/.test(term)); - } - - function scoreSourceForText(text, candidate) { - const haystack = normalizeSourceText(text || '').toLowerCase(); - if (!haystack) return 0; - const terms = [ - ...extractCitationTerms(candidate && candidate.label || ''), - ...extractCitationTerms(candidate && candidate.preview || '') - ]; - let score = 0; - const seen = new Set(); - terms.forEach((term) => { - if (!term || seen.has(term)) return; - seen.add(term); - if (haystack.includes(term)) { - score += term.length >= 6 ? 3 : 2; + if (links.every((item) => typeof item.html === 'string' && item.html.trim())) { + return links.map((item) => item.html).join(''); } + return buildInlineCitationChip(links); }); - const hostname = String(candidate && candidate.hostname || '').toLowerCase(); - if (hostname && haystack.includes(hostname.replace(/\.[a-z]+$/, ''))) { - score += 2; - } - return score; - } - - function buildStructuredInlineCitationHtml(items) { - const group = Array.isArray(items) ? items.filter(Boolean) : []; - if (!group.length) return ''; - return buildInlineCitationChip(group); - } - - function applyStructuredInlineCitations(root, sources) { - if (!root || !sources || typeof sources !== 'object') return; - if (root.querySelector('.inline-citation-chip')) return; - - const candidates = buildStructuredSourceCandidates(sources); - if (!candidates.length) return; - - const blocks = Array.from(root.querySelectorAll('p, li')) - .filter((node) => node instanceof HTMLElement) - .filter((node) => !node.querySelector('.inline-citation-chip')) - .map((node) => ({ node, text: normalizeSourceText(node.textContent || '') })) - .filter((item) => item.text.length >= 24); - - if (!blocks.length) return; - - const used = new Set(); - blocks.forEach(({ node, text }) => { - let best = null; - let bestScore = 0; - candidates.forEach((candidate) => { - if (used.has(candidate.href)) return; - const score = scoreSourceForText(text, candidate); - if (score > bestScore) { - bestScore = score; - best = candidate; - } - }); - if (!best || bestScore < 2) return; - used.add(best.href); - const html = buildStructuredInlineCitationHtml([best]); - if (html) { - node.insertAdjacentHTML('beforeend', html); - } - }); - - if (!used.size) { - const fallback = blocks[0] && blocks[0].node; - const html = buildStructuredInlineCitationHtml([candidates[0]]); - if (fallback && html) { - fallback.insertAdjacentHTML('beforeend', html); - } - } } function renderBasicMarkdown(rawText) { const text = (rawText || '').replace(/\\n/g, '\n'); const htmlLinks = []; - const normalizedText = text - .replace(/<\/?span\b[^>]*>/gi, '') + const linkExtractedText = text .replace(/]*href=(["'])(.*?)\1[^>]*>([\s\S]*?)<\/a>/gi, (match, quote, href, inner) => { const label = String(inner || '') .replace(/<[^>]+>/g, ' ') @@ -751,10 +805,12 @@ htmlLinks.push({ href: String(href || '').trim(), label, - hostname: getSourceHostname(href) + hostname: getSourceHostname(href), + html: match }); return token; }); + const normalizedText = linkExtractedText.replace(/<\/?span\b[^>]*>/gi, ''); const escaped = escapeHtml(normalizedText); const codeBlocks = []; const fenced = escaped.replace(/```([a-zA-Z0-9_-]+)?\n([\s\S]*?)```/g, (match, lang, code) => { @@ -788,10 +844,29 @@ }); output = renderInlineCitationTokens(output, htmlLinks); + output = linkifyPlainTextSegments(output); return output; }; + const linkifyPlainTextSegments = (html) => { + const segments = String(html || '').split(/(<[^>]+>)/g); + return segments.map((segment) => { + if (!segment || segment.startsWith('<')) return segment; + return segment.replace(/https?:\/\/[A-Za-z0-9\-._~:/?#\[\]@!$&'*+,;=%]+/gi, (rawUrl) => { + let url = rawUrl; + let trailing = ''; + while (/[),.;!?,。;:!?)\]]$/.test(url)) { + trailing = url.slice(-1) + trailing; + url = url.slice(0, -1); + } + if (!url) return rawUrl; + const safeUrl = escapeHtml(url); + return `${safeUrl}${escapeHtml(trailing)}`; + }); + }).join(''); + }; + const lines = fenced.split(/\r?\n/); const htmlParts = []; let inUl = false; @@ -820,7 +895,19 @@ const flushParagraph = () => { if (!paragraphLines.length) return; const joined = paragraphLines.join('
'); - htmlParts.push(`

${renderInline(joined)}

`); + const standaloneMediaLines = paragraphLines.every((line) => { + const trimmed = String(line || '').trim(); + if (!trimmed) return false; + return ( + /^!\[[^\]]*\]\([^)]+\)$/.test(trimmed) || + /^\[[^\]]+\]\((https?:\/\/[^)]+)\)$/.test(trimmed) + ); + }); + if (standaloneMediaLines) { + htmlParts.push(paragraphLines.map((line) => renderInline(line.trim())).join('')); + } else { + htmlParts.push(`

${renderInline(joined)}

`); + } paragraphLines = []; }; @@ -1142,6 +1229,7 @@ role, raw: content || '', sources: null, + rendering: null, committed: false, startedAt: Date.now(), firstTokenAt: null, @@ -1379,6 +1467,10 @@ if (!entry) return; entry.raw = content || ''; if (!entry.contentNode) return; + if (entry.role === 'user') { + renderUserMessage(entry, entry.raw, []); + return; + } const shouldPreserveScroll = isSending && !followStreamScroll; const scrollContainer = shouldPreserveScroll ? getScrollContainer() : null; const preservedScrollTop = scrollContainer ? scrollContainer.scrollTop : 0; @@ -1423,12 +1515,13 @@ if (!entry.hasThink && entry.raw.includes('')) { entry.hasThink = true; } + const renderText = entry.role === 'assistant' ? getRenderableAssistantText(entry) : entry.raw; if (finalize) { entry.contentNode.classList.add('rendered'); - entry.contentNode.innerHTML = renderMarkdown(entry.raw); + entry.contentNode.innerHTML = renderMarkdown(renderText); } else { if (entry.role === 'assistant') { - entry.contentNode.innerHTML = renderMarkdown(entry.raw); + entry.contentNode.innerHTML = renderMarkdown(renderText); } else { entry.contentNode.textContent = entry.raw; } @@ -1448,13 +1541,11 @@ updateThinkSummary(entry, entry.thinkElapsed); } if (entry.role === 'assistant' || entry.role === 'user') { - if (entry.role === 'assistant') { - applyStructuredInlineCitations(entry.contentNode, entry.sources); - } liftThinkImages(entry.contentNode); applyImageGrid(entry.contentNode); enhanceBrokenImages(entry.contentNode); bindMessageImagePreview(entry.contentNode); + bindInlineCitationExpand(entry.contentNode); } if (entry.role === 'assistant') { bindCodeCopyButtons(entry.contentNode); @@ -1514,14 +1605,25 @@ const summaries = entry.contentNode.querySelectorAll('.think-summary'); if (!summaries.length) return; const text = typeof elapsedSec === 'number' ? `思考 ${elapsedSec} 秒` : '思考中'; + const spinDurationMs = 5500; + const elapsedMs = Math.max(0, Date.now() - (entry.startedAt || Date.now())); + const spinOffset = `-${(elapsedMs % spinDurationMs)}ms`; summaries.forEach((node) => { node.textContent = text; const block = node.closest('.think-block'); if (!block) return; if (typeof elapsedSec === 'number') { block.removeAttribute('data-thinking'); + node.style.removeProperty('--think-spin-delay'); + block.querySelectorAll('.think-agent summary').forEach((summary) => { + summary.style.removeProperty('--think-spin-delay'); + }); } else { block.setAttribute('data-thinking', 'true'); + node.style.setProperty('--think-spin-delay', spinOffset); + block.querySelectorAll('.think-agent summary').forEach((summary) => { + summary.style.setProperty('--think-spin-delay', spinOffset); + }); } }); } @@ -1543,6 +1645,19 @@ return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(hostname)}&sz=256`; } + function bindInlineCitationExpand(root) { + if (!root || root.dataset.citationExpandBound === '1') return; + root.dataset.citationExpandBound = '1'; + root.addEventListener('click', (event) => { + const target = event.target instanceof Element ? event.target.closest('.inline-citation-cluster.inline-citation-chip') : null; + if (!(target instanceof HTMLElement)) return; + if (target.dataset.expanded === '1') return; + event.preventDefault(); + event.stopPropagation(); + expandInlineCitationCluster(target); + }); + } + function cleanExtractedUrl(url) { return String(url || '').trim().replace(/[),.;]+$/g, ''); } @@ -1629,6 +1744,10 @@ const structured = entry && entry.sources && typeof entry.sources === 'object' ? entry.sources : null; const groups = Array.isArray(structured && structured.groups) ? structured.groups : []; const citations = Array.isArray(structured && structured.citations) ? structured.citations : []; + const rawSourceCount = citations.length + groups.reduce((sum, group) => { + const results = Array.isArray(group && group.results) ? group.results : []; + return sum + results.length; + }, 0); const data = (!groups.length && !citations.length) ? extractAssistantSources(entry && entry.contentNode) : { links: [], searches: [] }; @@ -1645,6 +1764,7 @@ type: 'citation', label: hostname || url, meta: '引用来源', + preview: normalizeSourceText(item && item.preview || ''), url, hostname }); @@ -1669,6 +1789,7 @@ type: 'visit', label: normalizeSourceText(item && item.title || '') || hostname || url, meta: hostname || '搜索结果', + preview: normalizeSourceText(item && item.preview || ''), url, hostname }); @@ -1682,7 +1803,7 @@ const summary = document.createElement('summary'); summary.className = 'sources-chip'; - const summaryCount = sources.length || searches.length; + const summaryCount = rawSourceCount || sources.length || searches.length; const summaryLabel = sources.length ? `${summaryCount} sources` : `${summaryCount} searches`; summary.setAttribute('aria-label', summaryLabel); @@ -1721,12 +1842,54 @@ const panel = document.createElement('div'); panel.className = 'sources-panel'; - if (sources.length) { - const title = document.createElement('div'); - title.className = 'sources-section-title'; - title.textContent = '可验证来源'; - panel.appendChild(title); - } + const panelHeader = document.createElement('div'); + panelHeader.className = 'sources-panel-header'; + + const panelHeading = document.createElement('div'); + panelHeading.className = 'sources-section-title sources-panel-heading'; + panelHeading.textContent = '可验证来源'; + + const closeButton = document.createElement('button'); + closeButton.type = 'button'; + closeButton.className = 'sources-panel-close'; + closeButton.setAttribute('aria-label', '关闭来源面板'); + closeButton.textContent = '×'; + closeButton.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + wrapper.open = false; + }); + + panelHeader.appendChild(panelHeading); + panelHeader.appendChild(closeButton); + panel.appendChild(panelHeader); + + const sectionAnchors = []; + const registerSectionAnchor = (labelText) => { + const anchor = document.createElement('div'); + anchor.className = 'sources-section-anchor'; + anchor.dataset.sectionLabel = labelText; + panel.appendChild(anchor); + sectionAnchors.push(anchor); + return anchor; + }; + + const updatePanelHeading = () => { + if (!sectionAnchors.length) return; + const headerHeight = panelHeader.offsetHeight || 0; + const threshold = panel.scrollTop + headerHeight + 8; + let activeLabel = sectionAnchors[0].dataset.sectionLabel || ''; + sectionAnchors.forEach((anchor) => { + if (anchor.offsetTop <= threshold) { + activeLabel = anchor.dataset.sectionLabel || activeLabel; + } + }); + if (activeLabel) { + panelHeading.textContent = activeLabel; + } + }; + + registerSectionAnchor('可验证来源'); sources.forEach((item) => { const row = document.createElement(item.url ? 'a' : 'div'); @@ -1760,14 +1923,21 @@ title.className = 'source-row-title'; title.textContent = item.label || item.url || ''; - textWrap.appendChild(meta); textWrap.appendChild(title); + if (item.preview) { + const preview = document.createElement('span'); + preview.className = 'source-row-preview'; + preview.textContent = item.preview; + textWrap.appendChild(preview); + } + textWrap.appendChild(meta); row.appendChild(icon); row.appendChild(textWrap); panel.appendChild(row); }); if (searches.length) { + registerSectionAnchor('搜索轨迹'); const title = document.createElement('div'); title.className = 'sources-section-title'; title.textContent = '搜索轨迹'; @@ -1802,6 +1972,12 @@ wrapper.appendChild(summary); wrapper.appendChild(panel); + panel.addEventListener('scroll', updatePanelHeading, { passive: true }); + wrapper.addEventListener('toggle', () => { + if (wrapper.open) { + updatePanelHeading(); + } + }); return wrapper; } @@ -2268,9 +2444,9 @@ if (!assistantEntry.committed) { assistantEntry.committed = true; if (retrySessionId) { - commitToSession(retrySessionId, assistantEntry.raw || '', assistantEntry.sources); + commitToSession(retrySessionId, assistantEntry.raw || '', assistantEntry.sources, assistantEntry.rendering); } else { - messageHistory.push({ role: 'assistant', content: assistantEntry.raw || '', sources: assistantEntry.sources || null }); + messageHistory.push({ role: 'assistant', content: assistantEntry.raw || '', sources: assistantEntry.sources || null, rendering: assistantEntry.rendering || null }); } } setStatus('error', '已中止'); @@ -2363,9 +2539,9 @@ if (!assistantEntry.committed) { assistantEntry.committed = true; if (sendSessionId) { - commitToSession(sendSessionId, assistantEntry.raw || '', assistantEntry.sources); + commitToSession(sendSessionId, assistantEntry.raw || '', assistantEntry.sources, assistantEntry.rendering); } else { - messageHistory.push({ role: 'assistant', content: assistantEntry.raw || '', sources: assistantEntry.sources || null }); + messageHistory.push({ role: 'assistant', content: assistantEntry.raw || '', sources: assistantEntry.sources || null, rendering: assistantEntry.rendering || null }); } } } else { @@ -2380,14 +2556,15 @@ } } - function commitToSession(sessionId, assistantText, assistantSources = null) { + function commitToSession(sessionId, assistantText, assistantSources = null, assistantRendering = null) { if (!sessionId || !sessionsData) return; const session = sessionsData.sessions.find((s) => s.id === sessionId); if (!session) return; session.messages.push({ role: 'assistant', content: assistantText, - sources: assistantSources || null + sources: assistantSources || null, + rendering: assistantRendering || null }); if (session.messages.length > MAX_CONTEXT_MESSAGES) { session.messages = session.messages.slice(-MAX_CONTEXT_MESSAGES); @@ -2432,9 +2609,9 @@ } assistantEntry.committed = true; if (targetSessionId) { - commitToSession(targetSessionId, assistantText, assistantEntry.sources); + commitToSession(targetSessionId, assistantText, assistantEntry.sources, assistantEntry.rendering); } else { - messageHistory.push({ role: 'assistant', content: assistantText, sources: assistantEntry.sources || null }); + messageHistory.push({ role: 'assistant', content: assistantText, sources: assistantEntry.sources || null, rendering: assistantEntry.rendering || null }); } return; } @@ -2442,13 +2619,16 @@ const json = JSON.parse(payload); if (json && json.sources && typeof json.sources === 'object') { assistantEntry.sources = json.sources; - if (assistantEntry.contentNode) { - applyStructuredInlineCitations(assistantEntry.contentNode, assistantEntry.sources); - } if (assistantEntry.row && assistantEntry.row.querySelector('.message-actions')) { attachAssistantActions(assistantEntry); } } + if (json && json.rendering && typeof json.rendering === 'object') { + assistantEntry.rendering = json.rendering; + if (!targetSessionId || (sessionsData && sessionsData.activeId === targetSessionId)) { + updateMessage(assistantEntry, assistantText, false); + } + } const delta = json && json.choices && json.choices[0] && json.choices[0].delta ? json.choices[0].delta.content : ''; @@ -2479,9 +2659,9 @@ } assistantEntry.committed = true; if (targetSessionId) { - commitToSession(targetSessionId, assistantText, assistantEntry.sources); + commitToSession(targetSessionId, assistantText, assistantEntry.sources, assistantEntry.rendering); } else { - messageHistory.push({ role: 'assistant', content: assistantText, sources: assistantEntry.sources || null }); + messageHistory.push({ role: 'assistant', content: assistantText, sources: assistantEntry.sources || null, rendering: assistantEntry.rendering || null }); } } finally { activeStreamInfo = null;