@arraystar/tokenscope 0.1.0
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 +87 -0
- package/bun.lock +1170 -0
- package/components.json +25 -0
- package/eslint.config.js +23 -0
- package/index.html +13 -0
- package/package.json +46 -0
- package/public/data.json +41 -0
- package/public/favicon.svg +1 -0
- package/public/icons.svg +24 -0
- package/src/App.css +184 -0
- package/src/App.tsx +98 -0
- package/src/PricingContext.tsx +131 -0
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/vite.svg +1 -0
- package/src/cli.ts +98 -0
- package/src/components/Overview.tsx +468 -0
- package/src/components/PricingSheet.tsx +209 -0
- package/src/components/SessionDetail.tsx +244 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/card.tsx +103 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/separator.tsx +23 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +721 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/table.tsx +114 -0
- package/src/components/ui/tooltip.tsx +64 -0
- package/src/data.ts +90 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/i18n.tsx +148 -0
- package/src/index.css +138 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/src/parser/claude.ts +225 -0
- package/src/parser/codex.ts +181 -0
- package/src/parser/index.ts +83 -0
- package/src/parser/types.ts +35 -0
- package/src/pricing.ts +139 -0
- package/src/types.ts +56 -0
- package/tsconfig.app.json +30 -0
- package/tsconfig.json +13 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +13 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils"
|
|
2
|
+
|
|
3
|
+
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
data-slot="skeleton"
|
|
7
|
+
className={cn("animate-pulse rounded-md bg-muted", className)}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Skeleton }
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
data-slot="table-container"
|
|
9
|
+
className="relative w-full overflow-x-auto"
|
|
10
|
+
>
|
|
11
|
+
<table
|
|
12
|
+
data-slot="table"
|
|
13
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
|
21
|
+
return (
|
|
22
|
+
<thead
|
|
23
|
+
data-slot="table-header"
|
|
24
|
+
className={cn("[&_tr]:border-b", className)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
|
31
|
+
return (
|
|
32
|
+
<tbody
|
|
33
|
+
data-slot="table-body"
|
|
34
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
|
41
|
+
return (
|
|
42
|
+
<tfoot
|
|
43
|
+
data-slot="table-footer"
|
|
44
|
+
className={cn(
|
|
45
|
+
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
54
|
+
return (
|
|
55
|
+
<tr
|
|
56
|
+
data-slot="table-row"
|
|
57
|
+
className={cn(
|
|
58
|
+
"border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
67
|
+
return (
|
|
68
|
+
<th
|
|
69
|
+
data-slot="table-head"
|
|
70
|
+
className={cn(
|
|
71
|
+
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
|
|
72
|
+
className
|
|
73
|
+
)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
80
|
+
return (
|
|
81
|
+
<td
|
|
82
|
+
data-slot="table-cell"
|
|
83
|
+
className={cn(
|
|
84
|
+
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
|
|
85
|
+
className
|
|
86
|
+
)}
|
|
87
|
+
{...props}
|
|
88
|
+
/>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function TableCaption({
|
|
93
|
+
className,
|
|
94
|
+
...props
|
|
95
|
+
}: React.ComponentProps<"caption">) {
|
|
96
|
+
return (
|
|
97
|
+
<caption
|
|
98
|
+
data-slot="table-caption"
|
|
99
|
+
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
Table,
|
|
107
|
+
TableHeader,
|
|
108
|
+
TableBody,
|
|
109
|
+
TableFooter,
|
|
110
|
+
TableHead,
|
|
111
|
+
TableRow,
|
|
112
|
+
TableCell,
|
|
113
|
+
TableCaption,
|
|
114
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function TooltipProvider({
|
|
6
|
+
delay = 0,
|
|
7
|
+
...props
|
|
8
|
+
}: TooltipPrimitive.Provider.Props) {
|
|
9
|
+
return (
|
|
10
|
+
<TooltipPrimitive.Provider
|
|
11
|
+
data-slot="tooltip-provider"
|
|
12
|
+
delay={delay}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
|
19
|
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
|
23
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function TooltipContent({
|
|
27
|
+
className,
|
|
28
|
+
side = "top",
|
|
29
|
+
sideOffset = 4,
|
|
30
|
+
align = "center",
|
|
31
|
+
alignOffset = 0,
|
|
32
|
+
children,
|
|
33
|
+
...props
|
|
34
|
+
}: TooltipPrimitive.Popup.Props &
|
|
35
|
+
Pick<
|
|
36
|
+
TooltipPrimitive.Positioner.Props,
|
|
37
|
+
"align" | "alignOffset" | "side" | "sideOffset"
|
|
38
|
+
>) {
|
|
39
|
+
return (
|
|
40
|
+
<TooltipPrimitive.Portal>
|
|
41
|
+
<TooltipPrimitive.Positioner
|
|
42
|
+
align={align}
|
|
43
|
+
alignOffset={alignOffset}
|
|
44
|
+
side={side}
|
|
45
|
+
sideOffset={sideOffset}
|
|
46
|
+
className="isolate z-50"
|
|
47
|
+
>
|
|
48
|
+
<TooltipPrimitive.Popup
|
|
49
|
+
data-slot="tooltip-content"
|
|
50
|
+
className={cn(
|
|
51
|
+
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
|
58
|
+
</TooltipPrimitive.Popup>
|
|
59
|
+
</TooltipPrimitive.Positioner>
|
|
60
|
+
</TooltipPrimitive.Portal>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
package/src/data.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { DashboardData, SessionData } from "./types";
|
|
2
|
+
import data from "../public/data.json" with { type: "json" };
|
|
3
|
+
|
|
4
|
+
export function useData(): DashboardData {
|
|
5
|
+
return data as DashboardData;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function fmt(n: number): string {
|
|
9
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
10
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
11
|
+
return n.toLocaleString();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function fmtCost(cost: number, symbol = "$"): string {
|
|
15
|
+
if (cost >= 1) return `${symbol}${cost.toFixed(2)}`;
|
|
16
|
+
if (cost >= 0.01) return `${symbol}${cost.toFixed(3)}`;
|
|
17
|
+
return `${symbol}${cost.toFixed(4)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function fmtPercent(value: number, total: number): string {
|
|
21
|
+
if (total === 0) return "0%";
|
|
22
|
+
return `${((value / total) * 100).toFixed(1)}%`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const COLORS = [
|
|
26
|
+
"hsl(220, 70%, 50%)",
|
|
27
|
+
"hsl(160, 60%, 45%)",
|
|
28
|
+
"hsl(30, 80%, 55%)",
|
|
29
|
+
"hsl(280, 60%, 55%)",
|
|
30
|
+
"hsl(0, 70%, 55%)",
|
|
31
|
+
"hsl(190, 70%, 45%)",
|
|
32
|
+
"hsl(45, 80%, 50%)",
|
|
33
|
+
"hsl(330, 60%, 50%)",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export function projectColor(index: number): string {
|
|
37
|
+
return COLORS[index % COLORS.length];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SkillStat {
|
|
41
|
+
name: string;
|
|
42
|
+
total: number;
|
|
43
|
+
cost: number;
|
|
44
|
+
turns: number;
|
|
45
|
+
color: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getSkillStats(data: DashboardData): SkillStat[] {
|
|
49
|
+
const map = new Map<string, { total: number; cost: number; turns: number }>();
|
|
50
|
+
for (const modelData of Object.values(data.models)) {
|
|
51
|
+
for (const sessionData of Object.values(modelData.sessions)) {
|
|
52
|
+
for (const turn of sessionData.turns) {
|
|
53
|
+
const m = turn.user.match(/skills\/([^/\s]+)/);
|
|
54
|
+
if (!m) continue;
|
|
55
|
+
const name = m[1];
|
|
56
|
+
const cur = map.get(name) ?? { total: 0, cost: 0, turns: 0 };
|
|
57
|
+
cur.total += turn.total;
|
|
58
|
+
cur.cost += turn.cost;
|
|
59
|
+
cur.turns += 1;
|
|
60
|
+
map.set(name, cur);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return Array.from(map.entries())
|
|
65
|
+
.map(([name, v], i) => ({ name, ...v, color: COLORS[i % COLORS.length] }))
|
|
66
|
+
.sort((a, b) => b.total - a.total);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getAllSessions(data: DashboardData): (SessionData & { model: string; sessionKey: string })[] {
|
|
70
|
+
const sessions: (SessionData & { model: string; sessionKey: string })[] = [];
|
|
71
|
+
for (const [modelName, modelData] of Object.entries(data.models)) {
|
|
72
|
+
for (const [sessionKey, sessionData] of Object.entries(modelData.sessions)) {
|
|
73
|
+
sessions.push({ ...sessionData, model: modelName, sessionKey });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return sessions.sort((a, b) => b.stats.total - a.stats.total);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getSources(data: DashboardData): ("claude" | "codex")[] {
|
|
80
|
+
if (data.sources?.length) return data.sources;
|
|
81
|
+
// Auto-detect: if any session has source field, collect unique values
|
|
82
|
+
const found = new Set<"claude" | "codex">();
|
|
83
|
+
for (const modelData of Object.values(data.models)) {
|
|
84
|
+
for (const sessionData of Object.values(modelData.sessions)) {
|
|
85
|
+
if (sessionData.source) found.add(sessionData.source);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return found.size > 0 ? Array.from(found) : ["claude"]; // default to claude for legacy data
|
|
89
|
+
}
|
|
90
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768
|
|
4
|
+
|
|
5
|
+
export function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
|
7
|
+
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
10
|
+
const onChange = () => {
|
|
11
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
12
|
+
}
|
|
13
|
+
mql.addEventListener("change", onChange)
|
|
14
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
15
|
+
return () => mql.removeEventListener("change", onChange)
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
return !!isMobile
|
|
19
|
+
}
|
package/src/i18n.tsx
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
export type Lang = "zh" | "en";
|
|
4
|
+
|
|
5
|
+
const t = {
|
|
6
|
+
zh: {
|
|
7
|
+
title: "TokenScope",
|
|
8
|
+
subtitle: "Your token, visualized.",
|
|
9
|
+
overview: "概览",
|
|
10
|
+
sessions: "会话",
|
|
11
|
+
generated: "生成于",
|
|
12
|
+
totalTokens: "总 Tokens",
|
|
13
|
+
messages: "条消息",
|
|
14
|
+
cacheRead: "缓存读取",
|
|
15
|
+
ofTotal: "% 占总量",
|
|
16
|
+
sessionsCount: "会话数",
|
|
17
|
+
projects: "个项目",
|
|
18
|
+
estCost: "预估费用",
|
|
19
|
+
basedOnPricing: "基于 API 定价",
|
|
20
|
+
tokenByProject: "按项目分布",
|
|
21
|
+
tokenBreakdown: "Token 构成",
|
|
22
|
+
tokenBySkill: "按技能分布",
|
|
23
|
+
turns: "轮",
|
|
24
|
+
dailyUsage: "每日 Token 用量",
|
|
25
|
+
topSessions: "热门会话",
|
|
26
|
+
input: "输入",
|
|
27
|
+
output: "输出",
|
|
28
|
+
cacheReadLabel: "缓存读",
|
|
29
|
+
cacheWriteLabel: "缓存写",
|
|
30
|
+
msgs: "条消息",
|
|
31
|
+
sessionNotFound: "未找到该会话",
|
|
32
|
+
backToOverview: "返回总览",
|
|
33
|
+
session: "会话",
|
|
34
|
+
tokens: "tokens",
|
|
35
|
+
tokenUsagePerTurn: "每轮对话 Token 用量",
|
|
36
|
+
conversationTurns: "对话记录",
|
|
37
|
+
turn: "轮",
|
|
38
|
+
time: "时间",
|
|
39
|
+
message: "消息",
|
|
40
|
+
inputCol: "输入",
|
|
41
|
+
outputCol: "输出",
|
|
42
|
+
cacheRCol: "缓存读",
|
|
43
|
+
cacheWriteCol: "缓存写",
|
|
44
|
+
totalCol: "总计",
|
|
45
|
+
costCol: "费用",
|
|
46
|
+
totalLabel: "总计",
|
|
47
|
+
sortAsc: "升序",
|
|
48
|
+
sortDesc: "降序",
|
|
49
|
+
turnLabel: "轮次",
|
|
50
|
+
timeLabel: "时间",
|
|
51
|
+
messageLabel: "消息",
|
|
52
|
+
settings: "设置",
|
|
53
|
+
modelPricing: "模型定价",
|
|
54
|
+
detectedModels: "检测到的模型",
|
|
55
|
+
selectPricing: "选择定价方案",
|
|
56
|
+
customPricing: "自定义",
|
|
57
|
+
inputPrice: "输入价格",
|
|
58
|
+
outputPrice: "输出价格",
|
|
59
|
+
cacheReadPrice: "缓存读价格",
|
|
60
|
+
perMillionTokens: "每百万 tokens",
|
|
61
|
+
resetToDefaults: "恢复默认",
|
|
62
|
+
noPricing: "未设置定价",
|
|
63
|
+
currency: "货币",
|
|
64
|
+
sourceFilter: "数据来源",
|
|
65
|
+
allSources: "全部",
|
|
66
|
+
claudeCode: "Claude Code",
|
|
67
|
+
codexCli: "Codex CLI",
|
|
68
|
+
},
|
|
69
|
+
en: {
|
|
70
|
+
title: "TokenScope",
|
|
71
|
+
subtitle: "Your token, visualized.",
|
|
72
|
+
overview: "Overview",
|
|
73
|
+
sessions: "Sessions",
|
|
74
|
+
generated: "Generated",
|
|
75
|
+
totalTokens: "Total Tokens",
|
|
76
|
+
messages: "messages",
|
|
77
|
+
cacheRead: "Cache Read",
|
|
78
|
+
ofTotal: "% of total",
|
|
79
|
+
sessionsCount: "Sessions",
|
|
80
|
+
projects: "projects",
|
|
81
|
+
estCost: "Est. Cost",
|
|
82
|
+
basedOnPricing: "Based on API pricing",
|
|
83
|
+
tokenByProject: "Token Usage by Project",
|
|
84
|
+
tokenBreakdown: "Token Breakdown",
|
|
85
|
+
tokenBySkill: "Token Usage by Skill",
|
|
86
|
+
turns: "turns",
|
|
87
|
+
dailyUsage: "Daily Token Usage",
|
|
88
|
+
topSessions: "Top Sessions",
|
|
89
|
+
input: "Input",
|
|
90
|
+
output: "Output",
|
|
91
|
+
cacheReadLabel: "Cache Read",
|
|
92
|
+
cacheWriteLabel: "Cache Write",
|
|
93
|
+
msgs: "messages",
|
|
94
|
+
sessionNotFound: "Session not found",
|
|
95
|
+
backToOverview: "Back to Overview",
|
|
96
|
+
session: "Session",
|
|
97
|
+
tokens: "tokens",
|
|
98
|
+
tokenUsagePerTurn: "Token Usage per Turn",
|
|
99
|
+
conversationTurns: "Conversation Turns",
|
|
100
|
+
turn: "#",
|
|
101
|
+
time: "Time",
|
|
102
|
+
message: "Message",
|
|
103
|
+
inputCol: "Input",
|
|
104
|
+
outputCol: "Output",
|
|
105
|
+
cacheRCol: "CacheR",
|
|
106
|
+
cacheWriteCol: "CacheW",
|
|
107
|
+
totalCol: "Total",
|
|
108
|
+
costCol: "Cost",
|
|
109
|
+
totalLabel: "Total",
|
|
110
|
+
sortAsc: "asc",
|
|
111
|
+
sortDesc: "desc",
|
|
112
|
+
turnLabel: "#",
|
|
113
|
+
timeLabel: "Time",
|
|
114
|
+
messageLabel: "Message",
|
|
115
|
+
settings: "Settings",
|
|
116
|
+
modelPricing: "Model Pricing",
|
|
117
|
+
detectedModels: "Detected Models",
|
|
118
|
+
selectPricing: "Select pricing profile",
|
|
119
|
+
customPricing: "Custom",
|
|
120
|
+
inputPrice: "Input Price",
|
|
121
|
+
outputPrice: "Output Price",
|
|
122
|
+
cacheReadPrice: "Cache Read Price",
|
|
123
|
+
perMillionTokens: "per 1M tokens",
|
|
124
|
+
resetToDefaults: "Reset to Defaults",
|
|
125
|
+
noPricing: "No pricing set",
|
|
126
|
+
currency: "Currency",
|
|
127
|
+
sourceFilter: "Data Source",
|
|
128
|
+
allSources: "All",
|
|
129
|
+
claudeCode: "Claude Code",
|
|
130
|
+
codexCli: "Codex CLI",
|
|
131
|
+
},
|
|
132
|
+
} as const;
|
|
133
|
+
|
|
134
|
+
export type Translations = (typeof t)["zh"];
|
|
135
|
+
export type TranslationKey = keyof Translations;
|
|
136
|
+
|
|
137
|
+
export function useTranslations(lang: Lang): Translations {
|
|
138
|
+
return t[lang] as Translations;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const LangContext = createContext<{
|
|
142
|
+
lang: Lang;
|
|
143
|
+
setLang: (l: Lang) => void;
|
|
144
|
+
}>({ lang: "zh", setLang: () => {} });
|
|
145
|
+
|
|
146
|
+
export function useLang() {
|
|
147
|
+
return useContext(LangContext);
|
|
148
|
+
}
|
package/src/index.css
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
@import "shadcn/tailwind.css";
|
|
4
|
+
@import "@fontsource-variable/geist";
|
|
5
|
+
|
|
6
|
+
@custom-variant dark (&:is(.dark *));
|
|
7
|
+
|
|
8
|
+
@theme inline {
|
|
9
|
+
--font-heading: var(--font-sans);
|
|
10
|
+
--font-sans: 'Geist Variable', sans-serif;
|
|
11
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
12
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
13
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
14
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
15
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
16
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
17
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
18
|
+
--color-sidebar: var(--sidebar);
|
|
19
|
+
--color-chart-5: var(--chart-5);
|
|
20
|
+
--color-chart-4: var(--chart-4);
|
|
21
|
+
--color-chart-3: var(--chart-3);
|
|
22
|
+
--color-chart-2: var(--chart-2);
|
|
23
|
+
--color-chart-1: var(--chart-1);
|
|
24
|
+
--color-ring: var(--ring);
|
|
25
|
+
--color-input: var(--input);
|
|
26
|
+
--color-border: var(--border);
|
|
27
|
+
--color-destructive: var(--destructive);
|
|
28
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
29
|
+
--color-accent: var(--accent);
|
|
30
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
31
|
+
--color-muted: var(--muted);
|
|
32
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
33
|
+
--color-secondary: var(--secondary);
|
|
34
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
35
|
+
--color-primary: var(--primary);
|
|
36
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
37
|
+
--color-popover: var(--popover);
|
|
38
|
+
--color-card-foreground: var(--card-foreground);
|
|
39
|
+
--color-card: var(--card);
|
|
40
|
+
--color-foreground: var(--foreground);
|
|
41
|
+
--color-background: var(--background);
|
|
42
|
+
--radius-sm: calc(var(--radius) * 0.6);
|
|
43
|
+
--radius-md: calc(var(--radius) * 0.8);
|
|
44
|
+
--radius-lg: var(--radius);
|
|
45
|
+
--radius-xl: calc(var(--radius) * 1.4);
|
|
46
|
+
--radius-2xl: calc(var(--radius) * 1.8);
|
|
47
|
+
--radius-3xl: calc(var(--radius) * 2.2);
|
|
48
|
+
--radius-4xl: calc(var(--radius) * 2.6);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
:root {
|
|
52
|
+
--background: oklch(1 0 0);
|
|
53
|
+
--foreground: oklch(0.145 0 0);
|
|
54
|
+
--card: oklch(1 0 0);
|
|
55
|
+
--card-foreground: oklch(0.145 0 0);
|
|
56
|
+
--popover: oklch(1 0 0);
|
|
57
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
58
|
+
--primary: oklch(0.205 0 0);
|
|
59
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
60
|
+
--secondary: oklch(0.97 0 0);
|
|
61
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
62
|
+
--muted: oklch(0.97 0 0);
|
|
63
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
64
|
+
--accent: oklch(0.97 0 0);
|
|
65
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
66
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
67
|
+
--border: oklch(0.922 0 0);
|
|
68
|
+
--input: oklch(0.922 0 0);
|
|
69
|
+
--ring: oklch(0.708 0 0);
|
|
70
|
+
--chart-1: oklch(0.87 0 0);
|
|
71
|
+
--chart-2: oklch(0.556 0 0);
|
|
72
|
+
--chart-3: oklch(0.439 0 0);
|
|
73
|
+
--chart-4: oklch(0.371 0 0);
|
|
74
|
+
--chart-5: oklch(0.269 0 0);
|
|
75
|
+
--radius: 0.625rem;
|
|
76
|
+
--sidebar: oklch(0.985 0 0);
|
|
77
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
78
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
79
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
80
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
81
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
82
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
83
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.dark {
|
|
87
|
+
--background: oklch(0.145 0 0);
|
|
88
|
+
--foreground: oklch(0.985 0 0);
|
|
89
|
+
--card: oklch(0.205 0 0);
|
|
90
|
+
--card-foreground: oklch(0.985 0 0);
|
|
91
|
+
--popover: oklch(0.205 0 0);
|
|
92
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
93
|
+
--primary: oklch(0.922 0 0);
|
|
94
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
95
|
+
--secondary: oklch(0.269 0 0);
|
|
96
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
97
|
+
--muted: oklch(0.269 0 0);
|
|
98
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
99
|
+
--accent: oklch(0.269 0 0);
|
|
100
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
101
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
102
|
+
--border: oklch(1 0 0 / 10%);
|
|
103
|
+
--input: oklch(1 0 0 / 15%);
|
|
104
|
+
--ring: oklch(0.556 0 0);
|
|
105
|
+
--chart-1: oklch(0.87 0 0);
|
|
106
|
+
--chart-2: oklch(0.556 0 0);
|
|
107
|
+
--chart-3: oklch(0.439 0 0);
|
|
108
|
+
--chart-4: oklch(0.371 0 0);
|
|
109
|
+
--chart-5: oklch(0.269 0 0);
|
|
110
|
+
--sidebar: oklch(0.205 0 0);
|
|
111
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
112
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
113
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
114
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
115
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
116
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
117
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@layer base {
|
|
121
|
+
* {
|
|
122
|
+
@apply border-border outline-ring/50;
|
|
123
|
+
}
|
|
124
|
+
body {
|
|
125
|
+
@apply bg-background text-foreground;
|
|
126
|
+
}
|
|
127
|
+
html {
|
|
128
|
+
@apply font-sans;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@keyframes slide-in {
|
|
133
|
+
from { transform: translateX(100%); }
|
|
134
|
+
to { transform: translateX(0); }
|
|
135
|
+
}
|
|
136
|
+
.animate-slide-in {
|
|
137
|
+
animation: slide-in 0.2s ease-out;
|
|
138
|
+
}
|
package/src/lib/utils.ts
ADDED
package/src/main.tsx
ADDED