@usetheo/ui 0.23.0 → 0.26.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/dist/components/composites/annotation-input/annotation-input.d.ts +38 -0
- package/dist/components/composites/annotation-input/index.d.ts +4 -0
- package/dist/components/composites/annotation-input/types.d.ts +31 -0
- package/dist/components/composites/histogram/histogram.d.ts +19 -0
- package/dist/components/composites/histogram/index.d.ts +2 -0
- package/dist/components/composites/percentile-chart/index.d.ts +2 -0
- package/dist/components/composites/percentile-chart/percentile-chart.d.ts +37 -0
- package/dist/components/composites/session-summary/index.d.ts +1 -0
- package/dist/components/composites/session-summary/session-summary.d.ts +18 -0
- package/dist/components/composites/session-timeline/index.d.ts +1 -0
- package/dist/components/composites/session-timeline/session-timeline.d.ts +22 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +614 -1
- package/dist/lib/chart/histogram.d.ts +21 -0
- package/dist/lib/chart/index.d.ts +1 -0
- package/dist/lib/session/aggregate.d.ts +18 -0
- package/dist/lib/session/index.d.ts +2 -0
- package/dist/lib/session/types.d.ts +30 -0
- package/package.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AnnotationCategoricalConfig, AnnotationContinuousConfig, AnnotationFreeformConfig } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* AnnotationInput — a controlled, config-driven human annotation input (M12).
|
|
4
|
+
* Dispatches on `config.type` (Phoenix's three-type model): categorical →
|
|
5
|
+
* RadioGroup over named options; continuous → number input bounded by the
|
|
6
|
+
* config; freeform → textarea. Fully controlled (`value` + `onValueChange`);
|
|
7
|
+
* the consumer owns the config (fixture or backend store) and the submission.
|
|
8
|
+
* Composes the DS form primitives — zero new dependency (M12 ADR D2).
|
|
9
|
+
*
|
|
10
|
+
* <AnnotationInput name="Quality" config={qualityConfig}
|
|
11
|
+
* value={v} onValueChange={setV} />
|
|
12
|
+
*/
|
|
13
|
+
interface AnnotationInputCommon {
|
|
14
|
+
/** The score name — rendered as the field label / radiogroup accessible name. */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Overrides the generated control id (for external label association). */
|
|
17
|
+
id?: string;
|
|
18
|
+
/** Optional helper text, wired via aria-describedby. */
|
|
19
|
+
description?: string;
|
|
20
|
+
required?: boolean;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
export type AnnotationInputProps = (AnnotationInputCommon & {
|
|
25
|
+
config: AnnotationCategoricalConfig;
|
|
26
|
+
value: string | null;
|
|
27
|
+
onValueChange: (value: string | null) => void;
|
|
28
|
+
}) | (AnnotationInputCommon & {
|
|
29
|
+
config: AnnotationContinuousConfig;
|
|
30
|
+
value: number | null;
|
|
31
|
+
onValueChange: (value: number | null) => void;
|
|
32
|
+
}) | (AnnotationInputCommon & {
|
|
33
|
+
config: AnnotationFreeformConfig;
|
|
34
|
+
value: string | null;
|
|
35
|
+
onValueChange: (value: string | null) => void;
|
|
36
|
+
});
|
|
37
|
+
declare function AnnotationInput(props: AnnotationInputProps): import("react").JSX.Element;
|
|
38
|
+
export { AnnotationInput };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { AnnotationInput } from "./annotation-input.js";
|
|
2
|
+
export type { AnnotationInputProps } from "./annotation-input.js";
|
|
3
|
+
export type { CategoricalOption, AnnotationCategoricalConfig, AnnotationContinuousConfig, AnnotationFreeformConfig, AnnotationConfig, } from "./types.js";
|
|
4
|
+
export { isCategoricalConfig, isContinuousConfig, isFreeformConfig } from "./types.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Annotation config types — the named-score-type contract for AnnotationInput
|
|
3
|
+
* (M12). Follows Phoenix's three-type model (categorical / continuous /
|
|
4
|
+
* freeform); Langfuse's BOOLEAN is categorical-of-two and CORRECTION is
|
|
5
|
+
* system-only, so neither adds to a controlled human input. The config is a
|
|
6
|
+
* plain descriptor the consumer owns (fixture or backend store) — the DS
|
|
7
|
+
* component is config-driven and stateless.
|
|
8
|
+
*/
|
|
9
|
+
/** One named categorical choice; `score` optionally maps the label to a numeric value on submit. */
|
|
10
|
+
export interface CategoricalOption {
|
|
11
|
+
label: string;
|
|
12
|
+
score?: number | null;
|
|
13
|
+
}
|
|
14
|
+
export interface AnnotationCategoricalConfig {
|
|
15
|
+
type: "categorical";
|
|
16
|
+
options: CategoricalOption[];
|
|
17
|
+
}
|
|
18
|
+
export interface AnnotationContinuousConfig {
|
|
19
|
+
type: "continuous";
|
|
20
|
+
min: number;
|
|
21
|
+
max: number;
|
|
22
|
+
step?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface AnnotationFreeformConfig {
|
|
25
|
+
type: "freeform";
|
|
26
|
+
maxLength?: number;
|
|
27
|
+
}
|
|
28
|
+
export type AnnotationConfig = AnnotationCategoricalConfig | AnnotationContinuousConfig | AnnotationFreeformConfig;
|
|
29
|
+
export declare function isCategoricalConfig(c: AnnotationConfig): c is AnnotationCategoricalConfig;
|
|
30
|
+
export declare function isContinuousConfig(c: AnnotationConfig): c is AnnotationContinuousConfig;
|
|
31
|
+
export declare function isFreeformConfig(c: AnnotationConfig): c is AnnotationFreeformConfig;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
import { type HistogramBin } from "../../../lib/chart/histogram.js";
|
|
3
|
+
type HistogramData = {
|
|
4
|
+
bins: HistogramBin[];
|
|
5
|
+
values?: never;
|
|
6
|
+
binCount?: never;
|
|
7
|
+
} | {
|
|
8
|
+
values: number[];
|
|
9
|
+
binCount: number;
|
|
10
|
+
bins?: never;
|
|
11
|
+
};
|
|
12
|
+
export type HistogramProps = HTMLAttributes<HTMLElement> & {
|
|
13
|
+
title: string;
|
|
14
|
+
height?: number;
|
|
15
|
+
/** Formats the count axis label and the bin bounds in the a11y table. */
|
|
16
|
+
valueFormatter?: (v: number) => string;
|
|
17
|
+
} & HistogramData;
|
|
18
|
+
declare const Histogram: import("react").ForwardRefExoticComponent<HistogramProps & import("react").RefAttributes<HTMLElement>>;
|
|
19
|
+
export { Histogram };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* PercentileChart — p50/p95/p99 over time as shaded BANDS (not three flat
|
|
4
|
+
* lines), pure-SVG (M3 anti-chart-lib ADR upheld, M11 ADR D1). The p50–p95 and
|
|
5
|
+
* p95–p99 spans render as filled areas + a p50 reference line, so the spread is
|
|
6
|
+
* read at a glance (phoenix `TraceLatencyPercentilesTimeSeries` pattern, M11
|
|
7
|
+
* ADR D2 — the upgrade over the lens' current three-line overlay). Reuses
|
|
8
|
+
* `linScale`/`niceMax`/`seriesPath` from TrendChart — zero new chart dep.
|
|
9
|
+
*
|
|
10
|
+
* <PercentileChart title="Latency" buckets={latencyBuckets}
|
|
11
|
+
* valueFormatter={(v) => `${v}ms`} />
|
|
12
|
+
*/
|
|
13
|
+
export interface PercentileBucket {
|
|
14
|
+
/** p50 value for this time bucket. */
|
|
15
|
+
p50: number;
|
|
16
|
+
/** p95 value for this time bucket. */
|
|
17
|
+
p95: number;
|
|
18
|
+
/** p99 value for this time bucket. */
|
|
19
|
+
p99: number;
|
|
20
|
+
/** Optional bucket label (read in the a11y table). */
|
|
21
|
+
label?: string;
|
|
22
|
+
}
|
|
23
|
+
export type PercentileChartProps = HTMLAttributes<HTMLElement> & {
|
|
24
|
+
title: string;
|
|
25
|
+
buckets: PercentileBucket[];
|
|
26
|
+
height?: number;
|
|
27
|
+
/** Formats the y-axis label and the a11y table cells. */
|
|
28
|
+
valueFormatter?: (v: number) => string;
|
|
29
|
+
};
|
|
30
|
+
declare const PercentileChart: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLElement> & {
|
|
31
|
+
title: string;
|
|
32
|
+
buckets: PercentileBucket[];
|
|
33
|
+
height?: number;
|
|
34
|
+
/** Formats the y-axis label and the a11y table cells. */
|
|
35
|
+
valueFormatter?: (v: number) => string;
|
|
36
|
+
} & import("react").RefAttributes<HTMLElement>>;
|
|
37
|
+
export { PercentileChart };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SessionSummary, type SessionSummaryProps } from "./session-summary.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SessionMetrics, SessionTraceItem } from "../../../lib/session/types.js";
|
|
2
|
+
export interface SessionSummaryProps {
|
|
3
|
+
/** The session's traces — aggregated internally. Supply `metrics` instead to pass a pre-aggregate. */
|
|
4
|
+
items?: SessionTraceItem[];
|
|
5
|
+
/** Pre-computed metrics (overrides `items`). */
|
|
6
|
+
metrics?: SessionMetrics;
|
|
7
|
+
/** USD cost formatter. Default `$0.0000`. */
|
|
8
|
+
formatCostUsd?: (usd: number) => string;
|
|
9
|
+
className?: string;
|
|
10
|
+
"aria-label"?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* SessionSummary — a `<dl>` of honest aggregate metrics over a session's traces: trace count, the
|
|
14
|
+
* session wall-clock window, ∑ cost, ∑ tokens, error count (highlighted destructive when > 0), and
|
|
15
|
+
* the distinct models. Sums of zero are honest (`$0.0000`, `0`), never an em-dash. Pass `items` (it
|
|
16
|
+
* aggregates via `aggregateSession`) or a pre-computed `metrics`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function SessionSummary({ items, metrics, formatCostUsd, className, "aria-label": ariaLabel, }: SessionSummaryProps): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SessionTimeline, type SessionTimelineProps } from "./session-timeline.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SessionTraceItem } from "../../../lib/session/types.js";
|
|
2
|
+
export interface SessionTimelineProps {
|
|
3
|
+
/** The session's traces (rendered in start-time order regardless of input order). */
|
|
4
|
+
items: SessionTraceItem[];
|
|
5
|
+
selectedId: string | null;
|
|
6
|
+
onSelect: (id: string) => void;
|
|
7
|
+
/** ns → label for the row timestamp. Default ISO datetime. */
|
|
8
|
+
formatTimestamp?: (ns: bigint) => string;
|
|
9
|
+
/** USD cost formatter. Default `$0.0000`. */
|
|
10
|
+
formatCostUsd?: (usd: number) => string;
|
|
11
|
+
/** Row count at/above which the virtualized path engages. Default 200. */
|
|
12
|
+
virtualizeThreshold?: number;
|
|
13
|
+
className?: string;
|
|
14
|
+
"aria-label"?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* SessionTimeline — a temporal replay of a session: one row per trace, sorted by start time, each
|
|
18
|
+
* showing the timestamp, name, a duration bar relative to the session window (via the trace-core
|
|
19
|
+
* `computeBarLayout`), status, and cost. Controlled selection; virtualizes above the threshold.
|
|
20
|
+
* Escaped text only.
|
|
21
|
+
*/
|
|
22
|
+
export declare function SessionTimeline({ items, selectedId, onSelect, formatTimestamp, formatCostUsd, virtualizeThreshold, className, "aria-label": ariaLabel, }: SessionTimelineProps): import("react").JSX.Element;
|
package/dist/index.d.ts
CHANGED
|
@@ -77,3 +77,17 @@ export { SpanGraph } from "./components/composites/span-graph/index.js";
|
|
|
77
77
|
export type { SpanGraphProps } from "./components/composites/span-graph/index.js";
|
|
78
78
|
export { TraceCompare } from "./components/composites/trace-compare/index.js";
|
|
79
79
|
export type { TraceCompareProps, CompareLane, } from "./components/composites/trace-compare/index.js";
|
|
80
|
+
export { aggregateSession, sessionBounds } from "./lib/session/index.js";
|
|
81
|
+
export type { SessionTraceItem, SessionMetrics } from "./lib/session/index.js";
|
|
82
|
+
export { SessionSummary } from "./components/composites/session-summary/index.js";
|
|
83
|
+
export type { SessionSummaryProps } from "./components/composites/session-summary/index.js";
|
|
84
|
+
export { SessionTimeline } from "./components/composites/session-timeline/index.js";
|
|
85
|
+
export type { SessionTimelineProps } from "./components/composites/session-timeline/index.js";
|
|
86
|
+
export { computeHistogram } from "./lib/chart/index.js";
|
|
87
|
+
export type { HistogramBin } from "./lib/chart/index.js";
|
|
88
|
+
export { Histogram } from "./components/composites/histogram/index.js";
|
|
89
|
+
export type { HistogramProps } from "./components/composites/histogram/index.js";
|
|
90
|
+
export { PercentileChart } from "./components/composites/percentile-chart/index.js";
|
|
91
|
+
export type { PercentileBucket, PercentileChartProps, } from "./components/composites/percentile-chart/index.js";
|
|
92
|
+
export { AnnotationInput, isCategoricalConfig, isContinuousConfig, isFreeformConfig, } from "./components/composites/annotation-input/index.js";
|
|
93
|
+
export type { AnnotationInputProps, CategoricalOption, AnnotationCategoricalConfig, AnnotationContinuousConfig, AnnotationFreeformConfig, AnnotationConfig, } from "./components/composites/annotation-input/index.js";
|
package/dist/index.js
CHANGED
|
@@ -7535,4 +7535,617 @@ function DeltaCell({
|
|
|
7535
7535
|
] });
|
|
7536
7536
|
}
|
|
7537
7537
|
|
|
7538
|
-
|
|
7538
|
+
// src/lib/session/aggregate.ts
|
|
7539
|
+
function sessionBounds(items) {
|
|
7540
|
+
let minStart = null;
|
|
7541
|
+
let maxEnd = null;
|
|
7542
|
+
for (const it of items) {
|
|
7543
|
+
const start = toNs(it.startTime);
|
|
7544
|
+
if (start === null) continue;
|
|
7545
|
+
if (minStart === null || start < minStart) minStart = start;
|
|
7546
|
+
const end = toNs(it.endTime ?? null) ?? start;
|
|
7547
|
+
const eff = end >= start ? end : start;
|
|
7548
|
+
if (maxEnd === null || eff > maxEnd) maxEnd = eff;
|
|
7549
|
+
}
|
|
7550
|
+
const s = minStart ?? 0n;
|
|
7551
|
+
return { startNs: s, endNs: maxEnd ?? s };
|
|
7552
|
+
}
|
|
7553
|
+
function aggregateSession(items) {
|
|
7554
|
+
let totalCostUsd = 0;
|
|
7555
|
+
let totalTokens = 0;
|
|
7556
|
+
let errorCount = 0;
|
|
7557
|
+
const models = /* @__PURE__ */ new Set();
|
|
7558
|
+
for (const it of items) {
|
|
7559
|
+
if (typeof it.costUsd === "number" && Number.isFinite(it.costUsd) && it.costUsd > 0) {
|
|
7560
|
+
totalCostUsd += it.costUsd;
|
|
7561
|
+
}
|
|
7562
|
+
if (typeof it.totalTokens === "number" && Number.isFinite(it.totalTokens) && it.totalTokens > 0) {
|
|
7563
|
+
totalTokens += it.totalTokens;
|
|
7564
|
+
}
|
|
7565
|
+
if (it.status === "ERROR") errorCount++;
|
|
7566
|
+
if (it.model) models.add(it.model);
|
|
7567
|
+
}
|
|
7568
|
+
const { startNs, endNs } = sessionBounds(items);
|
|
7569
|
+
const windowMs = Number((endNs - startNs) / 1000000n);
|
|
7570
|
+
return {
|
|
7571
|
+
traceCount: items.length,
|
|
7572
|
+
windowMs,
|
|
7573
|
+
totalCostUsd,
|
|
7574
|
+
totalTokens,
|
|
7575
|
+
errorCount,
|
|
7576
|
+
models: [...models].sort()
|
|
7577
|
+
};
|
|
7578
|
+
}
|
|
7579
|
+
var defaultCost4 = (usd) => `$${usd.toFixed(4)}`;
|
|
7580
|
+
function SessionSummary({
|
|
7581
|
+
items,
|
|
7582
|
+
metrics,
|
|
7583
|
+
formatCostUsd = defaultCost4,
|
|
7584
|
+
className,
|
|
7585
|
+
"aria-label": ariaLabel = "Session summary"
|
|
7586
|
+
}) {
|
|
7587
|
+
const m = useMemo(() => metrics ?? aggregateSession(items ?? []), [metrics, items]);
|
|
7588
|
+
return /* @__PURE__ */ jsxs(
|
|
7589
|
+
"dl",
|
|
7590
|
+
{
|
|
7591
|
+
"data-slot": "session-summary",
|
|
7592
|
+
"aria-label": ariaLabel,
|
|
7593
|
+
className: cn(
|
|
7594
|
+
"grid grid-cols-2 gap-3 rounded-md border border-border bg-muted/20 p-3 text-xs sm:grid-cols-3 lg:grid-cols-6",
|
|
7595
|
+
className
|
|
7596
|
+
),
|
|
7597
|
+
children: [
|
|
7598
|
+
/* @__PURE__ */ jsx(Stat2, { label: "Traces", value: String(m.traceCount) }),
|
|
7599
|
+
/* @__PURE__ */ jsx(
|
|
7600
|
+
Stat2,
|
|
7601
|
+
{
|
|
7602
|
+
label: "Duration",
|
|
7603
|
+
value: m.windowMs > 0 ? formatDurationMs(m.windowMs) : "\u2014",
|
|
7604
|
+
testId: "session-duration"
|
|
7605
|
+
}
|
|
7606
|
+
),
|
|
7607
|
+
/* @__PURE__ */ jsx(Stat2, { label: "Cost", value: formatCostUsd(m.totalCostUsd), testId: "session-cost" }),
|
|
7608
|
+
/* @__PURE__ */ jsx(Stat2, { label: "Tokens", value: String(m.totalTokens), testId: "session-tokens" }),
|
|
7609
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
7610
|
+
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Errors" }),
|
|
7611
|
+
/* @__PURE__ */ jsx(
|
|
7612
|
+
"dd",
|
|
7613
|
+
{
|
|
7614
|
+
"data-slot": "session-error",
|
|
7615
|
+
"data-error": m.errorCount > 0 ? "true" : void 0,
|
|
7616
|
+
className: cn(
|
|
7617
|
+
"font-medium font-mono tabular-nums",
|
|
7618
|
+
m.errorCount > 0 && "text-destructive"
|
|
7619
|
+
),
|
|
7620
|
+
children: m.errorCount
|
|
7621
|
+
}
|
|
7622
|
+
)
|
|
7623
|
+
] }),
|
|
7624
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
7625
|
+
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Models" }),
|
|
7626
|
+
/* @__PURE__ */ jsx("dd", { "data-testid": "session-models", className: "flex flex-wrap gap-1", children: m.models.length > 0 ? m.models.map((model) => /* @__PURE__ */ jsx(BadgeWithDot, { variant: "outline", className: "font-mono", children: model }, model)) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" }) })
|
|
7627
|
+
] })
|
|
7628
|
+
]
|
|
7629
|
+
}
|
|
7630
|
+
);
|
|
7631
|
+
}
|
|
7632
|
+
function Stat2({ label, value, testId }) {
|
|
7633
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
7634
|
+
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: label }),
|
|
7635
|
+
/* @__PURE__ */ jsx("dd", { "data-testid": testId, className: "font-medium font-mono tabular-nums", children: value })
|
|
7636
|
+
] });
|
|
7637
|
+
}
|
|
7638
|
+
var defaultTimestamp2 = (ns) => new Date(Number(ns / 1000000n)).toISOString();
|
|
7639
|
+
var defaultCost5 = (usd) => `$${usd.toFixed(4)}`;
|
|
7640
|
+
function SessionTimeline({
|
|
7641
|
+
items,
|
|
7642
|
+
selectedId,
|
|
7643
|
+
onSelect,
|
|
7644
|
+
formatTimestamp = defaultTimestamp2,
|
|
7645
|
+
formatCostUsd = defaultCost5,
|
|
7646
|
+
virtualizeThreshold = 200,
|
|
7647
|
+
className,
|
|
7648
|
+
"aria-label": ariaLabel = "Session timeline"
|
|
7649
|
+
}) {
|
|
7650
|
+
const sorted = useMemo(() => {
|
|
7651
|
+
const withNs = items.map((it) => ({ it, ns: toNs(it.startTime) ?? 0n }));
|
|
7652
|
+
withNs.sort((a, b) => a.ns < b.ns ? -1 : a.ns > b.ns ? 1 : a.it.id < b.it.id ? -1 : 1);
|
|
7653
|
+
return withNs.map((w) => w.it);
|
|
7654
|
+
}, [items]);
|
|
7655
|
+
const bounds = useMemo(() => sessionBounds(sorted), [sorted]);
|
|
7656
|
+
if (sorted.length === 0) {
|
|
7657
|
+
return /* @__PURE__ */ jsx(EmptyState, { title: "No traces", description: "This session has no traces to replay." });
|
|
7658
|
+
}
|
|
7659
|
+
const virtualized = sorted.length >= virtualizeThreshold;
|
|
7660
|
+
const ctx = { bounds, selectedId, onSelect, formatTimestamp, formatCostUsd };
|
|
7661
|
+
return (
|
|
7662
|
+
// biome-ignore lint/a11y/useSemanticElements: role="list" keeps the virtualized wrapper divs valid (a <div> cannot be an <li>)
|
|
7663
|
+
/* @__PURE__ */ jsx(
|
|
7664
|
+
"div",
|
|
7665
|
+
{
|
|
7666
|
+
role: "list",
|
|
7667
|
+
"data-slot": "session-timeline",
|
|
7668
|
+
"aria-label": ariaLabel,
|
|
7669
|
+
className: cn("space-y-1", className),
|
|
7670
|
+
children: virtualized ? /* @__PURE__ */ jsx(VirtualRows, { items: sorted, ...ctx }) : sorted.map((it) => /* @__PURE__ */ jsx(Row4, { item: it, ...ctx }, it.id))
|
|
7671
|
+
}
|
|
7672
|
+
)
|
|
7673
|
+
);
|
|
7674
|
+
}
|
|
7675
|
+
function Row4({ item, ...ctx }) {
|
|
7676
|
+
const { leftPct, widthPct, unbounded } = computeBarLayout(
|
|
7677
|
+
item.startTime,
|
|
7678
|
+
item.endTime,
|
|
7679
|
+
ctx.bounds.startNs,
|
|
7680
|
+
ctx.bounds.endNs
|
|
7681
|
+
);
|
|
7682
|
+
const error = isSpanError(item.status);
|
|
7683
|
+
const selected = item.id === ctx.selectedId;
|
|
7684
|
+
const startNs = toNs(item.startTime);
|
|
7685
|
+
const dur = durationMs({ startTime: item.startTime, endTime: item.endTime });
|
|
7686
|
+
return (
|
|
7687
|
+
// biome-ignore lint/a11y/useSemanticElements: pairs with the role="list" container (virtualized wrappers forbid <li>)
|
|
7688
|
+
/* @__PURE__ */ jsx("div", { role: "listitem", children: /* @__PURE__ */ jsxs(
|
|
7689
|
+
"button",
|
|
7690
|
+
{
|
|
7691
|
+
type: "button",
|
|
7692
|
+
"data-slot": "session-timeline-row",
|
|
7693
|
+
"aria-current": selected ? "true" : void 0,
|
|
7694
|
+
onClick: () => ctx.onSelect(item.id),
|
|
7695
|
+
className: cn(
|
|
7696
|
+
"flex w-full items-center gap-3 rounded-md p-2 text-left hover:bg-muted/40",
|
|
7697
|
+
selected && "bg-muted ring-1 ring-primary"
|
|
7698
|
+
),
|
|
7699
|
+
children: [
|
|
7700
|
+
/* @__PURE__ */ jsx("span", { className: "w-32 shrink-0 font-mono text-[10px] text-muted-foreground", children: startNs !== null ? ctx.formatTimestamp(startNs) : "\u2014" }),
|
|
7701
|
+
/* @__PURE__ */ jsx("span", { "data-testid": "session-trace-name", className: "w-40 shrink-0 truncate font-mono text-xs", children: item.name ?? item.id }),
|
|
7702
|
+
/* @__PURE__ */ jsx(
|
|
7703
|
+
"span",
|
|
7704
|
+
{
|
|
7705
|
+
className: "relative block h-4 flex-1 overflow-hidden rounded-sm bg-muted/30",
|
|
7706
|
+
"aria-hidden": "true",
|
|
7707
|
+
children: /* @__PURE__ */ jsx(
|
|
7708
|
+
"span",
|
|
7709
|
+
{
|
|
7710
|
+
"data-slot": "session-timeline-bar",
|
|
7711
|
+
"data-error": error ? "true" : void 0,
|
|
7712
|
+
"data-unbounded": unbounded ? "true" : void 0,
|
|
7713
|
+
style: { left: `${leftPct}%`, width: `${widthPct}%`, minWidth: "2px" },
|
|
7714
|
+
className: cn(
|
|
7715
|
+
"absolute top-0.5 bottom-0.5 rounded-sm",
|
|
7716
|
+
error ? "bg-destructive" : "bg-primary/60",
|
|
7717
|
+
unbounded && "border border-primary/60 border-dashed bg-transparent"
|
|
7718
|
+
)
|
|
7719
|
+
}
|
|
7720
|
+
)
|
|
7721
|
+
}
|
|
7722
|
+
),
|
|
7723
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-[10px] text-muted-foreground tabular-nums", children: dur !== null ? formatDurationMs(dur) : "\u2014" }),
|
|
7724
|
+
item.costUsd !== void 0 ? /* @__PURE__ */ jsx(BadgeWithDot, { variant: "outline", className: "shrink-0 font-mono text-[10px]", children: ctx.formatCostUsd(item.costUsd) }) : null,
|
|
7725
|
+
error ? /* @__PURE__ */ jsx(BadgeWithDot, { variant: "destructive", className: "shrink-0", children: "error" }) : null
|
|
7726
|
+
]
|
|
7727
|
+
}
|
|
7728
|
+
) })
|
|
7729
|
+
);
|
|
7730
|
+
}
|
|
7731
|
+
function VirtualRows({ items, ...ctx }) {
|
|
7732
|
+
const parentRef = useRef(null);
|
|
7733
|
+
const virtualizer = useVirtualizer({
|
|
7734
|
+
count: items.length,
|
|
7735
|
+
getScrollElement: () => parentRef.current,
|
|
7736
|
+
estimateSize: () => 44,
|
|
7737
|
+
overscan: 12,
|
|
7738
|
+
initialRect: { width: 640, height: 640 }
|
|
7739
|
+
});
|
|
7740
|
+
return /* @__PURE__ */ jsx(
|
|
7741
|
+
"div",
|
|
7742
|
+
{
|
|
7743
|
+
ref: parentRef,
|
|
7744
|
+
"data-slot": "session-timeline-virtual",
|
|
7745
|
+
className: "max-h-[70vh] overflow-auto",
|
|
7746
|
+
children: /* @__PURE__ */ jsx("div", { style: { height: virtualizer.getTotalSize(), position: "relative", width: "100%" }, children: virtualizer.getVirtualItems().map((vi) => {
|
|
7747
|
+
const item = items[vi.index];
|
|
7748
|
+
if (!item) return null;
|
|
7749
|
+
return /* @__PURE__ */ jsx(
|
|
7750
|
+
"div",
|
|
7751
|
+
{
|
|
7752
|
+
"data-index": vi.index,
|
|
7753
|
+
ref: virtualizer.measureElement,
|
|
7754
|
+
style: {
|
|
7755
|
+
position: "absolute",
|
|
7756
|
+
top: 0,
|
|
7757
|
+
left: 0,
|
|
7758
|
+
width: "100%",
|
|
7759
|
+
transform: `translateY(${vi.start}px)`
|
|
7760
|
+
},
|
|
7761
|
+
children: /* @__PURE__ */ jsx(Row4, { item, ...ctx })
|
|
7762
|
+
},
|
|
7763
|
+
`${vi.index}-${item.id}`
|
|
7764
|
+
);
|
|
7765
|
+
}) })
|
|
7766
|
+
}
|
|
7767
|
+
);
|
|
7768
|
+
}
|
|
7769
|
+
|
|
7770
|
+
// src/lib/chart/histogram.ts
|
|
7771
|
+
function computeHistogram(values, binCount) {
|
|
7772
|
+
if (!Number.isFinite(binCount) || binCount <= 0) return [];
|
|
7773
|
+
const finite = values.filter((v) => Number.isFinite(v));
|
|
7774
|
+
if (finite.length === 0) return [];
|
|
7775
|
+
const n = Math.floor(binCount);
|
|
7776
|
+
let min = Number.POSITIVE_INFINITY;
|
|
7777
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
7778
|
+
for (const v of finite) {
|
|
7779
|
+
if (v < min) min = v;
|
|
7780
|
+
if (v > max) max = v;
|
|
7781
|
+
}
|
|
7782
|
+
const span = max - min;
|
|
7783
|
+
const width = span / n;
|
|
7784
|
+
const bins = Array.from({ length: n }, (_, i) => ({
|
|
7785
|
+
lo: span === 0 ? min : min + i * width,
|
|
7786
|
+
hi: span === 0 ? min : min + (i + 1) * width,
|
|
7787
|
+
count: 0
|
|
7788
|
+
}));
|
|
7789
|
+
for (const v of finite) {
|
|
7790
|
+
const idx = span === 0 ? 0 : Math.min(n - 1, Math.floor((v - min) / width));
|
|
7791
|
+
const bin = bins[idx];
|
|
7792
|
+
if (bin) bin.count += 1;
|
|
7793
|
+
}
|
|
7794
|
+
return bins;
|
|
7795
|
+
}
|
|
7796
|
+
var VIEW_W2 = 600;
|
|
7797
|
+
var PAD3 = { top: 8, right: 8, bottom: 4, left: 40 };
|
|
7798
|
+
var BAR_GAP = 2;
|
|
7799
|
+
var Histogram = forwardRef((props, ref) => {
|
|
7800
|
+
const {
|
|
7801
|
+
title,
|
|
7802
|
+
height = 180,
|
|
7803
|
+
valueFormatter = (v) => String(v),
|
|
7804
|
+
className,
|
|
7805
|
+
bins: binsProp,
|
|
7806
|
+
values,
|
|
7807
|
+
binCount,
|
|
7808
|
+
...rest
|
|
7809
|
+
} = props;
|
|
7810
|
+
const bins = binsProp ?? computeHistogram(values ?? [], binCount ?? 0);
|
|
7811
|
+
const hasData = bins.length > 0 && bins.some((b) => b.count > 0);
|
|
7812
|
+
if (!hasData) {
|
|
7813
|
+
return /* @__PURE__ */ jsxs(
|
|
7814
|
+
"figure",
|
|
7815
|
+
{
|
|
7816
|
+
"data-slot": "histogram",
|
|
7817
|
+
ref,
|
|
7818
|
+
className: cn("space-y-1", className),
|
|
7819
|
+
"aria-label": `${title} \u2014 no data`,
|
|
7820
|
+
...rest,
|
|
7821
|
+
children: [
|
|
7822
|
+
/* @__PURE__ */ jsx("figcaption", { className: "font-semibold text-label-caps text-muted-foreground uppercase", children: title }),
|
|
7823
|
+
/* @__PURE__ */ jsx(
|
|
7824
|
+
"div",
|
|
7825
|
+
{
|
|
7826
|
+
"data-slot": "histogram-empty",
|
|
7827
|
+
className: "flex items-center justify-center rounded-md border border-dashed text-body-sm text-muted-foreground",
|
|
7828
|
+
style: { height },
|
|
7829
|
+
children: "No data to distribute."
|
|
7830
|
+
}
|
|
7831
|
+
)
|
|
7832
|
+
]
|
|
7833
|
+
}
|
|
7834
|
+
);
|
|
7835
|
+
}
|
|
7836
|
+
const countMax = niceMax(bins.map((b) => b.count)) || 1;
|
|
7837
|
+
const yScale = linScale([0, countMax], [height - PAD3.bottom, PAD3.top]);
|
|
7838
|
+
const plotW = VIEW_W2 - PAD3.left - PAD3.right;
|
|
7839
|
+
const slotW = plotW / bins.length;
|
|
7840
|
+
const barW = Math.max(1, slotW - BAR_GAP);
|
|
7841
|
+
return /* @__PURE__ */ jsxs("figure", { "data-slot": "histogram", ref, className: cn("space-y-1", className), ...rest, children: [
|
|
7842
|
+
/* @__PURE__ */ jsx("figcaption", { className: "font-semibold text-label-caps text-muted-foreground uppercase", children: title }),
|
|
7843
|
+
/* @__PURE__ */ jsxs(
|
|
7844
|
+
"svg",
|
|
7845
|
+
{
|
|
7846
|
+
viewBox: `0 0 ${VIEW_W2} ${height}`,
|
|
7847
|
+
preserveAspectRatio: "none",
|
|
7848
|
+
role: "img",
|
|
7849
|
+
"aria-label": `${title} distribution`,
|
|
7850
|
+
className: "w-full",
|
|
7851
|
+
style: { height },
|
|
7852
|
+
children: [
|
|
7853
|
+
/* @__PURE__ */ jsx(
|
|
7854
|
+
"line",
|
|
7855
|
+
{
|
|
7856
|
+
x1: PAD3.left,
|
|
7857
|
+
y1: height - PAD3.bottom,
|
|
7858
|
+
x2: VIEW_W2 - PAD3.right,
|
|
7859
|
+
y2: height - PAD3.bottom,
|
|
7860
|
+
className: "stroke-border",
|
|
7861
|
+
strokeWidth: 1
|
|
7862
|
+
}
|
|
7863
|
+
),
|
|
7864
|
+
/* @__PURE__ */ jsx("text", { x: 4, y: PAD3.top + 8, className: "fill-muted-foreground text-[10px]", children: valueFormatter(countMax) }),
|
|
7865
|
+
/* @__PURE__ */ jsx("text", { x: 4, y: height - PAD3.bottom, className: "fill-muted-foreground text-[10px]", children: "0" }),
|
|
7866
|
+
bins.map((b, i) => {
|
|
7867
|
+
const y = yScale(b.count);
|
|
7868
|
+
const barH = Math.max(0, height - PAD3.bottom - y);
|
|
7869
|
+
return /* @__PURE__ */ jsx(
|
|
7870
|
+
"rect",
|
|
7871
|
+
{
|
|
7872
|
+
"data-slot": "histogram-bar",
|
|
7873
|
+
"data-count": b.count,
|
|
7874
|
+
x: PAD3.left + i * slotW + BAR_GAP / 2,
|
|
7875
|
+
y,
|
|
7876
|
+
width: barW,
|
|
7877
|
+
height: barH,
|
|
7878
|
+
className: "fill-primary",
|
|
7879
|
+
children: /* @__PURE__ */ jsx("title", { children: `${valueFormatter(b.lo)}\u2013${valueFormatter(b.hi)}: ${b.count}` })
|
|
7880
|
+
},
|
|
7881
|
+
i
|
|
7882
|
+
);
|
|
7883
|
+
})
|
|
7884
|
+
]
|
|
7885
|
+
}
|
|
7886
|
+
),
|
|
7887
|
+
/* @__PURE__ */ jsxs("table", { "data-slot": "histogram-table", className: "sr-only", children: [
|
|
7888
|
+
/* @__PURE__ */ jsxs("caption", { children: [
|
|
7889
|
+
title,
|
|
7890
|
+
" \u2014 data table"
|
|
7891
|
+
] }),
|
|
7892
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
7893
|
+
/* @__PURE__ */ jsx("th", { children: "Range" }),
|
|
7894
|
+
/* @__PURE__ */ jsx("th", { children: "Count" })
|
|
7895
|
+
] }) }),
|
|
7896
|
+
/* @__PURE__ */ jsx("tbody", { children: bins.map((b, i) => (
|
|
7897
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: bins are positional buckets — index IS the identity
|
|
7898
|
+
/* @__PURE__ */ jsxs("tr", { children: [
|
|
7899
|
+
/* @__PURE__ */ jsx("td", { children: `${valueFormatter(b.lo)}\u2013${valueFormatter(b.hi)}` }),
|
|
7900
|
+
/* @__PURE__ */ jsx("td", { children: b.count })
|
|
7901
|
+
] }, i)
|
|
7902
|
+
)) })
|
|
7903
|
+
] })
|
|
7904
|
+
] });
|
|
7905
|
+
});
|
|
7906
|
+
Histogram.displayName = "Histogram";
|
|
7907
|
+
var VIEW_W3 = 600;
|
|
7908
|
+
var PAD4 = { top: 8, right: 8, bottom: 4, left: 40 };
|
|
7909
|
+
function bandPath(upper, lower, xScale, yScale) {
|
|
7910
|
+
const runs = [];
|
|
7911
|
+
let run = [];
|
|
7912
|
+
for (let i = 0; i < upper.length; i++) {
|
|
7913
|
+
const u = upper[i];
|
|
7914
|
+
const l = lower[i];
|
|
7915
|
+
if (u !== void 0 && l !== void 0 && Number.isFinite(u) && Number.isFinite(l)) {
|
|
7916
|
+
run.push(i);
|
|
7917
|
+
} else if (run.length > 0) {
|
|
7918
|
+
runs.push(run);
|
|
7919
|
+
run = [];
|
|
7920
|
+
}
|
|
7921
|
+
}
|
|
7922
|
+
if (run.length > 0) runs.push(run);
|
|
7923
|
+
return runs.map((idxs) => {
|
|
7924
|
+
const top = idxs.map(
|
|
7925
|
+
(i, k) => `${k === 0 ? "M" : "L"}${xScale(i).toFixed(2)},${yScale(upper[i]).toFixed(2)}`
|
|
7926
|
+
);
|
|
7927
|
+
const bottom = idxs.slice().reverse().map((i) => `L${xScale(i).toFixed(2)},${yScale(lower[i]).toFixed(2)}`);
|
|
7928
|
+
return `${top.join(" ")} ${bottom.join(" ")} Z`;
|
|
7929
|
+
}).join(" ");
|
|
7930
|
+
}
|
|
7931
|
+
var PercentileChart = forwardRef((props, ref) => {
|
|
7932
|
+
const {
|
|
7933
|
+
title,
|
|
7934
|
+
buckets,
|
|
7935
|
+
height = 180,
|
|
7936
|
+
valueFormatter = (v) => String(v),
|
|
7937
|
+
className,
|
|
7938
|
+
...rest
|
|
7939
|
+
} = props;
|
|
7940
|
+
const hasData = buckets.length > 0;
|
|
7941
|
+
if (!hasData) {
|
|
7942
|
+
return /* @__PURE__ */ jsxs(
|
|
7943
|
+
"figure",
|
|
7944
|
+
{
|
|
7945
|
+
"data-slot": "percentile-chart",
|
|
7946
|
+
ref,
|
|
7947
|
+
className: cn("space-y-1", className),
|
|
7948
|
+
"aria-label": `${title} \u2014 no data`,
|
|
7949
|
+
...rest,
|
|
7950
|
+
children: [
|
|
7951
|
+
/* @__PURE__ */ jsx("figcaption", { className: "font-semibold text-label-caps text-muted-foreground uppercase", children: title }),
|
|
7952
|
+
/* @__PURE__ */ jsx(
|
|
7953
|
+
"div",
|
|
7954
|
+
{
|
|
7955
|
+
"data-slot": "percentile-empty",
|
|
7956
|
+
className: "flex items-center justify-center rounded-md border border-dashed text-body-sm text-muted-foreground",
|
|
7957
|
+
style: { height },
|
|
7958
|
+
children: "No data in this time range."
|
|
7959
|
+
}
|
|
7960
|
+
)
|
|
7961
|
+
]
|
|
7962
|
+
}
|
|
7963
|
+
);
|
|
7964
|
+
}
|
|
7965
|
+
const p50 = buckets.map((b) => b.p50);
|
|
7966
|
+
const p95 = buckets.map((b) => b.p95);
|
|
7967
|
+
const p99 = buckets.map((b) => b.p99);
|
|
7968
|
+
const yMax = niceMax([...p99]) || 1;
|
|
7969
|
+
const xScale = linScale([0, Math.max(1, buckets.length - 1)], [PAD4.left, VIEW_W3 - PAD4.right]);
|
|
7970
|
+
const yScale = linScale([0, yMax], [height - PAD4.bottom, PAD4.top]);
|
|
7971
|
+
const p50Points = p50.map((y, x) => ({ x, y }));
|
|
7972
|
+
return /* @__PURE__ */ jsxs("figure", { "data-slot": "percentile-chart", ref, className: cn("space-y-1", className), ...rest, children: [
|
|
7973
|
+
/* @__PURE__ */ jsx("figcaption", { className: "font-semibold text-label-caps text-muted-foreground uppercase", children: title }),
|
|
7974
|
+
/* @__PURE__ */ jsxs(
|
|
7975
|
+
"svg",
|
|
7976
|
+
{
|
|
7977
|
+
viewBox: `0 0 ${VIEW_W3} ${height}`,
|
|
7978
|
+
preserveAspectRatio: "none",
|
|
7979
|
+
role: "img",
|
|
7980
|
+
"aria-label": `${title} percentiles`,
|
|
7981
|
+
className: "w-full",
|
|
7982
|
+
style: { height },
|
|
7983
|
+
children: [
|
|
7984
|
+
/* @__PURE__ */ jsx(
|
|
7985
|
+
"line",
|
|
7986
|
+
{
|
|
7987
|
+
x1: PAD4.left,
|
|
7988
|
+
y1: height - PAD4.bottom,
|
|
7989
|
+
x2: VIEW_W3 - PAD4.right,
|
|
7990
|
+
y2: height - PAD4.bottom,
|
|
7991
|
+
className: "stroke-border",
|
|
7992
|
+
strokeWidth: 1
|
|
7993
|
+
}
|
|
7994
|
+
),
|
|
7995
|
+
/* @__PURE__ */ jsx("text", { x: 4, y: PAD4.top + 8, className: "fill-muted-foreground text-[10px]", children: valueFormatter(yMax) }),
|
|
7996
|
+
/* @__PURE__ */ jsx("text", { x: 4, y: height - PAD4.bottom, className: "fill-muted-foreground text-[10px]", children: "0" }),
|
|
7997
|
+
/* @__PURE__ */ jsx(
|
|
7998
|
+
"path",
|
|
7999
|
+
{
|
|
8000
|
+
"data-slot": "percentile-band",
|
|
8001
|
+
"data-band": "p95-p99",
|
|
8002
|
+
d: bandPath(p99, p95, xScale, yScale),
|
|
8003
|
+
className: "fill-primary",
|
|
8004
|
+
fillOpacity: 0.15,
|
|
8005
|
+
children: /* @__PURE__ */ jsx("title", { children: "p95\u2013p99" })
|
|
8006
|
+
}
|
|
8007
|
+
),
|
|
8008
|
+
/* @__PURE__ */ jsx(
|
|
8009
|
+
"path",
|
|
8010
|
+
{
|
|
8011
|
+
"data-slot": "percentile-band",
|
|
8012
|
+
"data-band": "p50-p95",
|
|
8013
|
+
d: bandPath(p95, p50, xScale, yScale),
|
|
8014
|
+
className: "fill-primary",
|
|
8015
|
+
fillOpacity: 0.3,
|
|
8016
|
+
children: /* @__PURE__ */ jsx("title", { children: "p50\u2013p95" })
|
|
8017
|
+
}
|
|
8018
|
+
),
|
|
8019
|
+
/* @__PURE__ */ jsx(
|
|
8020
|
+
"path",
|
|
8021
|
+
{
|
|
8022
|
+
"data-slot": "percentile-p50-line",
|
|
8023
|
+
d: seriesPath(p50Points, xScale, yScale),
|
|
8024
|
+
fill: "none",
|
|
8025
|
+
className: "stroke-primary",
|
|
8026
|
+
strokeWidth: 1.5,
|
|
8027
|
+
children: /* @__PURE__ */ jsx("title", { children: "p50" })
|
|
8028
|
+
}
|
|
8029
|
+
)
|
|
8030
|
+
]
|
|
8031
|
+
}
|
|
8032
|
+
),
|
|
8033
|
+
/* @__PURE__ */ jsxs("table", { "data-slot": "percentile-table", className: "sr-only", children: [
|
|
8034
|
+
/* @__PURE__ */ jsxs("caption", { children: [
|
|
8035
|
+
title,
|
|
8036
|
+
" \u2014 data table"
|
|
8037
|
+
] }),
|
|
8038
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
8039
|
+
/* @__PURE__ */ jsx("th", { children: "Bucket" }),
|
|
8040
|
+
/* @__PURE__ */ jsx("th", { children: "p50" }),
|
|
8041
|
+
/* @__PURE__ */ jsx("th", { children: "p95" }),
|
|
8042
|
+
/* @__PURE__ */ jsx("th", { children: "p99" })
|
|
8043
|
+
] }) }),
|
|
8044
|
+
/* @__PURE__ */ jsx("tbody", { children: buckets.map((b, i) => (
|
|
8045
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: buckets are positional time slots — index IS the identity
|
|
8046
|
+
/* @__PURE__ */ jsxs("tr", { children: [
|
|
8047
|
+
/* @__PURE__ */ jsx("td", { children: b.label ?? i + 1 }),
|
|
8048
|
+
/* @__PURE__ */ jsx("td", { children: Number.isFinite(b.p50) ? valueFormatter(b.p50) : "\u2014" }),
|
|
8049
|
+
/* @__PURE__ */ jsx("td", { children: Number.isFinite(b.p95) ? valueFormatter(b.p95) : "\u2014" }),
|
|
8050
|
+
/* @__PURE__ */ jsx("td", { children: Number.isFinite(b.p99) ? valueFormatter(b.p99) : "\u2014" })
|
|
8051
|
+
] }, i)
|
|
8052
|
+
)) })
|
|
8053
|
+
] })
|
|
8054
|
+
] });
|
|
8055
|
+
});
|
|
8056
|
+
PercentileChart.displayName = "PercentileChart";
|
|
8057
|
+
function AnnotationInput(props) {
|
|
8058
|
+
const { name, description, required, disabled, className } = props;
|
|
8059
|
+
const generatedId = useId();
|
|
8060
|
+
const controlId = props.id ?? generatedId;
|
|
8061
|
+
const descId = description ? `${controlId}-desc` : void 0;
|
|
8062
|
+
const isCategorical = props.config.type === "categorical";
|
|
8063
|
+
const labelId = isCategorical ? `${controlId}-label` : void 0;
|
|
8064
|
+
const label = /* @__PURE__ */ jsx(Label, { id: labelId, htmlFor: isCategorical ? void 0 : controlId, required, children: name });
|
|
8065
|
+
const a11y = {
|
|
8066
|
+
"aria-required": required || void 0,
|
|
8067
|
+
"aria-describedby": descId,
|
|
8068
|
+
disabled
|
|
8069
|
+
};
|
|
8070
|
+
let control = null;
|
|
8071
|
+
if (props.config.type === "categorical") {
|
|
8072
|
+
const { config, value, onValueChange } = props;
|
|
8073
|
+
control = /* @__PURE__ */ jsx(
|
|
8074
|
+
RadioGroup,
|
|
8075
|
+
{
|
|
8076
|
+
value: value ?? "",
|
|
8077
|
+
onValueChange: (v) => onValueChange(v === "" ? null : v),
|
|
8078
|
+
"aria-labelledby": labelId,
|
|
8079
|
+
className: "gap-1.5",
|
|
8080
|
+
...a11y,
|
|
8081
|
+
children: config.options.map((opt) => {
|
|
8082
|
+
const optId = `${controlId}-${opt.label}`;
|
|
8083
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
8084
|
+
/* @__PURE__ */ jsx(RadioGroup.Item, { value: opt.label, id: optId, disabled }),
|
|
8085
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: optId, className: "font-normal", children: opt.label })
|
|
8086
|
+
] }, opt.label);
|
|
8087
|
+
})
|
|
8088
|
+
}
|
|
8089
|
+
);
|
|
8090
|
+
} else if (props.config.type === "continuous") {
|
|
8091
|
+
const { config, value, onValueChange } = props;
|
|
8092
|
+
const onNumber = (e) => {
|
|
8093
|
+
const raw = e.target.value;
|
|
8094
|
+
if (raw === "") return onValueChange(null);
|
|
8095
|
+
const n = Number(raw);
|
|
8096
|
+
onValueChange(Number.isFinite(n) ? n : null);
|
|
8097
|
+
};
|
|
8098
|
+
control = /* @__PURE__ */ jsx(
|
|
8099
|
+
Input,
|
|
8100
|
+
{
|
|
8101
|
+
...a11y,
|
|
8102
|
+
id: controlId,
|
|
8103
|
+
type: "number",
|
|
8104
|
+
min: config.min,
|
|
8105
|
+
max: config.max,
|
|
8106
|
+
step: config.step,
|
|
8107
|
+
value: value ?? "",
|
|
8108
|
+
onChange: onNumber
|
|
8109
|
+
}
|
|
8110
|
+
);
|
|
8111
|
+
} else if (props.config.type === "freeform") {
|
|
8112
|
+
const { config, value, onValueChange } = props;
|
|
8113
|
+
const onText = (e) => onValueChange(e.target.value === "" ? null : e.target.value);
|
|
8114
|
+
control = /* @__PURE__ */ jsx(
|
|
8115
|
+
Textarea,
|
|
8116
|
+
{
|
|
8117
|
+
...a11y,
|
|
8118
|
+
id: controlId,
|
|
8119
|
+
maxLength: config.maxLength,
|
|
8120
|
+
value: value ?? "",
|
|
8121
|
+
onChange: onText
|
|
8122
|
+
}
|
|
8123
|
+
);
|
|
8124
|
+
}
|
|
8125
|
+
return /* @__PURE__ */ jsxs(
|
|
8126
|
+
"div",
|
|
8127
|
+
{
|
|
8128
|
+
"data-slot": "annotation-input",
|
|
8129
|
+
"data-type": props.config.type,
|
|
8130
|
+
className: cn("space-y-1.5", className),
|
|
8131
|
+
children: [
|
|
8132
|
+
label,
|
|
8133
|
+
description ? /* @__PURE__ */ jsx("p", { id: descId, className: "text-body-sm text-muted-foreground", children: description }) : null,
|
|
8134
|
+
control
|
|
8135
|
+
]
|
|
8136
|
+
}
|
|
8137
|
+
);
|
|
8138
|
+
}
|
|
8139
|
+
|
|
8140
|
+
// src/components/composites/annotation-input/types.ts
|
|
8141
|
+
function isCategoricalConfig(c) {
|
|
8142
|
+
return c.type === "categorical";
|
|
8143
|
+
}
|
|
8144
|
+
function isContinuousConfig(c) {
|
|
8145
|
+
return c.type === "continuous";
|
|
8146
|
+
}
|
|
8147
|
+
function isFreeformConfig(c) {
|
|
8148
|
+
return c.type === "freeform";
|
|
8149
|
+
}
|
|
8150
|
+
|
|
8151
|
+
export { AccountMenu, ActionBar, Alert, AnnotationInput, AttributesTable, Avatar, BadgeWithDot as Badge, Breadcrumb, Button, Card, Checkbox, CodeBlock, Combobox, CommandPalette, ConfirmDialog, CopyButton, DangerZone, DataTable, DeploymentRow, DescriptionList, Dialog, DomainConfig, DropdownMenu, EmptyState, EnvVarEditor, FileDropzone, FormField, GRAPH_NODE_CAP, Histogram, IOCards, Input, JsonViewer, KIND_LABEL, Label, LoginSplit, MessageCard, MessageItem, MetricCard, MetricsPanel, PageShell, Pagination, PercentileChart, PinInput, PlanBadge, PreviewEnvCard, Progress, ProjectCard, RadioGroup, RollbackUI, ScrollArea, Select, SessionSummary, SessionTimeline, Sheet, Sidebar, Skeleton, Slider, SocialAuthRow, SpanGraph, SpanTree, SpanTreeRow, SpanWaterfall, StatTile, StatusDot, StatusIndicator, Stepper, Switch, Table, Tabs, TaskHeader, Textarea, Timestamp, Toast, Toaster, TooltipWithStatics as Tooltip, TopNav, TraceCompare, TraceTranscript, TrendChart, UpdateBanner, VIRTUALIZE_THRESHOLD, aggregateCost, aggregateSession, alignSpanTrees, asChat, avatarVariants, badgeVariants, buildLayeredGraph, buttonVariants, cn, computeBarLayout, computeHistogram, computePageRange, computeTraceBounds, contentText, deriveSpanKind, deriveSteps, durationMs, flattenAll, flattenVisible, groupByNamespace, isCategoricalConfig, isContinuousConfig, isFreeformConfig, isNearBottom, isRedactedThinking, isSpanError, linScale, matchesAccept, niceAxisTicks, niceMax, packRows, prettyValue, seriesPath, sessionBounds, sheetVariants, spanCostUsd, toNs, toTranscriptRows, traceMetrics, useStickToBottom, useToast, validateFiles };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure histogram binning — distributes numeric values into `binCount`
|
|
3
|
+
* equal-width bins over [min, max] and counts how many fall in each.
|
|
4
|
+
*
|
|
5
|
+
* Zero-dependency, total, deterministic (M11 ADR D2 — binning is pure logic,
|
|
6
|
+
* distinct from the SVG scaling reused from TrendChart). Non-finite values
|
|
7
|
+
* (NaN / ±Infinity) are dropped, mirroring the trace-core `Number.isFinite`
|
|
8
|
+
* discipline. A degenerate range (all values equal) collapses to a single
|
|
9
|
+
* populated bin so no count is lost.
|
|
10
|
+
*/
|
|
11
|
+
/** One histogram bucket: half-open [lo, hi) except the final bin, which is [lo, hi] (max-inclusive). */
|
|
12
|
+
export interface HistogramBin {
|
|
13
|
+
lo: number;
|
|
14
|
+
hi: number;
|
|
15
|
+
count: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Bin `values` into `binCount` equal-width buckets.
|
|
19
|
+
* Returns `[]` for an empty input or a non-positive `binCount`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function computeHistogram(values: number[], binCount: number): HistogramBin[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./histogram.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, total session-metric aggregation. Never throws, never does I/O. Honest sums: absent
|
|
3
|
+
* cost/tokens count as 0 (never NaN); the window is max(end) − min(start) over parseable timestamps
|
|
4
|
+
* (an unparseable start is skipped from the window, never a crash).
|
|
5
|
+
*/
|
|
6
|
+
import type { SessionMetrics, SessionTraceItem } from "./types.js";
|
|
7
|
+
/**
|
|
8
|
+
* The session's wall-clock window in ns: [min start, max effective end] over the parseable
|
|
9
|
+
* timestamps. An unparseable start is skipped (never a crash); a clock-skewed/absent end falls back
|
|
10
|
+
* to its own start. A session with no parseable timestamps yields 0/0. This is the SINGLE definition
|
|
11
|
+
* of "a session's time window" — both `aggregateSession` (for `windowMs`) and `SessionTimeline` (for
|
|
12
|
+
* the per-trace bar denominator) consume it, so the window semantics live in one place.
|
|
13
|
+
*/
|
|
14
|
+
export declare function sessionBounds(items: SessionTraceItem[]): {
|
|
15
|
+
startNs: bigint;
|
|
16
|
+
endNs: bigint;
|
|
17
|
+
};
|
|
18
|
+
export declare function aggregateSession(items: SessionTraceItem[]): SessionMetrics;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data contracts for the session-level components (M9). A session groups traces that share a
|
|
3
|
+
* `session.id`; `SessionTraceItem` is the trace-level shape both `SessionSummary` and
|
|
4
|
+
* `SessionTimeline` consume — the intersection of the theo-lens traces-list row and the peer
|
|
5
|
+
* session views (phoenix numTraces+latency+cost, langfuse timestamp+name+latency). Timestamps
|
|
6
|
+
* accept unix-ns (bigint | numeric string) or ISO — normalized by the trace-core `toNs`.
|
|
7
|
+
*/
|
|
8
|
+
export interface SessionTraceItem {
|
|
9
|
+
id: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
/** Unix ns (bigint | numeric string) or ISO-8601 string. */
|
|
12
|
+
startTime?: bigint | string;
|
|
13
|
+
/** Absent/null = in-flight (renders unbounded on the timeline). */
|
|
14
|
+
endTime?: bigint | string | null;
|
|
15
|
+
status?: "OK" | "ERROR";
|
|
16
|
+
model?: string;
|
|
17
|
+
costUsd?: number;
|
|
18
|
+
totalTokens?: number;
|
|
19
|
+
}
|
|
20
|
+
/** Aggregate metrics over a session's traces. All numeric fields are 0 (never NaN) when absent. */
|
|
21
|
+
export interface SessionMetrics {
|
|
22
|
+
traceCount: number;
|
|
23
|
+
/** Session wall-clock window in ms: max(end) − min(start) across all traces. */
|
|
24
|
+
windowMs: number;
|
|
25
|
+
totalCostUsd: number;
|
|
26
|
+
totalTokens: number;
|
|
27
|
+
errorCount: number;
|
|
28
|
+
/** Distinct models seen, sorted. */
|
|
29
|
+
models: string[];
|
|
30
|
+
}
|
package/package.json
CHANGED