Add collapsible models section with bar chart and token summary

This commit is contained in:
iamtoruk 2026-04-29 14:12:24 -07:00
parent 637c88a86d
commit d61ea56b3e
3 changed files with 176 additions and 0 deletions

View file

@ -9,6 +9,7 @@ import { USD, formatCompactCurrency, formatCurrency } from './lib/currency'
import { PayloadCache } from './lib/cache'
import { AgentTabStrip } from './components/AgentTabStrip'
import type { Provider } from './components/AgentTabStrip'
import { ModelsSection } from './components/ModelsSection'
const payloadCache = new PayloadCache<MenubarPayload>()
@ -175,6 +176,14 @@ export function App() {
</section>
)}
<ModelsSection
models={payload.current.topModels}
inputTokens={payload.current.inputTokens}
outputTokens={payload.current.outputTokens}
cacheHitPercent={payload.current.cacheHitPercent}
currency={currency}
/>
{payload.optimize.findingCount > 0 && (
<section className="findings">
<button className="findings-cta" onClick={openOptimize}>

View file

@ -0,0 +1,71 @@
import { useState } from 'react'
import type { Model } from '../lib/payload'
import type { CurrencyState } from '../lib/currency'
import { formatCompactCurrency } from '../lib/currency'
function formatTokens(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
return String(n)
}
type Props = {
models: Model[]
inputTokens: number
outputTokens: number
cacheHitPercent: number
currency: CurrencyState
}
export function ModelsSection({ models, inputTokens, outputTokens, cacheHitPercent, currency }: Props) {
const [expanded, setExpanded] = useState(true)
if (models.length === 0) return null
const maxCost = Math.max(...models.map(m => m.cost))
return (
<section className="models-section">
<button className="section-header" onClick={() => setExpanded(!expanded)}>
<span className="section-dot" />
<span className="section-caption">Models</span>
{expanded && (
<span className="section-columns">
<span>Cost</span>
<span>Calls</span>
</span>
)}
<span className={`chevron ${expanded ? 'chevron-open' : ''}`}>&#9656;</span>
</button>
{expanded && (
<>
{models.map(m => {
const fillPct = maxCost > 0 ? (m.cost / maxCost) * 100 : 0
return (
<div key={m.name} className="model-row">
<div className="row-bar-container">
<div className="row-bar-fill" style={{ width: `${fillPct}%` }} />
</div>
<div className="row-label">{m.name}</div>
<div className="row-cost">{formatCompactCurrency(m.cost, currency)}</div>
<div className="row-calls">{m.calls}</div>
</div>
)
})}
{(inputTokens > 0 || outputTokens > 0) && (
<div className="tokens-line">
<span className="tokens-label">Tokens</span>
<span className="tokens-value">{formatTokens(inputTokens)} in</span>
<span className="tokens-sep">&middot;</span>
<span className="tokens-value">{formatTokens(outputTokens)} out</span>
<span className="tokens-sep">&middot;</span>
<span className="tokens-value">{Math.round(cacheHitPercent)}% cache hit</span>
</div>
)}
</>
)}
</section>
)
}

View file

@ -182,6 +182,102 @@ html, body, #root {
border-radius: 3px;
}
/* ---- collapsible section header ---- */
.section-header {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
border: 0;
background: transparent;
padding: 0 0 var(--spacing-sm);
cursor: pointer;
color: var(--text-primary);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.2px;
text-transform: uppercase;
}
.section-dot {
width: 5px; height: 5px; border-radius: 50%;
background: var(--brand-accent);
flex-shrink: 0;
}
.section-caption { color: var(--brand-accent); }
.section-columns {
margin-left: auto;
display: flex;
gap: 16px;
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
color: var(--text-tertiary);
letter-spacing: 0.3px;
}
.chevron {
font-size: 10px;
color: var(--text-tertiary);
transition: transform 0.15s ease;
margin-left: 4px;
}
.chevron-open { transform: rotate(90deg); }
/* ---- models ---- */
.models-section {
padding: var(--spacing-sm) var(--spacing-lg) var(--spacing-md);
border-top: 1px solid rgba(0, 0, 0, 0.06);
}
.model-row {
display: grid;
grid-template-columns: 56px 1fr auto 36px;
align-items: center;
gap: var(--spacing-sm);
padding: 3px 0;
font-size: 11px;
font-variant-numeric: tabular-nums;
}
.row-bar-container {
height: 6px;
background: rgba(0, 0, 0, 0.06);
border-radius: 3px;
overflow: hidden;
}
.row-bar-fill {
height: 100%;
background: var(--brand-accent);
border-radius: 3px;
min-width: 1px;
}
.row-calls {
color: var(--text-secondary);
text-align: right;
font-size: 10.5px;
}
/* ---- tokens line ---- */
.tokens-line {
display: flex;
align-items: center;
gap: 5px;
padding-top: var(--spacing-sm);
margin-top: var(--spacing-xs);
border-top: 1px solid rgba(0, 0, 0, 0.04);
font-size: 10px;
}
.tokens-label {
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.2px;
}
.tokens-value {
font-family: var(--font-mono);
color: var(--text-secondary);
}
.tokens-sep {
color: var(--text-tertiary);
}
/* ---- findings ---- */
.findings { padding: 0 var(--spacing-lg) var(--spacing-sm); }
.findings-cta {