Improve document agent streaming and chat metrics

This commit is contained in:
pewdiepie-archdaemon
2026-06-30 05:14:41 +00:00
parent 699dbc6ae3
commit b8338b2399
6 changed files with 317 additions and 35 deletions
+3 -3
View File
@@ -219,7 +219,7 @@
}, { once: true });
})();
</script>
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="/static/style.css?v=20260630toolmetrics">
<link rel="modulepreload" href="/static/app.js">
<link rel="modulepreload" href="/static/js/chat.js">
<link rel="modulepreload" href="/static/js/ui.js">
@@ -2499,10 +2499,10 @@
<script type="module" src="/static/js/tts-ai.js"></script>
<script type="module" src="/static/js/document.js"></script>
<script type="module" src="/static/js/gallery.js"></script>
<script type="module" src="/static/js/chatRenderer.js"></script>
<script type="module" src="/static/js/chatRenderer.js?v=20260630toolmetrics"></script>
<script type="module" src="/static/js/codeRunner.js"></script>
<script type="module" src="/static/js/chatStream.js"></script>
<script type="module" src="/static/js/chat.js?v=20260609ws"></script>
<script type="module" src="/static/js/chat.js?v=20260630toolmetrics"></script>
<script type="module" src="/static/js/cookbook.js"></script>
<script src="/static/js/cookbookSchedule.js"></script>
<script type="module" src="/static/js/search-chat.js"></script>
+27 -7
View File
@@ -1048,6 +1048,7 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
let roundHolder = holder; // Current AI text bubble (changes per round)
let roundText = ''; // Text accumulated for current round
let currentToolBubble = null; // Current tool execution bubble
let lastToolThread = null; // Visible tool timeline for tool-only turns
let roundFinalized = false; // Whether current round's text is finalized
let _sourcesHtml = ''; // Sources box HTML to prepend to body
let _sourcesExpanded = false; // Track if user expanded sources during stream
@@ -1055,6 +1056,14 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
let _sourcesType = ''; // 'web' or 'research'
let _findingsData = null; // Raw findings data for collapsible box
// _keepResearchOn removed — clarification state now persisted server-side via DB mode
function _metricsTargetForTurn() {
const visibleRound = (roundHolder && roundHolder.style.display !== 'none') ? roundHolder : null;
const visibleText = visibleRound ? (visibleRound.querySelector('.body')?.textContent || '').trim() : '';
if (lastToolThread && lastToolThread.isConnected && (!visibleRound || !visibleText || visibleText === 'Done.')) {
return lastToolThread;
}
return visibleRound || holder;
}
// Insert sources box as a stable DOM node that won't be replaced during streaming.
// Returns the content container to use for innerHTML updates.
function _ensureStreamLayout(body) {
@@ -1332,7 +1341,9 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
_streamSawDone = true;
// Always update background map if entry exists (even if user switched back)
var bgDone = _backgroundStreams.get(streamSessionId);
if (bgDone) {
if (bgDone && !_isBg) {
_backgroundStreams.delete(streamSessionId);
} else if (bgDone) {
bgDone.status = 'completed';
bgDone.accumulated = accumulated;
if (_isBg) {
@@ -1456,9 +1467,10 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
}
// --- Text-fence doc streaming (for models that don't use native tool calls) ---
if (!_docFenceOpened && documentModule && roundText.includes('```create_document\n')) {
const fenceIdx = roundText.indexOf('```create_document\n');
const afterFence = roundText.slice(fenceIdx + '```create_document\n'.length);
if (!_docFenceOpened && documentModule && (roundText.includes('```create_document\n') || roundText.includes('```document\n'))) {
const fenceMarker = roundText.includes('```document\n') ? '```document\n' : '```create_document\n';
const fenceIdx = roundText.indexOf(fenceMarker);
const afterFence = roundText.slice(fenceIdx + fenceMarker.length);
const fenceLines = afterFence.split('\n');
if (fenceLines.length >= 1 && fenceLines[0].trim()) {
_docFenceOpened = true;
@@ -1467,7 +1479,7 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
const knownLangs = ['python','py','javascript','js','typescript','ts','html','css','json','yaml','bash','sql','rust','go','java','c','cpp','markdown','text','plain','ruby','swift','kotlin','php','email','csv','xml','toml','ini'];
const isLang = fenceLines.length >= 2 && knownLangs.includes(fenceLines[1].trim().toLowerCase());
const lang = isLang ? fenceLines[1].trim() : '';
_docFenceContentStart = fenceIdx + '```create_document\n'.length + title.length + 1 + (isLang ? fenceLines[1].length + 1 : 0);
_docFenceContentStart = fenceIdx + fenceMarker.length + title.length + 1 + (isLang ? fenceLines[1].length + 1 : 0);
documentModule.streamDocOpen(title, lang);
}
}
@@ -2033,6 +2045,10 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
if (bgM) bgM.metrics = json.data;
continue;
}
if (metrics) {
const metricsTarget = _metricsTargetForTurn();
if (metricsTarget) displayMetrics(metricsTarget, metrics);
}
} else if (json.type === 'message_saved') {
// Wire the persisted DB id onto the just-streamed bubble so it
@@ -2113,6 +2129,7 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
chatBox.appendChild(threadWrap);
}
threadWrap.classList.add('streaming');
lastToolThread = threadWrap;
const toolLabel = _toolLabels[json.tool.toLowerCase()] || json.tool;
const toolIcon = _toolIcons[json.tool.toLowerCase()] || '\u25B6';
const node = document.createElement('div')
@@ -2471,6 +2488,7 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
}
_renderStream();
if (spinner && spinner.element) { try { spinner.destroy(); } catch (_) {} spinner = null; }
_cancelThinkingTimer();
_removeThinkingSpinner();
// Stop any thread pulse animations
@@ -2650,7 +2668,9 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
// Attach footer to the last visible bubble (roundHolder for multi-round agent, holder for single)
const footerTarget = (roundHolder && roundHolder !== holder && roundHolder.style.display !== 'none') ? roundHolder : holder;
footerTarget.appendChild(createMsgFooter(footerTarget));
if (!footerTarget.querySelector('.msg-footer')) {
footerTarget.appendChild(createMsgFooter(footerTarget));
}
// Add "View Report" link for completed research
if (_researchingStreamIds.has(streamSessionId)) {
_appendViewReportLink(footerTarget, streamSessionId);
@@ -2690,7 +2710,7 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
}
}
if (metrics) {
displayMetrics(footerTarget, metrics);
displayMetrics(_metricsTargetForTurn() || footerTarget, metrics);
}
// Attach variant navigation if this was a regeneration
_attachVariantNav(footerTarget);
+13 -4
View File
@@ -1753,7 +1753,7 @@ export function displayMetrics(messageElement, metrics) {
const cost = _billableCost(model, inputTokens, outputTokens);
// Nothing useful to show — bail out (only if ALL metrics are missing)
if (!responseTime && !outputTokens && tps == null && !ctxPct) return;
if (!responseTime && !inputTokens && !outputTokens && tps == null && !ctxPct) return;
// Accumulate session cost (only on fresh metrics, not history reload)
if (!metrics._fromHistory) {
@@ -1776,9 +1776,11 @@ export function displayMetrics(messageElement, metrics) {
? `${outputTokens} tok · ${costStr0}`
: outputTokens
? `${outputTokens} tok · ${responseTime != null ? responseTime + 's' : ''}`
: responseTime != null
? `${responseTime}s`
: '';
: inputTokens
? `${inputTokens} in${responseTime != null ? ' · ' + responseTime + 's' : ''}`
: responseTime != null
? `${responseTime}s`
: '';
if (!metricsLabel) return;
metricsContainer.textContent = metricsLabel;
metricsContainer.style.cursor = 'pointer';
@@ -1996,6 +1998,13 @@ export function displayMetrics(messageElement, metrics) {
}
let footer = messageElement.querySelector('.msg-footer');
if (!footer) {
footer = createMsgFooter(messageElement);
if (messageElement.classList?.contains('agent-thread')) {
footer.classList.add('agent-thread-footer');
}
messageElement.appendChild(footer);
}
if (footer) {
const actions = footer.querySelector('.msg-actions');
if (actions) {
+4
View File
@@ -3363,6 +3363,10 @@ body.bg-pattern-sparkles {
transition: color .15s;
}
.msg-footer .response-metrics:hover { color:var(--fg); }
.agent-thread > .agent-thread-footer {
margin-top: 4px;
margin-left: 2px;
}
/* Context usage ring — right side of footer */
.ctx-ring {
display: inline-flex;