Merge pull request #6 from RooCodeInc/cte/mock

This commit is contained in:
Chris Estreich 2025-05-14 15:28:45 -07:00 committed by GitHub
commit 3aff8994ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 10189 additions and 15 deletions

2
.gitignore vendored
View file

@ -53,5 +53,3 @@ local
# docker
.docker/*
!.docker/scripts

View file

@ -28,15 +28,23 @@
"@clerk/nextjs": "^6.18.3",
"@clerk/themes": "^2.1.36",
"@clickhouse/client": "^1.11.1",
"@hookform/resolvers": "^5.0.1",
"@logtail/pino": "^0.5.2",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-slider": "^1.3.4",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-toast": "^1.2.13",
"@radix-ui/react-tooltip": "^1.1.3",
"@sentry/nextjs": "^9.17.0",
"@spotlightjs/spotlight": "^2.5.0",
"@t3-oss/env-nextjs": "^0.13.4",
"@tailwindcss/postcss": "^4.1.6",
"@tanstack/react-table": "^8.20.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.43.1",

300
pnpm-lock.yaml generated
View file

@ -20,21 +20,42 @@ importers:
'@clickhouse/client':
specifier: ^1.11.1
version: 1.11.1
'@hookform/resolvers':
specifier: ^5.0.1
version: 5.0.1(react-hook-form@7.56.3(react@19.1.0))
'@logtail/pino':
specifier: ^0.5.2
version: 0.5.4(pino@9.6.0)
'@radix-ui/react-accordion':
specifier: ^1.2.1
version: 1.2.10(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-checkbox':
specifier: ^1.3.1
version: 1.3.1(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-dropdown-menu':
specifier: ^2.1.14
version: 2.1.14(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-icons':
specifier: ^1.3.2
version: 1.3.2(react@19.1.0)
'@radix-ui/react-label':
specifier: ^2.1.0
version: 2.1.6(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-separator':
specifier: ^1.1.6
version: 1.1.6(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slider':
specifier: ^1.3.4
version: 1.3.4(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot':
specifier: ^1.2.2
version: 1.2.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-toast':
specifier: ^1.2.13
version: 1.2.13(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-tooltip':
specifier: ^1.1.3
version: 1.2.6(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@sentry/nextjs':
specifier: ^9.17.0
version: 9.17.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.99.8(@swc/core@1.11.24)(esbuild@0.25.4))
@ -47,6 +68,9 @@ importers:
'@tailwindcss/postcss':
specifier: ^4.1.6
version: 4.1.6
'@tanstack/react-table':
specifier: ^8.20.5
version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@ -1429,6 +1453,11 @@ packages:
'@hapi/topo@5.1.0':
resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==}
'@hookform/resolvers@5.0.1':
resolution: {integrity: sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==}
peerDependencies:
react-hook-form: ^7.55.0
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@ -2381,9 +2410,25 @@ packages:
peerDependencies:
'@opentelemetry/api': ^1.8
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
'@radix-ui/primitive@1.1.2':
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
'@radix-ui/react-accordion@1.2.10':
resolution: {integrity: sha512-x+URzV1siKmeXPSUIQ22L81qp2eOhjpy3tgteF+zOr4d1u0qJnFuyBF4MoQRhmKP6ivDxlvDAvqaF77gh7DOIw==}
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-arrow@1.1.6':
resolution: {integrity: sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==}
peerDependencies:
@ -2397,6 +2442,32 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-checkbox@1.3.1':
resolution: {integrity: sha512-xTaLKAO+XXMPK/BpVTSaAAhlefmvMSACjIhK9mGsImvX2ljcTDm8VGR1CuS1uYcNdR5J+oiOhoJZc5un6bh3VQ==}
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-collapsible@1.1.10':
resolution: {integrity: sha512-O2mcG3gZNkJ/Ena34HurA3llPOEA/M4dJtIRMa6y/cknRDC8XY5UZBInKTsUwW5cUue9A4k0wi1XU5fKBzKe1w==}
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-collection@1.1.6':
resolution: {integrity: sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==}
peerDependencies:
@ -2499,6 +2570,19 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-label@2.1.6':
resolution: {integrity: sha512-S/hv1mTlgcPX2gCTJrWuTjSXf7ER3Zf7zWGtOprxhIIY93Qin3n5VgNA0Ez9AgrK/lEtlYgzLd4f5x6AVar4Yw==}
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-menu@2.1.14':
resolution: {integrity: sha512-0zSiBAIFq9GSKoSH5PdEaQeRB3RnEGxC+H2P0egtnKoKKLNBH8VBHyVO6/jskhjAezhOIplyRUj7U2lds9A+Yg==}
peerDependencies:
@ -2590,6 +2674,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-slider@1.3.4':
resolution: {integrity: sha512-Cp6hEmQtRJFci285vkdIJ+HCDLTRDk+25VhFwa1fcubywjMUE3PynBgtN5RLudOgSCYMlT4jizCXdmV+8J7Y2w==}
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-slot@1.2.2':
resolution: {integrity: sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==}
peerDependencies:
@ -2599,6 +2696,32 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-toast@1.2.13':
resolution: {integrity: sha512-e/e43mQAwgYs8BY4y9l99xTK6ig1bK2uXsFLOMn9IZ16lAgulSTsotcPHVT2ZlSb/ye6Sllq7IgyDB8dGhpeXQ==}
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.2.6':
resolution: {integrity: sha512-zYb+9dc9tkoN2JjBDIIPLQtk3gGyz8FMKoqYTb8EMVQ5a5hBcdHPECrsZVI4NpPAUOixhkoqg7Hj5ry5USowfA==}
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-use-callback-ref@1.1.1':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies:
@ -2644,6 +2767,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-previous@1.1.1':
resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-rect@1.1.1':
resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
peerDependencies:
@ -2662,6 +2794,19 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-visually-hidden@1.2.2':
resolution: {integrity: sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==}
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/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
@ -3046,6 +3191,9 @@ packages:
engines: {node: '>=18'}
hasBin: true
'@standard-schema/utils@0.3.0':
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
'@storybook/addon-actions@8.6.12':
resolution: {integrity: sha512-B5kfiRvi35oJ0NIo53CGH66H471A3XTzrfaa6SxXEJsgxxSeKScG5YeXcCvLiZfvANRQ7QDsmzPUgg0o3hdMXw==}
peerDependencies:
@ -3461,6 +3609,17 @@ packages:
'@tailwindcss/postcss@4.1.6':
resolution: {integrity: sha512-ELq+gDMBuRXPJlpE3PEen+1MhnHAQQrh2zF0dI1NXOlEWfr2qWf2CQdr5jl9yANv8RErQaQ2l6nIFO9OSCVq/g==}
'@tanstack/react-table@8.21.3':
resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==}
engines: {node: '>=12'}
peerDependencies:
react: '>=16.8'
react-dom: '>=16.8'
'@tanstack/table-core@8.21.3':
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
engines: {node: '>=12'}
'@testing-library/dom@10.4.0':
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
engines: {node: '>=18'}
@ -9854,6 +10013,11 @@ snapshots:
dependencies:
'@hapi/hoek': 9.3.0
'@hookform/resolvers@5.0.1(react-hook-form@7.56.3(react@19.1.0))':
dependencies:
'@standard-schema/utils': 0.3.0
react-hook-form: 7.56.3(react@19.1.0)
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6':
@ -11052,8 +11216,26 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@radix-ui/number@1.1.1': {}
'@radix-ui/primitive@1.1.2': {}
'@radix-ui/react-accordion@1.2.10(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-collapsible': 1.1.10(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-collection': 1.1.6(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-direction': 1.1.1(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-primitive': 2.1.2(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.3
'@radix-ui/react-arrow@1.1.6(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-primitive': 2.1.2(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@ -11062,6 +11244,36 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.3
'@radix-ui/react-checkbox@1.3.1(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-presence': 1.1.4(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.2(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-use-size': 1.1.1(@types/react@19.1.3)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.3
'@radix-ui/react-collapsible@1.1.10(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-presence': 1.1.4(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.2(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.3)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.3
'@radix-ui/react-collection@1.1.6(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
@ -11144,6 +11356,14 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.3
'@radix-ui/react-label@2.1.6(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-primitive': 2.1.2(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.3
'@radix-ui/react-menu@2.1.14(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
@ -11236,6 +11456,24 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.3
'@radix-ui/react-slider@1.3.4(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/number': 1.1.1
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-collection': 1.1.6(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-direction': 1.1.1(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-primitive': 2.1.2(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-use-size': 1.1.1(@types/react@19.1.3)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.3
'@radix-ui/react-slot@1.2.2(@types/react@19.1.3)(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
@ -11243,6 +11481,44 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.3
'@radix-ui/react-toast@1.2.13(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-collection': 1.1.6(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-dismissable-layer': 1.1.9(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-portal': 1.1.8(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-presence': 1.1.4(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.2(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-visually-hidden': 1.2.2(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.3
'@radix-ui/react-tooltip@1.2.6(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-dismissable-layer': 1.1.9(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-popper': 1.2.6(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-portal': 1.1.8(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-presence': 1.1.4(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.2(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot': 1.2.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.3)(react@19.1.0)
'@radix-ui/react-visually-hidden': 1.2.2(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.3
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.3)(react@19.1.0)':
dependencies:
react: 19.1.0
@ -11277,6 +11553,12 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.3
'@radix-ui/react-use-previous@1.1.1(@types/react@19.1.3)(react@19.1.0)':
dependencies:
react: 19.1.0
optionalDependencies:
'@types/react': 19.1.3
'@radix-ui/react-use-rect@1.1.1(@types/react@19.1.3)(react@19.1.0)':
dependencies:
'@radix-ui/rect': 1.1.1
@ -11291,6 +11573,14 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.3
'@radix-ui/react-visually-hidden@1.2.2(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-primitive': 2.1.2(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.3
'@radix-ui/rect@1.1.1': {}
'@rollup/plugin-commonjs@28.0.1(rollup@4.35.0)':
@ -11704,6 +11994,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@standard-schema/utils@0.3.0': {}
'@storybook/addon-actions@8.6.12(storybook@8.6.12(prettier@3.5.3))':
dependencies:
'@storybook/global': 5.0.0
@ -12231,6 +12523,14 @@ snapshots:
postcss: 8.5.3
tailwindcss: 4.1.6
'@tanstack/react-table@8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@tanstack/table-core': 8.21.3
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
'@tanstack/table-core@8.21.3': {}
'@testing-library/dom@10.4.0':
dependencies:
'@babel/code-frame': 7.27.1

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 226 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/apple-touch-icon.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
public/favicon-16x16.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 445 B

After

Width:  |  Height:  |  Size: 322 B

BIN
public/favicon-32x32.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,001 B

After

Width:  |  Height:  |  Size: 600 B

BIN
public/favicon.ico Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

1
public/placeholder.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
public/placeholder_pfp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View file

@ -0,0 +1,23 @@
'use client';
import { useTranslations } from 'next-intl';
import { TitleBar } from '@/components/dashboard/TitleBar';
import { AnalyticsPage } from '@/features/analytics/AnalyticsPage';
const AnalyticsPageContainer = () => {
const t = useTranslations('Analytics');
return (
<>
<TitleBar
title={t('title_bar')}
description={t('title_bar_description')}
/>
<AnalyticsPage />
</>
);
};
export default AnalyticsPageContainer;

View file

@ -0,0 +1,98 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Drawer } from '@/components/ui/drawer';
import { TitleBar } from '@/components/dashboard/TitleBar';
import { AuditLogDetails } from '@/features/dashboard/AuditLogDetails';
import { AuditLogEntry } from '@/features/dashboard/AuditLogEntry';
import type { AuditLog } from '@/features/dashboard/mockAuditLogs';
import { getFilteredLogs } from '@/features/dashboard/mockAuditLogs';
type TimePeriod = '7' | '30' | '90';
const AuditLogsPage = () => {
const [timePeriod, setTimePeriod] = useState<TimePeriod>('7');
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
// Get filtered logs based on selected time period
const logs = getFilteredLogs(Number(timePeriod));
const handleLogClick = (log: AuditLog) => {
setSelectedLog(log);
setIsDrawerOpen(true);
};
const handleCloseDrawer = () => {
setIsDrawerOpen(false);
};
return (
<>
<TitleBar
title="Audit Logs"
description="View all organization activity and changes"
/>
<div className="w-2/3 rounded-md bg-card p-5">
<div className="mb-4">
<h3 className="text-lg font-semibold">All Activity</h3>
<p className="text-sm text-muted-foreground">
Showing all audit logs for your organization
</p>
</div>
{/* Time period toggle */}
<div className="mb-4 flex space-x-2">
<Button
variant={timePeriod === '7' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('7')}
>
Last 7 days
</Button>
<Button
variant={timePeriod === '30' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('30')}
>
Last 30 days
</Button>
<Button
variant={timePeriod === '90' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('90')}
>
Last 90 days
</Button>
</div>
{/* Log entries */}
<div className="space-y-1">
{logs.length > 0 ? (
logs.map((log: AuditLog) => (
<AuditLogEntry key={log.id} log={log} onClick={handleLogClick} />
))
) : (
<div className="py-8 text-center">
<p className="text-sm text-muted-foreground">No activity found</p>
</div>
)}
</div>
</div>
{/* Drawer for log details */}
<Drawer
isOpen={isDrawerOpen}
onClose={handleCloseDrawer}
title="Activity Details"
>
{selectedLog && <AuditLogDetails log={selectedLog} />}
</Drawer>
</>
);
};
export default AuditLogsPage;

