@notenkidev/claude-token-dashboard 0.1.11 โ†’ 0.1.12

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/app/page.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import { collect } from "@/lib/collect"
2
2
  import SummaryCards from "@/components/SummaryCards"
3
+ import InsightPanel from "@/components/InsightPanel"
3
4
  import ProjectTable from "@/components/ProjectTable"
4
5
  import DailyChart from "@/components/DailyChart"
5
6
  import RefreshButton from "@/components/RefreshButton"
@@ -46,6 +47,9 @@ export default function Page() {
46
47
  skippedDup={skippedDup}
47
48
  />
48
49
 
50
+ {/* Insights */}
51
+ <InsightPanel byProject={byProject} byProjectClaudeMd={byProjectClaudeMd} />
52
+
49
53
  {/* Daily chart */}
50
54
  <Card>
51
55
  <CardHeader>
@@ -0,0 +1,64 @@
1
+ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
2
+ import type { TokenStats } from "@/lib/collect"
3
+ import { generateInsights, type InsightLevel } from "@/lib/insights"
4
+
5
+ interface Props {
6
+ byProject: Record<string, TokenStats>
7
+ byProjectClaudeMd: Record<string, number | null>
8
+ }
9
+
10
+ const LEVEL_STYLES: Record<InsightLevel, { bar: string; icon: string; badge: string }> = {
11
+ alert: { bar: "bg-red-500", icon: "๐Ÿ”ด", badge: "text-red-400" },
12
+ warning: { bar: "bg-amber-500", icon: "โš ๏ธ", badge: "text-amber-400" },
13
+ tip: { bar: "bg-emerald-500",icon: "๐Ÿ’ก", badge: "text-emerald-400" },
14
+ }
15
+
16
+ const LEVEL_LABEL: Record<InsightLevel, string> = {
17
+ alert: "้‡่ฆ",
18
+ warning: "ๆณจๆ„",
19
+ tip: "ๆๆกˆ",
20
+ }
21
+
22
+ export default function InsightPanel({ byProject, byProjectClaudeMd }: Props) {
23
+ const insights = generateInsights(byProject, byProjectClaudeMd)
24
+
25
+ if (insights.length === 0) return null
26
+
27
+ return (
28
+ <Card>
29
+ <CardHeader className="pb-3">
30
+ <CardTitle className="text-base font-semibold text-foreground">
31
+ ๅ‰Šๆธ›ๆๆกˆ
32
+ </CardTitle>
33
+ </CardHeader>
34
+ <CardContent className="space-y-3">
35
+ {insights.map((insight, i) => {
36
+ const s = LEVEL_STYLES[insight.level]
37
+ return (
38
+ <div
39
+ key={i}
40
+ className="flex gap-3 rounded-lg border border-border bg-muted/20 overflow-hidden"
41
+ >
42
+ {/* ๅทฆใ‚ซใƒฉใƒผใƒใƒผ */}
43
+ <div className={`w-1 shrink-0 ${s.bar}`} />
44
+ <div className="flex items-start gap-2 py-3 pr-4">
45
+ <span className="text-base leading-none mt-0.5">{s.icon}</span>
46
+ <div>
47
+ <span className={`text-xs font-semibold ${s.badge} mr-2`}>
48
+ {LEVEL_LABEL[insight.level]}
49
+ </span>
50
+ <span className="text-xs font-medium text-foreground mr-2">
51
+ {insight.title}
52
+ </span>
53
+ <span className="text-xs text-muted-foreground">
54
+ {insight.message}
55
+ </span>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ )
60
+ })}
61
+ </CardContent>
62
+ </Card>
63
+ )
64
+ }
@@ -0,0 +1,86 @@
1
+ import type { TokenStats } from "./collect"
2
+ import { calcCost, PRICING } from "./pricing"
3
+
4
+ export type InsightLevel = "warning" | "alert" | "tip"
5
+
6
+ export interface Insight {
7
+ level: InsightLevel
8
+ title: string
9
+ message: string
10
+ }
11
+
12
+ const CLAUDE_MD_WARN_BYTES = 5 * 1024
13
+ const HIGH_COST_SHARE_THRESHOLD = 0.20 // 20% โ†’ alert
14
+ const HIGH_CACHE_CREATE_RATIO = 0.50 // cache_create > 50% of own cost โ†’ warning
15
+
16
+ function cacheCreateCost(stats: TokenStats): number {
17
+ return stats.cacheCreate * PRICING.cacheCreate
18
+ }
19
+
20
+ export function generateInsights(
21
+ byProject: Record<string, TokenStats>,
22
+ byProjectClaudeMd: Record<string, number | null>,
23
+ ): Insight[] {
24
+ const insights: Insight[] = []
25
+
26
+ const entries = Object.entries(byProject)
27
+ if (entries.length === 0) return insights
28
+
29
+ const totalCost = entries.reduce((s, [, v]) => s + calcCost(v), 0)
30
+ if (totalCost === 0) return insights
31
+
32
+ // โ”€โ”€ Insight 1: ๆœ€ใ‚‚ใ‚ณใ‚นใƒˆใŒ้ซ˜ใ„ใƒ—ใƒญใ‚ธใ‚งใ‚ฏใƒˆ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
33
+ const [topProject, topStats] = entries.reduce(
34
+ (best, cur) => (calcCost(cur[1]) > calcCost(best[1]) ? cur : best),
35
+ )
36
+ const topShare = calcCost(topStats) / totalCost
37
+ insights.push({
38
+ level: topShare >= HIGH_COST_SHARE_THRESHOLD ? "alert" : "tip",
39
+ title: "ๆœ€ๅคงใ‚ณใ‚นใƒˆใƒ—ใƒญใ‚ธใ‚งใ‚ฏใƒˆ",
40
+ message: `${topProject} ใŒๅ…จไฝ“ใฎ ${(topShare * 100).toFixed(1)}% ใ‚’ๅ ใ‚ใฆใ„ใพใ™ใ€‚CLAUDE.mdใฎใ‚ตใ‚คใ‚บใ‚’็ขบ่ชใ—ใฆใใ ใ•ใ„ใ€‚`,
41
+ })
42
+
43
+ // โ”€โ”€ Insight 2: cache_creation ใŒ้ซ˜ใ„ใƒ—ใƒญใ‚ธใ‚งใ‚ฏใƒˆ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
44
+ const highCacheProjects = entries
45
+ .filter(([, stats]) => {
46
+ const own = calcCost(stats)
47
+ return own > 0 && cacheCreateCost(stats) / own >= HIGH_CACHE_CREATE_RATIO
48
+ })
49
+ .sort(([, a], [, b]) => cacheCreateCost(b) - cacheCreateCost(a))
50
+ .slice(0, 3)
51
+
52
+ for (const [project] of highCacheProjects) {
53
+ insights.push({
54
+ level: "warning",
55
+ title: "cache_creation ้Žๅคš",
56
+ message: `${project} ใฎ cache_creation ใŒ้ซ˜ใ„ใงใ™ใ€‚CLAUDE.mdใŒ่‚ฅๅคงๅŒ–ใ—ใฆใ„ใ‚‹ๅฏ่ƒฝๆ€งใŒใ‚ใ‚Šใพใ™ใ€‚`,
57
+ })
58
+ }
59
+
60
+ // โ”€โ”€ Insight 3: ๅ…จไฝ“ๆœ€้ฉๅŒ–ใƒใƒ†ใƒณใ‚ทใƒฃใƒซ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
61
+ // ๅคงใใช CLAUDE.md๏ผˆโ‰ฅ5KB๏ผ‰ใ‚’ๆŒใคใƒ—ใƒญใ‚ธใ‚งใ‚ฏใƒˆใฎ cache_create ใ‚ณใ‚นใƒˆใ‚’
62
+ // 50% ๅ‰Šๆธ›ใงใใ‚‹ใจไปฎๅฎšใ—ใŸๅ ดๅˆใฎ็ฏ€็ด„้ก
63
+ const heavyProjects = entries.filter(
64
+ ([label]) => (byProjectClaudeMd[label] ?? 0) >= CLAUDE_MD_WARN_BYTES,
65
+ )
66
+
67
+ const targetProjects = heavyProjects.length > 0
68
+ ? heavyProjects
69
+ : entries.sort(([, a], [, b]) => cacheCreateCost(b) - cacheCreateCost(a)).slice(0, 3)
70
+
71
+ const optimizableCost = targetProjects.reduce(
72
+ (s, [, stats]) => s + cacheCreateCost(stats),
73
+ 0,
74
+ )
75
+ const savingPct = (optimizableCost * 0.5) / totalCost * 100
76
+
77
+ if (savingPct >= 1) {
78
+ insights.push({
79
+ level: "tip",
80
+ title: "ๆœ€้ฉๅŒ–ใƒใƒ†ใƒณใ‚ทใƒฃใƒซ",
81
+ message: `ๆœ€้ฉๅŒ–ใ™ใ‚ŒใฐๆŽจๅฎš ${savingPct.toFixed(1)}% ใฎใ‚ณใ‚นใƒˆๅ‰Šๆธ›ใŒๅฏ่ƒฝใงใ™ใ€‚`,
82
+ })
83
+ }
84
+
85
+ return insights
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notenkidev/claude-token-dashboard",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Visualize your Claude Code token usage by project and date",
5
5
  "bin": {
6
6
  "claude-token-dashboard": "bin/cli.js"