@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.
Files changed (192) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +13 -11
  3. package/build.ts +5 -49
  4. package/dist/client/index.css +1 -1
  5. package/dist/client/index.html +2 -2
  6. package/dist/client/index.js +105 -126
  7. package/dist/types/aggregator.d.ts +25 -22
  8. package/dist/types/client/api.d.ts +11 -7
  9. package/dist/types/client/app/LiveChip.d.ts +2 -0
  10. package/dist/types/client/app/Shell.d.ts +16 -0
  11. package/dist/types/client/app/ThemeToggle.d.ts +1 -0
  12. package/dist/types/client/app/nav.d.ts +18 -0
  13. package/dist/types/client/charts/BarList.d.ts +17 -0
  14. package/dist/types/client/charts/Chart.d.ts +49 -0
  15. package/dist/types/client/charts/Legend.d.ts +16 -0
  16. package/dist/types/client/charts/ShareBar.d.ts +11 -0
  17. package/dist/types/client/charts/Sparkline.d.ts +11 -0
  18. package/dist/types/client/charts/TimeChart.d.ts +9 -0
  19. package/dist/types/client/charts/index.d.ts +7 -0
  20. package/dist/types/client/charts/types.d.ts +25 -0
  21. package/dist/types/client/charts/useWidth.d.ts +3 -0
  22. package/dist/types/client/data/colors.d.ts +25 -0
  23. package/dist/types/client/data/formatters.d.ts +9 -1
  24. package/dist/types/client/data/live.d.ts +29 -0
  25. package/dist/types/client/data/query.d.ts +35 -0
  26. package/dist/types/client/data/range.d.ts +31 -0
  27. package/dist/types/client/data/series.d.ts +28 -0
  28. package/dist/types/client/data/useHashRoute.d.ts +18 -4
  29. package/dist/types/client/data/view-models.d.ts +111 -28
  30. package/dist/types/client/index.d.ts +0 -1
  31. package/dist/types/client/routes/CostsRoute.d.ts +2 -2
  32. package/dist/types/client/routes/ErrorsRoute.d.ts +2 -2
  33. package/dist/types/client/routes/FrustrationRoute.d.ts +7 -0
  34. package/dist/types/client/routes/GainRoute.d.ts +1 -2
  35. package/dist/types/client/routes/ModelsRoute.d.ts +2 -2
  36. package/dist/types/client/routes/OverviewRoute.d.ts +1 -2
  37. package/dist/types/client/routes/ProjectsRoute.d.ts +2 -2
  38. package/dist/types/client/routes/ProvidersRoute.d.ts +2 -2
  39. package/dist/types/client/routes/RequestsRoute.d.ts +1 -2
  40. package/dist/types/client/routes/ToolsRoute.d.ts +2 -2
  41. package/dist/types/client/routes/TracesRoute.d.ts +2 -2
  42. package/dist/types/client/routes/index.d.ts +1 -1
  43. package/dist/types/client/traces/AggregatesPanel.d.ts +4 -3
  44. package/dist/types/client/traces/SummaryStrip.d.ts +2 -2
  45. package/dist/types/client/traces/trace-colors.d.ts +18 -12
  46. package/dist/types/client/ui/Badge.d.ts +19 -0
  47. package/dist/types/client/ui/Card.d.ts +25 -0
  48. package/dist/types/client/ui/Drawer.d.ts +25 -0
  49. package/dist/types/client/ui/JsonBlock.d.ts +2 -2
  50. package/dist/types/client/ui/Modal.d.ts +23 -0
  51. package/dist/types/client/ui/RequestDrawer.d.ts +10 -1
  52. package/dist/types/client/ui/SearchInput.d.ts +8 -0
  53. package/dist/types/client/ui/Segmented.d.ts +15 -0
  54. package/dist/types/client/ui/Stat.d.ts +36 -0
  55. package/dist/types/client/ui/States.d.ts +41 -0
  56. package/dist/types/client/ui/Table.d.ts +48 -0
  57. package/dist/types/client/ui/index.d.ts +9 -10
  58. package/dist/types/db.d.ts +90 -106
  59. package/dist/types/frustration.d.ts +70 -0
  60. package/dist/types/index.d.ts +3 -1
  61. package/dist/types/live.d.ts +30 -0
  62. package/dist/types/rollup.d.ts +115 -0
  63. package/dist/types/server.d.ts +6 -1
  64. package/dist/types/shared-types.d.ts +116 -56
  65. package/dist/types/types.d.ts +4 -0
  66. package/dist/types/user-metrics.d.ts +8 -0
  67. package/package.json +7 -13
  68. package/src/aggregator.ts +276 -211
  69. package/src/client/App.tsx +38 -80
  70. package/src/client/api.ts +65 -7
  71. package/src/client/app/LiveChip.tsx +97 -0
  72. package/src/client/app/Shell.tsx +181 -0
  73. package/src/client/app/ThemeToggle.tsx +5 -2
  74. package/src/client/app/nav.ts +78 -0
  75. package/src/client/charts/BarList.tsx +48 -0
  76. package/src/client/charts/Chart.tsx +388 -0
  77. package/src/client/charts/Legend.tsx +61 -0
  78. package/src/client/charts/ShareBar.tsx +26 -0
  79. package/src/client/charts/Sparkline.tsx +38 -0
  80. package/src/client/charts/TimeChart.tsx +22 -0
  81. package/src/client/charts/index.ts +7 -0
  82. package/src/client/charts/types.ts +27 -0
  83. package/src/client/charts/useWidth.ts +19 -0
  84. package/src/client/data/colors.ts +45 -0
  85. package/src/client/data/formatters.ts +30 -9
  86. package/src/client/data/live.tsx +85 -0
  87. package/src/client/data/query.ts +166 -0
  88. package/src/client/data/range.ts +82 -0
  89. package/src/client/data/series.ts +85 -0
  90. package/src/client/data/useHashRoute.ts +41 -83
  91. package/src/client/data/view-models.ts +347 -140
  92. package/src/client/index.tsx +3 -6
  93. package/src/client/routes/CostsRoute.tsx +394 -222
  94. package/src/client/routes/ErrorsRoute.tsx +401 -98
  95. package/src/client/routes/FrustrationRoute.tsx +924 -0
  96. package/src/client/routes/GainRoute.tsx +209 -206
  97. package/src/client/routes/ModelsRoute.tsx +540 -431
  98. package/src/client/routes/OverviewRoute.tsx +283 -311
  99. package/src/client/routes/ProjectsRoute.tsx +298 -156
  100. package/src/client/routes/ProvidersRoute.tsx +948 -416
  101. package/src/client/routes/RequestsRoute.tsx +295 -107
  102. package/src/client/routes/ToolsRoute.tsx +495 -398
  103. package/src/client/routes/TracesRoute.tsx +136 -117
  104. package/src/client/routes/costs.css +24 -0
  105. package/src/client/routes/errors.css +60 -0
  106. package/src/client/routes/frustration.css +123 -0
  107. package/src/client/routes/index.ts +1 -1
  108. package/src/client/routes/models.css +43 -0
  109. package/src/client/routes/projects.css +6 -0
  110. package/src/client/routes/providers.css +28 -0
  111. package/src/client/routes/tools.css +14 -0
  112. package/src/client/styles.css +1396 -1299
  113. package/src/client/traces/AggregatesPanel.tsx +55 -74
  114. package/src/client/traces/Minimap.tsx +6 -15
  115. package/src/client/traces/SpanDrawer.tsx +128 -194
  116. package/src/client/traces/SummaryStrip.tsx +21 -24
  117. package/src/client/traces/TimelineCanvas.tsx +27 -44
  118. package/src/client/traces/TraceView.tsx +175 -130
  119. package/src/client/traces/TranscriptList.tsx +13 -41
  120. package/src/client/traces/trace-colors.ts +60 -53
  121. package/src/client/traces/traces.css +279 -0
  122. package/src/client/ui/Badge.tsx +29 -0
  123. package/src/client/ui/Card.tsx +61 -0
  124. package/src/client/ui/Drawer.tsx +93 -0
  125. package/src/client/ui/JsonBlock.tsx +26 -44
  126. package/src/client/ui/Modal.tsx +106 -0
  127. package/src/client/ui/RequestDrawer.tsx +163 -188
  128. package/src/client/ui/SearchInput.tsx +25 -0
  129. package/src/client/ui/Segmented.tsx +61 -0
  130. package/src/client/ui/Stat.tsx +104 -0
  131. package/src/client/ui/States.tsx +81 -0
  132. package/src/client/ui/Table.tsx +229 -0
  133. package/src/client/ui/index.ts +9 -10
  134. package/src/client/ui/modal.css +54 -0
  135. package/src/client/ui/request-drawer.css +40 -0
  136. package/src/db.ts +379 -983
  137. package/src/frustration.ts +473 -0
  138. package/src/index.ts +22 -22
  139. package/src/live.ts +259 -0
  140. package/src/parser.ts +55 -66
  141. package/src/rollup.ts +951 -0
  142. package/src/server.ts +123 -40
  143. package/src/shared-types.ts +120 -56
  144. package/src/trace.ts +14 -7
  145. package/src/types.ts +4 -0
  146. package/src/user-metrics.ts +13 -0
  147. package/dist/client/styles.css +0 -1976
  148. package/dist/types/client/app/AppLayout.d.ts +0 -16
  149. package/dist/types/client/app/NavRail.d.ts +0 -7
  150. package/dist/types/client/app/RangeControl.d.ts +0 -7
  151. package/dist/types/client/app/SyncButton.d.ts +0 -14
  152. package/dist/types/client/app/TopBar.d.ts +0 -15
  153. package/dist/types/client/app/routes.d.ts +0 -12
  154. package/dist/types/client/components/AgentTokenShare.d.ts +0 -5
  155. package/dist/types/client/components/chart-shared.d.ts +0 -178
  156. package/dist/types/client/components/models-table-shared.d.ts +0 -175
  157. package/dist/types/client/components/range-meta.d.ts +0 -21
  158. package/dist/types/client/data/charts.d.ts +0 -1
  159. package/dist/types/client/data/useResource.d.ts +0 -13
  160. package/dist/types/client/routes/BehaviorRoute.d.ts +0 -7
  161. package/dist/types/client/ui/AsyncBoundary.d.ts +0 -12
  162. package/dist/types/client/ui/DataTable.d.ts +0 -17
  163. package/dist/types/client/ui/EmptyState.d.ts +0 -7
  164. package/dist/types/client/ui/ErrorState.d.ts +0 -6
  165. package/dist/types/client/ui/MetricCluster.d.ts +0 -5
  166. package/dist/types/client/ui/Panel.d.ts +0 -7
  167. package/dist/types/client/ui/SegmentedControl.d.ts +0 -12
  168. package/dist/types/client/ui/Skeleton.d.ts +0 -8
  169. package/dist/types/client/ui/StatusPill.d.ts +0 -7
  170. package/src/client/app/AppLayout.tsx +0 -93
  171. package/src/client/app/NavRail.tsx +0 -44
  172. package/src/client/app/RangeControl.tsx +0 -39
  173. package/src/client/app/SyncButton.tsx +0 -75
  174. package/src/client/app/TopBar.tsx +0 -73
  175. package/src/client/app/routes.ts +0 -93
  176. package/src/client/components/AgentTokenShare.tsx +0 -68
  177. package/src/client/components/chart-shared.tsx +0 -273
  178. package/src/client/components/models-table-shared.tsx +0 -255
  179. package/src/client/components/range-meta.ts +0 -73
  180. package/src/client/data/charts.ts +0 -14
  181. package/src/client/data/useResource.ts +0 -154
  182. package/src/client/routes/BehaviorRoute.tsx +0 -623
  183. package/src/client/ui/AsyncBoundary.tsx +0 -54
  184. package/src/client/ui/DataTable.tsx +0 -122
  185. package/src/client/ui/EmptyState.tsx +0 -16
  186. package/src/client/ui/ErrorState.tsx +0 -25
  187. package/src/client/ui/MetricCluster.tsx +0 -92
  188. package/src/client/ui/Panel.tsx +0 -24
  189. package/src/client/ui/SegmentedControl.tsx +0 -36
  190. package/src/client/ui/Skeleton.tsx +0 -17
  191. package/src/client/ui/StatusPill.tsx +0 -15
  192. package/tailwind.config.js +0 -40