View file

@ -1,10 +1,16 @@
'use client';
import { useTranslations } from 'next-intl';
import { useTheme } from 'next-themes';
import { dark } from '@clerk/themes';
import { OrganizationProfile } from '@clerk/nextjs';
import DefaultParametersPage from '@/features/dashboard/DefaultParametersPage';
import ProviderWhitelistPage from '@/features/dashboard/ProviderWhitelistPage';
export const OrgProfile = () => {
const t = useTranslations('OrganizationProfile');
const { resolvedTheme } = useTheme();
const baseTheme = resolvedTheme === 'dark' ? dark : undefined;
@ -14,6 +20,70 @@ export const OrgProfile = () => {
routing="path"
path="/dashboard/org"
afterLeaveOrganizationUrl="/onboarding/select-org"
/>
>
<OrganizationProfile.Page
label={t('provider_whitelist')}
url="provider-whitelist"
labelIcon={<SettingsIcon />}
>
<ProviderWhitelistPage />
</OrganizationProfile.Page>
<OrganizationProfile.Page
label={t('default_parameters')}
url="default-parameters"
labelIcon={<ParametersIcon />}
>
<DefaultParametersPage />
</OrganizationProfile.Page>
</OrganizationProfile>
);
};
const SettingsIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-4"
>
<path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z" />
<path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z" />
<path d="M12 2v2" />
<path d="M12 22v-2" />
<path d="m17 20.66-1-1.73" />
<path d="M11 10.27 7 3.34" />
<path d="m20.66 17-1.73-1" />
<path d="m3.34 7 1.73 1" />
<path d="M14 12h8" />
<path d="M2 12h2" />
<path d="m20.66 7-1.73 1" />
<path d="m3.34 17 1.73-1" />
<path d="m17 3.34-1 1.73" />
<path d="m7 20.66 1-1.73" />
</svg>
);
};
const ParametersIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-4"
>
<path d="M3 3v18h18" />
<path d="M18.4 9a9 9 0 0 0-9.4 9" />
<path d="M8 12l4-4 4 4" />
</svg>
);
};

View file

@ -1,11 +1,20 @@
import { useTranslations } from 'next-intl';
import { TitleBar } from '@/components/dashboard/TitleBar';
import { AuditLogCard } from '@/features/dashboard/AuditLogCard';
import { UsageAnalyticsCard } from '@/features/dashboard/UsageAnalyticsCard';
export default function Page() {
const t = useTranslations('DashboardIndex');
return (
<TitleBar title={t('title_bar')} description={t('title_bar_description')} />
<>
<TitleBar
title={t('title_bar')}
description={t('title_bar_description')}
/>
<UsageAnalyticsCard />
<AuditLogCard />
</>
);
}

View file

@ -7,6 +7,9 @@ import { ClerkProvider } from '@clerk/nextjs';
import { getClerkLocale } from '@/lib/locale';
import { ThemeProvider } from '@/components/layout';
import { ToastProvider, ToastViewport } from '@/components/ui/toast';
import { ToastProvider as ToastContextProvider } from '@/components/ui/toast-context';
import { Toaster } from '@/components/ui/toaster';
export const metadata: Metadata = {
icons: [
@ -43,7 +46,13 @@ export default async function RootLayout({
<NextIntlClientProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ClerkProvider localization={getClerkLocale(locale)}>
{children}
<ToastProvider>
<ToastContextProvider>
{children}
<Toaster />
<ToastViewport />
</ToastContextProvider>
</ToastProvider>
</ClerkProvider>
</ThemeProvider>
</NextIntlClientProvider>

View file

@ -1,5 +1,25 @@
import { APP_NAME } from '@/lib/constants';
export const Logo = () => (
<div className="text-lg font-semibold">{APP_NAME}</div>
import Image from 'next/image';
export const Logo = (props: { isTextHidden?: boolean }) => (
<div className="flex items-center text-xl font-semibold">
<Image
src="/Roo-Code-Logo-Horiz-blk.svg"
alt="Roo Code Logo"
width={120}
height={26}
className="mr-2 dark:hidden"
style={{ height: 'auto' }}
/>
<Image
src="/Roo-Code-Logo-Horiz-white.svg"
alt="Roo Code Logo"
width={120}
height={26}
className="mr-2 hidden dark:block"
style={{ height: 'auto' }}
/>
{!props.isTextHidden && APP_NAME}
</div>
);

View file

@ -0,0 +1,46 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const badgeVariants = cva(
'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
secondary:
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
destructive:
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
);
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<'span'> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'span';
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
);
}
export { Badge, badgeVariants };

View file

@ -0,0 +1,30 @@
'use client';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { Check } from 'lucide-react';
import * as React from 'react';
import { cn } from '@/lib/utils';
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn('flex items-center justify-center text-current')}
>
<Check className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };

View file

@ -0,0 +1,80 @@
'use client';
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
}
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
);
}

View file

@ -0,0 +1,109 @@
'use client';
import type { VariantProps } from 'class-variance-authority';
import { X } from 'lucide-react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { Button } from './button';
import { drawerTransformVariants, drawerVariants } from './drawerVariants';
export type DrawerProps = VariantProps<typeof drawerVariants> & {
isOpen: boolean;
onClose: () => void;
title?: React.ReactNode;
children: React.ReactNode;
className?: string;
overlayClassName?: string;
};
export function Drawer({
isOpen,
onClose,
title,
children,
side = 'right',
size = 'md',
className,
overlayClassName,
...props
}: DrawerProps) {
// Handle ESC key press to close the drawer
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
onClose();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
// Handle focus trap
const drawerRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (isOpen && drawerRef.current) {
const focusableElements = drawerRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
if (focusableElements.length > 0) {
(focusableElements[0] as HTMLElement).focus();
}
}
}, [isOpen]);
if (!isOpen) {
return null;
}
// Get the transform class based on the side
const transformClass =
drawerTransformVariants[side as keyof typeof drawerTransformVariants];
const openClass = transformClass ? transformClass.open : '';
const closedClass = transformClass ? transformClass.closed : '';
return (
<div className="fixed inset-0 z-50 overflow-hidden">
{/* Overlay */}
<div
className={cn(
'fixed inset-0 bg-black/50 transition-opacity',
overlayClassName,
)}
onClick={onClose}
aria-hidden="true"
/>
{/* Drawer */}
<div
ref={drawerRef}
className={cn(
drawerVariants({ side, size }),
isOpen ? openClass : closedClass,
className,
)}
{...props}
>
{/* Header */}
<div className="flex items-center justify-between border-b p-4">
<h2 className="text-lg font-semibold">{title}</h2>
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="size-8 p-0"
aria-label="Close"
>
<X className="size-4" />
</Button>
</div>
{/* Content */}
<div className="overflow-auto p-4">{children}</div>
</div>
</div>
);
}

View file

@ -0,0 +1,46 @@
import { cva } from 'class-variance-authority';
export const drawerVariants = cva(
'fixed z-50 bg-background shadow-lg transition-transform duration-300 ease-in-out',
{
variants: {
side: {
top: 'inset-x-0 top-0 border-b',
right: 'inset-y-0 right-0 border-l',
bottom: 'inset-x-0 bottom-0 border-t',
left: 'inset-y-0 left-0 border-r',
},
size: {
sm: 'sm:max-w-sm',
md: 'sm:max-w-md',
lg: 'sm:max-w-lg',
xl: 'sm:max-w-xl',
'2xl': 'sm:max-w-2xl',
full: 'sm:max-w-full',
},
},
defaultVariants: {
side: 'right',
size: 'md',
},
},
);
export const drawerTransformVariants = {
top: {
open: 'translate-y-0',
closed: '-translate-y-full',
},
right: {
open: 'translate-x-0',
closed: 'translate-x-full',
},
bottom: {
open: 'translate-y-0',
closed: 'translate-y-full',
},
left: {
open: 'translate-x-0',
closed: '-translate-x-full',
},
};

168
src/components/ui/form.tsx Normal file
View file

@ -0,0 +1,168 @@
'use client';
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from 'react-hook-form';
import { cn } from '@/lib/utils';
import { Label } from '@/components/ui/label';
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>');
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue,
);
function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn('grid gap-2', className)}
{...props}
/>
</FormItemContext.Provider>
);
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField();
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn('data-[error=true]:text-destructive', className)}
htmlFor={formItemId}
{...props}
/>
);
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
}
function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
const { formDescriptionId } = useFormField();
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? '') : props.children;
if (!body) {
return null;
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn('text-destructive text-sm', className)}
{...props}
>
{body}
</p>
);
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};

View file

@ -0,0 +1,21 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
<input
type={type}
data-slot="input"
className={cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className,
)}
{...props}
/>
);
}
export { Input };

View file

@ -0,0 +1,24 @@
'use client';
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cn } from '@/lib/utils';
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
className,
)}
{...props}
/>
);
}
export { Label };

View file

@ -0,0 +1,28 @@
'use client';
import * as SliderPrimitive from '@radix-ui/react-slider';
import * as React from 'react';
import { cn } from '@/lib/utils';
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
'relative flex w-full touch-none select-none items-center',
className,
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block size-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };

116
src/components/ui/table.tsx Normal file
View file

