@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,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
|
-
}
|
package/src/client/app/routes.ts
DELETED
|
@@ -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
|
-
}
|