@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,149 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { niceScale } from "./Chart";
|
|
3
|
+
|
|
4
|
+
/** One estimate on a row: a point and its interval. */
|
|
5
|
+
export interface DotWhiskerMark {
|
|
6
|
+
key: string;
|
|
7
|
+
value: number | null;
|
|
8
|
+
lo: number | null;
|
|
9
|
+
hi: number | null;
|
|
10
|
+
color: string;
|
|
11
|
+
/** `dot` (default) filled; `ring` hollow (a counterfactual); `diamond` a reference level; `square` a variant. */
|
|
12
|
+
shape?: "dot" | "ring" | "diamond" | "square";
|
|
13
|
+
/** Vertical offset from the row's center in px, to keep several marks of one row apart. */
|
|
14
|
+
offset?: number;
|
|
15
|
+
/** Hover text naming the mark. */
|
|
16
|
+
title?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DotWhiskerRow {
|
|
20
|
+
key: string;
|
|
21
|
+
label: ReactNode;
|
|
22
|
+
marks: readonly DotWhiskerMark[];
|
|
23
|
+
/** Right-column figure; defaults to the first mark's value. */
|
|
24
|
+
figure?: ReactNode;
|
|
25
|
+
/** Small print under the figure; defaults to the first mark's interval. */
|
|
26
|
+
detail?: ReactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DotWhiskerGuide {
|
|
30
|
+
key: string;
|
|
31
|
+
value: number;
|
|
32
|
+
color: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A shaded span through every row (e.g. a reference's interval). */
|
|
36
|
+
export interface DotWhiskerBand {
|
|
37
|
+
lo: number;
|
|
38
|
+
hi: number;
|
|
39
|
+
color: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DotWhiskerProps {
|
|
43
|
+
rows: readonly DotWhiskerRow[];
|
|
44
|
+
/** Vertical guides through every row (e.g. chance levels). */
|
|
45
|
+
guides?: readonly DotWhiskerGuide[];
|
|
46
|
+
band?: DotWhiskerBand;
|
|
47
|
+
format: (value: number) => string;
|
|
48
|
+
/** Axis tick formatter; defaults to `format`. */
|
|
49
|
+
formatTick?: (value: number) => string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Point estimates with their intervals on one shared axis that always spans
|
|
54
|
+
* zero (and extends below it for signed values), one row per group. Layout is
|
|
55
|
+
* percentage-based, so it never measures the DOM.
|
|
56
|
+
*/
|
|
57
|
+
export function DotWhisker({ rows, guides, band, format, formatTick }: DotWhiskerProps) {
|
|
58
|
+
let max = 0;
|
|
59
|
+
let min = 0;
|
|
60
|
+
const extend = (v: number | null) => {
|
|
61
|
+
if (v === null) return;
|
|
62
|
+
max = Math.max(max, v);
|
|
63
|
+
min = Math.min(min, v);
|
|
64
|
+
};
|
|
65
|
+
for (const row of rows) {
|
|
66
|
+
for (const mark of row.marks) {
|
|
67
|
+
extend(mark.value);
|
|
68
|
+
extend(mark.lo);
|
|
69
|
+
extend(mark.hi);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const guide of guides ?? []) extend(guide.value);
|
|
73
|
+
if (band) {
|
|
74
|
+
extend(band.lo);
|
|
75
|
+
extend(band.hi);
|
|
76
|
+
}
|
|
77
|
+
const scale = niceScale(max, min);
|
|
78
|
+
const span = scale.max - scale.min;
|
|
79
|
+
const at = (v: number) => ((Math.min(scale.max, Math.max(scale.min, v)) - scale.min) / span) * 100;
|
|
80
|
+
const pct = (v: number) => `${at(v)}%`;
|
|
81
|
+
const underlay = (
|
|
82
|
+
<>
|
|
83
|
+
{band && (
|
|
84
|
+
<span
|
|
85
|
+
className="dw-band"
|
|
86
|
+
style={{ left: pct(band.lo), width: `${at(band.hi) - at(band.lo)}%`, "--band": band.color }}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
{guides?.map(guide => (
|
|
90
|
+
<span key={guide.key} className="dw-guide" style={{ left: pct(guide.value), "--guide": guide.color }} />
|
|
91
|
+
))}
|
|
92
|
+
</>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="dw">
|
|
97
|
+
{rows.map(row => {
|
|
98
|
+
const first = row.marks[0];
|
|
99
|
+
const interval =
|
|
100
|
+
first !== undefined && first.lo !== null && first.hi !== null
|
|
101
|
+
? `${format(first.lo)}–${format(first.hi)}`
|
|
102
|
+
: "";
|
|
103
|
+
return (
|
|
104
|
+
<div key={row.key} className="dw-row">
|
|
105
|
+
<div className="dw-label">{row.label}</div>
|
|
106
|
+
<div className="dw-track">
|
|
107
|
+
{underlay}
|
|
108
|
+
{row.marks.map(mark => (
|
|
109
|
+
<span
|
|
110
|
+
key={mark.key}
|
|
111
|
+
className="dw-mark"
|
|
112
|
+
data-shape={mark.shape ?? "dot"}
|
|
113
|
+
style={{ "--dot": mark.color, "--dy": `${mark.offset ?? 0}px` }}
|
|
114
|
+
>
|
|
115
|
+
{mark.lo !== null && mark.hi !== null && (
|
|
116
|
+
<span
|
|
117
|
+
className="dw-whisker"
|
|
118
|
+
style={{ left: pct(mark.lo), width: `${at(mark.hi) - at(mark.lo)}%` }}
|
|
119
|
+
/>
|
|
120
|
+
)}
|
|
121
|
+
{mark.value !== null && (
|
|
122
|
+
<span className="dw-dot" style={{ left: pct(mark.value) }} title={mark.title} />
|
|
123
|
+
)}
|
|
124
|
+
</span>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
<div className="dw-value">
|
|
128
|
+
<span className="num">
|
|
129
|
+
{row.figure ?? (first?.value === null || first === undefined ? "–" : format(first.value))}
|
|
130
|
+
</span>
|
|
131
|
+
<span className="dw-detail">{row.detail ?? interval}</span>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
})}
|
|
136
|
+
<div className="dw-row dw-axis" aria-hidden="true">
|
|
137
|
+
<div />
|
|
138
|
+
<div className="dw-track">
|
|
139
|
+
{scale.ticks.map(t => (
|
|
140
|
+
<span key={t} className="dw-tick" style={{ left: pct(t) }}>
|
|
141
|
+
{(formatTick ?? format)(t)}
|
|
142
|
+
</span>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
<div />
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -10,18 +10,19 @@ export interface SparklineProps {
|
|
|
10
10
|
area?: boolean;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
/** Axis-free trend line for stat tiles and table cells. */
|
|
13
|
+
/** Axis-free trend line for stat tiles and table cells; negative values dip below the zero line. */
|
|
14
14
|
export function Sparkline({ values, color = "var(--chart-primary)", height = 28, width, area = true }: SparklineProps) {
|
|
15
15
|
const [ref, measured] = useWidth<HTMLDivElement>();
|
|
16
16
|
const w = width ?? measured;
|
|
17
17
|
const max = Math.max(0, ...values);
|
|
18
|
+
const min = Math.min(0, ...values);
|
|
18
19
|
const n = values.length;
|
|
19
20
|
let line = "";
|
|
20
21
|
if (w > 0 && n > 0) {
|
|
21
22
|
const step = n > 1 ? w / (n - 1) : 0;
|
|
22
23
|
for (let i = 0; i < n; i++) {
|
|
23
24
|
const x = n > 1 ? i * step : w / 2;
|
|
24
|
-
const y = max >
|
|
25
|
+
const y = max > min ? height - 2 - ((values[i] - min) / (max - min)) * (height - 4) : height - 2;
|
|
25
26
|
line += `${i === 0 ? "M" : "L"}${x.toFixed(1)},${y.toFixed(1)}`;
|
|
26
27
|
}
|
|
27
28
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface WaterfallStep {
|
|
4
|
+
key: string;
|
|
5
|
+
label: ReactNode;
|
|
6
|
+
/** `total` bars start at zero and reset the running total; `delta` bars float from it. */
|
|
7
|
+
kind: "total" | "delta";
|
|
8
|
+
/** A total's level, or a delta's signed change. */
|
|
9
|
+
value: number;
|
|
10
|
+
/** Any CSS background (solid color or gradient). */
|
|
11
|
+
color: string;
|
|
12
|
+
/** Dim figure after the value (e.g. share of the first total). */
|
|
13
|
+
note?: ReactNode;
|
|
14
|
+
title?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WaterfallProps {
|
|
18
|
+
steps: readonly WaterfallStep[];
|
|
19
|
+
/** Formats magnitudes; deltas get their sign from the component. */
|
|
20
|
+
format: (value: number) => string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface PlacedStep extends WaterfallStep {
|
|
24
|
+
from: number;
|
|
25
|
+
to: number;
|
|
26
|
+
/** Running total before this step (the connector's position). */
|
|
27
|
+
before: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Horizontal bridge chart: a total, the changes that eat into it, and the
|
|
32
|
+
* total left at the end. Rows read top to bottom; dashed connectors carry the
|
|
33
|
+
* running total from one bar to the next. Pure layout (percentages), so it
|
|
34
|
+
* never measures the DOM and reflows with its column.
|
|
35
|
+
*/
|
|
36
|
+
export function Waterfall({ steps, format }: WaterfallProps) {
|
|
37
|
+
let running = 0;
|
|
38
|
+
const placed: PlacedStep[] = steps.map(step => {
|
|
39
|
+
const before = running;
|
|
40
|
+
if (step.kind === "total") {
|
|
41
|
+
running = step.value;
|
|
42
|
+
return { ...step, from: 0, to: step.value, before };
|
|
43
|
+
}
|
|
44
|
+
running += step.value;
|
|
45
|
+
return { ...step, from: before, to: running, before };
|
|
46
|
+
});
|
|
47
|
+
let lo = 0;
|
|
48
|
+
let hi = 0;
|
|
49
|
+
for (const step of placed) {
|
|
50
|
+
lo = Math.min(lo, step.from, step.to);
|
|
51
|
+
hi = Math.max(hi, step.from, step.to);
|
|
52
|
+
}
|
|
53
|
+
const span = hi - lo || 1;
|
|
54
|
+
const pct = (v: number) => ((v - lo) / span) * 100;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<ol className="waterfall">
|
|
58
|
+
{placed.map((step, i) => {
|
|
59
|
+
const left = pct(Math.min(step.from, step.to));
|
|
60
|
+
const width = Math.abs(pct(step.to) - pct(step.from));
|
|
61
|
+
const sign = step.kind === "delta" ? (step.value < 0 ? "−" : "+") : step.value < 0 ? "−" : "";
|
|
62
|
+
return (
|
|
63
|
+
<li key={step.key} className="waterfall-row" data-kind={step.kind} title={step.title}>
|
|
64
|
+
<span className="waterfall-label">{step.label}</span>
|
|
65
|
+
<span className="waterfall-track">
|
|
66
|
+
{lo < 0 && <span className="waterfall-zero" style={{ left: `${pct(0)}%` }} />}
|
|
67
|
+
{i > 0 && <span className="waterfall-link" style={{ left: `${pct(step.before)}%` }} />}
|
|
68
|
+
<span
|
|
69
|
+
className="waterfall-bar"
|
|
70
|
+
data-empty={step.value === 0}
|
|
71
|
+
style={{ left: `${left}%`, width: `${width}%`, "--bar": step.color }}
|
|
72
|
+
/>
|
|
73
|
+
</span>
|
|
74
|
+
<span className="waterfall-value">
|
|
75
|
+
<span className="num">
|
|
76
|
+
{sign}
|
|
77
|
+
{format(Math.abs(step.value))}
|
|
78
|
+
</span>
|
|
79
|
+
{step.note !== undefined && <span className="waterfall-note">{step.note}</span>}
|
|
80
|
+
</span>
|
|
81
|
+
</li>
|
|
82
|
+
);
|
|
83
|
+
})}
|
|
84
|
+
</ol>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -11,13 +11,20 @@ export interface ChartSeries {
|
|
|
11
11
|
axis?: "left" | "right";
|
|
12
12
|
/** Diagonal hatching instead of a solid fill (bars/areas). */
|
|
13
13
|
pattern?: "hatch";
|
|
14
|
-
/** Dashed stroke (lines). */
|
|
14
|
+
/** Dashed stroke (lines, area outlines). */
|
|
15
15
|
dashed?: boolean;
|
|
16
16
|
/** Leave this series out of the tooltip (e.g. a trend line that repeats a bar). */
|
|
17
17
|
tooltip?: false;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
/** `dots` marks each value with a point and no connecting segment (raw samples under a trend line). */
|
|
21
|
+
export type ChartKind = "bars" | "area" | "line" | "dots";
|
|
22
|
+
|
|
23
|
+
/** Vertical guide at an x slot (e.g. an experiment start or a compaction). */
|
|
24
|
+
export interface SlotMarker {
|
|
25
|
+
index: number;
|
|
26
|
+
label?: string;
|
|
27
|
+
}
|
|
21
28
|
|
|
22
29
|
/** Horizontal guide drawn across the plot (e.g. a 100% capacity line). */
|
|
23
30
|
export interface ReferenceLine {
|
package/src/client/data/query.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* can resolve to that same object; an unchanged reference does not re-render.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { toError } from "@oh-my-pi/pi-utils/type-guards";
|
|
18
19
|
import { startTransition, useCallback, useEffect, useReducer, useRef, useState } from "react";
|
|
19
20
|
import { currentLiveVersion, useLiveVersion } from "./live";
|
|
20
21
|
import { cachedEntry, type Inflight, inflightRequest, loadQuery, type QueryFetcher, releaseQuery } from "./query-store";
|
|
@@ -60,6 +61,7 @@ export function useQuery<T>(key: readonly unknown[], fetcher: QueryFetcher<T>, o
|
|
|
60
61
|
const version = useLiveVersion(enabled);
|
|
61
62
|
|
|
62
63
|
const [, rerender] = useReducer((n: number) => n + 1, 0);
|
|
64
|
+
const [settled, recheck] = useReducer((n: number) => n + 1, 0);
|
|
63
65
|
const [error, setError] = useState<{ key: string; error: Error } | null>(null);
|
|
64
66
|
const [refreshing, setRefreshing] = useState(false);
|
|
65
67
|
|
|
@@ -82,8 +84,10 @@ export function useQuery<T>(key: readonly unknown[], fetcher: QueryFetcher<T>, o
|
|
|
82
84
|
waiting.current = { key: targetKey, request };
|
|
83
85
|
// Join the new request before leaving the old one, so a shared request isn't aborted in between.
|
|
84
86
|
if (current) releaseQuery(current.key, current.request);
|
|
87
|
+
let succeeded = false;
|
|
85
88
|
request.promise
|
|
86
89
|
.then(data => {
|
|
90
|
+
succeeded = true;
|
|
87
91
|
if (keyRef.current !== targetKey) return;
|
|
88
92
|
// Revalidated to the very same object: nothing on screen changes.
|
|
89
93
|
if (data === shownBefore) {
|
|
@@ -97,11 +101,15 @@ export function useQuery<T>(key: readonly unknown[], fetcher: QueryFetcher<T>, o
|
|
|
97
101
|
})
|
|
98
102
|
.catch((err: unknown) => {
|
|
99
103
|
if (keyRef.current !== targetKey || request.controller.signal.aborted) return;
|
|
100
|
-
setError({ key: targetKey, error:
|
|
104
|
+
setError({ key: targetKey, error: toError(err) });
|
|
101
105
|
})
|
|
102
106
|
.finally(() => {
|
|
103
|
-
if (waiting.current?.request
|
|
104
|
-
|
|
107
|
+
if (waiting.current?.request !== request) return;
|
|
108
|
+
waiting.current = null;
|
|
109
|
+
if (keyRef.current !== targetKey) return;
|
|
110
|
+
setRefreshing(false);
|
|
111
|
+
// A newer live version may have arrived while this older request was in flight.
|
|
112
|
+
if (succeeded && (cachedEntry(targetKey)?.version ?? -1) < currentLiveVersion()) recheck();
|
|
105
113
|
});
|
|
106
114
|
}, []);
|
|
107
115
|
|
|
@@ -128,7 +136,7 @@ export function useQuery<T>(key: readonly unknown[], fetcher: QueryFetcher<T>, o
|
|
|
128
136
|
}
|
|
129
137
|
const timer = setTimeout(() => run(keyString), wait);
|
|
130
138
|
return () => clearTimeout(timer);
|
|
131
|
-
}, [keyString, enabled, version, run, leave]);
|
|
139
|
+
}, [keyString, enabled, version, settled, run, leave]);
|
|
132
140
|
|
|
133
141
|
useEffect(() => {
|
|
134
142
|
if (!enabled || !pollMs) return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "react";
|
|
2
2
|
|
|
3
3
|
declare module "react" {
|
|
4
|
-
/** Custom properties (`--
|
|
4
|
+
/** Custom properties (`--kind`, `--i` …) in `style` objects, which React passes through to the element. */
|
|
5
5
|
interface CSSProperties {
|
|
6
6
|
[property: `--${string}`]: string | number | undefined;
|
|
7
7
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { ScanSearch } from "lucide-react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { getSpotlightDashboardStats } from "../api";
|
|
4
|
+
import { useQuery } from "../data/query";
|
|
5
|
+
import { rangeMeta } from "../data/range";
|
|
6
|
+
import { Compounding } from "../spotlight/Compounding";
|
|
7
|
+
import { Coverage } from "../spotlight/Coverage";
|
|
8
|
+
import { Economics } from "../spotlight/Economics";
|
|
9
|
+
import { FetchBack, FetchBackGaps } from "../spotlight/FetchBack";
|
|
10
|
+
import { SpotlightHero } from "../spotlight/Hero";
|
|
11
|
+
import { Overhead } from "../spotlight/Overhead";
|
|
12
|
+
import { Ranking } from "../spotlight/Ranking";
|
|
13
|
+
import { ResultsFeed } from "../spotlight/ResultsFeed";
|
|
14
|
+
import { SavingsOverTime, WhereSavingsWent } from "../spotlight/Savings";
|
|
15
|
+
import { ShareTrend } from "../spotlight/ShareTrend";
|
|
16
|
+
import { Steps } from "../spotlight/Steps";
|
|
17
|
+
import { TimeCost } from "../spotlight/TimeCost";
|
|
18
|
+
import type { SpotlightAgentScope, TimeRange } from "../types";
|
|
19
|
+
import { Card, ChartSkeleton, PageHeader, QueryView, Segmented } from "../ui";
|
|
20
|
+
import "../spotlight/spotlight.css";
|
|
21
|
+
|
|
22
|
+
export interface SpotlightRouteProps {
|
|
23
|
+
active: boolean;
|
|
24
|
+
range: TimeRange;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const AGENT_OPTIONS = [
|
|
28
|
+
{ value: "all", label: "All", title: "Every experiment session, subagents and advisors included" },
|
|
29
|
+
{ value: "main", label: "Main", title: "Sessions you drove: the main agent only" },
|
|
30
|
+
{ value: "subagent", label: "Subagents", title: "Sessions the task tool spawned" },
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
/** Prose for the scope in empty states ("main-agent "). */
|
|
34
|
+
const AGENT_SCOPE_PROSE: Record<SpotlightAgentScope, string> = { all: "", main: "main-agent ", subagent: "subagent " };
|
|
35
|
+
|
|
36
|
+
/** The page reads as four questions, in order; each heading opens the cards that answer one. */
|
|
37
|
+
function SectionHeading({ id, title, children }: { id: string; title: string; children: string }) {
|
|
38
|
+
return (
|
|
39
|
+
<header className="spot-section rise" id={id}>
|
|
40
|
+
<h2 className="spot-section-title">{title}</h2>
|
|
41
|
+
<p className="spot-section-copy">{children}</p>
|
|
42
|
+
</header>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function SpotlightRoute({ active, range }: SpotlightRouteProps) {
|
|
47
|
+
// Every experiment session shapes, subagents included; scoping keeps their very different workloads apart.
|
|
48
|
+
const [agent, setAgent] = useState<SpotlightAgentScope>("all");
|
|
49
|
+
const stats = useQuery(
|
|
50
|
+
["spotlight", range, agent],
|
|
51
|
+
({ signal }) => getSpotlightDashboardStats(range, agent, signal),
|
|
52
|
+
{ enabled: active },
|
|
53
|
+
);
|
|
54
|
+
const meta = rangeMeta(range);
|
|
55
|
+
const experimentStart = stats.data?.experimentStart ?? null;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="page spot-page">
|
|
59
|
+
<PageHeader
|
|
60
|
+
title="Spotlight"
|
|
61
|
+
description={`What shaping tool output saved in ${meta.windowLabel} against what pi would have shown unshaped, after taking back what the agent fetched again because of a cut.`}
|
|
62
|
+
actions={<Segmented aria-label="Sessions" options={AGENT_OPTIONS} value={agent} onChange={setAgent} />}
|
|
63
|
+
/>
|
|
64
|
+
|
|
65
|
+
<QueryView
|
|
66
|
+
query={stats}
|
|
67
|
+
skeleton={<ChartSkeleton height={220} />}
|
|
68
|
+
isEmpty={data => data.sessions === 0 && data.recent.length === 0}
|
|
69
|
+
empty={
|
|
70
|
+
<Card>
|
|
71
|
+
<div className="spot-empty">
|
|
72
|
+
<span className="spot-empty-icon" aria-hidden="true">
|
|
73
|
+
<ScanSearch size={20} />
|
|
74
|
+
</span>
|
|
75
|
+
<h2 className="spot-empty-title">
|
|
76
|
+
{experimentStart === null
|
|
77
|
+
? "The Spotlight experiment hasn't run yet"
|
|
78
|
+
: `No ${AGENT_SCOPE_PROSE[agent]}experiment sessions in ${meta.windowLabel}`}
|
|
79
|
+
</h2>
|
|
80
|
+
<p className="spot-empty-hint">
|
|
81
|
+
{experimentStart === null ? (
|
|
82
|
+
<>
|
|
83
|
+
Turn it on with <code>spotlight.experiment.enabled</code> in your settings or{" "}
|
|
84
|
+
<code>PI_SPOTLIGHT_EXPERIMENT=1</code>. Every session then shapes tool output, a random
|
|
85
|
+
share of results is held out unshaped as the counterfactual, and this page tracks what the
|
|
86
|
+
cuts save as you work.
|
|
87
|
+
</>
|
|
88
|
+
) : agent === "all" ? (
|
|
89
|
+
"Try a longer range, or start a session with the experiment enabled."
|
|
90
|
+
) : (
|
|
91
|
+
"Try a longer range, or switch to All sessions."
|
|
92
|
+
)}
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
</Card>
|
|
96
|
+
}
|
|
97
|
+
>
|
|
98
|
+
{data => (
|
|
99
|
+
<>
|
|
100
|
+
<SpotlightHero stats={data} windowLabel={meta.windowLabel} stale={stats.stale} />
|
|
101
|
+
|
|
102
|
+
<SectionHeading id="spot-saved" title="What it saved">
|
|
103
|
+
Where the saving came from, what fetch-backs took back, and what later requests no longer carry.
|
|
104
|
+
</SectionHeading>
|
|
105
|
+
<div className="grid grid-main-side">
|
|
106
|
+
<SavingsOverTime stats={data} range={range} stale={stats.stale} />
|
|
107
|
+
<WhereSavingsWent stats={data} stale={stats.stale} />
|
|
108
|
+
</div>
|
|
109
|
+
<Compounding stats={data} active={active} stale={stats.stale} />
|
|
110
|
+
<ShareTrend stats={data} range={range} stale={stats.stale} />
|
|
111
|
+
|
|
112
|
+
<SectionHeading id="spot-cost" title="What it cost">
|
|
113
|
+
Agent time, Spotlight's latency and failures, and whether shaped sessions got worse.
|
|
114
|
+
</SectionHeading>
|
|
115
|
+
<TimeCost stats={data} stale={stats.stale} />
|
|
116
|
+
<Overhead stats={data} stale={stats.stale} />
|
|
117
|
+
|
|
118
|
+
<SectionHeading id="spot-work" title="Is Spotlight doing the work?">
|
|
119
|
+
Whether its heat finds the lines the agent needs, and whether its cuts come back less than cuts
|
|
120
|
+
made without it.
|
|
121
|
+
</SectionHeading>
|
|
122
|
+
<Ranking stats={data} stale={stats.stale} />
|
|
123
|
+
<FetchBack stats={data} stale={stats.stale} />
|
|
124
|
+
<FetchBackGaps stats={data} stale={stats.stale} />
|
|
125
|
+
|
|
126
|
+
<SectionHeading id="spot-tune" title="Where to tune">
|
|
127
|
+
Which steps, sizes and tools pay, and which tools Spotlight doesn't shape yet.
|
|
128
|
+
</SectionHeading>
|
|
129
|
+
<Steps stats={data} stale={stats.stale} />
|
|
130
|
+
<Economics stats={data} stale={stats.stale} />
|
|
131
|
+
<Coverage stats={data} stale={stats.stale} />
|
|
132
|
+
<ResultsFeed rows={data.recent} stale={stats.stale} />
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
135
|
+
</QueryView>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|