@ -0,0 +1,116 @@
'use client';
import * as React from 'react';
import { cn } from '@/lib/utils';
function Table({ className, ...props }: React.ComponentProps<'table'>) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn('w-full caption-bottom text-sm', className)}
{...props}
/>
</div>
);
}
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
return (
<thead
data-slot="table-header"
className={cn('[&_tr]:border-b', className)}
{...props}
/>
);
}
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
return (
<tbody
data-slot="table-body"
className={cn('[&_tr:last-child]:border-0', className)}
{...props}
/>
);
}
function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
return (
<tfoot
data-slot="table-footer"
className={cn(
'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
className,
)}
{...props}
/>
);
}
function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
return (
<tr
data-slot="table-row"
className={cn(
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
className,
)}
{...props}
/>
);
}
function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
return (
<th
data-slot="table-head"
className={cn(
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className,
)}
{...props}
/>
);
}
function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
return (
<td
data-slot="table-cell"
className={cn(
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className,
)}
{...props}
/>
);
}
function TableCaption({
className,
...props
}: React.ComponentProps<'caption'>) {
return (
<caption
data-slot="table-caption"
className={cn('text-muted-foreground mt-4 text-sm', className)}
{...props}
/>
);
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

View file

@ -0,0 +1,65 @@
'use client';
import * as React from 'react';
import type { ToastActionElement, ToastProps } from '@/components/ui/toast';
type ToastType = ToastProps & {
id: string;
title?: React.ReactNode;
description?: React.ReactNode;
action?: ToastActionElement;
};
const ToastContext = React.createContext<{
toasts: ToastType[];
addToast: (props: Omit<ToastType, 'id'>) => void;
removeToast: (id: string) => void;
}>({
toasts: [],
addToast: () => {},
removeToast: () => {},
});
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = React.useState<ToastType[]>([]);
const addToast = React.useCallback(
(props: Omit<ToastType, 'id'>) => {
const id = Math.random().toString(36).substring(2, 9);
setToasts((prev) => [...prev, { id, ...props }]);
// Auto-dismiss after 5 seconds
setTimeout(() => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
}, 5000);
},
[setToasts],
);
const removeToast = React.useCallback(
(id: string) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
},
[setToasts],
);
const contextValue = React.useMemo(
() => ({ toasts, addToast, removeToast }),
[toasts, addToast, removeToast],
);
return (
<ToastContext.Provider value={contextValue}>
{children}
</ToastContext.Provider>
);
}
export const useToast = () => {
const context = React.useContext(ToastContext);
if (context === undefined) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};

129
src/components/ui/toast.tsx Normal file
View file

@ -0,0 +1,129 @@
'use client';
import { Cross2Icon } from '@radix-ui/react-icons';
import * as ToastPrimitives from '@radix-ui/react-toast';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { cn } from '@/lib/utils';
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
{
variants: {
variant: {
default: 'border bg-background text-foreground',
destructive:
'destructive group border-destructive bg-destructive text-destructive-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
);
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
className,
)}
toast-close=""
{...props}
>
<Cross2Icon className="size-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn('text-sm font-semibold', className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn('text-sm opacity-90', className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export {
Toast,
ToastAction,
type ToastActionElement,
ToastClose,
ToastDescription,
type ToastProps,
ToastProvider,
ToastTitle,
ToastViewport,
};

View file

@ -0,0 +1,28 @@
'use client';
import {
Toast,
ToastClose,
ToastDescription,
ToastTitle,
} from '@/components/ui/toast';
import { useToast } from '@/components/ui/toast-context';
export function Toaster() {
const { toasts } = useToast();
return (
<>
{toasts.map(({ id, title, description, action, ...props }) => (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>}
</div>
{action}
<ToastClose />
</Toast>
))}
</>
);
}

View file