@@ -1,44 +0,0 @@
1
- import { type DashboardSection, routes } from "./routes";
2
-
3
- export interface NavRailProps {
4
- activeSection: DashboardSection;
5
- onSectionChange: (section: DashboardSection) => void;
6
- className?: string;
7
- }
8
-
9
- export function NavRail({ activeSection, onSectionChange, className = "" }: NavRailProps) {
10
- return (
11
- <aside className={`stats-nav-rail ${className}`}>
12
- <div className="stats-nav-rail-header">
13
- <div className="stats-logo-container">
14
- <span className="stats-logo-text">OH MY PI</span>
15
- <span className="stats-logo-subtext">Observability</span>
16
- </div>
17
- </div>
18
-
19
- <nav className="stats-nav-rail-menu">
20
- {routes.map(route => {
21
- const isActive = route.id === activeSection;
22
- const Icon = route.icon;
23
- return (
24
- <button
25
- key={route.id}
26
- type="button"
27
- onClick={() => onSectionChange(route.id)}
28
- className="stats-nav-rail-item"
29
- data-active={isActive ? "true" : "false"}
30
- aria-current={isActive ? "page" : undefined}
31
- >
32
- <Icon size={16} className="stats-nav-rail-item-icon" />
33
- <span className="stats-nav-rail-item-label">{route.label}</span>
34
- </button>
35
- );
36
- })}
37
- </nav>
38
-
39
- <div className="stats-nav-rail-footer">
40
- <span className="stats-version-tag">OMP Stats v1.0.0</span>
41
- </div>
42
- </aside>
43
- );
44
- }
@@ -1,39 +0,0 @@
1
- import type { TimeRange } from "../types";
2
-
3
- export interface RangeControlProps {
4
- value: TimeRange;
5
- onChange: (value: TimeRange) => void;
6
- className?: string;
7
- }
8
-
9
- const RANGE_OPTIONS: { value: TimeRange; label: string }[] = [
10
- { value: "1h", label: "1h" },
11
- { value: "24h", label: "24h" },
12
- { value: "7d", label: "7d" },
13
- { value: "30d", label: "30d" },
14
- { value: "90d", label: "90d" },
15
- { value: "all", label: "All" },
16
- ];
17
-
18
- export function RangeControl({ value, onChange, className = "" }: RangeControlProps) {
19
- return (
20
- <div className={`stats-range-control ${className}`} role="radiogroup" aria-label="Select time range">
21
- {RANGE_OPTIONS.map(opt => {
22
- const isActive = opt.value === value;
23
- return (
24
- <button
25
- key={opt.value}
26
- type="button"
27
- role="radio"
28
- aria-checked={isActive}
29
- data-active={isActive ? "true" : "false"}
30
- className="stats-range-control-btn"
31
- onClick={() => onChange(opt.value)}
32
- >
33
- {opt.label}
34
- </button>
35
- );
36
- })}
37
- </div>
38
- );
39
- }
@@ -1,75 +0,0 @@
1
- import { RefreshCw } from "lucide-react";
2
- import { useState } from "react";
3
- import { sync } from "../api";
4
-
5
- export interface SyncButtonProps {
6
- onSyncStart?: () => void;
7
- onSyncComplete?: (result: {
8
- success: boolean;
9
- data?: { processed: number; files: number; totalMessages: number };
10
- error?: string;
11
- }) => void;
12
- className?: string;
13
- }
14
-
15
- export function SyncButton({ onSyncStart, onSyncComplete, className = "" }: SyncButtonProps) {
16
- const [syncing, setSyncing] = useState(false);
17
- const [status, setStatus] = useState<{ type: "success" | "error"; message: string } | null>(null);
18
-
19
- const handleSync = async () => {
20
- if (syncing) return;
21
-
22
- setSyncing(true);
23
- setStatus(null);
24
- if (onSyncStart) {
25
- onSyncStart();
26
- }
27
-
28
- try {
29
- const data = await sync();
30
- const result = {
31
- processed: typeof data?.processed === "number" ? data.processed : 0,
32
- files: typeof data?.files === "number" ? data.files : 0,
33
- totalMessages: typeof data?.totalMessages === "number" ? data.totalMessages : 0,
34
- };
35
- setStatus({
36
- type: "success",
37
- message: `Synced: ${result.processed} new request${result.processed === 1 ? "" : "s"} found.`,
38
- });
39
- if (onSyncComplete) {
40
- onSyncComplete({ success: true, data: result });
41
- }
42
- } catch (err) {
43
- const errorMessage = err instanceof Error ? err.message : String(err);
44
- setStatus({
45
- type: "error",
46
- message: `Sync failed: ${errorMessage}`,
47
- });
48
- if (onSyncComplete) {
49
- onSyncComplete({ success: false, error: errorMessage });
50
- }
51
- } finally {
52
- setSyncing(false);
53
- }
54
- };
55
-
56
- return (
57
- <div className={`stats-sync-container ${className}`}>
58
- {status && (
59
- <span className="stats-sync-status-msg" data-type={status.type}>
60
- {status.message}
61
- </span>
62
- )}
63
- <button
64
- type="button"
65
- onClick={handleSync}
66
- disabled={syncing}
67
- className="stats-button stats-button-primary stats-sync-btn"
68
- aria-busy={syncing}
69
- >
70
- <RefreshCw size={14} className={`stats-sync-icon ${syncing ? "stats-spin" : ""}`} />
71
- {syncing ? "Syncing..." : "Sync DB"}
72
- </button>
73
- </div>
74
- );
75
- }
@@ -1,73 +0,0 @@
1
- import { Menu } from "lucide-react";
2
- import type { TimeRange } from "../types";
3
- import { RangeControl } from "./RangeControl";
4
- import type { DashboardSection } from "./routes";
5
- import { routes } from "./routes";
6
- import { SyncButton } from "./SyncButton";
7
- import { ThemeToggle } from "./ThemeToggle";
8
-
9
- export interface TopBarProps {
10
- activeSection: DashboardSection;
11
- range: TimeRange;
12
- onRangeChange: (range: TimeRange) => void;
13
- updatedAt: number | null;
14
- onSyncStart?: () => void;
15
- onSyncComplete?: (result: { success: boolean }) => void;
16
- onMenuToggle?: () => void;
17
- className?: string;
18
- }
19
-
20
- export function TopBar({
21
- activeSection,
22
- range,
23
- onRangeChange,
24
- updatedAt,
25
- onSyncStart,
26
- onSyncComplete,
27
- onMenuToggle,
28
- className = "",
29
- }: TopBarProps) {
30
- const currentRoute = routes.find(r => r.id === activeSection);
31
- const title = currentRoute?.label || "Observability";
32
-
33
- const formatLastUpdated = (time: number | null) => {
34
- if (!time) return "Not updated";
35
- const date = new Date(time);
36
- return `Updated ${date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" })}`;
37
- };
38
-
39
- return (
40
- <header className={`stats-top-bar ${className}`}>
41
- <div className="stats-top-bar-left">
42
- {onMenuToggle && (
43
- <button
44
- type="button"
45
- onClick={onMenuToggle}
46
- className="stats-mobile-menu-btn"
47
- aria-label="Open navigation menu"
48
- >
49
- <Menu size={20} />
50
- </button>
51
- )}
52
- <h1 className="stats-page-title">{title}</h1>
53
- </div>
54
-
55
- <div className="stats-top-bar-right">
56
- <div className="stats-top-bar-meta">
57
- <span
58
- className="stats-last-updated"
59
- title={updatedAt ? new Date(updatedAt).toLocaleString() : undefined}
60
- >
61
- {formatLastUpdated(updatedAt)}
62
- </span>
63
- </div>
64
-
65
- <RangeControl value={range} onChange={onRangeChange} />
66
-
67
- <ThemeToggle />
68
-
69
- <SyncButton onSyncStart={onSyncStart} onSyncComplete={onSyncComplete} />
70
- </div>
71
- </header>
72
- );
73
- }
@@ -1,93 +0,0 @@
1
- import {
2
- Activity,
3
- AlertCircle,
4
- Coins,
5
- Cpu,
6
- Folder,
7
- LayoutDashboard,
8
- ListTree,
9
- Plug,
10
- Smile,
11
- TrendingUp,
12
- Wrench,
13
- } from "lucide-react";
14
- import type React from "react";
15
-
16
- export type DashboardSection =
17
- | "overview"
18
- | "requests"
19
- | "traces"
20
- | "errors"
21
- | "models"
22
- | "providers"
23
- | "tools"
24
- | "costs"
25
- | "behavior"
26
- | "projects"
27
- | "gain";
28
-
29
- export interface DashboardRoute {
30
- id: DashboardSection;
31
- label: string;
32
- shortLabel?: string;
33
- icon: React.ComponentType<{ size?: number; className?: string }>;
34
- }
35
-
36
- export const routes: DashboardRoute[] = [
37
- {
38
- id: "overview",
39
- label: "Overview",
40
- icon: LayoutDashboard,
41
- },
42
- {
43
- id: "requests",
44
- label: "Requests",
45
- icon: Activity,
46
- },
47
- {
48
- id: "traces",
49
- label: "Traces",
50
- icon: ListTree,
51
- },
52
- {
53
- id: "errors",
54
- label: "Errors",
55
- icon: AlertCircle,
56
- },
57
- {
58
- id: "models",
59
- label: "Models",
60
- icon: Cpu,
61
- },
62
- {
63
- id: "providers",
64
- label: "Providers",
65
- icon: Plug,
66
- },
67
- {
68
- id: "tools",
69
- label: "Tools",
70
- icon: Wrench,
71
- },
72
- {
73
- id: "costs",
74
- label: "Costs",
75
- icon: Coins,
76
- },
77
- {
78
- id: "behavior",
79
- label: "Behavior",
80
- shortLabel: "Behavior",
81
- icon: Smile,
82
- },
83
- {
84
- id: "projects",
85
- label: "Projects",
86
- icon: Folder,
87
- },
88
- {
89
- id: "gain",
90
- label: "Gain",
91
- icon: TrendingUp,
92
- },
93
- ];
@@ -1,68 +0,0 @@
1
- import { useMemo } from "react";
2
- import { formatCompact, formatInteger, formatPercent } from "../data/formatters";
3
- import { buildAgentTokenShare } from "../data/view-models";
4
- import type { AgentType, AgentTypeStats } from "../types";
5
-
6
- /**
7
- * Per-agent-type display chrome. Colors follow the OMP brand palette
8
- * (pink -> violet -> cyan) used by the dashboard charts so the bar reads on
9
- * both themes without per-theme overrides.
10
- */
11
- const AGENT_META: Record<AgentType, { label: string; color: string }> = {
12
- main: { label: "Main agent", color: "#ed4abf" },
13
- subagent: { label: "Subagents", color: "#9b4dff" },
14
- advisor: { label: "Advisor", color: "#5ad8e6" },
15
- };
16
-
17
- export interface AgentTokenShareProps {
18
- stats: AgentTypeStats[];
19
- }
20
-
21
- export function AgentTokenShare({ stats }: AgentTokenShareProps) {
22
- const view = useMemo(() => buildAgentTokenShare(stats), [stats]);
23
-
24
- if (view.totalTokens === 0) {
25
- return <div className="py-8 text-center stats-text-muted text-sm">No token usage in this range</div>;
26
- }
27
-
28
- return (
29
- <div className="space-y-4">
30
- <div className="flex h-3 w-full overflow-hidden rounded-full" style={{ background: "var(--surface-2)" }}>
31
- {view.segments.map(
32
- seg =>
33
- seg.share > 0 && (
34
- <div
35
- key={seg.agentType}
36
- className="h-full"
37
- style={{ width: `${seg.share * 100}%`, background: AGENT_META[seg.agentType].color }}
38
- title={`${AGENT_META[seg.agentType].label}: ${formatPercent(seg.share)}`}
39
- />
40
- ),
41
- )}
42
- </div>
43
-
44
- <div className="space-y-2">
45
- {view.segments.map(seg => (
46
- <div key={seg.agentType} className="flex items-center justify-between gap-3 text-sm">
47
- <div className="flex items-center gap-2 min-w-0">
48
- <span
49
- className="w-2.5 h-2.5 rounded-full flex-shrink-0"
50
- style={{ background: AGENT_META[seg.agentType].color }}
51
- />
52
- <span className="stats-text-primary truncate">{AGENT_META[seg.agentType].label}</span>
53
- <span className="stats-text-muted stats-text-xs whitespace-nowrap">
54
- {formatInteger(seg.requests)} req
55
- </span>
56
- </div>
57
- <div className="flex items-center gap-3 whitespace-nowrap">
58
- <span className="stats-text-secondary">{formatCompact(seg.tokens)} tok</span>
59
- <span className="stats-font-semibold stats-text-primary tabular-nums">
60
- {formatPercent(seg.share)}
61
- </span>
62
- </div>
63
- </div>
64
- ))}
65
- </div>
66
- </div>
67
- );
68
- }
@@ -1,273 +0,0 @@
1
- /**
2
- * Shared chart primitives for the dashboard timeline charts: the OMP color
3
- * palette, light/dark chart chrome, legend/tooltip + scale plumbing, dataset
4
- * styling, and the top-N-by-model / aggregate bucketing used by the cost and
5
- * behavior series.
6
- */
7
-
8
- import { format } from "@oh-my-pi/pi-utils/dates";
9
-
10
- // OMP brand palette (packages/collab-web/src/styles/tokens.css): pink/purple/cyan.
11
- // Categorical series lead with the brand gradient hues (pink -> purple -> cyan).
12
- export const MODEL_COLORS = [
13
- "#ed4abf", // brand pink (accent)
14
- "#9b4dff", // brand violet
15
- "#5ad8e6", // brand cyan
16
- "#62d394", // green
17
- "#c77dff", // light purple
18
- "#ff8fd1", // light pink
19
- "#f5c14b", // amber
20
- "#ff6b7d", // rose
21
- ];
22
-
23
- export function buildModelColorLookup(
24
- records: readonly { model: string; provider: string; totalRequests: number }[],
25
- ): Map<string, string> {
26
- const rankedRecords = [...records].sort(
27
- (a, b) =>
28
- b.totalRequests - a.totalRequests || `${a.model}::${a.provider}`.localeCompare(`${b.model}::${b.provider}`),
29
- );
30
-
31
- return new Map(
32
- rankedRecords.map((record, index) => [
33
- `${record.model}::${record.provider}`,
34
- MODEL_COLORS[index % MODEL_COLORS.length],
35
- ]),
36
- );
37
- }
38
-
39
- export const CHART_THEMES = {
40
- dark: {
41
- legendLabel: "#a89fb3",
42
- tooltipBackground: "#241a2e",
43
- tooltipTitle: "#eae5ef",
44
- tooltipBody: "#a89fb3",
45
- tooltipBorder: "rgba(255, 255, 255, 0.12)",
46
- grid: "rgba(255, 255, 255, 0.06)",
47
- tick: "#867a93",
48
- },
49
- light: {
50
- legendLabel: "#5a5462",
51
- tooltipBackground: "#ffffff",
52
- tooltipTitle: "#241a2e",
53
- tooltipBody: "#5a5462",
54
- tooltipBorder: "rgba(20, 12, 28, 0.15)",
55
- grid: "rgba(20, 12, 28, 0.08)",
56
- tick: "#6a6275",
57
- },
58
- } as const;
59
-
60
- export type ChartTheme = (typeof CHART_THEMES)[keyof typeof CHART_THEMES];
61
-
62
- export interface ChartSeries {
63
- labels: string[];
64
- datasets: Array<{ label: string; data: number[] }>;
65
- }
66
-
67
- interface TooltipItem {
68
- parsed: { y: number | null };
69
- }
70
-
71
- /** Tooltip + legend config common to bar and line variants of the time charts. */
72
- export function buildSharedPlugins(opts: {
73
- chartTheme: ChartTheme;
74
- showLegend: boolean;
75
- defaultLabel: string;
76
- formatValue: (n: number) => string;
77
- footer?: (items: TooltipItem[]) => string | undefined;
78
- }) {
79
- const { chartTheme, showLegend, defaultLabel, formatValue, footer } = opts;
80
- return {
81
- legend: {
82
- display: showLegend,
83
- position: "top" as const,
84
- align: "start" as const,
85
- labels: {
86
- color: chartTheme.legendLabel,
87
- usePointStyle: true,
88
- padding: 16,
89
- font: { size: 12 },
90
- boxWidth: 8,
91
- },
92
- },
93
- tooltip: {
94
- backgroundColor: chartTheme.tooltipBackground,
95
- titleColor: chartTheme.tooltipTitle,
96
- bodyColor: chartTheme.tooltipBody,
97
- borderColor: chartTheme.tooltipBorder,
98
- borderWidth: 1,
99
- padding: 12,
100
- cornerRadius: 8,
101
- callbacks: {
102
- label: (ctx: { dataset: { label?: string }; parsed: { y: number | null } }) => {
103
- const label = ctx.dataset.label ?? defaultLabel;
104
- const value = ctx.parsed.y ?? 0;
105
- return `${label}: ${formatValue(value)}`;
106
- },
107
- ...(footer ? { footer } : {}),
108
- },
109
- },
110
- };
111
- }
112
-
113
- /** Y-axis tick formatter + grid/tick styling shared by both charts. */
114
- export function buildSharedScales(opts: { chartTheme: ChartTheme; formatY: (n: number) => string }) {
115
- const { chartTheme, formatY } = opts;
116
- const sharedScaleBase = {
117
- grid: { color: chartTheme.grid, drawBorder: false },
118
- ticks: { color: chartTheme.tick, font: { size: 11 } },
119
- };
120
- const yScale = {
121
- ...sharedScaleBase,
122
- ticks: {
123
- ...sharedScaleBase.ticks,
124
- callback: (value: number | string) => formatY(Number(value)),
125
- },
126
- min: 0,
127
- };
128
- return { sharedScaleBase, yScale };
129
- }
130
-
131
- /** Stylistic defaults for a single line dataset in a stacked/by-model chart. */
132
- export function lineDatasetStyle(color: string) {
133
- return {
134
- borderColor: color,
135
- backgroundColor: `${color}20`,
136
- fill: true,
137
- tension: 0,
138
- pointRadius: 3,
139
- pointHoverRadius: 4,
140
- borderWidth: 2,
141
- };
142
- }
143
-
144
- /** Stylistic defaults for a single bar dataset in a stacked chart. */
145
- export function barDatasetStyle(color: string) {
146
- return {
147
- backgroundColor: color,
148
- borderColor: color,
149
- borderWidth: 0,
150
- borderRadius: 4,
151
- maxBarThickness: 56,
152
- };
153
- }
154
-
155
- /**
156
- * Map a generic ChartSeries' datasets through a per-index style function so
157
- * callers can supply line or bar styling without repeating the label/data
158
- * spread at every chart site.
159
- */
160
- export function styleDatasets(series: ChartSeries, styleFor: (index: number) => Record<string, unknown>) {
161
- return series.datasets.map((ds, index) => ({
162
- label: ds.label,
163
- data: ds.data,
164
- ...styleFor(index),
165
- }));
166
- }
167
-
168
- /**
169
- * Bucket points by day into a single aggregate series. Caller supplies the
170
- * per-bucket accumulator + final value extractor; mirrors the shape of
171
- * `buildTopNByModelSeries` for the non-by-model variant of each time chart.
172
- */
173
- export function buildAggregateTimeSeries<T extends { timestamp: number }, B>(
174
- points: T[],
175
- label: string,
176
- opts: {
177
- initBucket: () => B;
178
- accumulate: (bucket: B, point: T) => void;
179
- bucketToValue: (bucket: B) => number;
180
- },
181
- ): ChartSeries {
182
- if (points.length === 0) return { labels: [], datasets: [] };
183
- const { initBucket, accumulate, bucketToValue } = opts;
184
- const byDay = new Map<number, B>();
185
- for (const point of points) {
186
- const bucket = byDay.get(point.timestamp) ?? initBucket();
187
- accumulate(bucket, point);
188
- byDay.set(point.timestamp, bucket);
189
- }
190
- const sorted = [...byDay.entries()].sort((a, b) => a[0] - b[0]);
191
- return {
192
- labels: sorted.map(([ts]) => format(new Date(ts), "MMM d")),
193
- datasets: [{ label, data: sorted.map(([, bucket]) => bucketToValue(bucket)) }],
194
- };
195
- }
196
-
197
- interface ModelKeyedPoint {
198
- timestamp: number;
199
- model: string;
200
- provider: string;
201
- }
202
-
203
- /**
204
- * Bucket points by day and by top-N model (with an "Other" rollup), producing
205
- * a ChartSeries. Caller controls how points contribute to ranking and to each
206
- * day-bucket value via the `rankWeight`/`accumulate`/`bucketToValue` callbacks
207
- * — keeps the behavior chart's rate math separate from the cost chart's sum.
208
- */
209
- export function buildTopNByModelSeries<T extends ModelKeyedPoint, B>(
210
- points: T[],
211
- opts: {
212
- topN?: number;
213
- rankWeight: (point: T) => number;
214
- initBucket: () => B;
215
- accumulate: (bucket: B, point: T) => void;
216
- bucketToValue: (bucket: B) => number;
217
- },
218
- ): ChartSeries {
219
- if (points.length === 0) return { labels: [], datasets: [] };
220
- const { topN = 5, rankWeight, initBucket, accumulate, bucketToValue } = opts;
221
-
222
- const totals = new Map<string, { model: string; provider: string; weight: number }>();
223
- for (const point of points) {
224
- const key = `${point.model}::${point.provider}`;
225
- const existing = totals.get(key);
226
- if (existing) {
227
- existing.weight += rankWeight(point);
228
- } else {
229
- totals.set(key, { model: point.model, provider: point.provider, weight: rankWeight(point) });
230
- }
231
- }
232
-
233
- const sorted = [...totals.entries()].sort((a, b) => b[1].weight - a[1].weight);
234
- const topEntries = sorted.slice(0, topN);
235
- const topKeys = new Set(topEntries.map(([key]) => key));
236
-
237
- const modelCount = new Map<string, number>();
238
- for (const [, { model }] of topEntries) {
239
- modelCount.set(model, (modelCount.get(model) ?? 0) + 1);
240
- }
241
- const labelByKey = new Map<string, string>();
242
- for (const [key, { model, provider }] of topEntries) {
243
- labelByKey.set(key, (modelCount.get(model) ?? 0) > 1 ? `${model} (${provider})` : model);
244
- }
245
-
246
- const allDays = [...new Set(points.map(p => p.timestamp))].sort((a, b) => a - b);
247
- const seriesNames = topEntries.map(([key]) => labelByKey.get(key) ?? key);
248
- const hasOther = points.some(p => !topKeys.has(`${p.model}::${p.provider}`));
249
- if (hasOther) seriesNames.push("Other");
250
-
251
- const dayMap = new Map<number, Record<string, B>>();
252
- for (const day of allDays) dayMap.set(day, {});
253
- for (const point of points) {
254
- const key = `${point.model}::${point.provider}`;
255
- const label = topKeys.has(key) ? (labelByKey.get(key) ?? point.model) : "Other";
256
- const row = dayMap.get(point.timestamp);
257
- if (!row) continue;
258
- const bucket = row[label] ?? initBucket();
259
- accumulate(bucket, point);
260
- row[label] = bucket;
261
- }
262
-
263
- return {
264
- labels: allDays.map(ts => format(new Date(ts), "MMM d")),
265
- datasets: seriesNames.map(name => ({
266
- label: name,
267
- data: allDays.map(day => {
268
- const bucket = dayMap.get(day)?.[name];
269
- return bucket ? bucketToValue(bucket) : 0;
270
- }),
271
- })),
272
- };
273
- }