@oh-my-pi/omp-stats 18.3.4 → 18.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +13 -11
- package/build.ts +5 -49
- package/dist/client/index.css +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/index.js +105 -126
- package/dist/types/aggregator.d.ts +25 -22
- package/dist/types/client/api.d.ts +11 -7
- package/dist/types/client/app/LiveChip.d.ts +2 -0
- package/dist/types/client/app/Shell.d.ts +16 -0
- package/dist/types/client/app/ThemeToggle.d.ts +1 -0
- package/dist/types/client/app/nav.d.ts +18 -0
- package/dist/types/client/charts/BarList.d.ts +17 -0
- package/dist/types/client/charts/Chart.d.ts +49 -0
- package/dist/types/client/charts/Legend.d.ts +16 -0
- package/dist/types/client/charts/ShareBar.d.ts +11 -0
- package/dist/types/client/charts/Sparkline.d.ts +11 -0
- package/dist/types/client/charts/TimeChart.d.ts +9 -0
- package/dist/types/client/charts/index.d.ts +7 -0
- package/dist/types/client/charts/types.d.ts +25 -0
- package/dist/types/client/charts/useWidth.d.ts +3 -0
- package/dist/types/client/data/colors.d.ts +25 -0
- package/dist/types/client/data/formatters.d.ts +9 -1
- package/dist/types/client/data/live.d.ts +29 -0
- package/dist/types/client/data/query.d.ts +35 -0
- package/dist/types/client/data/range.d.ts +31 -0
- package/dist/types/client/data/series.d.ts +28 -0
- package/dist/types/client/data/useHashRoute.d.ts +18 -4
- package/dist/types/client/data/view-models.d.ts +111 -28
- package/dist/types/client/index.d.ts +0 -1
- package/dist/types/client/routes/CostsRoute.d.ts +2 -2
- package/dist/types/client/routes/ErrorsRoute.d.ts +2 -2
- package/dist/types/client/routes/FrustrationRoute.d.ts +7 -0
- package/dist/types/client/routes/GainRoute.d.ts +1 -2
- package/dist/types/client/routes/ModelsRoute.d.ts +2 -2
- package/dist/types/client/routes/OverviewRoute.d.ts +1 -2
- package/dist/types/client/routes/ProjectsRoute.d.ts +2 -2
- package/dist/types/client/routes/ProvidersRoute.d.ts +2 -2
- package/dist/types/client/routes/RequestsRoute.d.ts +1 -2
- package/dist/types/client/routes/ToolsRoute.d.ts +2 -2
- package/dist/types/client/routes/TracesRoute.d.ts +2 -2
- package/dist/types/client/routes/index.d.ts +1 -1
- package/dist/types/client/traces/AggregatesPanel.d.ts +4 -3
- package/dist/types/client/traces/SummaryStrip.d.ts +2 -2
- package/dist/types/client/traces/trace-colors.d.ts +18 -12
- package/dist/types/client/ui/Badge.d.ts +19 -0
- package/dist/types/client/ui/Card.d.ts +25 -0
- package/dist/types/client/ui/Drawer.d.ts +25 -0
- package/dist/types/client/ui/JsonBlock.d.ts +2 -2
- package/dist/types/client/ui/Modal.d.ts +23 -0
- package/dist/types/client/ui/RequestDrawer.d.ts +10 -1
- package/dist/types/client/ui/SearchInput.d.ts +8 -0
- package/dist/types/client/ui/Segmented.d.ts +15 -0
- package/dist/types/client/ui/Stat.d.ts +36 -0
- package/dist/types/client/ui/States.d.ts +41 -0
- package/dist/types/client/ui/Table.d.ts +48 -0
- package/dist/types/client/ui/index.d.ts +9 -10
- package/dist/types/db.d.ts +90 -106
- package/dist/types/frustration.d.ts +70 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/live.d.ts +30 -0
- package/dist/types/rollup.d.ts +115 -0
- package/dist/types/server.d.ts +6 -1
- package/dist/types/shared-types.d.ts +116 -56
- package/dist/types/types.d.ts +4 -0
- package/dist/types/user-metrics.d.ts +8 -0
- package/package.json +7 -13
- package/src/aggregator.ts +276 -211
- package/src/client/App.tsx +38 -80
- package/src/client/api.ts +65 -7
- package/src/client/app/LiveChip.tsx +97 -0
- package/src/client/app/Shell.tsx +181 -0
- package/src/client/app/ThemeToggle.tsx +5 -2
- package/src/client/app/nav.ts +78 -0
- package/src/client/charts/BarList.tsx +48 -0
- package/src/client/charts/Chart.tsx +388 -0
- package/src/client/charts/Legend.tsx +61 -0
- package/src/client/charts/ShareBar.tsx +26 -0
- package/src/client/charts/Sparkline.tsx +38 -0
- package/src/client/charts/TimeChart.tsx +22 -0
- package/src/client/charts/index.ts +7 -0
- package/src/client/charts/types.ts +27 -0
- package/src/client/charts/useWidth.ts +19 -0
- package/src/client/data/colors.ts +45 -0
- package/src/client/data/formatters.ts +30 -9
- package/src/client/data/live.tsx +85 -0
- package/src/client/data/query.ts +166 -0
- package/src/client/data/range.ts +82 -0
- package/src/client/data/series.ts +85 -0
- package/src/client/data/useHashRoute.ts +41 -83
- package/src/client/data/view-models.ts +347 -140
- package/src/client/index.tsx +3 -6
- package/src/client/routes/CostsRoute.tsx +394 -222
- package/src/client/routes/ErrorsRoute.tsx +401 -98
- package/src/client/routes/FrustrationRoute.tsx +924 -0
- package/src/client/routes/GainRoute.tsx +209 -206
- package/src/client/routes/ModelsRoute.tsx +540 -431
- package/src/client/routes/OverviewRoute.tsx +283 -311
- package/src/client/routes/ProjectsRoute.tsx +298 -156
- package/src/client/routes/ProvidersRoute.tsx +948 -416
- package/src/client/routes/RequestsRoute.tsx +295 -107
- package/src/client/routes/ToolsRoute.tsx +495 -398
- package/src/client/routes/TracesRoute.tsx +136 -117
- package/src/client/routes/costs.css +24 -0
- package/src/client/routes/errors.css +60 -0
- package/src/client/routes/frustration.css +123 -0
- package/src/client/routes/index.ts +1 -1
- package/src/client/routes/models.css +43 -0
- package/src/client/routes/projects.css +6 -0
- package/src/client/routes/providers.css +28 -0
- package/src/client/routes/tools.css +14 -0
- package/src/client/styles.css +1396 -1299
- package/src/client/traces/AggregatesPanel.tsx +55 -74
- package/src/client/traces/Minimap.tsx +6 -15
- package/src/client/traces/SpanDrawer.tsx +128 -194
- package/src/client/traces/SummaryStrip.tsx +21 -24
- package/src/client/traces/TimelineCanvas.tsx +27 -44
- package/src/client/traces/TraceView.tsx +175 -130
- package/src/client/traces/TranscriptList.tsx +13 -41
- package/src/client/traces/trace-colors.ts +60 -53
- package/src/client/traces/traces.css +279 -0
- package/src/client/ui/Badge.tsx +29 -0
- package/src/client/ui/Card.tsx +61 -0
- package/src/client/ui/Drawer.tsx +93 -0
- package/src/client/ui/JsonBlock.tsx +26 -44
- package/src/client/ui/Modal.tsx +106 -0
- package/src/client/ui/RequestDrawer.tsx +163 -188
- package/src/client/ui/SearchInput.tsx +25 -0
- package/src/client/ui/Segmented.tsx +61 -0
- package/src/client/ui/Stat.tsx +104 -0
- package/src/client/ui/States.tsx +81 -0
- package/src/client/ui/Table.tsx +229 -0
- package/src/client/ui/index.ts +9 -10
- package/src/client/ui/modal.css +54 -0
- package/src/client/ui/request-drawer.css +40 -0
- package/src/db.ts +379 -983
- package/src/frustration.ts +473 -0
- package/src/index.ts +22 -22
- package/src/live.ts +259 -0
- package/src/parser.ts +55 -66
- package/src/rollup.ts +951 -0
- package/src/server.ts +123 -40
- package/src/shared-types.ts +120 -56
- package/src/trace.ts +14 -7
- package/src/types.ts +4 -0
- package/src/user-metrics.ts +13 -0
- package/dist/client/styles.css +0 -1976
- package/dist/types/client/app/AppLayout.d.ts +0 -16
- package/dist/types/client/app/NavRail.d.ts +0 -7
- package/dist/types/client/app/RangeControl.d.ts +0 -7
- package/dist/types/client/app/SyncButton.d.ts +0 -14
- package/dist/types/client/app/TopBar.d.ts +0 -15
- package/dist/types/client/app/routes.d.ts +0 -12
- package/dist/types/client/components/AgentTokenShare.d.ts +0 -5
- package/dist/types/client/components/chart-shared.d.ts +0 -178
- package/dist/types/client/components/models-table-shared.d.ts +0 -175
- package/dist/types/client/components/range-meta.d.ts +0 -21
- package/dist/types/client/data/charts.d.ts +0 -1
- package/dist/types/client/data/useResource.d.ts +0 -13
- package/dist/types/client/routes/BehaviorRoute.d.ts +0 -7
- package/dist/types/client/ui/AsyncBoundary.d.ts +0 -12
- package/dist/types/client/ui/DataTable.d.ts +0 -17
- package/dist/types/client/ui/EmptyState.d.ts +0 -7
- package/dist/types/client/ui/ErrorState.d.ts +0 -6
- package/dist/types/client/ui/MetricCluster.d.ts +0 -5
- package/dist/types/client/ui/Panel.d.ts +0 -7
- package/dist/types/client/ui/SegmentedControl.d.ts +0 -12
- package/dist/types/client/ui/Skeleton.d.ts +0 -8
- package/dist/types/client/ui/StatusPill.d.ts +0 -7
- package/src/client/app/AppLayout.tsx +0 -93
- package/src/client/app/NavRail.tsx +0 -44
- package/src/client/app/RangeControl.tsx +0 -39
- package/src/client/app/SyncButton.tsx +0 -75
- package/src/client/app/TopBar.tsx +0 -73
- package/src/client/app/routes.ts +0 -93
- package/src/client/components/AgentTokenShare.tsx +0 -68
- package/src/client/components/chart-shared.tsx +0 -273
- package/src/client/components/models-table-shared.tsx +0 -255
- package/src/client/components/range-meta.ts +0 -73
- package/src/client/data/charts.ts +0 -14
- package/src/client/data/useResource.ts +0 -154
- package/src/client/routes/BehaviorRoute.tsx +0 -623
- package/src/client/ui/AsyncBoundary.tsx +0 -54
- package/src/client/ui/DataTable.tsx +0 -122
- package/src/client/ui/EmptyState.tsx +0 -16
- package/src/client/ui/ErrorState.tsx +0 -25
- package/src/client/ui/MetricCluster.tsx +0 -92
- package/src/client/ui/Panel.tsx +0 -24
- package/src/client/ui/SegmentedControl.tsx +0 -36
- package/src/client/ui/Skeleton.tsx +0 -17
- package/src/client/ui/StatusPill.tsx +0 -15
- package/tailwind.config.js +0 -40
|
@@ -1,77 +1,84 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* mirror `CHART_THEMES` so the canvas matches the chart.js surfaces.
|
|
2
|
+
* Canvas palette for the trace flamegraph, resolved from the dashboard's CSS
|
|
3
|
+
* design tokens so canvas drawing follows the active theme.
|
|
5
4
|
*/
|
|
6
5
|
|
|
6
|
+
import { useEffect, useMemo, useState } from "react";
|
|
7
7
|
import type { TraceSpanKind } from "../types";
|
|
8
|
+
import { useSystemTheme } from "../useSystemTheme";
|
|
8
9
|
|
|
9
10
|
export interface TraceTheme {
|
|
10
11
|
/** Span fill per category. */
|
|
11
12
|
category: Record<TraceSpanKind, string>;
|
|
12
|
-
/** Error
|
|
13
|
+
/** Error left-edge accent. */
|
|
13
14
|
error: string;
|
|
15
|
+
/** Error fill tint. */
|
|
16
|
+
errorSoft: string;
|
|
14
17
|
/** Marker diamond fill. */
|
|
15
18
|
marker: string;
|
|
16
19
|
/** Alternating turn-band fill. */
|
|
17
20
|
turnBand: string;
|
|
18
|
-
/** Canvas background. */
|
|
19
|
-
surface: string;
|
|
20
21
|
/** Ruler/lane hairlines. */
|
|
21
22
|
grid: string;
|
|
22
23
|
/** Ruler tick labels. */
|
|
23
24
|
tick: string;
|
|
24
25
|
/** Span label text drawn inside blocks. */
|
|
25
26
|
spanText: string;
|
|
26
|
-
/** Selection outline
|
|
27
|
+
/** Selection outline / minimap brush. */
|
|
27
28
|
selection: string;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
/** Minimap brush fill. */
|
|
30
|
+
brush: string;
|
|
31
|
+
/** Canvas font stacks. */
|
|
32
|
+
fontSans: string;
|
|
33
|
+
fontMono: string;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
grid: "rgba(255, 255, 255, 0.06)",
|
|
48
|
-
tick: "#867a93",
|
|
49
|
-
spanText: "#16121c",
|
|
50
|
-
selection: "#5ad8e6",
|
|
51
|
-
tooltipBackground: "#241a2e",
|
|
52
|
-
tooltipTitle: "#eae5ef",
|
|
53
|
-
tooltipBody: "#a89fb3",
|
|
54
|
-
tooltipBorder: "rgba(255, 255, 255, 0.12)",
|
|
55
|
-
},
|
|
56
|
-
light: {
|
|
36
|
+
/** CSS custom property backing each category (also used by DOM chips). */
|
|
37
|
+
export const CATEGORY_VARS: Record<TraceSpanKind, string> = {
|
|
38
|
+
turn: "--ink-2",
|
|
39
|
+
model: "--chart-primary",
|
|
40
|
+
tool: "--warn",
|
|
41
|
+
subagent: "--chart-secondary",
|
|
42
|
+
background: "--ink-4",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function readTheme(): TraceTheme {
|
|
46
|
+
const style = getComputedStyle(document.documentElement);
|
|
47
|
+
const v = (name: string, fallback: string) => style.getPropertyValue(name).trim() || fallback;
|
|
48
|
+
return {
|
|
57
49
|
category: {
|
|
58
|
-
turn: "#
|
|
59
|
-
model: "#
|
|
60
|
-
tool: "#
|
|
61
|
-
subagent: "#
|
|
62
|
-
background: "#
|
|
50
|
+
turn: v(CATEGORY_VARS.turn, "#a0a0a8"),
|
|
51
|
+
model: v(CATEGORY_VARS.model, "#5ad8e6"),
|
|
52
|
+
tool: v(CATEGORY_VARS.tool, "#f5a524"),
|
|
53
|
+
subagent: v(CATEGORY_VARS.subagent, "#ed4abf"),
|
|
54
|
+
background: v(CATEGORY_VARS.background, "#46464c"),
|
|
63
55
|
},
|
|
64
|
-
error: "#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
grid: "rgba(
|
|
69
|
-
tick: "#
|
|
70
|
-
spanText: "#
|
|
71
|
-
selection: "#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
56
|
+
error: v("--bad", "#ff6166"),
|
|
57
|
+
errorSoft: v("--bad-soft", "rgba(255, 97, 102, 0.13)"),
|
|
58
|
+
marker: v("--ink-1", "#ededef"),
|
|
59
|
+
turnBand: v("--hover", "rgba(255, 255, 255, 0.035)"),
|
|
60
|
+
grid: v("--chart-grid", "rgba(255, 255, 255, 0.055)"),
|
|
61
|
+
tick: v("--chart-axis", "#5c5c64"),
|
|
62
|
+
spanText: v("--ink-inverse", "#0a0a0b"),
|
|
63
|
+
selection: v("--focus", "#5ad8e6"),
|
|
64
|
+
brush: v("--selected", "rgba(255, 255, 255, 0.075)"),
|
|
65
|
+
fontSans: v("--font-sans", "system-ui, sans-serif"),
|
|
66
|
+
fontMono: v("--font-mono", "ui-monospace, monospace"),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Current trace palette. Re-reads the tokens whenever the resolved theme
|
|
72
|
+
* changes (store or a direct `data-theme` attribute write).
|
|
73
|
+
*/
|
|
74
|
+
export function useTraceTheme(): TraceTheme {
|
|
75
|
+
const theme = useSystemTheme();
|
|
76
|
+
const [attrVersion, setAttrVersion] = useState(0);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const observer = new MutationObserver(() => setAttrVersion(n => n + 1));
|
|
79
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
|
|
80
|
+
return () => observer.disconnect();
|
|
81
|
+
}, []);
|
|
82
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: theme/attrVersion are re-read triggers.
|
|
83
|
+
return useMemo(readTheme, [theme, attrVersion]);
|
|
84
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/* Traces page: session list + single-session trace viewer. */
|
|
2
|
+
|
|
3
|
+
.traces-models {
|
|
4
|
+
display: inline-flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
gap: 6px;
|
|
7
|
+
max-width: 220px;
|
|
8
|
+
min-width: 0;
|
|
9
|
+
color: var(--ink-2);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.traces-title {
|
|
13
|
+
display: block;
|
|
14
|
+
max-width: 720px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.traces-spin {
|
|
18
|
+
animation: traces-spin 900ms linear infinite;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@keyframes traces-spin {
|
|
22
|
+
to {
|
|
23
|
+
transform: rotate(360deg);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Toolbar above the timeline. */
|
|
28
|
+
.traces-toolbar {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
flex-wrap: wrap;
|
|
32
|
+
gap: 12px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.traces-check {
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: 6px;
|
|
39
|
+
font-size: 12.5px;
|
|
40
|
+
color: var(--ink-2);
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
user-select: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.traces-check input {
|
|
46
|
+
accent-color: var(--accent);
|
|
47
|
+
margin: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.traces-search {
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: 4px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.traces-match-count {
|
|
57
|
+
font-size: 12px;
|
|
58
|
+
min-width: 36px;
|
|
59
|
+
text-align: center;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.traces-legend {
|
|
63
|
+
display: inline-flex;
|
|
64
|
+
flex-wrap: wrap;
|
|
65
|
+
gap: 12px;
|
|
66
|
+
margin-left: auto;
|
|
67
|
+
font-size: 12px;
|
|
68
|
+
color: var(--ink-2);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.traces-legend-item {
|
|
72
|
+
display: inline-flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 6px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Minimap brush strip. */
|
|
78
|
+
.traces-minimap {
|
|
79
|
+
width: 100%;
|
|
80
|
+
display: block;
|
|
81
|
+
cursor: crosshair;
|
|
82
|
+
border-radius: var(--r-chip);
|
|
83
|
+
border: 1px solid var(--line-2);
|
|
84
|
+
background: var(--panel);
|
|
85
|
+
touch-action: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Flamegraph: DOM gutter + canvas. */
|
|
89
|
+
.traces-timeline {
|
|
90
|
+
position: relative;
|
|
91
|
+
display: flex;
|
|
92
|
+
max-height: 560px;
|
|
93
|
+
overflow-y: auto;
|
|
94
|
+
overflow-x: hidden;
|
|
95
|
+
border-top: 1px solid var(--line-1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.traces-canvas {
|
|
99
|
+
cursor: grab;
|
|
100
|
+
outline: none;
|
|
101
|
+
touch-action: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.traces-canvas:focus-visible {
|
|
105
|
+
outline: 1px solid var(--focus);
|
|
106
|
+
outline-offset: -1px;
|
|
107
|
+
border-radius: var(--r-chip);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.traces-canvas:active {
|
|
111
|
+
cursor: grabbing;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.traces-chevron {
|
|
115
|
+
display: inline-flex;
|
|
116
|
+
background: none;
|
|
117
|
+
border: none;
|
|
118
|
+
padding: 0;
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
color: var(--ink-3);
|
|
121
|
+
border-radius: 4px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.traces-chevron:hover {
|
|
125
|
+
color: var(--ink-1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.traces-chevron:focus-visible {
|
|
129
|
+
outline: 2px solid var(--focus);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.traces-gutter-track {
|
|
133
|
+
font-size: 12px;
|
|
134
|
+
font-weight: 500;
|
|
135
|
+
color: var(--ink-1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.traces-gutter-model {
|
|
139
|
+
font-size: 10.5px;
|
|
140
|
+
color: var(--ink-3);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.traces-gutter-lane {
|
|
144
|
+
position: absolute;
|
|
145
|
+
font-size: 10.5px;
|
|
146
|
+
color: var(--ink-3);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.traces-tooltip {
|
|
150
|
+
position: fixed;
|
|
151
|
+
z-index: 60;
|
|
152
|
+
max-width: 300px;
|
|
153
|
+
pointer-events: none;
|
|
154
|
+
background: var(--overlay);
|
|
155
|
+
backdrop-filter: blur(12px);
|
|
156
|
+
border: 1px solid var(--line-3);
|
|
157
|
+
border-radius: var(--r-control);
|
|
158
|
+
padding: 8px 10px;
|
|
159
|
+
font-size: 12px;
|
|
160
|
+
line-height: 1.5;
|
|
161
|
+
box-shadow: var(--shadow-2);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.traces-tooltip-title {
|
|
165
|
+
font-weight: 500;
|
|
166
|
+
color: var(--ink-1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.traces-tooltip-detail {
|
|
170
|
+
color: var(--ink-2);
|
|
171
|
+
word-break: break-word;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* Transcript list (virtualised, fixed 28px rows). */
|
|
175
|
+
.traces-list {
|
|
176
|
+
height: 340px;
|
|
177
|
+
overflow-y: auto;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.traces-row {
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
gap: 10px;
|
|
184
|
+
height: 28px;
|
|
185
|
+
width: 100%;
|
|
186
|
+
padding: 0 16px;
|
|
187
|
+
background: transparent;
|
|
188
|
+
border: none;
|
|
189
|
+
border-bottom: 1px solid var(--line-1);
|
|
190
|
+
color: inherit;
|
|
191
|
+
font: inherit;
|
|
192
|
+
text-align: left;
|
|
193
|
+
cursor: pointer;
|
|
194
|
+
transition: background var(--dur-1) var(--ease);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.traces-row:hover {
|
|
198
|
+
background: var(--hover);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.traces-row:focus-visible {
|
|
202
|
+
outline: 2px solid var(--focus);
|
|
203
|
+
outline-offset: -2px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.traces-row[data-selected="true"] {
|
|
207
|
+
background: var(--selected);
|
|
208
|
+
box-shadow: inset 2px 0 0 var(--accent);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.traces-row[data-static="true"] {
|
|
212
|
+
cursor: default;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.traces-row[data-static="true"]:hover {
|
|
216
|
+
background: transparent;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.traces-kind {
|
|
220
|
+
flex-shrink: 0;
|
|
221
|
+
width: 72px;
|
|
222
|
+
font-size: 11px;
|
|
223
|
+
font-weight: 500;
|
|
224
|
+
border-radius: var(--r-chip);
|
|
225
|
+
padding: 1px 0;
|
|
226
|
+
text-align: center;
|
|
227
|
+
overflow: hidden;
|
|
228
|
+
text-overflow: ellipsis;
|
|
229
|
+
white-space: nowrap;
|
|
230
|
+
color: var(--ink-1);
|
|
231
|
+
background: color-mix(in srgb, var(--traces-kind-color) 18%, transparent);
|
|
232
|
+
box-shadow: inset 2px 0 0 var(--traces-kind-color);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.traces-row-track {
|
|
236
|
+
flex-shrink: 0;
|
|
237
|
+
max-width: 110px;
|
|
238
|
+
font-size: 11px;
|
|
239
|
+
color: var(--ink-3);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.traces-row-label {
|
|
243
|
+
font-size: 12.5px;
|
|
244
|
+
color: var(--ink-1);
|
|
245
|
+
min-width: 0;
|
|
246
|
+
flex-shrink: 1;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.traces-row-detail {
|
|
250
|
+
font-size: 12px;
|
|
251
|
+
color: var(--ink-3);
|
|
252
|
+
min-width: 0;
|
|
253
|
+
flex: 1;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.traces-row-meta {
|
|
257
|
+
margin-left: auto;
|
|
258
|
+
flex-shrink: 0;
|
|
259
|
+
display: inline-flex;
|
|
260
|
+
gap: 12px;
|
|
261
|
+
font-size: 11.5px;
|
|
262
|
+
color: var(--ink-3);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* Span drawer. */
|
|
266
|
+
.traces-drawer {
|
|
267
|
+
gap: 16px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.traces-pre {
|
|
271
|
+
margin: 0;
|
|
272
|
+
white-space: pre-wrap;
|
|
273
|
+
word-break: break-word;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.traces-pre[data-scroll="true"] {
|
|
277
|
+
max-height: 360px;
|
|
278
|
+
overflow-y: auto;
|
|
279
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type Tone = "neutral" | "ok" | "warn" | "bad" | "accent";
|
|
4
|
+
|
|
5
|
+
/** Pill label; status badges always carry a word, never color alone. */
|
|
6
|
+
export function Badge({ tone = "neutral", mono, children }: { tone?: Tone; mono?: boolean; children: ReactNode }) {
|
|
7
|
+
return (
|
|
8
|
+
<span className="badge" data-tone={tone} data-mono={mono ?? false}>
|
|
9
|
+
{children}
|
|
10
|
+
</span>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Round status dot; `pulse` for things happening right now. */
|
|
15
|
+
export function Dot({ tone = "neutral", pulse }: { tone?: Tone | "live"; pulse?: boolean }) {
|
|
16
|
+
return <span className="dot" data-tone={tone} data-pulse={pulse ?? false} />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Color square matching a chart series. */
|
|
20
|
+
export function Swatch({ color }: { color: string }) {
|
|
21
|
+
return <span className="swatch" style={{ background: color }} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Badge tone for an error rate (fraction). */
|
|
25
|
+
export function errorRateTone(rate: number): Tone {
|
|
26
|
+
if (rate >= 0.05) return "bad";
|
|
27
|
+
if (rate >= 0.01) return "warn";
|
|
28
|
+
return "ok";
|
|
29
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface CardProps {
|
|
4
|
+
title?: ReactNode;
|
|
5
|
+
description?: ReactNode;
|
|
6
|
+
/** Right-aligned header controls (segmented toggles, selects, buttons). */
|
|
7
|
+
actions?: ReactNode;
|
|
8
|
+
/** Body without padding — for full-bleed tables and lists. */
|
|
9
|
+
flush?: boolean;
|
|
10
|
+
footer?: ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
/** Stagger index for the entrance animation. */
|
|
13
|
+
index?: number;
|
|
14
|
+
/** Dim the body while previous data is on screen (see `useQuery().stale`). */
|
|
15
|
+
stale?: boolean;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The dashboard's container: top-lit hairline edge, optional header and footer. */
|
|
20
|
+
export function Card({ title, description, actions, flush, footer, className, index, stale, children }: CardProps) {
|
|
21
|
+
const hasHeader = title !== undefined || description !== undefined || actions !== undefined;
|
|
22
|
+
return (
|
|
23
|
+
<section
|
|
24
|
+
className={className ? `card rise ${className}` : "card rise"}
|
|
25
|
+
style={index !== undefined ? ({ "--i": index } as CSSProperties) : undefined}
|
|
26
|
+
>
|
|
27
|
+
{hasHeader && (
|
|
28
|
+
<header className="card-header">
|
|
29
|
+
<div className="card-titles">
|
|
30
|
+
{title !== undefined && <h2 className="card-title">{title}</h2>}
|
|
31
|
+
{description !== undefined && <p className="card-description">{description}</p>}
|
|
32
|
+
</div>
|
|
33
|
+
{actions !== undefined && <div className="card-actions">{actions}</div>}
|
|
34
|
+
</header>
|
|
35
|
+
)}
|
|
36
|
+
<div className="card-body" data-flush={flush ?? false} data-stale={stale ?? false}>
|
|
37
|
+
{children}
|
|
38
|
+
</div>
|
|
39
|
+
{footer !== undefined && <footer className="card-footer">{footer}</footer>}
|
|
40
|
+
</section>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PageHeaderProps {
|
|
45
|
+
title: ReactNode;
|
|
46
|
+
description?: ReactNode;
|
|
47
|
+
actions?: ReactNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Title band at the top of every page. */
|
|
51
|
+
export function PageHeader({ title, description, actions }: PageHeaderProps) {
|
|
52
|
+
return (
|
|
53
|
+
<header className="page-header rise">
|
|
54
|
+
<div>
|
|
55
|
+
<h1 className="page-title">{title}</h1>
|
|
56
|
+
{description !== undefined && <p className="page-description">{description}</p>}
|
|
57
|
+
</div>
|
|
58
|
+
{actions !== undefined && <div className="page-actions">{actions}</div>}
|
|
59
|
+
</header>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { X } from "lucide-react";
|
|
2
|
+
import { type CSSProperties, type ReactNode, useEffect, useRef } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
|
|
5
|
+
export interface DrawerProps {
|
|
6
|
+
open: boolean;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
title: ReactNode;
|
|
9
|
+
subtitle?: ReactNode;
|
|
10
|
+
/** Header controls left of the close button. */
|
|
11
|
+
actions?: ReactNode;
|
|
12
|
+
/** Sheet width in px. Default 560. */
|
|
13
|
+
width?: number;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Right-side glass sheet over a scrim, portalled to `document.body`. Esc and
|
|
19
|
+
* scrim clicks close it; focus returns to the opener.
|
|
20
|
+
*/
|
|
21
|
+
export function Drawer({ open, onClose, title, subtitle, actions, width, children }: DrawerProps) {
|
|
22
|
+
const onCloseRef = useRef(onClose);
|
|
23
|
+
onCloseRef.current = onClose;
|
|
24
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!open) return;
|
|
28
|
+
const opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
29
|
+
panelRef.current?.focus();
|
|
30
|
+
const onKey = (e: KeyboardEvent) => {
|
|
31
|
+
if (e.key === "Escape") {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
onCloseRef.current();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
window.addEventListener("keydown", onKey);
|
|
37
|
+
return () => {
|
|
38
|
+
window.removeEventListener("keydown", onKey);
|
|
39
|
+
opener?.focus();
|
|
40
|
+
};
|
|
41
|
+
}, [open]);
|
|
42
|
+
|
|
43
|
+
if (!open) return null;
|
|
44
|
+
return createPortal(
|
|
45
|
+
<>
|
|
46
|
+
<div className="drawer-scrim" onClick={onClose} role="presentation" />
|
|
47
|
+
<div
|
|
48
|
+
ref={panelRef}
|
|
49
|
+
className="drawer"
|
|
50
|
+
role="dialog"
|
|
51
|
+
aria-modal="true"
|
|
52
|
+
tabIndex={-1}
|
|
53
|
+
style={width !== undefined ? ({ "--drawer-w": `${width}px` } as CSSProperties) : undefined}
|
|
54
|
+
>
|
|
55
|
+
<header className="drawer-header">
|
|
56
|
+
<div className="drawer-titles">
|
|
57
|
+
<h2 className="drawer-title">{title}</h2>
|
|
58
|
+
{subtitle !== undefined && <div className="drawer-subtitle">{subtitle}</div>}
|
|
59
|
+
</div>
|
|
60
|
+
{actions}
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
className="btn"
|
|
64
|
+
data-variant="ghost"
|
|
65
|
+
data-icon="true"
|
|
66
|
+
onClick={onClose}
|
|
67
|
+
aria-label="Close"
|
|
68
|
+
>
|
|
69
|
+
<X size={16} />
|
|
70
|
+
</button>
|
|
71
|
+
</header>
|
|
72
|
+
<div className="drawer-body">{children}</div>
|
|
73
|
+
</div>
|
|
74
|
+
</>,
|
|
75
|
+
document.body,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Label/value grid for detail panels. */
|
|
80
|
+
export function KeyValues({ items }: { items: readonly { key: string; label: ReactNode; value: ReactNode }[] }) {
|
|
81
|
+
return (
|
|
82
|
+
<dl className="kv" style={{ margin: 0 }}>
|
|
83
|
+
{items.map(item => (
|
|
84
|
+
<div key={item.key} className="kv-item">
|
|
85
|
+
<dt className="kv-key">{item.label}</dt>
|
|
86
|
+
<dd className="kv-value" style={{ margin: 0 }}>
|
|
87
|
+
{item.value}
|
|
88
|
+
</dd>
|
|
89
|
+
</div>
|
|
90
|
+
))}
|
|
91
|
+
</dl>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { Check, Copy } from "lucide-react";
|
|
2
|
-
import type React from "react";
|
|
1
|
+
import { Check, ChevronDown, ChevronRight, Copy } from "lucide-react";
|
|
3
2
|
import { useEffect, useRef, useState } from "react";
|
|
4
3
|
|
|
5
4
|
export interface JsonBlockProps {
|
|
@@ -8,27 +7,19 @@ export interface JsonBlockProps {
|
|
|
8
7
|
initialCollapsed?: boolean;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
/** Collapsible, copyable pretty-printed JSON. */
|
|
11
|
+
export function JsonBlock({ data, title = "JSON", initialCollapsed = false }: JsonBlockProps) {
|
|
12
12
|
const [collapsed, setCollapsed] = useState(initialCollapsed);
|
|
13
13
|
const [copied, setCopied] = useState(false);
|
|
14
14
|
const copyResetRef = useRef<number>(0);
|
|
15
|
-
const
|
|
15
|
+
const json = JSON.stringify(data, null, 2);
|
|
16
16
|
|
|
17
17
|
// Clear the pending "Copied" reset if the block unmounts (e.g. drawer close).
|
|
18
18
|
useEffect(() => () => window.clearTimeout(copyResetRef.current), []);
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
22
|
-
e.preventDefault();
|
|
23
|
-
setCollapsed(!collapsed);
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const handleCopy = async (e: React.MouseEvent) => {
|
|
28
|
-
// Don't toggle the collapse state when copying.
|
|
29
|
-
e.stopPropagation();
|
|
20
|
+
const copy = async () => {
|
|
30
21
|
try {
|
|
31
|
-
await navigator.clipboard.writeText(
|
|
22
|
+
await navigator.clipboard.writeText(json);
|
|
32
23
|
setCopied(true);
|
|
33
24
|
window.clearTimeout(copyResetRef.current);
|
|
34
25
|
copyResetRef.current = window.setTimeout(() => setCopied(false), 1500);
|
|
@@ -38,37 +29,28 @@ export function JsonBlock({ data, title, initialCollapsed = false }: JsonBlockPr
|
|
|
38
29
|
};
|
|
39
30
|
|
|
40
31
|
return (
|
|
41
|
-
<div className="
|
|
42
|
-
<div
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
{copied ? <Check size={13} /> : <Copy size={13} />}
|
|
59
|
-
{copied ? "Copied" : "Copy"}
|
|
60
|
-
</button>
|
|
61
|
-
<span className="stats-json-block-toggle-indicator" data-collapsed={collapsed}>
|
|
62
|
-
{collapsed ? "▶ Show" : "▼ Hide"}
|
|
63
|
-
</span>
|
|
64
|
-
</div>
|
|
32
|
+
<div className="stack" style={{ gap: 6 }}>
|
|
33
|
+
<div className="row" style={{ justifyContent: "space-between" }}>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
className="btn"
|
|
37
|
+
data-variant="ghost"
|
|
38
|
+
data-size="sm"
|
|
39
|
+
onClick={() => setCollapsed(c => !c)}
|
|
40
|
+
aria-expanded={!collapsed}
|
|
41
|
+
>
|
|
42
|
+
{collapsed ? <ChevronRight size={13} /> : <ChevronDown size={13} />}
|
|
43
|
+
{title}
|
|
44
|
+
</button>
|
|
45
|
+
<button type="button" className="btn" data-variant="ghost" data-size="sm" onClick={copy}>
|
|
46
|
+
{copied ? <Check size={13} /> : <Copy size={13} />}
|
|
47
|
+
{copied ? "Copied" : "Copy"}
|
|
48
|
+
</button>
|
|
65
49
|
</div>
|
|
66
50
|
{!collapsed && (
|
|
67
|
-
<
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
</pre>
|
|
71
|
-
</div>
|
|
51
|
+
<pre className="code-block">
|
|
52
|
+
<code>{json}</code>
|
|
53
|
+
</pre>
|
|
72
54
|
)}
|
|
73
55
|
</div>
|
|
74
56
|
);
|