u
This commit is contained in:
@@ -73,7 +73,7 @@ function parseMessagePayload(raw) {
|
|||||||
return { content: raw };
|
return { content: raw };
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatUsageText(usage) {
|
function formatUsageText(usage, prefix = '') {
|
||||||
if (!usage) return '';
|
if (!usage) return '';
|
||||||
const input = Number(usage.input ?? usage.prompt_tokens ?? 0);
|
const input = Number(usage.input ?? usage.prompt_tokens ?? 0);
|
||||||
const output = Number(usage.output ?? usage.completion_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 cost = typeof usage.cost === 'number' ? ` · $${usage.cost.toFixed(6)}` : '';
|
||||||
const fmt = (n) => n.toLocaleString('de-DE');
|
const fmt = (n) => n.toLocaleString('de-DE');
|
||||||
const cachedPart = cached > 0 ? ` / ${fmt(cached)} Cache` : '';
|
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) {
|
function formatToolTooltip(content, result, parameters, prefix, error) {
|
||||||
@@ -652,11 +685,11 @@ function WorkIconView({ tool }) {
|
|||||||
return iconElement;
|
return iconElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
function UsageChipView({ usage }) {
|
function UsageChipView({ usage, prefix = '', isSummary = false, title = '' }) {
|
||||||
const text = formatUsageText(usage);
|
const text = formatUsageText(usage, prefix);
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
|
|
||||||
return (
|
const chip = (
|
||||||
<Box
|
<Box
|
||||||
component="span"
|
component="span"
|
||||||
sx={{
|
sx={{
|
||||||
@@ -664,8 +697,11 @@ function UsageChipView({ usage }) {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
fontSize: '0.68rem',
|
fontSize: '0.68rem',
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
color: 'text.secondary',
|
color: isSummary ? 'text.primary' : 'text.secondary',
|
||||||
bgcolor: 'action.hover',
|
fontWeight: isSummary ? 600 : 400,
|
||||||
|
bgcolor: isSummary ? 'action.selected' : 'action.hover',
|
||||||
|
border: isSummary ? '1px solid' : 'none',
|
||||||
|
borderColor: isSummary ? 'divider' : 'transparent',
|
||||||
px: 0.8,
|
px: 0.8,
|
||||||
py: 0.25,
|
py: 0.25,
|
||||||
borderRadius: 1.5,
|
borderRadius: 1.5,
|
||||||
@@ -675,6 +711,16 @@ function UsageChipView({ usage }) {
|
|||||||
{text}
|
{text}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
return (
|
||||||
|
<Tooltip title={title} arrow placement="top">
|
||||||
|
{chip}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chip;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TurnRow({ turn, isLive = false }) {
|
export function TurnRow({ turn, isLive = false }) {
|
||||||
@@ -960,11 +1006,22 @@ export function normalizeChatMessages(rawMessages = []) {
|
|||||||
function MessageRow({ msg, showTime = true }) {
|
function MessageRow({ msg, showTime = true }) {
|
||||||
// If the message has structured turns, render each turn
|
// If the message has structured turns, render each turn
|
||||||
if (msg.turns && Array.isArray(msg.turns) && msg.turns.length > 0) {
|
if (msg.turns && Array.isArray(msg.turns) && msg.turns.length > 0) {
|
||||||
|
const summaryUsage = aggregateUsage(msg.turns);
|
||||||
|
const turnCount = msg.turns.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
|
||||||
{msg.turns.map((t, idx) => (
|
{msg.turns.map((t, idx) => (
|
||||||
<TurnRow key={t.id || idx} turn={t} isLive={false} />
|
<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>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1198,11 +1255,32 @@ export default class ChatFlowList extends PureComponent {
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Live streaming turns */}
|
{/* Live streaming turns */}
|
||||||
{activeTurns && activeTurns.length > 0
|
{activeTurns && activeTurns.length > 0 ? (
|
||||||
? activeTurns.map((turn, idx) => (
|
<>
|
||||||
|
{activeTurns.map((turn, idx) => (
|
||||||
<TurnRow key={turn.id || idx} turn={turn} isLive={true} />
|
<TurnRow key={turn.id || idx} turn={turn} isLive={true} />
|
||||||
))
|
))}
|
||||||
: null}
|
{(() => {
|
||||||
|
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 }} />
|
<div ref={this.bottomRef} style={{ height: 1, flexShrink: 0 }} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user