@oh-my-pi-zen/omp-stats 16.3.6-zen.1
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 +197 -0
- package/README.md +82 -0
- package/build.ts +95 -0
- package/dist/client/index.css +1 -0
- package/dist/client/index.html +24 -0
- package/dist/client/index.js +257 -0
- package/dist/client/styles.css +1656 -0
- package/dist/types/aggregator.d.ts +87 -0
- package/dist/types/client/App.d.ts +1 -0
- package/dist/types/client/api.d.ts +21 -0
- package/dist/types/client/app/AppLayout.d.ts +16 -0
- package/dist/types/client/app/NavRail.d.ts +7 -0
- package/dist/types/client/app/RangeControl.d.ts +7 -0
- package/dist/types/client/app/SyncButton.d.ts +14 -0
- package/dist/types/client/app/ThemeToggle.d.ts +1 -0
- package/dist/types/client/app/TopBar.d.ts +15 -0
- package/dist/types/client/app/routes.d.ts +12 -0
- package/dist/types/client/components/AgentTokenShare.d.ts +5 -0
- package/dist/types/client/components/chart-shared.d.ts +173 -0
- package/dist/types/client/components/models-table-shared.d.ts +175 -0
- package/dist/types/client/components/range-meta.d.ts +21 -0
- package/dist/types/client/data/charts.d.ts +1 -0
- package/dist/types/client/data/formatters.d.ts +8 -0
- package/dist/types/client/data/useHashRoute.d.ts +8 -0
- package/dist/types/client/data/useResource.d.ts +13 -0
- package/dist/types/client/data/view-models.d.ts +67 -0
- package/dist/types/client/index.d.ts +2 -0
- package/dist/types/client/routes/BehaviorRoute.d.ts +7 -0
- package/dist/types/client/routes/CostsRoute.d.ts +7 -0
- package/dist/types/client/routes/ErrorsRoute.d.ts +8 -0
- package/dist/types/client/routes/GainRoute.d.ts +7 -0
- package/dist/types/client/routes/ModelsRoute.d.ts +7 -0
- package/dist/types/client/routes/OverviewRoute.d.ts +8 -0
- package/dist/types/client/routes/ProjectsRoute.d.ts +7 -0
- package/dist/types/client/routes/RequestsRoute.d.ts +8 -0
- package/dist/types/client/routes/ToolsRoute.d.ts +7 -0
- package/dist/types/client/routes/index.d.ts +9 -0
- package/dist/types/client/types.d.ts +63 -0
- package/dist/types/client/ui/AsyncBoundary.d.ts +12 -0
- package/dist/types/client/ui/DataTable.d.ts +17 -0
- package/dist/types/client/ui/EmptyState.d.ts +7 -0
- package/dist/types/client/ui/ErrorState.d.ts +6 -0
- package/dist/types/client/ui/JsonBlock.d.ts +7 -0
- package/dist/types/client/ui/MetricCluster.d.ts +5 -0
- package/dist/types/client/ui/Panel.d.ts +7 -0
- package/dist/types/client/ui/RequestDrawer.d.ts +5 -0
- package/dist/types/client/ui/SegmentedControl.d.ts +12 -0
- package/dist/types/client/ui/Skeleton.d.ts +8 -0
- package/dist/types/client/ui/StatusPill.d.ts +7 -0
- package/dist/types/client/ui/index.d.ts +11 -0
- package/dist/types/client/useSystemTheme.d.ts +11 -0
- package/dist/types/db.d.ts +144 -0
- package/dist/types/embedded-client.d.ts +18 -0
- package/dist/types/gain-aggregator.d.ts +26 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/parser.d.ts +53 -0
- package/dist/types/server.d.ts +7 -0
- package/dist/types/shared-types.d.ts +301 -0
- package/dist/types/sync-worker.d.ts +31 -0
- package/dist/types/types.d.ts +164 -0
- package/dist/types/user-metrics.d.ts +72 -0
- package/package.json +95 -0
- package/src/aggregator.ts +501 -0
- package/src/client/App.tsx +109 -0
- package/src/client/api.ts +102 -0
- package/src/client/app/AppLayout.tsx +93 -0
- package/src/client/app/NavRail.tsx +44 -0
- package/src/client/app/RangeControl.tsx +39 -0
- package/src/client/app/SyncButton.tsx +75 -0
- package/src/client/app/ThemeToggle.tsx +37 -0
- package/src/client/app/TopBar.tsx +73 -0
- package/src/client/app/routes.ts +69 -0
- package/src/client/components/AgentTokenShare.tsx +68 -0
- package/src/client/components/chart-shared.tsx +257 -0
- package/src/client/components/models-table-shared.tsx +255 -0
- package/src/client/components/range-meta.ts +73 -0
- package/src/client/css.d.ts +1 -0
- package/src/client/data/charts.ts +14 -0
- package/src/client/data/formatters.ts +45 -0
- package/src/client/data/useHashRoute.ts +87 -0
- package/src/client/data/useResource.ts +154 -0
- package/src/client/data/view-models.ts +251 -0
- package/src/client/index.tsx +10 -0
- package/src/client/routes/BehaviorRoute.tsx +623 -0
- package/src/client/routes/CostsRoute.tsx +234 -0
- package/src/client/routes/ErrorsRoute.tsx +118 -0
- package/src/client/routes/GainRoute.tsx +226 -0
- package/src/client/routes/ModelsRoute.tsx +430 -0
- package/src/client/routes/OverviewRoute.tsx +342 -0
- package/src/client/routes/ProjectsRoute.tsx +163 -0
- package/src/client/routes/RequestsRoute.tsx +123 -0
- package/src/client/routes/ToolsRoute.tsx +463 -0
- package/src/client/routes/index.ts +9 -0
- package/src/client/styles.css +1330 -0
- package/src/client/types.ts +80 -0
- package/src/client/ui/AsyncBoundary.tsx +54 -0
- package/src/client/ui/DataTable.tsx +122 -0
- package/src/client/ui/EmptyState.tsx +16 -0
- package/src/client/ui/ErrorState.tsx +25 -0
- package/src/client/ui/JsonBlock.tsx +75 -0
- package/src/client/ui/MetricCluster.tsx +67 -0
- package/src/client/ui/Panel.tsx +24 -0
- package/src/client/ui/RequestDrawer.tsx +208 -0
- package/src/client/ui/SegmentedControl.tsx +36 -0
- package/src/client/ui/Skeleton.tsx +17 -0
- package/src/client/ui/StatusPill.tsx +15 -0
- package/src/client/ui/index.ts +11 -0
- package/src/client/useSystemTheme.ts +87 -0
- package/src/db.ts +1530 -0
- package/src/embedded-client.generated.txt +0 -0
- package/src/embedded-client.ts +26 -0
- package/src/gain-aggregator.ts +281 -0
- package/src/index.ts +194 -0
- package/src/parser.ts +450 -0
- package/src/server.ts +352 -0
- package/src/shared-types.ts +323 -0
- package/src/sync-worker.ts +40 -0
- package/src/types.ts +171 -0
- package/src/user-metrics.ts +685 -0
- package/tailwind.config.js +40 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { X } from "lucide-react";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import type { TimeRange } from "../types";
|
|
5
|
+
import { NavRail } from "./NavRail";
|
|
6
|
+
import type { DashboardSection } from "./routes";
|
|
7
|
+
import { TopBar } from "./TopBar";
|
|
8
|
+
|
|
9
|
+
export interface AppLayoutProps {
|
|
10
|
+
activeSection: DashboardSection;
|
|
11
|
+
onSectionChange: (section: DashboardSection) => void;
|
|
12
|
+
range: TimeRange;
|
|
13
|
+
onRangeChange: (range: TimeRange) => void;
|
|
14
|
+
updatedAt: number | null;
|
|
15
|
+
onSyncStart?: () => void;
|
|
16
|
+
onSyncComplete?: (result: { success: boolean }) => void;
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function AppLayout({
|
|
21
|
+
activeSection,
|
|
22
|
+
onSectionChange,
|
|
23
|
+
range,
|
|
24
|
+
onRangeChange,
|
|
25
|
+
updatedAt,
|
|
26
|
+
onSyncStart,
|
|
27
|
+
onSyncComplete,
|
|
28
|
+
children,
|
|
29
|
+
}: AppLayoutProps) {
|
|
30
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
31
|
+
|
|
32
|
+
const handleSectionChange = (section: DashboardSection) => {
|
|
33
|
+
onSectionChange(section);
|
|
34
|
+
setMenuOpen(false);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="stats-app-container">
|
|
39
|
+
{/* Desktop Rail */}
|
|
40
|
+
<NavRail activeSection={activeSection} onSectionChange={handleSectionChange} className="stats-desktop-nav" />
|
|
41
|
+
|
|
42
|
+
{/* Mobile Nav Drawer */}
|
|
43
|
+
{menuOpen && (
|
|
44
|
+
<div className="stats-mobile-drawer-overlay" onClick={() => setMenuOpen(false)} role="presentation">
|
|
45
|
+
<div
|
|
46
|
+
className="stats-mobile-drawer"
|
|
47
|
+
onClick={e => e.stopPropagation()}
|
|
48
|
+
role="dialog"
|
|
49
|
+
aria-modal="true"
|
|
50
|
+
aria-label="Navigation menu"
|
|
51
|
+
>
|
|
52
|
+
<div className="stats-mobile-drawer-header">
|
|
53
|
+
<div className="stats-logo-container">
|
|
54
|
+
<span className="stats-logo-text">OH MY PI</span>
|
|
55
|
+
<span className="stats-logo-subtext">Observability</span>
|
|
56
|
+
</div>
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={() => setMenuOpen(false)}
|
|
60
|
+
className="stats-drawer-close-btn"
|
|
61
|
+
aria-label="Close navigation menu"
|
|
62
|
+
>
|
|
63
|
+
<X size={18} />
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
<NavRail
|
|
67
|
+
activeSection={activeSection}
|
|
68
|
+
onSectionChange={handleSectionChange}
|
|
69
|
+
className="stats-mobile-nav"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{/* Main Layout Pane */}
|
|
76
|
+
<div className="stats-main-pane">
|
|
77
|
+
<TopBar
|
|
78
|
+
activeSection={activeSection}
|
|
79
|
+
range={range}
|
|
80
|
+
onRangeChange={onRangeChange}
|
|
81
|
+
updatedAt={updatedAt}
|
|
82
|
+
onSyncStart={onSyncStart}
|
|
83
|
+
onSyncComplete={onSyncComplete}
|
|
84
|
+
onMenuToggle={() => setMenuOpen(true)}
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
<main className="stats-content-area">
|
|
88
|
+
<div className="stats-content-inner">{children}</div>
|
|
89
|
+
</main>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type LucideIcon, Monitor, Moon, Sun } from "lucide-react";
|
|
2
|
+
import { type ThemePreference, useThemePreference } from "../useSystemTheme";
|
|
3
|
+
|
|
4
|
+
const NEXT_PREFERENCE: Record<ThemePreference, ThemePreference> = {
|
|
5
|
+
system: "light",
|
|
6
|
+
light: "dark",
|
|
7
|
+
dark: "system",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const PREFERENCE_ICON: Record<ThemePreference, LucideIcon> = {
|
|
11
|
+
system: Monitor,
|
|
12
|
+
light: Sun,
|
|
13
|
+
dark: Moon,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const PREFERENCE_LABEL: Record<ThemePreference, string> = {
|
|
17
|
+
system: "System theme",
|
|
18
|
+
light: "Light theme",
|
|
19
|
+
dark: "Dark theme",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function ThemeToggle() {
|
|
23
|
+
const { preference, setPreference } = useThemePreference();
|
|
24
|
+
const Icon = PREFERENCE_ICON[preference];
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
className="stats-theme-toggle"
|
|
30
|
+
onClick={() => setPreference(NEXT_PREFERENCE[preference])}
|
|
31
|
+
aria-label={`${PREFERENCE_LABEL[preference]} (click to switch)`}
|
|
32
|
+
title={`${PREFERENCE_LABEL[preference]} — click to switch`}
|
|
33
|
+
>
|
|
34
|
+
<Icon size={16} />
|
|
35
|
+
</button>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Activity, AlertCircle, Coins, Cpu, Folder, LayoutDashboard, Smile, TrendingUp, Wrench } from "lucide-react";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
|
|
4
|
+
export type DashboardSection =
|
|
5
|
+
| "overview"
|
|
6
|
+
| "requests"
|
|
7
|
+
| "errors"
|
|
8
|
+
| "models"
|
|
9
|
+
| "tools"
|
|
10
|
+
| "costs"
|
|
11
|
+
| "behavior"
|
|
12
|
+
| "projects"
|
|
13
|
+
| "gain";
|
|
14
|
+
|
|
15
|
+
export interface DashboardRoute {
|
|
16
|
+
id: DashboardSection;
|
|
17
|
+
label: string;
|
|
18
|
+
shortLabel?: string;
|
|
19
|
+
icon: React.ComponentType<{ size?: number; className?: string }>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const routes: DashboardRoute[] = [
|
|
23
|
+
{
|
|
24
|
+
id: "overview",
|
|
25
|
+
label: "Overview",
|
|
26
|
+
icon: LayoutDashboard,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "requests",
|
|
30
|
+
label: "Requests",
|
|
31
|
+
icon: Activity,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "errors",
|
|
35
|
+
label: "Errors",
|
|
36
|
+
icon: AlertCircle,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "models",
|
|
40
|
+
label: "Models",
|
|
41
|
+
icon: Cpu,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "tools",
|
|
45
|
+
label: "Tools",
|
|
46
|
+
icon: Wrench,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "costs",
|
|
50
|
+
label: "Costs",
|
|
51
|
+
icon: Coins,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "behavior",
|
|
55
|
+
label: "Behavior",
|
|
56
|
+
shortLabel: "Behavior",
|
|
57
|
+
icon: Smile,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "projects",
|
|
61
|
+
label: "Projects",
|
|
62
|
+
icon: Folder,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "gain",
|
|
66
|
+
label: "Gain",
|
|
67
|
+
icon: TrendingUp,
|
|
68
|
+
},
|
|
69
|
+
];
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
}
|