Skip to content

Commit df2f637

Browse files
authored
fix(frontend): improve Logs page UX — live duration, cost layout, null display (#411)
## Summary Several UX improvements to the Logs page desktop view: ### Live Duration for In-Flight Requests - **Fix NaN duration** — newly received SSE `started` events have no `durationMs`, causing `formatMs` to render "NaNs". Now computes live elapsed time from `startTime` (`Date.now() - startTime`), updated every 100ms. - **Consistent "Duration:" label** — duration is always shown as the top row in the Perf column, whether the request is in progress (with bytes/throughput below) or completed (with TTFT/TPS below). - **Gauge icon** — added speedometer icon before bytes/sec rate in the progress view. <img width="203" height="173" alt="Screenshot 2026-05-16 at 4 08 07 PM" src="https://github.com/user-attachments/assets/70f02033-dd45-4d20-ae6b-ab8768a88ee6" /> ### Cost Column Redesign - **Stacked layout** — replaced side-by-side (total | breakdown) with a compact 3-row layout: total cost on top, 2×2 breakdown grid below. - **Separator** — subtle border between total cost and breakdown rows. - **CSS grid alignment** — breakdown uses `grid-template-columns: auto 1fr auto 1fr` so `$` characters align across rows. - **6-decimal total, 4-decimal breakdown** — total cost shows full precision; components are concise. - **`$<X` prefix** — `formatCost` now shows `$<0.0001` for sub-threshold values instead of implying zero. - **`$-.----` for null costs** — replaces `∅` with format-consistent placeholder. <img width="355" height="177" alt="Screenshot 2026-05-16 at 4 07 48 PM" src="https://github.com/user-attachments/assets/ae0b9ef9-85eb-42b6-a772-25510bb7a786" /> ### Null/Zero Display - **Cost** — centered `-` instead of `∅` for no-data rows; `$-.----` for zero breakdown components. - **Meta** — `-` instead of `0` for `messageCount`, `toolCallsCount`, `toolsDefined`.
2 parents 3bc237d + 2eb519d commit df2f637

2 files changed

Lines changed: 120 additions & 84 deletions

File tree

packages/frontend/src/lib/format.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,21 @@ export function formatTokens(tokens: number): string {
6262
}
6363

6464
/**
65-
* Format cost in dollars (e.g., "$0.001234", "$1.23")
66-
*/
67-
export function formatCost(cost: number, maxDecimals: number = 6): string {
68-
if (cost === 0) return '$0';
65+
* Format cost in dollars, always showing 4 decimal places.
66+
* Costs below $0.0001 are shown as "$<0.0001" to avoid implying zero.
67+
* Costs of exactly $0 show "$0.0000".
68+
*/
69+
export function formatCost(cost: number, decimals: number = 4): string {
70+
if (cost === 0) return `$${cost.toFixed(decimals)}`;
71+
const threshold = Math.pow(10, -decimals);
72+
if (cost > 0 && cost < threshold) return `$<${threshold.toFixed(decimals)}`;
6973
if (cost >= 0.01) {
70-
// For costs >= 1 cent, use 2-4 decimals
7174
return `$${cost.toLocaleString(undefined, {
72-
minimumFractionDigits: 2,
73-
maximumFractionDigits: 4,
75+
minimumFractionDigits: decimals,
76+
maximumFractionDigits: decimals,
7477
})}`;
7578
}
76-
// For small costs, show up to maxDecimals
77-
return `$${cost.toFixed(maxDecimals)}`;
79+
return `$${cost.toFixed(decimals)}`;
7880
}
7981

8082
/**

packages/frontend/src/pages/Logs.tsx

Lines changed: 109 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import {
6565
Timer,
6666
CheckCircle,
6767
XCircle,
68+
Gauge,
6869
} from 'lucide-react';
6970
import { clsx } from 'clsx';
7071
import { useNavigate } from 'react-router-dom';
@@ -167,6 +168,13 @@ export const Logs = () => {
167168
// progressTick is incremented to trigger re-renders when progress data changes.
168169
// The value itself is intentionally unused; only the setter is called.
169170
const [, setProgressTick] = useState(0);
171+
// liveTick triggers re-renders every 100ms so pending-request durations update live.
172+
const [, setLiveTick] = useState(0);
173+
174+
useEffect(() => {
175+
const interval = setInterval(() => setLiveTick((t) => t + 1), 100);
176+
return () => clearInterval(interval);
177+
}, []);
170178

171179
const loadLogs = async () => {
172180
setLoading(true);
@@ -645,31 +653,45 @@ export const Logs = () => {
645653
log.responseStatus === 'pending'
646654
? progressMapRef.current.get(log.requestId)
647655
: undefined;
656+
const liveDuration = formatMs(
657+
log.durationMs != null ? log.durationMs : Date.now() - log.startTime
658+
);
648659
if (progress) {
649660
return (
650661
<div
651662
style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}
652663
>
653-
<div
654-
style={{ display: 'flex', alignItems: 'center', gap: '4px' }}
664+
<span>Duration: {liveDuration}</span>
665+
<span
666+
style={{
667+
color: 'var(--color-text-secondary)',
668+
fontSize: '0.85em',
669+
display: 'flex',
670+
alignItems: 'center',
671+
gap: '4px',
672+
}}
655673
>
656674
<CloudDownload size={11} className="text-yellow-400" />
657675
<span>{formatBytes(progress.bytesReceived)}</span>
658-
</div>
676+
</span>
659677
{progress.bytesPerSec != null && (
660678
<span
661679
style={{
662680
color: 'var(--color-text-secondary)',
663681
fontSize: '0.85em',
682+
display: 'flex',
683+
alignItems: 'center',
684+
gap: '4px',
664685
}}
665686
>
687+
<Gauge size={11} className="text-text-secondary" />
666688
{formatBytes(progress.bytesPerSec)}/s
667689
</span>
668690
)}
669691
</div>
670692
);
671693
}
672-
return formatMs(log.durationMs);
694+
return liveDuration;
673695
})()}
674696
</div>
675697
</div>
@@ -678,7 +700,8 @@ export const Logs = () => {
678700
Meta
679701
</div>
680702
<div className="text-text">
681-
{log.messageCount || 0} msg / {log.toolCallsCount || 0} tools
703+
{(log.messageCount || 0) === 0 ? '-' : log.messageCount} msg /{' '}
704+
{(log.toolCallsCount || 0) === 0 ? '-' : log.toolCallsCount} tools
682705
</div>
683706
</div>
684707
</div>
@@ -754,7 +777,7 @@ export const Logs = () => {
754777
</th>
755778
<th
756779
className="px-2 py-1.5 text-center border-b border-border-glass border-r border-r-border-glass bg-bg-hover font-semibold text-text-secondary text-[11px] uppercase tracking-wider whitespace-nowrap"
757-
style={{ minWidth: '70px' }}
780+
style={{ minWidth: '130px' }}
758781
>
759782
{renderSortableHeader('Cost', 'costTotal')}
760783
</th>
@@ -1135,81 +1158,78 @@ export const Logs = () => {
11351158
</td>
11361159
<td className="px-2 py-1.5 border-b border-border-glass text-text align-middle">
11371160
{log.costTotal !== undefined && log.costTotal !== null ? (
1138-
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
1139-
{/* Left side: Total cost */}
1140-
<div style={{ minWidth: '50px' }}>
1161+
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
1162+
{/* Row 1: Total cost */}
1163+
<div>
11411164
{log.costSource ? (
11421165
<CostToolTip
11431166
source={log.costSource}
11441167
costMetadata={log.costMetadata}
11451168
>
11461169
<span style={{ fontWeight: '500', cursor: 'help' }}>
1147-
{log.costTotal === 0 ? '' : formatCost(log.costTotal)}
1170+
{log.costTotal === 0 ? '-' : formatCost(log.costTotal, 6)}
11481171
</span>
11491172
</CostToolTip>
11501173
) : (
11511174
<span style={{ fontWeight: '500' }}>
1152-
{log.costTotal === 0 ? '' : formatCost(log.costTotal)}
1175+
{log.costTotal === 0 ? '-' : formatCost(log.costTotal, 6)}
11531176
</span>
11541177
)}
11551178
</div>
1156-
{/* Right side: Breakdown */}
1157-
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
1158-
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
1159-
<CloudUpload size={10} className="text-blue-400" />
1160-
<span
1161-
style={{
1162-
color: 'var(--color-text-secondary)',
1163-
fontSize: '0.85em',
1164-
minWidth: '35px',
1165-
}}
1166-
>
1167-
{log.costInput === 0 ? '∅' : formatCost(log.costInput || 0)}
1168-
</span>
1169-
</div>
1170-
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
1171-
<CloudDownload size={10} className="text-green-400" />
1172-
<span
1173-
style={{
1174-
color: 'var(--color-text-secondary)',
1175-
fontSize: '0.85em',
1176-
minWidth: '35px',
1177-
}}
1178-
>
1179-
{log.costOutput === 0 ? '∅' : formatCost(log.costOutput || 0)}
1180-
</span>
1181-
</div>
1182-
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
1183-
<PackageOpen size={10} className="text-orange-400" />
1184-
<span
1185-
style={{
1186-
color: 'var(--color-text-secondary)',
1187-
fontSize: '0.85em',
1188-
minWidth: '35px',
1189-
}}
1190-
>
1191-
{log.costCached === 0 ? '∅' : formatCost(log.costCached || 0)}
1192-
</span>
1193-
</div>
1194-
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
1195-
<PencilLine size={10} className="text-fuchsia-400" />
1196-
<span
1197-
style={{
1198-
color: 'var(--color-text-secondary)',
1199-
fontSize: '0.85em',
1200-
minWidth: '35px',
1201-
}}
1202-
>
1203-
{log.costCacheWrite === 0
1204-
? '∅'
1205-
: formatCost(log.costCacheWrite || 0)}
1206-
</span>
1207-
</div>
1179+
{/* Separator */}
1180+
<div
1181+
style={{
1182+
borderTop: '1px solid var(--color-border-glass)',
1183+
margin: '1px 2px',
1184+
}}
1185+
/>
1186+
{/* Breakdown grid: 2 rows x 4 columns (icon, value, icon, value) */}
1187+
<div
1188+
style={{
1189+
display: 'grid',
1190+
gridTemplateColumns: 'auto 1fr auto 1fr',
1191+
gap: '2px 4px',
1192+
alignItems: 'center',
1193+
}}
1194+
>
1195+
<CloudUpload size={10} className="text-blue-400" />
1196+
<span
1197+
style={{ color: 'var(--color-text-secondary)', fontSize: '0.85em' }}
1198+
>
1199+
{log.costInput === 0 ? '$-.----' : formatCost(log.costInput || 0)}
1200+
</span>
1201+
<CloudDownload size={10} className="text-green-400" />
1202+
<span
1203+
style={{ color: 'var(--color-text-secondary)', fontSize: '0.85em' }}
1204+
>
1205+
{log.costOutput === 0 ? '$-.----' : formatCost(log.costOutput || 0)}
1206+
</span>
1207+
<PackageOpen size={10} className="text-orange-400" />
1208+
<span
1209+
style={{ color: 'var(--color-text-secondary)', fontSize: '0.85em' }}
1210+
>
1211+
{log.costCached === 0 ? '$-.----' : formatCost(log.costCached || 0)}
1212+
</span>
1213+
<PencilLine size={10} className="text-fuchsia-400" />
1214+
<span
1215+
style={{ color: 'var(--color-text-secondary)', fontSize: '0.85em' }}
1216+
>
1217+
{log.costCacheWrite === 0
1218+
? '$-.----'
1219+
: formatCost(log.costCacheWrite || 0)}
1220+
</span>
12081221
</div>
12091222
</div>
12101223
) : (
1211-
<span style={{ color: 'var(--color-text-secondary)', fontSize: '1.2em' }}>
1212-
1224+
<span
1225+
style={{
1226+
color: 'var(--color-text-secondary)',
1227+
fontSize: '1.2em',
1228+
display: 'block',
1229+
textAlign: 'center',
1230+
}}
1231+
>
1232+
-
12131233
</span>
12141234
)}
12151235
</td>
@@ -1219,22 +1239,36 @@ export const Logs = () => {
12191239
log.responseStatus === 'pending'
12201240
? progressMapRef.current.get(log.requestId)
12211241
: undefined;
1242+
const liveDuration = formatMs(
1243+
log.durationMs != null ? log.durationMs : Date.now() - log.startTime
1244+
);
12221245
if (progress) {
12231246
return (
1224-
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
1225-
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
1247+
<div style={{ display: 'flex', flexDirection: 'column' }}>
1248+
<span>Duration: {liveDuration}</span>
1249+
<span
1250+
style={{
1251+
color: 'var(--color-text-secondary)',
1252+
fontSize: '0.85em',
1253+
display: 'flex',
1254+
alignItems: 'center',
1255+
gap: '4px',
1256+
}}
1257+
>
12261258
<CloudDownload size={12} className="text-yellow-400" />
1227-
<span style={{ fontSize: '0.9em' }}>
1228-
{formatBytes(progress.bytesReceived)}
1229-
</span>
1230-
</div>
1259+
<span>{formatBytes(progress.bytesReceived)}</span>
1260+
</span>
12311261
{progress.bytesPerSec != null && (
12321262
<span
12331263
style={{
12341264
color: 'var(--color-text-secondary)',
12351265
fontSize: '0.85em',
1266+
display: 'flex',
1267+
alignItems: 'center',
1268+
gap: '4px',
12361269
}}
12371270
>
1271+
<Gauge size={12} className="text-text-secondary" />
12381272
{formatBytes(progress.bytesPerSec)}/s
12391273
</span>
12401274
)}
@@ -1243,7 +1277,7 @@ export const Logs = () => {
12431277
}
12441278
return (
12451279
<div style={{ display: 'flex', flexDirection: 'column' }}>
1246-
<span>Duration: {formatMs(log.durationMs)}</span>
1280+
<span>Duration: {liveDuration}</span>
12471281
<span
12481282
style={{
12491283
color: 'var(--color-text-secondary)',
@@ -1292,7 +1326,7 @@ export const Logs = () => {
12921326
<span
12931327
style={{ fontWeight: '500', fontSize: '0.9em', minWidth: '20px' }}
12941328
>
1295-
{log.messageCount || 0}
1329+
{(log.messageCount || 0) === 0 ? '-' : log.messageCount}
12961330
</span>
12971331
</div>
12981332
<div
@@ -1307,7 +1341,7 @@ export const Logs = () => {
13071341
minWidth: '20px',
13081342
}}
13091343
>
1310-
{log.toolCallsCount || 0}
1344+
{(log.toolCallsCount || 0) === 0 ? '-' : log.toolCallsCount}
13111345
</span>
13121346
</div>
13131347
</div>
@@ -1321,7 +1355,7 @@ export const Logs = () => {
13211355
<span
13221356
style={{ fontWeight: '500', fontSize: '0.9em', minWidth: '20px' }}
13231357
>
1324-
{log.toolsDefined || 0}
1358+
{(log.toolsDefined || 0) === 0 ? '-' : log.toolsDefined}
13251359
</span>
13261360
</div>
13271361
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>

0 commit comments

Comments
 (0)