mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-01 04:15:53 +00:00
Performance and coherence: - Settings > General 'Refresh every' (Manual/30s/1m/3m/5m/10m), live via RefreshCadenceContext; Manual polls only on demand - usePolled memoKey LRU: provider/period switches paint the last-good result instantly with a switching hairline while refreshing quietly - quota TTL 5min + honest rate-limited copy on 429 backoff - version-suffixed cache files (session-cache.v5.json, daily-cache.v12, auto-minted on future bumps); legacy files never written or deleted, adopted once when versions match: old and new binaries coexist without clobbering (field-observed menubar-vs-desktop ping-pong) - advisory hydration lock: concurrent cold starts share one scan (wait-then-read-warm), stale/dead locks self-heal, never a correctness gate Telemetry v1 + onboarding (desktop only, per owner decisions): - first-launch onboarding (3 feature screens + consent); region-split default (EU/EEA/UK/CH off, elsewhere on, unknown off); nothing sends before consent completion or while off; dev builds never send - anonymous install UUID, rotated on opt-out; day-granularity events, cost buckets only; whitelisted names; 200-event queue, 5min flush - Settings > Privacy live toggle replaces the static claim Zero computed-number changes. App 316/316, root 1805. Wire contract targets api.codeburn.app/v1/telemetry (Worker follows).
273 lines
10 KiB
TypeScript
273 lines
10 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react'
|
|
|
|
import { CliErrorPanel } from '../components/CliErrorPanel'
|
|
import { Dropdown } from '../components/Dropdown'
|
|
import { EmptyNote } from '../components/EmptyState'
|
|
import { Panel } from '../components/Panel'
|
|
import { SectionSkeleton } from '../components/Skeleton'
|
|
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')
|
|
}
|
|
|
|
// 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">
|
|
Compare uses the selected period, custom dates are not supported yet.
|
|
</p>
|
|
)
|
|
}
|
|
|
|
export function Compare({
|
|
period,
|
|
provider,
|
|
range = null,
|
|
refreshToken = 0,
|
|
ready = true,
|
|
}: {
|
|
period: Period
|
|
provider: string
|
|
range?: DateRange | null
|
|
refreshToken?: number
|
|
ready?: boolean
|
|
}) {
|
|
const models = usePolled<ModelStats[]>(
|
|
() => codeburn.getCompareModels(period, provider),
|
|
[period, provider, refreshToken],
|
|
{ enabled: ready, memoKey: `comparemodels|${period}|${provider}` },
|
|
)
|
|
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 <SectionSkeleton label="Scanning model usage…" rows={4} />
|
|
}
|
|
|
|
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))
|
|
}}
|
|
/>
|
|
<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))
|
|
}}
|
|
/>
|
|
</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],
|
|
{ memoKey: `compare|${period}|${provider}|${modelA}|${modelB}` },
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (report.error) onError()
|
|
}, [report.error, onError])
|
|
|
|
if (!report.data) {
|
|
if (report.error) return <CliErrorPanel error={report.error} subject="model comparisons" />
|
|
return <SectionSkeleton label="Comparing models…" rows={4} />
|
|
}
|
|
|
|
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>
|
|
)
|
|
}
|