@ -0,0 +1,943 @@
'use client';
import type { ColumnDef } from '@tanstack/react-table';
import { X } from 'lucide-react';
import { useTranslations } from 'next-intl';
import React, { useMemo, useState } from 'react';
import { Button } from '@/components/ui/button';
import { DataTable } from '@/components/ui/data-table';
// Types
type TimePeriod = '7' | '30' | '90';
type ViewMode = 'developers' | 'models' | 'tasks';
// Mock data types
type Developer = {
id: string;
name: string;
email: string;
tasksStarted: number;
tasksCompleted: number;
tokensConsumed: number;
cost: number;
};
type Model = {
id: string;
name: string;
provider: string;
tasks: number;
tokensConsumed: number;
cost: number;
};
type ConversationMessage = {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: Date;
};
type Task = {
id: string;
date: Date;
developerId: string;
developerName: string;
modelId: string;
modelName: string;
tokensConsumed: number;
cost: number;
status: 'completed' | 'started' | 'failed';
conversation: ConversationMessage[];
};
type SummaryData = {
activeDevelopers: number;
tasksStarted: number;
tasksCompleted: number;
tokensConsumed: string;
costs: number;
};
type Filter = {
type: 'developer' | 'model';
id: string;
name: string;
} | null;
// Generate mock conversation data
const generateMockConversation = (taskId: string): ConversationMessage[] => {
const baseConversations: Record<string, ConversationMessage[]> = {
task1: [
{
id: 'msg1',
role: 'user',
content:
'Create a React component that displays a list of items with pagination.',
timestamp: new Date('2025-05-01T10:00:00'),
},
{
id: 'msg2',
role: 'assistant',
content:
"I'll create a React component for displaying a paginated list. Here's how we can implement it...",
timestamp: new Date('2025-05-01T10:00:30'),
},
{
id: 'msg3',
role: 'user',
content: 'Can you add sorting functionality as well?',
timestamp: new Date('2025-05-01T10:02:00'),
},
{
id: 'msg4',
role: 'assistant',
content:
"Certainly! I'll add sorting functionality to the component. Here's the updated implementation...",
timestamp: new Date('2025-05-01T10:02:30'),
},
],
task2: [
{
id: 'msg1',
role: 'user',
content:
'Write a function to calculate the Fibonacci sequence recursively.',
timestamp: new Date('2025-05-02T11:30:00'),
},
{
id: 'msg2',
role: 'assistant',
content:
"Here's a recursive function to calculate the Fibonacci sequence in JavaScript...",
timestamp: new Date('2025-05-02T11:30:30'),
},
{
id: 'msg3',
role: 'user',
content: 'Can you optimize it to avoid redundant calculations?',
timestamp: new Date('2025-05-02T11:32:00'),
},
{
id: 'msg4',
role: 'assistant',
content:
"Yes, we can optimize it using memoization to avoid redundant calculations. Here's the optimized version...",
timestamp: new Date('2025-05-02T11:32:30'),
},
],
};
// Return specific conversation if available, otherwise generate a generic one
if (baseConversations[taskId]) {
return baseConversations[taskId];
}
// Generic conversation
return [
{
id: `${taskId}-msg1`,
role: 'user',
content: 'I need help with a coding task.',
timestamp: new Date(),
},
{
id: `${taskId}-msg2`,
role: 'assistant',
content:
"I'd be happy to help! What kind of coding task are you working on?",
timestamp: new Date(Date.now() + 30000),
},
{
id: `${taskId}-msg3`,
role: 'user',
content: "I'm trying to implement a feature in my application.",
timestamp: new Date(Date.now() + 60000),
},
{
id: `${taskId}-msg4`,
role: 'assistant',
content:
'I can help with that. Let me provide some guidance on implementing that feature...',
timestamp: new Date(Date.now() + 90000),
},
];
};
// Mock data
const mockDevelopers: Developer[] = [
{
id: 'dev1',
name: 'John Doe',
email: 'john@example.com',
tasksStarted: 42,
tasksCompleted: 38,
tokensConsumed: 1200000,
cost: 24.5,
},
{
id: 'dev2',
name: 'Jane Smith',
email: 'jane@example.com',
tasksStarted: 35,
tasksCompleted: 32,
tokensConsumed: 980000,
cost: 19.6,
},
{
id: 'dev3',
name: 'Bob Johnson',
email: 'bob@example.com',
tasksStarted: 28,
tasksCompleted: 25,
tokensConsumed: 750000,
cost: 15.0,
},
{
id: 'dev4',
name: 'Alice Williams',
email: 'alice@example.com',
tasksStarted: 31,
tasksCompleted: 29,
tokensConsumed: 820000,
cost: 16.4,
},
{
id: 'dev5',
name: 'Charlie Brown',
email: 'charlie@example.com',
tasksStarted: 22,
tasksCompleted: 19,
tokensConsumed: 650000,
cost: 13.0,
},
];
const mockModels: Model[] = [
{
id: 'model1',
name: 'GPT-4',
provider: 'OpenAI',
tasks: 45,
tokensConsumed: 1500000,
cost: 30.0,
},
{
id: 'model2',
name: 'Claude 3 Opus',
provider: 'Anthropic',
tasks: 35,
tokensConsumed: 1000000,
cost: 20.0,
},
{
id: 'model3',
name: 'Mistral Large',
provider: 'Mistral AI',
tasks: 25,
tokensConsumed: 430000,
cost: 9.1,
},
{
id: 'model4',
name: 'GPT-3.5 Turbo',
provider: 'OpenAI',
tasks: 38,
tokensConsumed: 850000,
cost: 8.5,
},
{
id: 'model5',
name: 'Claude 3 Sonnet',
provider: 'Anthropic',
tasks: 30,
tokensConsumed: 720000,
cost: 14.4,
},
];
const mockTasks: Task[] = [
{
id: 'task1',
date: new Date('2025-05-01T10:00:00'),
developerId: 'dev1',
developerName: 'John Doe',
modelId: 'model1',
modelName: 'GPT-4',
tokensConsumed: 15000,
cost: 0.3,
status: 'completed',
conversation: generateMockConversation('task1'),
},
{
id: 'task2',
date: new Date('2025-05-02T11:30:00'),
developerId: 'dev2',
developerName: 'Jane Smith',
modelId: 'model2',
modelName: 'Claude 3 Opus',
tokensConsumed: 12000,
cost: 0.24,
status: 'completed',
conversation: generateMockConversation('task2'),
},
{
id: 'task3',
date: new Date('2025-05-03T14:15:00'),
developerId: 'dev3',
developerName: 'Bob Johnson',
modelId: 'model3',
modelName: 'Mistral Large',
tokensConsumed: 8000,
cost: 0.16,
status: 'completed',
conversation: generateMockConversation('task3'),
},
{
id: 'task4',
date: new Date('2025-05-04T09:45:00'),
developerId: 'dev1',
developerName: 'John Doe',
modelId: 'model2',
modelName: 'Claude 3 Opus',
tokensConsumed: 10000,
cost: 0.2,
status: 'completed',
conversation: generateMockConversation('task4'),
},
{
id: 'task5',
date: new Date('2025-05-05T16:30:00'),
developerId: 'dev2',
developerName: 'Jane Smith',
modelId: 'model1',
modelName: 'GPT-4',
tokensConsumed: 18000,
cost: 0.36,
status: 'started',
conversation: generateMockConversation('task5'),
},
{
id: 'task6',
date: new Date('2025-05-06T13:20:00'),
developerId: 'dev4',
developerName: 'Alice Williams',
modelId: 'model4',
modelName: 'GPT-3.5 Turbo',
tokensConsumed: 7500,
cost: 0.08,
status: 'completed',
conversation: generateMockConversation('task6'),
},
{
id: 'task7',
date: new Date('2025-05-07T10:10:00'),
developerId: 'dev5',
developerName: 'Charlie Brown',
modelId: 'model5',
modelName: 'Claude 3 Sonnet',
tokensConsumed: 9200,
cost: 0.18,
status: 'failed',
conversation: generateMockConversation('task7'),
},
{
id: 'task8',
date: new Date('2025-05-07T15:45:00'),
developerId: 'dev3',
developerName: 'Bob Johnson',
modelId: 'model1',
modelName: 'GPT-4',
tokensConsumed: 14000,
cost: 0.28,
status: 'completed',
conversation: generateMockConversation('task8'),
},
];
// Component for summary metrics
const SummaryMetrics = ({
data,
timePeriod,
setTimePeriod,
}: {
data: SummaryData;
timePeriod: TimePeriod;
setTimePeriod: (period: TimePeriod) => void;
}) => {
const t = useTranslations('Analytics');
return (
<div className="mb-6 rounded-md bg-card p-5">
<div className="mb-4">
<h3 className="text-lg font-semibold">{t('summary_title')}</h3>
</div>
{/* Time period toggle */}
<div className="mb-4 flex space-x-2">
<Button
variant={timePeriod === '7' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('7')}
>
{t('period_7_days')}
</Button>
<Button
variant={timePeriod === '30' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('30')}
>
{t('period_30_days')}
</Button>
<Button
variant={timePeriod === '90' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('90')}
>
{t('period_90_days')}
</Button>
</div>
{/* Metrics grid */}
<div className="grid grid-cols-2 gap-4 md:grid-cols-5">
{/* Active Developers */}
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('active_developers')}
</div>
<div className="mt-1 text-2xl font-semibold">
{data.activeDevelopers}
</div>
</div>
{/* Tasks Started */}
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('tasks_started')}
</div>
<div className="mt-1 text-2xl font-semibold">{data.tasksStarted}</div>
</div>
{/* Tasks Completed */}
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('tasks_completed')}
</div>
<div className="mt-1 text-2xl font-semibold">
{data.tasksCompleted}
</div>
</div>
{/* Tokens Consumed */}
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('tokens_consumed')}
</div>
<div className="mt-1 text-2xl font-semibold">
{data.tokensConsumed}
</div>
</div>
{/* LLM Model Costs */}
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">{t('llm_costs')}</div>
<div className="mt-1 text-2xl font-semibold">
${data.costs.toFixed(2)}
</div>
</div>
</div>
</div>
);
};
// Component for view mode toggle
const ViewModeToggle = ({
viewMode,
setViewMode,
}: {
viewMode: ViewMode;
setViewMode: (mode: ViewMode) => void;
}) => {
const t = useTranslations('Analytics');
return (
<div className="mb-4">
<h3 className="mb-2 text-sm font-medium">{t('view_mode_title')}</h3>
<div className="flex space-x-2">
<Button
variant={viewMode === 'developers' ? 'default' : 'outline'}
size="sm"
onClick={() => setViewMode('developers')}
>
{t('view_mode_developers')}
</Button>
<Button
variant={viewMode === 'models' ? 'default' : 'outline'}
size="sm"
onClick={() => setViewMode('models')}
>
{t('view_mode_models')}
</Button>
<Button
variant={viewMode === 'tasks' ? 'default' : 'outline'}
size="sm"
onClick={() => setViewMode('tasks')}
>
{t('view_mode_tasks')}
</Button>
</div>
</div>
);
};
// Component for active filter display
const ActiveFilter = ({
filter,
onClear,
}: {
filter: Filter;
onClear: () => void;
}) => {
if (!filter) {
return null;
}
return (
<div className="mb-4 flex items-center rounded-md bg-muted p-2">
<span className="mr-2 text-sm">
Filtered by {filter.type}: <strong>{filter.name}</strong>
</span>
<Button
variant="ghost"
size="sm"
onClick={onClear}
className="size-6 p-0"
>
<X className="size-4" />
</Button>
</div>
);
};
// Component for task details drawer
const TaskDetailsDrawer = ({
task,
isOpen,
onClose,
}: {
task: Task | null;
isOpen: boolean;
onClose: () => void;
}) => {
if (!isOpen || !task) {
return null;
}
return (
<div className="fixed inset-0 z-50 bg-black/50">
<div className="absolute right-0 size-full max-w-md overflow-auto bg-background p-6 shadow-lg">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-xl font-semibold">Task Details</h2>
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="size-8 p-0"
>
<X className="size-5" />
</Button>
</div>
<div className="mb-6 space-y-2">
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Task ID:</span>
<span className="font-medium">{task.id}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Date:</span>
<span className="font-medium">{task.date.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Developer:</span>
<span className="font-medium">{task.developerName}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Model:</span>
<span className="font-medium">{task.modelName}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Tokens:</span>
<span className="font-medium">
{task.tokensConsumed.toLocaleString()}
</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Cost:</span>
<span className="font-medium">${task.cost.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Status:</span>
<span
className={
task.status === 'completed'
? 'font-medium text-green-500'
: task.status === 'started'
? 'font-medium text-blue-500'
: 'font-medium text-red-500'
}
>
{task.status.charAt(0).toUpperCase() + task.status.slice(1)}
</span>
</div>
</div>
<h3 className="mb-2 text-lg font-medium">Conversation</h3>
<div className="space-y-4">
{task.conversation.map((message) => (
<div
key={message.id}
className={`rounded-lg p-3 ${
message.role === 'user'
? 'ml-4 bg-primary/10'
: message.role === 'assistant'
? 'mr-4 bg-secondary/10'
: 'bg-muted'
}`}
>
<div className="mb-1 text-xs font-medium text-muted-foreground">
{message.role.charAt(0).toUpperCase() + message.role.slice(1)}
{message.timestamp.toLocaleTimeString()}
</div>
<div className="text-sm">{message.content}</div>
</div>
))}
</div>
</div>
</div>
);
};
// Main component
export const AnalyticsPage = () => {
const [timePeriod, setTimePeriod] = useState<TimePeriod>('7');
const [viewMode, setViewMode] = useState<ViewMode>('tasks');
const [filter, setFilter] = useState<Filter>(null);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
// Generate summary data based on time period
const summaryData = useMemo<SummaryData>(() => {
// In a real implementation, this would fetch data from an API
// For now, we'll use mock data with different values for each time period
switch (timePeriod) {
case '7':
return {
activeDevelopers: 8,
tasksStarted: 42,
tasksCompleted: 38,
tokensConsumed: '1.2M',
costs: 24.5,
};
case '30':
return {
activeDevelopers: 12,
tasksStarted: 187,
tasksCompleted: 165,
tokensConsumed: '5.8M',
costs: 112.75,
};
case '90':
return {
activeDevelopers: 15,
tasksStarted: 563,
tasksCompleted: 498,
tokensConsumed: '18.3M',
costs: 347.2,
};
}
}, [timePeriod]);
// Handle developer click
const handleDeveloperClick = (developer: Developer) => {
setFilter({
type: 'developer',
id: developer.id,
name: developer.name,
});
setViewMode('tasks');
};
// Handle model click
const handleModelClick = (model: Model) => {
setFilter({
type: 'model',
id: model.id,
name: model.name,
});
setViewMode('tasks');
};
// Handle task click
const handleTaskClick = (task: Task) => {
setSelectedTask(task);
setIsDrawerOpen(true);
};
// Clear filter
const clearFilter = () => {
setFilter(null);
};
// Close drawer
const closeDrawer = () => {
setIsDrawerOpen(false);
setSelectedTask(null);
};
// Data tables for each view mode
const DevelopersTable = () => {
const developerColumns: ColumnDef<Developer>[] = [
{
accessorKey: 'name',
header: 'Developer',
cell: ({ row }) => {
const developer = row.original;
return (
<button
onClick={() => handleDeveloperClick(developer)}
className="text-left font-medium text-primary hover:underline"
>
{developer.name}
</button>
);
},
},
{
accessorKey: 'email',
header: 'Email',
},
{
accessorKey: 'tasksStarted',
header: 'Tasks Started',
},
{
accessorKey: 'tasksCompleted',
header: 'Tasks Completed',
},
{
accessorKey: 'tokensConsumed',
header: 'Tokens',
cell: ({ row }) => {
const tokens = row.getValue('tokensConsumed') as number;
return tokens >= 1000000
? `${(tokens / 1000000).toFixed(1)}M`
: tokens >= 1000
? `${(tokens / 1000).toFixed(1)}K`
: tokens;
},
},
{
accessorKey: 'cost',
header: 'Cost (USD)',
cell: ({ row }) => {
const cost = row.getValue('cost') as number;
return `$${cost.toFixed(2)}`;
},
},
];
return <DataTable columns={developerColumns} data={mockDevelopers} />;
};
const ModelsTable = () => {
const modelColumns: ColumnDef<Model>[] = [
{
accessorKey: 'name',
header: 'Model',
cell: ({ row }) => {
const model = row.original;
return (
<button
onClick={() => handleModelClick(model)}
className="text-left font-medium text-primary hover:underline"
>
{model.name}
</button>
);
},
},
{
accessorKey: 'provider',
header: 'Provider',
},
{
accessorKey: 'tasks',
header: 'Tasks',
},
{
accessorKey: 'tokensConsumed',
header: 'Tokens',
cell: ({ row }) => {
const tokens = row.getValue('tokensConsumed') as number;
return tokens >= 1000000
? `${(tokens / 1000000).toFixed(1)}M`
: tokens >= 1000
? `${(tokens / 1000).toFixed(1)}K`
: tokens;
},
},
{
accessorKey: 'cost',
header: 'Cost (USD)',
cell: ({ row }) => {
const cost = row.getValue('cost') as number;
return `$${cost.toFixed(2)}`;
},
},
];
return <DataTable columns={modelColumns} data={mockModels} />;
};
const TasksTable = () => {
// Filter tasks based on the active filter
const filteredTasks = useMemo(() => {
if (!filter) {
return mockTasks;
}
return mockTasks.filter((task) =>
filter.type === 'developer'
? task.developerId === filter.id
: task.modelId === filter.id,
);
}, []);
const taskColumns: ColumnDef<Task>[] = [
{
accessorKey: 'id',
header: 'Task ID',
cell: ({ row }) => {
const task = row.original;
return (
<button
onClick={() => handleTaskClick(task)}
className="text-left font-medium text-primary hover:underline"
>
{task.id}
</button>
);
},
},
{
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => {
const date = row.getValue('date') as Date;
return date.toLocaleString();
},
},
{
accessorKey: 'developerName',
header: 'Developer',
},
{
accessorKey: 'modelName',
header: 'Model',
},
{
accessorKey: 'tokensConsumed',
header: 'Tokens',
cell: ({ row }) => {
const tokens = row.getValue('tokensConsumed') as number;
return tokens >= 1000000
? `${(tokens / 1000000).toFixed(1)}M`
: tokens >= 1000
? `${(tokens / 1000).toFixed(1)}K`
: tokens;
},
},
{
accessorKey: 'cost',
header: 'Cost (USD)',
cell: ({ row }) => {
const cost = row.getValue('cost') as number;
return `$${cost.toFixed(2)}`;
},
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ row }) => {
const status = row.getValue('status') as string;
return (
<span
className={
status === 'completed'
? 'text-green-500'
: status === 'started'
? 'text-blue-500'
: 'text-red-500'
}
>
{status.charAt(0).toUpperCase() + status.slice(1)}
</span>
);
},
},
];
return <DataTable columns={taskColumns} data={filteredTasks} />;
};
// Render the appropriate table based on view mode
const renderTable = () => {
switch (viewMode) {
case 'developers':
return <DevelopersTable />;
case 'models':
return <ModelsTable />;
case 'tasks':
return <TasksTable />;
}
};
return (
<div>
{/* Summary metrics */}
<SummaryMetrics
data={summaryData}
timePeriod={timePeriod}
setTimePeriod={setTimePeriod}
/>
{/* View mode toggle */}
<ViewModeToggle viewMode={viewMode} setViewMode={setViewMode} />
{/* Active filter */}
{filter && <ActiveFilter filter={filter} onClear={clearFilter} />}
{/* Data table */}
<div className="rounded-md border bg-card p-5">
<div className="mb-4">
<h3 className="text-lg font-semibold">
{viewMode === 'developers'
? 'Developers'
: viewMode === 'models'
? 'Models'
: 'Tasks'}
</h3>
</div>
{renderTable()}
</div>
{/* Task details drawer */}
<TaskDetailsDrawer
task={selectedTask}
isOpen={isDrawerOpen}
onClose={closeDrawer}
/>
</div>
);
};

