@oh-my-pi/omp-stats 18.15.0 → 18.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/client/index.css +1 -1
- package/dist/client/index.js +102 -99
- package/dist/types/client/api.d.ts +4 -1
- package/dist/types/client/app/nav.d.ts +1 -1
- package/dist/types/client/charts/Chart.d.ts +16 -8
- package/dist/types/client/charts/DotWhisker.d.ts +50 -0
- package/dist/types/client/charts/Sparkline.d.ts +1 -1
- package/dist/types/client/charts/Waterfall.d.ts +26 -0
- package/dist/types/client/charts/index.d.ts +2 -0
- package/dist/types/client/charts/types.d.ts +8 -2
- package/dist/types/client/routes/SpotlightRoute.d.ts +7 -0
- package/dist/types/client/routes/index.d.ts +1 -0
- package/dist/types/client/spotlight/Compounding.d.ts +8 -0
- package/dist/types/client/spotlight/Coverage.d.ts +6 -0
- package/dist/types/client/spotlight/Economics.d.ts +10 -0
- package/dist/types/client/spotlight/FetchBack.d.ts +9 -0
- package/dist/types/client/spotlight/Hero.d.ts +12 -0
- package/dist/types/client/spotlight/Overhead.d.ts +6 -0
- package/dist/types/client/spotlight/Ranking.d.ts +6 -0
- package/dist/types/client/spotlight/ResultsFeed.d.ts +6 -0
- package/dist/types/client/spotlight/Savings.d.ts +10 -0
- package/dist/types/client/spotlight/ShareTrend.d.ts +8 -0
- package/dist/types/client/spotlight/Steps.d.ts +6 -0
- package/dist/types/client/spotlight/TimeCost.d.ts +6 -0
- package/dist/types/client/spotlight/chips.d.ts +16 -0
- package/dist/types/client/spotlight/meta.d.ts +64 -0
- package/dist/types/db.d.ts +26 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/parser.d.ts +4 -0
- package/dist/types/rollup.d.ts +22 -1
- package/dist/types/shared-types.d.ts +419 -0
- package/dist/types/spotlight-aggregator.d.ts +189 -0
- package/dist/types/spotlight-bootstrap.d.ts +32 -0
- package/dist/types/spotlight-ingest.d.ts +258 -0
- package/dist/types/spotlight-measures.d.ts +51 -0
- package/package.json +5 -5
- package/src/aggregator.ts +3 -1
- package/src/client/App.tsx +3 -0
- package/src/client/api.ts +21 -0
- package/src/client/app/nav.ts +4 -1
- package/src/client/charts/Chart.tsx +105 -22
- package/src/client/charts/DotWhisker.tsx +149 -0
- package/src/client/charts/Sparkline.tsx +3 -2
- package/src/client/charts/Waterfall.tsx +86 -0
- package/src/client/charts/index.ts +2 -0
- package/src/client/charts/types.ts +9 -2
- package/src/client/data/query.ts +12 -4
- package/src/client/react-css.d.ts +1 -1
- package/src/client/routes/SpotlightRoute.tsx +138 -0
- package/src/client/routes/index.ts +1 -0
- package/src/client/spotlight/Compounding.tsx +222 -0
- package/src/client/spotlight/Coverage.tsx +96 -0
- package/src/client/spotlight/Economics.tsx +167 -0
- package/src/client/spotlight/FetchBack.tsx +222 -0
- package/src/client/spotlight/Hero.tsx +177 -0
- package/src/client/spotlight/Overhead.tsx +135 -0
- package/src/client/spotlight/Ranking.tsx +248 -0
- package/src/client/spotlight/ResultsFeed.tsx +354 -0
- package/src/client/spotlight/Savings.tsx +355 -0
- package/src/client/spotlight/ShareTrend.tsx +103 -0
- package/src/client/spotlight/Steps.tsx +225 -0
- package/src/client/spotlight/TimeCost.tsx +64 -0
- package/src/client/spotlight/chips.tsx +63 -0
- package/src/client/spotlight/meta.ts +206 -0
- package/src/client/spotlight/spotlight.css +1387 -0
- package/src/client/styles.css +289 -1
- package/src/client/traces/SpanDrawer.tsx +2 -1
- package/src/db.ts +480 -2
- package/src/index.ts +10 -1
- package/src/live.ts +21 -9
- package/src/parser.ts +44 -8
- package/src/rollup.ts +203 -10
- package/src/server.ts +42 -0
- package/src/shared-types.ts +453 -0
- package/src/spotlight-aggregator.ts +1738 -0
- package/src/spotlight-bootstrap.ts +110 -0
- package/src/spotlight-ingest.ts +981 -0
- package/src/spotlight-measures.ts +667 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { format } from "@oh-my-pi/pi-utils/dates";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { getSpotlightSessionContext } from "../api";
|
|
4
|
+
import { Chart, type ChartSeries, Legend, type SlotMarker } from "../charts";
|
|
5
|
+
import { formatCompact, formatFolder, formatInteger } from "../data/formatters";
|
|
6
|
+
import { useQuery } from "../data/query";
|
|
7
|
+
import type { SpotlightDashboardStats, SpotlightSessionContext, SpotlightSessionSummary } from "../types";
|
|
8
|
+
import { Badge, Card, ChartSkeleton, EmptyState, ErrorState } from "../ui";
|
|
9
|
+
import { formatShare, formatSignedCompact, formatSignedCost } from "./meta";
|
|
10
|
+
|
|
11
|
+
export interface CompoundingProps {
|
|
12
|
+
stats: SpotlightDashboardStats;
|
|
13
|
+
active: boolean;
|
|
14
|
+
stale: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Title, else a subagent's file name (`Consumers.jsonl`), else the project
|
|
19
|
+
* folder. Subagent and advisor sessions are indented under an arrow and tagged,
|
|
20
|
+
* since they sit in the newest-first list beside the session that spawned them.
|
|
21
|
+
*/
|
|
22
|
+
function sessionLabel(session: SpotlightSessionSummary): string {
|
|
23
|
+
const main = session.agentType === "main";
|
|
24
|
+
const base = (session.sessionFile.split("/").pop() ?? "").replace(/\.jsonl$/, "");
|
|
25
|
+
const name = session.title?.trim() || (main ? formatFolder(session.folder) : base);
|
|
26
|
+
const head = main ? name : `↳ ${name} (${session.agentType})`;
|
|
27
|
+
return `${head} · ${formatCompact(session.savedTokens)} saved`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A cut keeps paying: every later request would have re-read it. One session's context with and without shaping. */
|
|
31
|
+
export function Compounding({ stats, active, stale }: CompoundingProps) {
|
|
32
|
+
const { compounding } = stats;
|
|
33
|
+
const [picked, setPicked] = useState<string | null>(null);
|
|
34
|
+
const sessions = stats.sessionList;
|
|
35
|
+
// Default: the newest session that saved anything (the list is newest first).
|
|
36
|
+
const fallback = sessions.find(s => s.savedTokens > 0) ?? sessions[0];
|
|
37
|
+
const file = picked !== null && sessions.some(s => s.sessionFile === picked) ? picked : fallback?.sessionFile;
|
|
38
|
+
|
|
39
|
+
const context = useQuery(
|
|
40
|
+
["spotlight-session", file ?? null],
|
|
41
|
+
({ signal }) => getSpotlightSessionContext(file ?? "", signal),
|
|
42
|
+
{ enabled: active && file !== undefined },
|
|
43
|
+
);
|
|
44
|
+
const avoided = compounding.avoidedTokens - compounding.recoveredCarriedTokens;
|
|
45
|
+
// Saving-weighted mean of the requests each cut stayed out of.
|
|
46
|
+
const carry = stats.gross.savedTokens > 0 ? compounding.avoidedTokens / stats.gross.savedTokens : null;
|
|
47
|
+
const points = context.data?.points;
|
|
48
|
+
// The selected session's own share: what its requests would have carried without shaping, and didn't.
|
|
49
|
+
const sessionShare = useMemo(() => {
|
|
50
|
+
if (!points) return null;
|
|
51
|
+
let gap = 0;
|
|
52
|
+
let without = 0;
|
|
53
|
+
for (const p of points) {
|
|
54
|
+
gap += p.withoutTokens - p.contextTokens;
|
|
55
|
+
without += p.withoutTokens;
|
|
56
|
+
}
|
|
57
|
+
return without > 0 ? { share: gap / without, tokens: gap } : null;
|
|
58
|
+
}, [points]);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Card
|
|
62
|
+
index={3}
|
|
63
|
+
title="Compounding"
|
|
64
|
+
description="A cut is never re-read: each later request of the session carries a smaller context"
|
|
65
|
+
actions={
|
|
66
|
+
sessions.length > 0 && (
|
|
67
|
+
<select
|
|
68
|
+
className="input spot-session-select"
|
|
69
|
+
aria-label="Session"
|
|
70
|
+
value={file ?? ""}
|
|
71
|
+
onChange={e => setPicked(e.target.value)}
|
|
72
|
+
>
|
|
73
|
+
{sessions.map(s => (
|
|
74
|
+
<option key={s.sessionFile} value={s.sessionFile}>
|
|
75
|
+
{sessionLabel(s)}
|
|
76
|
+
</option>
|
|
77
|
+
))}
|
|
78
|
+
</select>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
stale={stale}
|
|
82
|
+
footer="Context = input + cache read + cache write of each request. A compaction is a request whose context is under 60% of the previous one's; it ends the carry."
|
|
83
|
+
>
|
|
84
|
+
<div className="spot-compounding">
|
|
85
|
+
<div className="spot-compounding-chart">
|
|
86
|
+
{file === undefined ? (
|
|
87
|
+
<EmptyState title="No experiment sessions in this range" />
|
|
88
|
+
) : context.data === null ? (
|
|
89
|
+
context.error ? (
|
|
90
|
+
<ErrorState error={context.error} onRetry={context.refetch} />
|
|
91
|
+
) : (
|
|
92
|
+
<ChartSkeleton height={280} />
|
|
93
|
+
)
|
|
94
|
+
) : (
|
|
95
|
+
<ContextChart context={context.data} stale={context.stale} />
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
<aside className="spot-compounding-side">
|
|
99
|
+
<div className="spot-eyebrow">Context avoided in this session</div>
|
|
100
|
+
<div className="spot-side-value">{formatShare(sessionShare?.share ?? null)}</div>
|
|
101
|
+
{points !== undefined && (
|
|
102
|
+
<p className="spot-side-copy">
|
|
103
|
+
{sessionShare === null
|
|
104
|
+
? "This session has no recorded requests."
|
|
105
|
+
: `of what its requests would have carried without Spotlight: ${formatSignedCompact(sessionShare.tokens)} prompt tokens, net of recoveries.`}
|
|
106
|
+
</p>
|
|
107
|
+
)}
|
|
108
|
+
{context.data && <SessionFacts session={context.data.session} />}
|
|
109
|
+
<p className="spot-side-range">
|
|
110
|
+
<span className="spot-side-range-value">{formatShare(compounding.avoidedShare)}</span> across all the
|
|
111
|
+
sessions shown: cuts kept {formatCompact(Math.max(0, avoided))} tokens out of later requests
|
|
112
|
+
{carry !== null && <>, each saved token re-sent {carry.toFixed(1)} times on average</>}.
|
|
113
|
+
</p>
|
|
114
|
+
</aside>
|
|
115
|
+
</div>
|
|
116
|
+
</Card>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function SessionFacts({ session }: { session: SpotlightSessionSummary }) {
|
|
121
|
+
return (
|
|
122
|
+
<dl className="spot-facts">
|
|
123
|
+
<div>
|
|
124
|
+
<dt>Agent</dt>
|
|
125
|
+
<dd className="row">
|
|
126
|
+
<Badge>{session.agentType}</Badge>
|
|
127
|
+
</dd>
|
|
128
|
+
</div>
|
|
129
|
+
<div>
|
|
130
|
+
<dt>Requests</dt>
|
|
131
|
+
<dd className="num">{formatInteger(session.requests)}</dd>
|
|
132
|
+
</div>
|
|
133
|
+
<div>
|
|
134
|
+
<dt>Saved</dt>
|
|
135
|
+
<dd className="num">{formatSignedCompact(session.savedTokens)}</dd>
|
|
136
|
+
</div>
|
|
137
|
+
<div>
|
|
138
|
+
<dt>Net $</dt>
|
|
139
|
+
<dd className="num">{formatSignedCost(session.netUsd)}</dd>
|
|
140
|
+
</div>
|
|
141
|
+
</dl>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const CONTEXT_LEGEND = [
|
|
146
|
+
{ key: "actual", label: "Context", color: "var(--spot-gross)" },
|
|
147
|
+
{ key: "ghost", label: "Without Spotlight", color: "var(--spot-ghost)" },
|
|
148
|
+
{ key: "compaction", label: "Compaction", color: "var(--line-4)" },
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
/** One session's context per request. The x axis is the request number (several can share a minute); the tooltip carries the time. */
|
|
152
|
+
function ContextChart({ context, stale }: { context: SpotlightSessionContext; stale: boolean }) {
|
|
153
|
+
const { points } = context;
|
|
154
|
+
const chart = useMemo(() => {
|
|
155
|
+
const series: ChartSeries[] = [
|
|
156
|
+
{
|
|
157
|
+
key: "actual",
|
|
158
|
+
label: "Context",
|
|
159
|
+
color: "var(--spot-gross)",
|
|
160
|
+
kind: "area",
|
|
161
|
+
values: points.map(p => p.contextTokens),
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
key: "ghost",
|
|
165
|
+
label: "Avoided",
|
|
166
|
+
color: "var(--spot-ghost)",
|
|
167
|
+
kind: "area",
|
|
168
|
+
pattern: "hatch",
|
|
169
|
+
dashed: true,
|
|
170
|
+
values: points.map(p => Math.max(0, p.withoutTokens - p.contextTokens)),
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
const markers: SlotMarker[] = [];
|
|
174
|
+
points.forEach((p, i) => {
|
|
175
|
+
if (p.compacted) markers.push({ index: i });
|
|
176
|
+
});
|
|
177
|
+
return { series, markers };
|
|
178
|
+
}, [points]);
|
|
179
|
+
|
|
180
|
+
if (points.length === 0) {
|
|
181
|
+
return <EmptyState title="No requests recorded for this session" />;
|
|
182
|
+
}
|
|
183
|
+
return (
|
|
184
|
+
<div className="spot-context" data-stale={stale}>
|
|
185
|
+
<Legend items={CONTEXT_LEGEND} />
|
|
186
|
+
<Chart
|
|
187
|
+
slots={points.length}
|
|
188
|
+
series={chart.series}
|
|
189
|
+
markers={chart.markers}
|
|
190
|
+
height={280}
|
|
191
|
+
tickLabel={i => `#${i + 1}`}
|
|
192
|
+
tooltipTitle={i => `Request ${i + 1} · ${format(new Date(points[i].timestamp), "MMM d HH:mm:ss")}`}
|
|
193
|
+
formatTooltip={formatInteger}
|
|
194
|
+
showTotal={false}
|
|
195
|
+
tooltipExtra={i => {
|
|
196
|
+
const p = points[i];
|
|
197
|
+
return (
|
|
198
|
+
<div className="spot-tooltip-extra">
|
|
199
|
+
<div className="chart-tooltip-row chart-tooltip-total">
|
|
200
|
+
<span className="chart-tooltip-label">Without Spotlight</span>
|
|
201
|
+
<span className="chart-tooltip-value">{formatInteger(p.withoutTokens)}</span>
|
|
202
|
+
</div>
|
|
203
|
+
{p.saved > 0 && (
|
|
204
|
+
<div className="chart-tooltip-row">
|
|
205
|
+
<span className="chart-tooltip-label">Cut since last request</span>
|
|
206
|
+
<span className="chart-tooltip-value">{formatInteger(p.saved)}</span>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
{p.recovered > 0 && (
|
|
210
|
+
<div className="chart-tooltip-row">
|
|
211
|
+
<span className="chart-tooltip-label">Fetched back</span>
|
|
212
|
+
<span className="chart-tooltip-value">{formatInteger(p.recovered)}</span>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
{p.compacted && <div className="chart-tooltip-row dim">Compaction before this request</div>}
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}}
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { BarList, Legend, ShareBar } from "../charts";
|
|
2
|
+
import { formatCompact, formatInteger } from "../data/formatters";
|
|
3
|
+
import type { SpotlightDashboardStats } from "../types";
|
|
4
|
+
import { Badge, Card } from "../ui";
|
|
5
|
+
import { foldTools, formatShare } from "./meta";
|
|
6
|
+
|
|
7
|
+
const SHAPED_COLOR = "var(--spot-applied)";
|
|
8
|
+
const UNSHAPED_COLOR = "var(--spot-unshaped)";
|
|
9
|
+
|
|
10
|
+
/** Which tools' output the models of experiment sessions saw, shaped and not: where shaping could reach next. */
|
|
11
|
+
export function Coverage({ stats, stale }: { stats: SpotlightDashboardStats; stale: boolean }) {
|
|
12
|
+
const rows = [...stats.coverage].sort((a, b) => b.tokens - a.tokens);
|
|
13
|
+
const { listed, rest } = foldTools(rows);
|
|
14
|
+
let shaped = 0;
|
|
15
|
+
let unshaped = 0;
|
|
16
|
+
for (const row of rows) {
|
|
17
|
+
if (row.shaped) shaped += row.share;
|
|
18
|
+
else unshaped += row.share;
|
|
19
|
+
}
|
|
20
|
+
const items = listed.map(row => ({
|
|
21
|
+
key: row.tool,
|
|
22
|
+
label: (
|
|
23
|
+
<>
|
|
24
|
+
<span className="spot-coverage-tool">{row.tool}</span>
|
|
25
|
+
{row.shaped && <Badge>shaped</Badge>}
|
|
26
|
+
<span className="dim num">{formatInteger(row.calls)} calls</span>
|
|
27
|
+
</>
|
|
28
|
+
),
|
|
29
|
+
value: row.share,
|
|
30
|
+
display: `${formatShare(row.share)} · ${formatCompact(row.tokens)}`,
|
|
31
|
+
color: row.shaped ? SHAPED_COLOR : UNSHAPED_COLOR,
|
|
32
|
+
}));
|
|
33
|
+
if (rest.length > 0) {
|
|
34
|
+
let share = 0;
|
|
35
|
+
let tokens = 0;
|
|
36
|
+
let calls = 0;
|
|
37
|
+
for (const row of rest) {
|
|
38
|
+
share += row.share;
|
|
39
|
+
tokens += row.tokens;
|
|
40
|
+
calls += row.calls;
|
|
41
|
+
}
|
|
42
|
+
items.push({
|
|
43
|
+
key: "\u0000rest",
|
|
44
|
+
label: (
|
|
45
|
+
<>
|
|
46
|
+
<span className="spot-coverage-tool">
|
|
47
|
+
{formatInteger(rest.length)} more {rest.length === 1 ? "tool" : "tools"}
|
|
48
|
+
</span>
|
|
49
|
+
<span className="dim num">{formatInteger(calls)} calls</span>
|
|
50
|
+
</>
|
|
51
|
+
),
|
|
52
|
+
value: share,
|
|
53
|
+
display: `${formatShare(share)} · ${formatCompact(tokens)}`,
|
|
54
|
+
color: rest.some(row => row.shaped) ? SHAPED_COLOR : UNSHAPED_COLOR,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Card
|
|
60
|
+
index={5}
|
|
61
|
+
title="Not shaped yet"
|
|
62
|
+
description="Share of the tool output experiment sessions' models saw, by tool: the unshaped ones are where shaping could reach next"
|
|
63
|
+
actions={
|
|
64
|
+
<Legend
|
|
65
|
+
items={[
|
|
66
|
+
{ key: "shaped", label: "Shaped", color: SHAPED_COLOR },
|
|
67
|
+
{ key: "unshaped", label: "Not shaped", color: UNSHAPED_COLOR },
|
|
68
|
+
]}
|
|
69
|
+
/>
|
|
70
|
+
}
|
|
71
|
+
stale={stale}
|
|
72
|
+
footer="Tokens are tool-result chars ÷ 4 over the experiment's sessions in the range; a tool counts as shaped when the experiment evaluated any of its results in the range."
|
|
73
|
+
>
|
|
74
|
+
{rows.length === 0 ? (
|
|
75
|
+
<div className="empty">No tool output in this range.</div>
|
|
76
|
+
) : (
|
|
77
|
+
<div className="spot-coverage">
|
|
78
|
+
<div className="spot-coverage-summary">
|
|
79
|
+
<div className="spot-coverage-headline">
|
|
80
|
+
<span className="num">{formatShare(unshaped)}</span> of tool output comes from tools the experiment
|
|
81
|
+
did not shape in this range
|
|
82
|
+
</div>
|
|
83
|
+
<ShareBar
|
|
84
|
+
height={8}
|
|
85
|
+
segments={[
|
|
86
|
+
{ key: "shaped", label: "Shaped", value: shaped, color: SHAPED_COLOR },
|
|
87
|
+
{ key: "unshaped", label: "Not shaped", value: unshaped, color: UNSHAPED_COLOR },
|
|
88
|
+
]}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
<BarList items={items} />
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</Card>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { formatCompact, formatCost, formatInteger } from "../data/formatters";
|
|
2
|
+
import type { SpotlightCutEconomics, SpotlightDashboardStats } from "../types";
|
|
3
|
+
import { Card } from "../ui";
|
|
4
|
+
import { foldTools, formatSignedCost, OTHER_TOOLS, toolIcon, toolsBySaving } from "./meta";
|
|
5
|
+
|
|
6
|
+
export interface EconomicsProps {
|
|
7
|
+
stats: SpotlightDashboardStats;
|
|
8
|
+
stale: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Tint strength of the strongest cell; the rest scale with √|net| so the sign reads first, size second, and one
|
|
13
|
+
* outlier does not wash out every other cell.
|
|
14
|
+
*/
|
|
15
|
+
const MAX_TINT = 30;
|
|
16
|
+
const MIN_TINT = 5;
|
|
17
|
+
|
|
18
|
+
/** Narrowest tool column before the table scrolls sideways instead of squeezing its cells. */
|
|
19
|
+
const TOOL_COLUMN_PX = 120;
|
|
20
|
+
|
|
21
|
+
function cellTitle(cell: SpotlightCutEconomics): string {
|
|
22
|
+
return [
|
|
23
|
+
`${cell.tool}: ${formatInteger(cell.cuts)} cuts`,
|
|
24
|
+
`Saved ${formatCompact(cell.savedTokens)} tokens (${formatCost(cell.savedUsd)})`,
|
|
25
|
+
`Fetched back ${formatCompact(cell.recoveredTokens)} tokens (${formatCost(cell.recoveryUsd)})`,
|
|
26
|
+
`${formatInteger(cell.extraTurns)} extra turns (${formatCost(cell.turnUsd)})`,
|
|
27
|
+
`Net ${formatSignedCost(cell.netUsd)}`,
|
|
28
|
+
].join("\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The folded tools' cells summed into an `other` cell. */
|
|
32
|
+
function otherCell(cells: readonly SpotlightCutEconomics[]): SpotlightCutEconomics | undefined {
|
|
33
|
+
if (cells.length === 0) return undefined;
|
|
34
|
+
const sum: SpotlightCutEconomics = {
|
|
35
|
+
tool: OTHER_TOOLS,
|
|
36
|
+
cuts: 0,
|
|
37
|
+
savedTokens: 0,
|
|
38
|
+
recoveredTokens: 0,
|
|
39
|
+
extraTurns: 0,
|
|
40
|
+
savedUsd: 0,
|
|
41
|
+
recoveryUsd: 0,
|
|
42
|
+
turnUsd: 0,
|
|
43
|
+
netUsd: 0,
|
|
44
|
+
extraTurnsPer100Cuts: 0,
|
|
45
|
+
netUsdPer100Cuts: 0,
|
|
46
|
+
};
|
|
47
|
+
for (const cell of cells) {
|
|
48
|
+
sum.cuts += cell.cuts;
|
|
49
|
+
sum.savedTokens += cell.savedTokens;
|
|
50
|
+
sum.recoveredTokens += cell.recoveredTokens;
|
|
51
|
+
sum.extraTurns += cell.extraTurns;
|
|
52
|
+
sum.savedUsd += cell.savedUsd;
|
|
53
|
+
sum.recoveryUsd += cell.recoveryUsd;
|
|
54
|
+
sum.turnUsd += cell.turnUsd;
|
|
55
|
+
sum.netUsd += cell.netUsd;
|
|
56
|
+
}
|
|
57
|
+
if (sum.cuts > 0) {
|
|
58
|
+
sum.extraTurnsPer100Cuts = (sum.extraTurns * 100) / sum.cuts;
|
|
59
|
+
sum.netUsdPer100Cuts = (sum.netUsd * 100) / sum.cuts;
|
|
60
|
+
}
|
|
61
|
+
return sum;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Per tool: what a hundred cuts earn after fetch-backs, extra turns and Spotlight's bill. Tools with cuts, the
|
|
66
|
+
* largest saving first; past the top ones the rest sum into one `other` column.
|
|
67
|
+
*/
|
|
68
|
+
export function Economics({ stats, stale }: EconomicsProps) {
|
|
69
|
+
const cut = new Set(stats.economics.filter(cell => cell.cuts > 0).map(cell => cell.tool));
|
|
70
|
+
const { listed, rest } = foldTools(toolsBySaving(stats).filter(tool => cut.has(tool)));
|
|
71
|
+
const folded = new Set(rest);
|
|
72
|
+
const cells = new Map(stats.economics.filter(cell => !folded.has(cell.tool)).map(cell => [cell.tool, cell]));
|
|
73
|
+
const other = otherCell(stats.economics.filter(cell => folded.has(cell.tool)));
|
|
74
|
+
if (other) cells.set(OTHER_TOOLS, other);
|
|
75
|
+
const tools = rest.length > 0 ? [...listed, OTHER_TOOLS] : listed;
|
|
76
|
+
let maxAbs = 0;
|
|
77
|
+
for (const cell of cells.values()) if (cell.cuts > 0) maxAbs = Math.max(maxAbs, Math.abs(cell.netUsdPer100Cuts));
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Card
|
|
81
|
+
index={3}
|
|
82
|
+
title="What cuts earn, by tool"
|
|
83
|
+
description="Net dollars per 100 cuts after fetch-backs, extra turns and Spotlight's own bill"
|
|
84
|
+
actions={
|
|
85
|
+
maxAbs > 0 && (
|
|
86
|
+
<span className="spot-econ-scale" aria-hidden="true">
|
|
87
|
+
<span className="num">{formatSignedCost(-maxAbs)}</span>
|
|
88
|
+
<span className="spot-econ-ramp" />
|
|
89
|
+
<span className="num">{formatSignedCost(maxAbs)}</span>
|
|
90
|
+
</span>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
stale={stale}
|
|
94
|
+
footer="Shortened results only; held-out ones save nothing. Every fetch-back counts against the cut here, the share held-out results show comes back anyway included, so this is the cautious view. An extra turn is a request whose every tool call fetched back output these cuts hid. Dollars price saved and fetched-back tokens as carried context, like Compounding."
|
|
95
|
+
>
|
|
96
|
+
{tools.length === 0 ? (
|
|
97
|
+
<div className="empty">No shortened results in this range.</div>
|
|
98
|
+
) : (
|
|
99
|
+
<div className="spot-econ-scroll">
|
|
100
|
+
<table className="spot-econ" style={{ minWidth: `${tools.length * TOOL_COLUMN_PX}px` }}>
|
|
101
|
+
<thead>
|
|
102
|
+
<tr>
|
|
103
|
+
{tools.map(tool => {
|
|
104
|
+
const Icon = toolIcon(tool);
|
|
105
|
+
return (
|
|
106
|
+
<th key={tool} scope="col" title={tool === OTHER_TOOLS ? rest.join(", ") : tool}>
|
|
107
|
+
<span className="row spot-econ-tool">
|
|
108
|
+
<Icon size={13} aria-hidden="true" />
|
|
109
|
+
<span className="spot-econ-tool-name">{tool}</span>
|
|
110
|
+
</span>
|
|
111
|
+
</th>
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
114
|
+
</tr>
|
|
115
|
+
</thead>
|
|
116
|
+
<tbody>
|
|
117
|
+
<tr>
|
|
118
|
+
{tools.map(tool => {
|
|
119
|
+
const cell = cells.get(tool);
|
|
120
|
+
if (cell === undefined || cell.cuts === 0) {
|
|
121
|
+
return (
|
|
122
|
+
<td key={tool}>
|
|
123
|
+
<div className="spot-econ-cell" data-empty="true">
|
|
124
|
+
<span className="dim">No cuts</span>
|
|
125
|
+
</div>
|
|
126
|
+
</td>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const net = cell.netUsdPer100Cuts;
|
|
130
|
+
const tint =
|
|
131
|
+
maxAbs > 0 ? MIN_TINT + Math.sqrt(Math.abs(net) / maxAbs) * (MAX_TINT - MIN_TINT) : 0;
|
|
132
|
+
const tone = net < 0 ? "bad" : net > 0 ? "ok" : "neutral";
|
|
133
|
+
return (
|
|
134
|
+
<td key={tool}>
|
|
135
|
+
<div
|
|
136
|
+
className="spot-econ-cell"
|
|
137
|
+
data-tone={tone}
|
|
138
|
+
style={{ "--tint": `${tint}%` }}
|
|
139
|
+
title={cellTitle(cell)}
|
|
140
|
+
>
|
|
141
|
+
<span className="spot-econ-net num">{formatSignedCost(net)}</span>
|
|
142
|
+
<dl className="spot-econ-facts">
|
|
143
|
+
<div>
|
|
144
|
+
<dt>cuts</dt>
|
|
145
|
+
<dd className="num">{formatInteger(cell.cuts)}</dd>
|
|
146
|
+
</div>
|
|
147
|
+
<div>
|
|
148
|
+
<dt>saved</dt>
|
|
149
|
+
<dd className="num">{formatCompact(cell.savedTokens)}</dd>
|
|
150
|
+
</div>
|
|
151
|
+
<div>
|
|
152
|
+
<dt>extra turns /100</dt>
|
|
153
|
+
<dd className="num">{cell.extraTurnsPer100Cuts.toFixed(1)}</dd>
|
|
154
|
+
</div>
|
|
155
|
+
</dl>
|
|
156
|
+
</div>
|
|
157
|
+
</td>
|
|
158
|
+
);
|
|
159
|
+
})}
|
|
160
|
+
</tr>
|
|
161
|
+
</tbody>
|
|
162
|
+
</table>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
</Card>
|
|
166
|
+
);
|
|
167
|
+
}
|