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:*)",
"Skill(web-fetch)",
"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": [],
"ask": []
+3 -1
View File
@@ -30,6 +30,8 @@
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@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",
"@remixicon/react": "^4.6.0",
"@tanstack/react-query": "^5.90.2",
@@ -45,7 +47,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dayjs": "^1.11.19",
"i18next": "^25.5.3",
"i18next-browser-languagedetector": "^8.2.0",
"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))
'@lobehub/icons':
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':
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)
@@ -68,6 +68,12 @@ importers:
'@radix-ui/react-tabs':
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)
'@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':
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)
@@ -113,9 +119,9 @@ importers:
cmdk:
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)
date-fns:
specifier: ^4.1.0
version: 4.1.0
dayjs:
specifier: ^1.11.19
version: 1.11.19
i18next:
specifier: ^25.5.3
version: 25.5.3(typescript@5.8.3)
@@ -1569,6 +1575,32 @@ packages:
'@types/react-dom':
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':
resolution: {integrity: sha512-cDKVcfzyO6PpckZekODJZDe5ZxZ2fCZlzKzTmPhe4mX9qTHRfLcKgqb0OKf22xLwDequ2tVleim+ZYx3rabD5w==}
peerDependencies:
@@ -2890,11 +2922,8 @@ packages:
date-fns@3.6.0:
resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
dayjs@1.11.18:
resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==}
dayjs@1.11.19:
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
@@ -6527,12 +6556,12 @@ snapshots:
'@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:
'@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)
antd: 5.27.4(date-fns@4.1.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)
'@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@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@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
lodash-es: 4.17.21
lucide-react: 0.469.0(react@19.2.0)
@@ -6549,11 +6578,11 @@ snapshots:
- micromark-util-types
- 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:
'@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)
antd: 5.27.4(date-fns@4.1.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)
'@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@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@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)
polished: 4.3.1
react: 19.2.0
@@ -6568,7 +6597,7 @@ snapshots:
- micromark-util-types
- 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:
'@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)
@@ -6579,19 +6608,19 @@ snapshots:
'@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)
'@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/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/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@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/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)
'@shikijs/transformers': 3.13.0
'@splinetool/runtime': 0.9.526
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-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: 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@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
class-variance-authority: 0.7.1
dayjs: 1.11.18
dayjs: 1.11.19
emoji-mart: 5.6.0
fast-deep-equal: 3.1.3
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-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)':
dependencies:
'@babel/runtime': 7.28.4
@@ -8061,7 +8116,7 @@ snapshots:
dependencies:
'@babel/runtime': 7.28.4
'@types/js-cookie': 3.0.6
dayjs: 1.11.18
dayjs: 1.11.19
intersection-observer: 0.12.2
js-cookie: 3.0.5
lodash: 4.17.21
@@ -8089,7 +8144,7 @@ snapshots:
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:
'@ant-design/cssinjs': 1.24.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@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/serialize': 1.3.3
'@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
use-merge-value: 1.2.0(react@19.2.0)
transitivePeerDependencies:
@@ -8106,7 +8161,7 @@ snapshots:
- react-dom
- 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:
'@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)
@@ -8122,7 +8177,7 @@ snapshots:
'@rc-component/trigger': 2.3.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
classnames: 2.5.1
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-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)
@@ -8138,7 +8193,7 @@ snapshots:
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-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-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)
@@ -8616,9 +8671,7 @@ snapshots:
date-fns@3.6.0: {}
date-fns@4.1.0: {}
dayjs@1.11.18: {}
dayjs@1.11.19: {}
debug@4.4.3:
dependencies:
@@ -9705,7 +9758,7 @@ snapshots:
d3: 7.9.0
d3-sankey: 0.12.3
dagre-d3-es: 7.0.11
dayjs: 1.11.18
dayjs: 1.11.19
dompurify: 3.2.7
katex: 0.16.23
khroma: 2.1.0
@@ -10464,7 +10517,7 @@ snapshots:
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:
'@babel/runtime': 7.28.4
'@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-dom: 19.2.0(react@19.2.0)
optionalDependencies:
date-fns: 4.1.0
dayjs: 1.11.18
date-fns: 3.6.0
dayjs: 1.11.19
rc-progress@4.0.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
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 {
format,
startOfDay,
startOfMonth,
startOfWeek,
subDays,
subHours,
} from "date-fns";
import dayjs from 'dayjs';
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { AreaChart } from "@/components/ui/area-chart";
@@ -59,44 +52,44 @@ export function TokenUsageChart({
}
// Filter by time range
const now = new Date();
let startTime: Date;
const now = dayjs();
let startTime: dayjs.Dayjs;
switch (timeRange) {
case "5h":
startTime = subHours(now, 5);
startTime = now.subtract(5, 'hour');
break;
case "today":
startTime = startOfDay(now);
startTime = now.startOf('day');
break;
case "7d":
startTime = subDays(now, 6);
startTime = now.subtract(6, 'day');
break;
case "week":
startTime = startOfWeek(now);
startTime = now.day(0); // Sunday (start of week)
break;
case "month":
startTime = startOfMonth(now);
startTime = now.startOf('month');
break;
case "all":
// For "all time", find the earliest timestamp in the data
if (filtered.length > 0) {
const earliestTime = new Date(
const earliestTime = dayjs(
Math.min(
...filtered.map((record) => new Date(record.timestamp).getTime()),
...filtered.map((record) => dayjs(record.timestamp).valueOf()),
),
);
startTime = earliestTime;
} else {
startTime = new Date(0);
startTime = dayjs(0);
}
break;
default:
startTime = subHours(now, 5);
startTime = now.subtract(5, 'hour');
}
filtered = filtered.filter(
(record) => new Date(record.timestamp) >= startTime,
(record) => dayjs(record.timestamp).isAfter(startTime.subtract(1, 'millisecond')),
);
return filtered;
@@ -131,43 +124,41 @@ export function TokenUsageChart({
const intervals: {
[key: string]: { input: number; output: number; cache: number };
} = {};
const now = new Date();
const now = dayjs();
if (timeRange === "all") {
// For all time, group by week
const earliestTime =
records.length > 0
? new Date(
? dayjs(
Math.min(
...records.map((record) =>
new Date(record.timestamp).getTime(),
dayjs(record.timestamp).valueOf(),
),
),
)
: new Date();
: dayjs();
// Get start of the week for earliest time
let currentWeekStart = startOfWeek(earliestTime);
const nowWeekStart = startOfWeek(now);
let currentWeekStart = earliestTime.day(0); // Sunday
const nowWeekStart = now.day(0); // Sunday
// Generate weekly intervals from earliest week to current week
while (currentWeekStart <= nowWeekStart) {
intervals[currentWeekStart.getTime()] = {
while (currentWeekStart.isBefore(nowWeekStart) || currentWeekStart.isSame(nowWeekStart)) {
intervals[currentWeekStart.valueOf()] = {
input: 0,
output: 0,
cache: 0,
};
// Move to next week (7 days)
currentWeekStart = new Date(
currentWeekStart.getTime() + 7 * 24 * 60 * 60 * 1000,
);
currentWeekStart = currentWeekStart.add(1, 'week');
}
// Group records into weekly intervals
records.forEach((record) => {
const recordTime = new Date(record.timestamp);
const weekStart = startOfWeek(recordTime);
const weekKey = weekStart.getTime();
const recordTime = dayjs(record.timestamp);
const weekStart = recordTime.day(0); // Sunday
const weekKey = weekStart.valueOf();
if (intervals[weekKey]) {
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)
const currentIntervalKey =
Math.floor(now.getTime() / intervalMs) * intervalMs;
Math.floor(now.valueOf() / intervalMs) * intervalMs;
// Generate intervals (10 intervals for 5 hours)
for (let i = 0; i < 10; i++) {
@@ -192,9 +183,9 @@ export function TokenUsageChart({
// Group records into 30-minute intervals
records.forEach((record) => {
const recordTime = new Date(record.timestamp);
const recordTime = dayjs(record.timestamp);
const recordIntervalKey =
Math.floor(recordTime.getTime() / intervalMs) * intervalMs;
Math.floor(recordTime.valueOf() / intervalMs) * intervalMs;
if (intervals[recordIntervalKey]) {
intervals[recordIntervalKey].input += record.usage?.input_tokens || 0;
@@ -206,19 +197,18 @@ export function TokenUsageChart({
});
} else if (timeRange === "today") {
// Group by hour for today
const startOfToday = startOfDay(now);
const currentHour = now.getHours();
const startOfToday = now.startOf('day');
const currentHour = now.hour();
for (let i = 0; i <= currentHour; i++) {
const hourTime = new Date(startOfToday.getTime() + i * 60 * 60 * 1000);
intervals[hourTime.getTime()] = { input: 0, output: 0, cache: 0 };
const hourTime = startOfToday.add(i, 'hour');
intervals[hourTime.valueOf()] = { input: 0, output: 0, cache: 0 };
}
records.forEach((record) => {
const recordTime = new Date(record.timestamp);
const hourStart = new Date(recordTime);
hourStart.setMinutes(0, 0, 0);
const hourKey = hourStart.getTime();
const recordTime = dayjs(record.timestamp);
const hourStart = recordTime.startOf('hour');
const hourKey = hourStart.valueOf();
if (intervals[hourKey]) {
intervals[hourKey].input += record.usage?.input_tokens || 0;
@@ -229,45 +219,34 @@ export function TokenUsageChart({
});
} else {
// Group by day for longer periods (7d, week, month)
let startDate: Date;
let startDate: dayjs.Dayjs;
let days: number;
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)
const todayStart = startOfDay(now);
days =
Math.floor(
(todayStart.getTime() - startDate.getTime()) /
(24 * 60 * 60 * 1000),
) + 1;
const todayStart = now.startOf('day');
days = todayStart.diff(startDate, 'day') + 1;
} 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)
const todayStart = startOfDay(now);
days =
Math.floor(
(todayStart.getTime() - startDate.getTime()) /
(24 * 60 * 60 * 1000),
) + 1;
const todayStart = now.startOf('day');
days = todayStart.diff(startDate, 'day') + 1;
} else {
// For 7d, start from (days-1) days ago to include today
days = 7;
startDate = startOfDay(subDays(now, days - 1));
startDate = now.subtract(days - 1, 'day').startOf('day');
}
for (let i = 0; i < days; i++) {
const dayTime = startOfDay(
new Date(startDate.getTime() + i * 24 * 60 * 60 * 1000),
);
intervals[dayTime.getTime()] = { input: 0, output: 0, cache: 0 };
const dayTime = startDate.add(i, 'day');
intervals[dayTime.valueOf()] = { input: 0, output: 0, cache: 0 };
}
records.forEach((record) => {
const recordTime = new Date(record.timestamp);
const dayStart = new Date(recordTime);
dayStart.setHours(0, 0, 0, 0);
const dayKey = dayStart.getTime();
const recordTime = dayjs(record.timestamp);
const dayStart = recordTime.startOf('day');
const dayKey = dayStart.valueOf();
if (intervals[dayKey]) {
intervals[dayKey].input += record.usage?.input_tokens || 0;
@@ -287,16 +266,16 @@ export function TokenUsageChart({
.map(Number)
.sort((a, b) => a - b)
.map((timestamp) => {
const date = new Date(timestamp);
const date = dayjs(timestamp);
let label: string;
if (timeRange === "all") {
label = format(date, "MMM dd, yyyy");
label = date.format("MMM DD, YYYY");
} else if (timeRange === "today") {
label = format(date, "HH:mm");
label = date.format("HH:mm");
} else if (timeRange === "5h") {
label = format(date, "HH:mm");
label = date.format("HH:mm");
} else {
label = format(date, "MMM dd");
label = date.format("MMM DD");
}
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",
"usage.cacheReadTokens": "Cache Read Tokens",
"usage.cost": "Cost",
"usage.days": "days",
"usage.description": "Monitor your token usage",
"usage.error": "Error loading usage data: {{error}}",
"usage.inputTokens": "Input Tokens",
"usage.loading": "Loading usage data...",
"usage.noActivity": "No activity",
"usage.noData": "No usage data found.",
"usage.outputTokens": "Output Tokens",
"usage.refresh": "Refresh",
"usage.refreshing": "Refreshing",
"usage.requests": "requests",
"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.allTime": "All time",
"usageChart.last5Hours": "Last 5 hours",
+18 -1
View File
@@ -246,5 +246,22 @@
"kimi.apiKeyPlaceholder": "Entrez votre clé API Kimi",
"kimi.creating": "Création...",
"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.creating": "作成中...",
"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.startOfToday": "今天开始",
"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";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { ActivityGrid } from "@/components/ActivityGrid";
import { TokenUsageChart } from "@/components/TokenUsageChart";
import { Button } from "@/components/ui/button";
import { TooltipProvider } from "@/components/ui/tooltip";
import { type ProjectUsageRecord, useProjectUsageFiles } from "@/lib/query";
import { cn, formatLargeNumber } from "@/lib/utils";
@@ -32,45 +34,63 @@ export function UsagePage() {
}, [usageData]);
return (
<div className="">
<TooltipProvider>
<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
>
<div data-tauri-drag-region>
<h3 className="font-bold" data-tauri-drag-region>
{t("usage.title")}
</h3>
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
{t("usage.description")}
</p>
</div>
<div
className="flex items-center justify-between"
data-tauri-drag-region
>
<div data-tauri-drag-region>
<h3 className="font-bold" data-tauri-drag-region>
{t("usage.title")}
</h3>
<p className="text-sm text-muted-foreground" data-tauri-drag-region>
{t("usage.description")}
</p>
</div>
<div>
<Button
disabled={isRefetching || isLoading}
onClick={(_) => {
refetch();
}}
variant="ghost"
size="sm"
className="text-muted-foreground"
>
<RefreshCwIcon
className={cn({
"animate-spin": isRefetching || isLoading,
})}
/>
{isRefetching || isLoading
? t("usage.refreshing")
: t("usage.refresh")}
</Button>
<div className="flex items-center gap-2">
<Button
disabled={isRefetching || isLoading}
onClick={(_) => {
refetch();
}}
variant="ghost"
size="sm"
className="text-muted-foreground h-8 px-3"
>
<RefreshCwIcon
className={cn({
"animate-spin": isRefetching || isLoading,
})}
/>
{isRefetching || isLoading
? t("usage.refreshing")
: t("usage.refresh")}
</Button>
</div>
</div>
</div>
<div className="px-4 space-y-4">
{isLoading ? (
<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) => (
<div
key={i}
@@ -89,6 +109,9 @@ export function UsagePage() {
<p>{t("usage.error", { error: error.message })}</p>
) : 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="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">
@@ -146,8 +169,7 @@ export function UsagePage() {
</p>
</div> */}
</div>
<div className=" bg-card p-6 rounded-lg w-full min-w-0 border">
<div className="mt-6 bg-card p-6 rounded-lg w-full min-w-0 border">
<TokenUsageChart
data={usageData}
onFilteredDataChange={setFilteredUsageData}
@@ -158,6 +180,6 @@ export function UsagePage() {
<p>{t("usage.noData")}</p>
)}
</div>
</div>
</TooltipProvider>
);
}