@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,87 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BarChart,
|
|
5
|
+
Bar,
|
|
6
|
+
Cell,
|
|
7
|
+
XAxis,
|
|
8
|
+
YAxis,
|
|
9
|
+
CartesianGrid,
|
|
10
|
+
Tooltip,
|
|
11
|
+
ResponsiveContainer,
|
|
12
|
+
} from 'recharts'
|
|
13
|
+
import { useTheme } from '@/components/theme-provider'
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
hourCounts: Record<string, number>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
function CustomTooltip({ active, payload, label }: any) {
|
|
21
|
+
if (!active || !payload?.length) return null
|
|
22
|
+
const hour = parseInt(label)
|
|
23
|
+
const period = hour < 12 ? 'AM' : 'PM'
|
|
24
|
+
const h12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour
|
|
25
|
+
return (
|
|
26
|
+
<div className="bg-card border border-border rounded-lg px-3 py-2 text-[13px]">
|
|
27
|
+
<p className="text-muted-foreground">{h12}:00 {period}</p>
|
|
28
|
+
<p className="text-primary font-bold">{payload[0].value} sessions</p>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function PeakHoursChart({ hourCounts }: Props) {
|
|
34
|
+
const { theme } = useTheme()
|
|
35
|
+
const isDark = theme === 'dark'
|
|
36
|
+
|
|
37
|
+
const data = Array.from({ length: 24 }, (_, i) => ({
|
|
38
|
+
hour: String(i),
|
|
39
|
+
count: hourCounts[String(i)] ?? 0,
|
|
40
|
+
}))
|
|
41
|
+
|
|
42
|
+
const sorted = [...data].sort((a, b) => b.count - a.count)
|
|
43
|
+
const top3Hours = new Set(sorted.slice(0, 3).map(d => d.hour))
|
|
44
|
+
|
|
45
|
+
// Light mode: bright amber for top, soft amber for rest
|
|
46
|
+
// Dark mode: keep the original darker tones
|
|
47
|
+
const topFill = isDark ? '#d97706' : '#f59e0b'
|
|
48
|
+
const normalFill = isDark ? '#78350f' : '#fde68a'
|
|
49
|
+
const strokeColor = isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.4)'
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div>
|
|
53
|
+
<ResponsiveContainer width="100%" height={160}>
|
|
54
|
+
<BarChart data={data} margin={{ top: 4, right: 4, left: -16, bottom: 0 }}>
|
|
55
|
+
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" vertical={false} />
|
|
56
|
+
<XAxis
|
|
57
|
+
dataKey="hour"
|
|
58
|
+
tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }}
|
|
59
|
+
tickLine={false}
|
|
60
|
+
axisLine={false}
|
|
61
|
+
tickFormatter={v => {
|
|
62
|
+
const h = parseInt(v)
|
|
63
|
+
if (h === 0) return '12a'
|
|
64
|
+
if (h === 12) return '12p'
|
|
65
|
+
if (h < 12) return `${h}a`
|
|
66
|
+
return `${h - 12}p`
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
<YAxis hide />
|
|
70
|
+
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'rgba(180,83,9,0.08)' }} />
|
|
71
|
+
<Bar dataKey="count" radius={[2, 2, 0, 0]} maxBarSize={14}
|
|
72
|
+
stroke={strokeColor} strokeWidth={0.75}>
|
|
73
|
+
{data.map(d => (
|
|
74
|
+
<Cell
|
|
75
|
+
key={d.hour}
|
|
76
|
+
fill={top3Hours.has(d.hour) ? topFill : normalFill}
|
|
77
|
+
/>
|
|
78
|
+
))}
|
|
79
|
+
</Bar>
|
|
80
|
+
</BarChart>
|
|
81
|
+
</ResponsiveContainer>
|
|
82
|
+
<p className="text-[11px] font-mono text-muted-foreground/60 mt-1">
|
|
83
|
+
top 3 peak hours highlighted
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
PieChart,
|
|
5
|
+
Pie,
|
|
6
|
+
Cell,
|
|
7
|
+
Tooltip,
|
|
8
|
+
ResponsiveContainer,
|
|
9
|
+
Legend,
|
|
10
|
+
} from 'recharts'
|
|
11
|
+
import type { ProjectSummary } from '@/types/claude'
|
|
12
|
+
import { formatTokens } from '@/lib/decode'
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
projects: ProjectSummary[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Solid hex only — Recharts SVG `fill` does not resolve `var(--*)` reliably (shows as black). */
|
|
19
|
+
const PROJECT_COLORS = [
|
|
20
|
+
'#d97706',
|
|
21
|
+
'#16a34a',
|
|
22
|
+
'#2563eb',
|
|
23
|
+
'#ea580c',
|
|
24
|
+
'#34d399',
|
|
25
|
+
'#64748b',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
function CustomTooltip({ active, payload }: any) {
|
|
30
|
+
if (!active || !payload?.length) return null
|
|
31
|
+
const { name, value } = payload[0]
|
|
32
|
+
return (
|
|
33
|
+
<div className="bg-card border border-border rounded px-3 py-2 text-[13px]">
|
|
34
|
+
<p className="text-muted-foreground">{name}</p>
|
|
35
|
+
<p className="text-foreground font-bold">{formatTokens(value)} tokens</p>
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function ProjectActivityDonut({ projects }: Props) {
|
|
41
|
+
const totalTokens = projects.reduce(
|
|
42
|
+
(s, p) => s + (p.input_tokens ?? 0) + (p.output_tokens ?? 0),
|
|
43
|
+
0
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const data = projects
|
|
47
|
+
.slice(0, 5)
|
|
48
|
+
.map((p) => ({
|
|
49
|
+
name: p.display_name,
|
|
50
|
+
value: (p.input_tokens ?? 0) + (p.output_tokens ?? 0),
|
|
51
|
+
}))
|
|
52
|
+
.filter((d) => d.value > 0)
|
|
53
|
+
|
|
54
|
+
const othersTokens = totalTokens - data.reduce((s, d) => s + d.value, 0)
|
|
55
|
+
if (othersTokens > 0) {
|
|
56
|
+
data.push({ name: 'others', value: othersTokens })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (data.length === 0) {
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex items-center justify-center h-48 text-muted-foreground text-sm">
|
|
62
|
+
no project data
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<ResponsiveContainer width="100%" height={220}>
|
|
69
|
+
<PieChart margin={{ top: 4, right: 4, bottom: 4, left: 4 }}>
|
|
70
|
+
<Pie
|
|
71
|
+
data={data}
|
|
72
|
+
cx="50%"
|
|
73
|
+
cy="50%"
|
|
74
|
+
innerRadius={50}
|
|
75
|
+
outerRadius={80}
|
|
76
|
+
paddingAngle={2}
|
|
77
|
+
dataKey="value"
|
|
78
|
+
strokeWidth={0}
|
|
79
|
+
>
|
|
80
|
+
{data.map((_, i) => (
|
|
81
|
+
<Cell key={i} fill={PROJECT_COLORS[i % PROJECT_COLORS.length]} />
|
|
82
|
+
))}
|
|
83
|
+
</Pie>
|
|
84
|
+
<Tooltip content={<CustomTooltip />} />
|
|
85
|
+
<Legend
|
|
86
|
+
iconType="circle"
|
|
87
|
+
iconSize={8}
|
|
88
|
+
wrapperStyle={{ fontSize: 12 }}
|
|
89
|
+
formatter={(value) => (
|
|
90
|
+
<span style={{ color: 'var(--muted-foreground)', fontSize: 12 }}>{value}</span>
|
|
91
|
+
)}
|
|
92
|
+
/>
|
|
93
|
+
</PieChart>
|
|
94
|
+
</ResponsiveContainer>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { TrendingUp, TrendingDown } from 'lucide-react'
|
|
4
|
+
import { LineChart, Line, ResponsiveContainer } from 'recharts'
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
|
6
|
+
import { Badge } from '@/components/ui/badge'
|
|
7
|
+
import { useTheme } from '@/components/theme-provider'
|
|
8
|
+
|
|
9
|
+
interface StatCardProps {
|
|
10
|
+
title: string
|
|
11
|
+
value: string
|
|
12
|
+
description?: string
|
|
13
|
+
/** Percentage change vs previous period: positive = up, negative = down */
|
|
14
|
+
trend?: number
|
|
15
|
+
/** Raw values for sparkline (last N days) */
|
|
16
|
+
sparkData?: number[]
|
|
17
|
+
accentColor?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Recharts sets SVG stroke/fill from strings; `var(--*)` often does not resolve on SVG → black. */
|
|
21
|
+
function resolveChartColor(accentColor: string | undefined, theme: 'light' | 'dark'): string {
|
|
22
|
+
if (!accentColor) return theme === 'light' ? '#f97316' : '#d97706'
|
|
23
|
+
switch (accentColor) {
|
|
24
|
+
case 'var(--viz-sky)':
|
|
25
|
+
return theme === 'light' ? '#1d4ed8' : '#60a5fa'
|
|
26
|
+
case 'var(--foreground)':
|
|
27
|
+
return theme === 'light' ? '#18181b' : '#e8eaed'
|
|
28
|
+
default:
|
|
29
|
+
return accentColor
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function StatCard({ title, value, description, trend, sparkData, accentColor }: StatCardProps) {
|
|
34
|
+
const { theme } = useTheme()
|
|
35
|
+
const resolvedAccent = resolveChartColor(accentColor, theme)
|
|
36
|
+
const hasTrend = trend !== undefined && !isNaN(trend)
|
|
37
|
+
const isUp = hasTrend && trend! >= 0
|
|
38
|
+
const trendColor = hasTrend
|
|
39
|
+
? isUp
|
|
40
|
+
? theme === 'light'
|
|
41
|
+
? '#059669'
|
|
42
|
+
: '#34d399'
|
|
43
|
+
: theme === 'light'
|
|
44
|
+
? '#dc2626'
|
|
45
|
+
: '#f87171'
|
|
46
|
+
: undefined
|
|
47
|
+
const rawSpark = sparkData ?? []
|
|
48
|
+
// Single point does not draw a line in Recharts; duplicate for a flat segment.
|
|
49
|
+
const chartData =
|
|
50
|
+
rawSpark.length === 1
|
|
51
|
+
? [{ v: rawSpark[0]! }, { v: rawSpark[0]! }]
|
|
52
|
+
: rawSpark.map(v => ({ v }))
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Card className="gap-3">
|
|
56
|
+
<CardHeader className="pb-0">
|
|
57
|
+
<CardDescription className="text-sm font-medium">{title}</CardDescription>
|
|
58
|
+
<div className="flex items-end justify-between mt-1">
|
|
59
|
+
<CardTitle
|
|
60
|
+
className="text-3xl font-bold tabular-nums leading-none"
|
|
61
|
+
style={{ color: resolvedAccent }}
|
|
62
|
+
>
|
|
63
|
+
{value}
|
|
64
|
+
</CardTitle>
|
|
65
|
+
{hasTrend && (
|
|
66
|
+
<Badge
|
|
67
|
+
variant="outline"
|
|
68
|
+
className="gap-1 text-xs font-medium"
|
|
69
|
+
style={{
|
|
70
|
+
color: trendColor,
|
|
71
|
+
borderColor: `${trendColor}40`,
|
|
72
|
+
backgroundColor: `${trendColor}12`,
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
{isUp ? <TrendingUp className="w-3 h-3" /> : <TrendingDown className="w-3 h-3" />}
|
|
76
|
+
{Math.abs(trend!).toFixed(1)}%
|
|
77
|
+
</Badge>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
{description && (
|
|
81
|
+
<CardDescription className="text-xs mt-1">{description}</CardDescription>
|
|
82
|
+
)}
|
|
83
|
+
</CardHeader>
|
|
84
|
+
{chartData.length > 0 && (
|
|
85
|
+
<CardContent className="pt-0 pb-4 px-6">
|
|
86
|
+
<ResponsiveContainer width="100%" height={48}>
|
|
87
|
+
<LineChart data={chartData} margin={{ top: 4, right: 4, left: 4, bottom: 0 }}>
|
|
88
|
+
<Line
|
|
89
|
+
type="monotone"
|
|
90
|
+
dataKey="v"
|
|
91
|
+
stroke={resolvedAccent}
|
|
92
|
+
strokeWidth={1.5}
|
|
93
|
+
dot={false}
|
|
94
|
+
strokeOpacity={0.7}
|
|
95
|
+
/>
|
|
96
|
+
</LineChart>
|
|
97
|
+
</ResponsiveContainer>
|
|
98
|
+
</CardContent>
|
|
99
|
+
)}
|
|
100
|
+
</Card>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AreaChart,
|
|
5
|
+
Area,
|
|
6
|
+
XAxis,
|
|
7
|
+
YAxis,
|
|
8
|
+
CartesianGrid,
|
|
9
|
+
Tooltip,
|
|
10
|
+
ResponsiveContainer,
|
|
11
|
+
Legend,
|
|
12
|
+
} from 'recharts'
|
|
13
|
+
import type { DailyActivity } from '@/types/claude'
|
|
14
|
+
import { format, parseISO, subDays } from 'date-fns'
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
data: DailyActivity[]
|
|
18
|
+
days?: number
|
|
19
|
+
dateFrom?: string
|
|
20
|
+
dateTo?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
function CustomTooltip({ active, payload, label }: any) {
|
|
25
|
+
if (!active || !payload?.length) return null
|
|
26
|
+
return (
|
|
27
|
+
<div className="bg-card border border-border rounded-lg px-3 py-2 text-[13px]">
|
|
28
|
+
<p className="text-muted-foreground mb-1">{label}</p>
|
|
29
|
+
{payload.map((p: { name: string; value: number; color: string }) => (
|
|
30
|
+
<p key={p.name} style={{ color: p.color }}>
|
|
31
|
+
{p.name}: <span className="font-bold">{p.value.toLocaleString()}</span>
|
|
32
|
+
</p>
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toIsoDate(val: string): string | null {
|
|
39
|
+
const m = val.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})$/)
|
|
40
|
+
if (m) {
|
|
41
|
+
const [, month, day, year] = m
|
|
42
|
+
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`
|
|
43
|
+
}
|
|
44
|
+
const iso = val.match(/^(\d{4})-(\d{2})-(\d{2})/)
|
|
45
|
+
return iso ? val.slice(0, 10) : null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseDateInput(val: string): Date | null {
|
|
49
|
+
const parsed = new Date(val)
|
|
50
|
+
return isNaN(parsed.getTime()) ? null : parsed
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function UsageOverTimeChart({ data, days = 90, dateFrom, dateTo }: Props) {
|
|
54
|
+
let filtered: { date: string; messages: number; sessions: number }[]
|
|
55
|
+
|
|
56
|
+
if (dateFrom && dateTo) {
|
|
57
|
+
const fromStr = toIsoDate(dateFrom.trim())
|
|
58
|
+
const toStr = toIsoDate(dateTo.trim())
|
|
59
|
+
if (fromStr && toStr && fromStr <= toStr) {
|
|
60
|
+
filtered = data
|
|
61
|
+
.filter((d) => {
|
|
62
|
+
const dataDate = /^\d{4}-\d{2}-\d{2}/.test(d.date)
|
|
63
|
+
? d.date.slice(0, 10)
|
|
64
|
+
: toIsoDate(d.date)
|
|
65
|
+
if (!dataDate) return false
|
|
66
|
+
return dataDate >= fromStr && dataDate <= toStr
|
|
67
|
+
})
|
|
68
|
+
.map((d) => ({
|
|
69
|
+
date: (() => {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = d.date.includes('-') ? parseISO(d.date) : new Date(d.date)
|
|
72
|
+
return isNaN(parsed.getTime()) ? d.date : format(parsed, 'MMM d')
|
|
73
|
+
} catch {
|
|
74
|
+
return d.date
|
|
75
|
+
}
|
|
76
|
+
})(),
|
|
77
|
+
messages: d.messageCount,
|
|
78
|
+
sessions: d.sessionCount,
|
|
79
|
+
}))
|
|
80
|
+
} else {
|
|
81
|
+
filtered = []
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
const cutoff = subDays(new Date(), days)
|
|
85
|
+
filtered = data
|
|
86
|
+
.filter((d) => parseISO(d.date) >= cutoff)
|
|
87
|
+
.map((d) => ({
|
|
88
|
+
date: format(parseISO(d.date), 'MMM d'),
|
|
89
|
+
messages: d.messageCount,
|
|
90
|
+
sessions: d.sessionCount,
|
|
91
|
+
}))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (filtered.length === 0) {
|
|
95
|
+
const from = dateFrom && dateTo ? parseDateInput(dateFrom) : null
|
|
96
|
+
const to = dateFrom && dateTo ? parseDateInput(dateTo) : null
|
|
97
|
+
const isFuture = from && to && from > new Date()
|
|
98
|
+
return (
|
|
99
|
+
<div className="flex flex-col items-center justify-center h-48 text-muted-foreground text-sm gap-1">
|
|
100
|
+
<span>no data</span>
|
|
101
|
+
{isFuture && (
|
|
102
|
+
<span className="text-[12px] text-muted-foreground/60">(selected range is in the future)</span>
|
|
103
|
+
)}
|
|
104
|
+
{dateFrom && dateTo && !isFuture && (
|
|
105
|
+
<span className="text-[12px] text-muted-foreground/60">(no activity in this date range)</span>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<ResponsiveContainer width="100%" height={220}>
|
|
113
|
+
<AreaChart data={filtered} margin={{ top: 8, right: 8, left: -10, bottom: 0 }}>
|
|
114
|
+
<defs>
|
|
115
|
+
<linearGradient id="gradMessages" x1="0" y1="0" x2="0" y2="1">
|
|
116
|
+
<stop offset="5%" stopColor="#d97706" stopOpacity={0.3} />
|
|
117
|
+
<stop offset="95%" stopColor="#d97706" stopOpacity={0} />
|
|
118
|
+
</linearGradient>
|
|
119
|
+
<linearGradient id="gradSessions" x1="0" y1="0" x2="0" y2="1">
|
|
120
|
+
<stop offset="5%" stopColor="#34d399" stopOpacity={0.2} />
|
|
121
|
+
<stop offset="95%" stopColor="#34d399" stopOpacity={0} />
|
|
122
|
+
</linearGradient>
|
|
123
|
+
</defs>
|
|
124
|
+
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" vertical={false} />
|
|
125
|
+
<XAxis
|
|
126
|
+
dataKey="date"
|
|
127
|
+
tick={{ fontSize: 12, fill: 'var(--muted-foreground)' }}
|
|
128
|
+
tickLine={false}
|
|
129
|
+
axisLine={false}
|
|
130
|
+
interval="preserveStartEnd"
|
|
131
|
+
/>
|
|
132
|
+
<YAxis
|
|
133
|
+
tick={{ fontSize: 12, fill: 'var(--muted-foreground)' }}
|
|
134
|
+
tickLine={false}
|
|
135
|
+
axisLine={false}
|
|
136
|
+
tickFormatter={v => v >= 1000 ? `${(v/1000).toFixed(0)}k` : String(v)}
|
|
137
|
+
/>
|
|
138
|
+
<Tooltip content={<CustomTooltip />} />
|
|
139
|
+
<Legend
|
|
140
|
+
wrapperStyle={{ fontSize: 12, paddingTop: 8 }}
|
|
141
|
+
formatter={(value) => (
|
|
142
|
+
<span style={{ color: 'var(--muted-foreground)', fontSize: 12 }}>{value}</span>
|
|
143
|
+
)}
|
|
144
|
+
/>
|
|
145
|
+
<Area
|
|
146
|
+
type="monotone"
|
|
147
|
+
dataKey="messages"
|
|
148
|
+
stroke="#d97706"
|
|
149
|
+
strokeWidth={2}
|
|
150
|
+
fill="url(#gradMessages)"
|
|
151
|
+
dot={false}
|
|
152
|
+
activeDot={{ r: 3, fill: '#fbbf24' }}
|
|
153
|
+
/>
|
|
154
|
+
<Area
|
|
155
|
+
type="monotone"
|
|
156
|
+
dataKey="sessions"
|
|
157
|
+
stroke="#34d399"
|
|
158
|
+
strokeWidth={1.5}
|
|
159
|
+
fill="url(#gradSessions)"
|
|
160
|
+
dot={false}
|
|
161
|
+
activeDot={{ r: 3, fill: '#6ee7b7' }}
|
|
162
|
+
/>
|
|
163
|
+
</AreaChart>
|
|
164
|
+
</ResponsiveContainer>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import { formatCost, formatDuration, formatRelativeDate } from '@/lib/decode'
|
|
5
|
+
import { categoryColorMix, toolBarColor } from '@/lib/tool-categories'
|
|
6
|
+
import type { ProjectSummary } from '@/types/claude'
|
|
7
|
+
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
|
8
|
+
import { Badge } from '@/components/ui/badge'
|
|
9
|
+
import { Clock, MessageSquare, GitBranch, Plug, Bot } from 'lucide-react'
|
|
10
|
+
|
|
11
|
+
const LANG_COLORS: Record<string, string> = {
|
|
12
|
+
TypeScript: 'bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-500/25',
|
|
13
|
+
JavaScript: 'bg-yellow-400/10 text-yellow-700 dark:text-yellow-400 border-yellow-400/30',
|
|
14
|
+
Python: 'bg-green-500/10 text-green-700 dark:text-green-400 border-green-500/25',
|
|
15
|
+
Rust: 'bg-orange-500/10 text-orange-700 dark:text-orange-400 border-orange-500/25',
|
|
16
|
+
Go: 'bg-cyan-500/10 text-cyan-700 dark:text-cyan-400 border-cyan-500/25',
|
|
17
|
+
Java: 'bg-red-500/10 text-red-700 dark:text-red-400 border-red-500/25',
|
|
18
|
+
'C++': 'bg-pink-500/10 text-pink-700 dark:text-pink-400 border-pink-500/25',
|
|
19
|
+
C: 'bg-slate-500/10 text-slate-700 dark:text-slate-400 border-slate-500/25',
|
|
20
|
+
'C#': 'bg-violet-500/10 text-violet-700 dark:text-violet-400 border-violet-500/25',
|
|
21
|
+
Ruby: 'bg-red-400/10 text-red-600 dark:text-red-400 border-red-400/25',
|
|
22
|
+
PHP: 'bg-indigo-500/10 text-indigo-700 dark:text-indigo-400 border-indigo-500/25',
|
|
23
|
+
Swift: 'bg-orange-400/10 text-orange-600 dark:text-orange-400 border-orange-400/25',
|
|
24
|
+
Kotlin: 'bg-purple-500/10 text-purple-700 dark:text-purple-400 border-purple-500/25',
|
|
25
|
+
CSS: 'bg-blue-700/10 dark:bg-sky-500/10 text-blue-700 dark:text-sky-400 border-blue-700/25 dark:border-sky-500/25',
|
|
26
|
+
HTML: 'bg-rose-500/10 text-rose-700 dark:text-rose-400 border-rose-500/25',
|
|
27
|
+
Shell: 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border-emerald-500/25',
|
|
28
|
+
Bash: 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border-emerald-500/25',
|
|
29
|
+
Markdown: 'bg-gray-400/10 text-gray-600 dark:text-gray-400 border-gray-400/25',
|
|
30
|
+
JSON: 'bg-amber-400/10 text-amber-700 dark:text-amber-400 border-amber-400/25',
|
|
31
|
+
YAML: 'bg-lime-500/10 text-lime-700 dark:text-lime-500 border-lime-500/25',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const FALLBACK_PALETTE = [
|
|
35
|
+
'bg-teal-500/10 text-teal-700 dark:text-teal-400 border-teal-500/25',
|
|
36
|
+
'bg-fuchsia-500/10 text-fuchsia-700 dark:text-fuchsia-400 border-fuchsia-500/25',
|
|
37
|
+
'bg-lime-500/10 text-lime-700 dark:text-lime-400 border-lime-500/25',
|
|
38
|
+
'bg-blue-700/10 dark:bg-sky-600/10 text-blue-700 dark:text-sky-400 border-blue-700/25 dark:border-sky-600/25',
|
|
39
|
+
'bg-rose-500/10 text-rose-700 dark:text-rose-400 border-rose-500/25',
|
|
40
|
+
'bg-amber-600/10 text-amber-700 dark:text-amber-400 border-amber-600/25',
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
function langColor(lang: string): string {
|
|
44
|
+
if (LANG_COLORS[lang]) return LANG_COLORS[lang]
|
|
45
|
+
let hash = 0
|
|
46
|
+
for (let i = 0; i < lang.length; i++) hash = (hash * 31 + lang.charCodeAt(i)) >>> 0
|
|
47
|
+
return FALLBACK_PALETTE[hash % FALLBACK_PALETTE.length]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function ProjectCard({ project }: { project: ProjectSummary }) {
|
|
51
|
+
const topTools = Object.entries(project.tool_counts ?? {})
|
|
52
|
+
.sort(([, a], [, b]) => b - a)
|
|
53
|
+
.slice(0, 5)
|
|
54
|
+
const maxToolCount = topTools[0]?.[1] ?? 1
|
|
55
|
+
|
|
56
|
+
const topLangs = Object.entries(project.languages ?? {})
|
|
57
|
+
.sort(([, a], [, b]) => b - a)
|
|
58
|
+
.slice(0, 4)
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Link href={`/projects/${project.slug}`} className="block group">
|
|
62
|
+
<Card className="h-full gap-0 py-0 hover:border-primary/40 transition-colors overflow-hidden">
|
|
63
|
+
<CardHeader className="px-4 pt-4 pb-3 gap-2">
|
|
64
|
+
{/* Title row */}
|
|
65
|
+
<div className="flex items-start justify-between gap-2">
|
|
66
|
+
<h3 className="text-sm font-semibold text-foreground group-hover:text-primary transition-colors truncate leading-snug">
|
|
67
|
+
{project.display_name}
|
|
68
|
+
</h3>
|
|
69
|
+
<span className="text-[11px] text-muted-foreground/60 whitespace-nowrap shrink-0 mt-0.5">
|
|
70
|
+
{formatRelativeDate(project.last_active)}
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Path */}
|
|
75
|
+
<p className="text-[11px] text-muted-foreground/50 font-mono truncate -mt-1">
|
|
76
|
+
{project.project_path}
|
|
77
|
+
</p>
|
|
78
|
+
|
|
79
|
+
{/* Language + feature badges */}
|
|
80
|
+
<div className="flex flex-wrap gap-1.5">
|
|
81
|
+
{topLangs.map(([lang]) => (
|
|
82
|
+
<Badge key={lang} variant="outline" className={`text-[11px] px-1.5 py-0 h-5 ${langColor(lang)}`}>
|
|
83
|
+
{lang}
|
|
84
|
+
</Badge>
|
|
85
|
+
))}
|
|
86
|
+
{project.uses_mcp && (
|
|
87
|
+
<Badge variant="outline" className="text-[11px] px-1.5 py-0 h-5 bg-blue-500/10 text-blue-500 border-blue-500/20 gap-1">
|
|
88
|
+
<Plug className="w-2.5 h-2.5" /> MCP
|
|
89
|
+
</Badge>
|
|
90
|
+
)}
|
|
91
|
+
{project.uses_task_agent && (
|
|
92
|
+
<Badge variant="outline" className="text-[11px] px-1.5 py-0 h-5 bg-purple-500/10 text-purple-500 border-purple-500/20 gap-1">
|
|
93
|
+
<Bot className="w-2.5 h-2.5" /> Agent
|
|
94
|
+
</Badge>
|
|
95
|
+
)}
|
|
96
|
+
{project.branches.length > 0 && (
|
|
97
|
+
<>
|
|
98
|
+
<span
|
|
99
|
+
className="h-4 w-px shrink-0 self-center bg-border/50"
|
|
100
|
+
aria-hidden
|
|
101
|
+
/>
|
|
102
|
+
<GitBranch className="h-3 w-3 shrink-0 self-center text-muted-foreground/45" aria-hidden />
|
|
103
|
+
{project.branches.slice(0, 3).map(b => (
|
|
104
|
+
<Badge
|
|
105
|
+
key={b}
|
|
106
|
+
variant="outline"
|
|
107
|
+
className="h-5 max-w-28 truncate border-border/50 px-1.5 py-0 font-mono text-[11px] text-muted-foreground/80"
|
|
108
|
+
title={b}
|
|
109
|
+
>
|
|
110
|
+
{b}
|
|
111
|
+
</Badge>
|
|
112
|
+
))}
|
|
113
|
+
{project.branches.length > 3 && (
|
|
114
|
+
<span className="self-center text-[11px] text-muted-foreground/45">
|
|
115
|
+
+{project.branches.length - 3}
|
|
116
|
+
</span>
|
|
117
|
+
)}
|
|
118
|
+
</>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</CardHeader>
|
|
122
|
+
|
|
123
|
+
<CardContent className="px-4 pb-4 space-y-3">
|
|
124
|
+
{/* Stats row */}
|
|
125
|
+
<div className="flex items-center gap-3 text-[12px] text-muted-foreground">
|
|
126
|
+
<span className="flex items-center gap-1">
|
|
127
|
+
<MessageSquare className="w-3 h-3" />
|
|
128
|
+
{project.session_count} sessions
|
|
129
|
+
</span>
|
|
130
|
+
<span className="text-border">·</span>
|
|
131
|
+
<span className="flex items-center gap-1">
|
|
132
|
+
<Clock className="w-3 h-3" />
|
|
133
|
+
{formatDuration(project.total_duration_minutes)}
|
|
134
|
+
</span>
|
|
135
|
+
{(project.total_lines_added ?? 0) > 0 && (
|
|
136
|
+
<>
|
|
137
|
+
<span className="text-border">·</span>
|
|
138
|
+
<span className="text-emerald-500 font-mono">+{project.total_lines_added.toLocaleString()}</span>
|
|
139
|
+
<span className="text-red-400 font-mono">-{project.total_lines_removed.toLocaleString()}</span>
|
|
140
|
+
</>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Tool bar chart */}
|
|
145
|
+
{topTools.length > 0 && (
|
|
146
|
+
<div className="space-y-1">
|
|
147
|
+
{topTools.map(([tool, count]) => {
|
|
148
|
+
const color = toolBarColor(tool)
|
|
149
|
+
const width = Math.max(8, Math.round((count / maxToolCount) * 100))
|
|
150
|
+
return (
|
|
151
|
+
<div key={tool} className="flex items-center gap-2 text-[11px]">
|
|
152
|
+
<span className="text-muted-foreground/50 w-16 truncate">{tool}</span>
|
|
153
|
+
<div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
|
|
154
|
+
<div
|
|
155
|
+
className="h-full rounded-full transition-all"
|
|
156
|
+
style={{ width: `${width}%`, backgroundColor: categoryColorMix(color, 58) }}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
<span className="text-muted-foreground/40 w-7 text-right tabular-nums">{count}</span>
|
|
160
|
+
</div>
|
|
161
|
+
)
|
|
162
|
+
})}
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
{/* Cost footer */}
|
|
167
|
+
<div className="flex items-center justify-between pt-2 border-t border-border/30">
|
|
168
|
+
<span className="text-[11px] text-muted-foreground/50">Est. cost</span>
|
|
169
|
+
<span className="text-sm font-bold text-primary tabular-nums">{formatCost(project.estimated_cost)}</span>
|
|
170
|
+
</div>
|
|
171
|
+
</CardContent>
|
|
172
|
+
</Card>
|
|
173
|
+
</Link>
|
|
174
|
+
)
|
|
175
|
+
}
|