mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-11 22:51:26 +00:00
Show usage charts (#63)
This commit is contained in:
parent
db2d8d3d50
commit
e4a69574ad
7 changed files with 787 additions and 44 deletions
|
|
@ -72,6 +72,7 @@
|
|||
"react-hook-form": "^7.56.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-use": "^17.6.0",
|
||||
"recharts": "^2.15.3",
|
||||
"sonner": "^2.0.3",
|
||||
"stripe": "^18.2.0",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
|
|
|
|||
237
pnpm-lock.yaml
generated
237
pnpm-lock.yaml
generated
|
|
@ -143,6 +143,9 @@ importers:
|
|||
react-use:
|
||||
specifier: ^17.6.0
|
||||
version: 17.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
recharts:
|
||||
specifier: ^2.15.3
|
||||
version: 2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
sonner:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
|
|
@ -3570,6 +3573,33 @@ packages:
|
|||
'@types/connect@3.4.38':
|
||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||
|
||||
'@types/d3-array@3.2.1':
|
||||
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
|
||||
|
||||
'@types/d3-color@3.1.3':
|
||||
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
|
||||
|
||||
'@types/d3-ease@3.0.2':
|
||||
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
|
||||
|
||||
'@types/d3-interpolate@3.0.4':
|
||||
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
|
||||
|
||||
'@types/d3-path@3.1.1':
|
||||
resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
|
||||
|
||||
'@types/d3-scale@4.0.9':
|
||||
resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
|
||||
|
||||
'@types/d3-shape@3.1.7':
|
||||
resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==}
|
||||
|
||||
'@types/d3-time@3.0.4':
|
||||
resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
|
||||
|
||||
'@types/d3-timer@3.0.2':
|
||||
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
|
|
@ -4612,6 +4642,50 @@ packages:
|
|||
resolution: {integrity: sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
d3-array@3.2.4:
|
||||
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-color@3.1.0:
|
||||
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-ease@3.0.1:
|
||||
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-format@3.1.0:
|
||||
resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-interpolate@3.0.1:
|
||||
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-path@3.1.0:
|
||||
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-scale@4.0.2:
|
||||
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-shape@3.2.0:
|
||||
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-time-format@4.1.0:
|
||||
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-time@3.1.0:
|
||||
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
d3-timer@3.0.1:
|
||||
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
data-uri-to-buffer@6.0.2:
|
||||
resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
|
@ -4658,6 +4732,9 @@ packages:
|
|||
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
decimal.js-light@2.5.1:
|
||||
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
|
||||
|
||||
decimal.js@10.5.0:
|
||||
resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==}
|
||||
|
||||
|
|
@ -4762,6 +4839,9 @@ packages:
|
|||
dom-converter@0.2.0:
|
||||
resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==}
|
||||
|
||||
dom-helpers@5.2.1:
|
||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||
|
||||
dom-serializer@0.2.2:
|
||||
resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==}
|
||||
|
||||
|
|
@ -5199,6 +5279,10 @@ packages:
|
|||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
fast-equals@5.2.2:
|
||||
resolution: {integrity: sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
fast-glob@3.3.1:
|
||||
resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
|
|
@ -5716,6 +5800,10 @@ packages:
|
|||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
internmap@2.0.3:
|
||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
intl-messageformat@10.7.16:
|
||||
resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==}
|
||||
|
||||
|
|
@ -7413,6 +7501,12 @@ packages:
|
|||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react-smooth@4.0.4:
|
||||
resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
react-style-singleton@2.2.3:
|
||||
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -7423,6 +7517,12 @@ packages:
|
|||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react-transition-group@4.4.5:
|
||||
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
||||
peerDependencies:
|
||||
react: '>=16.6.0'
|
||||
react-dom: '>=16.6.0'
|
||||
|
||||
react-universal-interface@0.6.2:
|
||||
resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==}
|
||||
peerDependencies:
|
||||
|
|
@ -7474,6 +7574,16 @@ packages:
|
|||
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
recharts-scale@0.4.5:
|
||||
resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
|
||||
|
||||
recharts@2.15.3:
|
||||
resolution: {integrity: sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
redent@3.0.0:
|
||||
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -8602,6 +8712,9 @@ packages:
|
|||
vfile@6.0.3:
|
||||
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
||||
|
||||
victory-vendor@36.9.2:
|
||||
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
|
||||
|
||||
vite-node@3.1.4:
|
||||
resolution: {integrity: sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
|
|
@ -12491,6 +12604,30 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/node': 22.15.27
|
||||
|
||||
'@types/d3-array@3.2.1': {}
|
||||
|
||||
'@types/d3-color@3.1.3': {}
|
||||
|
||||
'@types/d3-ease@3.0.2': {}
|
||||
|
||||
'@types/d3-interpolate@3.0.4':
|
||||
dependencies:
|
||||
'@types/d3-color': 3.1.3
|
||||
|
||||
'@types/d3-path@3.1.1': {}
|
||||
|
||||
'@types/d3-scale@4.0.9':
|
||||
dependencies:
|
||||
'@types/d3-time': 3.0.4
|
||||
|
||||
'@types/d3-shape@3.1.7':
|
||||
dependencies:
|
||||
'@types/d3-path': 3.1.1
|
||||
|
||||
'@types/d3-time@3.0.4': {}
|
||||
|
||||
'@types/d3-timer@3.0.2': {}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
|
|
@ -13736,6 +13873,44 @@ snapshots:
|
|||
find-pkg: 0.1.2
|
||||
fs-exists-sync: 0.1.0
|
||||
|
||||
d3-array@3.2.4:
|
||||
dependencies:
|
||||
internmap: 2.0.3
|
||||
|
||||
d3-color@3.1.0: {}
|
||||
|
||||
d3-ease@3.0.1: {}
|
||||
|
||||
d3-format@3.1.0: {}
|
||||
|
||||
d3-interpolate@3.0.1:
|
||||
dependencies:
|
||||
d3-color: 3.1.0
|
||||
|
||||
d3-path@3.1.0: {}
|
||||
|
||||
d3-scale@4.0.2:
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
d3-format: 3.1.0
|
||||
d3-interpolate: 3.0.1
|
||||
d3-time: 3.1.0
|
||||
d3-time-format: 4.1.0
|
||||
|
||||
d3-shape@3.2.0:
|
||||
dependencies:
|
||||
d3-path: 3.1.0
|
||||
|
||||
d3-time-format@4.1.0:
|
||||
dependencies:
|
||||
d3-time: 3.1.0
|
||||
|
||||
d3-time@3.1.0:
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
|
||||
d3-timer@3.0.1: {}
|
||||
|
||||
data-uri-to-buffer@6.0.2: {}
|
||||
|
||||
data-urls@5.0.0:
|
||||
|
|
@ -13778,6 +13953,8 @@ snapshots:
|
|||
|
||||
decamelize@1.2.0: {}
|
||||
|
||||
decimal.js-light@2.5.1: {}
|
||||
|
||||
decimal.js@10.5.0: {}
|
||||
|
||||
decode-named-character-reference@1.1.0:
|
||||
|
|
@ -13868,6 +14045,11 @@ snapshots:
|
|||
dependencies:
|
||||
utila: 0.4.0
|
||||
|
||||
dom-helpers@5.2.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.3
|
||||
csstype: 3.1.3
|
||||
|
||||
dom-serializer@0.2.2:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
|
@ -14384,6 +14566,8 @@ snapshots:
|
|||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-equals@5.2.2: {}
|
||||
|
||||
fast-glob@3.3.1:
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
|
|
@ -14963,6 +15147,8 @@ snapshots:
|
|||
hasown: 2.0.2
|
||||
side-channel: 1.1.0
|
||||
|
||||
internmap@2.0.3: {}
|
||||
|
||||
intl-messageformat@10.7.16:
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.4
|
||||
|
|
@ -17083,6 +17269,14 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/react': 19.1.6
|
||||
|
||||
react-smooth@4.0.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
fast-equals: 5.2.2
|
||||
prop-types: 15.8.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
react-transition-group: 4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
|
||||
react-style-singleton@2.2.3(@types/react@19.1.6)(react@19.1.0):
|
||||
dependencies:
|
||||
get-nonce: 1.0.1
|
||||
|
|
@ -17091,6 +17285,15 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/react': 19.1.6
|
||||
|
||||
react-transition-group@4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.3
|
||||
dom-helpers: 5.2.1
|
||||
loose-envify: 1.4.0
|
||||
prop-types: 15.8.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
react-universal-interface@0.6.2(react@19.1.0)(tslib@2.8.1):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
|
|
@ -17174,6 +17377,23 @@ snapshots:
|
|||
tiny-invariant: 1.3.3
|
||||
tslib: 2.8.1
|
||||
|
||||
recharts-scale@0.4.5:
|
||||
dependencies:
|
||||
decimal.js-light: 2.5.1
|
||||
|
||||
recharts@2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
eventemitter3: 4.0.7
|
||||
lodash: 4.17.21
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
react-is: 18.3.1
|
||||
react-smooth: 4.0.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
recharts-scale: 0.4.5
|
||||
tiny-invariant: 1.3.3
|
||||
victory-vendor: 36.9.2
|
||||
|
||||
redent@3.0.0:
|
||||
dependencies:
|
||||
indent-string: 4.0.0
|
||||
|
|
@ -18433,6 +18653,23 @@ snapshots:
|
|||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.2
|
||||
|
||||
victory-vendor@36.9.2:
|
||||
dependencies:
|
||||
'@types/d3-array': 3.2.1
|
||||
'@types/d3-ease': 3.0.2
|
||||
'@types/d3-interpolate': 3.0.4
|
||||
'@types/d3-scale': 4.0.9
|
||||
'@types/d3-shape': 3.1.7
|
||||
'@types/d3-time': 3.0.4
|
||||
'@types/d3-timer': 3.0.2
|
||||
d3-array: 3.2.4
|
||||
d3-ease: 3.0.1
|
||||
d3-interpolate: 3.0.1
|
||||
d3-scale: 4.0.2
|
||||
d3-shape: 3.2.0
|
||||
d3-time: 3.1.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
vite-node@3.1.4(@types/node@22.15.27)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(tsx@4.19.4)(yaml@2.8.0):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
|
|
|
|||
|
|
@ -302,3 +302,69 @@ export const getTasks = async ({
|
|||
.map((usage) => ({ ...usage, user: users[usage.userId] }))
|
||||
.filter((usage): usage is TaskWithUser => !!usage.user);
|
||||
};
|
||||
|
||||
/**
|
||||
* getDailyUsageByUser
|
||||
*/
|
||||
|
||||
const dailyUsageByUserSchema = z.object({
|
||||
date: z.string(),
|
||||
userId: z.string(),
|
||||
tasks: z.coerce.number(),
|
||||
tokens: z.coerce.number(),
|
||||
cost: z.coerce.number(),
|
||||
});
|
||||
|
||||
export type DailyUsageByUser = z.infer<typeof dailyUsageByUserSchema> & {
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const getDailyUsageByUser = async ({
|
||||
orgId,
|
||||
timePeriod = 90,
|
||||
}: {
|
||||
orgId?: string | null;
|
||||
timePeriod?: TimePeriod;
|
||||
}): Promise<DailyUsageByUser[]> => {
|
||||
if (!orgId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results = await analytics.query({
|
||||
query: `
|
||||
SELECT
|
||||
toString(toDate(fromUnixTimestamp(timestamp))) as date,
|
||||
userId,
|
||||
SUM(CASE WHEN type = '${TelemetryEventName.TASK_CREATED}' THEN 1 ELSE 0 END) AS tasks,
|
||||
SUM(CASE WHEN type = '${TelemetryEventName.LLM_COMPLETION}' THEN COALESCE(inputTokens, 0) + COALESCE(outputTokens, 0) ELSE 0 END) AS tokens,
|
||||
SUM(CASE WHEN type = '${TelemetryEventName.LLM_COMPLETION}' THEN COALESCE(cost, 0) ELSE 0 END) AS cost
|
||||
FROM events
|
||||
WHERE
|
||||
orgId = {orgId: String}
|
||||
AND timestamp >= toUnixTimestamp(now() - INTERVAL {timePeriod: Int32} DAY)
|
||||
AND type IN ({types: Array(String)})
|
||||
GROUP BY 1, 2
|
||||
ORDER BY date DESC, userId
|
||||
`,
|
||||
format: 'JSONEachRow',
|
||||
query_params: {
|
||||
orgId,
|
||||
timePeriod,
|
||||
types: [
|
||||
TelemetryEventName.TASK_CREATED,
|
||||
TelemetryEventName.TASK_COMPLETED,
|
||||
TelemetryEventName.LLM_COMPLETION,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const dailyUsages = z
|
||||
.array(dailyUsageByUserSchema)
|
||||
.parse(await results.json());
|
||||
|
||||
const users = await getUsersById(dailyUsages.map(({ userId }) => userId));
|
||||
|
||||
return dailyUsages
|
||||
.map((usage) => ({ ...usage, user: users[usage.userId] }))
|
||||
.filter((usage): usage is DailyUsageByUser => !!usage.user);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,10 +12,9 @@ import { TelemetryEventName } from '@roo-code/types';
|
|||
|
||||
import { type TimePeriod, timePeriods } from '@/types';
|
||||
import { getUsage } from '@/actions/analytics';
|
||||
import { formatCurrency } from '@/lib/formatters';
|
||||
import { formatCurrency, formatNumber } from '@/lib/formatters';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
|
|
@ -24,12 +23,15 @@ import {
|
|||
} from '@/components/ui';
|
||||
import { Button as EnhancedButton } from '@/components/ui/ecosystem';
|
||||
|
||||
import { Metric } from './Metric';
|
||||
import { UsageChart } from './UsageChart';
|
||||
|
||||
type MetricType = 'tasks' | 'tokens' | 'cost';
|
||||
|
||||
export const UsageCard = () => {
|
||||
const t = useTranslations('DashboardIndex');
|
||||
const { orgId } = useAuth();
|
||||
const [timePeriod, setTimePeriod] = useState<TimePeriod>(7);
|
||||
const [selectedMetric, setSelectedMetric] = useState<MetricType>('tasks');
|
||||
|
||||
const path = usePathname();
|
||||
|
||||
|
|
@ -65,48 +67,95 @@ export const UsageCard = () => {
|
|||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-row flex-wrap gap-1.5">
|
||||
{timePeriods.map((period) => (
|
||||
<Button
|
||||
key={period}
|
||||
variant={period === timePeriod ? 'default' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => setTimePeriod(period)}
|
||||
className="text-xs px-2.5 h-8"
|
||||
>
|
||||
{t(`analytics_period_${period}_days`)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-5">
|
||||
<Metric
|
||||
label={t('analytics_active_developers')}
|
||||
value={usage[TelemetryEventName.TASK_CREATED]?.users}
|
||||
isPending={isPending}
|
||||
/>
|
||||
<Metric
|
||||
label={t('analytics_tasks_started')}
|
||||
value={usage[TelemetryEventName.TASK_CREATED]?.events}
|
||||
isPending={isPending}
|
||||
/>
|
||||
<Metric
|
||||
label={t('analytics_tasks_completed')}
|
||||
value={usage[TelemetryEventName.TASK_COMPLETED]?.events}
|
||||
isPending={isPending}
|
||||
/>
|
||||
<Metric
|
||||
label={t('analytics_tokens_consumed')}
|
||||
value={usage[TelemetryEventName.LLM_COMPLETION]?.tokens}
|
||||
isPending={isPending}
|
||||
/>
|
||||
<Metric
|
||||
label={t('analytics_llm_costs')}
|
||||
value={formatCurrency(
|
||||
usage[TelemetryEventName.LLM_COMPLETION]?.cost,
|
||||
<div className="flex flex-row flex-wrap gap-4 justify-between items-center">
|
||||
<div className="flex flex-row gap-3 items-center">
|
||||
{/* Time period toggles */}
|
||||
<div className="flex flex-row gap-1">
|
||||
{timePeriods.map((period) => (
|
||||
<button
|
||||
key={period}
|
||||
onClick={() => setTimePeriod(period)}
|
||||
className={`px-2 py-1 text-xs font-medium rounded transition-colors ${
|
||||
period === timePeriod
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
{period}d
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Chart metric toggles - only show on usage page */}
|
||||
{path === '/usage' && (
|
||||
<div className="flex flex-row gap-1">
|
||||
{(['tasks', 'tokens', 'cost'] as MetricType[]).map(
|
||||
(metric) => (
|
||||
<button
|
||||
key={metric}
|
||||
onClick={() => setSelectedMetric(metric)}
|
||||
className={`px-2 py-1 text-xs font-medium rounded transition-colors capitalize ${
|
||||
metric === selectedMetric
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
{metric}
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
isPending={isPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Compact inline metrics */}
|
||||
<div className="flex flex-wrap gap-4 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-muted-foreground">Developers:</span>
|
||||
<span className="font-medium">
|
||||
{isPending
|
||||
? '...'
|
||||
: usage[TelemetryEventName.TASK_CREATED]?.users || 0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-muted-foreground">Tasks:</span>
|
||||
<span className="font-medium">
|
||||
{isPending
|
||||
? '...'
|
||||
: `${usage[TelemetryEventName.TASK_CREATED]?.events || 0}/${usage[TelemetryEventName.TASK_COMPLETED]?.events || 0}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-muted-foreground">Tokens:</span>
|
||||
<span className="font-medium">
|
||||
{isPending
|
||||
? '...'
|
||||
: formatNumber(
|
||||
usage[TelemetryEventName.LLM_COMPLETION]?.tokens || 0,
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-muted-foreground">Cost:</span>
|
||||
<span className="font-medium">
|
||||
{isPending
|
||||
? '...'
|
||||
: formatCurrency(
|
||||
usage[TelemetryEventName.LLM_COMPLETION]?.cost || 0,
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chart displayed below on usage page */}
|
||||
{path === '/usage' && (
|
||||
<UsageChart
|
||||
timePeriod={timePeriod}
|
||||
selectedMetric={selectedMetric}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
386
src/components/usage/UsageChart.tsx
Normal file
386
src/components/usage/UsageChart.tsx
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
'use client';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { useAuth } from '@clerk/nextjs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
|
||||
import type { TimePeriod } from '@/types';
|
||||
import { getDailyUsageByUser } from '@/actions/analytics';
|
||||
import { formatNumber } from '@/lib/formatters';
|
||||
import { Skeleton } from '@/components/ui';
|
||||
|
||||
type MetricType = 'tasks' | 'tokens' | 'cost';
|
||||
|
||||
const metricTypes: MetricType[] = ['tasks', 'tokens', 'cost'];
|
||||
|
||||
interface TickProps {
|
||||
x?: number;
|
||||
y?: number;
|
||||
payload?: {
|
||||
value: string;
|
||||
};
|
||||
formatValue?: (value: number) => string;
|
||||
}
|
||||
|
||||
interface TooltipEntry {
|
||||
dataKey: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface TooltipProps {
|
||||
active?: boolean;
|
||||
payload?: TooltipEntry[];
|
||||
label?: string;
|
||||
selectedMetric?: MetricType;
|
||||
formatValue?: (value: number) => string;
|
||||
}
|
||||
|
||||
interface ChartDataPoint {
|
||||
date: string;
|
||||
total: number;
|
||||
[userKey: string]: string | number;
|
||||
}
|
||||
|
||||
// Elegant color palette inspired by modern design systems
|
||||
const generateUserColor = (index: number): string => {
|
||||
const colors = [
|
||||
'#3B82F6', // Blue
|
||||
'#10B981', // Emerald
|
||||
'#8B5CF6', // Violet
|
||||
'#F59E0B', // Amber
|
||||
'#EF4444', // Red
|
||||
'#06B6D4', // Cyan
|
||||
'#84CC16', // Lime
|
||||
'#EC4899', // Pink
|
||||
'#6366F1', // Indigo
|
||||
'#14B8A6', // Teal
|
||||
];
|
||||
return colors[index % colors.length]!;
|
||||
};
|
||||
|
||||
interface UsageChartProps {
|
||||
timePeriod: TimePeriod;
|
||||
selectedMetric?: MetricType;
|
||||
}
|
||||
|
||||
// Custom tick components for theme-aware labels
|
||||
const CustomXAxisTick = (props: TickProps) => {
|
||||
const { x, y, payload } = props;
|
||||
|
||||
if (!payload?.value) return null;
|
||||
|
||||
// Parse date string as local date to avoid timezone issues
|
||||
const [year, month, day] = payload.value.split('-').map(Number);
|
||||
if (!year || !month || !day) return null;
|
||||
|
||||
const date = new Date(year, month - 1, day); // month is 0-indexed
|
||||
const formattedDate = date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
return (
|
||||
<g transform={`translate(${x},${y})`}>
|
||||
<text
|
||||
x={0}
|
||||
y={0}
|
||||
dy={16}
|
||||
textAnchor="middle"
|
||||
className="fill-muted-foreground text-xs font-medium"
|
||||
>
|
||||
{formattedDate}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomYAxisTick = (props: TickProps) => {
|
||||
const { x, y, payload, formatValue } = props;
|
||||
|
||||
if (!payload?.value) return null;
|
||||
|
||||
const value =
|
||||
typeof payload.value === 'string'
|
||||
? parseFloat(payload.value)
|
||||
: payload.value;
|
||||
|
||||
return (
|
||||
<g transform={`translate(${x},${y})`}>
|
||||
<text
|
||||
x={0}
|
||||
y={0}
|
||||
dy={4}
|
||||
textAnchor="end"
|
||||
className="fill-muted-foreground text-xs font-medium"
|
||||
>
|
||||
{formatValue && typeof value === 'number'
|
||||
? formatValue(value)
|
||||
: payload.value}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
// Custom tooltip component
|
||||
const CustomTooltip = ({
|
||||
active,
|
||||
payload,
|
||||
label,
|
||||
formatValue,
|
||||
}: TooltipProps) => {
|
||||
if (active && payload && payload.length && label) {
|
||||
// Parse date string as local date to avoid timezone issues
|
||||
const [year, month, day] = label.split('-').map(Number);
|
||||
if (!year || !month || !day) return null;
|
||||
|
||||
const date = new Date(year, month - 1, day); // month is 0-indexed
|
||||
const formattedDate = date.toLocaleDateString('en-US', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="bg-popover border border-border rounded-lg shadow-lg p-3 min-w-[200px]">
|
||||
<p className="text-sm font-medium text-foreground mb-2">
|
||||
{formattedDate}
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{payload.map((entry, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between gap-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-sm"
|
||||
style={{ backgroundColor: entry.color }}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{entry.dataKey}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-foreground">
|
||||
{typeof entry.value === 'number' && formatValue
|
||||
? formatValue(entry.value)
|
||||
: entry.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const UsageChart = ({
|
||||
timePeriod,
|
||||
selectedMetric = 'tasks',
|
||||
}: UsageChartProps) => {
|
||||
const { orgId } = useAuth();
|
||||
|
||||
const { data: dailyUsage = [], isPending } = useQuery({
|
||||
queryKey: ['getDailyUsageByUser', orgId, timePeriod],
|
||||
queryFn: () => getDailyUsageByUser({ orgId, timePeriod }),
|
||||
enabled: !!orgId,
|
||||
});
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!dailyUsage.length) return [];
|
||||
|
||||
// Group data by date
|
||||
const dateGroups = dailyUsage.reduce(
|
||||
(acc, item) => {
|
||||
const date = item.date;
|
||||
if (!acc[date]) {
|
||||
acc[date] = { date, total: 0 };
|
||||
}
|
||||
|
||||
const value = item[selectedMetric];
|
||||
acc[date][item.user.name || item.user.email || 'Unknown'] = value;
|
||||
acc[date].total += value;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, ChartDataPoint>,
|
||||
);
|
||||
|
||||
// Convert to array and sort by date
|
||||
const result = Object.values(dateGroups).sort((a, b) => {
|
||||
// Parse dates as local dates to avoid timezone issues
|
||||
const [yearA, monthA, dayA] = a.date.split('-').map(Number);
|
||||
const [yearB, monthB, dayB] = b.date.split('-').map(Number);
|
||||
|
||||
if (!yearA || !monthA || !dayA || !yearB || !monthB || !dayB) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const dateA = new Date(yearA, monthA - 1, dayA);
|
||||
const dateB = new Date(yearB, monthB - 1, dayB);
|
||||
return dateA.getTime() - dateB.getTime();
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [dailyUsage, selectedMetric]);
|
||||
|
||||
const uniqueUsers = useMemo(() => {
|
||||
const users = new Set<string>();
|
||||
dailyUsage.forEach((item) => {
|
||||
users.add(item.user.name || item.user.email || 'Unknown');
|
||||
});
|
||||
return Array.from(users).sort();
|
||||
}, [dailyUsage]);
|
||||
|
||||
const formatValue = (value: number) => {
|
||||
switch (selectedMetric) {
|
||||
case 'cost':
|
||||
return value < 1 ? `$${value.toFixed(2)}` : `$${value.toFixed(0)}`;
|
||||
case 'tokens':
|
||||
return value >= 1000000
|
||||
? `${(value / 1000000).toFixed(1)}M`
|
||||
: formatNumber(value);
|
||||
default:
|
||||
return value.toString();
|
||||
}
|
||||
};
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap gap-2 justify-center sm:justify-start">
|
||||
{metricTypes.map((metric) => (
|
||||
<Skeleton key={metric} className="h-9 w-20 rounded-md" />
|
||||
))}
|
||||
</div>
|
||||
<div className="h-72 w-full rounded-lg border bg-card p-3">
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<div className="space-y-6 w-full">
|
||||
<div className="flex justify-between items-end h-48 px-4">
|
||||
{Array.from({ length: 7 }).map((_, i) => (
|
||||
<Skeleton
|
||||
key={i}
|
||||
className="w-12 rounded-t-md"
|
||||
style={{ height: `${Math.random() * 60 + 30}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pt-4">
|
||||
<Skeleton className="h-3 w-20" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
<Skeleton className="h-3 w-16" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!chartData.length) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="h-72 w-full rounded-lg border bg-card p-3 flex items-center justify-center">
|
||||
<div className="text-center space-y-3">
|
||||
<div className="w-12 h-12 mx-auto rounded-full bg-muted/20 flex items-center justify-center">
|
||||
<svg
|
||||
className="w-6 h-6 text-muted-foreground/50"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
No data available
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No usage data found for the selected period
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="h-72 w-full rounded-lg border bg-card p-3">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={chartData}
|
||||
margin={{
|
||||
top: 5,
|
||||
right: 5,
|
||||
left: 0,
|
||||
bottom: 5,
|
||||
}}
|
||||
barCategoryGap="10%"
|
||||
>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
stroke="hsl(var(--border))"
|
||||
opacity={0.3}
|
||||
vertical={false}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={<CustomXAxisTick />}
|
||||
height={25}
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={formatValue}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={<CustomYAxisTick formatValue={formatValue} />}
|
||||
width={55}
|
||||
/>
|
||||
<Tooltip
|
||||
content={
|
||||
<CustomTooltip
|
||||
selectedMetric={selectedMetric}
|
||||
formatValue={formatValue}
|
||||
/>
|
||||
}
|
||||
cursor={{
|
||||
fill: 'hsl(var(--muted))',
|
||||
opacity: 0.1,
|
||||
}}
|
||||
/>
|
||||
{uniqueUsers.map((user, index) => (
|
||||
<Bar
|
||||
key={user}
|
||||
dataKey={user}
|
||||
stackId="usage"
|
||||
fill={generateUserColor(index)}
|
||||
radius={
|
||||
index === uniqueUsers.length - 1 ? [4, 4, 0, 0] : [0, 0, 0, 0]
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
export { UsageCard } from './UsageCard';
|
||||
export { TaskCard } from './TaskCard';
|
||||
export { UsageChart } from './UsageChart';
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@
|
|||
"view_mode_title": "View By",
|
||||
"view_mode_developers": "Developers",
|
||||
"view_mode_models": "Models",
|
||||
"view_mode_tasks": "Tasks"
|
||||
"view_mode_tasks": "Tasks",
|
||||
"chart_metric_tasks": "Tasks",
|
||||
"chart_metric_tokens": "Tokens",
|
||||
"chart_metric_cost": "Cost"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue