Skip to content

Commit ff2a7ad

Browse files
haasonsaasclaude
andcommitted
Extend Events and Admin pages with full feature set
Events page: - Click-through links to /review/:id from event rows - Cost estimation per review using model pricing data - Export filtered events as CSV or JSON - Live tail mode with pulsing indicator and new-row highlight - Comparison mode: select 2 events for side-by-side delta analysis Admin page: - Time-range selector (1h/24h/7d/30d/All) filters all analytics - Severity trends stacked area chart (Error/Warning/Info/Suggestion) - Token cost dashboard with cumulative cost chart and monthly projection - Category x Repository heatmap showing finding distribution patterns - Period comparison mode splitting events into halves with delta indicators Shared: add cost estimation utility (lib/cost.ts) using MODEL_PRESETS pricing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a500968 commit ff2a7ad

File tree

3 files changed

+982
-215
lines changed

3 files changed

+982
-215
lines changed

web/src/lib/cost.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { MODEL_PRESETS } from './models'
2+
import type { ReviewEvent } from '../api/types'
3+
4+
// Parse price string like "$3", "$0.25", "free" into per-million-token cost
5+
function parsePricePerMillion(price: string): number {
6+
if (price === 'free') return 0
7+
const match = price.match(/\$?([\d.]+)/)
8+
return match ? parseFloat(match[1]) : 0
9+
}
10+
11+
// Build a lookup: normalized model fragment -> price per million tokens
12+
const priceLookup: [string[], number][] = MODEL_PRESETS.map(p => {
13+
// Extract recognizable fragments from the preset ID
14+
// e.g. "anthropic/claude-sonnet-4.6" -> ["claude-sonnet-4.6", "claude-sonnet", "sonnet-4.6"]
15+
const parts = p.id.split('/')
16+
const modelPart = parts[parts.length - 1].toLowerCase()
17+
const fragments = [modelPart]
18+
// Also store without version suffixes
19+
const noVersion = modelPart.replace(/[-.]?\d+(\.\d+)*$/, '')
20+
if (noVersion && noVersion !== modelPart) fragments.push(noVersion)
21+
return [fragments, parsePricePerMillion(p.price)]
22+
})
23+
24+
/** Estimate cost in USD for a review event based on total tokens and model pricing. */
25+
export function estimateCost(event: ReviewEvent): number {
26+
const tokens = event.tokens_total ?? 0
27+
if (tokens === 0) return 0
28+
29+
const modelLower = event.model.toLowerCase()
30+
// Try to find a matching preset
31+
for (const [fragments, pricePerM] of priceLookup) {
32+
for (const frag of fragments) {
33+
if (modelLower.includes(frag) || frag.includes(modelLower)) {
34+
return (tokens / 1_000_000) * pricePerM
35+
}
36+
}
37+
}
38+
39+
// Fallback: assume $1/M tokens (conservative middle ground)
40+
return (tokens / 1_000_000) * 1
41+
}
42+
43+
/** Format cost as a readable string */
44+
export function formatCost(usd: number): string {
45+
if (usd === 0) return '$0'
46+
if (usd < 0.001) return '<$0.001'
47+
if (usd < 0.01) return `$${usd.toFixed(4)}`
48+
if (usd < 1) return `$${usd.toFixed(3)}`
49+
return `$${usd.toFixed(2)}`
50+
}
51+
52+
/** Estimate total cost across multiple events */
53+
export function totalCost(events: ReviewEvent[]): number {
54+
return events.reduce((sum, e) => sum + estimateCost(e), 0)
55+
}

0 commit comments

Comments
 (0)