codeburn/app/renderer/sections/Compare.tsx
iamtoruk cbddd3a9d7 fix(app): accuracy batch A — provider scoping, honest chart axes, range threading, stale-error banner
- Provider filter: Models-this-period sources current.topModels (history
  days carry empty topModels per provider); StackedBars falls back to a
  cost-proportional single segment like the Swift menubar
- Chart windows align to the CLI (week -7, 30days -30, all = 6 months)
  and zero-fill a contiguous calendar window; history.daily is sparse and
  both sides key local YYYY-MM-DD, so lookups are safe (verified on real
  CLI output)
- Custom range threads into Overview panels (chart, models table) and
  suppresses MTD/projected and vs-last-week comparisons; Compare states
  that custom dates fall back to the period
- usePolled clears errors per attempt and sections show a StaleBanner
  instead of silently rendering last-good data after a failed refresh
- Cache-hit denominator matches the CLI; hero savings relabeled 'Saved by
  applied fixes' + new 'Saved via local models' line; formatCompact
  everywhere; Optimize 'Fixes' count matches the rendered list

App suite 163/163 (was 147), root 1613/1613.
2026-07-16 01:14:41 -07:00

281 lines
10 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react'
import { CliErrorPanel } from '../components/CliErrorPanel'
import { Dropdown } from '../components/Dropdown'
import { Panel } from '../components/Panel'
import { usePolled } from '../hooks/usePolled'
import { formatCompact, formatUsd } from '../lib/format'
import { codeburn } from '../lib/ipc'
import type { CompareJsonReport, ComparisonRow, DateRange, ModelStats, Period, WorkingStyleRow } from '../lib/types'
function fmtMetric(v: number | null, fn: 'cost' | 'number' | 'percent' | 'decimal'): string {
if (v === null) return '—'
if (fn === 'cost') return formatUsd(v)
if (fn === 'percent') return `${v.toFixed(0)}%`
if (fn === 'decimal') return v.toFixed(2)
return Math.round(v).toLocaleString('en-US')
}
function EmptyNote({ children }: { children: React.ReactNode }) {
return <p style={{ color: 'var(--t3)', margin: 0, fontSize: 12 }}>{children}</p>
}
// The CLI `compare` command has no --from/--to, so a custom range falls back to
// the selected period. Say so instead of silently ignoring the dates.
function RangeNote() {
return (
<p className="cmp-range-note" role="status" style={{ color: 'var(--mut)', margin: '0 0 6px', fontSize: 11.5 }}>
Compare uses the selected period, custom dates are not supported yet.
</p>
)
}
export function Compare({
period,
provider,
range = null,
refreshToken = 0,
}: {
period: Period
provider: string
range?: DateRange | null
refreshToken?: number
}) {
const models = usePolled<ModelStats[]>(
() => codeburn.getCompareModels(period, provider),
[period, provider, refreshToken],
)
const [modelA, setModelA] = useState<string | null>(null)
const [modelB, setModelB] = useState<string | null>(null)
useEffect(() => {
if (!models.data) return
const available = new Set(models.data.map(model => model.model))
setModelA(current => current && available.has(current) ? current : models.data?.[0]?.model ?? null)
setModelB(current => current && available.has(current) ? current : models.data?.[1]?.model ?? null)
}, [models.data])
const resetToDefaults = useCallback(() => {
if (!models.data) return
setModelA(models.data[0]?.model ?? null)
setModelB(models.data[1]?.model ?? null)
}, [models.data])
if (!models.data) {
if (models.error) return <CliErrorPanel error={models.error} subject="model comparisons" />
return (
<Panel title="Compare">
<EmptyNote>Scanning model usage</EmptyNote>
</Panel>
)
}
if (models.data.length < 2) {
return (
<Panel title="Compare">
<EmptyNote>Need at least two models with usage in this range to compare.</EmptyNote>
</Panel>
)
}
const modelRows = models.data
const nudgeDistinct = (chosen: string) => modelRows.find(model => model.model !== chosen)?.model ?? null
return (
<>
{range && <RangeNote />}
<div className="cmp-picker" aria-label="Models being compared">
<Dropdown
id="compare-first-model"
ariaLabel="First model"
value={modelA ?? ''}
options={modelRows.map(model => ({ value: model.model, label: `${model.model} · ${model.calls.toLocaleString()} calls` }))}
onChange={next => {
setModelA(next)
if (next === modelB) setModelB(nudgeDistinct(next))
}}
width={230}
/>
<span className="cmp-vs">vs</span>
<Dropdown
id="compare-second-model"
ariaLabel="Second model"
value={modelB ?? ''}
options={modelRows.map(model => ({ value: model.model, label: `${model.model} · ${model.calls.toLocaleString()} calls` }))}
onChange={next => {
setModelB(next)
if (next === modelA) setModelA(nudgeDistinct(next))
}}
width={230}
/>
</div>
{modelA && modelB && modelA !== modelB && (
<CompareReport
period={period}
provider={provider}
modelA={modelA}
modelB={modelB}
refreshToken={refreshToken}
onError={resetToDefaults}
/>
)}
</>
)
}
function CompareReport({
period,
provider,
modelA,
modelB,
refreshToken,
onError,
}: {
period: Period
provider: string
modelA: string
modelB: string
refreshToken: number
onError: () => void
}) {
const report = usePolled<CompareJsonReport>(
() => codeburn.getCompare(period, provider, modelA, modelB),
[period, provider, modelA, modelB, refreshToken],
)
useEffect(() => {
if (report.error) onError()
}, [report.error, onError])
if (!report.data) {
if (report.error) return <CliErrorPanel error={report.error} subject="model comparisons" />
return (
<Panel title="Compare">
<EmptyNote>Comparing models</EmptyNote>
</Panel>
)
}
const performance = report.data.metrics.filter(metric => metric.section === 'Performance')
const efficiency = report.data.metrics.filter(metric => metric.section === 'Efficiency')
return (
<div className="cmp-body">
<div className="cmp-pair">
<MetricCard title="Performance" rows={performance} modelA={report.data.modelA.model} modelB={report.data.modelB.model} showWinners />
<MetricCard title="Efficiency" rows={efficiency} modelA={report.data.modelA.model} modelB={report.data.modelB.model} showWinners />
</div>
<CategoryCard report={report.data} />
<div className="cmp-pair">
<MetricCard title="Working style" rows={report.data.workingStyle} modelA={report.data.modelA.model} modelB={report.data.modelB.model} />
<ContextCard modelA={report.data.modelA} modelB={report.data.modelB} />
</div>
</div>
)
}
function MetricCard({
title,
rows,
modelA,
modelB,
showWinners = false,
}: {
title: string
rows: Array<ComparisonRow | WorkingStyleRow>
modelA: string
modelB: string
showWinners?: boolean
}) {
return (
<div className="panel cmp-card">
<div className="cmp-head"><h3>{title}</h3></div>
<div className="cmp-metrics">
<MetricHeader modelA={modelA} modelB={modelB} />
{rows.map(row => {
const winner = 'winner' in row ? row.winner : 'none'
return (
<div className="cmp-metric" key={row.label}>
<span className="cmp-label">{row.label}</span>
<span className={`cmp-value${showWinners && winner === 'a' ? ' cmp-best' : ''}`}>{fmtMetric(row.valueA, row.formatFn)}</span>
<span className={`cmp-value${showWinners && winner === 'b' ? ' cmp-best' : ''}`}>{fmtMetric(row.valueB, row.formatFn)}</span>
</div>
)
})}
</div>
{showWinners && <div className="cmp-foot">Green = better on that metric.</div>}
</div>
)
}
function MetricHeader({ modelA, modelB }: { modelA: string; modelB: string }) {
return <div className="cmp-metric-head"><span>Metric</span><span>{modelA}</span><span>{modelB}</span></div>
}
function CategoryCard({ report }: { report: CompareJsonReport }) {
return (
<div className="panel cmp-card">
<div className="cmp-head"><h3>Category head-to-head</h3><span className="cmp-head-note">One-shot rate · edit turns</span></div>
<div className="cmp-category-body">
<div className="cmp-legend">
<span className="cmp-legend-item"><span className="cmp-key" />{report.modelA.model}</span>
<span className="cmp-legend-item"><span className="cmp-key cmp-key-b" />{report.modelB.model}</span>
</div>
<div className="cmp-categories">
{report.categories.map(category => (
<div className="cmp-category" key={category.category}>
<span className="cmp-category-name">{category.category}</span>
<div className="cmp-bars">
<div className="cmp-bar-row">
<span className="cmp-track"><span className="cmp-bar" style={{ width: `${category.oneShotRateA ?? 0}%` }} /></span>
<span className={`cmp-bar-value${category.winner === 'a' ? ' cmp-best' : ''}`}>{fmtMetric(category.oneShotRateA, 'percent')} <span className="cmp-turns">({category.editTurnsA})</span></span>
</div>
<div className="cmp-bar-row">
<span className="cmp-track"><span className="cmp-bar cmp-bar-b" style={{ width: `${category.oneShotRateB ?? 0}%` }} /></span>
<span className={`cmp-bar-value${category.winner === 'b' ? ' cmp-best' : ''}`}>{fmtMetric(category.oneShotRateB, 'percent')} <span className="cmp-turns">({category.editTurnsB})</span></span>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)
}
function cacheHitRate(model: ModelStats): string {
// reads over reads + fresh input (matches menubar-json + compare-stats).
const total = model.inputTokens + model.cacheReadTokens
return total > 0 ? `${Math.round(model.cacheReadTokens / total * 100)}%` : '—'
}
function daysOfData(model: ModelStats): string {
if (!model.firstSeen || !model.lastSeen) return '—'
return String(Math.max(1, Math.round((new Date(model.lastSeen).getTime() - new Date(model.firstSeen).getTime()) / 86_400_000) + 1))
}
function ContextCard({ modelA, modelB }: { modelA: ModelStats; modelB: ModelStats }) {
const rows = [
['Calls', modelA.calls.toLocaleString(), modelB.calls.toLocaleString()],
['Total cost', formatUsd(modelA.cost), formatUsd(modelB.cost)],
['Input tokens', formatCompact(modelA.inputTokens), formatCompact(modelB.inputTokens)],
['Output tokens', formatCompact(modelA.outputTokens), formatCompact(modelB.outputTokens)],
['Edit turns', modelA.editTurns.toLocaleString(), modelB.editTurns.toLocaleString()],
['Self-corrections', modelA.selfCorrections.toLocaleString(), modelB.selfCorrections.toLocaleString()],
['Cache hit rate', cacheHitRate(modelA), cacheHitRate(modelB)],
['Days of data', daysOfData(modelA), daysOfData(modelB)],
]
return (
<div className="panel cmp-card">
<div className="cmp-head"><h3>Context</h3></div>
<div className="cmp-metrics">
<MetricHeader modelA={modelA.model} modelB={modelB.model} />
{rows.map(([label, valueA, valueB]) => (
<div className="cmp-metric" key={label}>
<span className="cmp-label">{label}</span><span className="cmp-value">{valueA}</span><span className="cmp-value">{valueB}</span>
</div>
))}
</div>
</div>
)
}