Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion dash/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { DataTable } from '@/components/DataTable'
import { GranularUsageChart, DeviceUsageChart, type Unit } from '@/components/UsageChart'
import { DeviceSearchModal } from '@/components/DeviceSearchModal'
import { ContextExplorer } from '@/components/ContextExplorer'
import { WorkflowPanel, hasWorkflowContent } from '@/components/WorkflowPanel'
import { Punchcard } from '@/components/Punchcard'

const n = (v: number | undefined): number => v ?? 0

Expand Down Expand Up @@ -86,6 +88,15 @@ function DeviceView({ payload, isRemote, unit }: { payload?: Payload; isRemote:
const activityBars: BarItem[] = c
? c.topActivities.filter((a) => a.cost > 0).map((a) => ({ name: a.name, value: a.cost, display: usd(a.cost) }))
: []
// Workflow rides beside Model efficiency only when it carries data. Without
// it the row is a single full-width Model efficiency panel, so an older peer
// (no workflow block) renders exactly as the dashboard did before.
const workflowPanel = c && hasWorkflowContent(c) ? (
<Panel title="Workflow">
<WorkflowPanel current={c} />
</Panel>
) : null
const timeline = payload?.history.timeline

return (
<>
Expand Down Expand Up @@ -150,7 +161,7 @@ function DeviceView({ payload, isRemote, unit }: { payload?: Payload; isRemote:
</Panel>
</div>

<div className="mb-3">
<div className={cn('mb-3 grid gap-3', workflowPanel && 'lg:grid-cols-2')}>
<Panel title="Model efficiency">
<DataTable
columns={[
Expand All @@ -165,8 +176,17 @@ function DeviceView({ payload, isRemote, unit }: { payload?: Payload; isRemote:
}))}
/>
</Panel>
{workflowPanel}
</div>

{timeline && (
<div className="mb-3">
<Panel title="Spend punchcard">
<Punchcard timeline={timeline} />
</Panel>
</div>
)}

<div className="mb-3 grid gap-3 lg:grid-cols-2">
<Panel title="Top projects">
{isRemote ? (
Expand Down
174 changes: 174 additions & 0 deletions dash/src/components/Punchcard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { useMemo, useRef, useState } from 'react'

import type { GranularHistory } from '@/lib/api'
import { usd } from '@/lib/utils'

const WEEKDAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
const WEEKDAYS_FULL = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
const HOURS = Array.from({ length: 24 }, (_, h) => h)

type Cell = { cost: number; covered: boolean }

function pad2(v: number): string {
return String(v).padStart(2, '0')
}

// Mon-first weekday index from a Date. getDay() is 0=Sun..6=Sat.
function weekdayIndex(d: Date): number {
return (d.getDay() + 6) % 7
}

// Perceptual ramp: sqrt keeps small spends visible against the largest cell
// instead of collapsing to an invisible dot.
function intensity(cost: number, max: number): number {
if (max <= 0 || cost <= 0) return 0
return Math.sqrt(cost / max)
}

export function Punchcard({ timeline }: { timeline: GranularHistory }) {
const wrapRef = useRef<HTMLDivElement | null>(null)
const [hover, setHover] = useState<{ x: number; y: number; wd: number; h: number } | null>(null)

const { grid, max, hasBucket } = useMemo(() => {
const g: Cell[][] = Array.from({ length: 7 }, () => Array.from({ length: 24 }, () => ({ cost: 0, covered: false })))
let m = 0
let any = false
for (const p of timeline.points) {
const d = new Date(p.timestamp)
if (!Number.isFinite(d.getTime())) continue
any = true
const cell = g[weekdayIndex(d)]![d.getHours()]!
cell.covered = true
cell.cost += p.cost > 0 ? p.cost : 0
if (cell.cost > m) m = cell.cost
}
return { grid: g, max: m, hasBucket: any }
}, [timeline])

// Buckets coarser than an hour carry no hour-of-day signal: every daily bucket
// is timestamped at local midnight, so plotting it would assert all spend
// happened at hour 0. Show the honest limitation instead of a fake column.
const hourResolved = timeline.bucketMinutes < 1440

const bucketNote = timeline.bucketMinutes >= 60 ? 'Hourly buckets' : `${timeline.bucketMinutes}-minute buckets`

if (!hasBucket) {
return <div className="py-10 text-center text-sm text-tertiary-foreground">No timestamped usage in this period.</div>
}

if (!hourResolved) {
return (
<div className="py-8 text-center text-sm text-tertiary-foreground">
Hour-of-day detail needs sub-daily buckets. Switch to Today or 7 days to see the punchcard.
</div>
)
}

const hovered = hover ? grid[hover.wd]![hover.h]! : null

return (
<div>
<div className="mb-3 flex items-center justify-between gap-3">
<span className="text-[10px] font-medium uppercase tracking-[0.1em] text-tertiary-foreground">{bucketNote} · local time</span>
<div className="flex items-center gap-1.5 text-[10px] text-tertiary-foreground">
<span>Less</span>
{[0.12, 0.4, 0.7, 1].map((t) => (
<span
key={t}
className="rounded-full"
style={{
width: `${5 + t * 7}px`,
height: `${5 + t * 7}px`,
background: 'var(--color-primary)',
opacity: 0.35 + t * 0.65,
}}
/>
))}
<span>More</span>
</div>
</div>

<div className="overflow-x-auto">
<div
ref={wrapRef}
className="relative min-w-[560px]"
onMouseLeave={() => setHover(null)}
>
{/* Hour axis */}
<div className="grid items-center" style={{ gridTemplateColumns: '2.25rem repeat(24, minmax(0, 1fr))' }}>
<span />
{HOURS.map((h) => (
<span key={h} className="pb-1 text-center text-[9.5px] tabular-nums text-tertiary-foreground">
{h % 3 === 0 ? h : ''}
</span>
))}
</div>

{/* Weekday rows */}
{WEEKDAYS.map((wdLabel, wd) => (
<div
key={wdLabel}
className="grid items-center"
style={{ gridTemplateColumns: '2.25rem repeat(24, minmax(0, 1fr))' }}
>
<span className="pr-2 text-right text-[11px] tabular-nums text-tertiary-foreground">{wdLabel}</span>
{HOURS.map((h) => {
const cell = grid[wd]![h]!
const t = intensity(cell.cost, max)
const active = hover?.wd === wd && hover?.h === h
return (
<div
key={h}
className="flex aspect-square items-center justify-center p-[2px]"
onMouseEnter={(e) => {
if (!cell.covered || !wrapRef.current) return
const r = wrapRef.current.getBoundingClientRect()
setHover({ x: e.clientX - r.left, y: e.clientY - r.top, wd, h })
}}
onMouseMove={(e) => {
if (!cell.covered || !wrapRef.current) return
const r = wrapRef.current.getBoundingClientRect()
setHover({ x: e.clientX - r.left, y: e.clientY - r.top, wd, h })
}}
>
<div
className="flex h-full w-full items-center justify-center rounded-[3px]"
style={{ background: cell.covered ? 'var(--color-interactive-secondary)' : 'transparent' }}
>
{cell.cost > 0 ? (
<div
className="rounded-full ring-1 ring-inset ring-black/5 transition-transform"
style={{
width: `${22 + t * 70}%`,
height: `${22 + t * 70}%`,
background: 'var(--color-primary)',
opacity: 0.45 + t * 0.55,
transform: active ? 'scale(1.18)' : undefined,
}}
/>
) : cell.covered ? (
<div className="h-[3px] w-[3px] rounded-full bg-tertiary-foreground opacity-25" />
) : null}
</div>
</div>
)
})}
</div>
))}

{hover && hovered && (
<div
className="pointer-events-none absolute z-10 -translate-x-1/2 -translate-y-full rounded-lg border border-border bg-popover px-2.5 py-1.5 text-xs shadow-xl ring-1 ring-black/5"
style={{ left: hover.x, top: hover.y - 8 }}
>
<div className="font-medium text-foreground">
{WEEKDAYS_FULL[hover.wd]} {pad2(hover.h)}:00
</div>
<div className="tabular-nums text-tertiary-foreground">{usd(hovered.cost)}</div>
</div>
)}
</div>
</div>
</div>
)
}
109 changes: 109 additions & 0 deletions dash/src/components/WorkflowPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { ReactNode } from 'react'
import type { Current } from '@/lib/api'
import { fmtNum } from '@/lib/utils'

// Median time-to-first-edit is milliseconds; render it compactly (sub-second up
// to hours) so a fast 0.8s and a slow 12m both read at a glance.
function fmtDuration(ms: number): string {
if (!isFinite(ms) || ms < 0) return '—'
if (ms < 1000) return `${Math.round(ms)}ms`
const s = ms / 1000
if (s < 10) return `${s.toFixed(1)}s`
if (s < 60) return `${Math.round(s)}s`
const m = Math.floor(s / 60)
const rs = Math.round(s % 60)
if (m < 60) return rs ? `${m}m ${rs}s` : `${m}m`
const h = Math.floor(m / 60)
const rm = m % 60
return rm ? `${h}h ${rm}m` : `${h}h`
}

// The panel earns its place only when at least one signal carries real data.
// An older peer (or a period with no edit activity and no priced calls) leaves
// every field empty, and the whole panel is hidden so the dashboard renders as
// it did before this block existed.
export function hasWorkflowContent(c: Current): boolean {
const w = c.workflow
return (
w?.correctionRate != null ||
w?.medianTimeToFirstEditMs != null ||
(w?.corrections ?? 0) > 0 ||
c.pricingCoverage != null ||
(c.topReworkedFiles?.length ?? 0) > 0
)
}

function Row({ label, hint, value }: { label: string; hint?: string; value: ReactNode }) {
return (
<div className="flex items-baseline justify-between gap-3 text-sm">
<span className="text-tertiary-foreground" title={hint}>
{label}
</span>
<span className="shrink-0 tabular-nums">{value}</span>
</div>
)
}

export function WorkflowPanel({ current }: { current: Current }) {
const w = current.workflow
const reworked = current.topReworkedFiles ?? []
const coverage = current.pricingCoverage

const correction =
w?.correctionRate == null ? (
<span className="text-tertiary-foreground">—</span>
) : (
<>
<span className="font-medium text-foreground">{Math.round(w.correctionRate * 100)}%</span>
<span className="text-tertiary-foreground"> · {fmtNum(w.corrections)}</span>
</>
)

return (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2.5">
<Row
label="Correction rate"
hint="Share of prompts where you corrected the assistant, and the correction count"
value={correction}
/>
<Row
label="First-edit time"
hint="Median time from a prompt to the first file edit"
value={
w?.medianTimeToFirstEditMs == null ? (
<span className="text-tertiary-foreground">—</span>
) : (
<span className="font-medium text-foreground">{fmtDuration(w.medianTimeToFirstEditMs)}</span>
)
}
/>
{coverage != null && (
<Row
label="Pricing coverage"
hint="Share of cost-bearing calls with a resolved price"
value={<span className="font-medium text-foreground">{Math.round(coverage * 100)}%</span>}
/>
)}
</div>

{reworked.length > 0 && (
<div className="border-t border-border pt-3">
<div className="mb-2 text-[11px] font-medium uppercase tracking-wider text-tertiary-foreground">Most reworked</div>
<div className="flex flex-col gap-1.5">
{reworked.slice(0, 8).map((f) => (
<div key={f.path} className="flex items-baseline justify-between gap-3">
<span className="truncate font-mono text-[12.5px] text-foreground" title={f.path}>
{f.path}
</span>
<span className="shrink-0 text-xs tabular-nums text-tertiary-foreground">
<span className="font-medium text-foreground">{fmtNum(f.edits)}</span> edits · {fmtNum(f.sessions)} sess
</span>
</div>
))}
</div>
</div>
)}
</div>
)
}
21 changes: 21 additions & 0 deletions dash/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ export type Current = {
skills: Array<{ name: string; turns: number; cost: number }>
mcpServers: Array<{ name: string; calls: number }>
modelEfficiency: Array<{ name: string; costPerEdit: number; oneShotRate: number }>
// Workflow-intelligence rollup for the period. Optional: an older peer's
// payload predates the block, and the Workflow panel hides when it is absent.
workflow?: { corrections: number; correctionRate: number | null; medianTimeToFirstEditMs: number | null }
// Files most reworked by edit-family calls (top 8), basenames only.
topReworkedFiles?: Array<{ path: string; sessions: number; edits: number }>
// Share (0-1) of cost-bearing calls that resolved a price. null when not
// computable; "unknown" must never render as 100% coverage.
pricingCoverage?: number | null
localModelSavings: { totalUSD: number }
retryTax: { totalUSD: number; retries: number }
routingWaste: { totalSavingsUSD: number }
Expand Down Expand Up @@ -138,6 +146,19 @@ function normalizePayload(p?: Payload): Payload | undefined {
skills: c.skills ?? [],
mcpServers: c.mcpServers ?? [],
modelEfficiency: c.modelEfficiency ?? [],
workflow: c.workflow
? {
corrections: c.workflow.corrections ?? 0,
correctionRate: c.workflow.correctionRate ?? null,
medianTimeToFirstEditMs: c.workflow.medianTimeToFirstEditMs ?? null,
}
: undefined,
topReworkedFiles: (c.topReworkedFiles ?? []).map((f) => ({
path: f.path,
sessions: f.sessions ?? 0,
edits: f.edits ?? 0,
})),
pricingCoverage: c.pricingCoverage ?? null,
localModelSavings: c.localModelSavings ?? { totalUSD: 0 },
retryTax: c.retryTax ?? { totalUSD: 0, retries: 0 },
routingWaste: c.routingWaste ?? { totalSavingsUSD: 0 },
Expand Down