View file

@ -0,0 +1,72 @@
'use client';
import Link from 'next/link';
import React, { useState } from 'react';
import { Drawer } from '@/components/ui/drawer';
import { AuditLogDetails } from './AuditLogDetails';
import { AuditLogEntry } from './AuditLogEntry';
import type { AuditLog } from './mockAuditLogs';
import { mockAuditLogs } from './mockAuditLogs';
export function AuditLogCard() {
// Always show the most recent 5 entries
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
// Get the 5 most recent logs
const logs = mockAuditLogs.slice(0, 5);
const handleLogClick = (log: AuditLog) => {
setSelectedLog(log);
setIsDrawerOpen(true);
};
const handleCloseDrawer = () => {
setIsDrawerOpen(false);
};
return (
<div className="mb-6 w-2/3 rounded-md bg-card p-5">
<div className="mb-4">
<h3 className="text-lg font-semibold">Recent Activity</h3>
<p className="text-sm text-muted-foreground">
Organization audit logs and changes
</p>
</div>
{/* Log entries */}
<div className="space-y-1">
{logs.length > 0 ? (
logs.map((log: AuditLog) => (
<AuditLogEntry key={log.id} log={log} onClick={handleLogClick} />
))
) : (
<div className="py-8 text-center">
<p className="text-sm text-muted-foreground">No recent activity</p>
</div>
)}
</div>
{/* "See all logs" link */}
<div className="mt-4 text-right">
<Link
href="/dashboard/audit-logs"
className="text-sm text-primary hover:underline"
>
See all logs
</Link>
</div>
{/* Drawer for log details */}
<Drawer
isOpen={isDrawerOpen}
onClose={handleCloseDrawer}
title="Activity Details"
>
{selectedLog && <AuditLogDetails log={selectedLog} />}
</Drawer>
</div>
);
}

View file

@ -0,0 +1,98 @@
'use client';
import { ArrowRight, Calendar, Clock, User } from 'lucide-react';
import Link from 'next/link';
import React from 'react';
import type { AuditLog } from './mockAuditLogs';
type AuditLogDetailsProps = {
log: AuditLog;
};
const formatValue = (value: unknown): React.ReactNode => {
if (value === null || value === undefined) {
return <span className="italic text-muted-foreground">None</span>;
}
if (Array.isArray(value)) {
return (
<ul className="list-disc pl-5">
{value.map((item, index) => (
<li key={index}>{String(item)}</li>
))}
</ul>
);
}
if (typeof value === 'object') {
return (
<div className="space-y-1">
{Object.entries(value).map(([key, val]) => (
<div key={key} className="grid grid-cols-2 gap-2">
<span className="text-sm font-medium">{key}:</span>
<span className="text-sm">{String(val)}</span>
</div>
))}
</div>
);
}
return String(value);
};
export function AuditLogDetails({ log }: AuditLogDetailsProps) {
return (
<div className="space-y-6">
{/* Header information */}
<div className="space-y-4">
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
<Calendar className="size-4" />
<span>{log.timestamp.toLocaleDateString()}</span>
<Clock className="ml-2 size-4" />
<span>{log.timestamp.toLocaleTimeString()}</span>
</div>
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
<User className="size-4" />
<span>{log.user}</span>
</div>
{log.path && (
<Link
href={log.path}
className="mt-2 inline-flex items-center text-sm font-medium text-primary hover:underline"
>
View in settings
<ArrowRight className="ml-1 size-4" />
</Link>
)}
</div>
{/* Change details */}
<div className="rounded-md border p-4">
<h3 className="mb-4 text-sm font-medium">Changes</h3>
<div className="space-y-4">
<div className="space-y-2">
<h4 className="text-xs font-medium uppercase text-muted-foreground">
Before
</h4>
<div className="rounded-md bg-muted p-3">
{formatValue(log.details.before)}
</div>
</div>
<div className="space-y-2">
<h4 className="text-xs font-medium uppercase text-muted-foreground">
After
</h4>
<div className="rounded-md bg-muted p-3">
{formatValue(log.details.after)}
</div>
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,110 @@
'use client';
import { Settings, Sliders, Users } from 'lucide-react';
import React from 'react';
import { cn } from '@/lib/utils';
import type { AuditLog, AuditLogType } from './mockAuditLogs';
import { getFormattedTime } from './mockAuditLogs';
type AuditLogEntryProps = {
log: AuditLog;
onClick: (log: AuditLog) => void;
};
const getIconByType = (type: AuditLogType) => {
switch (type) {
case 'provider_whitelist':
return <Settings className="size-4 text-purple-500" />;
case 'default_parameters':
return <Sliders className="size-4 text-green-500" />;
case 'member_change':
return <Users className="size-4 text-amber-500" />;
default:
return <Settings className="size-4 text-gray-500" />;
}
};
export function AuditLogEntry({ log, onClick }: AuditLogEntryProps) {
return (
<button
onClick={() => onClick(log)}
className={cn(
'w-full rounded-md p-3 text-left transition-colors',
'hover:bg-muted focus:bg-muted focus:outline-none',
)}
>
<div className="flex items-start gap-3">
<div className="mt-0.5 shrink-0">{getIconByType(log.type)}</div>
<div className="flex-1 space-y-1">
<p className="text-sm text-foreground">{log.description}</p>
{/* Summary of changes */}
<div className="mt-1 text-xs text-muted-foreground">
{log.type === 'provider_whitelist' && (
<span>
{(() => {
const before = log.details.before as string[];
const after = log.details.after as string[];
if (Array.isArray(before) && Array.isArray(after)) {
if (after.length > before.length) {
const added = after.filter((p) => !before.includes(p));
return `Added: ${added.join(', ')}`;
} else {
const removed = before.filter((p) => !after.includes(p));
return `Removed: ${removed.join(', ')}`;
}
}
return '';
})()}
</span>
)}
{log.type === 'default_parameters' && (
<span>
{(() => {
const before = log.details.before as Record<string, unknown>;
const after = log.details.after as Record<string, unknown>;
if (
before &&
after &&
typeof before === 'object' &&
typeof after === 'object'
) {
return Object.entries(after)
.filter(([key, val]) => before[key] !== val)
.map(([key, val]) => `${key}: ${before[key]}${val}`)
.join(', ');
}
return '';
})()}
</span>
)}
{log.type === 'member_change' && (
<span>
{(() => {
if (!log.details.before) {
return 'New member added';
}
if (!log.details.after) {
return 'Member removed';
}
return 'Role updated';
})()}
</span>
)}
</div>
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">{log.user}</p>
<p className="text-xs text-muted-foreground">
{getFormattedTime(log.timestamp)}
</p>
</div>
</div>
</div>
</button>
);
}

View file

@ -0,0 +1,793 @@
/* eslint-disable react/no-unescaped-entities */
'use client';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Slider } from '@/components/ui/slider';
import { useToast } from '@/components/ui/toast-context';
// Default parameters form type
type DefaultParamsFormValues = {
experimentalPowerSteering: boolean;
terminalOutputLimit: number;
compressProgressBar: boolean;
inheritEnvVars: boolean;
disableShellIntegration: boolean;
shellIntegrationTimeout: number;
commandDelay: number;
enablePowerShellCounter: boolean;
clearZshEol: boolean;
enableOhMyZsh: boolean;
enablePowerlevel10k: boolean;
openTabsLimit: number;
workspaceFilesLimit: number;
showRooignoreFiles: boolean;
fileReadThreshold: number;
alwaysReadEntireFile: boolean;
enableAutoCheckpoints: boolean;
useCustomTemperature: boolean;
temperature: number;
rateLimit: number;
enableEditingThroughDiffs: boolean;
matchPrecision: number;
};
const DefaultParametersPage = () => {
const t = useTranslations('ProviderWhitelist');
const { addToast } = useToast();
const [isSaving, setIsSaving] = useState(false);
// Form for default parameters
const form = useForm<DefaultParamsFormValues>({
defaultValues: {
experimentalPowerSteering: true,
terminalOutputLimit: 500,
compressProgressBar: true,
inheritEnvVars: true,
disableShellIntegration: false,
shellIntegrationTimeout: 5,
commandDelay: 0,
enablePowerShellCounter: false,
clearZshEol: true,
enableOhMyZsh: false,
enablePowerlevel10k: false,
openTabsLimit: 20,
workspaceFilesLimit: 200,
showRooignoreFiles: true,
fileReadThreshold: 500,
alwaysReadEntireFile: false,
enableAutoCheckpoints: true,
useCustomTemperature: true,
temperature: 0,
rateLimit: 0,
enableEditingThroughDiffs: true,
matchPrecision: 100,
},
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const onSubmit = (_data: DefaultParamsFormValues) => {
setIsSaving(true);
// Simulate API call
setTimeout(() => {
// Show success toast
addToast({
title: 'Settings saved',
description: 'Default parameters have been updated successfully.',
variant: 'default',
});
setIsSaving(false);
}, 1000);
};
return (
<>
<div className="cl-header 🔒️ cl-internal-qo3qk7">
<div className="cl-internal-1pr5xvn">
<h1 className="cl-headerTitle 🔒️ cl-internal-190cjq9">
{t('parameters_section_title')}
</h1>
</div>
</div>
<p className="mb-6 text-sm text-muted-foreground">
{t('parameters_section_description')}
</p>
<div className="space-y-8">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
{/* Checkpoints Section */}
<div className="space-y-4 rounded-lg bg-muted/50 p-4">
<div className="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-5"
>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10" />
<path d="m9 12 2 2 4-4" />
</svg>
<h2 className="text-lg font-medium">Checkpoints</h2>
</div>
<div className="mt-4 space-y-4">
<FormField
control={form.control}
name="enableAutoCheckpoints"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Enable automatic checkpoints</FormLabel>
<FormDescription>
When enabled, Roo will automatically create
checkpoints during task execution, making it easy to
review changes or revert to earlier states.
</FormDescription>
</div>
</FormItem>
)}
/>
</div>
</div>
{/* Context Settings Section */}
<div className="space-y-4 rounded-lg bg-muted/50 p-4">
<div className="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-5"
>
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
<line x1="8" y1="21" x2="16" y2="21" />
<line x1="12" y1="17" x2="12" y2="21" />
</svg>
<h2 className="text-lg font-medium">Context</h2>
</div>
<p className="text-sm text-muted-foreground">
Control what information is included in the AI's context window,
affecting token usage and response quality
</p>
<div className="mt-4 space-y-6">
<FormField
control={form.control}
name="openTabsLimit"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>Open tabs context limit</FormLabel>
<span className="text-sm">{field.value}</span>
</div>
<FormControl>
<Slider
min={0}
max={50}
step={1}
value={[field.value]}
onValueChange={(value: number[]) =>
field.onChange(value[0])
}
className="w-full"
/>
</FormControl>
<FormDescription>
Maximum number of VSCode open tabs to include in
context. Higher values provide more context but increase
token usage.
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="workspaceFilesLimit"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>Workspace files context limit</FormLabel>
<span className="text-sm">{field.value}</span>
</div>
<FormControl>
<Slider
min={0}
max={500}
step={10}
value={[field.value]}
onValueChange={(value: number[]) =>
field.onChange(value[0])
}
className="w-full"
/>
</FormControl>
<FormDescription>
Maximum number of files to include in current working
directory details. Higher values provide more context
but increase token usage.
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="showRooignoreFiles"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
Show .rooignore'd files in lists and searches
</FormLabel>
<FormDescription>
When enabled, files matching patterns in .rooignore
will be shown in lists with a lock symbol. When
disabled, these files will be completely hidden from
file lists and searches.
</FormDescription>
</div>
</FormItem>
)}
/>
<div className="space-y-2">
<FormLabel>File read auto-truncate threshold</FormLabel>
<div className="flex items-center gap-2">
<FormField
control={form.control}
name="fileReadThreshold"
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input
type="number"
min={0}
max={10000}
{...field}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
className="w-32"
/>
</FormControl>
</FormItem>
)}
/>
<span>lines</span>
<FormField
control={form.control}
name="alwaysReadEntireFile"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel>Always read entire file</FormLabel>
</FormItem>
)}
/>
</div>
<FormDescription>
Roo reads this number of lines when the model omits
start/end values. If this number is less than the file's
total, Roo generates a line number index of code
definitions. Special cases: -1 instructs Roo to read the
entire file (without indexing), and 0 instructs it to read
no lines and provides line indexes only for minimal context.
Lower values minimize initial context usage, enabling
precise subsequent line-range reads. Explicit start/end
requests are not limited by this setting.
</FormDescription>
</div>
</div>
</div>
{/* Model Settings Section */}
<div className="space-y-4 rounded-lg bg-muted/50 p-4">
<div className="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-5"
>
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
<polyline points="3.29 7 12 12 20.71 7" />
<line x1="12" y1="22" x2="12" y2="12" />
</svg>
<h2 className="text-lg font-medium">Model Settings</h2>
</div>
<div className="mt-4 space-y-4">
<FormField
control={form.control}
name="enableEditingThroughDiffs"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Enable editing through diffs</FormLabel>
<FormDescription>
When enabled, Roo will be able to edit files more
quickly and will automatically reject truncated
full-file writes. Works best with the latest Claude
3.7 Sonnet model.
</FormDescription>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="matchPrecision"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>Match precision</FormLabel>
<span className="text-sm">{field.value}%</span>
</div>
<FormControl>
<Slider
min={50}
max={100}
step={1}
value={[field.value]}
onValueChange={(value: number[]) =>
field.onChange(value[0])
}
className="w-full"
/>
</FormControl>
<FormDescription>
This slider controls how precisely code sections must
match when applying diffs. Lower values allow more
flexible matching but increase the risk of incorrect
replacements. Use values below 100% with extreme
caution.
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="useCustomTemperature"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Use custom temperature</FormLabel>
<FormDescription>
Controls randomness in the model's responses.
</FormDescription>
</div>
</FormItem>
)}
/>
{form.watch('useCustomTemperature') && (
<FormField
control={form.control}
name="temperature"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>Temperature</FormLabel>
<span className="text-sm">{field.value}</span>
</div>
<FormControl>
<Slider
min={0}
max={2}
step={0.1}
value={[field.value]}
onValueChange={(value: number[]) =>
field.onChange(value[0])
}
className="w-full"
/>
</FormControl>
<FormDescription>
Higher values make output more random, lower values
make it more deterministic.
</FormDescription>
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="rateLimit"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>Rate limit</FormLabel>
<span className="text-sm">{field.value}s</span>
</div>
<FormControl>
<Slider
min={0}
max={10}
step={0.1}
value={[field.value]}
onValueChange={(value: number[]) =>
field.onChange(value[0])
}
className="w-full"
/>
</FormControl>
<FormDescription>
Minimum time between API requests.
</FormDescription>
</FormItem>
)}
/>
</div>
</div>
{/* Terminal Settings Section */}
<div className="space-y-4 rounded-lg bg-muted/50 p-4">
<div className="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-5"
>
<polyline points="4 17 10 11 4 5" />
<line x1="12" y1="19" x2="20" y2="19" />
</svg>
<h2 className="text-lg font-medium">Terminal</h2>
</div>
<div className="mt-4 space-y-6">
<h3 className="text-base font-medium">
Terminal Settings: Basic
</h3>
<FormField
control={form.control}
name="terminalOutputLimit"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>Terminal output limit</FormLabel>
<span className="text-sm">{field.value}</span>
</div>
<FormControl>
<Slider
min={100}
max={1000}
step={10}
value={[field.value]}
onValueChange={(value: number[]) =>
field.onChange(value[0])
}
className="w-full"
/>
</FormControl>
<FormDescription>
Maximum number of lines to include in terminal output
when executing commands. When exceeded lines will be
removed from the middle, saving tokens.
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="compressProgressBar"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Compress progress bar output</FormLabel>
<FormDescription>
When enabled, processes terminal output with carriage
returns (\r) to simulate how a real terminal would
display content. This removes intermediate progress
bar states, retaining only the final state, which
conserves context space for more relevant information.
</FormDescription>
</div>
</FormItem>
)}
/>
<h3 className="text-base font-medium">
Terminal Settings: Advanced
</h3>
<p className="text-sm text-muted-foreground">
The following options may require a terminal restart to apply
the setting.
</p>
<FormField
control={form.control}
name="inheritEnvVars"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Inherit environment variables</FormLabel>
<FormDescription>
When enabled, the terminal will inherit environment
variables from VSCode's parent process, such as
user-profile-defined shell integration settings. This
directly toggles VSCode global setting
'terminal.integrated.inheritEnv'
</FormDescription>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="disableShellIntegration"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
Disable terminal shell integration
</FormLabel>
<FormDescription>
Enable this if terminal commands aren't working
correctly or you see 'Shell Integration Unavailable'
errors. This uses a simpler method to run commands,
bypassing some advanced terminal features.
</FormDescription>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="shellIntegrationTimeout"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>
Terminal shell integration timeout
</FormLabel>
<span className="text-sm">{field.value}s</span>
</div>
<FormControl>
<Slider
min={1}
max={10}
step={1}
value={[field.value]}
onValueChange={(value: number[]) =>
field.onChange(value[0])
}
className="w-full"
/>
</FormControl>
<FormDescription>
Maximum time to wait for shell integration to initialize
before executing commands. For users with long shell
startup times, this value may need to be increased if
you see "Shell Integration Unavailable" errors in the
terminal.
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="commandDelay"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>Terminal command delay</FormLabel>
<span className="text-sm">
{field.value}
ms
</span>
</div>
<FormControl>
<Slider
min={0}
max={1000}
step={10}
value={[field.value]}
onValueChange={(value: number[]) =>
field.onChange(value[0])
}
className="w-full"
/>
</FormControl>
<FormDescription>
Delay in milliseconds to add after command execution.
The default setting of 0 disables the delay completely.
This can help ensure command output is fully captured in
terminals with timing issues. In most terminals it is
implemented by setting `PROMPT_COMMAND='sleep N'` and
Powershell appends `start-sleep` to the end of each
command. Originally was workaround for VSCode bug#237208
and may not be needed.
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="clearZshEol"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Clear ZSH EOL mark</FormLabel>
<FormDescription>
When enabled, clears the ZSH end-of-line mark by
setting PROMPT_EOL_MARK=''. This prevents issues with
command output interpretation when output ends with
special characters like '%'.
</FormDescription>
</div>
</FormItem>
)}
/>
</div>
</div>
{/* Experimental Section */}
<div className="space-y-4 rounded-lg bg-muted/50 p-4">
<div className="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-5"
>
<path d="M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2" />
<path d="M8.5 2h7" />
<path d="M7 16h10" />
</svg>
<h2 className="text-lg font-medium">Experimental</h2>
</div>
<div className="mt-4 space-y-4">
<FormField
control={form.control}
name="experimentalPowerSteering"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
Use experimental "power steering" mode
</FormLabel>
<FormDescription>
When enabled, Roo will remind the model about the
details of its current mode definition more
frequently. This will lead to stronger adherence to
role definitions and custom instructions, but will use
more tokens per message.
</FormDescription>
</div>
</FormItem>
)}
/>
</div>
</div>
<div className="flex justify-end space-x-4 border-t pt-4">
<Button variant="outline">Reset to Defaults</Button>
<Button type="submit" disabled={isSaving}>
{isSaving ? (
<>
<span className="mr-2">Saving...</span>
<span className="animate-spin"></span>
</>
) : (
'Save Changes'
)}
</Button>
</div>
</form>
</Form>
</div>
</>
);
};
export default DefaultParametersPage;

