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\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\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;