activity grid

This commit is contained in:
Randy Lu
2025-11-16 17:05:49 +08:00
parent 3664836f58
commit d6a506afa3
11 changed files with 629 additions and 148 deletions
+8 -1
View File
@@ -18,7 +18,14 @@
"Bash(gh workflow run:*)", "Bash(gh workflow run:*)",
"Skill(web-fetch)", "Skill(web-fetch)",
"Bash(gh workflow list:*)", "Bash(gh workflow list:*)",
"Bash(gh release view:*)" "Bash(gh release view:*)",
"mcp__shadcn__view_items_in_registries",
"Bash(pnpm remove:*)",
"Bash(for:*)",
"Bash(do echo:*)",
"Bash(jq:*)",
"Bash(done)",
"Bash(pnpm exec biome format:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []
+3 -1
View File
@@ -30,6 +30,8 @@
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@remixicon/react": "^4.6.0", "@remixicon/react": "^4.6.0",
"@tanstack/react-query": "^5.90.2", "@tanstack/react-query": "^5.90.2",
@@ -45,7 +47,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"date-fns": "^4.1.0", "dayjs": "^1.11.19",
"i18next": "^25.5.3", "i18next": "^25.5.3",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
+89 -36
View File
@@ -31,7 +31,7 @@ importers:
version: 5.2.2(react-hook-form@7.64.0(react@19.2.0)) version: 5.2.2(react-hook-form@7.64.0(react@19.2.0))
'@lobehub/icons': '@lobehub/icons':
specifier: ^2.38.0 specifier: ^2.38.0
version: 2.38.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 2.38.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-accordion': '@radix-ui/react-accordion':
specifier: ^1.2.12 specifier: ^1.2.12
version: 1.2.12(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 1.2.12(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -68,6 +68,12 @@ importers:
'@radix-ui/react-tabs': '@radix-ui/react-tabs':
specifier: ^1.1.13 specifier: ^1.1.13
version: 1.1.13(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 1.1.13(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-toggle':
specifier: ^1.1.10
version: 1.1.10(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-toggle-group':
specifier: ^1.1.11
version: 1.1.11(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-tooltip': '@radix-ui/react-tooltip':
specifier: ^1.2.8 specifier: ^1.2.8
version: 1.2.8(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 1.2.8(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -113,9 +119,9 @@ importers:
cmdk: cmdk:
specifier: ^1.1.1 specifier: ^1.1.1
version: 1.1.1(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 1.1.1(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
date-fns: dayjs:
specifier: ^4.1.0 specifier: ^1.11.19
version: 4.1.0 version: 1.11.19
i18next: i18next:
specifier: ^25.5.3 specifier: ^25.5.3
version: 25.5.3(typescript@5.8.3) version: 25.5.3(typescript@5.8.3)
@@ -1569,6 +1575,32 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/react-toggle-group@1.1.11':
resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-toggle@1.1.10':
resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-tooltip@1.0.5': '@radix-ui/react-tooltip@1.0.5':
resolution: {integrity: sha512-cDKVcfzyO6PpckZekODJZDe5ZxZ2fCZlzKzTmPhe4mX9qTHRfLcKgqb0OKf22xLwDequ2tVleim+ZYx3rabD5w==} resolution: {integrity: sha512-cDKVcfzyO6PpckZekODJZDe5ZxZ2fCZlzKzTmPhe4mX9qTHRfLcKgqb0OKf22xLwDequ2tVleim+ZYx3rabD5w==}
peerDependencies: peerDependencies:
@@ -2890,11 +2922,8 @@ packages:
date-fns@3.6.0: date-fns@3.6.0:
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
date-fns@4.1.0: dayjs@1.11.19:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
dayjs@1.11.18:
resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==}
debug@4.4.3: debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
@@ -6527,12 +6556,12 @@ snapshots:
'@lobehub/emojilib@1.0.0': {} '@lobehub/emojilib@1.0.0': {}
'@lobehub/fluent-emoji@2.0.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': '@lobehub/fluent-emoji@2.0.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies: dependencies:
'@lobehub/emojilib': 1.0.0 '@lobehub/emojilib': 1.0.0
'@lobehub/ui': 2.13.2(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@lobehub/ui': 2.13.2(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
antd: 5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) antd: 5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
antd-style: 3.7.1(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) antd-style: 3.7.1(@types/react@19.2.0)(antd@5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
emoji-regex: 10.5.0 emoji-regex: 10.5.0
lodash-es: 4.17.21 lodash-es: 4.17.21
lucide-react: 0.469.0(react@19.2.0) lucide-react: 0.469.0(react@19.2.0)
@@ -6549,11 +6578,11 @@ snapshots:
- micromark-util-types - micromark-util-types
- supports-color - supports-color
'@lobehub/icons@2.38.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': '@lobehub/icons@2.38.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies: dependencies:
'@lobehub/ui': 2.13.2(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@lobehub/ui': 2.13.2(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
antd: 5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) antd: 5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
antd-style: 3.7.1(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) antd-style: 3.7.1(@types/react@19.2.0)(antd@5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
lucide-react: 0.469.0(react@19.2.0) lucide-react: 0.469.0(react@19.2.0)
polished: 4.3.1 polished: 4.3.1
react: 19.2.0 react: 19.2.0
@@ -6568,7 +6597,7 @@ snapshots:
- micromark-util-types - micromark-util-types
- supports-color - supports-color
'@lobehub/ui@2.13.2(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': '@lobehub/ui@2.13.2(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies: dependencies:
'@ant-design/cssinjs': 1.24.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@ant-design/cssinjs': 1.24.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@dnd-kit/core': 6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@dnd-kit/core': 6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -6579,19 +6608,19 @@ snapshots:
'@emoji-mart/react': 1.1.1(emoji-mart@5.6.0)(react@19.2.0) '@emoji-mart/react': 1.1.1(emoji-mart@5.6.0)(react@19.2.0)
'@floating-ui/react': 0.27.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@floating-ui/react': 0.27.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@giscus/react': 3.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@giscus/react': 3.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@lobehub/fluent-emoji': 2.0.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@lobehub/fluent-emoji': 2.0.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@lobehub/icons': 2.38.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@lobehub/icons': 2.38.0(@babel/core@7.28.4)(@types/mdast@4.0.4)(@types/react@19.2.0)(antd@5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@mdx-js/mdx': 3.1.1 '@mdx-js/mdx': 3.1.1
'@mdx-js/react': 3.1.1(@types/react@19.2.0)(react@19.2.0) '@mdx-js/react': 3.1.1(@types/react@19.2.0)(react@19.2.0)
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.0)(react@19.2.0) '@radix-ui/react-slot': 1.2.3(@types/react@19.2.0)(react@19.2.0)
'@shikijs/transformers': 3.13.0 '@shikijs/transformers': 3.13.0
'@splinetool/runtime': 0.9.526 '@splinetool/runtime': 0.9.526
ahooks: 3.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) ahooks: 3.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
antd: 5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) antd: 5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
antd-style: 3.7.1(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) antd-style: 3.7.1(@types/react@19.2.0)(antd@5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
chroma-js: 3.1.2 chroma-js: 3.1.2
class-variance-authority: 0.7.1 class-variance-authority: 0.7.1
dayjs: 1.11.18 dayjs: 1.11.19
emoji-mart: 5.6.0 emoji-mart: 5.6.0
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
framer-motion: 12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0) framer-motion: 12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -7207,6 +7236,32 @@ snapshots:
'@types/react': 19.2.0 '@types/react': 19.2.0
'@types/react-dom': 19.2.0(@types/react@19.2.0) '@types/react-dom': 19.2.0(@types/react@19.2.0)
'@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-context': 1.1.2(@types/react@19.2.0)(react@19.2.0)
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.0)(react@19.2.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.0)(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
optionalDependencies:
'@types/react': 19.2.0
'@types/react-dom': 19.2.0(@types/react@19.2.0)
'@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.0)(react@19.2.0)
react: 19.2.0
react-dom: 19.2.0(react@19.2.0)
optionalDependencies:
'@types/react': 19.2.0
'@types/react-dom': 19.2.0(@types/react@19.2.0)
'@radix-ui/react-tooltip@1.0.5(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': '@radix-ui/react-tooltip@1.0.5(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
@@ -8061,7 +8116,7 @@ snapshots:
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
'@types/js-cookie': 3.0.6 '@types/js-cookie': 3.0.6
dayjs: 1.11.18 dayjs: 1.11.19
intersection-observer: 0.12.2 intersection-observer: 0.12.2
js-cookie: 3.0.5 js-cookie: 3.0.5
lodash: 4.17.21 lodash: 4.17.21
@@ -8089,7 +8144,7 @@ snapshots:
ansis@4.2.0: {} ansis@4.2.0: {}
antd-style@3.7.1(@types/react@19.2.0)(antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0): antd-style@3.7.1(@types/react@19.2.0)(antd@5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies: dependencies:
'@ant-design/cssinjs': 1.24.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@ant-design/cssinjs': 1.24.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
@@ -8098,7 +8153,7 @@ snapshots:
'@emotion/react': 11.14.0(@types/react@19.2.0)(react@19.2.0) '@emotion/react': 11.14.0(@types/react@19.2.0)(react@19.2.0)
'@emotion/serialize': 1.3.3 '@emotion/serialize': 1.3.3
'@emotion/utils': 1.4.2 '@emotion/utils': 1.4.2
antd: 5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) antd: 5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react: 19.2.0 react: 19.2.0
use-merge-value: 1.2.0(react@19.2.0) use-merge-value: 1.2.0(react@19.2.0)
transitivePeerDependencies: transitivePeerDependencies:
@@ -8106,7 +8161,7 @@ snapshots:
- react-dom - react-dom
- supports-color - supports-color
antd@5.27.4(date-fns@4.1.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): antd@5.27.4(date-fns@3.6.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies: dependencies:
'@ant-design/colors': 7.2.1 '@ant-design/colors': 7.2.1
'@ant-design/cssinjs': 1.24.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@ant-design/cssinjs': 1.24.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -8122,7 +8177,7 @@ snapshots:
'@rc-component/trigger': 2.3.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@rc-component/trigger': 2.3.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
classnames: 2.5.1 classnames: 2.5.1
copy-to-clipboard: 3.3.3 copy-to-clipboard: 3.3.3
dayjs: 1.11.18 dayjs: 1.11.19
rc-cascader: 3.34.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) rc-cascader: 3.34.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
rc-checkbox: 3.5.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) rc-checkbox: 3.5.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
rc-collapse: 3.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) rc-collapse: 3.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -8138,7 +8193,7 @@ snapshots:
rc-motion: 2.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) rc-motion: 2.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
rc-notification: 5.6.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) rc-notification: 5.6.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
rc-pagination: 5.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) rc-pagination: 5.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
rc-picker: 4.11.3(date-fns@4.1.0)(dayjs@1.11.18)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) rc-picker: 4.11.3(date-fns@3.6.0)(dayjs@1.11.19)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
rc-progress: 4.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) rc-progress: 4.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
rc-rate: 2.13.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) rc-rate: 2.13.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
rc-resize-observer: 1.4.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) rc-resize-observer: 1.4.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -8616,9 +8671,7 @@ snapshots:
date-fns@3.6.0: {} date-fns@3.6.0: {}
date-fns@4.1.0: {} dayjs@1.11.19: {}
dayjs@1.11.18: {}
debug@4.4.3: debug@4.4.3:
dependencies: dependencies:
@@ -9705,7 +9758,7 @@ snapshots:
d3: 7.9.0 d3: 7.9.0
d3-sankey: 0.12.3 d3-sankey: 0.12.3
dagre-d3-es: 7.0.11 dagre-d3-es: 7.0.11
dayjs: 1.11.18 dayjs: 1.11.19
dompurify: 3.2.7 dompurify: 3.2.7
katex: 0.16.23 katex: 0.16.23
khroma: 2.1.0 khroma: 2.1.0
@@ -10464,7 +10517,7 @@ snapshots:
react: 19.2.0 react: 19.2.0
react-dom: 19.2.0(react@19.2.0) react-dom: 19.2.0(react@19.2.0)
rc-picker@4.11.3(date-fns@4.1.0)(dayjs@1.11.18)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): rc-picker@4.11.3(date-fns@3.6.0)(dayjs@1.11.19)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
'@rc-component/trigger': 2.3.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@rc-component/trigger': 2.3.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -10475,8 +10528,8 @@ snapshots:
react: 19.2.0 react: 19.2.0
react-dom: 19.2.0(react@19.2.0) react-dom: 19.2.0(react@19.2.0)
optionalDependencies: optionalDependencies:
date-fns: 4.1.0 date-fns: 3.6.0
dayjs: 1.11.18 dayjs: 1.11.19
rc-progress@4.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): rc-progress@4.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies: dependencies:
+307
View File
@@ -0,0 +1,307 @@
import dayjs from 'dayjs';
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { type ProjectUsageRecord } from "@/lib/query";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface ActivityGridProps {
data: ProjectUsageRecord[];
onFilteredDataChange?: (data: ProjectUsageRecord[]) => void;
}
interface DayData {
date: Date;
dateKey: string;
totalTokens: number;
count: number;
records: ProjectUsageRecord[];
}
interface ActivityLevel {
level: number;
color: string;
darkColor: string;
}
export function ActivityGrid({ data }: ActivityGridProps) {
const { t } = useTranslation();
// Activity utilities included in the component
const getActivityData = useMemo(() => {
const today = dayjs();
const startDate = today.startOf('year');
// Create a map of all dates in the time range
const dateMap = new Map<string, DayData>();
// Initialize all dates with zero values
let currentDate = startDate;
while (currentDate.isBefore(today) || currentDate.isSame(today, 'day')) {
const dateKey = currentDate.format('YYYY-MM-DD');
const date = currentDate.toDate();
dateMap.set(dateKey, {
date,
dateKey,
totalTokens: 0,
count: 0,
records: []
});
currentDate = currentDate.add(1, 'day');
}
// Process usage data
data.forEach(record => {
try {
const recordDate = dayjs(record.timestamp);
const dateKey = recordDate.format('YYYY-MM-DD');
if (dateMap.has(dateKey)) {
const existing = dateMap.get(dateKey)!;
const tokens = (record.usage?.input_tokens || 0) +
(record.usage?.output_tokens || 0) +
(record.usage?.cache_read_input_tokens || 0);
existing.totalTokens += tokens;
existing.count += 1;
existing.records.push(record);
}
} catch (error) {
console.warn('Invalid date in record:', record.timestamp);
}
});
return Array.from(dateMap.values());
}, [data]);
const getIntensityLevel = (totalTokens: number): ActivityLevel => {
// Calculate intensity levels based on token usage percentiles
const allValues = getActivityData.map(d => d.totalTokens).filter(v => v > 0);
if (totalTokens === 0 || allValues.length === 0) {
return { level: 0, color: '#ebedf0', darkColor: '#161b22' };
}
// Simple intensity calculation using quartiles
const sorted = [...allValues].sort((a, b) => a - b);
const len = sorted.length;
if (totalTokens >= sorted[Math.floor(len * 0.9)]) {
return { level: 4, color: '#216e39', darkColor: '#0e4429' };
} else if (totalTokens >= sorted[Math.floor(len * 0.7)]) {
return { level: 3, color: '#30a14e', darkColor: '#006d32' };
} else if (totalTokens >= sorted[Math.floor(len * 0.5)]) {
return { level: 2, color: '#40c463', darkColor: '#26a641' };
} else if (totalTokens >= sorted[Math.floor(len * 0.3)]) {
return { level: 1, color: '#9be9a8', darkColor: '#39d353' };
} else {
return { level: 0, color: '#ebedf0', darkColor: '#161b22' };
}
};
const createGrid = () => {
const today = dayjs();
const startDate = today.startOf('year');
const weeks: DayData[][] = [];
// Start from January 1st and create weeks
let currentDate = startDate;
// Generate weeks until we pass today
while (currentDate.isBefore(today) || currentDate.isSame(today, 'day')) {
const currentWeek: DayData[] = [];
// Create a week (7 days), but only include days that are within our date range
for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
const dateKey = currentDate.format('YYYY-MM-DD');
const dayData = getActivityData.find(d => d.dateKey === dateKey);
currentWeek.push(dayData || {
date: currentDate.toDate(),
totalTokens: 0,
count: 0,
records: [],
dateKey
});
currentDate = currentDate.add(1, 'day');
// Stop if we've passed today
if (currentDate.isAfter(today)) {
break;
}
}
// Only add the week if it contains at least one day from our target period
if (currentWeek.some(day => {
const dayJs = dayjs(day.date);
return dayJs.isAfter(startDate.subtract(1, 'day')) && dayJs.isBefore(today.add(1, 'day'));
})) {
weeks.push(currentWeek);
}
}
return weeks;
};
const grid = createGrid();
const monthLabels = useMemo(() => {
if (grid.length === 0) return [];
const result: Array<{ month: string; weekIndex: number }> = [];
grid.forEach((week, weekIndex) => {
// Safety check for week data
if (!week || week.length === 0 || !week[0]) {
return;
}
const firstDay = dayjs(week[0].date);
// Safety check for date
if (!firstDay || !firstDay.isValid()) {
console.warn(`Invalid date at week ${weekIndex}:`, week[0].date);
return;
}
// Use dayjs to format month name directly
const monthName = firstDay.format('MMM');
// Only show label if this is the first week of the month or first week overall
const isFirstWeek = weekIndex === 0;
const previousWeekMonth = weekIndex > 0 && grid[weekIndex - 1]?.[0]?.date && dayjs(grid[weekIndex - 1][0].date).format('MMM');
const isFirstWeekOfMonth = !isFirstWeek && previousWeekMonth !== monthName;
if (isFirstWeek || isFirstWeekOfMonth) {
result.push({ month: monthName, weekIndex });
}
});
return result;
}, [grid]);
return (
<div className="space-y-4">
<div className="overflow-x-auto p-3">
<div className="inline-block min-w-full">
<div className="flex gap-3">
{/* Grid */}
<div className="flex-1">
{/* Month labels */}
<div className="flex mb-2 gap-1">
{grid.map((_, weekIndex) => {
const monthLabel = monthLabels.find(m => m.weekIndex === weekIndex);
const displayText = monthLabel?.month || '';
return (
<div
key={`week-label-week-${weekIndex}`}
className="w-3 flex justify-center"
title={`Week ${weekIndex}: ${displayText || 'No label'}`}
>
{displayText && (
<div className="text-xs text-muted-foreground whitespace-nowrap">
{displayText}
</div>
)}
</div>
);
})}
</div>
{/* Activity grid */}
<div className="flex gap-1">
{grid.map((week, weekIndex) => (
<div key={`activity-week-${weekIndex}`} className="flex flex-col gap-1">
{week.map((dayData, dayIndex) => {
const intensity = getIntensityLevel(dayData.totalTokens);
return (
<TooltipProvider key={`activity-day-${weekIndex}-${dayIndex}`}>
<Tooltip>
<TooltipTrigger asChild>
<div
className="w-3 h-3 rounded-sm cursor-pointer border"
style={{
backgroundColor: intensity.color,
borderColor: 'rgba(27, 31, 36, 0.06)'
}}
data-date={dayjs(dayData.date).format('YYYY-MM-DD')}
data-level={intensity.level}
/>
</TooltipTrigger>
<TooltipContent>
<div className="text-sm space-y-1">
<div className="font-medium">
{dayjs(dayData.date).format('MMM D, YYYY')}
</div>
{dayData.totalTokens > 0 ? (
<>
<div>
{formatLargeNumber(dayData.totalTokens)} {t("usage.tokens", "tokens")}
</div>
<div>
{dayData.count} {t("usage.requests", "requests")}
</div>
</>
) : (
<div className="text-muted-foreground">
{t("usage.noActivity", "No activity")}
</div>
)}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
})}
</div>
))}
</div>
</div>
</div>
</div>
</div>
{/* Legend - Fixed position, not scrollable */}
<div className="flex items-center px-3 gap-2 mt-3 text-xs text-muted-foreground">
<span>{t("usage.activityGrid.less", "Less")}</span>
<div className="flex gap-1">
{[0, 1, 2, 3, 4].map((level) => (
<div
key={level}
className="w-3 h-3 rounded-sm border"
style={{
backgroundColor: level === 0 ? '#ebedf0' :
level === 1 ? '#9be9a8' :
level === 2 ? '#40c463' :
level === 3 ? '#30a14e' : '#216e39',
borderColor: 'rgba(27, 31, 36, 0.06)'
}}
/>
))}
</div>
<span>{t("usage.activityGrid.more", "More")}</span>
</div>
</div>
);
}
// Helper function for formatting large numbers
function formatLargeNumber(num: number): string {
if (num >= 1000000) {
return `${(num / 1000000).toFixed(1)}M`;
} else if (num >= 1000) {
return `${(num / 1000).toFixed(1)}K`;
}
return num.toString();
}
+53 -74
View File
@@ -1,11 +1,4 @@
import { import dayjs from 'dayjs';
format,
startOfDay,
startOfMonth,
startOfWeek,
subDays,
subHours,
} from "date-fns";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { AreaChart } from "@/components/ui/area-chart"; import { AreaChart } from "@/components/ui/area-chart";
@@ -59,44 +52,44 @@ export function TokenUsageChart({
} }
// Filter by time range // Filter by time range
const now = new Date(); const now = dayjs();
let startTime: Date; let startTime: dayjs.Dayjs;
switch (timeRange) { switch (timeRange) {
case "5h": case "5h":
startTime = subHours(now, 5); startTime = now.subtract(5, 'hour');
break; break;
case "today": case "today":
startTime = startOfDay(now); startTime = now.startOf('day');
break; break;
case "7d": case "7d":
startTime = subDays(now, 6); startTime = now.subtract(6, 'day');
break; break;
case "week": case "week":
startTime = startOfWeek(now); startTime = now.day(0); // Sunday (start of week)
break; break;
case "month": case "month":
startTime = startOfMonth(now); startTime = now.startOf('month');
break; break;
case "all": case "all":
// For "all time", find the earliest timestamp in the data // For "all time", find the earliest timestamp in the data
if (filtered.length > 0) { if (filtered.length > 0) {
const earliestTime = new Date( const earliestTime = dayjs(
Math.min( Math.min(
...filtered.map((record) => new Date(record.timestamp).getTime()), ...filtered.map((record) => dayjs(record.timestamp).valueOf()),
), ),
); );
startTime = earliestTime; startTime = earliestTime;
} else { } else {
startTime = new Date(0); startTime = dayjs(0);
} }
break; break;
default: default:
startTime = subHours(now, 5); startTime = now.subtract(5, 'hour');
} }
filtered = filtered.filter( filtered = filtered.filter(
(record) => new Date(record.timestamp) >= startTime, (record) => dayjs(record.timestamp).isAfter(startTime.subtract(1, 'millisecond')),
); );
return filtered; return filtered;
@@ -131,43 +124,41 @@ export function TokenUsageChart({
const intervals: { const intervals: {
[key: string]: { input: number; output: number; cache: number }; [key: string]: { input: number; output: number; cache: number };
} = {}; } = {};
const now = new Date(); const now = dayjs();
if (timeRange === "all") { if (timeRange === "all") {
// For all time, group by week // For all time, group by week
const earliestTime = const earliestTime =
records.length > 0 records.length > 0
? new Date( ? dayjs(
Math.min( Math.min(
...records.map((record) => ...records.map((record) =>
new Date(record.timestamp).getTime(), dayjs(record.timestamp).valueOf(),
), ),
), ),
) )
: new Date(); : dayjs();
// Get start of the week for earliest time // Get start of the week for earliest time
let currentWeekStart = startOfWeek(earliestTime); let currentWeekStart = earliestTime.day(0); // Sunday
const nowWeekStart = startOfWeek(now); const nowWeekStart = now.day(0); // Sunday
// Generate weekly intervals from earliest week to current week // Generate weekly intervals from earliest week to current week
while (currentWeekStart <= nowWeekStart) { while (currentWeekStart.isBefore(nowWeekStart) || currentWeekStart.isSame(nowWeekStart)) {
intervals[currentWeekStart.getTime()] = { intervals[currentWeekStart.valueOf()] = {
input: 0, input: 0,
output: 0, output: 0,
cache: 0, cache: 0,
}; };
// Move to next week (7 days) // Move to next week (7 days)
currentWeekStart = new Date( currentWeekStart = currentWeekStart.add(1, 'week');
currentWeekStart.getTime() + 7 * 24 * 60 * 60 * 1000,
);
} }
// Group records into weekly intervals // Group records into weekly intervals
records.forEach((record) => { records.forEach((record) => {
const recordTime = new Date(record.timestamp); const recordTime = dayjs(record.timestamp);
const weekStart = startOfWeek(recordTime); const weekStart = recordTime.day(0); // Sunday
const weekKey = weekStart.getTime(); const weekKey = weekStart.valueOf();
if (intervals[weekKey]) { if (intervals[weekKey]) {
intervals[weekKey].input += record.usage?.input_tokens || 0; intervals[weekKey].input += record.usage?.input_tokens || 0;
@@ -182,7 +173,7 @@ export function TokenUsageChart({
// Round current time down to nearest 30-minute boundary (epoch-based) // Round current time down to nearest 30-minute boundary (epoch-based)
const currentIntervalKey = const currentIntervalKey =
Math.floor(now.getTime() / intervalMs) * intervalMs; Math.floor(now.valueOf() / intervalMs) * intervalMs;
// Generate intervals (10 intervals for 5 hours) // Generate intervals (10 intervals for 5 hours)
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
@@ -192,9 +183,9 @@ export function TokenUsageChart({
// Group records into 30-minute intervals // Group records into 30-minute intervals
records.forEach((record) => { records.forEach((record) => {
const recordTime = new Date(record.timestamp); const recordTime = dayjs(record.timestamp);
const recordIntervalKey = const recordIntervalKey =
Math.floor(recordTime.getTime() / intervalMs) * intervalMs; Math.floor(recordTime.valueOf() / intervalMs) * intervalMs;
if (intervals[recordIntervalKey]) { if (intervals[recordIntervalKey]) {
intervals[recordIntervalKey].input += record.usage?.input_tokens || 0; intervals[recordIntervalKey].input += record.usage?.input_tokens || 0;
@@ -206,19 +197,18 @@ export function TokenUsageChart({
}); });
} else if (timeRange === "today") { } else if (timeRange === "today") {
// Group by hour for today // Group by hour for today
const startOfToday = startOfDay(now); const startOfToday = now.startOf('day');
const currentHour = now.getHours(); const currentHour = now.hour();
for (let i = 0; i <= currentHour; i++) { for (let i = 0; i <= currentHour; i++) {
const hourTime = new Date(startOfToday.getTime() + i * 60 * 60 * 1000); const hourTime = startOfToday.add(i, 'hour');
intervals[hourTime.getTime()] = { input: 0, output: 0, cache: 0 }; intervals[hourTime.valueOf()] = { input: 0, output: 0, cache: 0 };
} }
records.forEach((record) => { records.forEach((record) => {
const recordTime = new Date(record.timestamp); const recordTime = dayjs(record.timestamp);
const hourStart = new Date(recordTime); const hourStart = recordTime.startOf('hour');
hourStart.setMinutes(0, 0, 0); const hourKey = hourStart.valueOf();
const hourKey = hourStart.getTime();
if (intervals[hourKey]) { if (intervals[hourKey]) {
intervals[hourKey].input += record.usage?.input_tokens || 0; intervals[hourKey].input += record.usage?.input_tokens || 0;
@@ -229,45 +219,34 @@ export function TokenUsageChart({
}); });
} else { } else {
// Group by day for longer periods (7d, week, month) // Group by day for longer periods (7d, week, month)
let startDate: Date; let startDate: dayjs.Dayjs;
let days: number; let days: number;
if (timeRange === "week") { if (timeRange === "week") {
startDate = startOfWeek(now); startDate = now.day(0); // Sunday
// Calculate actual days in the current week so far (from start of week to today) // Calculate actual days in the current week so far (from start of week to today)
const todayStart = startOfDay(now); const todayStart = now.startOf('day');
days = days = todayStart.diff(startDate, 'day') + 1;
Math.floor(
(todayStart.getTime() - startDate.getTime()) /
(24 * 60 * 60 * 1000),
) + 1;
} else if (timeRange === "month") { } else if (timeRange === "month") {
startDate = startOfMonth(now); startDate = now.startOf('month');
// Calculate actual days in the current month so far (from start of month to today) // Calculate actual days in the current month so far (from start of month to today)
const todayStart = startOfDay(now); const todayStart = now.startOf('day');
days = days = todayStart.diff(startDate, 'day') + 1;
Math.floor(
(todayStart.getTime() - startDate.getTime()) /
(24 * 60 * 60 * 1000),
) + 1;
} else { } else {
// For 7d, start from (days-1) days ago to include today // For 7d, start from (days-1) days ago to include today
days = 7; days = 7;
startDate = startOfDay(subDays(now, days - 1)); startDate = now.subtract(days - 1, 'day').startOf('day');
} }
for (let i = 0; i < days; i++) { for (let i = 0; i < days; i++) {
const dayTime = startOfDay( const dayTime = startDate.add(i, 'day');
new Date(startDate.getTime() + i * 24 * 60 * 60 * 1000), intervals[dayTime.valueOf()] = { input: 0, output: 0, cache: 0 };
);
intervals[dayTime.getTime()] = { input: 0, output: 0, cache: 0 };
} }
records.forEach((record) => { records.forEach((record) => {
const recordTime = new Date(record.timestamp); const recordTime = dayjs(record.timestamp);
const dayStart = new Date(recordTime); const dayStart = recordTime.startOf('day');
dayStart.setHours(0, 0, 0, 0); const dayKey = dayStart.valueOf();
const dayKey = dayStart.getTime();
if (intervals[dayKey]) { if (intervals[dayKey]) {
intervals[dayKey].input += record.usage?.input_tokens || 0; intervals[dayKey].input += record.usage?.input_tokens || 0;
@@ -287,16 +266,16 @@ export function TokenUsageChart({
.map(Number) .map(Number)
.sort((a, b) => a - b) .sort((a, b) => a - b)
.map((timestamp) => { .map((timestamp) => {
const date = new Date(timestamp); const date = dayjs(timestamp);
let label: string; let label: string;
if (timeRange === "all") { if (timeRange === "all") {
label = format(date, "MMM dd, yyyy"); label = date.format("MMM DD, YYYY");
} else if (timeRange === "today") { } else if (timeRange === "today") {
label = format(date, "HH:mm"); label = date.format("HH:mm");
} else if (timeRange === "5h") { } else if (timeRange === "5h") {
label = format(date, "HH:mm"); label = date.format("HH:mm");
} else { } else {
label = format(date, "MMM dd"); label = date.format("MMM DD");
} }
return { return {
+43
View File
@@ -0,0 +1,43 @@
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3 min-w-10",
sm: "h-9 px-2.5 min-w-9",
lg: "h-11 px-5 min-w-11",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
))
Toggle.displayName = TogglePrimitive.Root.displayName
export { Toggle, toggleVariants }
+17
View File
@@ -227,15 +227,32 @@
"updateButton.newVersionAvailable": "New version available", "updateButton.newVersionAvailable": "New version available",
"usage.cacheReadTokens": "Cache Read Tokens", "usage.cacheReadTokens": "Cache Read Tokens",
"usage.cost": "Cost", "usage.cost": "Cost",
"usage.days": "days",
"usage.description": "Monitor your token usage", "usage.description": "Monitor your token usage",
"usage.error": "Error loading usage data: {{error}}", "usage.error": "Error loading usage data: {{error}}",
"usage.inputTokens": "Input Tokens", "usage.inputTokens": "Input Tokens",
"usage.loading": "Loading usage data...", "usage.loading": "Loading usage data...",
"usage.noActivity": "No activity",
"usage.noData": "No usage data found.", "usage.noData": "No usage data found.",
"usage.outputTokens": "Output Tokens", "usage.outputTokens": "Output Tokens",
"usage.refresh": "Refresh", "usage.refresh": "Refresh",
"usage.refreshing": "Refreshing", "usage.refreshing": "Refreshing",
"usage.requests": "requests",
"usage.title": "Usage", "usage.title": "Usage",
"usage.tokens": "tokens",
"usage.totalTokens": "total tokens",
"usage.view.chart": "Chart view",
"usage.view.grid": "Grid view",
"usage.activityGrid.title": "Activity Grid",
"usage.activityGrid.description": "Daily token usage over the past year",
"usage.activityGrid.description3Months": "Daily token usage over the past 3 months",
"usage.activityGrid.description6Months": "Daily token usage over the past 6 months",
"usage.activityGrid.descriptionThisYear": "Daily token usage this year",
"usage.activityGrid.3Months": "3 months",
"usage.activityGrid.6Months": "6 months",
"usage.activityGrid.thisYear": "This year",
"usage.activityGrid.less": "Less",
"usage.activityGrid.more": "More",
"usageChart.allModels": "All models", "usageChart.allModels": "All models",
"usageChart.allTime": "All time", "usageChart.allTime": "All time",
"usageChart.last5Hours": "Last 5 hours", "usageChart.last5Hours": "Last 5 hours",
+18 -1
View File
@@ -246,5 +246,22 @@
"kimi.apiKeyPlaceholder": "Entrez votre clé API Kimi", "kimi.apiKeyPlaceholder": "Entrez votre clé API Kimi",
"kimi.creating": "Création...", "kimi.creating": "Création...",
"kimi.createConfig": "Créer la configuration", "kimi.createConfig": "Créer la configuration",
"kimi.useKimi": "Utiliser Kimi" "kimi.useKimi": "Utiliser Kimi",
"usage.days": "jours",
"usage.noActivity": "Aucune activité",
"usage.requests": "requêtes",
"usage.tokens": "jetons",
"usage.totalTokens": "jetons au total",
"usage.view.chart": "Vue graphique",
"usage.view.grid": "Vue grille",
"usage.activityGrid.title": "Grille d'activité",
"usage.activityGrid.description": "Utilisation quotidienne des jetons au cours de l'année écoulée",
"usage.activityGrid.description3Months": "Utilisation quotidienne des jetons au cours des 3 derniers mois",
"usage.activityGrid.description6Months": "Utilisation quotidienne des jetons au cours des 6 derniers mois",
"usage.activityGrid.descriptionThisYear": "Utilisation quotidienne des jetons cette année",
"usage.activityGrid.3Months": "3 mois",
"usage.activityGrid.6Months": "6 mois",
"usage.activityGrid.thisYear": "Cette année",
"usage.activityGrid.less": "Moins",
"usage.activityGrid.more": "Plus"
} }
+18 -1
View File
@@ -246,5 +246,22 @@
"kimi.apiKeyPlaceholder": "Kimi API キーを入力", "kimi.apiKeyPlaceholder": "Kimi API キーを入力",
"kimi.creating": "作成中...", "kimi.creating": "作成中...",
"kimi.createConfig": "設定を作成", "kimi.createConfig": "設定を作成",
"kimi.useKimi": "Kimi を使用" "kimi.useKimi": "Kimi を使用",
"usage.days": "日間",
"usage.noActivity": "アクティビティなし",
"usage.requests": "リクエスト",
"usage.tokens": "トークン",
"usage.totalTokens": "合計トークン",
"usage.view.chart": "チャート表示",
"usage.view.grid": "グリッド表示",
"usage.activityGrid.title": "アクティビティグリッド",
"usage.activityGrid.description": "過去1年間の毎日のトークン使用量",
"usage.activityGrid.description3Months": "過去3ヶ月間の毎日のトークン使用量",
"usage.activityGrid.description6Months": "過去6ヶ月間の毎日のトークン使用量",
"usage.activityGrid.descriptionThisYear": "今年の毎日のトークン使用量",
"usage.activityGrid.3Months": "3ヶ月",
"usage.activityGrid.6Months": "6ヶ月",
"usage.activityGrid.thisYear": "今年",
"usage.activityGrid.less": "少ない",
"usage.activityGrid.more": "多い"
} }
+18 -1
View File
@@ -246,5 +246,22 @@
"usageChart.startOfMonth": "本月开始", "usageChart.startOfMonth": "本月开始",
"usageChart.startOfToday": "今天开始", "usageChart.startOfToday": "今天开始",
"usageChart.startOfWeek": "本周开始", "usageChart.startOfWeek": "本周开始",
"usageChart.timeRange": "时间范围:" "usageChart.timeRange": "时间范围:",
"usage.days": "天",
"usage.noActivity": "无活动",
"usage.requests": "请求",
"usage.tokens": "令牌",
"usage.totalTokens": "总令牌",
"usage.view.chart": "图表视图",
"usage.view.grid": "网格视图",
"usage.activityGrid.title": "活动网格",
"usage.activityGrid.description": "过去一年的每日令牌使用情况",
"usage.activityGrid.description3Months": "过去3个月的每日令牌使用情况",
"usage.activityGrid.description6Months": "过去6个月的每日令牌使用情况",
"usage.activityGrid.descriptionThisYear": "今年的每日令牌使用情况",
"usage.activityGrid.3Months": "3个月",
"usage.activityGrid.6Months": "6个月",
"usage.activityGrid.thisYear": "今年",
"usage.activityGrid.less": "较少",
"usage.activityGrid.more": "较多"
} }
+55 -33
View File
@@ -6,8 +6,10 @@ import {
} from "lucide-react"; } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ActivityGrid } from "@/components/ActivityGrid";
import { TokenUsageChart } from "@/components/TokenUsageChart"; import { TokenUsageChart } from "@/components/TokenUsageChart";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { TooltipProvider } from "@/components/ui/tooltip";
import { type ProjectUsageRecord, useProjectUsageFiles } from "@/lib/query"; import { type ProjectUsageRecord, useProjectUsageFiles } from "@/lib/query";
import { cn, formatLargeNumber } from "@/lib/utils"; import { cn, formatLargeNumber } from "@/lib/utils";
@@ -32,45 +34,63 @@ export function UsagePage() {
}, [usageData]); }, [usageData]);
return ( return (
<div className=""> <TooltipProvider>
<div <div
className="flex items-center p-3 border-b px-3 justify-between sticky top-0 bg-background z-10 mb-4" className="flex flex-col p-3 border-b px-3 sticky top-0 bg-background z-10 mb-4"
data-tauri-drag-region data-tauri-drag-region
> >
<div data-tauri-drag-region> <div
<h3 className="font-bold" data-tauri-drag-region> className="flex items-center justify-between"
{t("usage.title")} data-tauri-drag-region
</h3> >
<p className="text-sm text-muted-foreground" data-tauri-drag-region> <div data-tauri-drag-region>
{t("usage.description")} <h3 className="font-bold" data-tauri-drag-region>
</p> {t("usage.title")}
</div> </h3>
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
{t("usage.description")}
</p>
</div>
<div> <div className="flex items-center gap-2">
<Button <Button
disabled={isRefetching || isLoading} disabled={isRefetching || isLoading}
onClick={(_) => { onClick={(_) => {
refetch(); refetch();
}} }}
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-muted-foreground" className="text-muted-foreground h-8 px-3"
> >
<RefreshCwIcon <RefreshCwIcon
className={cn({ className={cn({
"animate-spin": isRefetching || isLoading, "animate-spin": isRefetching || isLoading,
})} })}
/> />
{isRefetching || isLoading {isRefetching || isLoading
? t("usage.refreshing") ? t("usage.refreshing")
: t("usage.refresh")} : t("usage.refresh")}
</Button> </Button>
</div>
</div> </div>
</div> </div>
<div className="px-4 space-y-4"> <div className="px-4 space-y-4">
{isLoading ? ( {isLoading ? (
<div className="space-y-6"> <div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="p-1 border rounded-lg pb-5">
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div
key={i}
className="bg-card p-4 rounded-lg space-y-2 border"
>
<div className="h-4 rounded animate-pulse"></div>
<div className="h-8 rounded animate-pulse"></div>
</div>
))}
</div>
</div>
<div className="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4">
{[1, 2, 3].map((i) => ( {[1, 2, 3].map((i) => (
<div <div
key={i} key={i}
@@ -89,6 +109,9 @@ export function UsagePage() {
<p>{t("usage.error", { error: error.message })}</p> <p>{t("usage.error", { error: error.message })}</p>
) : usageData && usageData.length > 0 ? ( ) : usageData && usageData.length > 0 ? (
<> <>
<div className=" rounded-lg pb-5">
<ActivityGrid data={usageData} />
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-blue-50 border-blue-100 text-blue-700 border-2 p-4 rounded-lg space-y-2 dark:bg-blue-950/20 dark:border-blue-900/30 dark:text-blue-300"> <div className="bg-blue-50 border-blue-100 text-blue-700 border-2 p-4 rounded-lg space-y-2 dark:bg-blue-950/20 dark:border-blue-900/30 dark:text-blue-300">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -146,8 +169,7 @@ export function UsagePage() {
</p> </p>
</div> */} </div> */}
</div> </div>
<div className="mt-6 bg-card p-6 rounded-lg w-full min-w-0 border">
<div className=" bg-card p-6 rounded-lg w-full min-w-0 border">
<TokenUsageChart <TokenUsageChart
data={usageData} data={usageData}
onFilteredDataChange={setFilteredUsageData} onFilteredDataChange={setFilteredUsageData}
@@ -158,6 +180,6 @@ export function UsagePage() {
<p>{t("usage.noData")}</p> <p>{t("usage.noData")}</p>
)} )}
</div> </div>
</div> </TooltipProvider>
); );
} }