@oh-my-pi/omp-stats 16.0.3 → 16.0.5

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.
Files changed (107) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/build.ts +11 -0
  3. package/dist/client/index.css +1 -1
  4. package/dist/client/index.html +11 -0
  5. package/dist/client/index.js +108 -108
  6. package/dist/client/styles.css +1070 -631
  7. package/dist/types/client/api.d.ts +19 -10
  8. package/dist/types/client/app/AppLayout.d.ts +16 -0
  9. package/dist/types/client/app/NavRail.d.ts +7 -0
  10. package/dist/types/client/app/RangeControl.d.ts +7 -0
  11. package/dist/types/client/app/SyncButton.d.ts +14 -0
  12. package/dist/types/client/app/ThemeToggle.d.ts +1 -0
  13. package/dist/types/client/app/TopBar.d.ts +15 -0
  14. package/dist/types/client/app/routes.d.ts +12 -0
  15. package/dist/types/client/components/chart-shared.d.ts +26 -40
  16. package/dist/types/client/components/models-table-shared.d.ts +20 -40
  17. package/dist/types/client/data/charts.d.ts +1 -0
  18. package/dist/types/client/data/formatters.d.ts +7 -0
  19. package/dist/types/client/data/useHashRoute.d.ts +8 -0
  20. package/dist/types/client/data/useResource.d.ts +13 -0
  21. package/dist/types/client/data/view-models.d.ts +37 -0
  22. package/dist/types/client/index.d.ts +1 -0
  23. package/dist/types/client/routes/BehaviorRoute.d.ts +7 -0
  24. package/dist/types/client/routes/CostsRoute.d.ts +7 -0
  25. package/dist/types/client/routes/ErrorsRoute.d.ts +8 -0
  26. package/dist/types/client/routes/ModelsRoute.d.ts +7 -0
  27. package/dist/types/client/routes/OverviewRoute.d.ts +8 -0
  28. package/dist/types/client/routes/ProjectsRoute.d.ts +7 -0
  29. package/dist/types/client/routes/RequestsRoute.d.ts +8 -0
  30. package/dist/types/client/routes/index.d.ts +7 -0
  31. package/dist/types/client/ui/AsyncBoundary.d.ts +12 -0
  32. package/dist/types/client/ui/DataTable.d.ts +17 -0
  33. package/dist/types/client/ui/EmptyState.d.ts +7 -0
  34. package/dist/types/client/ui/ErrorState.d.ts +6 -0
  35. package/dist/types/client/ui/JsonBlock.d.ts +7 -0
  36. package/dist/types/client/ui/MetricCluster.d.ts +5 -0
  37. package/dist/types/client/ui/Panel.d.ts +7 -0
  38. package/dist/types/client/ui/RequestDrawer.d.ts +5 -0
  39. package/dist/types/client/ui/SegmentedControl.d.ts +12 -0
  40. package/dist/types/client/ui/Skeleton.d.ts +8 -0
  41. package/dist/types/client/ui/StatusPill.d.ts +7 -0
  42. package/dist/types/client/ui/index.d.ts +11 -0
  43. package/dist/types/client/useSystemTheme.d.ts +9 -0
  44. package/package.json +4 -4
  45. package/src/aggregator.ts +4 -3
  46. package/src/client/App.tsx +89 -207
  47. package/src/client/api.ts +55 -37
  48. package/src/client/app/AppLayout.tsx +93 -0
  49. package/src/client/app/NavRail.tsx +44 -0
  50. package/src/client/app/RangeControl.tsx +39 -0
  51. package/src/client/app/SyncButton.tsx +75 -0
  52. package/src/client/app/ThemeToggle.tsx +37 -0
  53. package/src/client/app/TopBar.tsx +73 -0
  54. package/src/client/app/routes.ts +50 -0
  55. package/src/client/components/chart-shared.tsx +28 -91
  56. package/src/client/components/models-table-shared.tsx +9 -29
  57. package/src/client/components/range-meta.ts +3 -2
  58. package/src/client/data/charts.ts +14 -0
  59. package/src/client/data/formatters.ts +38 -0
  60. package/src/client/data/useHashRoute.ts +85 -0
  61. package/src/client/data/useResource.ts +154 -0
  62. package/src/client/data/view-models.ts +178 -0
  63. package/src/client/index.tsx +4 -0
  64. package/src/client/routes/BehaviorRoute.tsx +623 -0
  65. package/src/client/routes/CostsRoute.tsx +234 -0
  66. package/src/client/routes/ErrorsRoute.tsx +118 -0
  67. package/src/client/routes/ModelsRoute.tsx +430 -0
  68. package/src/client/routes/OverviewRoute.tsx +332 -0
  69. package/src/client/routes/ProjectsRoute.tsx +163 -0
  70. package/src/client/routes/RequestsRoute.tsx +123 -0
  71. package/src/client/routes/index.ts +7 -0
  72. package/src/client/styles.css +1242 -225
  73. package/src/client/ui/AsyncBoundary.tsx +54 -0
  74. package/src/client/ui/DataTable.tsx +122 -0
  75. package/src/client/ui/EmptyState.tsx +16 -0
  76. package/src/client/ui/ErrorState.tsx +25 -0
  77. package/src/client/ui/JsonBlock.tsx +75 -0
  78. package/src/client/ui/MetricCluster.tsx +67 -0
  79. package/src/client/ui/Panel.tsx +24 -0
  80. package/src/client/ui/RequestDrawer.tsx +208 -0
  81. package/src/client/ui/SegmentedControl.tsx +36 -0
  82. package/src/client/ui/Skeleton.tsx +17 -0
  83. package/src/client/ui/StatusPill.tsx +15 -0
  84. package/src/client/ui/index.ts +11 -0
  85. package/src/client/useSystemTheme.ts +73 -17
  86. package/dist/types/client/components/BehaviorChart.d.ts +0 -6
  87. package/dist/types/client/components/BehaviorModelsTable.d.ts +0 -7
  88. package/dist/types/client/components/BehaviorSummary.d.ts +0 -7
  89. package/dist/types/client/components/ChartsContainer.d.ts +0 -7
  90. package/dist/types/client/components/CostChart.d.ts +0 -6
  91. package/dist/types/client/components/CostSummary.d.ts +0 -6
  92. package/dist/types/client/components/Header.d.ts +0 -12
  93. package/dist/types/client/components/ModelsTable.d.ts +0 -8
  94. package/dist/types/client/components/RequestDetail.d.ts +0 -6
  95. package/dist/types/client/components/RequestList.d.ts +0 -8
  96. package/dist/types/client/components/StatsGrid.d.ts +0 -6
  97. package/src/client/components/BehaviorChart.tsx +0 -189
  98. package/src/client/components/BehaviorModelsTable.tsx +0 -342
  99. package/src/client/components/BehaviorSummary.tsx +0 -95
  100. package/src/client/components/ChartsContainer.tsx +0 -221
  101. package/src/client/components/CostChart.tsx +0 -171
  102. package/src/client/components/CostSummary.tsx +0 -53
  103. package/src/client/components/Header.tsx +0 -72
  104. package/src/client/components/ModelsTable.tsx +0 -265
  105. package/src/client/components/RequestDetail.tsx +0 -172
  106. package/src/client/components/RequestList.tsx +0 -73
  107. package/src/client/components/StatsGrid.tsx +0 -135