View file

@ -0,0 +1,216 @@
'use client';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
// Mock data for providers and models
const initialProviders = [
{
id: 'openai',
name: 'OpenAI',
enabled: true,
models: [
{ id: 'gpt-4', name: 'GPT-4', enabled: true },
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo', enabled: true },
{ id: 'gpt-4o', name: 'GPT-4o', enabled: false },
],
},
{
id: 'anthropic',
name: 'Anthropic',
enabled: true,
models: [
{ id: 'claude-3-opus', name: 'Claude 3 Opus', enabled: true },
{ id: 'claude-3-sonnet', name: 'Claude 3 Sonnet', enabled: true },
{ id: 'claude-3-haiku', name: 'Claude 3 Haiku', enabled: false },
],
},
{
id: 'mistral',
name: 'Mistral AI',
enabled: false,
models: [
{ id: 'mistral-large', name: 'Mistral Large', enabled: false },
{ id: 'mistral-medium', name: 'Mistral Medium', enabled: false },
{ id: 'mistral-small', name: 'Mistral Small', enabled: false },
],
},
{
id: 'cohere',
name: 'Cohere',
enabled: false,
models: [
{ id: 'command-r', name: 'Command R', enabled: false },
{ id: 'command-r-plus', name: 'Command R+', enabled: false },
],
},
];
const ProviderWhitelistPage = () => {
const t = useTranslations('ProviderWhitelist');
// State for providers and models
const [providers, setProviders] = useState(initialProviders);
const [policyVersion] = useState(1);
const [allowAllProviders, setAllowAllProviders] = useState(true);
// Toggle allow all providers
const toggleAllowAllProviders = () => {
const newAllowAllProviders = !allowAllProviders;
setAllowAllProviders(newAllowAllProviders);
if (newAllowAllProviders) {
// Enable all providers and their models
setProviders(
providers.map((provider) => ({
...provider,
enabled: true,
models: provider.models.map((model) => ({
...model,
enabled: true,
})),
})),
);
}
};
// Toggle provider enabled state
const toggleProvider = (providerId: string) => {
if (allowAllProviders) {
return;
}
setProviders(
providers.map((provider) => {
if (provider.id === providerId) {
const newEnabled = !provider.enabled;
return {
...provider,
enabled: newEnabled,
// If provider is disabled, disable all its models
models: provider.models.map((model) => ({
...model,
enabled: newEnabled ? model.enabled : false,
})),
};
}
return provider;
}),
);
};
// Toggle model enabled state
const toggleModel = (providerId: string, modelId: string) => {
if (allowAllProviders) {
return;
}
setProviders(
providers.map((provider) => {
if (provider.id === providerId) {
return {
...provider,
models: provider.models.map((model) => {
if (model.id === modelId) {
return { ...model, enabled: !model.enabled };
}
return model;
}),
};
}
return provider;
}),
);
};
return (
<>
<div className="cl-header 🔒️ cl-internal-qo3qk7">
<div className="cl-internal-1pr5xvn">
<h1 className="cl-headerTitle 🔒️ cl-internal-190cjq9">
{t('providers_section_title')}
</h1>
</div>
</div>
<p className="mb-6 text-sm text-muted-foreground">
{t('providers_section_description')}
</p>
<div className="space-y-8">
<div className="space-y-6">
<div className="mb-4 flex items-center space-x-2">
<Checkbox
id="allow-all-providers"
checked={allowAllProviders}
onCheckedChange={toggleAllowAllProviders}
/>
<Label
htmlFor="allow-all-providers"
className="text-sm font-medium"
>
Allow All Providers
</Label>
</div>
<div className={allowAllProviders ? 'opacity-50' : ''}>
{providers.map((provider) => (
<div key={provider.id} className="mb-4">
<div className="mb-2 flex items-center space-x-2">
<Checkbox
id={`provider-${provider.id}`}
checked={provider.enabled}
disabled={allowAllProviders}
onCheckedChange={() => toggleProvider(provider.id)}
/>
<Label
htmlFor={`provider-${provider.id}`}
className="text-sm font-medium"
>
{provider.name}
</Label>
</div>
<div className="ml-6 space-y-2">
{provider.models.map((model) => (
<div key={model.id} className="flex items-center space-x-2">
<Checkbox
id={`model-${model.id}`}
checked={allowAllProviders || model.enabled}
disabled={allowAllProviders || !provider.enabled}
onCheckedChange={() =>
toggleModel(provider.id, model.id)
}
/>
<Label
htmlFor={`model-${model.id}`}
className={`text-xs ${allowAllProviders || !provider.enabled ? 'text-muted-foreground' : ''}`}
>
{model.name}
</Label>
</div>
))}
</div>
</div>
))}
</div>
<div className="flex items-center space-x-2 pt-2">
<Badge variant="outline" className="text-xs">
{`Policy v${policyVersion}`}
</Badge>
<span className="text-xs text-muted-foreground">
Changes will be pushed to SSE stream within 30 seconds
</span>
</div>
</div>
</div>
</>
);
};
export default ProviderWhitelistPage;

