This commit is contained in:
seb
2026-10-20 22:17:27 +02:00
parent 9bc6d55e85
commit c77297af72

View File

@@ -73,7 +73,7 @@ function parseMessagePayload(raw) {
return { content: raw };
}
function formatUsageText(usage) {
function formatUsageText(usage, prefix = '') {
if (!usage) return '';
const input = Number(usage.input ?? usage.prompt_tokens ?? 0);
const output = Number(usage.output ?? usage.completion_tokens ?? 0);
@@ -81,7 +81,40 @@ function formatUsageText(usage) {
const cost = typeof usage.cost === 'number' ? ` · $${usage.cost.toFixed(6)}` : '';
const fmt = (n) => n.toLocaleString('de-DE');
const cachedPart = cached > 0 ? ` / ${fmt(cached)} Cache` : '';
return `${fmt(input)} Prompt${cachedPart} · ${fmt(output)} Antwort${cost}`;
const pfx = prefix ? `${prefix} ` : '';
return `${pfx}${fmt(input)} Prompt${cachedPart} · ${fmt(output)} Antwort${cost}`;
}
export function aggregateUsage(turns = []) {
if (!Array.isArray(turns) || turns.length === 0) return null;
let hasUsage = false;
let totalInput = 0;
let totalOutput = 0;
let totalCached = 0;
let totalCost = 0;
let hasCost = false;
for (const turn of turns) {
const u = turn?.usage;
if (!u) continue;
hasUsage = true;
totalInput += Number(u.input ?? u.prompt_tokens ?? 0);
totalOutput += Number(u.output ?? u.completion_tokens ?? 0);
totalCached += Number(u.cached ?? 0);
if (typeof u.cost === 'number' && !Number.isNaN(u.cost)) {
totalCost += u.cost;
hasCost = true;
}
}
if (!hasUsage) return null;
return {
input: totalInput,
output: totalOutput,
cached: totalCached,
cost: hasCost ? totalCost : undefined,
};
}
function formatToolTooltip(content, result, parameters, prefix, error) {
@@ -455,12 +488,12 @@ function tryExtractObject(val) {
const trimmed = val.trim();
try {
return JSON.parse(trimmed);
} catch {}
} catch { }
const match = trimmed.match(/\{[\s\S]*\}/);
if (match) {
try {
return JSON.parse(match[0]);
} catch {}
} catch { }
}
}
return null;
@@ -652,11 +685,11 @@ function WorkIconView({ tool }) {
return iconElement;
}
function UsageChipView({ usage }) {
const text = formatUsageText(usage);
function UsageChipView({ usage, prefix = '', isSummary = false, title = '' }) {
const text = formatUsageText(usage, prefix);
if (!text) return null;
return (
const chip = (
<Box
component="span"
sx={{
@@ -664,8 +697,11 @@ function UsageChipView({ usage }) {
alignItems: 'center',
fontSize: '0.68rem',
fontFamily: 'monospace',
color: 'text.secondary',
bgcolor: 'action.hover',
color: isSummary ? 'text.primary' : 'text.secondary',
fontWeight: isSummary ? 600 : 400,
bgcolor: isSummary ? 'action.selected' : 'action.hover',
border: isSummary ? '1px solid' : 'none',
borderColor: isSummary ? 'divider' : 'transparent',
px: 0.8,
py: 0.25,
borderRadius: 1.5,
@@ -675,6 +711,16 @@ function UsageChipView({ usage }) {
{text}
</Box>
);
if (title) {
return (
<Tooltip title={title} arrow placement="top">
{chip}
</Tooltip>
);
}
return chip;
}
export function TurnRow({ turn, isLive = false }) {
@@ -960,11 +1006,22 @@ export function normalizeChatMessages(rawMessages = []) {
function MessageRow({ msg, showTime = true }) {
// If the message has structured turns, render each turn
if (msg.turns && Array.isArray(msg.turns) && msg.turns.length > 0) {
const summaryUsage = aggregateUsage(msg.turns);
const turnCount = msg.turns.length;
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
{msg.turns.map((t, idx) => (
<TurnRow key={t.id || idx} turn={t} isLive={false} />
))}
{summaryUsage ? (
<Box sx={{ display: 'flex', alignItems: 'center', px: 2, py: 0.4 }}>
<UsageChipView
usage={summaryUsage}
isSummary={true}
/>
</Box>
) : null}
</Box>
);
}
@@ -1198,11 +1255,32 @@ export default class ChatFlowList extends PureComponent {
})}
{/* Live streaming turns */}
{activeTurns && activeTurns.length > 0
? activeTurns.map((turn, idx) => (
<TurnRow key={turn.id || idx} turn={turn} isLive={true} />
))
: null}
{activeTurns && activeTurns.length > 0 ? (
<>
{activeTurns.map((turn, idx) => (
<TurnRow key={turn.id || idx} turn={turn} isLive={true} />
))}
{(() => {
const activeSummary = aggregateUsage(activeTurns);
if (!activeSummary) return null;
const count = activeTurns.length;
const prefix = count > 1 ? `Summe (${count} Turns):` : 'Summe:';
const tooltipTitle = count > 1
? `Zusammenfassung von ${count} LLM-Turns`
: 'Zusammenfassung des LLM-Turns';
return (
<Box sx={{ display: 'flex', alignItems: 'center', px: 2, py: 0.4 }}>
<UsageChipView
usage={activeSummary}
prefix={prefix}
isSummary={true}
title={tooltipTitle}
/>
</Box>
);
})()}
</>
) : null}
<div ref={this.bottomRef} style={{ height: 1, flexShrink: 0 }} />
</Box>