mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-20 17:47:19 +00:00
Add collapsible models section with bar chart and token summary
This commit is contained in:
parent
637c88a86d
commit
d61ea56b3e
3 changed files with 176 additions and 0 deletions
|
|
@ -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}>
|
||||
|
|
|
|||
71
desktop/src/components/ModelsSection.tsx
Normal file
71
desktop/src/components/ModelsSection.tsx
Normal 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' : ''}`}>▸</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">·</span>
|
||||
<span className="tokens-value">{formatTokens(outputTokens)} out</span>
|
||||
<span className="tokens-sep">·</span>
|
||||
<span className="tokens-value">{Math.round(cacheHitPercent)}% cache hit</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue