u
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user