View file

@ -0,0 +1,152 @@
'use client';
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import React, { useMemo, useState } from 'react';
import { Button } from '@/components/ui/button';
type TimePeriod = '7' | '30' | '90';
type AnalyticsData = {
tasksStarted: number;
tasksCompleted: number;
tokensConsumed: string;
costs: number;
activeDevelopers: number;
};
export const UsageAnalyticsCard = () => {
const t = useTranslations('DashboardIndex');
const [timePeriod, setTimePeriod] = useState<TimePeriod>('7');
// Mock data based on selected time period
const analyticsData = useMemo<AnalyticsData>(() => {
// Return different data based on timePeriod
switch (timePeriod) {
case '7':
return {
tasksStarted: 42,
tasksCompleted: 38,
tokensConsumed: '1.2M',
costs: 24.5,
activeDevelopers: 8,
};
case '30':
return {
tasksStarted: 187,
tasksCompleted: 165,
tokensConsumed: '5.8M',
costs: 112.75,
activeDevelopers: 12,
};
case '90':
return {
tasksStarted: 563,
tasksCompleted: 498,
tokensConsumed: '18.3M',
costs: 347.2,
activeDevelopers: 15,
};
}
}, [timePeriod]);
return (
<div className="mb-6 rounded-md bg-card p-5">
<div className="mb-4">
<h3 className="text-lg font-semibold">{t('analytics_title')}</h3>
<p className="text-sm text-muted-foreground">
{t('analytics_description')}
</p>
</div>
{/* Time period toggle */}
<div className="mb-4 flex space-x-2">
<Button
variant={timePeriod === '7' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('7')}
>
{t('analytics_period_7_days')}
</Button>
<Button
variant={timePeriod === '30' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('30')}
>
{t('analytics_period_30_days')}
</Button>
<Button
variant={timePeriod === '90' ? 'default' : 'outline'}
size="sm"
onClick={() => setTimePeriod('90')}
>
{t('analytics_period_90_days')}
</Button>
</div>
{/* Metrics grid */}
<div className="grid grid-cols-2 gap-4 md:grid-cols-5">
{/* Active Developers */}
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('analytics_active_developers')}
</div>
<div className="mt-1 text-2xl font-semibold">
{analyticsData.activeDevelopers}
</div>
</div>
{/* Tasks Started */}
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('analytics_tasks_started')}
</div>
<div className="mt-1 text-2xl font-semibold">
{analyticsData.tasksStarted}
</div>
</div>
{/* Tasks Completed */}
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('analytics_tasks_completed')}
</div>
<div className="mt-1 text-2xl font-semibold">
{analyticsData.tasksCompleted}
</div>
</div>
{/* Tokens Consumed */}
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('analytics_tokens_consumed')}
</div>
<div className="mt-1 text-2xl font-semibold">
{analyticsData.tokensConsumed}
</div>
</div>
{/* LLM Model Costs */}
<div className="rounded-lg bg-background p-3">
<div className="text-xs text-muted-foreground">
{t('analytics_llm_costs')}
</div>
<div className="mt-1 text-2xl font-semibold">
${analyticsData.costs.toFixed(2)}
</div>
</div>
</div>
{/* Link to detailed analytics */}
<div className="mt-4 text-right">
<Link
href="/dashboard/analytics"
className="text-sm text-primary hover:underline"
>
{t('analytics_view_details')}
</Link>
</div>
</div>
);
};

View file

@ -0,0 +1,144 @@
export type AuditLogType =
| 'provider_whitelist'
| 'default_parameters'
| 'member_change';
export type AuditLogDetails = {
before: unknown;
after: unknown;
};
export type AuditLog = {
id: string;
type: AuditLogType;
description: string;
timestamp: Date;
user: string;
details: AuditLogDetails;
path?: string;
};
export const mockAuditLogs: AuditLog[] = [
{
id: '1',
type: 'provider_whitelist',
description: 'Added OpenAI to provider whitelist',
timestamp: new Date(2025, 4, 10, 14, 30),
user: 'John Doe',
details: {
before: ['Anthropic', 'Cohere'],
after: ['Anthropic', 'Cohere', 'OpenAI'],
},
path: '/dashboard/organization-profile/provider-whitelist',
},
{
id: '2',
type: 'default_parameters',
description: 'Updated default temperature parameter',
timestamp: new Date(2025, 4, 9, 11, 15),
user: 'Jane Smith',
details: {
before: { temperature: 0.7 },
after: { temperature: 0.9 },
},
path: '/dashboard/organization-profile/default-parameters',
},
{
id: '3',
type: 'member_change',
description: 'Changed role for Alex Johnson from Member to Admin',
timestamp: new Date(2025, 4, 8, 9, 45),
user: 'Sarah Williams',
details: {
before: { role: 'Member' },
after: { role: 'Admin' },
},
path: '/dashboard/organization-profile/organization-members',
},
{
id: '4',
type: 'provider_whitelist',
description: 'Removed Claude from provider whitelist',
timestamp: new Date(2025, 4, 7, 16, 20),
user: 'Michael Brown',
details: {
before: ['OpenAI', 'Claude', 'Cohere'],
after: ['OpenAI', 'Cohere'],
},
path: '/dashboard/organization-profile/provider-whitelist',
},
{
id: '5',
type: 'default_parameters',
description: 'Updated max tokens parameter',
timestamp: new Date(2025, 4, 6, 13, 10),
user: 'Emily Davis',
details: {
before: { max_tokens: 1000 },
after: { max_tokens: 2000 },
},
path: '/dashboard/organization-profile/default-parameters',
},
{
id: '6',
type: 'member_change',
description: 'Added new member David Wilson',
timestamp: new Date(2025, 4, 5, 10, 30),
user: 'John Doe',
details: {
before: null,
after: {
name: 'David Wilson',
email: 'david@example.com',
role: 'Member',
},
},
path: '/dashboard/organization-profile/organization-members',
},
{
id: '7',
type: 'member_change',
description: 'Removed member Lisa Taylor',
timestamp: new Date(2025, 4, 4, 15, 45),
user: 'Sarah Williams',
details: {
before: {
name: 'Lisa Taylor',
email: 'lisa@example.com',
role: 'Member',
},
after: null,
},
path: '/dashboard/organization-profile/organization-members',
},
];
export const getFilteredLogs = (days: number): AuditLog[] => {
const now = new Date();
const cutoff = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
return mockAuditLogs.filter((log) => log.timestamp >= cutoff);
};
export const getFormattedTime = (date: Date): string => {
const now = new Date();
const diffInHours = Math.floor(
(now.getTime() - date.getTime()) / (1000 * 60 * 60),
);
if (diffInHours < 24) {
return diffInHours === 0
? 'Just now'
: diffInHours === 1
? '1 hour ago'
: `${diffInHours} hours ago`;
}
const diffInDays = Math.floor(diffInHours / 24);
if (diffInDays < 7) {
return diffInDays === 1 ? 'Yesterday' : `${diffInDays} days ago`;
}
return date.toLocaleDateString();
};

View file

