diff --git a/packages/app/src/components/inference/ui/ChartControls.tsx b/packages/app/src/components/inference/ui/ChartControls.tsx index d322202..48d3f6f 100644 --- a/packages/app/src/components/inference/ui/ChartControls.tsx +++ b/packages/app/src/components/inference/ui/ChartControls.tsx @@ -10,7 +10,6 @@ import { } from '@/components/ui/chart-selectors'; import { DateRangePicker } from '@/components/ui/date-range-picker'; import { LabelWithTooltip } from '@/components/ui/label-with-tooltip'; -import { MultiDatePicker } from '@/components/ui/multi-date-picker'; import { MultiSelect } from '@/components/ui/multi-select'; import { Select, @@ -76,14 +75,9 @@ const GROUPED_Y_AXIS_OPTIONS = METRIC_GROUPS.map((group) => ({ interface ChartControlsProps { /** Hide GPU Config selector and related date pickers (used by Historical Trends tab) */ hideGpuComparison?: boolean; - /** Intermediate dates within the comparison range that have changelog entries */ - intermediateDates?: string[]; } -export default function ChartControls({ - hideGpuComparison = false, - intermediateDates = [], -}: ChartControlsProps) { +export default function ChartControls({ hideGpuComparison = false }: ChartControlsProps) { const { selectedModel, setSelectedModel, @@ -99,8 +93,6 @@ export default function ChartControls({ availableGPUs, selectedDateRange, setSelectedDateRange, - selectedDates, - setSelectedDates, dateRangeAvailableDates, isCheckingAvailableDates, availablePrecisions, @@ -345,32 +337,6 @@ export default function ChartControls({ /> )} - - {!hideGpuComparison && - selectedGPUs.length > 0 && - selectedDateRange.startDate && - selectedDateRange.endDate && - intermediateDates.length > 0 && ( -
- - { - setSelectedDates(value); - track('inference_intermediate_dates_selected', { - dates: value.join(','), - }); - }} - availableDates={intermediateDates} - maxDates={2} - placeholder="Select intermediate dates" - /> -
- )} diff --git a/packages/app/src/components/inference/ui/ChartDisplay.tsx b/packages/app/src/components/inference/ui/ChartDisplay.tsx index e799b2a..fb51267 100644 --- a/packages/app/src/components/inference/ui/ChartDisplay.tsx +++ b/packages/app/src/components/inference/ui/ChartDisplay.tsx @@ -111,6 +111,8 @@ export default function ChartDisplay() { selectedE2eXAxisMetric, selectedGPUs, selectedPrecisions, + selectedDates, + setSelectedDates, selectedDateRange, dateRangeAvailableDates, selectedModel, @@ -127,7 +129,6 @@ export default function ChartDisplay() { const { changelogs, - intermediateDates, loading: changelogsLoading, totalDatesQueried, } = useComparisonChangelogs(selectedGPUs, selectedDateRange, dateRangeAvailableDates); @@ -677,7 +678,7 @@ export default function ChartDisplay() { - + {selectedGPUs.length === 0 && } {selectedGPUs.length > 0 && ( @@ -687,6 +688,21 @@ export default function ChartDisplay() { selectedPrecisions={selectedPrecisions} loading={changelogsLoading} totalDatesQueried={totalDatesQueried} + selectedDates={selectedDates} + selectedDateRange={selectedDateRange} + onAddDate={(date) => { + if (!selectedDates.includes(date)) { + setSelectedDates([...selectedDates, date]); + } + }} + onRemoveDate={(date) => { + setSelectedDates(selectedDates.filter((d) => d !== date)); + }} + onAddAllDates={(dates) => { + const merged = [...new Set([...selectedDates, ...dates])]; + setSelectedDates(merged); + }} + firstAvailableDate={dateRangeAvailableDates[0]} /> )} diff --git a/packages/app/src/components/inference/ui/ComparisonChangelog.test.ts b/packages/app/src/components/inference/ui/ComparisonChangelog.test.ts new file mode 100644 index 0000000..2f3c519 --- /dev/null +++ b/packages/app/src/components/inference/ui/ComparisonChangelog.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from 'vitest'; + +/** + * Tests for the "add to chart" logic used in ComparisonChangelog. + * Verifies date filtering: which dates are on chart, which are addable. + */ + +interface MockChangelog { + date: string; + entries: { config_keys: string[]; description: string; pr_link: string | null }[]; +} + +function computeDatesOnChart( + selectedDates: string[], + selectedDateRange: { startDate: string; endDate: string }, +): Set { + const set = new Set(selectedDates); + if (selectedDateRange.startDate) set.add(selectedDateRange.startDate); + if (selectedDateRange.endDate) set.add(selectedDateRange.endDate); + return set; +} + +function computeAddableDates( + filteredChangelogs: MockChangelog[], + datesOnChart: Set, +): string[] { + return filteredChangelogs.map((c) => c.date).filter((d) => !datesOnChart.has(d)); +} + +const changelogs: MockChangelog[] = [ + { + date: '2026-01-15', + entries: [{ config_keys: ['dsr1-fp8-h200-sglang'], description: 'Update', pr_link: null }], + }, + { + date: '2026-01-20', + entries: [{ config_keys: ['dsr1-fp8-h200-sglang'], description: 'Bump', pr_link: null }], + }, + { + date: '2026-01-25', + entries: [{ config_keys: ['dsr1-fp8-h200-sglang'], description: 'Tweak', pr_link: null }], + }, +]; + +describe('ComparisonChangelog add-to-chart logic', () => { + it('all dates are addable when none are selected', () => { + const onChart = computeDatesOnChart([], { startDate: '', endDate: '' }); + const addable = computeAddableDates(changelogs, onChart); + expect(addable).toEqual(['2026-01-15', '2026-01-20', '2026-01-25']); + }); + + it('dates in selectedDates are marked as on chart', () => { + const onChart = computeDatesOnChart(['2026-01-15', '2026-01-20'], { + startDate: '', + endDate: '', + }); + expect(onChart.has('2026-01-15')).toBe(true); + expect(onChart.has('2026-01-20')).toBe(true); + expect(onChart.has('2026-01-25')).toBe(false); + const addable = computeAddableDates(changelogs, onChart); + expect(addable).toEqual(['2026-01-25']); + }); + + it('range endpoints are marked as on chart', () => { + const onChart = computeDatesOnChart([], { + startDate: '2026-01-15', + endDate: '2026-01-25', + }); + expect(onChart.has('2026-01-15')).toBe(true); + expect(onChart.has('2026-01-25')).toBe(true); + const addable = computeAddableDates(changelogs, onChart); + expect(addable).toEqual(['2026-01-20']); + }); + + it('addable excludes both selectedDates and range endpoints', () => { + const onChart = computeDatesOnChart(['2026-01-20'], { + startDate: '2026-01-15', + endDate: '2026-01-25', + }); + const addable = computeAddableDates(changelogs, onChart); + expect(addable).toEqual([]); + }); + + it('returns empty addable when all dates are already on chart', () => { + const onChart = computeDatesOnChart(['2026-01-15', '2026-01-20', '2026-01-25'], { + startDate: '', + endDate: '', + }); + const addable = computeAddableDates(changelogs, onChart); + expect(addable).toEqual([]); + }); +}); diff --git a/packages/app/src/components/inference/ui/ComparisonChangelog.tsx b/packages/app/src/components/inference/ui/ComparisonChangelog.tsx index 882b1ee..d35b00c 100644 --- a/packages/app/src/components/inference/ui/ComparisonChangelog.tsx +++ b/packages/app/src/components/inference/ui/ComparisonChangelog.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ChevronDown, ChevronUp, FileText } from 'lucide-react'; +import { ChevronDown, ChevronUp, FileText, Lock, Minus, Plus } from 'lucide-react'; import { useMemo, useState } from 'react'; import { track } from '@/lib/analytics'; @@ -11,7 +11,8 @@ import { configKeyMatchesHwKey, formatChangelogDescription, } from '@/components/inference/utils/changelogFormatters'; -import { updateRepoUrl } from '@/lib/utils'; +import { HARDWARE_CONFIG } from '@/lib/constants'; +import { getDisplayLabel, updateRepoUrl } from '@/lib/utils'; interface ComparisonChangelogProps { changelogs: ComparisonChangelogType[]; @@ -19,6 +20,13 @@ interface ComparisonChangelogProps { selectedPrecisions: string[]; loading?: boolean; totalDatesQueried: number; + selectedDates: string[]; + selectedDateRange: { startDate: string; endDate: string }; + onAddDate: (date: string) => void; + onRemoveDate: (date: string) => void; + onAddAllDates: (dates: string[]) => void; + /** Earliest date the selected GPU config has benchmark data */ + firstAvailableDate?: string; } export default function ComparisonChangelog({ @@ -27,28 +35,63 @@ export default function ComparisonChangelog({ selectedPrecisions, loading, totalDatesQueried, + selectedDates, + selectedDateRange, + onAddDate, + onRemoveDate, + onAddAllDates, + firstAvailableDate, }: ComparisonChangelogProps) { const [isExpanded, setIsExpanded] = useState(true); - // Filter changelog entries to only show those matching selected GPUs and precisions + // Filter changelog entries to only show those matching selected GPUs and precisions. + // Always keep range endpoints and first appearance date visible. + const pinnedDates = useMemo(() => { + const set = new Set(); + if (selectedDateRange.startDate) set.add(selectedDateRange.startDate); + if (selectedDateRange.endDate) set.add(selectedDateRange.endDate); + if (firstAvailableDate) set.add(firstAvailableDate); + return set; + }, [selectedDateRange, firstAvailableDate]); + const filteredChangelogs = useMemo(() => { const precSet = new Set(selectedPrecisions); - return changelogs - .map((item) => ({ - ...item, - entries: item.entries.filter((entry) => - entry.config_keys.some((key) => { - const precision = key.split('-')[1]; - return ( - precSet.has(precision) && selectedGPUs.some((gpu) => configKeyMatchesHwKey(key, gpu)) - ); - }), - ), - })) - .filter((item) => item.entries.length > 0) + const mapped = changelogs.map((item) => ({ + ...item, + entries: item.entries.filter((entry) => + entry.config_keys.some((key) => { + const precision = key.split('-')[1]; + return ( + precSet.has(precision) && selectedGPUs.some((gpu) => configKeyMatchesHwKey(key, gpu)) + ); + }), + ), + })); + + // Ensure pinned dates are always present + for (const date of pinnedDates) { + if (!mapped.some((item) => item.date === date)) { + mapped.push({ date, entries: [] }); + } + } + + return mapped + .filter((item) => item.entries.length > 0 || pinnedDates.has(item.date)) .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); - }, [changelogs, selectedGPUs, selectedPrecisions]); + }, [changelogs, selectedGPUs, selectedPrecisions, pinnedDates]); + + const datesOnChart = useMemo(() => { + const set = new Set(selectedDates); + if (selectedDateRange.startDate) set.add(selectedDateRange.startDate); + if (selectedDateRange.endDate) set.add(selectedDateRange.endDate); + return set; + }, [selectedDates, selectedDateRange]); + + const addableDates = useMemo( + () => filteredChangelogs.map((c) => c.date).filter((d) => !datesOnChart.has(d)), + [filteredChangelogs, datesOnChart], + ); const handleToggle = () => { const newState = !isExpanded; @@ -65,22 +108,35 @@ export default function ComparisonChangelog({ return (
-
- {isExpanded ? ( - - ) : ( - + {isExpanded ? ( + + ) : ( + + )} + + {isExpanded && addableDates.length > 0 && ( + )} - +
{item.date} - {item.entries.length > 0 ? ( + {item.entries.length > 0 && ( <> {item.headRef && ( @@ -124,19 +180,71 @@ export default function ComparisonChangelog({ )} + )} + {datesOnChart.has(item.date) ? ( + selectedDates.includes(item.date) ? ( + + ) : ( + + + On chart + + ) ) : ( - - {item.date < '2025-12-30' - ? 'No changelog data (tracking began Dec 30, 2025)' - : 'No config changes recorded'} - + )}
- {item.entries.map((entry, entryIndex) => ( -
- {formatChangelogDescription(entry.description)} -
- ))} + {item.entries.length > 0 ? ( + item.entries.map((entry, entryIndex) => ( +
+ {selectedGPUs.length > 1 && + (() => { + const matchingGpus = selectedGPUs.filter((gpu) => + entry.config_keys.some((key) => configKeyMatchesHwKey(key, gpu)), + ); + const labels = matchingGpus.map((gpu) => + HARDWARE_CONFIG[gpu] ? getDisplayLabel(HARDWARE_CONFIG[gpu]) : gpu, + ); + return labels.length > 0 ? ( + + {labels.join(', ')} + + ) : null; + })()} + {formatChangelogDescription(entry.description)} +
+ )) + ) : ( + + {item.date === firstAvailableDate + ? 'First benchmark run for this configuration' + : item.date < '2025-12-30' + ? 'No changelog data (tracking began Dec 30, 2025)' + : filteredChangelogs.some((c) => c.date < item.date && c.entries.length > 0) + ? 'No config changes — same configuration as previous run' + : 'Initial configuration — no changelog entry recorded'} + + )}
)) )} diff --git a/packages/app/src/components/ui/chart-legend.tsx b/packages/app/src/components/ui/chart-legend.tsx index 9d11d08..c8662ce 100644 --- a/packages/app/src/components/ui/chart-legend.tsx +++ b/packages/app/src/components/ui/chart-legend.tsx @@ -395,7 +395,8 @@ export default function ChartLegend({
0 && 'mt-2', allHidden && 'h-0 m-0! p-0! overflow-hidden', )} > diff --git a/packages/app/src/hooks/api/use-comparison-changelogs.ts b/packages/app/src/hooks/api/use-comparison-changelogs.ts index 48aec9f..3b1aed2 100644 --- a/packages/app/src/hooks/api/use-comparison-changelogs.ts +++ b/packages/app/src/hooks/api/use-comparison-changelogs.ts @@ -72,16 +72,7 @@ export function useComparisonChangelogs( return results; }, [hasGPUs, datesToQuery, queries]); - // Intermediate dates with any changelog entries (excluding start/end when date range is set) - const intermediateDates = useMemo(() => { - if (!hasGPUs || !hasDateRange) return []; - return changelogs - .filter((c) => c.date !== selectedDateRange.startDate && c.date !== selectedDateRange.endDate) - .map((c) => c.date) - .sort(); - }, [hasGPUs, hasDateRange, changelogs, selectedDateRange.startDate, selectedDateRange.endDate]); - const loading = queries.some((q) => q.isLoading); - return { changelogs, intermediateDates, loading, totalDatesQueried: datesToQuery.length }; + return { changelogs, loading, totalDatesQueried: datesToQuery.length }; }