@axplusb/kepler 0.0.1 → 1.0.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/README.md +82 -0
- package/package.json +36 -4
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +52 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +142 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +159 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +52 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +182 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/claude-reader.ts +594 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +89 -0
- package/src/agents/parser.mjs +98 -0
- package/src/agents/teams.mjs +123 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +277 -0
- package/src/config/cli-args.mjs +173 -0
- package/src/config/env.mjs +263 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +270 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/core/agent-loop.mjs +480 -0
- package/src/core/approval.mjs +273 -0
- package/src/core/backend-url.mjs +57 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +149 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/headless.mjs +168 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +351 -0
- package/src/core/local-agent.mjs +429 -0
- package/src/core/local-store.mjs +325 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +101 -0
- package/src/core/pricing.mjs +314 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/safety.mjs +200 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +317 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +367 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +135 -0
- package/src/core/tool-executor.mjs +725 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +370 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +573 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/loader.mjs +147 -0
- package/src/skills/runner.mjs +55 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +421 -0
- package/src/terminal/main.mjs +150 -0
- package/src/terminal/repl.mjs +1484 -0
- package/src/terminal/tool-display.mjs +58 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +148 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +132 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/banner.mjs +237 -0
- package/src/ui/commands.mjs +499 -0
- package/src/ui/formatter.mjs +379 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/slash-commands.mjs +258 -0
- package/index.js +0 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { NextResponse } from 'next/server'
|
|
3
|
+
import { getSessions, listProjectSlugs, listProjectJSONLFiles, readJSONLLines } from '@/lib/claude-reader'
|
|
4
|
+
import { categorizeTool, isMcpTool, parseMcpTool } from '@/lib/tool-categories'
|
|
5
|
+
import type { ToolsAnalytics, ToolSummary, McpServerSummary, VersionRecord } from '@/types/claude'
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic'
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
type AnyLine = Record<string, any>
|
|
11
|
+
|
|
12
|
+
export async function GET() {
|
|
13
|
+
const sessions = await getSessions()
|
|
14
|
+
const totalSessions = sessions.length
|
|
15
|
+
|
|
16
|
+
// ── Aggregate tool counts across all sessions ──────────────────────────────
|
|
17
|
+
const toolTotals = new Map<string, number>()
|
|
18
|
+
const toolSessionCount = new Map<string, Set<string>>()
|
|
19
|
+
const mcpServerCalls = new Map<string, Map<string, number>>()
|
|
20
|
+
const mcpServerSessions = new Map<string, Set<string>>()
|
|
21
|
+
const errorCategories: Record<string, number> = {}
|
|
22
|
+
let totalErrors = 0
|
|
23
|
+
|
|
24
|
+
for (const s of sessions) {
|
|
25
|
+
const sid = s.session_id
|
|
26
|
+
for (const [tool, count] of Object.entries(s.tool_counts ?? {})) {
|
|
27
|
+
toolTotals.set(tool, (toolTotals.get(tool) ?? 0) + count)
|
|
28
|
+
if (!toolSessionCount.has(tool)) toolSessionCount.set(tool, new Set())
|
|
29
|
+
toolSessionCount.get(tool)!.add(sid)
|
|
30
|
+
|
|
31
|
+
if (isMcpTool(tool)) {
|
|
32
|
+
const parsed = parseMcpTool(tool)
|
|
33
|
+
if (parsed) {
|
|
34
|
+
if (!mcpServerCalls.has(parsed.server)) mcpServerCalls.set(parsed.server, new Map())
|
|
35
|
+
if (!mcpServerSessions.has(parsed.server)) mcpServerSessions.set(parsed.server, new Set())
|
|
36
|
+
const srv = mcpServerCalls.get(parsed.server)!
|
|
37
|
+
srv.set(parsed.tool, (srv.get(parsed.tool) ?? 0) + count)
|
|
38
|
+
mcpServerSessions.get(parsed.server)!.add(sid)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Error categories
|
|
44
|
+
for (const [cat, count] of Object.entries(s.tool_error_categories ?? {})) {
|
|
45
|
+
errorCategories[cat] = (errorCategories[cat] ?? 0) + count
|
|
46
|
+
totalErrors += count
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Build ToolSummary list ─────────────────────────────────────────────────
|
|
51
|
+
const tools: ToolSummary[] = [...toolTotals.entries()]
|
|
52
|
+
.map(([name, total_calls]) => ({
|
|
53
|
+
name,
|
|
54
|
+
category: categorizeTool(name),
|
|
55
|
+
total_calls,
|
|
56
|
+
session_count: toolSessionCount.get(name)?.size ?? 0,
|
|
57
|
+
error_count: 0,
|
|
58
|
+
}))
|
|
59
|
+
.sort((a, b) => b.total_calls - a.total_calls)
|
|
60
|
+
|
|
61
|
+
const totalToolCalls = tools.reduce((s, t) => s + t.total_calls, 0)
|
|
62
|
+
|
|
63
|
+
// ── MCP server summaries ───────────────────────────────────────────────────
|
|
64
|
+
const mcp_servers: McpServerSummary[] = [...mcpServerCalls.entries()]
|
|
65
|
+
.map(([server_name, toolMap]) => {
|
|
66
|
+
const toolArr = [...toolMap.entries()]
|
|
67
|
+
.map(([name, calls]) => ({ name, calls }))
|
|
68
|
+
.sort((a, b) => b.calls - a.calls)
|
|
69
|
+
const total_calls = toolArr.reduce((s, t) => s + t.calls, 0)
|
|
70
|
+
return {
|
|
71
|
+
server_name,
|
|
72
|
+
tools: toolArr,
|
|
73
|
+
total_calls,
|
|
74
|
+
session_count: mcpServerSessions.get(server_name)?.size ?? 0,
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
.sort((a, b) => b.total_calls - a.total_calls)
|
|
78
|
+
|
|
79
|
+
// ── Feature adoption ──────────────────────────────────────────────────────
|
|
80
|
+
const featureSessions = {
|
|
81
|
+
task_agents: sessions.filter(s => s.uses_task_agent || (s.tool_counts?.Task ?? 0) > 0).length,
|
|
82
|
+
mcp: sessions.filter(s => s.uses_mcp || Object.keys(s.tool_counts ?? {}).some(isMcpTool)).length,
|
|
83
|
+
web_search: sessions.filter(s => s.uses_web_search || (s.tool_counts?.WebSearch ?? 0) > 0).length,
|
|
84
|
+
web_fetch: sessions.filter(s => s.uses_web_fetch || (s.tool_counts?.WebFetch ?? 0) > 0).length,
|
|
85
|
+
plan_mode: sessions.filter(s => (s.tool_counts?.EnterPlanMode ?? 0) > 0).length,
|
|
86
|
+
git_commits: sessions.filter(s => (s.git_commits ?? 0) > 0).length,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const feature_adoption: Record<string, { sessions: number; pct: number }> = {}
|
|
90
|
+
for (const [key, count] of Object.entries(featureSessions)) {
|
|
91
|
+
feature_adoption[key] = { sessions: count, pct: totalSessions > 0 ? count / totalSessions : 0 }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Version + branch info from JSONL ─────────────────────────────────────
|
|
95
|
+
const versionData = new Map<string, { sessions: Set<string>; dates: string[] }>()
|
|
96
|
+
const branchTurns = new Map<string, number>()
|
|
97
|
+
|
|
98
|
+
const slugs = await listProjectSlugs()
|
|
99
|
+
await Promise.all(
|
|
100
|
+
slugs.map(async (slug) => {
|
|
101
|
+
const files = await listProjectJSONLFiles(slug)
|
|
102
|
+
await Promise.all(
|
|
103
|
+
files.map(async (f) => {
|
|
104
|
+
const sessionId = path.basename(f, '.jsonl')
|
|
105
|
+
let fileVersion: string | undefined
|
|
106
|
+
let fileDate: string | undefined
|
|
107
|
+
|
|
108
|
+
await readJSONLLines(f, (line: AnyLine) => {
|
|
109
|
+
if (!fileVersion && line.version) {
|
|
110
|
+
fileVersion = line.version
|
|
111
|
+
fileDate = line.timestamp
|
|
112
|
+
}
|
|
113
|
+
if (line.gitBranch && line.gitBranch !== 'HEAD') {
|
|
114
|
+
branchTurns.set(line.gitBranch, (branchTurns.get(line.gitBranch) ?? 0) + 1)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
if (fileVersion) {
|
|
119
|
+
if (!versionData.has(fileVersion)) {
|
|
120
|
+
versionData.set(fileVersion, { sessions: new Set(), dates: [] })
|
|
121
|
+
}
|
|
122
|
+
const vd = versionData.get(fileVersion)!
|
|
123
|
+
vd.sessions.add(sessionId)
|
|
124
|
+
if (fileDate) vd.dates.push(fileDate)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
const versions: VersionRecord[] = [...versionData.entries()]
|
|
132
|
+
.map(([version, data]) => {
|
|
133
|
+
const sortedDates = data.dates.sort()
|
|
134
|
+
return {
|
|
135
|
+
version,
|
|
136
|
+
session_count: data.sessions.size,
|
|
137
|
+
first_seen: sortedDates[0] ?? '',
|
|
138
|
+
last_seen: sortedDates[sortedDates.length - 1] ?? '',
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
.sort((a, b) => b.last_seen.localeCompare(a.last_seen))
|
|
142
|
+
|
|
143
|
+
const branches = [...branchTurns.entries()]
|
|
144
|
+
.map(([branch, turns]) => ({ branch, turns }))
|
|
145
|
+
.sort((a, b) => b.turns - a.turns)
|
|
146
|
+
.slice(0, 15)
|
|
147
|
+
|
|
148
|
+
const result: ToolsAnalytics = {
|
|
149
|
+
tools,
|
|
150
|
+
mcp_servers,
|
|
151
|
+
feature_adoption,
|
|
152
|
+
versions,
|
|
153
|
+
branches,
|
|
154
|
+
error_categories: errorCategories,
|
|
155
|
+
total_tool_calls: totalToolCalls,
|
|
156
|
+
total_errors: totalErrors,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return NextResponse.json(result)
|
|
160
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import useSWR from 'swr'
|
|
4
|
+
import { TopBar } from '@/components/layout/top-bar'
|
|
5
|
+
import { CostOverTimeChart } from '@/components/costs/cost-over-time-chart'
|
|
6
|
+
import { CostByProjectChart } from '@/components/costs/cost-by-project-chart'
|
|
7
|
+
import { ModelTokenTable } from '@/components/costs/model-token-table'
|
|
8
|
+
import { CacheEfficiencyPanel } from '@/components/costs/cache-efficiency-panel'
|
|
9
|
+
import { formatCost, formatTokens } from '@/lib/decode'
|
|
10
|
+
import { PRICING } from '@/lib/pricing'
|
|
11
|
+
import type { CostAnalytics } from '@/types/claude'
|
|
12
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
13
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
14
|
+
import { Alert, AlertDescription } from '@/components/ui/alert'
|
|
15
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
16
|
+
import { AlertTriangle, TrendingDown, DollarSign, Banknote } from 'lucide-react'
|
|
17
|
+
|
|
18
|
+
const fetcher = (url: string) =>
|
|
19
|
+
fetch(url).then(r => { if (!r.ok) throw new Error(`API error ${r.status}`); return r.json() })
|
|
20
|
+
|
|
21
|
+
export default function CostsPage() {
|
|
22
|
+
const { data, error, isLoading } = useSWR<CostAnalytics>('/api/costs', fetcher, { refreshInterval: 5_000 })
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="flex flex-col min-h-screen">
|
|
26
|
+
<TopBar title="Costs" subtitle="Estimated spend from ~/.orca/" />
|
|
27
|
+
<div className="p-6 space-y-6">
|
|
28
|
+
|
|
29
|
+
{error && (
|
|
30
|
+
<Alert variant="destructive">
|
|
31
|
+
<AlertTriangle className="h-4 w-4" />
|
|
32
|
+
<AlertDescription>Error loading data: {String(error)}</AlertDescription>
|
|
33
|
+
</Alert>
|
|
34
|
+
)}
|
|
35
|
+
|
|
36
|
+
{isLoading && (
|
|
37
|
+
<div className="space-y-4">
|
|
38
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
39
|
+
{Array.from({ length: 3 }).map((_, i) => <Skeleton key={i} className="h-28 rounded-xl" />)}
|
|
40
|
+
</div>
|
|
41
|
+
{Array.from({ length: 3 }).map((_, i) => <Skeleton key={i} className="h-56 rounded-xl" />)}
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
|
|
45
|
+
{data && (
|
|
46
|
+
<>
|
|
47
|
+
{/* Hero stat cards */}
|
|
48
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
49
|
+
<Card>
|
|
50
|
+
<CardHeader className="pb-2">
|
|
51
|
+
<CardDescription className="flex items-center gap-2">
|
|
52
|
+
<DollarSign className="w-4 h-4" /> Total Estimated Cost
|
|
53
|
+
</CardDescription>
|
|
54
|
+
<CardTitle className="text-3xl font-bold tabular-nums text-[#d97706]">
|
|
55
|
+
{formatCost(data.total_cost)}
|
|
56
|
+
</CardTitle>
|
|
57
|
+
</CardHeader>
|
|
58
|
+
<CardContent>
|
|
59
|
+
<p className="text-xs text-muted-foreground">All time spend across all projects</p>
|
|
60
|
+
</CardContent>
|
|
61
|
+
</Card>
|
|
62
|
+
|
|
63
|
+
<Card>
|
|
64
|
+
<CardHeader className="pb-2">
|
|
65
|
+
<CardDescription className="flex items-center gap-2">
|
|
66
|
+
<TrendingDown className="w-4 h-4" /> Cache Savings
|
|
67
|
+
</CardDescription>
|
|
68
|
+
<CardTitle className="text-3xl font-bold tabular-nums text-[#34d399]">
|
|
69
|
+
{formatCost(data.total_savings)}
|
|
70
|
+
</CardTitle>
|
|
71
|
+
</CardHeader>
|
|
72
|
+
<CardContent>
|
|
73
|
+
<p className="text-xs text-muted-foreground">Saved by prompt caching</p>
|
|
74
|
+
</CardContent>
|
|
75
|
+
</Card>
|
|
76
|
+
|
|
77
|
+
<Card>
|
|
78
|
+
<CardHeader className="pb-2">
|
|
79
|
+
<CardDescription className="flex items-center gap-2">
|
|
80
|
+
<Banknote className="w-4 h-4" /> Without Cache
|
|
81
|
+
</CardDescription>
|
|
82
|
+
<CardTitle className="text-3xl font-bold tabular-nums text-red-400">
|
|
83
|
+
{formatCost(data.total_cost + data.total_savings)}
|
|
84
|
+
</CardTitle>
|
|
85
|
+
</CardHeader>
|
|
86
|
+
<CardContent>
|
|
87
|
+
<p className="text-xs text-muted-foreground">What you would have spent</p>
|
|
88
|
+
</CardContent>
|
|
89
|
+
</Card>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{/* Cost over time */}
|
|
93
|
+
{data.daily.length > 0 && (
|
|
94
|
+
<Card>
|
|
95
|
+
<CardHeader>
|
|
96
|
+
<CardTitle>Cost Over Time</CardTitle>
|
|
97
|
+
<CardDescription>Daily estimated spend</CardDescription>
|
|
98
|
+
</CardHeader>
|
|
99
|
+
<CardContent>
|
|
100
|
+
<CostOverTimeChart daily={data.daily} />
|
|
101
|
+
</CardContent>
|
|
102
|
+
</Card>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
{/* Cost by project */}
|
|
106
|
+
{data.by_project.length > 0 && (
|
|
107
|
+
<Card>
|
|
108
|
+
<CardHeader>
|
|
109
|
+
<CardTitle>Cost by Project</CardTitle>
|
|
110
|
+
<CardDescription>Spend breakdown across projects</CardDescription>
|
|
111
|
+
</CardHeader>
|
|
112
|
+
<CardContent>
|
|
113
|
+
<CostByProjectChart projects={data.by_project} />
|
|
114
|
+
</CardContent>
|
|
115
|
+
</Card>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{/* Per-model table */}
|
|
119
|
+
<Card>
|
|
120
|
+
<CardHeader>
|
|
121
|
+
<CardTitle>Per-Model Token Breakdown</CardTitle>
|
|
122
|
+
<CardDescription>Token usage and cost by model</CardDescription>
|
|
123
|
+
</CardHeader>
|
|
124
|
+
<CardContent>
|
|
125
|
+
<ModelTokenTable models={data.models} />
|
|
126
|
+
</CardContent>
|
|
127
|
+
</Card>
|
|
128
|
+
|
|
129
|
+
{/* Cache efficiency */}
|
|
130
|
+
<Card>
|
|
131
|
+
<CardHeader>
|
|
132
|
+
<CardTitle>Cache Efficiency</CardTitle>
|
|
133
|
+
<CardDescription>How much caching is saving you</CardDescription>
|
|
134
|
+
</CardHeader>
|
|
135
|
+
<CardContent>
|
|
136
|
+
<CacheEfficiencyPanel models={data.models} totalSavings={data.total_savings} />
|
|
137
|
+
</CardContent>
|
|
138
|
+
</Card>
|
|
139
|
+
|
|
140
|
+
{/* Pricing reference */}
|
|
141
|
+
<Card>
|
|
142
|
+
<CardHeader>
|
|
143
|
+
<CardTitle>Pricing Reference</CardTitle>
|
|
144
|
+
<CardDescription>
|
|
145
|
+
Estimates only — update rates in{' '}
|
|
146
|
+
<code className="text-xs bg-muted px-1 rounded">lib/pricing.ts</code>
|
|
147
|
+
</CardDescription>
|
|
148
|
+
</CardHeader>
|
|
149
|
+
<CardContent>
|
|
150
|
+
<Table>
|
|
151
|
+
<TableHeader>
|
|
152
|
+
<TableRow>
|
|
153
|
+
<TableHead>Model</TableHead>
|
|
154
|
+
<TableHead className="text-right">Input /MTok</TableHead>
|
|
155
|
+
<TableHead className="text-right">Output /MTok</TableHead>
|
|
156
|
+
<TableHead className="text-right">Cache Write /MTok</TableHead>
|
|
157
|
+
<TableHead className="text-right">Cache Read /MTok</TableHead>
|
|
158
|
+
</TableRow>
|
|
159
|
+
</TableHeader>
|
|
160
|
+
<TableBody>
|
|
161
|
+
{Object.entries(PRICING).map(([model, p]) => (
|
|
162
|
+
<TableRow key={model}>
|
|
163
|
+
<TableCell className="font-mono text-sm">{model}</TableCell>
|
|
164
|
+
<TableCell className="text-right font-mono text-blue-700 dark:text-[#60a5fa]">${(p.input * 1_000_000).toFixed(2)}</TableCell>
|
|
165
|
+
<TableCell className="text-right font-mono text-[#d97706]">${(p.output * 1_000_000).toFixed(2)}</TableCell>
|
|
166
|
+
<TableCell className="text-right font-mono text-[#a78bfa]">${(p.cacheWrite * 1_000_000).toFixed(2)}</TableCell>
|
|
167
|
+
<TableCell className="text-right font-mono text-[#34d399]">${(p.cacheRead * 1_000_000).toFixed(2)}</TableCell>
|
|
168
|
+
</TableRow>
|
|
169
|
+
))}
|
|
170
|
+
</TableBody>
|
|
171
|
+
</Table>
|
|
172
|
+
</CardContent>
|
|
173
|
+
</Card>
|
|
174
|
+
</>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
)
|
|
179
|
+
}
|