-
- Source:{' '}
-
- SemiAnalysis Market August 2025 Pricing Surveys & AI Cloud TCO Model
-
-
-
-
- >
- )}
-
-
- Note: Disaggregated inference configurations (e.g., MoRI
- SGLang, Dynamo TRT) calculate cost per decode GPU or per prefill GPU,
- rather than per total GPU count. This makes direct cost comparison with
- aggregated configs not an apples-to-apples comparison.
-
-
-
-
- Note: Disaggregated inference configurations (e.g., MoRI
- SGLang, Dynamo TRT) calculate cost per decode GPU or per prefill GPU,
- rather than per total GPU count. This makes direct cost comparison with
- aggregated configs not an apples-to-apples comparison.
-
-
-
-
- Note: Disaggregated inference configurations (e.g., MoRI
- SGLang, Dynamo TRT) calculate power per decode GPU or per prefill GPU,
- rather than per total GPU count. This makes direct power comparison with
- aggregated configs not an apples-to-apples comparison.
-
- All in Power/GPU:{' '}
- {Object.entries(GPU_SPECS).map(([base, specs]) => (
-
- {base.toUpperCase()}: {specs.power}kW
-
- ))}
-
-
-
- Source:{' '}
-
- SemiAnalysis Datacenter Industry Model
-
-
-
-
- >
- )}
-
-
- Note: Disaggregated inference configurations (e.g., MoRI
- SGLang, Dynamo TRT) calculate Joules per decode GPU or per prefill GPU,
- rather than per total GPU count. This makes direct Joules per token
- comparison with aggregated configs not an apples-to-apples comparison.
-
-
+
-
-
-
-
-
-
-
+
diff --git a/packages/app/src/components/reliability/ui/ChartDisplay.tsx b/packages/app/src/components/reliability/ui/ChartDisplay.tsx
index 92af6c2..514eca0 100644
--- a/packages/app/src/components/reliability/ui/ChartDisplay.tsx
+++ b/packages/app/src/components/reliability/ui/ChartDisplay.tsx
@@ -4,9 +4,8 @@ import { useCallback } from 'react';
import { useReliabilityContext } from '@/components/reliability/ReliabilityContext';
import { Card } from '@/components/ui/card';
+import { ChartShareActions } from '@/components/ui/chart-display-helpers';
import { ChartSection } from '@/components/ui/chart-section';
-import { ShareButton } from '@/components/ui/share-button';
-import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons';
import { exportToCsv } from '@/lib/csv-export';
import { reliabilityChartToCsv } from '@/lib/csv-export-helpers';
@@ -35,13 +34,7 @@ export default function ReliabilityChartDisplay() {
reliability for inference runs over time.
-
-
-
-
-
-
-
+
diff --git a/packages/app/src/components/trends/HistoricalTrendsDisplay.tsx b/packages/app/src/components/trends/HistoricalTrendsDisplay.tsx
index 4b1f220..95d194d 100644
--- a/packages/app/src/components/trends/HistoricalTrendsDisplay.tsx
+++ b/packages/app/src/components/trends/HistoricalTrendsDisplay.tsx
@@ -1,7 +1,6 @@
'use client';
import { track } from '@/lib/analytics';
-import Link from 'next/link';
import React, { useCallback, useMemo, useState } from 'react';
import { useInference } from '@/components/inference/InferenceContext';
@@ -9,20 +8,17 @@ import { useInterpolatedTrendData } from '@/components/inference/hooks/useInterp
import { TrendLineConfig } from '@/components/inference/types';
import ChartControls from '@/components/inference/ui/ChartControls';
import TrendChart from '@/components/inference/ui/TrendChart';
-import { Badge } from '@/components/ui/badge';
import { Card } from '@/components/ui/card';
import { ChartButtons } from '@/components/ui/chart-buttons';
+import { ChartShareActions, MetricAssumptionNotes } from '@/components/ui/chart-display-helpers';
import { exportToCsv } from '@/lib/csv-export';
import { historicalTrendToCsv } from '@/lib/csv-export-helpers';
import ChartLegend from '@/components/ui/chart-legend';
-import { ExternalLinkIcon } from '@/components/ui/external-link-icon';
import { Input } from '@/components/ui/input';
import { LabelWithTooltip } from '@/components/ui/label-with-tooltip';
-import { ShareButton } from '@/components/ui/share-button';
-import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons';
import { TooltipProvider } from '@/components/ui/tooltip';
import { Skeleton } from '@/components/ui/skeleton';
-import { getModelSortIndex, GPU_SPECS } from '@/lib/constants';
+import { getModelSortIndex } from '@/lib/constants';
import {
getModelLabel,
getPrecisionLabel,
@@ -199,13 +195,7 @@ export default function HistoricalTrendsDisplay() {
Interpolated performance metrics over time at a fixed interactivity operating point.
-
-
- Source:{' '}
-
- SemiAnalysis Market August 2025 Pricing Surveys & AI Cloud TCO Model
-
-
-
-
- >
- )}
-
-
- Note: Disaggregated inference configurations (e.g., MoRI
- SGLang, Dynamo TRT) calculate cost per decode GPU or per prefill GPU, rather
- than per total GPU count. This makes direct cost comparison with aggregated
- configs not an apples-to-apples comparison.
-
-
-
-
- Note: Disaggregated inference configurations (e.g., MoRI
- SGLang, Dynamo TRT) calculate cost per decode GPU or per prefill GPU, rather
- than per total GPU count. This makes direct cost comparison with aggregated
- configs not an apples-to-apples comparison.
-
- All in Power/GPU:{' '}
- {Object.entries(GPU_SPECS).map(([base, specs]) => (
-
- {base.toUpperCase()}: {specs.power}kW
-
- ))}
-
-
-
- Source:{' '}
-
- SemiAnalysis Datacenter Industry Model
-
-
-
-
- >
- )}
-
-
- Note: Disaggregated inference configurations (e.g., MoRI
- SGLang, Dynamo TRT) calculate Joules per decode GPU or per prefill GPU,
- rather than per total GPU count. This makes direct Joules per token
- comparison with aggregated configs not an apples-to-apples comparison.
-
-
+
root.render(ui));
+}
+
+function getVisibleText() {
+ return container.textContent ?? '';
+}
+
+function getVisibleCaveatText() {
+ return Array.from(container.querySelectorAll('div.max-h-20 p'))
+ .map((element) => element.textContent ?? '')
+ .join(' ');
+}
+
+beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ root = createRoot(container);
+});
+
+afterEach(() => {
+ act(() => root.unmount());
+ container.remove();
+});
+
+describe('ChartShareActions', () => {
+ it('renders the shared copy-link, X, and LinkedIn buttons with stable test ids', () => {
+ renderUi();
+
+ expect(container.querySelector('[data-testid="share-button"]')).not.toBeNull();
+ expect(container.querySelector('[data-testid="share-twitter"]')).not.toBeNull();
+ expect(container.querySelector('[data-testid="share-linkedin"]')).not.toBeNull();
+ });
+});
+
+describe('MetricAssumptionNotes', () => {
+ it('shows power source badges and the per-MW disaggregation caveat for inference metrics', () => {
+ renderUi();
+
+ expect(getVisibleText()).toContain('All in Power/GPU:');
+ expect(getVisibleText()).toContain('SemiAnalysis Datacenter Industry Model');
+ expect(getVisibleCaveatText()).toContain('calculate power per decode GPU or per prefill GPU');
+ });
+
+ it('preserves historical-trends semantics when both compatibility flags are disabled', () => {
+ renderUi(
+ ,
+ );
+
+ expect(getVisibleText()).not.toContain('SemiAnalysis Datacenter Industry Model');
+ expect(getVisibleCaveatText()).not.toContain(
+ 'calculate power per decode GPU or per prefill GPU',
+ );
+
+ renderUi(
+ ,
+ );
+
+ expect(getVisibleText()).toContain('SemiAnalysis Datacenter Industry Model');
+ expect(getVisibleCaveatText()).not.toContain(
+ 'calculate power per decode GPU or per prefill GPU',
+ );
+ });
+
+ it('renders TCO notes, source attribution, and the cost disaggregation caveat', () => {
+ renderUi();
+
+ expect(getVisibleText()).toContain('TCO $/GPU/hr:');
+ expect(getVisibleText()).toContain(
+ 'SemiAnalysis Market August 2025 Pricing Surveys & AI Cloud TCO Model',
+ );
+ expect(getVisibleCaveatText()).toContain('calculate cost per decode GPU or per prefill GPU');
+ });
+
+ it('renders metric-specific throughput caveats and preserves Joules wording semantics', () => {
+ renderUi();
+
+ expect(getVisibleCaveatText()).toContain(
+ 'calculate input throughput per decode GPU or per prefill GPU',
+ );
+ expect(getVisibleCaveatText()).toContain('direct input throughput comparison');
+
+ renderUi();
+
+ expect(getVisibleText()).toContain('SemiAnalysis Datacenter Industry Model');
+ expect(getVisibleCaveatText()).toContain('calculate Joules per decode GPU or per prefill GPU');
+ expect(getVisibleCaveatText()).toContain('direct Joules per token comparison');
+ });
+});
diff --git a/packages/app/src/components/ui/chart-display-helpers.tsx b/packages/app/src/components/ui/chart-display-helpers.tsx
new file mode 100644
index 0000000..f08605b
--- /dev/null
+++ b/packages/app/src/components/ui/chart-display-helpers.tsx
@@ -0,0 +1,177 @@
+'use client';
+
+import Link from 'next/link';
+import type { ReactNode } from 'react';
+
+import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons';
+import { Badge } from '@/components/ui/badge';
+import { ExternalLinkIcon } from '@/components/ui/external-link-icon';
+import { ShareButton } from '@/components/ui/share-button';
+import { GPU_SPECS } from '@/lib/constants';
+
+// Keep these metric-key groups in sync with chart-utils/chart configs when new source-backed
+// metrics are added; this helper owns which caption notes and caveats appear for each family.
+const POWER_SOURCE_METRICS = new Set(['y_tpPerMw', 'y_inputTputPerMw', 'y_outputTputPerMw']);
+const TOTAL_COST_METRICS = new Set(['y_costh', 'y_costn', 'y_costr']);
+const OUTPUT_COST_METRICS = new Set(['y_costhOutput', 'y_costnOutput', 'y_costrOutput']);
+const INPUT_COST_METRICS = new Set(['y_costhi', 'y_costni', 'y_costri']);
+const POWER_VALUES = Object.fromEntries(
+ Object.entries(GPU_SPECS).map(([base, specs]) => [base, `${specs.power}kW`]),
+);
+
+function MetricBadges({
+ label,
+ values,
+}: {
+ label: string;
+ values: Record;
+}) {
+ return (
+
+ Note: Disaggregated inference configurations (e.g., MoRI SGLang, Dynamo
+ TRT) calculate {calculationNoun} per decode GPU or per prefill GPU, rather than per total
+ GPU count. This makes direct {comparisonNoun} comparison with aggregated configs not an
+ apples-to-apples comparison.
+