Merge pull request #6 from RooCodeInc/cte/mock
2
.gitignore
vendored
|
|
@ -53,5 +53,3 @@ local
|
|||
# docker
|
||||
.docker/*
|
||||
!.docker/scripts
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
2971
public/Roo-Code-Logo-Horiz-blk.svg
Normal file
|
After Width: | Height: | Size: 226 KiB |
2965
public/Roo-Code-Logo-Horiz-white.svg
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/apple-touch-icon.png
Executable file → Normal file
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 3.9 KiB |
BIN
public/favicon-16x16.png
Executable file → Normal file
|
Before Width: | Height: | Size: 445 B After Width: | Height: | Size: 322 B |
BIN
public/favicon-32x32.png
Executable file → Normal file
|
Before Width: | Height: | Size: 1,001 B After Width: | Height: | Size: 600 B |
BIN
public/favicon.ico
Executable file → Normal file
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
1
public/placeholder.svg
Normal 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
|
After Width: | Height: | Size: 63 KiB |
23
src/app/(authenticated)/analytics/page.tsx
Normal 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;
|
||||
98
src/app/(authenticated)/audit-logs/page.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
46
src/components/ui/badge.tsx
Normal 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 };
|
||||
30
src/components/ui/checkbox.tsx
Normal 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 };
|
||||
80
src/components/ui/data-table.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
109
src/components/ui/drawer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
46
src/components/ui/drawerVariants.ts
Normal 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
|
|
@ -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,
|
||||
};
|
||||
21
src/components/ui/input.tsx
Normal 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 };
|
||||
24
src/components/ui/label.tsx
Normal 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 };
|
||||
28
src/components/ui/slider.tsx
Normal 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
|
|
@ -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,
|
||||
};
|
||||
65
src/components/ui/toast-context.tsx
Normal 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
|
|
@ -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,
|
||||
};
|
||||
28
src/components/ui/toaster.tsx
Normal 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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
943
src/features/analytics/AnalyticsPage.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
72
src/features/dashboard/AuditLogCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
98
src/features/dashboard/AuditLogDetails.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
110
src/features/dashboard/AuditLogEntry.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
793
src/features/dashboard/DefaultParametersPage.tsx
Normal 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;
|
||||
216
src/features/dashboard/ProviderWhitelistPage.tsx
Normal 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;
|
||||
152
src/features/dashboard/UsageAnalyticsCard.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
144
src/features/dashboard/mockAuditLogs.ts
Normal 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();
|
||||
};
|
||||
|
|
@ -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..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 l’organisation",
|
||||
"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..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||