@tangle-network/charts 0.14.0 → 0.15.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/README.md +28 -11
- package/dist/{timeline-BvVxPTGp.d.ts → flow-trace-B5dlYNsD.d.ts} +30 -1
- package/dist/index.d.ts +2 -2
- package/dist/react/flow-waterfall.css +76 -0
- package/dist/react/index.d.ts +32 -6
- package/dist/react/index.js +518 -15
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -4,15 +4,9 @@ Benchmark figures as static SVG and HTML tables, plus optional React timing prim
|
|
|
4
4
|
|
|
5
5
|
The React entry also exports `DayBars`, `HBars`, `Histogram`, `Lines`, `Meter`, `Spark`, and `Gantt` for live operational pages. Import `@tangle-network/charts/react/live.css` for their layout and set `--chart-live-*` theme tokens on an ancestor. Data, state colors, destination links, labels, and interpretation remain with the caller. `DayBars` accepts an `onOpen(index)` action with keyboard activation.
|
|
6
6
|
|
|
7
|
-
`
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
same `{ startMs, endMs }` window to every row. Hierarchy, selection, attribution,
|
|
11
|
-
formatting and failure interpretation stay with the consumer. The original
|
|
12
|
-
minimum/clipped marker remains visible; its tooltip reports the actual duration.
|
|
13
|
-
Set `--waterfall-timing-width`, `--waterfall-duration-width` and the existing
|
|
14
|
-
`--intel-waterfall-*` colors for the consumer's layout and theme. No stylesheet,
|
|
15
|
-
router or React DOM dependency is required.
|
|
7
|
+
`FlowWaterfall` from `@tangle-network/charts/react` is the shared live trace inspector. It owns hierarchy layout, folding, search, playback and running-span growth. Producers own identity, `parentId`, explicit `status` and observed totals. Import `@tangle-network/charts/react/flow-waterfall.css`; its `fw-*` classes and six-field summary support hosts without a Tailwind build. The React-free root exports the `FlowSpan` and `FlowTrace` types.
|
|
8
|
+
|
|
9
|
+
`WaterfallHeader`, `WaterfallTimeAxis`, `WaterfallBar` and `WaterfallRow` remain available for existing consumers that own their own hierarchy and selection. All use the same `waterfallSpanGeometry` and `waterfallTicks`. Existing fixed-width cells, tones, formatting, attribution slots and unordered-time `—` marks are preserved. Optional fluid tracks, inner-bar colour/class, overlay and clipping fields support the shared inspector without changing existing calls. The primitives were extracted from Intelligence under [brand #211](https://github.com/tangle-network/brand/pull/211); the shared inspector extends that presentation ownership while leaving failure interpretation with producers.
|
|
16
10
|
|
|
17
11
|
Five figures cover the decisions a benchmark reader makes:
|
|
18
12
|
|
|
@@ -44,7 +38,7 @@ phaseWaterfall([{ id: "route", label: "Route", ms: 5 }, { id: "restore", label:
|
|
|
44
38
|
|
|
45
39
|
The producer maps its own blocked or absent states to `no-data`; Charts never assigns a status from a value. `latencyRange` refuses any row below `minN`; when the producer omits those rows, pass their reasons in `excluded` so the note names them. `phaseWaterfall` refuses phases whose sum exceeds the reported total because overlapping spans cannot be drawn as a cumulative waterfall. Its input order is retained, including zero-duration phases. The producer marks known unaccounted leaves with `kind: "unaccounted"`; Charts also marks any difference between their sum and `totalMs`.
|
|
46
40
|
|
|
47
|
-
|
|
41
|
+
Charts owns presentation and interaction; producers own interpretation and status. The library never derives failure or running state from metadata values.
|
|
48
42
|
Ranks, intervals, frontier membership, estimate labels and pass counts come from the producer: a board record, a VerticalBench report or an agent-eval lens.
|
|
49
43
|
A figure function returns a `Refusal` in place of a figure it cannot draw honestly, for example a bar over too few units, an interval that names no method, or a frontier the producer did not compute.
|
|
50
44
|
|
|
@@ -319,8 +313,31 @@ npm run build
|
|
|
319
313
|
|
|
320
314
|
The package-boundary test builds and packs Charts, installs the tarball in an OS-temporary consumer with a normal offline npm install (no peer-omission flags), asserts React/React DOM/React types cannot resolve, and executes the static root without DOM globals. It compares root exports, static SVG/tables, Refusals, report/chart/plot HTML and CSS with the untouched source implementation, then compiles a static-only consumer with `lib: ["ES2022"]`, `types: []` and `skipLibCheck: false`. Only afterward does it supply the installed React peer/types to verify the packed React runtime and declarations. It runs under `npm test`.
|
|
321
315
|
|
|
322
|
-
The component tests cover gap/zero/one/equal/negative cases, controlled identity, keyboard/touch selection, nonvisual values and resize contracts. The jsdom resize check verifies fluid/minimum-width attributes and preserved selection, **not browser-computed layout**; The retained packed-browser fixture
|
|
316
|
+
The component tests cover gap/zero/one/equal/negative cases, controlled identity, keyboard/touch selection, nonvisual values and resize contracts. The jsdom resize check verifies fluid/minimum-width attributes and preserved selection, **not browser-computed layout**; The retained [packed-browser fixture](scripts/charts-browser-smoke.mjs), ported from brand, and its [browser proof](scripts/charts-browser-proof.mjs) check Chromium at 1100px and 390px, keyboard selection, emulated touch, light/dark Brand tokens, reduced motion, and top-layer positioning inside a transformed, paint-contained panel. It passes with React 18 and React 19; the React 18 consumer also compiles the packed declarations with React 18 types. Physical touch hardware and screen-reader audio remain unchecked.
|
|
323
317
|
|
|
324
318
|
### Figure browser proof
|
|
325
319
|
|
|
326
320
|
`node scripts/render-figures.mjs` writes a comparison page from the retained fixtures and approved speed reference. `node scripts/capture-figures.mjs` uses Playwright Chromium to capture all three figures at phone and desktop widths in light and dark themes. The reviewed images are in [`docs/screenshots`](docs/screenshots).
|
|
321
|
+
|
|
322
|
+
## Live trace waterfall
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
import { FlowWaterfall } from '@tangle-network/charts/react';
|
|
326
|
+
import type { FlowTrace } from '@tangle-network/charts';
|
|
327
|
+
import '@tangle-network/charts/react/flow-waterfall.css';
|
|
328
|
+
|
|
329
|
+
// The producer supplies stable identities, explicit status and measured totals.
|
|
330
|
+
<FlowWaterfall trace={trace satisfies FlowTrace} live={running} />;
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Search supports Cmd/Ctrl+F and keeps ancestor context. Folding, five playback speeds, scrubbing and Follow live affect presentation only. Running spans grow between snapshots. Missing cost and authoritative LLM counts stay Unknown. Invalid intervals retain a per-row `—` timing mark, preserving the rest of the trace. The static phase waterfall remains a separate chart for disjoint phases.
|
|
334
|
+
|
|
335
|
+
**Provenance.** FlowWaterfall and its recorded Discovery fixture move from agent-app #1267/#1273 at `78a3e8f7fd9bf93b23afa8290db6d721db544237`; the independent implementation copied no paid reference assets. [Fixture provenance](docs/fixtures/discovery-waterfall-provenance.md) records measured timing, observed token totals and unknowns. The historical [baseline](docs/fixtures/live-playback-rates-baseline.json) and [fixed](docs/fixtures/live-playback-rates-fixed.json) rate measurements are retained. The packed public consumer proof measures all five rates against actual browser wall time during live growth, with ±15% acceptance, using React 18 and 19 at 1100px and 390px.
|
|
336
|
+
|
|
337
|
+
```sh
|
|
338
|
+
npm run build
|
|
339
|
+
CHARTS_REACT_VERSION=18 CHARTS_PROOF_DIR=/tmp/charts-react18 node scripts/charts-browser-proof.mjs --record
|
|
340
|
+
CHARTS_REACT_VERSION=19 CHARTS_PROOF_DIR=/tmp/charts-react19 node scripts/charts-browser-proof.mjs --record
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Consumers: agent-record adapts record provenance; agent-app supplies application trace producers; Creative consumes both. Intelligence already uses the row/axis primitives with its controlled virtualized tree. Tangle Website’s `RecordedRunWaterfall` uses the Header/Row primitives; Blueprint Agent’s execution timeline is a separately owned follow-up. Keep these existing primitives exported. Agent-record’s earlier agent-app dependency increased its installed graph from 134 to 256 package versions (tangle-tools #1674); adopting the lower chart owner removes that framework dependency, with the actual new graph measured by the deployment owner.
|
|
@@ -304,4 +304,33 @@ interface TimelineOptions {
|
|
|
304
304
|
}
|
|
305
305
|
declare function timeline(lanes: TimelineLane[], o: TimelineOptions): Figure | Refusal;
|
|
306
306
|
//#endregion
|
|
307
|
-
|
|
307
|
+
//#region src/flow-trace.d.ts
|
|
308
|
+
/** Producer-owned identities, explicit status and observed totals; no React dependency. */
|
|
309
|
+
interface FlowSpan {
|
|
310
|
+
kind: 'pipeline' | 'model' | 'tool' | 'waiting' | 'handoff';
|
|
311
|
+
/** Stable identity and parent identity for nested, incrementally updated traces. */
|
|
312
|
+
id?: string;
|
|
313
|
+
parentId?: string;
|
|
314
|
+
/** Running spans extend from endMs to the viewer's observation time. */
|
|
315
|
+
status?: 'running' | 'done' | 'failed';
|
|
316
|
+
name: string;
|
|
317
|
+
startMs: number;
|
|
318
|
+
endMs: number;
|
|
319
|
+
approx?: boolean;
|
|
320
|
+
meta?: Record<string, unknown>;
|
|
321
|
+
}
|
|
322
|
+
/** Describe the structure of a flow trace including spans, timing, tokens, cost, and tool calls */
|
|
323
|
+
interface FlowTrace {
|
|
324
|
+
spans: FlowSpan[];
|
|
325
|
+
totalMs: number;
|
|
326
|
+
promptTokens: number;
|
|
327
|
+
completionTokens: number;
|
|
328
|
+
/** Computed when per-token pricing is supplied. */
|
|
329
|
+
costUsd?: number;
|
|
330
|
+
toolCalls: number;
|
|
331
|
+
/** Authoritative LLM request count when supplied; absent means unknown. */
|
|
332
|
+
modelCalls?: number;
|
|
333
|
+
errors?: number;
|
|
334
|
+
}
|
|
335
|
+
//#endregion
|
|
336
|
+
export { CostBasis as A, Tier as B, MarkerShape as C, YTick as D, YTarget as E, IntervalMethod as F, isRefusal as H, MatrixCell as I, RateRow as L, Exclusion as M, Figure as N, BreakdownInput as O, Interval as P, Refusal as R, MarkVerdict as S, YScale as T, TierCounts as V, PointGlyph as _, TimelineSpan as a, ChartMark as b, CHECKER_HUES as c, OUTCOME_COLORS as d, OUTCOME_ORDER as f, PointFields as g, checkerColor as h, TimelineOptions as i, Estimate as j, ComparisonRow as k, ENDING_COLORS as l, TickOutcome as m, FlowTrace as n, TimelineTick as o, RunEnding as p, TimelineLane as r, timeline as s, FlowSpan as t, ENDING_ORDER as u, BandKind as v, XCategory as w, MarkKind as x, ChartBand as y, Setup as z };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as CostBasis, B as Tier, C as MarkerShape, D as YTick, E as YTarget, F as IntervalMethod, H as isRefusal, I as MatrixCell, L as RateRow, M as Exclusion, N as Figure, O as BreakdownInput, P as Interval, R as Refusal, S as MarkVerdict, T as YScale, V as TierCounts, _ as PointGlyph, a as TimelineSpan, b as ChartMark, c as CHECKER_HUES, d as OUTCOME_COLORS, f as OUTCOME_ORDER, g as PointFields, h as checkerColor, i as TimelineOptions, j as Estimate, k as ComparisonRow, l as ENDING_COLORS, m as TickOutcome, n as FlowTrace, o as TimelineTick, p as RunEnding, r as TimelineLane, s as timeline, t as FlowSpan, u as ENDING_ORDER, v as BandKind, w as XCategory, x as MarkKind, y as ChartBand, z as Setup } from "./flow-trace-B5dlYNsD.js";
|
|
2
2
|
//#region src/block.d.ts
|
|
3
3
|
interface RenderFigureOptions {
|
|
4
4
|
/** Chart uses a closed data disclosure; plot keeps only the SVG and accessible data. */
|
|
@@ -344,4 +344,4 @@ interface UnitDotOptions {
|
|
|
344
344
|
*/
|
|
345
345
|
export declare function unitDotLayout(groups: DotGroup[], width: number, height: number, o?: UnitDotOptions): UnitDotLayout;
|
|
346
346
|
//#endregion
|
|
347
|
-
export { type BandKind, type BreakdownInput, type BreakdownOptions, CHECKER_HUES, type ChartBand, type ChartMark, type ComparisonIntervalsOptions, type ComparisonRow, type CostBasis, type CostFrontierOptions, type DotGroup, type DotGroupRect, ENDING_COLORS, ENDING_ORDER, type Estimate, type Exclusion, type Figure, type HeatmapCell, type HeatmapColumn, type HeatmapOptions, type HeatmapRow, type HeatmapStatus, type Interval, type IntervalMethod, type LatencyRangeOptions, type LatencyRangeRow, type MarkKind, type MarkVerdict, type MatrixCell, type MetricBarsOptions, type MetricRow, type MultiplePanel, OUTCOME_COLORS, OUTCOME_ORDER, type Observations, type PhaseWaterfallOptions, type PointFields, type PointGlyph, type RankedRatesOptions, type RateIdentity, type RateRow, type Refusal, type RenderFigureOptions, type RunEnding, type Setup, type SmallMultiplesOptions, type TaskMatrixOptions, type TickOutcome, type Tier, type TierCounts, type TimePoint, type TimeSeriesOptions, type TimeSeriesRow, type TimelineLane, type TimelineOptions, type TimelineSpan, type TimelineTick, type UnitDotLayout, type UnitDotOptions, type WaterfallPhase, type XCategory, type YScale, type YTarget, type YTick, checkerColor, isRefusal, timeline };
|
|
347
|
+
export { type BandKind, type BreakdownInput, type BreakdownOptions, CHECKER_HUES, type ChartBand, type ChartMark, type ComparisonIntervalsOptions, type ComparisonRow, type CostBasis, type CostFrontierOptions, type DotGroup, type DotGroupRect, ENDING_COLORS, ENDING_ORDER, type Estimate, type Exclusion, type Figure, type FlowSpan, type FlowTrace, type HeatmapCell, type HeatmapColumn, type HeatmapOptions, type HeatmapRow, type HeatmapStatus, type Interval, type IntervalMethod, type LatencyRangeOptions, type LatencyRangeRow, type MarkKind, type MarkVerdict, type MatrixCell, type MetricBarsOptions, type MetricRow, type MultiplePanel, OUTCOME_COLORS, OUTCOME_ORDER, type Observations, type PhaseWaterfallOptions, type PointFields, type PointGlyph, type RankedRatesOptions, type RateIdentity, type RateRow, type Refusal, type RenderFigureOptions, type RunEnding, type Setup, type SmallMultiplesOptions, type TaskMatrixOptions, type TickOutcome, type Tier, type TierCounts, type TimePoint, type TimeSeriesOptions, type TimeSeriesRow, type TimelineLane, type TimelineOptions, type TimelineSpan, type TimelineTick, type UnitDotLayout, type UnitDotOptions, type WaterfallPhase, type XCategory, type YScale, type YTarget, type YTick, checkerColor, isRefusal, timeline };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* Standalone surface: consumes Brand/Agent App tokens; no Tailwind build required. */
|
|
2
|
+
.fw-root {
|
|
3
|
+
--waterfall-bar-height: 1rem;
|
|
4
|
+
--waterfall-bar-radius: .125rem;
|
|
5
|
+
--waterfall-duration-width: 100%;
|
|
6
|
+
--intel-waterfall-track: var(--fw-well);
|
|
7
|
+
--fw-text: var(--text-primary, hsl(var(--foreground)));
|
|
8
|
+
--fw-muted: var(--text-muted, hsl(var(--muted-foreground)));
|
|
9
|
+
--fw-surface: var(--bg-card, hsl(var(--background)));
|
|
10
|
+
--fw-border: var(--border-default, hsl(var(--border)));
|
|
11
|
+
--fw-well: var(--md3-surface-container, hsl(var(--secondary)));
|
|
12
|
+
--fw-accent: var(--accent-text, hsl(var(--primary)));
|
|
13
|
+
color: var(--fw-text);
|
|
14
|
+
background: var(--fw-surface);
|
|
15
|
+
border: 1px solid var(--fw-border);
|
|
16
|
+
border-radius: var(--radius-lg, .75rem);
|
|
17
|
+
min-width: 0;
|
|
18
|
+
font: inherit;
|
|
19
|
+
font-size: .8125rem;
|
|
20
|
+
line-height: 1.5;
|
|
21
|
+
}
|
|
22
|
+
.fw-root *, .fw-root *::before, .fw-root *::after { box-sizing: border-box; }
|
|
23
|
+
.fw-root :is(p, dl, dd) { margin: 0; }
|
|
24
|
+
.fw-root .fw-heading { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .5rem; padding: .75rem; border-bottom: 1px solid var(--fw-border); }
|
|
25
|
+
.fw-root .fw-title { display: flex; align-items: center; gap: .5rem; font-weight: 600; font-size: .875rem; }
|
|
26
|
+
.fw-root .fw-search { display: flex; min-width: 0; gap: .25rem; align-items: center; }
|
|
27
|
+
.fw-root .fw-control { appearance: auto; font: inherit; font-size: .75rem; line-height: 1.4; min-height: 2rem; padding: .25rem .5rem; border: 1px solid var(--fw-border); border-radius: var(--radius-sm, .375rem); color: var(--fw-text); background: var(--fw-surface); cursor: pointer; }
|
|
28
|
+
.fw-root .fw-control:hover, .fw-root .fw-toggle:hover { background: var(--fw-well); }
|
|
29
|
+
.fw-root :is(button, input, select, [tabindex]):focus-visible { outline: 2px solid var(--fw-accent); outline-offset: 2px; }
|
|
30
|
+
.fw-root :is(button, input):disabled { opacity: .5; cursor: default; }
|
|
31
|
+
.fw-root .fw-search-input { width: 10rem; min-width: 0; cursor: text; }
|
|
32
|
+
.fw-root .fw-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .75rem 1rem; padding: .75rem; border-bottom: 1px solid var(--fw-border); }
|
|
33
|
+
.fw-root .fw-muted { color: var(--fw-muted); font-size: .75rem; }
|
|
34
|
+
.fw-root .fw-value { margin-top: .125rem; font-family: var(--font-mono, monospace); font-size: .875rem; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
35
|
+
.fw-root .fw-legend { display: flex; flex-wrap: wrap; gap: .25rem .75rem; padding: .5rem .75rem; border-bottom: 1px solid var(--fw-border); color: var(--fw-muted); font-size: .75rem; }
|
|
36
|
+
.fw-root .fw-legend-item { display: inline-flex; align-items: center; gap: .25rem; }
|
|
37
|
+
.fw-root .fw-dot { width: .375rem; height: .375rem; border-radius: 50%; }
|
|
38
|
+
.fw-root .fw-orchestrator { --fw-bar-color: var(--fw-muted); background: var(--fw-bar-color); }
|
|
39
|
+
.fw-root .fw-model { --fw-bar-color: var(--chart-accent, hsl(var(--primary))); background: var(--fw-bar-color); }
|
|
40
|
+
.fw-root .fw-tool { --fw-bar-color: var(--status-cold, hsl(var(--accent))); background: var(--fw-bar-color); }
|
|
41
|
+
.fw-root .fw-waiting { --fw-bar-color: var(--status-stopped, hsl(var(--warning-strong))); background: var(--fw-bar-color); }
|
|
42
|
+
.fw-root .fw-handoff { --fw-bar-color: var(--status-running, hsl(var(--success))); background: var(--fw-bar-color); }
|
|
43
|
+
.fw-root .fw-failed { --fw-bar-color: var(--status-error, hsl(var(--destructive))); background: var(--fw-bar-color); }
|
|
44
|
+
.fw-root .fw-branches { display: flex; gap: .5rem; padding: .5rem .75rem 0; }
|
|
45
|
+
.fw-root .fw-timeline { overflow-x: auto; padding: .75rem; }
|
|
46
|
+
.fw-root .fw-track { min-width: 480px; }
|
|
47
|
+
.fw-root .fw-grid { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 3fr) 70px; align-items: center; gap: .5rem; }
|
|
48
|
+
.fw-root .fw-axis { padding-bottom: .5rem; color: var(--fw-muted); font-size: .75rem; }
|
|
49
|
+
.fw-root .fw-ticks { display: flex; justify-content: space-between; font-family: var(--font-mono, monospace); font-variant-numeric: tabular-nums; }
|
|
50
|
+
.fw-root .fw-align-right { text-align: right; }
|
|
51
|
+
.fw-root .fw-empty { padding: 2rem .5rem; text-align: center; color: var(--fw-muted); font-size: .875rem; }
|
|
52
|
+
.fw-root .fw-row { border-radius: .25rem; padding: .375rem 0; }
|
|
53
|
+
.fw-root .fw-slowest { background: var(--fw-well); outline: 1px solid var(--fw-border); }
|
|
54
|
+
.fw-root .fw-pending { opacity: .4; }
|
|
55
|
+
.fw-root .fw-name { display: flex; align-items: center; min-width: 0; }
|
|
56
|
+
.fw-root .fw-toggle { appearance: none; width: 1.5rem; height: 1.5rem; margin-right: .25rem; flex-shrink: 0; border: 0; border-radius: .25rem; background: transparent; color: var(--fw-text); font: inherit; cursor: pointer; }
|
|
57
|
+
.fw-root .fw-leaf { margin-right: .25rem; width: 1.5rem; flex-shrink: 0; text-align: center; color: var(--fw-muted); }
|
|
58
|
+
.fw-root .fw-span-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .75rem; }
|
|
59
|
+
.fw-root .fw-error-text { color: var(--text-danger, var(--status-error)); }
|
|
60
|
+
.fw-root .fw-bar-track { position: relative; height: 1rem; overflow: hidden; border-radius: .125rem; background: var(--fw-well); }
|
|
61
|
+
.fw-root .fw-bar { position: absolute; top: 0; bottom: 0; border-radius: .125rem; }
|
|
62
|
+
.fw-root .fw-approx { opacity: .7; }
|
|
63
|
+
.fw-root .fw-playhead { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--fw-text); }
|
|
64
|
+
.fw-root .fw-duration { padding-right: .25rem; text-align: right; font-family: var(--font-mono, monospace); font-size: .75rem; font-variant-numeric: tabular-nums; }
|
|
65
|
+
.fw-root .fw-slowest-caption { padding: 0 .75rem .5rem; color: var(--fw-muted); font-size: .75rem; overflow-wrap: anywhere; }
|
|
66
|
+
.fw-root .fw-text { color: var(--fw-text); }
|
|
67
|
+
.fw-root .fw-playback { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; border-top: 1px solid var(--fw-border); padding: .75rem; }
|
|
68
|
+
.fw-root .fw-speed, .fw-root .fw-position { display: flex; align-items: center; gap: .25rem; color: var(--fw-muted); font-size: .75rem; }
|
|
69
|
+
.fw-root .fw-position { flex: 1; min-width: 6rem; gap: .5rem; }
|
|
70
|
+
.fw-root .fw-slider { min-width: 0; flex: 1; accent-color: var(--fw-accent); }
|
|
71
|
+
.fw-root .fw-time { font-family: var(--font-mono, monospace); font-size: .75rem; font-variant-numeric: tabular-nums; color: var(--fw-muted); }
|
|
72
|
+
.fw-root .fw-sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
73
|
+
@media (max-width: 480px) {
|
|
74
|
+
.fw-root .fw-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
75
|
+
.fw-root .fw-position { flex-basis: 100%; }
|
|
76
|
+
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
import { ReactElement, ReactNode } from "react";
|
|
1
|
+
import { C as MarkerShape, D as YTick, E as YTarget, N as Figure, R as Refusal, S as MarkVerdict, T as YScale, _ as PointGlyph, a as TimelineSpan, b as ChartMark, g as PointFields, i as TimelineOptions, m as TickOutcome, n as FlowTrace, o as TimelineTick, r as TimelineLane, v as BandKind, w as XCategory, x as MarkKind, y as ChartBand } from "../flow-trace-B5dlYNsD.js";
|
|
2
|
+
import { CSSProperties, ReactElement, ReactNode } from "react";
|
|
3
3
|
//#region src/react/sparkline.d.ts
|
|
4
4
|
type SparklineDirection = "rising" | "falling" | "flat";
|
|
5
5
|
type SparklineValues = readonly (number | null)[];
|
|
@@ -111,8 +111,10 @@ interface WaterfallTimeAxisProps {
|
|
|
111
111
|
widthPx?: number;
|
|
112
112
|
formatDuration?: (ms: number) => string;
|
|
113
113
|
className?: string;
|
|
114
|
+
/** Fill the embedding grid track; the default retains Intelligence's fixed cell. */
|
|
115
|
+
fluid?: boolean;
|
|
114
116
|
}
|
|
115
|
-
export declare function WaterfallTimeAxis({ windowMs, originMs, widthPx, formatDuration, className }: WaterfallTimeAxisProps): import("react").JSX.Element | null;
|
|
117
|
+
export declare function WaterfallTimeAxis({ windowMs, originMs, widthPx, formatDuration, className, fluid }: WaterfallTimeAxisProps): import("react").JSX.Element | null;
|
|
116
118
|
interface WaterfallBarProps {
|
|
117
119
|
startMs: number;
|
|
118
120
|
endMs: number;
|
|
@@ -120,17 +122,26 @@ interface WaterfallBarProps {
|
|
|
120
122
|
tone?: "default" | "error" | "critical";
|
|
121
123
|
formatDuration?: (ms: number) => string;
|
|
122
124
|
className?: string;
|
|
125
|
+
/** Presentation only; the producer continues to own status and tone. */
|
|
126
|
+
color?: string;
|
|
127
|
+
barClassName?: string;
|
|
128
|
+
overlay?: ReactNode;
|
|
129
|
+
fluid?: boolean;
|
|
130
|
+
/** Clip playback without changing the recorded duration or its tooltip. */
|
|
131
|
+
clipAtMs?: number;
|
|
123
132
|
}
|
|
124
|
-
export declare function WaterfallBar({ startMs, endMs, window, tone, formatDuration, className }: WaterfallBarProps): import("react").JSX.Element;
|
|
133
|
+
export declare function WaterfallBar({ startMs, endMs, window, tone, formatDuration, className, color: suppliedColor, barClassName, overlay, fluid, clipAtMs }: WaterfallBarProps): import("react").JSX.Element;
|
|
125
134
|
interface WaterfallRowProps extends Omit<WaterfallBarProps, "className"> {
|
|
126
135
|
label: ReactNode;
|
|
127
136
|
attribution?: ReactNode;
|
|
128
137
|
timingClassName?: string;
|
|
129
138
|
durationClassName?: string;
|
|
130
139
|
className?: string;
|
|
140
|
+
style?: CSSProperties;
|
|
141
|
+
durationLabel?: ReactNode;
|
|
131
142
|
}
|
|
132
143
|
/** Non-interactive row body; its caller owns treeitems, buttons and selection. */
|
|
133
|
-
export declare function WaterfallRow({ label, attribution, startMs, endMs, window, tone, formatDuration, className, timingClassName, durationClassName }: WaterfallRowProps): import("react").JSX.Element;
|
|
144
|
+
export declare function WaterfallRow({ label, attribution, startMs, endMs, window, tone, formatDuration, className, timingClassName, durationClassName, style, durationLabel, ...bar }: WaterfallRowProps): import("react").JSX.Element;
|
|
134
145
|
interface WaterfallHeaderProps extends WaterfallTimeAxisProps {
|
|
135
146
|
label?: ReactNode;
|
|
136
147
|
attribution?: ReactNode;
|
|
@@ -336,4 +347,19 @@ interface DotMapProps {
|
|
|
336
347
|
export declare const DOT_MAP_CATEGORIES: readonly string[];
|
|
337
348
|
export declare function DotMap({ positions, radii, colors, palette, extent, rects, labels, dimmed, selected, minPixelRadius, onHover, onSelect, describe, height, ariaLabel }: DotMapProps): import("react").JSX.Element;
|
|
338
349
|
//#endregion
|
|
339
|
-
|
|
350
|
+
//#region src/react/flow-waterfall.d.ts
|
|
351
|
+
interface FlowWaterfallProps {
|
|
352
|
+
trace: FlowTrace;
|
|
353
|
+
/** Extend running spans between incoming snapshots. Inferred from span status by default. */
|
|
354
|
+
live?: boolean;
|
|
355
|
+
/** Epoch-ms origin when the producer supplies one; otherwise advance from the latest snapshot. */
|
|
356
|
+
startedAt?: number;
|
|
357
|
+
/** Accessible name also distinguishes multiple waterfalls on one page. */
|
|
358
|
+
label?: string;
|
|
359
|
+
/** Fold all branches on first mount for large traces; search still reveals matches. */
|
|
360
|
+
defaultCollapsed?: boolean;
|
|
361
|
+
}
|
|
362
|
+
/** Shared trace inspector. Data remains producer-owned; playback only clips its presentation. */
|
|
363
|
+
export declare function FlowWaterfall({ trace, live, startedAt, label, defaultCollapsed }: FlowWaterfallProps): import("react").JSX.Element;
|
|
364
|
+
//#endregion
|
|
365
|
+
export type { BandKind, ChartBand, ChartMark, DotMapLabel, DotMapProps, DotMapRect, Figure, FigureAtWidth, FigureViewProps, FlowWaterfallProps, LineSeries, MarkKind, MarkVerdict, MarkerShape, MeterSegment, PointFields, PointGlyph, Refusal, SparklineDirection, SparklineGeometry, SparklineGeometryOptions, SparklineLabelOptions, SparklinePoint, SparklineProps, SparklineValues, StackedBarBucket, StackedBarChartProps, StackedBarSegment, StackedBarSeries, TickOutcome, TimelineLane, TimelineOptions, TimelineSpan, TimelineTick, TimelineViewProps, WaterfallBarProps, WaterfallHeaderProps, WaterfallRowProps, WaterfallSpanGeometry, WaterfallTimeAxisProps, WaterfallWindow, XCategory, YScale, YTarget, YTick };
|
package/dist/react/index.js
CHANGED
|
@@ -653,7 +653,7 @@ const durationStyle = {
|
|
|
653
653
|
fontVariantNumeric: "tabular-nums"
|
|
654
654
|
};
|
|
655
655
|
const ordered = (start, end) => Number.isFinite(start) && Number.isFinite(end) && end >= start;
|
|
656
|
-
function WaterfallTimeAxis({ windowMs, originMs = 0, widthPx = 208, formatDuration = formatWaterfallDuration, className }) {
|
|
656
|
+
function WaterfallTimeAxis({ windowMs, originMs = 0, widthPx = 208, formatDuration = formatWaterfallDuration, className, fluid = false }) {
|
|
657
657
|
if (!ordered(0, windowMs) || !Number.isFinite(originMs)) return null;
|
|
658
658
|
const ticks = waterfallTicks(windowMs, widthPx);
|
|
659
659
|
if (windowMs === 0) return null;
|
|
@@ -662,7 +662,10 @@ function WaterfallTimeAxis({ windowMs, originMs = 0, widthPx = 208, formatDurati
|
|
|
662
662
|
"data-waterfall-axis": "",
|
|
663
663
|
"aria-hidden": "true",
|
|
664
664
|
style: {
|
|
665
|
-
...
|
|
665
|
+
...fluid ? {
|
|
666
|
+
minWidth: 0,
|
|
667
|
+
flex: "1 1 0%"
|
|
668
|
+
} : timingCell,
|
|
666
669
|
position: "relative",
|
|
667
670
|
height: "var(--waterfall-axis-height, 1.5rem)"
|
|
668
671
|
},
|
|
@@ -698,34 +701,40 @@ function WaterfallTimeAxis({ windowMs, originMs = 0, widthPx = 208, formatDurati
|
|
|
698
701
|
})
|
|
699
702
|
});
|
|
700
703
|
}
|
|
701
|
-
function WaterfallBar({ startMs, endMs, window, tone = "default", formatDuration = formatWaterfallDuration, className }) {
|
|
704
|
+
function WaterfallBar({ startMs, endMs, window, tone = "default", formatDuration = formatWaterfallDuration, className, color: suppliedColor, barClassName, overlay, fluid = false, clipAtMs }) {
|
|
705
|
+
const cell = fluid ? {
|
|
706
|
+
minWidth: 0,
|
|
707
|
+
flex: "1 1 0%"
|
|
708
|
+
} : timingCell;
|
|
702
709
|
if (!ordered(startMs, endMs) || !ordered(window.startMs, window.endMs)) return /* @__PURE__ */ jsx("span", {
|
|
703
710
|
className,
|
|
704
711
|
"data-waterfall-track": "",
|
|
705
712
|
"data-unavailable": "true",
|
|
706
713
|
title: "Timing unavailable",
|
|
707
714
|
style: {
|
|
708
|
-
...
|
|
715
|
+
...cell,
|
|
709
716
|
textAlign: "center"
|
|
710
717
|
},
|
|
711
718
|
children: "—"
|
|
712
719
|
});
|
|
713
720
|
const geometry = waterfallSpanGeometry(startMs, endMs, window);
|
|
714
|
-
const color = tone === "error" ? "var(--intel-waterfall-error, #dc2626)" : tone === "critical" ? "var(--intel-waterfall-critical, #0891b2)" : "var(--intel-waterfall-bar, #2563eb)";
|
|
715
|
-
|
|
721
|
+
const color = suppliedColor ?? (tone === "error" ? "var(--intel-waterfall-error, #dc2626)" : tone === "critical" ? "var(--intel-waterfall-critical, #0891b2)" : "var(--intel-waterfall-bar, #2563eb)");
|
|
722
|
+
const visible = clipAtMs === void 0 ? geometry : waterfallSpanGeometry(startMs, Math.max(startMs, Math.min(endMs, clipAtMs)), window);
|
|
723
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
716
724
|
className,
|
|
717
725
|
"data-waterfall-track": "",
|
|
718
726
|
"data-tone": tone,
|
|
719
727
|
title: `+${formatDuration(geometry.startOffsetMs)} · ${formatDuration(geometry.durationMs)}${tone === "critical" ? " · critical path" : ""}`,
|
|
720
728
|
style: {
|
|
721
|
-
...
|
|
729
|
+
...cell,
|
|
722
730
|
position: "relative",
|
|
723
731
|
height: "var(--waterfall-bar-height, 0.875rem)",
|
|
724
732
|
overflow: "hidden",
|
|
725
733
|
borderRadius: "var(--waterfall-bar-radius, 999px)",
|
|
726
734
|
background: "var(--intel-waterfall-track, rgb(148 163 184 / 22%))"
|
|
727
735
|
},
|
|
728
|
-
children: /* @__PURE__ */ jsx("span", {
|
|
736
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
737
|
+
className: barClassName,
|
|
729
738
|
"data-waterfall-bar": "",
|
|
730
739
|
"data-clipped": geometry.clipped || void 0,
|
|
731
740
|
style: {
|
|
@@ -735,18 +744,21 @@ function WaterfallBar({ startMs, endMs, window, tone = "default", formatDuration
|
|
|
735
744
|
borderRadius: "inherit",
|
|
736
745
|
background: color,
|
|
737
746
|
left: `${geometry.offsetPct}%`,
|
|
738
|
-
width: `${
|
|
747
|
+
width: `${clipAtMs !== void 0 && clipAtMs <= startMs ? 0 : visible.widthPct}%`,
|
|
739
748
|
boxShadow: tone === "default" ? void 0 : `0 0 0 1px ${color}`
|
|
740
749
|
}
|
|
741
|
-
})
|
|
750
|
+
}), overlay]
|
|
742
751
|
});
|
|
743
752
|
}
|
|
744
753
|
/** Non-interactive row body; its caller owns treeitems, buttons and selection. */
|
|
745
|
-
function WaterfallRow({ label, attribution, startMs, endMs, window, tone, formatDuration = formatWaterfallDuration, className, timingClassName, durationClassName }) {
|
|
754
|
+
function WaterfallRow({ label, attribution, startMs, endMs, window, tone, formatDuration = formatWaterfallDuration, className, timingClassName, durationClassName, style, durationLabel, ...bar }) {
|
|
746
755
|
return /* @__PURE__ */ jsxs("span", {
|
|
747
756
|
"data-waterfall-row": "",
|
|
748
757
|
className,
|
|
749
|
-
style:
|
|
758
|
+
style: style ? {
|
|
759
|
+
...rowStyle,
|
|
760
|
+
...style
|
|
761
|
+
} : rowStyle,
|
|
750
762
|
children: [
|
|
751
763
|
/* @__PURE__ */ jsx("span", {
|
|
752
764
|
"data-waterfall-label": "",
|
|
@@ -760,13 +772,14 @@ function WaterfallRow({ label, attribution, startMs, endMs, window, tone, format
|
|
|
760
772
|
window,
|
|
761
773
|
tone,
|
|
762
774
|
formatDuration,
|
|
763
|
-
className: timingClassName
|
|
775
|
+
className: timingClassName,
|
|
776
|
+
...bar
|
|
764
777
|
}),
|
|
765
778
|
/* @__PURE__ */ jsx("span", {
|
|
766
779
|
"data-waterfall-duration": "",
|
|
767
780
|
className: durationClassName,
|
|
768
781
|
style: durationStyle,
|
|
769
|
-
children: ordered(startMs, endMs) ? formatDuration(endMs - startMs) : "—"
|
|
782
|
+
children: durationLabel ?? (ordered(startMs, endMs) ? formatDuration(endMs - startMs) : "—")
|
|
770
783
|
})
|
|
771
784
|
]
|
|
772
785
|
});
|
|
@@ -2292,4 +2305,494 @@ function DotMap({ positions, radii, colors, palette, extent, rects, labels, dimm
|
|
|
2292
2305
|
});
|
|
2293
2306
|
}
|
|
2294
2307
|
//#endregion
|
|
2295
|
-
|
|
2308
|
+
//#region src/react/flow-waterfall-model.ts
|
|
2309
|
+
const spanFailed = (span) => span.status === "failed";
|
|
2310
|
+
const spanRunning = (span) => span.status === "running";
|
|
2311
|
+
/** Hierarchy only; the shared timing primitives own all bar geometry.
|
|
2312
|
+
* Orphaned parents become roots; cycles are broken without dropping any span. */
|
|
2313
|
+
function flowWaterfallRows(trace) {
|
|
2314
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2315
|
+
const rows = [...trace.spans].map((span, i) => ({
|
|
2316
|
+
span,
|
|
2317
|
+
key: span.id ?? `span-${i}`
|
|
2318
|
+
})).sort((a, b) => a.span.startMs - b.span.startMs).map(({ span, key }, index) => {
|
|
2319
|
+
let id = key;
|
|
2320
|
+
let suffix = index;
|
|
2321
|
+
while (ids.has(id)) id = `${key}:${suffix++}`;
|
|
2322
|
+
ids.add(id);
|
|
2323
|
+
return {
|
|
2324
|
+
id,
|
|
2325
|
+
parentId: span.parentId,
|
|
2326
|
+
name: span.name,
|
|
2327
|
+
kind: span.kind,
|
|
2328
|
+
durationLabel: Number.isFinite(span.startMs) && Number.isFinite(span.endMs) && span.endMs >= span.startMs ? `${((span.endMs - span.startMs) / 1e3).toFixed(1)}s${span.approx ? "~" : ""}` : "—",
|
|
2329
|
+
approx: span.approx === true,
|
|
2330
|
+
ok: !spanFailed(span),
|
|
2331
|
+
depth: 0,
|
|
2332
|
+
hasChildren: false,
|
|
2333
|
+
span
|
|
2334
|
+
};
|
|
2335
|
+
});
|
|
2336
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
2337
|
+
const children = /* @__PURE__ */ new Map();
|
|
2338
|
+
for (const row of rows) {
|
|
2339
|
+
const seen = /* @__PURE__ */ new Set([row.id]);
|
|
2340
|
+
let parent = row.parentId;
|
|
2341
|
+
while (parent && byId.has(parent)) {
|
|
2342
|
+
if (seen.has(parent)) {
|
|
2343
|
+
row.parentId = void 0;
|
|
2344
|
+
break;
|
|
2345
|
+
}
|
|
2346
|
+
seen.add(parent);
|
|
2347
|
+
parent = byId.get(parent)?.parentId;
|
|
2348
|
+
}
|
|
2349
|
+
if (row.parentId && !byId.has(row.parentId)) row.parentId = void 0;
|
|
2350
|
+
if (row.parentId) {
|
|
2351
|
+
const siblings = children.get(row.parentId) ?? [];
|
|
2352
|
+
siblings.push(row);
|
|
2353
|
+
children.set(row.parentId, siblings);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
const ordered = [];
|
|
2357
|
+
const visit = (row, depth) => {
|
|
2358
|
+
row.depth = depth;
|
|
2359
|
+
row.hasChildren = children.has(row.id);
|
|
2360
|
+
ordered.push(row);
|
|
2361
|
+
for (const child of children.get(row.id) ?? []) visit(child, depth + 1);
|
|
2362
|
+
};
|
|
2363
|
+
for (const row of rows) if (!row.parentId) visit(row, 0);
|
|
2364
|
+
return ordered;
|
|
2365
|
+
}
|
|
2366
|
+
/** Search keeps ancestor context and deliberately reveals matches inside collapsed branches. */
|
|
2367
|
+
function visibleFlowWaterfallRows(rows, query, collapsed) {
|
|
2368
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
2369
|
+
const term = query.trim().toLowerCase();
|
|
2370
|
+
if (term) {
|
|
2371
|
+
const matching = /* @__PURE__ */ new Set();
|
|
2372
|
+
for (const row of rows) {
|
|
2373
|
+
if (!`${row.name} ${row.kind} ${row.ok ? "" : "failed"} ${row.span.status ?? ""}`.toLowerCase().includes(term)) continue;
|
|
2374
|
+
let cursor = row;
|
|
2375
|
+
while (cursor && !matching.has(cursor.id)) {
|
|
2376
|
+
matching.add(cursor.id);
|
|
2377
|
+
cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0;
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
return rows.filter((row) => matching.has(row.id));
|
|
2381
|
+
}
|
|
2382
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
2383
|
+
return rows.filter((row) => {
|
|
2384
|
+
const isHidden = row.parentId !== void 0 && (collapsed.has(row.parentId) || hidden.has(row.parentId));
|
|
2385
|
+
if (isHidden) hidden.add(row.id);
|
|
2386
|
+
return !isHidden;
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
//#endregion
|
|
2390
|
+
//#region src/react/flow-waterfall.tsx
|
|
2391
|
+
const TONES = {
|
|
2392
|
+
pipeline: {
|
|
2393
|
+
label: "Orchestrator",
|
|
2394
|
+
color: "fw-orchestrator"
|
|
2395
|
+
},
|
|
2396
|
+
model: {
|
|
2397
|
+
label: "Model call",
|
|
2398
|
+
color: "fw-model"
|
|
2399
|
+
},
|
|
2400
|
+
tool: {
|
|
2401
|
+
label: "Tool call",
|
|
2402
|
+
color: "fw-tool"
|
|
2403
|
+
},
|
|
2404
|
+
waiting: {
|
|
2405
|
+
label: "Waiting",
|
|
2406
|
+
color: "fw-waiting"
|
|
2407
|
+
},
|
|
2408
|
+
handoff: {
|
|
2409
|
+
label: "Handoff",
|
|
2410
|
+
color: "fw-handoff"
|
|
2411
|
+
},
|
|
2412
|
+
failed: {
|
|
2413
|
+
label: "Failed",
|
|
2414
|
+
color: "fw-failed"
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
const SPEEDS = [
|
|
2418
|
+
.5,
|
|
2419
|
+
1,
|
|
2420
|
+
1.5,
|
|
2421
|
+
2,
|
|
2422
|
+
4
|
|
2423
|
+
];
|
|
2424
|
+
const controlClass = "fw-control";
|
|
2425
|
+
const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
|
|
2426
|
+
/** Shared trace inspector. Data remains producer-owned; playback only clips its presentation. */
|
|
2427
|
+
function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defaultCollapsed = false }) {
|
|
2428
|
+
const root = useRef(null);
|
|
2429
|
+
const search = useRef(null);
|
|
2430
|
+
const searchId = useId();
|
|
2431
|
+
const [query, setQuery] = useState("");
|
|
2432
|
+
const [collapsed, setCollapsed] = useState(() => new Set(defaultCollapsed ? trace.spans.flatMap((span) => span.parentId ? [span.parentId] : []) : []));
|
|
2433
|
+
const [speed, setSpeed] = useState(1);
|
|
2434
|
+
const [playing, setPlaying] = useState(false);
|
|
2435
|
+
const [position, setPosition] = useState(null);
|
|
2436
|
+
const observedTotal = Number.isFinite(trace.totalMs) && trace.totalMs >= 0 ? trace.totalMs : 0;
|
|
2437
|
+
const [elapsed, setElapsed] = useState(observedTotal);
|
|
2438
|
+
const isLive = live ?? trace.spans.some(spanRunning);
|
|
2439
|
+
useEffect(() => {
|
|
2440
|
+
setElapsed(observedTotal);
|
|
2441
|
+
if (!isLive) return;
|
|
2442
|
+
const receivedAt = Date.now();
|
|
2443
|
+
const tick = () => setElapsed(Math.max(observedTotal, startedAt === void 0 ? observedTotal + Date.now() - receivedAt : Date.now() - startedAt));
|
|
2444
|
+
tick();
|
|
2445
|
+
const timer = setInterval(tick, 100);
|
|
2446
|
+
return () => clearInterval(timer);
|
|
2447
|
+
}, [
|
|
2448
|
+
trace,
|
|
2449
|
+
observedTotal,
|
|
2450
|
+
isLive,
|
|
2451
|
+
startedAt
|
|
2452
|
+
]);
|
|
2453
|
+
const currentTrace = useMemo(() => {
|
|
2454
|
+
const totalMs = Math.max(observedTotal, isLive ? elapsed : 0, ...trace.spans.map((span) => Number.isFinite(span.endMs) ? span.endMs : 0));
|
|
2455
|
+
return {
|
|
2456
|
+
...trace,
|
|
2457
|
+
totalMs,
|
|
2458
|
+
spans: trace.spans.map((span) => isLive && spanRunning(span) && Number.isFinite(span.endMs) && span.endMs >= span.startMs ? {
|
|
2459
|
+
...span,
|
|
2460
|
+
endMs: Math.max(span.endMs, totalMs)
|
|
2461
|
+
} : span)
|
|
2462
|
+
};
|
|
2463
|
+
}, [
|
|
2464
|
+
trace,
|
|
2465
|
+
observedTotal,
|
|
2466
|
+
elapsed,
|
|
2467
|
+
isLive
|
|
2468
|
+
]);
|
|
2469
|
+
const totalMs = currentTrace.totalMs;
|
|
2470
|
+
const playhead = Math.min(position ?? totalMs, totalMs);
|
|
2471
|
+
const playbackLimit = useRef(totalMs);
|
|
2472
|
+
useEffect(() => {
|
|
2473
|
+
playbackLimit.current = totalMs;
|
|
2474
|
+
}, [totalMs]);
|
|
2475
|
+
useEffect(() => {
|
|
2476
|
+
if (!playing) return;
|
|
2477
|
+
let last = performance.now();
|
|
2478
|
+
const timer = setInterval(() => {
|
|
2479
|
+
const now = performance.now();
|
|
2480
|
+
const delta = (now - last) * speed;
|
|
2481
|
+
last = now;
|
|
2482
|
+
setPosition((previous) => Math.min(playbackLimit.current, (previous ?? 0) + delta));
|
|
2483
|
+
}, 50);
|
|
2484
|
+
return () => clearInterval(timer);
|
|
2485
|
+
}, [playing, speed]);
|
|
2486
|
+
useEffect(() => {
|
|
2487
|
+
if (playing && position !== null && position >= totalMs) setPlaying(false);
|
|
2488
|
+
}, [
|
|
2489
|
+
position,
|
|
2490
|
+
playing,
|
|
2491
|
+
totalMs
|
|
2492
|
+
]);
|
|
2493
|
+
useEffect(() => {
|
|
2494
|
+
const keydown = (event) => {
|
|
2495
|
+
if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "f") return;
|
|
2496
|
+
const active = document.activeElement;
|
|
2497
|
+
const ownsFocus = root.current?.contains(active);
|
|
2498
|
+
const first = Array.from(document.querySelectorAll("[data-flow-waterfall]")).find((element) => element.getClientRects().length > 0);
|
|
2499
|
+
if (!ownsFocus && (active !== document.body || first !== root.current)) return;
|
|
2500
|
+
event.preventDefault();
|
|
2501
|
+
search.current?.focus();
|
|
2502
|
+
search.current?.select();
|
|
2503
|
+
};
|
|
2504
|
+
document.addEventListener("keydown", keydown);
|
|
2505
|
+
return () => document.removeEventListener("keydown", keydown);
|
|
2506
|
+
}, []);
|
|
2507
|
+
const rows = useMemo(() => flowWaterfallRows(currentTrace), [currentTrace]);
|
|
2508
|
+
const visible = visibleFlowWaterfallRows(rows, query, collapsed);
|
|
2509
|
+
const slowest = rows.filter((row) => !row.hasChildren && row.kind !== "pipeline" && row.durationLabel !== "—").reduce((best, row) => !best || row.span.endMs - row.span.startMs > best.span.endMs - best.span.startMs ? row : best, void 0);
|
|
2510
|
+
const summary = [
|
|
2511
|
+
["Duration", seconds(totalMs)],
|
|
2512
|
+
["Cost", trace.costUsd === void 0 ? "Unknown" : `$${trace.costUsd.toFixed(trace.costUsd < .01 ? 6 : 4)}`],
|
|
2513
|
+
["Tokens", (trace.promptTokens + trace.completionTokens).toLocaleString()],
|
|
2514
|
+
["LLM calls", trace.modelCalls === void 0 ? "Unknown" : String(trace.modelCalls)],
|
|
2515
|
+
["Tool calls", String(trace.toolCalls)],
|
|
2516
|
+
["Errors", String(trace.errors ?? trace.spans.filter(spanFailed).length)]
|
|
2517
|
+
];
|
|
2518
|
+
const toggle = (id) => setCollapsed((previous) => {
|
|
2519
|
+
const next = new Set(previous);
|
|
2520
|
+
if (next.has(id)) next.delete(id);
|
|
2521
|
+
else next.add(id);
|
|
2522
|
+
return next;
|
|
2523
|
+
});
|
|
2524
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
2525
|
+
ref: root,
|
|
2526
|
+
"data-flow-waterfall": true,
|
|
2527
|
+
"aria-label": label,
|
|
2528
|
+
className: "fw-root",
|
|
2529
|
+
children: [
|
|
2530
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2531
|
+
className: "fw-heading",
|
|
2532
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2533
|
+
className: "fw-title",
|
|
2534
|
+
children: [label, isLive && /* @__PURE__ */ jsx("span", {
|
|
2535
|
+
className: "fw-muted",
|
|
2536
|
+
children: "Live"
|
|
2537
|
+
})]
|
|
2538
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2539
|
+
className: "fw-search",
|
|
2540
|
+
children: [
|
|
2541
|
+
/* @__PURE__ */ jsx("label", {
|
|
2542
|
+
htmlFor: searchId,
|
|
2543
|
+
className: "fw-sr-only",
|
|
2544
|
+
children: "Search trace"
|
|
2545
|
+
}),
|
|
2546
|
+
/* @__PURE__ */ jsx("input", {
|
|
2547
|
+
ref: search,
|
|
2548
|
+
id: searchId,
|
|
2549
|
+
type: "search",
|
|
2550
|
+
value: query,
|
|
2551
|
+
placeholder: "Search trace…",
|
|
2552
|
+
"aria-keyshortcuts": "Meta+f Control+f",
|
|
2553
|
+
onChange: (event) => setQuery(event.target.value),
|
|
2554
|
+
onKeyDown: (event) => {
|
|
2555
|
+
if (event.key === "Escape") {
|
|
2556
|
+
setQuery("");
|
|
2557
|
+
event.stopPropagation();
|
|
2558
|
+
}
|
|
2559
|
+
},
|
|
2560
|
+
className: `${controlClass} fw-search-input`
|
|
2561
|
+
}),
|
|
2562
|
+
/* @__PURE__ */ jsx("button", {
|
|
2563
|
+
type: "button",
|
|
2564
|
+
"aria-label": "Focus trace search",
|
|
2565
|
+
title: "Search trace (⌘F / Ctrl+F)",
|
|
2566
|
+
onClick: () => search.current?.focus(),
|
|
2567
|
+
className: controlClass,
|
|
2568
|
+
children: "⌘F"
|
|
2569
|
+
})
|
|
2570
|
+
]
|
|
2571
|
+
})]
|
|
2572
|
+
}),
|
|
2573
|
+
/* @__PURE__ */ jsx("dl", {
|
|
2574
|
+
className: "fw-summary",
|
|
2575
|
+
children: summary.map(([name, value]) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
|
|
2576
|
+
className: "fw-muted",
|
|
2577
|
+
children: name
|
|
2578
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
2579
|
+
className: "fw-value",
|
|
2580
|
+
title: name === "LLM calls" && trace.modelCalls === void 0 ? "Model output windows do not establish the number of LLM requests" : void 0,
|
|
2581
|
+
children: value
|
|
2582
|
+
})] }, name))
|
|
2583
|
+
}),
|
|
2584
|
+
/* @__PURE__ */ jsx("div", {
|
|
2585
|
+
className: "fw-legend",
|
|
2586
|
+
"aria-label": "Span types",
|
|
2587
|
+
children: Object.entries(TONES).map(([kind, tone]) => /* @__PURE__ */ jsxs("span", {
|
|
2588
|
+
className: "fw-legend-item",
|
|
2589
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2590
|
+
"aria-hidden": true,
|
|
2591
|
+
className: `fw-dot ${tone.color}`
|
|
2592
|
+
}), tone.label]
|
|
2593
|
+
}, kind))
|
|
2594
|
+
}),
|
|
2595
|
+
rows.some((row) => row.hasChildren) && /* @__PURE__ */ jsxs("div", {
|
|
2596
|
+
className: "fw-branches",
|
|
2597
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
2598
|
+
type: "button",
|
|
2599
|
+
className: controlClass,
|
|
2600
|
+
onClick: () => setCollapsed(/* @__PURE__ */ new Set()),
|
|
2601
|
+
children: "Expand all"
|
|
2602
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
2603
|
+
type: "button",
|
|
2604
|
+
className: controlClass,
|
|
2605
|
+
onClick: () => setCollapsed(new Set(rows.filter((row) => row.hasChildren).map((row) => row.id))),
|
|
2606
|
+
children: "Collapse all"
|
|
2607
|
+
})]
|
|
2608
|
+
}),
|
|
2609
|
+
/* @__PURE__ */ jsx("div", {
|
|
2610
|
+
className: "fw-timeline",
|
|
2611
|
+
role: "region",
|
|
2612
|
+
"aria-label": "Trace timeline",
|
|
2613
|
+
tabIndex: 0,
|
|
2614
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
2615
|
+
className: "fw-track",
|
|
2616
|
+
children: [
|
|
2617
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2618
|
+
className: "fw-axis fw-grid",
|
|
2619
|
+
children: [
|
|
2620
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
2621
|
+
"Steps · ",
|
|
2622
|
+
visible.length,
|
|
2623
|
+
"/",
|
|
2624
|
+
rows.length
|
|
2625
|
+
] }),
|
|
2626
|
+
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(WaterfallTimeAxis, {
|
|
2627
|
+
fluid: true,
|
|
2628
|
+
windowMs: totalMs,
|
|
2629
|
+
widthPx: 240,
|
|
2630
|
+
formatDuration: seconds
|
|
2631
|
+
}) }),
|
|
2632
|
+
/* @__PURE__ */ jsx("span", {
|
|
2633
|
+
className: "fw-align-right",
|
|
2634
|
+
children: "Duration"
|
|
2635
|
+
})
|
|
2636
|
+
]
|
|
2637
|
+
}),
|
|
2638
|
+
visible.length === 0 && /* @__PURE__ */ jsx("p", {
|
|
2639
|
+
role: "status",
|
|
2640
|
+
className: "fw-empty",
|
|
2641
|
+
children: rows.length === 0 ? "No spans yet. Waiting for trace events." : `No matching steps for “${query}”.`
|
|
2642
|
+
}),
|
|
2643
|
+
visible.map((row) => {
|
|
2644
|
+
const tone = !row.ok ? TONES.failed : TONES[row.kind];
|
|
2645
|
+
const pending = row.span.startMs > playhead;
|
|
2646
|
+
const running = spanRunning(row.span);
|
|
2647
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2648
|
+
"data-span-id": row.id,
|
|
2649
|
+
"data-span-kind": !row.ok ? "failed" : row.kind,
|
|
2650
|
+
"data-slowest": row.id === slowest?.id || void 0,
|
|
2651
|
+
className: `fw-row ${row.id === slowest?.id ? "fw-slowest" : ""} ${pending ? "fw-pending" : ""}`,
|
|
2652
|
+
children: /* @__PURE__ */ jsx(WaterfallRow, {
|
|
2653
|
+
className: "fw-grid",
|
|
2654
|
+
style: { display: "grid" },
|
|
2655
|
+
startMs: row.span.startMs,
|
|
2656
|
+
endMs: row.span.endMs,
|
|
2657
|
+
window: {
|
|
2658
|
+
startMs: 0,
|
|
2659
|
+
endMs: totalMs
|
|
2660
|
+
},
|
|
2661
|
+
fluid: true,
|
|
2662
|
+
color: "var(--fw-bar-color)",
|
|
2663
|
+
barClassName: `fw-bar ${tone.color} ${row.approx ? "fw-approx" : ""}`,
|
|
2664
|
+
timingClassName: "fw-bar-track",
|
|
2665
|
+
durationClassName: "fw-duration",
|
|
2666
|
+
durationLabel: row.durationLabel,
|
|
2667
|
+
formatDuration: seconds,
|
|
2668
|
+
clipAtMs: position === null ? void 0 : playhead,
|
|
2669
|
+
overlay: position !== null && /* @__PURE__ */ jsx("span", {
|
|
2670
|
+
className: "fw-playhead",
|
|
2671
|
+
style: { left: `${playhead / (totalMs || 1) * 100}%` }
|
|
2672
|
+
}),
|
|
2673
|
+
label: /* @__PURE__ */ jsxs("div", {
|
|
2674
|
+
className: "fw-name",
|
|
2675
|
+
style: { paddingLeft: Math.min(row.depth, 8) * 12 },
|
|
2676
|
+
children: [row.hasChildren ? /* @__PURE__ */ jsx("button", {
|
|
2677
|
+
type: "button",
|
|
2678
|
+
"aria-label": `${collapsed.has(row.id) && !query ? "Expand" : "Collapse"} ${row.name}`,
|
|
2679
|
+
"aria-expanded": query ? true : !collapsed.has(row.id),
|
|
2680
|
+
onClick: () => toggle(row.id),
|
|
2681
|
+
disabled: Boolean(query.trim()),
|
|
2682
|
+
className: "fw-toggle",
|
|
2683
|
+
children: collapsed.has(row.id) && !query ? "›" : "⌄"
|
|
2684
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
2685
|
+
className: "fw-leaf",
|
|
2686
|
+
"aria-hidden": true,
|
|
2687
|
+
children: "·"
|
|
2688
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
2689
|
+
title: `${row.name} · ${tone.label}${running ? " · running" : ""}`,
|
|
2690
|
+
className: "fw-span-name",
|
|
2691
|
+
children: [
|
|
2692
|
+
row.name,
|
|
2693
|
+
!row.ok && /* @__PURE__ */ jsx("span", {
|
|
2694
|
+
className: "fw-error-text",
|
|
2695
|
+
children: " · failed"
|
|
2696
|
+
}),
|
|
2697
|
+
running && /* @__PURE__ */ jsx("span", {
|
|
2698
|
+
className: "fw-muted",
|
|
2699
|
+
children: " · running"
|
|
2700
|
+
})
|
|
2701
|
+
]
|
|
2702
|
+
})]
|
|
2703
|
+
})
|
|
2704
|
+
})
|
|
2705
|
+
}, row.id);
|
|
2706
|
+
})
|
|
2707
|
+
]
|
|
2708
|
+
})
|
|
2709
|
+
}),
|
|
2710
|
+
slowest && /* @__PURE__ */ jsxs("p", {
|
|
2711
|
+
className: "fw-slowest-caption",
|
|
2712
|
+
children: [
|
|
2713
|
+
"Slowest step: ",
|
|
2714
|
+
/* @__PURE__ */ jsx("span", {
|
|
2715
|
+
className: "fw-text",
|
|
2716
|
+
children: slowest.name
|
|
2717
|
+
}),
|
|
2718
|
+
" · ",
|
|
2719
|
+
slowest.durationLabel
|
|
2720
|
+
]
|
|
2721
|
+
}),
|
|
2722
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2723
|
+
className: "fw-playback",
|
|
2724
|
+
children: [
|
|
2725
|
+
/* @__PURE__ */ jsx("button", {
|
|
2726
|
+
type: "button",
|
|
2727
|
+
className: controlClass,
|
|
2728
|
+
disabled: totalMs === 0,
|
|
2729
|
+
onClick: () => {
|
|
2730
|
+
if (playing) setPlaying(false);
|
|
2731
|
+
else {
|
|
2732
|
+
if (position === null || position >= totalMs) setPosition(0);
|
|
2733
|
+
setPlaying(true);
|
|
2734
|
+
}
|
|
2735
|
+
},
|
|
2736
|
+
children: playing ? "Pause" : "Play"
|
|
2737
|
+
}),
|
|
2738
|
+
/* @__PURE__ */ jsx("button", {
|
|
2739
|
+
type: "button",
|
|
2740
|
+
className: controlClass,
|
|
2741
|
+
disabled: totalMs === 0,
|
|
2742
|
+
onClick: () => {
|
|
2743
|
+
setPosition(0);
|
|
2744
|
+
setPlaying(false);
|
|
2745
|
+
},
|
|
2746
|
+
children: "Restart"
|
|
2747
|
+
}),
|
|
2748
|
+
/* @__PURE__ */ jsxs("label", {
|
|
2749
|
+
className: "fw-speed",
|
|
2750
|
+
children: ["Speed", /* @__PURE__ */ jsx("select", {
|
|
2751
|
+
"aria-label": "Playback speed",
|
|
2752
|
+
className: controlClass,
|
|
2753
|
+
value: speed,
|
|
2754
|
+
onChange: (event) => setSpeed(Number(event.target.value)),
|
|
2755
|
+
children: SPEEDS.map((value) => /* @__PURE__ */ jsxs("option", {
|
|
2756
|
+
value,
|
|
2757
|
+
children: [value, "x"]
|
|
2758
|
+
}, value))
|
|
2759
|
+
})]
|
|
2760
|
+
}),
|
|
2761
|
+
/* @__PURE__ */ jsxs("label", {
|
|
2762
|
+
className: "fw-position",
|
|
2763
|
+
children: ["Position", /* @__PURE__ */ jsx("input", {
|
|
2764
|
+
"aria-label": "Playback position",
|
|
2765
|
+
type: "range",
|
|
2766
|
+
min: 0,
|
|
2767
|
+
max: totalMs || 1,
|
|
2768
|
+
step: "any",
|
|
2769
|
+
value: playhead,
|
|
2770
|
+
disabled: totalMs === 0,
|
|
2771
|
+
onChange: (event) => {
|
|
2772
|
+
setPosition(Number(event.target.value));
|
|
2773
|
+
setPlaying(false);
|
|
2774
|
+
},
|
|
2775
|
+
className: "fw-slider"
|
|
2776
|
+
})]
|
|
2777
|
+
}),
|
|
2778
|
+
/* @__PURE__ */ jsx("output", {
|
|
2779
|
+
className: "fw-time",
|
|
2780
|
+
"aria-label": "Playback time",
|
|
2781
|
+
children: seconds(playhead)
|
|
2782
|
+
}),
|
|
2783
|
+
position !== null && /* @__PURE__ */ jsx("button", {
|
|
2784
|
+
type: "button",
|
|
2785
|
+
className: controlClass,
|
|
2786
|
+
onClick: () => {
|
|
2787
|
+
setPosition(null);
|
|
2788
|
+
setPlaying(false);
|
|
2789
|
+
},
|
|
2790
|
+
children: isLive ? "Follow live" : "Show full trace"
|
|
2791
|
+
})
|
|
2792
|
+
]
|
|
2793
|
+
})
|
|
2794
|
+
]
|
|
2795
|
+
});
|
|
2796
|
+
}
|
|
2797
|
+
//#endregion
|
|
2798
|
+
export { DEFAULT_SPARKLINE_EMPTY_LABEL, DEFAULT_SPARKLINE_HEIGHT, DEFAULT_SPARKLINE_LABEL, DEFAULT_SPARKLINE_UNAVAILABLE_LABEL, DEFAULT_SPARKLINE_WIDTH, DOT_MAP_CATEGORIES, DayBars, DotMap, Events, FigureView, FlowWaterfall, Gantt, HBars, Histogram, LIVE_SERIES, Lines, Meter, Spark, Sparkline, StackedBarChart, TimelineView, WaterfallBar, WaterfallHeader, WaterfallRow, WaterfallTimeAxis, formatCompact, formatSparklineValue, formatWaterfallDuration, sparklineGeometry, sparklineLabel, sparklinePointsAttribute, sparklineReadings, waterfallSpanGeometry, waterfallTicks };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/charts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Static SVG and HTML figures for benchmarks and operational timing. Draws from producer data and refuses what it cannot draw honestly.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"homepage": "https://github.com/tangle-network/charts",
|
|
15
15
|
"sideEffects": [
|
|
16
16
|
"./dist/figure.css",
|
|
17
|
-
"./dist/react/live.css"
|
|
17
|
+
"./dist/react/live.css",
|
|
18
|
+
"./dist/react/flow-waterfall.css"
|
|
18
19
|
],
|
|
19
20
|
"files": [
|
|
20
21
|
"dist",
|
|
@@ -32,7 +33,8 @@
|
|
|
32
33
|
"types": "./dist/react/index.d.ts",
|
|
33
34
|
"import": "./dist/react/index.js",
|
|
34
35
|
"default": "./dist/react/index.js"
|
|
35
|
-
}
|
|
36
|
+
},
|
|
37
|
+
"./react/flow-waterfall.css": "./dist/react/flow-waterfall.css"
|
|
36
38
|
},
|
|
37
39
|
"peerDependencies": {
|
|
38
40
|
"react": "^18.0.0 || ^19.0.0"
|
|
@@ -44,7 +46,7 @@
|
|
|
44
46
|
},
|
|
45
47
|
"scripts": {
|
|
46
48
|
"gen:css": "node scripts/gen-css.mjs",
|
|
47
|
-
"build": "node scripts/gen-css.mjs --check && tsdown && cp src/figure.css dist/figure.css && cp src/react/live.css dist/react/live.css",
|
|
49
|
+
"build": "node scripts/gen-css.mjs --check && tsdown && cp src/figure.css dist/figure.css && cp src/react/live.css dist/react/live.css && cp src/react/flow-waterfall.css dist/react/flow-waterfall.css",
|
|
48
50
|
"typecheck": "tsc --noEmit",
|
|
49
51
|
"test": "vitest run"
|
|
50
52
|
},
|
|
@@ -55,6 +57,7 @@
|
|
|
55
57
|
"@types/node": "^26.6.3",
|
|
56
58
|
"@types/react": "^19.3.0",
|
|
57
59
|
"@types/react-dom": "^19.2.3",
|
|
60
|
+
"esbuild": "^0.28.2",
|
|
58
61
|
"jsdom": "^30.1.1",
|
|
59
62
|
"playwright": "^1.63.0",
|
|
60
63
|
"react": "^19.3.0",
|