@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 +4 -0
- package/components/InsightPanel.tsx +64 -0
- package/lib/insights.ts +86 -0
- package/package.json +1 -1
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
|
+
}
|
package/lib/insights.ts
ADDED
|
@@ -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
|
+
}
|