From d6a506afa36d21bfa5548976e390b2b3863a45d4 Mon Sep 17 00:00:00 2001 From: Randy Lu Date: Sun, 16 Nov 2025 17:05:49 +0800 Subject: [PATCH] activity grid --- .claude/settings.local.json | 9 +- package.json | 4 +- pnpm-lock.yaml | 125 ++++++++---- src/components/ActivityGrid.tsx | 307 +++++++++++++++++++++++++++++ src/components/TokenUsageChart.tsx | 127 +++++------- src/components/ui/toggle.tsx | 43 ++++ src/i18n/locales/en.json | 17 ++ src/i18n/locales/fr.json | 19 +- src/i18n/locales/ja.json | 19 +- src/i18n/locales/zh.json | 19 +- src/pages/UsagePage.tsx | 88 +++++---- 11 files changed, 629 insertions(+), 148 deletions(-) create mode 100644 src/components/ActivityGrid.tsx create mode 100644 src/components/ui/toggle.tsx diff --git a/.claude/settings.local.json b/.claude/settings.local.json index bbbb247..9a27ff9 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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": [] diff --git a/package.json b/package.json index 4e745c8..9a0bd12 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 664507e..f79ed28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/components/ActivityGrid.tsx b/src/components/ActivityGrid.tsx new file mode 100644 index 0000000..0fa702e --- /dev/null +++ b/src/components/ActivityGrid.tsx @@ -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(); + + // 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 ( +
+ +
+
+
+ {/* Grid */} +
+ {/* Month labels */} +
+ {grid.map((_, weekIndex) => { + const monthLabel = monthLabels.find(m => m.weekIndex === weekIndex); + const displayText = monthLabel?.month || ''; + + return ( +
+ {displayText && ( +
+ {displayText} +
+ )} +
+ ); + })} +
+ + {/* Activity grid */} +
+ {grid.map((week, weekIndex) => ( +
+ {week.map((dayData, dayIndex) => { + const intensity = getIntensityLevel(dayData.totalTokens); + + return ( + + + +
+ + +
+
+ {dayjs(dayData.date).format('MMM D, YYYY')} +
+ {dayData.totalTokens > 0 ? ( + <> +
+ {formatLargeNumber(dayData.totalTokens)} {t("usage.tokens", "tokens")} +
+
+ {dayData.count} {t("usage.requests", "requests")} +
+ + ) : ( +
+ {t("usage.noActivity", "No activity")} +
+ )} +
+
+ + + ); + })} +
+ ))} +
+
+
+
+
+ + {/* Legend - Fixed position, not scrollable */} +
+ {t("usage.activityGrid.less", "Less")} +
+ {[0, 1, 2, 3, 4].map((level) => ( +
+ ))} +
+ {t("usage.activityGrid.more", "More")} +
+
+ ); +} + +// 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(); +} \ No newline at end of file diff --git a/src/components/TokenUsageChart.tsx b/src/components/TokenUsageChart.tsx index c2506f5..714b96f 100644 --- a/src/components/TokenUsageChart.tsx +++ b/src/components/TokenUsageChart.tsx @@ -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 { diff --git a/src/components/ui/toggle.tsx b/src/components/ui/toggle.tsx new file mode 100644 index 0000000..0da5cb8 --- /dev/null +++ b/src/components/ui/toggle.tsx @@ -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, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, size, ...props }, ref) => ( + +)) + +Toggle.displayName = TogglePrimitive.Root.displayName + +export { Toggle, toggleVariants } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 5c1c31e..b496294 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -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", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 7f16377..9772e04 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -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" } diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 8db09ae..6e84faa 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -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": "多い" } diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 9b672fd..2558993 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -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": "较多" } diff --git a/src/pages/UsagePage.tsx b/src/pages/UsagePage.tsx index e8df38e..6268de8 100644 --- a/src/pages/UsagePage.tsx +++ b/src/pages/UsagePage.tsx @@ -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 ( -
+
-
-

- {t("usage.title")} -

-

- {t("usage.description")} -

-
+
+
+

+ {t("usage.title")} +

+

+ {t("usage.description")} +

+
-
- +
+ +
{isLoading ? (
-
+
+
+ {[1, 2, 3].map((i) => ( +
+
+
+
+ ))} +
+
+
{[1, 2, 3].map((i) => (
{t("usage.error", { error: error.message })}

) : usageData && usageData.length > 0 ? ( <> +
+ +
@@ -146,8 +169,7 @@ export function UsagePage() {

*/}
- -
+
{t("usage.noData")}

)}
-
+ ); }