@@ -0,0 +1,54 @@
1
+ import type React from "react";
2
+ import { EmptyState } from "./EmptyState";
3
+ import { ErrorState } from "./ErrorState";
4
+ import { Skeleton } from "./Skeleton";
5
+
6
+ export interface AsyncBoundaryProps {
7
+ loading: boolean;
8
+ error: Error | null;
9
+ data: unknown | null;
10
+ empty?: boolean;
11
+ emptyText?: string;
12
+ fallback?: React.ReactNode;
13
+ onRetry?: () => void;
14
+ children: React.ReactNode;
15
+ }
16
+
17
+ export function AsyncBoundary({
18
+ loading,
19
+ error,
20
+ data,
21
+ empty = false,
22
+ emptyText = "No data available",
23
+ fallback,
24
+ onRetry,
25
+ children,
26
+ }: AsyncBoundaryProps) {
27
+ // If there's an error and no stale data, render ErrorState
28
+ if (error && data === null) {
29
+ return <ErrorState error={error} onRetry={onRetry} />;
30
+ }
31
+
32
+ // If it's loading and there is no stale data, render Loading state / Skeleton
33
+ if (loading && data === null) {
34
+ if (fallback) {
35
+ return <>{fallback}</>;
36
+ }
37
+ return (
38
+ <div className="stats-boundary-skeleton">
39
+ <Skeleton variant="text" width="60%" height={24} className="mb-4" />
40
+ <Skeleton variant="rect" width="100%" height={160} className="mb-4" />
41
+ <Skeleton variant="text" width="80%" height={20} className="mb-2" />
42
+ <Skeleton variant="text" width="40%" height={20} />
43
+ </div>
44
+ );
45
+ }
46
+
47
+ // If there is data but it's empty, render EmptyState
48
+ if (!loading && (empty || data === null)) {
49
+ return <EmptyState message={emptyText} />;
50
+ }
51
+
52
+ // Render children (stale data is kept visible even if loading is true in background)
53
+ return <>{children}</>;
54
+ }
@@ -0,0 +1,122 @@
1
+ import type React from "react";
2
+
3
+ export interface DataTableColumn<T> {
4
+ key: string;
5
+ header: React.ReactNode;
6
+ render?: (item: T) => React.ReactNode;
7
+ className?: string;
8
+ numeric?: boolean;
9
+ }
10
+
11
+ export interface DataTableProps<T> {
12
+ columns: DataTableColumn<T>[];
13
+ data: T[];
14
+ keyExtractor: (item: T) => string | number;
15
+ onRowClick?: (item: T) => void;
16
+ renderMobileCard?: (item: T, onClick?: () => void) => React.ReactNode;
17
+ emptyText?: string;
18
+ }
19
+
20
+ export function DataTable<T>({
21
+ columns,
22
+ data,
23
+ keyExtractor,
24
+ onRowClick,
25
+ renderMobileCard,
26
+ emptyText = "No data available",
27
+ }: DataTableProps<T>) {
28
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTableRowElement>, item: T) => {
29
+ if (onRowClick && (e.key === "Enter" || e.key === " ")) {
30
+ e.preventDefault();
31
+ onRowClick(item);
32
+ }
33
+ };
34
+
35
+ if (data.length === 0) {
36
+ return <div className="stats-table-empty">{emptyText}</div>;
37
+ }
38
+
39
+ return (
40
+ <div className="stats-table-wrapper">
41
+ {/* Mobile layout */}
42
+ {renderMobileCard && (
43
+ <div className="stats-table-mobile-only">
44
+ <div className="stats-table-mobile-list">
45
+ {data.map(item => {
46
+ const key = String(keyExtractor(item));
47
+ const onClick = onRowClick ? () => onRowClick(item) : undefined;
48
+ return (
49
+ <div key={key} className="stats-table-mobile-card-wrapper">
50
+ {renderMobileCard(item, onClick)}
51
+ </div>
52
+ );
53
+ })}
54
+ </div>
55
+ </div>
56
+ )}
57
+
58
+ {/* Desktop layout */}
59
+ <div className={renderMobileCard ? "stats-table-desktop-only" : "stats-table-container"}>
60
+ <table className="stats-table">
61
+ <thead>
62
+ <tr>
63
+ {columns.map(col => {
64
+ const headerClasses = [
65
+ "stats-table-th",
66
+ col.numeric ? "stats-text-right" : "stats-text-left",
67
+ col.className || "",
68
+ ]
69
+ .filter(Boolean)
70
+ .join(" ");
71
+
72
+ return (
73
+ <th key={col.key} className={headerClasses}>
74
+ {col.header}
75
+ </th>
76
+ );
77
+ })}
78
+ </tr>
79
+ </thead>
80
+ <tbody>
81
+ {data.map(item => {
82
+ const key = String(keyExtractor(item));
83
+ const isClickable = typeof onRowClick === "function";
84
+ const rowClasses = ["stats-table-tr", isClickable ? "stats-table-tr-clickable" : ""]
85
+ .filter(Boolean)
86
+ .join(" ");
87
+
88
+ return (
89
+ <tr
90
+ key={key}
91
+ className={rowClasses}
92
+ onClick={isClickable ? () => onRowClick!(item) : undefined}
93
+ onKeyDown={isClickable ? e => handleKeyDown(e, item) : undefined}
94
+ tabIndex={isClickable ? 0 : undefined}
95
+ role={isClickable ? "button" : undefined}
96
+ >
97
+ {columns.map(col => {
98
+ const cellClasses = [
99
+ "stats-table-td",
100
+ col.numeric ? "stats-text-right" : "stats-text-left",
101
+ col.className || "",
102
+ ]
103
+ .filter(Boolean)
104
+ .join(" ");
105
+
106
+ return (
107
+ <td key={col.key} className={cellClasses}>
108
+ {col.render
109
+ ? col.render(item)
110
+ : ((item as Record<string, unknown>)[col.key] as React.ReactNode)}
111
+ </td>
112
+ );
113
+ })}
114
+ </tr>
115
+ );
116
+ })}
117
+ </tbody>
118
+ </table>
119
+ </div>
120
+ </div>
121
+ );
122
+ }
@@ -0,0 +1,16 @@
1
+ import { Inbox, type LucideIcon } from "lucide-react";
2
+
3
+ export interface EmptyStateProps {
4
+ message?: string;
5
+ icon?: LucideIcon;
6
+ className?: string;
7
+ }
8
+
9
+ export function EmptyState({ message = "No data available", icon: Icon = Inbox, className = "" }: EmptyStateProps) {
10
+ return (
11
+ <div className={`stats-empty-state ${className}`}>
12
+ <Icon size={24} className="stats-empty-state-icon" aria-hidden="true" />
13
+ <p className="stats-empty-state-message">{message}</p>
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,25 @@
1
+ export interface ErrorStateProps {
2
+ error?: Error | null;
3
+ onRetry?: () => void;
4
+ className?: string;
5
+ }
6
+
7
+ export function ErrorState({ error, onRetry, className = "" }: ErrorStateProps) {
8
+ return (
9
+ <div className={`stats-error-state ${className}`}>
10
+ <div className="stats-error-state-content">
11
+ <h4 className="stats-error-state-title">Failed to load data</h4>
12
+ {error && <p className="stats-error-state-message">{error.message}</p>}
13
+ {onRetry && (
14
+ <button
15
+ type="button"
16
+ onClick={onRetry}
17
+ className="stats-button stats-button-secondary stats-error-state-btn"
18
+ >
19
+ Retry
20
+ </button>
21
+ )}
22
+ </div>
23
+ </div>
24
+ );
25
+ }
@@ -0,0 +1,75 @@
1
+ import { Check, Copy } from "lucide-react";
2
+ import type React from "react";
3
+ import { useEffect, useRef, useState } from "react";
4
+
5
+ export interface JsonBlockProps {
6
+ data: unknown;
7
+ title?: string;
8
+ initialCollapsed?: boolean;
9
+ }
10
+
11
+ export function JsonBlock({ data, title, initialCollapsed = false }: JsonBlockProps) {
12
+ const [collapsed, setCollapsed] = useState(initialCollapsed);
13
+ const [copied, setCopied] = useState(false);
14
+ const copyResetRef = useRef<number>(0);
15
+ const jsonStr = JSON.stringify(data, null, 2);
16
+
17
+ // Clear the pending "Copied" reset if the block unmounts (e.g. drawer close).
18
+ useEffect(() => () => window.clearTimeout(copyResetRef.current), []);
19
+
20
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
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();
30
+ try {
31
+ await navigator.clipboard.writeText(jsonStr);
32
+ setCopied(true);
33
+ window.clearTimeout(copyResetRef.current);
34
+ copyResetRef.current = window.setTimeout(() => setCopied(false), 1500);
35
+ } catch {
36
+ // Clipboard API unavailable (e.g. insecure context); silently no-op.
37
+ }
38
+ };
39
+
40
+ return (
41
+ <div className="stats-json-block">
42
+ <div
43
+ className="stats-json-block-header"
44
+ onClick={() => setCollapsed(!collapsed)}
45
+ onKeyDown={handleKeyDown}
46
+ tabIndex={0}
47
+ role="button"
48
+ aria-expanded={!collapsed}
49
+ >
50
+ <span className="stats-json-block-title">{title || "JSON"}</span>
51
+ <div className="stats-json-actions">
52
+ <button
53
+ type="button"
54
+ className="stats-json-copy-btn"
55
+ onClick={handleCopy}
56
+ aria-label={copied ? "Copied to clipboard" : "Copy JSON to clipboard"}
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>
65
+ </div>
66
+ {!collapsed && (
67
+ <div className="stats-json-block-content-wrapper">
68
+ <pre className="stats-json-block-content">
69
+ <code>{jsonStr}</code>
70
+ </pre>
71
+ </div>
72
+ )}
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,67 @@
1
+ import {
2
+ formatCompact,
3
+ formatCost,
4
+ formatDurationMs,
5
+ formatInteger,
6
+ formatPercent,
7
+ formatTokensPerSecond,
8
+ } from "../data/formatters";
9
+ import type { AggregatedStats } from "../types";
10
+
11
+ export interface MetricClusterProps {
12
+ stats: AggregatedStats;
13
+ }
14
+
15
+ export function MetricCluster({ stats }: MetricClusterProps) {
16
+ return (
17
+ <div className="stats-metric-cluster">
18
+ <div className="stats-metric-primary-grid">
19
+ <div className="stats-metric-card primary">
20
+ <div className="stats-metric-label">Total Cost</div>
21
+ <div className="stats-metric-value">
22
+ {formatCost(stats.totalCost, stats.totalCost > 0 && stats.totalCost < 0.01 ? 4 : 2)}
23
+ </div>
24
+ </div>
25
+ <div className="stats-metric-card primary">
26
+ <div className="stats-metric-label">Requests</div>
27
+ <div className="stats-metric-value">{formatInteger(stats.totalRequests)}</div>
28
+ </div>
29
+ <div className="stats-metric-card primary">
30
+ <div className="stats-metric-label">Cache Rate</div>
31
+ <div className="stats-metric-value">{formatPercent(stats.cacheRate)}</div>
32
+ </div>
33
+ <div className="stats-metric-card primary">
34
+ <div className="stats-metric-label">Error Rate</div>
35
+ <div className="stats-metric-value">{formatPercent(stats.errorRate)}</div>
36
+ </div>
37
+ </div>
38
+
39
+ <div className="stats-metric-secondary-grid">
40
+ <div className="stats-metric-card secondary">
41
+ <div className="stats-metric-label">Input Tokens</div>
42
+ <div className="stats-metric-value">{formatCompact(stats.totalInputTokens)}</div>
43
+ </div>
44
+ <div className="stats-metric-card secondary">
45
+ <div className="stats-metric-label">Output Tokens</div>
46
+ <div className="stats-metric-value">{formatCompact(stats.totalOutputTokens)}</div>
47
+ </div>
48
+ <div className="stats-metric-card secondary">
49
+ <div className="stats-metric-label">Premium Requests</div>
50
+ <div className="stats-metric-value">{formatInteger(stats.totalPremiumRequests)}</div>
51
+ </div>
52
+ <div className="stats-metric-card secondary">
53
+ <div className="stats-metric-label">Tokens/s</div>
54
+ <div className="stats-metric-value">{formatTokensPerSecond(stats.avgTokensPerSecond)}</div>
55
+ </div>
56
+ <div className="stats-metric-card secondary">
57
+ <div className="stats-metric-label">Avg Latency</div>
58
+ <div className="stats-metric-value">{formatDurationMs(stats.avgDuration)}</div>
59
+ </div>
60
+ <div className="stats-metric-card secondary">
61
+ <div className="stats-metric-label">Avg TTFT</div>
62
+ <div className="stats-metric-value">{formatDurationMs(stats.avgTtft)}</div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,24 @@
1
+ import type React from "react";
2
+
3
+ export interface PanelProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
4
+ title?: React.ReactNode;
5
+ subtitle?: React.ReactNode;
6
+ actions?: React.ReactNode;
7
+ }
8
+
9
+ export function Panel({ title, subtitle, actions, children, className = "", ...props }: PanelProps) {
10
+ return (
11
+ <div className={`stats-panel ${className}`} {...props}>
12
+ {(title || subtitle || actions) && (
13
+ <div className="stats-panel-header">
14
+ <div className="stats-panel-header-titles">
15
+ {title && <h3 className="stats-panel-title">{title}</h3>}
16
+ {subtitle && <p className="stats-panel-subtitle">{subtitle}</p>}
17
+ </div>
18
+ {actions && <div className="stats-panel-actions">{actions}</div>}
19
+ </div>
20
+ )}
21
+ <div className="stats-panel-body">{children}</div>
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,208 @@
1
+ import { Clock, Coins, Gauge, Hash, Star, X, Zap } from "lucide-react";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { getRequestDetails } from "../api";
4
+ import { formatCost, formatDurationMs, formatInteger } from "../data/formatters";
5
+ import type { RequestDetails } from "../types";
6
+ import { JsonBlock } from "./JsonBlock";
7
+ import { Skeleton } from "./Skeleton";
8
+ import { StatusPill } from "./StatusPill";
9
+
10
+ export interface RequestDrawerProps {
11
+ id: number | null;
12
+ onClose: () => void;
13
+ }
14
+
15
+ export function RequestDrawer({ id, onClose }: RequestDrawerProps) {
16
+ const [details, setDetails] = useState<RequestDetails | null>(null);
17
+ const [loading, setLoading] = useState(false);
18
+ const [error, setError] = useState<Error | null>(null);
19
+ const previousActiveElement = useRef<HTMLElement | null>(null);
20
+ const closeButtonRef = useRef<HTMLButtonElement>(null);
21
+
22
+ useEffect(() => {
23
+ if (id === null) {
24
+ setDetails(null);
25
+ return;
26
+ }
27
+
28
+ previousActiveElement.current = document.activeElement as HTMLElement | null;
29
+ setLoading(true);
30
+ setError(null);
31
+ setDetails(null);
32
+
33
+ const controller = new AbortController();
34
+ getRequestDetails(id, controller.signal)
35
+ .then(data => {
36
+ if (controller.signal.aborted) return;
37
+ setDetails(data);
38
+ // Focus the close button for accessibility
39
+ setTimeout(() => closeButtonRef.current?.focus(), 50);
40
+ })
41
+ .catch(err => {
42
+ if (controller.signal.aborted) return;
43
+ setError(err instanceof Error ? err : new Error(String(err)));
44
+ })
45
+ .finally(() => {
46
+ if (!controller.signal.aborted) setLoading(false);
47
+ });
48
+
49
+ return () => controller.abort();
50
+ }, [id]);
51
+
52
+ useEffect(() => {
53
+ if (id === null) return;
54
+
55
+ const handleKeyDown = (e: KeyboardEvent) => {
56
+ if (e.key === "Escape") {
57
+ onClose();
58
+ }
59
+ };
60
+
61
+ window.addEventListener("keydown", handleKeyDown);
62
+ return () => {
63
+ window.removeEventListener("keydown", handleKeyDown);
64
+ if (previousActiveElement.current) {
65
+ previousActiveElement.current.focus();
66
+ }
67
+ };
68
+ }, [id, onClose]);
69
+
70
+ if (id === null) return null;
71
+
72
+ const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
73
+ if (e.target === e.currentTarget) {
74
+ onClose();
75
+ }
76
+ };
77
+
78
+ return (
79
+ <div className="stats-drawer-overlay" onClick={handleOverlayClick} role="presentation">
80
+ <div className="stats-drawer" role="dialog" aria-modal="true" aria-label="Request details">
81
+ {/* Drawer Header */}
82
+ <div className="stats-drawer-header">
83
+ <div className="stats-drawer-header-left">
84
+ <h2 className="stats-drawer-title">Request Details</h2>
85
+ {details && <span className="stats-drawer-id">ID: {id}</span>}
86
+ </div>
87
+ <button
88
+ ref={closeButtonRef}
89
+ type="button"
90
+ onClick={onClose}
91
+ className="stats-drawer-close-btn"
92
+ aria-label="Close request details"
93
+ >
94
+ <X size={18} />
95
+ </button>
96
+ </div>
97
+
98
+ <div className="stats-drawer-body">
99
+ {loading && (
100
+ <div className="stats-drawer-loading">
101
+ <Skeleton variant="text" width="60%" height={24} className="mb-4" />
102
+ <Skeleton variant="rect" width="100%" height={80} className="mb-4" />
103
+ <Skeleton variant="rect" width="100%" height={120} className="mb-4" />
104
+ <Skeleton variant="rect" width="100%" height={200} />
105
+ </div>
106
+ )}
107
+
108
+ {error && (
109
+ <div className="stats-drawer-error">
110
+ <p className="stats-drawer-error-title">Failed to load request details</p>
111
+ <p className="stats-drawer-error-message">{error.message}</p>
112
+ </div>
113
+ )}
114
+
115
+ {details && (
116
+ <div className="stats-drawer-content">
117
+ {/* Status Card */}
118
+ <div className="stats-drawer-status-card">
119
+ <div className="stats-drawer-status-row">
120
+ <div>
121
+ <div className="stats-drawer-model">{details.model}</div>
122
+ <div className="stats-drawer-provider">{details.provider}</div>
123
+ </div>
124
+ <StatusPill variant={details.errorMessage ? "danger" : "success"}>
125
+ {details.errorMessage ? "Error" : "Success"}
126
+ </StatusPill>
127
+ </div>
128
+ {details.errorMessage && (
129
+ <div className="stats-drawer-error-block">
130
+ <div className="stats-drawer-error-label">Error Message</div>
131
+ <div className="stats-drawer-error-text">{details.errorMessage}</div>
132
+ </div>
133
+ )}
134
+ </div>
135
+
136
+ {/* Metrics Grid */}
137
+ <div className="stats-drawer-metrics-grid">
138
+ <div className="stats-drawer-metric-card">
139
+ <div className="stats-drawer-metric-label">
140
+ <Coins size={14} className="stats-drawer-metric-icon" />
141
+ Cost
142
+ </div>
143
+ <div className="stats-drawer-metric-value">{formatCost(details.usage.cost.total, 4)}</div>
144
+ </div>
145
+
146
+ <div className="stats-drawer-metric-card">
147
+ <div className="stats-drawer-metric-label">
148
+ <Star size={14} className="stats-drawer-metric-icon" />
149
+ Premium
150
+ </div>
151
+ <div className="stats-drawer-metric-value">
152
+ {formatInteger(details.usage.premiumRequests ?? 0)}
153
+ </div>
154
+ </div>
155
+
156
+ <div className="stats-drawer-metric-card">
157
+ <div className="stats-drawer-metric-label">
158
+ <Hash size={14} className="stats-drawer-metric-icon" />
159
+ Total Tokens
160
+ </div>
161
+ <div className="stats-drawer-metric-value">{formatInteger(details.usage.totalTokens)}</div>
162
+ <div className="stats-drawer-metric-sub">
163
+ {formatInteger(details.usage.input)} in · {formatInteger(details.usage.output)} out
164
+ </div>
165
+ </div>
166
+
167
+ <div className="stats-drawer-metric-card">
168
+ <div className="stats-drawer-metric-label">
169
+ <Clock size={14} className="stats-drawer-metric-icon" />
170
+ Duration
171
+ </div>
172
+ <div className="stats-drawer-metric-value">{formatDurationMs(details.duration)}</div>
173
+ </div>
174
+
175
+ <div className="stats-drawer-metric-card">
176
+ <div className="stats-drawer-metric-label">
177
+ <Zap size={14} className="stats-drawer-metric-icon" />
178
+ TTFT
179
+ </div>
180
+ <div className="stats-drawer-metric-value">{formatDurationMs(details.ttft)}</div>
181
+ </div>
182
+
183
+ {details.duration && details.usage.output > 0 && (
184
+ <div className="stats-drawer-metric-card">
185
+ <div className="stats-drawer-metric-label">
186
+ <Gauge size={14} className="stats-drawer-metric-icon" />
187
+ Throughput
188
+ </div>
189
+ <div className="stats-drawer-metric-value">
190
+ {((details.usage.output * 1000) / details.duration).toFixed(1)}
191
+ </div>
192
+ <div className="stats-drawer-metric-sub">tokens/second</div>
193
+ </div>
194
+ )}
195
+ </div>
196
+
197
+ {/* JSON blocks */}
198
+ <div className="stats-drawer-json-blocks">
199
+ <JsonBlock data={details.output} title="Output Payload" initialCollapsed={false} />
200
+ <JsonBlock data={details} title="Raw Request Metadata" initialCollapsed={true} />
201
+ </div>
202
+ </div>
203
+ )}
204
+ </div>
205
+ </div>
206
+ </div>
207
+ );
208
+ }
@@ -0,0 +1,36 @@
1
+ export interface SegmentedControlOption<T> {
2
+ value: T;
3
+ label: string;
4
+ title?: string;
5
+ }
6
+
7
+ export interface SegmentedControlProps<T> {
8
+ options: SegmentedControlOption<T>[];
9
+ value: T;
10
+ onChange: (value: T) => void;
11
+ className?: string;
12
+ }
13
+
14
+ export function SegmentedControl<T>({ options, value, onChange, className = "" }: SegmentedControlProps<T>) {
15
+ return (
16
+ <div className={`stats-segmented-control ${className}`} role="radiogroup">
17
+ {options.map(opt => {
18
+ const isActive = opt.value === value;
19
+ return (
20
+ <button
21
+ key={String(opt.value)}
22
+ type="button"
23
+ role="radio"
24
+ aria-checked={isActive}
25
+ data-active={isActive ? "true" : "false"}
26
+ className="stats-segmented-control-btn"
27
+ title={opt.title}
28
+ onClick={() => onChange(opt.value)}
29
+ >
30
+ {opt.label}
31
+ </button>
32
+ );
33
+ })}
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,17 @@
1
+ import type React from "react";
2
+
3
+ export interface SkeletonProps {
4
+ variant?: "text" | "rect" | "circle";
5
+ width?: string | number;
6
+ height?: string | number;
7
+ className?: string;
8
+ }
9
+
10
+ export function Skeleton({ variant = "text", width, height, className = "" }: SkeletonProps) {
11
+ const style: React.CSSProperties = {
12
+ width: width !== undefined ? (typeof width === "number" ? `${width}px` : width) : undefined,
13
+ height: height !== undefined ? (typeof height === "number" ? `${height}px` : height) : undefined,
14
+ };
15
+
16
+ return <div className={`stats-skeleton ${className}`} data-variant={variant} style={style} />;
17
+ }
@@ -0,0 +1,15 @@
1
+ import type React from "react";
2
+
3
+ export interface StatusPillProps {
4
+ variant: "success" | "danger" | "warning" | "info" | "default";
5
+ children: React.ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ export function StatusPill({ variant, children, className = "" }: StatusPillProps) {
10
+ return (
11
+ <span className={`stats-status-pill ${className}`} data-variant={variant}>
12
+ {children}
13
+ </span>
14
+ );
15
+ }
@@ -0,0 +1,11 @@
1
+ export * from "./AsyncBoundary";
2
+ export * from "./DataTable";
3
+ export * from "./EmptyState";
4
+ export * from "./ErrorState";
5
+ export * from "./JsonBlock";
6
+ export * from "./MetricCluster";
7
+ export * from "./Panel";
8
+ export * from "./RequestDrawer";
9
+ export * from "./SegmentedControl";
10
+ export * from "./Skeleton";
11
+ export * from "./StatusPill";