@ -13,8 +13,14 @@
"company": "Company"
},
"Hero": {
"follow_twitter": "Follow @Ixartz on Twitter",
"title": "The perfect <important>SaaS template</important> to build and scale your business with ease.",
"description": "A free and open-source landing page template for your SaaS business, built with React, TypeScript, Shadcn UI, and Tailwind CSS."
"description": "A free and open-source landing page template for your SaaS business, built with React, TypeScript, Shadcn UI, and Tailwind CSS.",
"primary_button": "Get Started",
"secondary_button": "Star on GitHub"
},
"Sponsors": {
"title": "Sponsored by"
},
"Features": {
"section_subtitle": "Features",
@ -28,6 +34,37 @@
"feature6_title": "ESLint",
"feature_description": "A free and open-source landing page template for your SaaS business, built with React, TypeScript, Shadcn UI, and Tailwind CSS."
},
"Pricing": {
"section_subtitle": "Features",
"section_title": "Unlock the Full Potential of the SaaS Template",
"section_description": "A free and open-source landing page template for your SaaS business, built with React, TypeScript, Shadcn UI, and Tailwind CSS.",
"button_text": "Get Started"
},
"PricingPlan": {
"free_plan_name": "Free",
"premium_plan_name": "Premium",
"enterprise_plan_name": "Enterprise",
"free_plan_description": "For individuals",
"premium_plan_description": "For small teams",
"enterprise_plan_description": "For industry leaders",
"feature_team_member": "{number} Team Members",
"feature_website": "{number} Websites",
"feature_storage": "{number} GB Storage",
"feature_transfer": "{number} TB Transfer",
"feature_email_support": "Email Support",
"plan_interval_month": "month",
"plan_interval_year": "year",
"next_renew_date": "Your subscription renews on {date}"
},
"FAQ": {
"question": "Lorem ipsum dolor sit amet, consectetur adipiscing elit?",
"answer": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam aliquam enim dui, id consequat turpis ullamcorper ac. Mauris id quam dolor. Nullam eu egestas turpis. Proin risus elit, sollicitudin in mi a, accumsan euismod turpis. In euismod mi sed diam tristique hendrerit."
},
"CTA": {
"title": "You are ready?",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"button_text": "Star on GitHub"
},
"Footer": {
"product": "Product",
"docs": "Docs",
@ -35,7 +72,11 @@
"community": "Community",
"company": "Company",
"terms_of_service": "Terms Of Service",
"privacy_policy": "Privacy Policy"
"privacy_policy": "Privacy Policy",
"designed_by": "Designed by <author></author>."
},
"ProtectFallback": {
"not_enough_permission": "You do not have the permissions to perform this action"
},
"SignIn": {
"meta_title": "Sign in",
@ -47,10 +88,13 @@
},
"DashboardLayout": {
"home": "Home",
"analytics": "Analytics",
"todos": "Todos",
"members": "Members",
"audit_logs": "Audit Logs",
"billing": "Billing",
"settings": "Settings"
"settings": "Settings",
"provider_whitelist": "Provider Whitelist"
},
"Dashboard": {
"meta_title": "SaaS Template Dashboard",
@ -59,7 +103,22 @@
"DashboardIndex": {
"title_bar": "Dashboard",
"title_bar_description": "Welcome to your dashboard",
"message_state_title": "Let's get started"
"message_state_title": "Let's get started",
"message_state_description": "You can customize this page by editing the file at <code>dashboard/page.tsx</code>",
"message_state_button": "Star on GitHub",
"message_state_alternative": "Want more features using the same stack? Try <url></url>.",
"analytics_title": "Usage Analytics",
"analytics_description": "Organization usage metrics and statistics",
"analytics_period_7_days": "Last 7 days",
"analytics_period_30_days": "Last 30 days",
"analytics_period_90_days": "Last 90 days",
"analytics_tasks_started": "Tasks Started",
"analytics_tasks_completed": "Tasks Completed",
"analytics_tokens_consumed": "Tokens Consumed",
"analytics_costs": "Costs (USD)",
"analytics_llm_costs": "LLM Model Costs (USD)",
"analytics_active_developers": "Active Developers",
"analytics_view_details": "View detailed analytics"
},
"UserProfile": {
"title_bar": "User Profile",
@ -67,6 +126,95 @@
},
"OrganizationProfile": {
"title_bar": "Organization Management",
"title_bar_description": "Manage your organization"
"title_bar_description": "Manage your organization",
"provider_whitelist": "Provider Whitelist",
"default_parameters": "Default Parameters"
},
"ProviderWhitelist": {
"title": "Provider Whitelist & Default Parameters",
"description": "Control which AI providers are allowed and set default parameters for your organization",
"providers_section_title": "Allowed Providers",
"providers_section_description": "Select which AI providers and models are allowed to be used",
"parameters_section_title": "Default Parameters",
"parameters_section_description": "Set global default parameters for AI model calls"
},
"Billing": {
"title_bar": "Billing",
"title_bar_description": "Manage your billing and subscription",
"current_section_title": "Current Plan",
"current_section_description": "Adjust your payment plan to best suit your requirements",
"manage_subscription_button": "Manage Subscription"
},
"BillingOptions": {
"current_plan": "Current Plan",
"upgrade_plan": "Get Started"
},
"CheckoutConfirmation": {
"title_bar": "Payment Confirmation",
"message_state_title": "Payment successful",
"message_state_description": "Your payment has been successfully processed. Thank you for your purchase!",
"message_state_button": "Go back to Billing"
},
"DataTable": {
"no_results": "No results."
},
"Todos": {
"title_bar": "Todo List",
"title_bar_description": "View and manage your todo list",
"add_todo_button": "New todo"
},
"TodoTableColumns": {
"open_menu": "Open menu",
"edit": "Edit",
"delete": "Delete",
"title_header": "Title",
"message_header": "Message",
"created_at_header": "Created at"
},
"AddTodo": {
"title_bar": "Add Todo",
"add_todo_section_title": "Create a new todo",
"add_todo_section_description": "Fill in the form below to create a new todo"
},
"EditTodo": {
"title_bar": "Edit todo",
"edit_todo_section_title": "Modify todo",
"edit_todo_section_description": "Fill in the form below to edit the todo"
},
"TodoForm": {
"title_label": "Title",
"title_description": "Enter a descriptive title for your todo.",
"message_title": "Message",
"message_description": "Enter a detailed message for your todo.",
"submit_button": "Submit"
},
"Analytics": {
"title_bar": "Usage Analytics",
"title_bar_description": "Detailed analytics for your organization",
"summary_title": "Usage Summary",
"period_7_days": "Last 7 days",
"period_30_days": "Last 30 days",
"period_90_days": "Last 90 days",
"active_developers": "Active Developers",
"tasks_started": "Tasks Started",
"tasks_completed": "Tasks Completed",
"tokens_consumed": "Tokens Consumed",
"llm_costs": "LLM Model Costs (USD)",
"view_mode_title": "View By",
"view_mode_developers": "Developers",
"view_mode_models": "Models",
"view_mode_tasks": "Tasks",
"filter_title": "Filters",
"filter_developer": "Developer",
"filter_model": "Model",
"filter_clear_all": "Clear All",
"task_details_title": "Task Details",
"task_details_close": "Close",
"pagination_prev": "Previous",
"pagination_next": "Next",
"pagination_of": "of",
"pagination_page_size": "Page size",
"no_data": "No data available",
"loading": "Loading data..."
}
}

View file

@ -13,8 +13,14 @@
"company": "Entreprise"
},
"Hero": {
"follow_twitter": "Suivez @Ixartz sur Twitter",
"title": "Le parfait <important>SaaS template</important> pour construire et mettre à l'échelle votre entreprise en toute simplicité.",
"description": "Un template gratuit et open-source de landing page pour votre entreprise SaaS, construit avec React, TypeScript, Shadcn UI et Tailwind CSS."
"description": "Un template gratuit et open-source de landing page pour votre entreprise SaaS, construit avec React, TypeScript, Shadcn UI et Tailwind CSS.",
"primary_button": "Démarrer",
"secondary_button": "Mettez une étoile sur GitHub"
},
"Sponsors": {
"title": "Sponsorisé par"
},
"Features": {
"section_subtitle": "Fonctionnalités",
@ -28,6 +34,37 @@
"feature6_title": "ESLint",
"feature_description": "Un template gratuit et open-source de landing page pour votre entreprise SaaS, construit avec React, TypeScript, Shadcn UI et Tailwind CSS."
},
"Pricing": {
"section_subtitle": "Fonctionnalités",
"section_title": "Débloquer le plein potentiel du SaaS Template",
"section_description": "Un template gratuit et open-source de landing page pour votre entreprise SaaS, construit avec React, TypeScript, Shadcn UI et Tailwind CSS.",
"button_text": "Démarrer"
},
"PricingPlan": {
"free_plan_name": "Gratuit",
"premium_plan_name": "Premium",
"enterprise_plan_name": "Enterprise",
"free_plan_description": "Pour les particuliers",
"premium_plan_description": "Pour les petites équipes",
"enterprise_plan_description": "Pour les leaders de l'industrie",
"feature_team_member": "{number} membres",
"feature_website": "{number} sites internet",
"feature_storage": "{number} Go de stockage",
"feature_transfer": "{number} TB de transfert",
"feature_email_support": "Support par e-mail",
"plan_interval_month": "mois",
"plan_interval_year": "année",
"next_renew_date": "Votre abonnement sera renouvelé le {date}"
},
"FAQ": {
"question": "Lorem ipsum dolor sit amet, consectetur adipiscing elit?",
"answer": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam aliquam enim dui, id consequat turpis ullamcorper ac. Mauris id quam dolor. Nullam eu egestas turpis. Proin risus elit, sollicitudin in mi a, accumsan euismod turpis. In euismod mi sed diam tristique hendrerit."
},
"CTA": {
"title": "Vous êtes prêt?",
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"button_text": "Mettez une étoile sur GitHub"
},
"Footer": {
"product": "Produit",
"docs": "Docs",
@ -35,7 +72,11 @@
"community": "Communauté",
"company": "Entreprise",
"terms_of_service": "Conditions d'utilisation",
"privacy_policy": "Politique de confidentialité"
"privacy_policy": "Politique de confidentialité",
"designed_by": "Designé by <author></author>."
},
"ProtectFallback": {
"not_enough_permission": "Vous n'avez pas les permissions pour effectuer cette action"
},
"SignIn": {
"meta_title": "Se connecter",
@ -47,6 +88,7 @@
},
"DashboardLayout": {
"home": "Accueil",
"analytics": "Analytiques",
"todos": "Todos",
"members": "Membres",
"billing": "Facturation",
@ -59,7 +101,22 @@
"DashboardIndex": {
"title_bar": "Tableau de bord",
"title_bar_description": "Bienvenue sur votre tableau de bord",
"message_state_title": "C'est parti"
"message_state_title": "C'est parti",
"message_state_description": "Vous pouvez personnaliser cette page en modifiant le fichier dans <code>dashboard/page.tsx</code>",
"message_state_button": "Mettez une étoile sur GitHub",
"message_state_alternative": "Vous voulez plus de fonctionnalités en utilisant la même stack ? Essayez <url></url>.",
"analytics_title": "Analytique d'Utilisation",
"analytics_description": "Métriques et statistiques d'utilisation de l'organisation",
"analytics_period_7_days": "7 derniers jours",
"analytics_period_30_days": "30 derniers jours",
"analytics_period_90_days": "90 derniers jours",
"analytics_tasks_started": "Tâches Commencées",
"analytics_tasks_completed": "Tâches Terminées",
"analytics_tokens_consumed": "Tokens Consommés",
"analytics_costs": "Coûts (USD)",
"analytics_llm_costs": "Coûts des Modèles LLM (USD)",
"analytics_active_developers": "Développeurs Actifs",
"analytics_view_details": "Voir les analytiques détaillées"
},
"UserProfile": {
"title_bar": "Profil utilisateur",
@ -68,5 +125,84 @@
"OrganizationProfile": {
"title_bar": "Gestion de lorganisation",
"title_bar_description": "Gérer votre organisation"
},
"Billing": {
"title_bar": "Facturation",
"title_bar_description": "Gérer votre facturation et votre abonnement",
"current_section_title": "Plan actuel",
"current_section_description": "Ajuster votre plan de paiement pour le mieux répondre à vos besoins",
"manage_subscription_button": "Gérer l'abonnement"
},
"BillingOptions": {
"current_plan": "Plan actuel",
"upgrade_plan": "Démarrer"
},
"CheckoutConfirmation": {
"title_bar": "Confirmation du paiement",
"message_state_title": "Paiement accepté",
"message_state_description": "Votre paiement a été traité avec succès. Merci pour votre achat !",
"message_state_button": "Revenir à la facturation"
},
"DataTable": {
"no_results": "Aucun résultat."
},
"Todos": {
"title_bar": "Liste de Todos",
"title_bar_description": "Afficher et gérer votre liste de todos",
"add_todo_button": "Nouveau todo"
},
"TodoTableColumns": {
"open_menu": "Ouvrir le menu",
"edit": "Modifier",
"delete": "Supprimer",
"title_header": "Titre",
"message_header": "Message",
"created_at_header": "Créé le"
},
"AddTodo": {
"title_bar": "Ajouter une todo",
"add_todo_section_title": "Créer une nouvelle todo",
"add_todo_section_description": "Remplissez le formulaire ci-dessous pour créer une nouvelle todo"
},
"EditTodo": {
"title_bar": "Editer le todo",
"edit_todo_section_title": "Modifier le todo",
"edit_todo_section_description": "Remplissez le formulaire ci-dessous pour modifier la todo"
},
"TodoForm": {
"title_label": "Titre",
"title_description": "Entrez un titre descriptif pour votre todo.",
"message_title": "Message",
"message_description": "Entrez un message détaillé pour votre todo.",
"submit_button": "Envoyer"
},
"Analytics": {
"title_bar": "Analytique d'Utilisation",
"title_bar_description": "Analytiques détaillées pour votre organisation",
"summary_title": "Résumé d'Utilisation",
"period_7_days": "7 derniers jours",
"period_30_days": "30 derniers jours",
"period_90_days": "90 derniers jours",
"active_developers": "Développeurs Actifs",
"tasks_started": "Tâches Commencées",
"tasks_completed": "Tâches Terminées",
"tokens_consumed": "Tokens Consommés",
"llm_costs": "Coûts des Modèles LLM (USD)",
"view_mode_title": "Afficher Par",
"view_mode_developers": "Développeurs",
"view_mode_models": "Modèles",
"view_mode_tasks": "Tâches",
"filter_title": "Filtres",
"filter_developer": "Développeur",
"filter_model": "Modèle",
"filter_clear_all": "Effacer Tout",
"task_details_title": "Détails de la Tâche",
"task_details_close": "Fermer",
"pagination_prev": "Précédent",
"pagination_next": "Suivant",
"pagination_of": "de",
"pagination_page_size": "Taille de page",
"no_data": "Aucune donnée disponible",
"loading": "Chargement des données..."
}
}