From 844e67c18734f9e1ee4b6e3824bad4b635effb53 Mon Sep 17 00:00:00 2001 From: seakee Date: Tue, 28 Jul 2026 15:47:16 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(monitoring):=20refine=20real?= =?UTF-8?q?time=20request=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refine realtime request metrics, status diagnostics, and token usage details. Keep TPS based on total latency to avoid misleading generation-rate spikes. Improve compact table alignment and tooltip readability without changing stored data. --- .../MonitoringCenterPage.module.scss | 213 +++++++-- .../components/RealtimeEventsPanel.test.tsx | 134 ++++-- .../components/RealtimeEventsPanel.tsx | 443 +++++++++++++----- .../monitoring/model/eventRows.test.ts | 6 +- .../styles/_monitoring-summary-table.scss | 6 +- apps/web/src/i18n/locales/en.json | 14 +- apps/web/src/i18n/locales/ru.json | 14 +- apps/web/src/i18n/locales/zh-CN.json | 14 +- apps/web/src/i18n/locales/zh-TW.json | 14 +- 9 files changed, 640 insertions(+), 218 deletions(-) diff --git a/apps/web/src/features/monitoring/MonitoringCenterPage.module.scss b/apps/web/src/features/monitoring/MonitoringCenterPage.module.scss index ef1b4bc14..459a9514b 100644 --- a/apps/web/src/features/monitoring/MonitoringCenterPage.module.scss +++ b/apps/web/src/features/monitoring/MonitoringCenterPage.module.scss @@ -1301,13 +1301,16 @@ @include summaryTable.styles; @include accountOverview.styles; .realtimeTable { - --realtime-col-source: 18%; - --realtime-col-model: 15%; + --realtime-col-source: 16%; + --realtime-col-model: 16%; + --realtime-col-tps: 8%; + --realtime-col-latency: 8%; + --realtime-col-time: 8%; } .realtimeTable .logTypeCell .primaryCell > span { display: inline-block; - max-width: min(240px, 100%); + max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -1319,10 +1322,19 @@ line-height: 1.45; } -.realtimeCacheTokenSummary { +.realtimeTable .logTypeCell .primaryCell > small { display: block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.realtimeUsageCell { + display: grid; + gap: 4px; + min-width: 0; font-variant-numeric: tabular-nums; - letter-spacing: 0.01em; cursor: help; &:focus-visible { @@ -1332,6 +1344,85 @@ } } +.realtimeUsageTotal { + color: var(--monitor-ink); +} + +.realtimeUsageFlow { + display: grid; + grid-template-columns: max-content max-content; + column-gap: 8px; + color: var(--monitor-muted); + font-size: 11px; + line-height: 1.25; + white-space: nowrap; +} + +.realtimeUsageMetric { + display: inline-flex; + align-items: center; + gap: 3px; +} + +.realtimeUsageTooltip { + --monitor-surface: var(--bg-primary); + --monitor-surface-elevated: var(--app-surface-strong); + --monitor-ink: var(--text-primary); + --monitor-muted: var(--text-secondary); + --monitor-line: var(--border-color); + + position: fixed; + z-index: 2030; + display: grid; + box-sizing: border-box; + gap: 6px; + min-width: 0; + max-width: calc(100vw - 24px); + padding: 10px 12px; + border: 1px solid color-mix(in srgb, var(--monitor-line) 86%, transparent); + border-radius: 8px; + background: var(--monitor-surface-elevated); + box-shadow: var(--shadow-lg); + color: var(--monitor-ink); + font-size: 12px; + line-height: 1.35; + opacity: 0; + pointer-events: none; + transform: translateX(4px); + transition: + opacity 0.12s ease, + transform 0.12s ease; + visibility: hidden; +} + +.realtimeUsageTooltipLeftAbove, +.realtimeUsageTooltipLeftBelow { + transform-origin: right center; +} + +.realtimeUsageTooltipOpen { + opacity: 1; + pointer-events: auto; + transform: translateX(0); + visibility: visible; +} + +.realtimeUsageTooltipLine { + display: grid; + grid-template-columns: 1fr max-content; + gap: 16px; +} + +.realtimeUsageTooltipLabel { + color: var(--monitor-muted); +} + +.realtimeUsageTooltipValue { + color: var(--monitor-ink); + font-variant-numeric: tabular-nums; + text-align: right; +} + .realtimeModelCell { max-width: 100%; } @@ -1352,69 +1443,115 @@ font-weight: 500; } -.realtimeMetricCell, -.realtimeTimeCell { - display: grid; - gap: 3px; - min-width: 0; - color: var(--monitor-ink); - font-variant-numeric: tabular-nums; - line-height: 1.25; -} - -.realtimeTimeLine, -.realtimeTpsCell { +.realtimeTimeLine { white-space: nowrap; } -.realtimeLatencyColumn { +.realtimeLatencyColumn, +.realtimeTimeColumn, +.realtimeTpsColumn, +.realtimeCenteredColumn { text-align: center !important; } -.realtimeLatencyHeader { - display: inline-grid; - grid-template-columns: minmax(42px, 1fr) 10px minmax(42px, 1fr); - align-items: center; +.realtimeSettingsColumn { + text-align: left !important; +} + +.realtimeCenteredColumn .primaryCell { + justify-items: center; + text-align: center; +} + +.realtimeCenteredColumn .recentStatusCell { justify-content: center; - width: 118px; +} + +.realtimeSettingsCell { + display: inline-grid; + grid-template-columns: max-content max-content; + column-gap: 6px; + row-gap: 3px; + justify-content: start; + min-width: 0; + color: var(--monitor-ink); font-size: 12px; + font-variant-numeric: tabular-nums; line-height: 1.25; - text-transform: none; + text-align: left; white-space: nowrap; } -.realtimeMetricCell { +.realtimeSettingLine { + display: contents; +} + +.realtimeSettingLabel, +.realtimeMetricLabel { + color: var(--monitor-muted); + font-weight: 400; +} + +.realtimeSettingValue { + font-weight: 400; +} + +.realtimeReasoningValue { + color: var(--monitor-accent-strong); +} + +.realtimeServiceValue { + color: var(--monitor-muted); +} + +.realtimeMetricCell, +.realtimeTimeCell { display: inline-grid; - grid-template-columns: minmax(48px, 1fr) 10px minmax(48px, 1fr); align-items: center; justify-content: center; - width: 118px; - min-width: 118px; + min-width: 0; + color: var(--monitor-ink); font-size: 12px; font-weight: 700; + font-variant-numeric: tabular-nums; + line-height: 1.25; white-space: nowrap; } -.realtimeTpsColumn { - text-align: center !important; +.realtimeMetricCell { + grid-template-columns: max-content max-content; + column-gap: 6px; + row-gap: 3px; } .realtimeTpsCell { display: inline-block; min-width: 48px; + color: var(--monitor-ink); + font-size: 12px; + font-weight: 700; font-variant-numeric: tabular-nums; + line-height: 1.25; } .realtimeTimeCell { - min-width: 94px; + grid-template-columns: max-content; + gap: 3px; + justify-items: center; + line-height: 1.3; + text-align: center; +} + +.realtimeMetricLine { + display: contents; } .realtimeTimeLine { - width: 92px; font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.3; + text-align: center; } .realtimeMetricText { @@ -1423,12 +1560,12 @@ line-height: 1.25; } -.realtimeMetricLeft { +.realtimeMetricLabel { min-width: 0; - text-align: right; + text-align: left; } -.realtimeMetricRight { +.realtimeMetricText { min-width: 0; text-align: left; } @@ -1445,12 +1582,6 @@ color: var(--monitor-red); } -.realtimeMetricSeparator { - color: var(--monitor-line); - font-weight: 600; - line-height: 1.25; - text-align: center; -} .logTypeCell { display: block; diff --git a/apps/web/src/features/monitoring/components/RealtimeEventsPanel.test.tsx b/apps/web/src/features/monitoring/components/RealtimeEventsPanel.test.tsx index 2809f0158..efa044296 100644 --- a/apps/web/src/features/monitoring/components/RealtimeEventsPanel.test.tsx +++ b/apps/web/src/features/monitoring/components/RealtimeEventsPanel.test.tsx @@ -23,6 +23,7 @@ const t = ((key: string, options?: Record) => { 'monitoring.column_output_tps': 'TPS', 'monitoring.column_source_api_key': 'Source / API Key', 'monitoring.column_success_rate': 'Success', + 'monitoring.column_success_rate_short': 'Success Rate', 'monitoring.column_time': 'Time', 'monitoring.column_type': 'Type', 'monitoring.elapsed_short': 'Elapsed', @@ -35,16 +36,25 @@ const t = ((key: string, options?: Record) => { 'monitoring.cached_tokens': 'Cached Tokens', 'monitoring.cache_read_tokens': 'Cache Read Tokens', 'monitoring.cache_creation_tokens': 'Cache Creation Tokens', + 'monitoring.realtime_usage_total_label': 'Total', + 'monitoring.realtime_usage_input_label': 'Input', + 'monitoring.realtime_usage_output_label': 'Output', + 'monitoring.realtime_usage_reasoning_label': 'Reasoning', + 'monitoring.realtime_usage_cached_label': 'Cached', + 'monitoring.realtime_usage_cache_read_label': 'Cache Read', + 'monitoring.realtime_usage_cache_creation_label': 'Cache Creation', 'monitoring.load_more_events': 'Load more', 'monitoring.log_rows': 'Rows', 'monitoring.no_more_events': 'No more events', 'monitoring.events_loaded_summary': 'Loaded {{loaded}} of {{total}} events', 'monitoring.events_all_loaded': 'All {{total}} events loaded', 'monitoring.events_retention_limited': 'Kept the newest {{loaded}} of {{total}} events', - 'monitoring.reasoning_effort': 'Effort', - 'monitoring.reasoning_effort_short': 'Effort', + 'monitoring.reasoning_service_short': 'Reasoning / Tier', + 'monitoring.realtime_reasoning_label': 'Reasoning', + 'monitoring.realtime_service_label': 'Service', 'monitoring.recent_failures': 'Failures', 'monitoring.recent_status': 'Recent', + 'monitoring.recent_status_short': 'Recent Status', 'monitoring.realtime_api_key_hash': 'API Key hash', 'monitoring.realtime_api_key_label': 'API Key', 'monitoring.realtime_api_key_masked': 'Masked key', @@ -127,7 +137,7 @@ const baseRow = (overrides: Partial = {}): PanelRow => ({ statsIncluded: true, latencyMs: 1500, ttftMs: 500, - tokensPerSecond: 20, + tokensPerSecond: 13.3, inputTokens: 10, outputTokens: 20, reasoningTokens: 3, @@ -209,26 +219,52 @@ describe('RealtimeEventsPanel', () => { }) ); - expect(markup).toContain('Effort'); + expect(markup).toContain( + `class="${styles.realtimeSettingsColumn}">Reasoning / Tier` + ); expect(markup).toContain('>TPS'); + expect(markup).toContain(styles.realtimeTpsColumn); + expect(markup).toContain(styles.realtimeLatencyColumn); + expect(markup).toContain(styles.realtimeTimeColumn); + expect(markup.match(new RegExp(styles.realtimeCenteredColumn, 'g'))).toHaveLength(8); + expect(markup.match(new RegExp(styles.realtimeSettingsColumn, 'g'))).toHaveLength(2); + expect(markup).toContain('>Recent Status'); + expect(markup).toContain('>Success Rate'); expect(markup).toContain('Source / API Key'); expect(markup).not.toContain('>Executor: codex<'); expect(markup).not.toContain('Executor: codex'); - expect(markup).toContain('medium'); - expect(markup).toContain('Requested tier: priority'); - expect(markup).toContain('Reported tier: default'); + expect(markup).toContain('>Reasoningmedium'); + expect(markup).toContain('>Servicepriority'); + expect(markup).not.toContain('default'); + expect(markup).toContain(styles.realtimeReasoningValue); + expect(markup).toContain(styles.realtimeServiceValue); expect(markup).toContain('client-gpt'); expect(markup).toContain('gpt-5.4'); expect(markup).not.toContain('Resolved'); expect(markup).not.toContain('POST /v1/chat/completions'); expect(markup).toContain('Failed'); - expect(markup).toMatch(/TTFT<\/span>|<\/span>Elapsed/); + expect(markup).toContain('>Elapsed'); + expect(markup).toContain('>TTFT13'); + expect(markup).not.toContain('>Gen'); + expect(markup).not.toContain('>E2E'); + expect(markup).toContain(styles.realtimeUsageCell); + expect(markup).toContain('>↑10'); + expect(markup).toContain('>↓20'); + expect(markup).toContain('>Total'); + expect(markup).toContain('>Input'); + expect(markup).toContain('>Output'); + expect(markup).toContain('>Reasoning'); + expect(markup).toContain('>Cached'); + expect(markup).toContain('>Cache Read'); + expect(markup).toContain('>Cache Creation'); + expect(markup).toContain(styles.realtimeUsageTooltip); + expect(markup).toContain(styles.realtimeUsageTooltipLeftBelow); expect(markup).toContain('role="tooltip"'); expect(markup).toContain(styles.realtimeFailureTooltip); expect(markup).toContain(styles.realtimeFailureTooltipBelow); @@ -284,7 +320,7 @@ describe('RealtimeEventsPanel', () => { expect(markup).toContain('Zero retention'); }); - it('renders safe response diagnostics on a successful request', () => { + it('does not render diagnostics for a successful request', () => { const markup = renderPanel( baseRow({ responseMetadata: { @@ -298,11 +334,11 @@ describe('RealtimeEventsPanel', () => { ); expect(markup).toContain('Success'); - expect(markup).toContain('API rate limit'); - expect(markup).toContain('Requests 20 / 21'); - expect(markup).toContain('Tokens 999000 / 1000000'); - expect(markup).toContain('Zero retention: No'); - expect(markup).toContain(styles.realtimeSuccessDiagnosticTooltip); + expect(markup).not.toContain('API rate limit'); + expect(markup).not.toContain('Requests 20 / 21'); + expect(markup).not.toContain('Tokens 999000 / 1000000'); + expect(markup).not.toContain('Zero retention: No'); + expect(markup).not.toContain(styles.realtimeFailureTooltip); }); it('renders safe defaults when optional usage fields are missing', () => { @@ -310,20 +346,25 @@ describe('RealtimeEventsPanel', () => { expect(markup).toContain(''); expect(markup.match(/Effort'); + expect( + markup.match(new RegExp(`class="[^"]*${styles.realtimeSettingValue}[^"]*">-`, 'g')) + ).toHaveLength(2); + expect(markup).toContain( + `class="${styles.realtimeSettingsColumn}">Reasoning / Tier` + ); expect(markup).toContain('>TPS'); expect(markup).toContain('Success'); - expect(markup).toMatch(/TTFT<\/span>|<\/span>Elapsed/); + expect(markup).toContain('>Elapsed'); + expect(markup).toContain('>TTFT10'); + expect(markup).toContain('>↓20'); + expect(markup).toContain('>Reasoning0'); + expect(markup).toContain(styles.realtimeUsageTooltip); + expect(markup).toContain('role="tooltip"'); + expect(markup).toContain('aria-describedby='); expect(markup).not.toContain('HTTP'); }); @@ -342,6 +383,7 @@ describe('RealtimeEventsPanel', () => { expect(markup).not.toContain('#12345678'); expect(markup).toContain('API Key hash: 1234567890abcdef'); expect(markup).toContain('Masked key: sk-...cdef'); + expect(markup).toMatch(/class="[^"]*realtimeApiKeyLine[^"]*" title=/); expect(markup).toContain('Executor: codex'); expect(markup).not.toContain('>Executor: codex<'); }); @@ -403,12 +445,13 @@ describe('RealtimeEventsPanel', () => { const markup = renderPanel(baseRow({ ttftMs: null })); expect(markup).toContain('>TPS'); - expect(markup).toMatch(/TTFT<\/span>|<\/span>Elapsed/); + expect(markup).toContain('>Elapsed'); expect(markup).not.toContain('500 ms'); expect(markup).toContain('1.5 s'); - expect(markup).toMatch( - /--<\/span>|<\/span>1\.5 s<\/span>/ - ); + expect(markup).toContain('>TTFT--'); + expect(markup).toContain('>Elapsed1.5 s'); }); it('keeps latency warning and error tone classes on plain text metrics', () => { @@ -422,15 +465,11 @@ describe('RealtimeEventsPanel', () => { it('colors normal millisecond and second metrics green for both ttft and elapsed time', () => { const markup = renderPanel(baseRow({ latencyMs: 470, ttftMs: 120 })); - expect(markup).toMatch( - /class="[^"]*realtimeMetricText[^"]*realtimeMetricLeft[^"]*goodText[^"]*">120 ms/ - ); - expect(markup).toMatch( - /class="[^"]*realtimeMetricText[^"]*realtimeMetricRight[^"]*goodText[^"]*">470 ms/ - ); + expect(markup).toMatch(/class="[^"]*realtimeMetricText[^"]*goodText[^"]*">120 ms/); + expect(markup).toMatch(/class="[^"]*realtimeMetricText[^"]*goodText[^"]*">470 ms/); }); - it('renders residual cached tokens even when they equal cache read tokens', () => { + it('renders each cache detail in the token usage tooltip', () => { const markup = renderPanel( baseRow({ cachedTokens: 4, @@ -439,12 +478,14 @@ describe('RealtimeEventsPanel', () => { }) ); - expect(markup).toContain('C 4'); - expect(markup).toContain('CR 4'); - expect(markup).toContain('CW 1'); + expect(markup).toContain('>CachedCache ReadCache Creation4'); + expect(markup).toContain('>1'); }); - it('hides zero legacy cache while showing GPT-5.6 cache read and write metrics', () => { + it('shows zero legacy cache alongside GPT-5.6 cache read and write metrics', () => { const markup = renderPanel( baseRow({ model: 'gpt-5.6-sol', @@ -455,9 +496,12 @@ describe('RealtimeEventsPanel', () => { }) ); - expect(markup).not.toContain('C 0'); - expect(markup).toContain('CR 151.0K · CW 1.0K'); - expect(markup).toContain('aria-label="Cache Read Tokens: 151.0K, Cache Creation Tokens: 1.0K"'); + expect(markup).toContain('>CachedCache ReadCache Creation151.0K'); + expect(markup).toContain('>1.0K'); + expect(markup).toContain('aria-label="Total: 33, Input: 152.6K, Output: 20, Reasoning: 3, Cached: 0, Cache Read: 151.0K, Cache Creation: 1.0K"'); expect(markup).toContain('tabindex="0"'); }); diff --git a/apps/web/src/features/monitoring/components/RealtimeEventsPanel.tsx b/apps/web/src/features/monitoring/components/RealtimeEventsPanel.tsx index 03fca98b3..5823d0ed0 100644 --- a/apps/web/src/features/monitoring/components/RealtimeEventsPanel.tsx +++ b/apps/web/src/features/monitoring/components/RealtimeEventsPanel.tsx @@ -84,6 +84,8 @@ const FAILURE_TOOLTIP_OFFSET = 8; const FAILURE_TOOLTIP_MAX_WIDTH = 420; const FAILURE_TOOLTIP_MAX_HEIGHT = 240; const FAILURE_TOOLTIP_CLOSE_DELAY_MS = 120; +const USAGE_TOOLTIP_WIDTH = 184; +const USAGE_TOOLTIP_ESTIMATED_HEIGHT = 170; type FailureTooltipPlacement = 'above' | 'below'; @@ -92,6 +94,13 @@ type FailureTooltipPosition = { style: CSSProperties; }; +type UsageTooltipPlacement = 'left-above' | 'left-below'; + +type UsageTooltipPosition = { + placement: UsageTooltipPlacement; + style: CSSProperties; +}; + const formatOptionalText = (value: string | null | undefined) => { const trimmed = String(value || '').trim(); return trimmed || '-'; @@ -172,6 +181,53 @@ const resolveFailureTooltipPosition = (anchor: HTMLElement): FailureTooltipPosit }; }; +const resolveUsageTooltipPosition = (anchor: HTMLElement): UsageTooltipPosition | null => { + if (typeof window === 'undefined') return null; + + const rect = anchor.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + const maxWidth = Math.max(0, viewportWidth - FAILURE_TOOLTIP_VIEWPORT_MARGIN * 2); + const availableLeftWidth = Math.max( + 0, + rect.left - FAILURE_TOOLTIP_VIEWPORT_MARGIN - FAILURE_TOOLTIP_OFFSET + ); + const width = Math.min(USAGE_TOOLTIP_WIDTH, maxWidth, availableLeftWidth); + const spaceBelow = + viewportHeight - rect.bottom - FAILURE_TOOLTIP_VIEWPORT_MARGIN - FAILURE_TOOLTIP_OFFSET; + const spaceAbove = rect.top - FAILURE_TOOLTIP_VIEWPORT_MARGIN - FAILURE_TOOLTIP_OFFSET; + const placement: UsageTooltipPlacement = + spaceAbove >= USAGE_TOOLTIP_ESTIMATED_HEIGHT || spaceAbove >= spaceBelow + ? 'left-above' + : 'left-below'; + const availableHeight = Math.max(0, placement === 'left-above' ? spaceAbove : spaceBelow); + const left = Math.max( + FAILURE_TOOLTIP_VIEWPORT_MARGIN, + rect.left - FAILURE_TOOLTIP_OFFSET - width + ); + const baseStyle: CSSProperties = { + left, + width, + maxHeight: Math.min(USAGE_TOOLTIP_ESTIMATED_HEIGHT, availableHeight), + }; + + return placement === 'left-below' + ? { + placement, + style: { + ...baseStyle, + top: rect.bottom + FAILURE_TOOLTIP_OFFSET, + }, + } + : { + placement, + style: { + ...baseStyle, + bottom: viewportHeight - rect.top + FAILURE_TOOLTIP_OFFSET, + }, + }; +}; + const buildRealtimeApiKeyDisplay = (row: MonitoringEventRow, t: TFunction) => { const label = formatReadableText(row.apiKeyLabel); const masked = formatReadableText(row.apiKeyMasked); @@ -456,14 +512,14 @@ const buildRequestDiagnosticMetaText = (row: MonitoringEventRow, t: TFunction, l }; const buildRequestDiagnosticDetails = (row: MonitoringEventRow, t: TFunction, locale: string) => { - const summary = row.failed ? maskSensitiveText(row.failSummary || '') : ''; + if (!row.failed) return null; + + const summary = maskSensitiveText(row.failSummary || ''); const diagnostics = buildHeaderDiagnosticParts(row, t, locale); - if (!row.failed && diagnostics.length === 0) return null; - if (row.failed && !row.failStatusCode && !summary && diagnostics.length === 0) return null; - const statusText = - row.failed && row.failStatusCode - ? `${shortLabel(t, 'monitoring.fail_status_code_short', 'monitoring.fail_status_code')} ${row.failStatusCode}` - : t(row.failed ? 'monitoring.result_failed' : 'monitoring.result_success'); + if (!row.failStatusCode && !summary && diagnostics.length === 0) return null; + const statusText = row.failStatusCode + ? `${shortLabel(t, 'monitoring.fail_status_code_short', 'monitoring.fail_status_code')} ${row.failStatusCode}` + : t('monitoring.result_failed'); return { failed: row.failed, statusCode: row.failStatusCode, @@ -594,7 +650,6 @@ function RealtimeRequestDiagnosticStatus({ const placement = tooltipPosition?.placement ?? 'below'; const tooltipClassName = [ styles.realtimeFailureTooltip, - !details.failed ? styles.realtimeSuccessDiagnosticTooltip : '', placement === 'above' ? styles.realtimeFailureTooltipAbove : styles.realtimeFailureTooltipBelow, open ? styles.realtimeFailureTooltipOpen : '', ] @@ -663,40 +718,201 @@ function RealtimeRequestDiagnosticStatus({ ); } -const buildRealtimeTokenSummary = (row: MonitoringEventRow, t: TFunction) => { - const primary = [ - `I ${formatCompactNumber(row.inputTokens)}`, - `O ${formatCompactNumber(row.outputTokens)}`, +type RealtimeTokenUsageDetails = { + total: string; + input: string; + output: string; + fields: Array<{ label: string; value: string }>; + ariaLabel: string; +}; + +const buildRealtimeTokenUsageDetails = (row: MonitoringEventRow, t: TFunction) => { + const fields = [ + { label: t('monitoring.realtime_usage_total_label'), value: formatCompactNumber(row.totalTokens) }, + { label: t('monitoring.realtime_usage_input_label'), value: formatCompactNumber(row.inputTokens) }, + { label: t('monitoring.realtime_usage_output_label'), value: formatCompactNumber(row.outputTokens) }, + { + label: t('monitoring.realtime_usage_reasoning_label'), + value: formatCompactNumber(row.reasoningTokens), + }, + { label: t('monitoring.realtime_usage_cached_label'), value: formatCompactNumber(row.cachedTokens) }, + { + label: t('monitoring.realtime_usage_cache_read_label'), + value: formatCompactNumber(row.cacheReadTokens), + }, + { + label: t('monitoring.realtime_usage_cache_creation_label'), + value: formatCompactNumber(row.cacheCreationTokens), + }, ]; - if (row.reasoningTokens > 0) { - primary.push(`R ${formatCompactNumber(row.reasoningTokens)}`); - } - const cache: string[] = []; - const cacheTitle: string[] = []; - if (row.cachedTokens > 0) { - cache.push(`C ${formatCompactNumber(row.cachedTokens)}`); - cacheTitle.push(`${t('monitoring.cached_tokens')}: ${formatCompactNumber(row.cachedTokens)}`); - } - if (row.cacheReadTokens > 0) { - cache.push(`CR ${formatCompactNumber(row.cacheReadTokens)}`); - cacheTitle.push( - `${t('monitoring.cache_read_tokens')}: ${formatCompactNumber(row.cacheReadTokens)}` - ); - } - if (row.cacheCreationTokens > 0) { - cache.push(`CW ${formatCompactNumber(row.cacheCreationTokens)}`); - cacheTitle.push( - `${t('monitoring.cache_creation_tokens')}: ${formatCompactNumber(row.cacheCreationTokens)}` - ); - } + return { - primary: primary.join(' · '), - cache: cache.join(' · '), - cacheTitle: cacheTitle.join('\n'), - cacheAriaLabel: cacheTitle.join(', '), - }; + total: fields[0].value, + input: fields[1].value, + output: fields[2].value, + fields, + ariaLabel: fields.map((field) => `${field.label}: ${field.value}`).join(', '), + } satisfies RealtimeTokenUsageDetails; +}; + +type RealtimeTokenUsageProps = { + details: RealtimeTokenUsageDetails; + tooltipId: string; }; +function RealtimeTokenUsage({ details, tooltipId }: RealtimeTokenUsageProps) { + const triggerRef = useRef(null); + const tooltipRef = useRef(null); + const closeTimerRef = useRef(null); + const rafRef = useRef(null); + const [open, setOpen] = useState(false); + const [tooltipPosition, setTooltipPosition] = useState(null); + const isBrowser = typeof document !== 'undefined'; + + const clearCloseTimer = useCallback(() => { + if (closeTimerRef.current === null || typeof window === 'undefined') return; + window.clearTimeout(closeTimerRef.current); + closeTimerRef.current = null; + }, []); + + const updateTooltipPosition = useCallback(() => { + if (!triggerRef.current) return; + const nextPosition = resolveUsageTooltipPosition(triggerRef.current); + if (nextPosition) { + setTooltipPosition(nextPosition); + } + }, []); + + const scheduleTooltipPositionUpdate = useCallback(() => { + if (typeof window === 'undefined') return; + if (rafRef.current !== null) { + window.cancelAnimationFrame(rafRef.current); + } + rafRef.current = window.requestAnimationFrame(() => { + rafRef.current = null; + updateTooltipPosition(); + }); + }, [updateTooltipPosition]); + + const showTooltip = useCallback(() => { + clearCloseTimer(); + updateTooltipPosition(); + setOpen(true); + }, [clearCloseTimer, updateTooltipPosition]); + + const requestHideTooltip = useCallback(() => { + clearCloseTimer(); + if (typeof window === 'undefined') { + setOpen(false); + return; + } + closeTimerRef.current = window.setTimeout(() => { + closeTimerRef.current = null; + setOpen(false); + }, FAILURE_TOOLTIP_CLOSE_DELAY_MS); + }, [clearCloseTimer]); + + const handleBlur = useCallback( + (event: FocusEvent) => { + const nextTarget = event.relatedTarget; + if ( + isNodeInside(triggerRef.current, nextTarget) || + isNodeInside(tooltipRef.current, nextTarget) + ) { + return; + } + requestHideTooltip(); + }, + [requestHideTooltip] + ); + + const handleKeyDown = useCallback((event: KeyboardEvent) => { + if (event.key !== 'Escape') return; + event.preventDefault(); + setOpen(false); + }, []); + + useEffect(() => { + return () => { + clearCloseTimer(); + if (rafRef.current !== null && typeof window !== 'undefined') { + window.cancelAnimationFrame(rafRef.current); + rafRef.current = null; + } + }; + }, [clearCloseTimer]); + + useEffect(() => { + if (!open || typeof window === 'undefined') return undefined; + + scheduleTooltipPositionUpdate(); + window.addEventListener('resize', scheduleTooltipPositionUpdate); + window.addEventListener('scroll', scheduleTooltipPositionUpdate, true); + + return () => { + window.removeEventListener('resize', scheduleTooltipPositionUpdate); + window.removeEventListener('scroll', scheduleTooltipPositionUpdate, true); + }; + }, [open, scheduleTooltipPositionUpdate]); + + const placement = tooltipPosition?.placement ?? 'left-below'; + const tooltip = ( + + ); + + return ( +
+ {details.total} + + + + {details.input} + + + + {details.output} + + + {!isBrowser ? tooltip : null} + {isBrowser && open ? createPortal(tooltip, document.body) : null} +
+ ); +} + export function RealtimeEventsPanelActions({ rowCount, scopedFailureCount, @@ -799,11 +1015,7 @@ export function RealtimeEventsPanel({ 'monitoring.column_source_api_key_short', 'monitoring.column_source_api_key' ); - const reasoningEffortLabel = shortLabel( - t, - 'monitoring.reasoning_effort_short', - 'monitoring.reasoning_effort' - ); + const reasoningServiceLabel = t('monitoring.reasoning_service_short'); const recentStatusLabel = shortLabel( t, 'monitoring.recent_status_short', @@ -871,22 +1083,14 @@ export function RealtimeEventsPanel({ {sourceApiKeyLabel} {t('monitoring.column_model')} - {reasoningEffortLabel} - {recentStatusLabel} - {requestStatusLabel} - {successRateLabel} - {totalCallsLabel} + {reasoningServiceLabel} + {recentStatusLabel} + {requestStatusLabel} + {successRateLabel} + {totalCallsLabel} {t('monitoring.column_output_tps')} - - - {t('monitoring.ttft_short')} - - - {t('monitoring.elapsed_short')} - - - - {t('monitoring.column_time')} + {t('monitoring.elapsed_short')} + {t('monitoring.column_time')} {usageLabel} {costLabel} @@ -903,6 +1107,12 @@ export function RealtimeEventsPanel({ const serviceTier = formatOptionalText(row.serviceTier); const requestServiceTier = formatOptionalText(row.requestServiceTier); const responseServiceTier = formatOptionalText(row.responseServiceTier); + const effectiveServiceTier = + requestServiceTier !== '-' + ? requestServiceTier + : serviceTier !== '-' + ? serviceTier + : responseServiceTier; const requestDiagnosticDetails = buildRequestDiagnosticDetails(row, t, locale); const requestDiagnosticTooltipId = requestDiagnosticDetails ? `${tooltipIdPrefix}-request-diagnostic-tooltip-${row.id}` @@ -911,7 +1121,7 @@ export function RealtimeEventsPanel({ const hasTtftMs = row.ttftMs !== null && row.ttftMs !== undefined; const ttftToneClass = getRealtimeDurationToneClass(row.ttftMs); const latencyToneClass = getRealtimeDurationToneClass(row.latencyMs); - const tokenSummary = buildRealtimeTokenSummary(row, t); + const tokenUsage = buildRealtimeTokenUsageDetails(row, t); return ( @@ -944,29 +1154,36 @@ export function RealtimeEventsPanel({ ) : null} - -
- {reasoningEffort !== '-' ? ( - {reasoningEffort} - ) : ( - - - )} - {requestServiceTier !== '-' ? ( - {`${t('monitoring.request_service_tier_short')}: ${requestServiceTier}`} - ) : serviceTier !== '-' ? ( - {`${shortLabel(t, 'monitoring.service_tier_short', 'monitoring.service_tier')}: ${serviceTier}`} - ) : null} - {responseServiceTier !== '-' ? ( - {`${t('monitoring.response_service_tier_short')}: ${responseServiceTier}`} - ) : null} + +
+ + + {t('monitoring.realtime_reasoning_label')} + + + {reasoningEffort} + + + + + {t('monitoring.realtime_service_label')} + + + {effectiveServiceTier} + +
- +
- +
{requestDiagnosticDetails ? ( = 0.95 ? styles.goodText : row.successRate >= 0.85 ? styles.warnText - : styles.badText - } + : styles.badText, + ].join(' ')} > {formatPercent(row.successRate)} - {formatCompactNumber(row.requestCount)} + + {formatCompactNumber(row.requestCount)} + {formatTokensPerSecond(row.tokensPerSecond, locale)} @@ -1015,52 +1235,43 @@ export function RealtimeEventsPanel({
- - {hasTtftMs ? formatRealtimeCompactDuration(row.ttftMs, locale) : '--'} + + + {t('monitoring.ttft_short')} + + + {hasTtftMs ? formatRealtimeCompactDuration(row.ttftMs, locale) : '--'} + - - - {formatRealtimeCompactDuration(row.latencyMs, locale)} + + + {t('monitoring.elapsed_short')} + + + {formatRealtimeCompactDuration(row.latencyMs, locale)} +
- +
{timeParts.date} {timeParts.time}
-
- {formatCompactNumber(row.totalTokens)} - {tokenSummary.primary} - {tokenSummary.cache ? ( - - {tokenSummary.cache} - - ) : null} -
+ {hasPrices ? formatUsd(row.totalCost) : '--'} diff --git a/apps/web/src/features/monitoring/model/eventRows.test.ts b/apps/web/src/features/monitoring/model/eventRows.test.ts index cc391aab8..d48ef157b 100644 --- a/apps/web/src/features/monitoring/model/eventRows.test.ts +++ b/apps/web/src/features/monitoring/model/eventRows.test.ts @@ -39,7 +39,7 @@ const buildRows = ( ); describe('buildEventRows', () => { - it('calculates output tokens per second from total latency', () => { + it('calculates tokens per second from total latency', () => { const [row] = buildRows(); expect(row.latencyMs).toBe(1500); @@ -47,7 +47,7 @@ describe('buildEventRows', () => { expect(row.tokensPerSecond).toBeCloseTo(20 / 1.5); }); - it('does not let TTFT change output tokens per second', () => { + it('does not use TTFT to calculate tokens per second', () => { const [withoutTTFT] = buildRows({ ttft_ms: undefined }); const [smallTTFT] = buildRows({ ttft_ms: 100 }); const [invalidTTFT] = buildRows({ ttft_ms: 2000 }); @@ -57,7 +57,7 @@ describe('buildEventRows', () => { expect(invalidTTFT.tokensPerSecond).toBeCloseTo(20 / 1.5); }); - it('does not calculate tokens per second without output tokens or total latency', () => { + it('does not calculate TPS without output tokens or total latency', () => { const [noOutput] = buildRows({ tokens: { output_tokens: 0 } }); const [noLatency] = buildRows({ latency_ms: undefined }); const [zeroLatency] = buildRows({ latency_ms: 0 }); diff --git a/apps/web/src/features/monitoring/styles/_monitoring-summary-table.scss b/apps/web/src/features/monitoring/styles/_monitoring-summary-table.scss index 92eb09bee..2e0018b06 100644 --- a/apps/web/src/features/monitoring/styles/_monitoring-summary-table.scss +++ b/apps/web/src/features/monitoring/styles/_monitoring-summary-table.scss @@ -1122,7 +1122,7 @@ .realtimeFailureTooltip { --monitor-surface: var(--bg-primary); - --monitor-surface-elevated: color-mix(in srgb, var(--bg-primary) 82%, var(--surface-subtle)); + --monitor-surface-elevated: var(--app-surface-strong); --monitor-surface-hover: color-mix(in srgb, var(--surface-subtle-hover) 74%, var(--bg-primary)); --monitor-ink: var(--text-primary); --monitor-muted: var(--text-secondary); @@ -1161,10 +1161,6 @@ user-select: text; } - .realtimeSuccessDiagnosticTooltip { - --monitor-diagnostic-color: var(--monitor-green); - } - .realtimeFailureTooltip::before { content: ''; position: absolute; diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json index 2ffb46ed7..77856cf0d 100644 --- a/apps/web/src/i18n/locales/en.json +++ b/apps/web/src/i18n/locales/en.json @@ -1812,8 +1812,18 @@ "cache_creation_tokens": "Cache Creation Tokens", "cache_read_tokens_short": "Read", "cache_creation_tokens_short": "Create", + "realtime_usage_total_label": "Total", + "realtime_usage_input_label": "Input", + "realtime_usage_output_label": "Output", + "realtime_usage_reasoning_label": "Reasoning", + "realtime_usage_cached_label": "Cached", + "realtime_usage_cache_read_label": "Cache Read", + "realtime_usage_cache_creation_label": "Cache Creation", "reasoning_effort": "Reasoning Effort", "reasoning_effort_short": "Effort", + "reasoning_service_short": "Reasoning / Tier", + "realtime_reasoning_label": "Reasoning", + "realtime_service_label": "Service", "service_tier": "Service Tier", "service_tier_short": "Tier", "request_service_tier_short": "Requested tier", @@ -2445,7 +2455,7 @@ "request_status": "Request Status", "request_status_short": "Status", "recent_status": "Recent Status", - "recent_status_short": "Recent", + "recent_status_short": "Recent Status", "latest_request_time": "Latest Request", "latest_request_time_short": "Latest", "column_time": "Time", @@ -2461,7 +2471,7 @@ "column_auths": "Creds", "column_models": "Models", "column_success_rate": "Success", - "column_success_rate_short": "Success", + "column_success_rate_short": "Success Rate", "column_tokens": "Tokens", "column_cost": "Cost", "column_latency": "Latency", diff --git a/apps/web/src/i18n/locales/ru.json b/apps/web/src/i18n/locales/ru.json index e329dc15a..8e9ae4f97 100644 --- a/apps/web/src/i18n/locales/ru.json +++ b/apps/web/src/i18n/locales/ru.json @@ -1812,8 +1812,18 @@ "cache_creation_tokens": "Токены создания кэша", "cache_read_tokens_short": "Чтение", "cache_creation_tokens_short": "Создание", + "realtime_usage_total_label": "Всего", + "realtime_usage_input_label": "Ввод", + "realtime_usage_output_label": "Вывод", + "realtime_usage_reasoning_label": "Рассуждение", + "realtime_usage_cached_label": "Кэш", + "realtime_usage_cache_read_label": "Чтение кэша", + "realtime_usage_cache_creation_label": "Создание кэша", "reasoning_effort": "Уровень рассуждения", "reasoning_effort_short": "Уровень", + "reasoning_service_short": "Мышление / сервис", + "realtime_reasoning_label": "Мышление", + "realtime_service_label": "Сервис", "service_tier": "Уровень сервиса", "service_tier_short": "Сервис", "request_service_tier_short": "Запрошенный уровень", @@ -2445,7 +2455,7 @@ "request_status": "Статус запроса", "request_status_short": "Статус", "recent_status": "Последний статус", - "recent_status_short": "Недавн.", + "recent_status_short": "Недавн. статус", "latest_request_time": "Последний запрос", "latest_request_time_short": "Посл.", "column_time": "Время", @@ -2461,7 +2471,7 @@ "column_auths": "Creds", "column_models": "Модели", "column_success_rate": "Успех", - "column_success_rate_short": "Успех", + "column_success_rate_short": "Успех %", "column_tokens": "Токены", "column_cost": "Стоимость", "column_latency": "Задержка", diff --git a/apps/web/src/i18n/locales/zh-CN.json b/apps/web/src/i18n/locales/zh-CN.json index d904a3317..f73387cd6 100644 --- a/apps/web/src/i18n/locales/zh-CN.json +++ b/apps/web/src/i18n/locales/zh-CN.json @@ -1812,8 +1812,18 @@ "cache_creation_tokens": "缓存创建 Tokens", "cache_read_tokens_short": "读取", "cache_creation_tokens_short": "创建", + "realtime_usage_total_label": "总量", + "realtime_usage_input_label": "输入", + "realtime_usage_output_label": "输出", + "realtime_usage_reasoning_label": "推理", + "realtime_usage_cached_label": "缓存", + "realtime_usage_cache_read_label": "缓存读取", + "realtime_usage_cache_creation_label": "缓存创建", "reasoning_effort": "思考强度", "reasoning_effort_short": "强度", + "reasoning_service_short": "推理/服务", + "realtime_reasoning_label": "思考", + "realtime_service_label": "服务", "service_tier": "服务等级", "service_tier_short": "等级", "request_service_tier_short": "请求等级", @@ -2445,7 +2455,7 @@ "request_status": "请求状态", "request_status_short": "状态", "recent_status": "最近状态", - "recent_status_short": "状态", + "recent_status_short": "最近状态", "latest_request_time": "最近请求时间", "latest_request_time_short": "最新", "column_time": "时间", @@ -2461,7 +2471,7 @@ "column_auths": "凭证数", "column_models": "模型数", "column_success_rate": "成功率", - "column_success_rate_short": "成功", + "column_success_rate_short": "成功率", "column_tokens": "Tokens", "column_cost": "花费", "column_latency": "延迟", diff --git a/apps/web/src/i18n/locales/zh-TW.json b/apps/web/src/i18n/locales/zh-TW.json index d7217689f..866ddd664 100644 --- a/apps/web/src/i18n/locales/zh-TW.json +++ b/apps/web/src/i18n/locales/zh-TW.json @@ -1812,8 +1812,18 @@ "cache_creation_tokens": "快取建立 Tokens", "cache_read_tokens_short": "讀取", "cache_creation_tokens_short": "建立", + "realtime_usage_total_label": "總量", + "realtime_usage_input_label": "輸入", + "realtime_usage_output_label": "輸出", + "realtime_usage_reasoning_label": "推理", + "realtime_usage_cached_label": "快取", + "realtime_usage_cache_read_label": "快取讀取", + "realtime_usage_cache_creation_label": "快取建立", "reasoning_effort": "思考強度", "reasoning_effort_short": "強度", + "reasoning_service_short": "推理/服務", + "realtime_reasoning_label": "思考", + "realtime_service_label": "服務", "service_tier": "服務等級", "service_tier_short": "等級", "request_service_tier_short": "請求等級", @@ -2445,7 +2455,7 @@ "request_status": "請求狀態", "request_status_short": "狀態", "recent_status": "最近狀態", - "recent_status_short": "狀態", + "recent_status_short": "最近狀態", "latest_request_time": "最近請求時間", "latest_request_time_short": "最新", "column_time": "時間", @@ -2461,7 +2471,7 @@ "column_auths": "憑證數", "column_models": "模型數", "column_success_rate": "成功率", - "column_success_rate_short": "成功", + "column_success_rate_short": "成功率", "column_tokens": "Tokens", "column_cost": "花費", "column_latency": "延遲",