Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebabf0e100 | ||
|
|
e05d4693d2 | ||
|
|
77d49adc00 | ||
|
|
7dc45f0074 | ||
|
|
44d1da3c9c | ||
|
|
564ba4bd68 | ||
|
|
63df788908 | ||
|
|
fb463cbf98 | ||
|
|
cfd99efd48 | ||
|
|
3b2c5f8bf3 | ||
|
|
d08a276b67 | ||
|
|
23bc2cd39c | ||
|
|
a31eda8c68 | ||
|
|
d044ae51d1 | ||
|
|
552578afc3 | ||
|
|
00997db325 | ||
|
|
550464cf0b | ||
|
|
e43568a3d2 | ||
|
|
4869e8a093 | ||
|
|
d9b307ec87 | ||
|
|
3abfbef4c3 | ||
|
|
86aa1eb34f | ||
|
|
59edc9851a | ||
|
|
0e31dcef99 | ||
|
|
a67235ab8a | ||
|
|
eb88c9bdf8 | ||
|
|
77e4791cbe | ||
|
|
5d922dfae7 | ||
|
|
c704c84451 | ||
|
|
6f23488438 | ||
|
|
ca50abb290 | ||
|
|
15067114a2 | ||
|
|
645f14535b | ||
|
|
250a5b03d2 | ||
|
|
5ee060dfc6 | ||
|
|
f08502cde4 | ||
|
|
90ecb88249 | ||
|
|
16e60f74da | ||
|
|
0ab252cc7c | ||
|
|
ebadbaf30d | ||
|
|
7ce6b6817e | ||
|
|
6d4040d5ed | ||
|
|
4a7a4c0add | ||
|
|
883e53d8b5 | ||
|
|
a9e6b9ae5c | ||
|
|
a61ee1f29f | ||
|
|
eefdf60e31 | ||
|
|
19a633a756 | ||
|
|
e84c1a3ab8 | ||
|
|
4d41aa7e53 | ||
|
|
a4c28cc324 | ||
|
|
187f2e7b1d | ||
|
|
b355825295 | ||
|
|
33dc18356a | ||
|
|
c804d90f6a | ||
|
|
09aa547de1 | ||
|
|
57839eae5c | ||
|
|
a43f5f38a1 | ||
|
|
8220d07057 | ||
|
|
89ffe20363 | ||
|
|
9a3adf54b1 | ||
|
|
459129118e | ||
|
|
0862370ad6 | ||
|
|
1bddde99f0 | ||
|
|
fe2e7681da | ||
|
|
d209766f01 | ||
|
|
82bfe03dba | ||
|
|
15fc8e4c3c | ||
|
|
374dbf9716 | ||
|
|
6819988f90 | ||
|
|
e3079afb2f | ||
|
|
3b6689f44b | ||
|
|
fdf417e961 | ||
|
|
a751f1f0f1 | ||
|
|
5bf348b55e | ||
|
|
4ffadc8124 | ||
|
|
8069fb7ddd | ||
|
|
133fc90718 | ||
|
|
32a2a7956d | ||
|
|
01cc9832c1 | ||
|
|
6127ea6442 | ||
|
|
22fbc2a89d | ||
|
|
1ed2e230e0 | ||
|
|
6563716f21 | ||
|
|
487e0a77d6 | ||
|
|
c084d42b4a | ||
|
|
2d8cc2baa3 | ||
|
|
47630f7c5f | ||
|
|
082dfaf339 | ||
|
|
f8efbd51da | ||
|
|
22d98256ce | ||
|
|
00caa680f2 | ||
|
|
48d35eb637 | ||
|
|
61786fedfb | ||
|
|
1f3e3603be | ||
|
|
cee2c797c4 | ||
|
|
35668469f0 | ||
|
|
3cfe7df03e | ||
|
|
3301475810 | ||
|
|
04f703dec0 | ||
|
|
bc064bd65b | ||
|
|
917bb8931e | ||
|
|
150651334c | ||
|
|
858a72aaa1 | ||
|
|
22134f3917 | ||
|
|
62a0d87d77 | ||
|
|
e158039639 | ||
|
|
f62752dfa7 | ||
|
|
2214d7d0ee | ||
|
|
0c8a7e97ba | ||
|
|
ee39eef953 | ||
|
|
8d10e66744 | ||
|
|
92900672c8 | ||
|
|
e59808c1a1 | ||
|
|
704ea7b6ca | ||
|
|
8aac4dbccf | ||
|
|
78676b48a9 | ||
|
|
0077058241 | ||
|
|
343b4b9ff1 | ||
|
|
b09bc03837 | ||
|
|
b83e285a87 | ||
|
|
40fcede095 | ||
|
|
1ef3c7d328 | ||
|
|
30ebbf96f7 | ||
|
|
2c6a3c31c1 | ||
|
|
4a4fa811a2 | ||
|
|
381ef1d690 | ||
|
|
47686034a1 | ||
|
|
7258cfdba5 | ||
|
|
87611fc4f8 | ||
|
|
3886821091 | ||
|
|
523ec0dd1f | ||
|
|
071becb796 | ||
|
|
2fe920bbba | ||
|
|
509dc92539 | ||
|
|
1116783587 | ||
|
|
5b3e8bd9d1 | ||
|
|
daa3292814 | ||
|
|
6abea1a028 | ||
|
|
660a238731 | ||
|
|
1c24604228 | ||
|
|
ba50a6f45d | ||
|
|
b65b055630 | ||
|
|
c7c72efeac | ||
|
|
6489d2f27e | ||
|
|
2d8a239d6d | ||
|
|
71a1f81b4d | ||
|
|
8574d028c0 | ||
|
|
0be0628d33 | ||
|
|
782e119add | ||
|
|
9079ffcdb1 | ||
|
|
a7c1ad1b0d | ||
|
|
091757973f | ||
|
|
762ac24f47 | ||
|
|
6f8ac2b796 | ||
|
|
5157b3f599 | ||
|
|
760879a183 | ||
|
|
63e3dda268 | ||
|
|
284d38f3c3 | ||
|
|
cc72964343 | ||
|
|
de4984cef6 | ||
|
|
dba56de614 | ||
|
|
bb102ba986 | ||
|
|
199bbcd6ae | ||
|
|
c8ee036dd2 | ||
|
|
dfff42074d | ||
|
|
565893307e | ||
|
|
ff32270464 | ||
|
|
98a91830f2 | ||
|
|
f72707ead5 | ||
|
|
963cf945cb | ||
|
|
d5aa809ff9 | ||
|
|
1dd9a0e04e | ||
|
|
dbf16ba433 | ||
|
|
d421c1e106 | ||
|
|
50cf25d949 | ||
|
|
1aeec5329e | ||
|
|
80120b0455 | ||
|
|
3f5ee4664a | ||
|
|
659003d8e2 | ||
|
|
2f48b4b33e | ||
|
|
1830e0285a | ||
|
|
d31b8ebc8b | ||
|
|
acb431e77f | ||
|
|
bfd3665d70 | ||
|
|
a181eb13f5 | ||
|
|
43dd396a64 | ||
|
|
921d4fa1d3 | ||
|
|
91aec645ba | ||
|
|
c209e338d4 | ||
|
|
0ba3e4f9e1 | ||
|
|
253b0cc38b | ||
|
|
8c7c7a34e5 | ||
|
|
8d6b57ec21 | ||
|
|
8994a9c663 | ||
|
|
c048372928 | ||
|
|
0271582c0d | ||
|
|
a4b71e0805 | ||
|
|
66ac595850 |
@@ -7,6 +7,7 @@ NEXTAUTH_COOKIE_DOMAIN=".langfuse.com"
|
||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
||||
DIRECT_URL="postgresql://postgres:[pw]@db.[db_id].supabase.co:5432/postgres"
|
||||
DATABASE_URL="postgres://postgres:[pw]@db.[db_id].supabase.co:6543/postgres?pgbouncer=true&connection_limit=1"
|
||||
# SHADOW_DATABASE_URL=
|
||||
|
||||
# Next Auth
|
||||
|
||||
@@ -48,3 +49,10 @@ AUTH_GITHUB_CLIENT_SECRET=
|
||||
# Email
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
# S3 storage (eg for exports), necessary for serverless deployments
|
||||
S3_ENDPOINT=""
|
||||
S3_ACCESS_KEY_ID=""
|
||||
S3_SECRET_ACCESS_KEY=""
|
||||
S3_BUCKET_NAME=""
|
||||
S3_REGION=""
|
||||
|
||||
+7
-1
@@ -16,7 +16,7 @@ const config = {
|
||||
rules: {
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-confusing-void-expression": "off",
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
@@ -35,6 +35,12 @@ const config = {
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
||||
"react/jsx-key": [
|
||||
"error",
|
||||
{
|
||||
warnOnDuplicates: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
contact_links:
|
||||
- name: 🤗 Get Help
|
||||
url: https://github.com/langfuse/langfuse/discussions/new?category=q-a
|
||||
about: If you can’t get something to work the way you expect, open a question in our discussion forums.
|
||||
- name: 💡 Feature Request
|
||||
url: https://github.com/langfuse/langfuse/discussions/new?category=ideas
|
||||
about: Suggest any ideas you have using our discussion forums.
|
||||
@@ -1,36 +0,0 @@
|
||||
# This template is heavily inspired by the Next.js's template:
|
||||
# See here: https://github.com/vercel/next.js/blob/canary/.github/ISSUE_TEMPLATE/3.feature_request.yml
|
||||
|
||||
name: 🛠 Feature Request
|
||||
description: Create a feature request for the core packages
|
||||
title: "feat: "
|
||||
labels: ["✨ enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to file a feature request. Please fill out this form as completely as possible.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature you'd like to request
|
||||
description: Please describe the feature as clear and concise as possible. Remember to add context as to why you believe this feature is needed.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like to see
|
||||
description: Please describe the solution you would like to see. Adding example usage is a good way to provide context.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here.
|
||||
- type: checkboxes
|
||||
id: contribute
|
||||
attributes:
|
||||
label: Contribute
|
||||
description: Are you willing to contribute to the implementation of this feature?
|
||||
options:
|
||||
- label: Yes, I can implement this and raise a PR
|
||||
|
||||
@@ -30,6 +30,36 @@ jobs:
|
||||
- name: lint
|
||||
run: npm run lint
|
||||
|
||||
test-docker-build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
NEXTAUTH_SECRET: "secret"
|
||||
SALT: "salt"
|
||||
NEXTAUTH_URL: "http://localhost:3030"
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
steps:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
build-args: |
|
||||
DATABASE_URL=${{ env.DATABASE_URL }}
|
||||
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
|
||||
NEXTAUTH_URL=${{ env.NEXTAUTH_URL }}
|
||||
SALT=${{ env.SALT }}
|
||||
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -101,7 +131,7 @@ jobs:
|
||||
all-ci-passed:
|
||||
# This allows us to have a branch protection rule for tests and deploys with matrix
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, tests, e2e-tests]
|
||||
needs: [lint, tests, e2e-tests, test-docker-build]
|
||||
if: always()
|
||||
steps:
|
||||
- name: Successful deploy
|
||||
|
||||
+75
-20
@@ -21,36 +21,86 @@ _Before making any significant changes, please [open an issue](https://github.co
|
||||
|
||||
Once we've discussed your changes and you've got your code ready, make sure that tests are passing and open your pull request.
|
||||
|
||||
## Getting started
|
||||
|
||||
A good first step is to search for open [issues](https://github.com/langfuse/langfuse/issues). Issues are labeled, and some good issues to start with are labeled: [good first issue](https://github.com/langfuse/langfuse/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
|
||||
|
||||
## Project Overview
|
||||
|
||||
### Technologies we use
|
||||
|
||||
- NextJS, pages router
|
||||
- NextAuth.js / Auth.js
|
||||
- tRPC
|
||||
- Prisma ORM
|
||||
- Zod
|
||||
- Tailwind CSS
|
||||
- shadcn/ui tailwind components (using Radix and tanstack)
|
||||
- Application (this repository)
|
||||
- NextJS 14, pages router
|
||||
- NextAuth.js / Auth.js
|
||||
- tRPC: Frontend APIs
|
||||
- Prisma ORM
|
||||
- Zod
|
||||
- Tailwind CSS
|
||||
- shadcn/ui tailwind components (using Radix and tanstack)
|
||||
- Fern: generate OpenAPI spec and Pydantic models
|
||||
- JS SDK ([langfuse/langfuse-js](https://github.com/langfuse/langfuse-js))
|
||||
- openapi-typescript to generated types based on OpenAPI spec
|
||||
- Python SDK ([langfuse/langfuse-python](https://github.com/langfuse/langfuse-python))
|
||||
- Pydantic for input validation, models generated by fern
|
||||
|
||||
### Architecture overview
|
||||
### Architecture Overview
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
flowchart TB
|
||||
subgraph s4["Clients"]
|
||||
subgraph s2["langfuse/langfuse-python"]
|
||||
Python["Python SDK"]
|
||||
OAI["OpenAI drop-in replacement"] -->|extends| Python
|
||||
LCPYTHON["Langchain Python Integration"] -->|extends| Python
|
||||
Langflow -->|uses| LCPYTHON
|
||||
LiteLLM -->|uses| Python
|
||||
end
|
||||
subgraph s3["langfuse/langfuse-js"]
|
||||
JS["JS SDK"]
|
||||
LCJS["Langchain JS Integration"] -->|extends| JS
|
||||
Flowise -->|uses| LCJS
|
||||
end
|
||||
end
|
||||
|
||||
A[Python SDK - Backend] -->|REST| C[NextJs Server]
|
||||
B[JS/TS SDK - Frontend + Backend] -->|REST| C
|
||||
C <-->|Prisma| D[Postgres Database]
|
||||
E[Langfuse UI] -->|tRPC| C
|
||||
DB[Postgres Database]
|
||||
subgraph s1["Application (langfuse/langfuse)"]
|
||||
API[Public HTTP API]
|
||||
G[TRPC API]
|
||||
I[NextAuth]
|
||||
H[React Frontend]
|
||||
Prisma[Prisma ORM]
|
||||
H --> G
|
||||
H --> I
|
||||
G --> I
|
||||
G --- Prisma
|
||||
API --- Prisma
|
||||
I --- Prisma
|
||||
end
|
||||
Prisma --- DB
|
||||
JS --- API
|
||||
Python --- API
|
||||
```
|
||||
|
||||
| Component | Technology | Description |
|
||||
| --------------------------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Python SDK](https://github.com/langfuse/langfuse-python) | Python | Fully async SDK. Events are sent to server in batches from a background thread. |
|
||||
| [JS/TS SDK](https://github.com/langfuse/langfuse-js) | TypeScript | Fully async SDK. As of SDK version 2.0.0, events will be sent in batches to the server. |
|
||||
| NextJs Server (JS) | TypeScript, [T3 Stack](https://create.t3.gg/) | We use Vercel Cloud Functions to serve our back-end and front-end. The front-end uses tRPC to communicate with the back-end. For self-hosting, this component is available as Docker container. |
|
||||
### Database Overview
|
||||
|
||||
The diagram below may not show all relationships if the foreign key is not defined in the database schema. For instance, `trace_id` in the `observation` table is not defined as a foreign key to the `trace` table to allow unordered ingestion of these objects, but it is still a foreign key in the application code.
|
||||
|
||||
Full database schema: [prisma/schema.prisma](prisma/schema.prisma)
|
||||
|
||||
<img src="./prisma/database.svg">
|
||||
|
||||
### Infrastructure & Network Overview
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Browser ---|Web UI & TRPC API| App
|
||||
Integrations/SDKs ---|Public HTTP API| App
|
||||
subgraph i1["Application Network"]
|
||||
App["Langfuse Application (Docker or Serverless)"]
|
||||
end
|
||||
subgraph i2["Database Network"]
|
||||
DB["Postgres Database"]
|
||||
end
|
||||
App --- DB
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
|
||||
@@ -96,6 +146,9 @@ Requirements
|
||||
npm run dev
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If you frequently switch branches, use `npm run dx` instead of `npm run dev`. This command will install dependencies, reset the database (wipe and apply all migrations), and run the database seeder with example data before starting the development server.
|
||||
|
||||
## Commit messages
|
||||
|
||||
On the main branch, we adhere to the best practices of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). All pull requests and branches are squash-merged to maintain a clean and readable history. This approach ensures the addition of a conventional commit message when merging contributions.
|
||||
@@ -153,3 +206,5 @@ When a new release is tagged on the `main` branch (excluding prereleases), it tr
|
||||
## License
|
||||
|
||||
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
When contributing to the Langfuse codebase, you need to agree to the [Contributor License Agreement](https://cla-assistant.io/langfuse/langfuse). You only need to do this once and the CLA bot will remind you if you haven't signed it yet.
|
||||
|
||||
+4
-2
@@ -13,8 +13,10 @@ if [ -z "$DATABASE_URL" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set DIRECT_URL to the value of DATABASE_URL, required for migrations
|
||||
export DIRECT_URL=$DATABASE_URL
|
||||
# Set DIRECT_URL to the value of DATABASE_URL if it is not set, required for migrations
|
||||
if [ -z "$DIRECT_URL" ]; then
|
||||
export DIRECT_URL=$DATABASE_URL
|
||||
fi
|
||||
|
||||
# Apply migrations
|
||||
prisma migrate deploy
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
name: langfuse
|
||||
docs: |
|
||||
## Authentication
|
||||
Authenticate with the API using Basic Auth, get API keys in the project settings:
|
||||
|
||||
- username: Langfuse Public Key
|
||||
- password: Langfuse Secret Key
|
||||
error-discrimination:
|
||||
strategy: status-code
|
||||
auth: basic
|
||||
|
||||
@@ -14,6 +14,7 @@ types:
|
||||
version: optional<string>
|
||||
userId: optional<string>
|
||||
metadata: optional<unknown>
|
||||
tags: optional<list<string>>
|
||||
public:
|
||||
type: optional<boolean>
|
||||
docs: Public traces are accessible via url without login
|
||||
@@ -59,6 +60,7 @@ types:
|
||||
level: ObservationLevel
|
||||
statusMessage: optional<string>
|
||||
parentObservationId: optional<string>
|
||||
promptId: optional<string>
|
||||
Usage:
|
||||
properties:
|
||||
input: optional<integer>
|
||||
@@ -78,7 +80,6 @@ types:
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
status: DatasetStatus
|
||||
projectId: string
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
@@ -128,6 +129,7 @@ types:
|
||||
- optional<string>
|
||||
- optional<integer>
|
||||
- optional<boolean>
|
||||
- optional<list<string>>
|
||||
DatasetStatus:
|
||||
enum:
|
||||
- ACTIVE
|
||||
|
||||
@@ -7,9 +7,22 @@ service:
|
||||
docs: Check health of API and database
|
||||
method: GET
|
||||
path: /health
|
||||
response: HealthResponse
|
||||
errors:
|
||||
- ServiceUnavailableError
|
||||
|
||||
types:
|
||||
HealthResponse:
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
docs: Langfuse server version
|
||||
status: string
|
||||
examples:
|
||||
- value:
|
||||
version: 1.25.0
|
||||
status: OK
|
||||
|
||||
errors:
|
||||
ServiceUnavailableError:
|
||||
status-code: 503
|
||||
|
||||
@@ -28,6 +28,7 @@ types:
|
||||
generation-update: UpdateGenerationEvent
|
||||
span-create: CreateSpanEvent
|
||||
span-update: UpdateSpanEvent
|
||||
sdk-log: SDKLogEvent
|
||||
|
||||
# both are legacy
|
||||
observation-create: CreateObservationEvent
|
||||
@@ -91,6 +92,8 @@ types:
|
||||
model: optional<string>
|
||||
modelParameters: optional<map<string, commons.MapValue>>
|
||||
usage: optional<IngestionUsage>
|
||||
promptName: optional<string>
|
||||
promptVersion: optional<integer>
|
||||
|
||||
UpdateGenerationBody:
|
||||
extends: UpdateSpanBody
|
||||
@@ -99,6 +102,8 @@ types:
|
||||
model: optional<string>
|
||||
modelParameters: optional<map<string, commons.MapValue>>
|
||||
usage: optional<IngestionUsage>
|
||||
promptName: optional<string>
|
||||
promptVersion: optional<integer>
|
||||
|
||||
ObservationBody:
|
||||
properties:
|
||||
@@ -131,10 +136,15 @@ types:
|
||||
release: optional<string>
|
||||
version: optional<string>
|
||||
metadata: optional<unknown>
|
||||
tags: optional<list<string>>
|
||||
public:
|
||||
type: optional<boolean>
|
||||
docs: Make trace publicly accessible via url
|
||||
|
||||
SDKLogBody:
|
||||
properties:
|
||||
log: unknown
|
||||
|
||||
ScoreBody:
|
||||
properties:
|
||||
id: optional<string>
|
||||
@@ -170,6 +180,11 @@ types:
|
||||
properties:
|
||||
body: ScoreBody
|
||||
|
||||
SDKLogEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
body: SDKLogBody
|
||||
|
||||
CreateGenerationEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
pagination: ./utils/pagination.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
get:
|
||||
docs: Get a specific prompt
|
||||
method: GET
|
||||
path: /prompts
|
||||
request:
|
||||
name: GetParameterRequest
|
||||
query-parameters:
|
||||
name: string
|
||||
version: optional<integer>
|
||||
response: Prompt
|
||||
create:
|
||||
docs: Create a specific prompt
|
||||
method: POST
|
||||
path: /prompts
|
||||
request: CreatePromptRequest
|
||||
response: Prompt
|
||||
|
||||
types:
|
||||
CreatePromptRequest:
|
||||
properties:
|
||||
name: string
|
||||
isActive: boolean
|
||||
prompt: string
|
||||
Prompt:
|
||||
properties:
|
||||
name: string
|
||||
version: integer
|
||||
prompt: string
|
||||
@@ -26,6 +26,13 @@ service:
|
||||
limit: optional<integer>
|
||||
userId: optional<string>
|
||||
name: optional<string>
|
||||
orderBy:
|
||||
type: string
|
||||
docs: Format of the string sort_by=timestamp.asc (id, timestamp, name, userId, release, version, public, bookmarked, sessionId)
|
||||
tags:
|
||||
type: optional<string>
|
||||
allow-multiple: true
|
||||
docs: Only traces that include all of these tags will be returned.
|
||||
response: Traces
|
||||
|
||||
types:
|
||||
@@ -33,3 +40,6 @@ types:
|
||||
properties:
|
||||
data: list<commons.TraceWithDetails>
|
||||
meta: pagination.MetaResponse
|
||||
Sort:
|
||||
properties:
|
||||
id: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"organization": "finto",
|
||||
"version": "0.16.7"
|
||||
"version": "0.16.22"
|
||||
}
|
||||
@@ -2,6 +2,17 @@ openapi: 3.0.1
|
||||
info:
|
||||
title: langfuse
|
||||
version: ''
|
||||
description: >-
|
||||
## Authentication
|
||||
|
||||
|
||||
Authenticate with the API using Basic Auth, get API keys in the project
|
||||
settings:
|
||||
|
||||
|
||||
- username: Langfuse Public Key
|
||||
|
||||
- password: Langfuse Secret Key
|
||||
paths:
|
||||
/api/public/dataset-items:
|
||||
post:
|
||||
@@ -290,8 +301,12 @@ paths:
|
||||
- Health
|
||||
parameters: []
|
||||
responses:
|
||||
'204':
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HealthResponse'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
@@ -541,6 +556,102 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/prompts:
|
||||
get:
|
||||
description: Get a specific prompt
|
||||
operationId: prompts_get
|
||||
tags:
|
||||
- Prompts
|
||||
parameters:
|
||||
- name: name
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: version
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
nullable: true
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Prompt'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
post:
|
||||
description: Create a specific prompt
|
||||
operationId: prompts_create
|
||||
tags:
|
||||
- Prompts
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Prompt'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreatePromptRequest'
|
||||
/api/public/scores:
|
||||
post:
|
||||
description: Add a score to the database, upserts on id
|
||||
@@ -773,6 +884,23 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
- name: orderBy
|
||||
in: query
|
||||
description: >-
|
||||
Format of the string sort_by=timestamp.asc (id, timestamp, name,
|
||||
userId, release, version, public, bookmarked, sessionId)
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: tags
|
||||
in: query
|
||||
description: Only traces that include all of these tags will be returned.
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
@@ -839,6 +967,11 @@ components:
|
||||
nullable: true
|
||||
metadata:
|
||||
nullable: true
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
public:
|
||||
type: boolean
|
||||
nullable: true
|
||||
@@ -962,6 +1095,9 @@ components:
|
||||
parentObservationId:
|
||||
type: string
|
||||
nullable: true
|
||||
promptId:
|
||||
type: string
|
||||
nullable: true
|
||||
required:
|
||||
- id
|
||||
- type
|
||||
@@ -1019,8 +1155,6 @@ components:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
status:
|
||||
$ref: '#/components/schemas/DatasetStatus'
|
||||
projectId:
|
||||
type: string
|
||||
createdAt:
|
||||
@@ -1040,7 +1174,6 @@ components:
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- status
|
||||
- projectId
|
||||
- createdAt
|
||||
- updatedAt
|
||||
@@ -1150,6 +1283,10 @@ components:
|
||||
nullable: true
|
||||
- type: boolean
|
||||
nullable: true
|
||||
- type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
DatasetStatus:
|
||||
title: DatasetStatus
|
||||
type: string
|
||||
@@ -1193,6 +1330,20 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
HealthResponse:
|
||||
title: HealthResponse
|
||||
type: object
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
description: Langfuse server version
|
||||
example: 1.25.0
|
||||
status:
|
||||
type: string
|
||||
example: OK
|
||||
required:
|
||||
- version
|
||||
- status
|
||||
IngestionEvent:
|
||||
title: IngestionEvent
|
||||
oneOf:
|
||||
@@ -1273,6 +1424,17 @@ components:
|
||||
- $ref: '#/components/schemas/UpdateSpanEvent'
|
||||
required:
|
||||
- type
|
||||
- type: object
|
||||
allOf:
|
||||
- type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- sdk-log
|
||||
- $ref: '#/components/schemas/SDKLogEvent'
|
||||
required:
|
||||
- type
|
||||
- type: object
|
||||
allOf:
|
||||
- type: object
|
||||
@@ -1410,6 +1572,12 @@ components:
|
||||
usage:
|
||||
$ref: '#/components/schemas/IngestionUsage'
|
||||
nullable: true
|
||||
promptName:
|
||||
type: string
|
||||
nullable: true
|
||||
promptVersion:
|
||||
type: integer
|
||||
nullable: true
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CreateSpanBody'
|
||||
UpdateGenerationBody:
|
||||
@@ -1431,6 +1599,12 @@ components:
|
||||
usage:
|
||||
$ref: '#/components/schemas/IngestionUsage'
|
||||
nullable: true
|
||||
promptName:
|
||||
type: string
|
||||
nullable: true
|
||||
promptVersion:
|
||||
type: integer
|
||||
nullable: true
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/UpdateSpanBody'
|
||||
ObservationBody:
|
||||
@@ -1519,10 +1693,22 @@ components:
|
||||
nullable: true
|
||||
metadata:
|
||||
nullable: true
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
public:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: Make trace publicly accessible via url
|
||||
SDKLogBody:
|
||||
title: SDKLogBody
|
||||
type: object
|
||||
properties:
|
||||
log: {}
|
||||
required:
|
||||
- log
|
||||
ScoreBody:
|
||||
title: ScoreBody
|
||||
type: object
|
||||
@@ -1600,6 +1786,16 @@ components:
|
||||
- body
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/BaseEvent'
|
||||
SDKLogEvent:
|
||||
title: SDKLogEvent
|
||||
type: object
|
||||
properties:
|
||||
body:
|
||||
$ref: '#/components/schemas/SDKLogBody'
|
||||
required:
|
||||
- body
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/BaseEvent'
|
||||
CreateGenerationEvent:
|
||||
title: CreateGenerationEvent
|
||||
type: object
|
||||
@@ -1726,6 +1922,34 @@ components:
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
CreatePromptRequest:
|
||||
title: CreatePromptRequest
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
isActive:
|
||||
type: boolean
|
||||
prompt:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- isActive
|
||||
- prompt
|
||||
Prompt:
|
||||
title: Prompt
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
version:
|
||||
type: integer
|
||||
prompt:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- version
|
||||
- prompt
|
||||
CreateScoreRequest:
|
||||
title: CreateScoreRequest
|
||||
type: object
|
||||
@@ -1776,6 +2000,14 @@ components:
|
||||
required:
|
||||
- data
|
||||
- meta
|
||||
Sort:
|
||||
title: Sort
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
utilsMetaResponse:
|
||||
title: utilsMetaResponse
|
||||
type: object
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"info": {
|
||||
"name": "Langfuse",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"description": null
|
||||
"description": "## Authentication\n\nAuthenticate with the API using Basic Auth, get API keys in the project settings:\n\n- username: Langfuse Public Key\n- password: Langfuse Secret Key"
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
@@ -320,7 +320,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"body\": {\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"sessionId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"public\": true\n },\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"metadata\": \"UNKNOWN\"\n }\n ]\n}",
|
||||
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"body\": {\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"sessionId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"tags\": [\n \"example\"\n ],\n \"public\": true\n },\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"metadata\": \"UNKNOWN\"\n }\n ]\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -464,6 +464,82 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Prompts",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": "Get a specific prompt",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/prompts?name=&version=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"prompts"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "name",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "version",
|
||||
"value": "",
|
||||
"description": null
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"description": "Create a specific prompt",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/prompts",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"prompts"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "POST",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"example\",\n \"isActive\": true,\n \"prompt\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
@@ -632,7 +708,7 @@
|
||||
"request": {
|
||||
"description": "Get list of traces",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=",
|
||||
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&orderBy=&tags=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
@@ -661,6 +737,16 @@
|
||||
"key": "name",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "orderBy",
|
||||
"value": "",
|
||||
"description": "Format of the string sort_by=timestamp.asc (id, timestamp, name, userId, release, version, public, bookmarked, sessionId)"
|
||||
},
|
||||
{
|
||||
"key": "tags",
|
||||
"value": "",
|
||||
"description": "Only traces that include all of these tags will be returned."
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
|
||||
+6
-1
@@ -18,6 +18,8 @@ const serverTestConfig = {
|
||||
testEnvironment: "jest-environment-node",
|
||||
};
|
||||
|
||||
// To avoid the "Cannot use import statement outside a module" errors while transforming ESM.
|
||||
const esModules = ["superjson"];
|
||||
// Add any custom config to be passed to Jest
|
||||
/** @type {import('jest').Config} */
|
||||
const config = {
|
||||
@@ -26,7 +28,10 @@ const config = {
|
||||
verbose: true,
|
||||
projects: [
|
||||
await createJestConfig(clientTestConfig)(),
|
||||
await createJestConfig(serverTestConfig)(),
|
||||
{
|
||||
...(await createJestConfig(serverTestConfig)()),
|
||||
transformIgnorePatterns: [`/node_modules/(?!(${esModules.join("|")})/)`],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
Generated
+2906
-909
File diff suppressed because it is too large
Load Diff
+42
-36
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "1.19.4",
|
||||
"version": "1.34.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"dx": "npm i && npm run db:reset && npm run db:seed:examples && npm run dev",
|
||||
"postinstall": "prisma generate",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
@@ -15,7 +16,8 @@
|
||||
"test": "jest --runInBand",
|
||||
"test:watch": "jest --watch --runInBand",
|
||||
"test:e2e": "playwright test",
|
||||
"db:migrate": "npx prisma migrate dev",
|
||||
"db:migrate": "DISABLE_ERD=false npx prisma migrate dev",
|
||||
"db:reset": "npx prisma migrate reset",
|
||||
"db:seed": "npx prisma db seed",
|
||||
"db:seed:examples": "npx prisma db seed -- --environment examples",
|
||||
"release": "release-it"
|
||||
@@ -25,14 +27,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@formkit/auto-animate": "^0.8.1",
|
||||
"@aws-sdk/client-s3": "^3.485.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.485.0",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"@hookform/resolvers": "^3.3.2",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^5.7.1",
|
||||
"@prisma/client": "^5.8.1",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
@@ -42,6 +45,7 @@
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
@@ -52,86 +56,83 @@
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@react-email/components": "^0.0.12",
|
||||
"@react-email/render": "^0.0.10",
|
||||
"@sentry/nextjs": "^7.88.0",
|
||||
"@sentry/profiling-node": "^1.3.2",
|
||||
"@sentry/nextjs": "^7.95.0",
|
||||
"@sentry/profiling-node": "^1.3.5",
|
||||
"@sentry/types": "^7.88.0",
|
||||
"@t3-oss/env-nextjs": "^0.7.1",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.10.7",
|
||||
"@tanstack/react-table": "^8.11.3",
|
||||
"@tremor/react": "^3.11.1",
|
||||
"@trpc/client": "^10.44.1",
|
||||
"@trpc/next": "^10.44.1",
|
||||
"@trpc/react-query": "^10.44.1",
|
||||
"@trpc/server": "^10.44.1",
|
||||
"@vercel/edge-config": "^0.4.1",
|
||||
"axios": "^1.6.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"core-js": "^3.35.0",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^2.30.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.298.0",
|
||||
"next": "^14.0.4",
|
||||
"lucide-react": "^0.314.0",
|
||||
"next": "^14.1.0",
|
||||
"next-auth": "^4.24.5",
|
||||
"next-query-params": "^5.0.0",
|
||||
"nodemailer": "^6.9.7",
|
||||
"posthog-js": "^1.96.1",
|
||||
"nodemailer": "^6.9.8",
|
||||
"posthog-js": "^1.100.0",
|
||||
"posthog-node": "^3.2.1",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.9.1",
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.49.2",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react18-json-view": "^0.2.7-canary.2",
|
||||
"superagent": "^8.1.2",
|
||||
"superjson": "2.2.1",
|
||||
"tailwind-merge": "^2.1.0",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tiktoken": "^1.0.11",
|
||||
"ts-pattern": "^5.0.6",
|
||||
"use-query-params": "^2.2.1",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jedmao/location": "^3.0.0",
|
||||
"@mermaid-js/mermaid-cli": "^10.6.1",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@release-it/bumper": "^6.0.1",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/eslint": "^8.44.9",
|
||||
"@types/eslint": "^8.56.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "20.10.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/react": "^18.2.45",
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
||||
"@typescript-eslint/parser": "^6.19.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.0.4",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"postcss": "^8.4.32",
|
||||
"postcss": "^8.4.33",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-tailwindcss": "^0.5.9",
|
||||
"prisma": "^5.7.1",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"prisma": "^5.8.1",
|
||||
"prisma-erd-generator": "^1.11.2",
|
||||
"release-it": "^17.0.1",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.3.3"
|
||||
@@ -151,7 +152,12 @@
|
||||
"release": true,
|
||||
"web": true,
|
||||
"autoGenerate": true,
|
||||
"releaseName": "v${version}"
|
||||
"releaseName": "v${version}",
|
||||
"comments": {
|
||||
"submit": true,
|
||||
"issue": ":rocket: _This issue has been resolved in v${version}. See [${releaseName}](${releaseUrl}) for release notes._",
|
||||
"pr": ":rocket: _This pull request is included in v${version}. See [${releaseName}](${releaseUrl}) for release notes._"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it/bumper": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 194 KiB |
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "projects" ADD COLUMN "cloud_config" JSONB;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Account" ADD COLUMN "expires_in" INTEGER,
|
||||
ADD COLUMN "ext_expires_in" INTEGER;
|
||||
@@ -0,0 +1,29 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "observations" ADD COLUMN "prompt_id" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "prompts" (
|
||||
"id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"project_id" TEXT NOT NULL,
|
||||
"created_by" TEXT NOT NULL,
|
||||
"prompt" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"is_active" BOOLEAN NOT NULL,
|
||||
|
||||
CONSTRAINT "prompts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "prompts_project_id_name_version_idx" ON "prompts"("project_id", "name", "version");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "prompts_project_id_name_version_key" ON "prompts"("project_id", "name", "version");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "observations" ADD CONSTRAINT "observations_prompt_id_fkey" FOREIGN KEY ("prompt_id") REFERENCES "prompts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "prompts" ADD CONSTRAINT "prompts_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,30 @@
|
||||
-- Migration to add gpt-3.5-turbo-1106 and gpt-3.5-turbo-instruct models
|
||||
|
||||
INSERT INTO pricings (
|
||||
id,
|
||||
model_name,
|
||||
pricing_unit,
|
||||
price,
|
||||
currency,
|
||||
token_type
|
||||
)
|
||||
VALUES
|
||||
('clqqpc2pr000008l3hvy63gxy','gpt-3.5-turbo-1106', 'PER_1000_TOKENS', 0.001, 'USD', 'PROMPT'),
|
||||
('clqqpcb6d000208l3atrfbmou','gpt-3.5-turbo-1106', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION'),
|
||||
('clqqpdh45000008lfgrnx76cv','gpt-3.5-turbo-instruct', 'PER_1000_TOKENS', 0.0015, 'USD', 'PROMPT'),
|
||||
('clqqpdjya000108lf3s4b4c4m','gpt-3.5-turbo-instruct', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION'),
|
||||
('clnnuiuq6000008l4dxp43wc6', 'claude-instant-1.2', 'PER_1000_TOKENS', 0.00163, 'USD', 'PROMPT'),
|
||||
('clnnujlmj000108l48wne4ii9', 'claude-instant-1.2', 'PER_1000_TOKENS', 0.00551, 'USD', 'COMPLETION'),
|
||||
('clnnukutg000208l49qqt9lyr', 'claude-instant-1.1', 'PER_1000_TOKENS', 0.00163, 'USD', 'PROMPT'),
|
||||
('clnnun3x3000408l490qz9uv0', 'claude-instant-1.1', 'PER_1000_TOKENS', 0.00551, 'USD', 'COMPLETION'),
|
||||
('clnnuosvy000508l4gsjy2pp4', 'claude-2.0', 'PER_1000_TOKENS', 0.01102, 'USD', 'PROMPT'),
|
||||
('clnnuqcns000608l40qaz8trt', 'claude-2.0', 'PER_1000_TOKENS', 0.03268, 'USD', 'COMPLETION'),
|
||||
('clnnuuif8000808l4gal4fjq4', 'claude-1.0', 'PER_1000_TOKENS', 0.01102, 'USD', 'PROMPT'),
|
||||
('clnnutptp000708l47r091vvd', 'claude-1.0', 'PER_1000_TOKENS', 0.03268, 'USD', 'COMPLETION'),
|
||||
('clnon8riv000308mlgfr1agiv', 'text-embedding-ada-002', 'PER_1000_TOKENS', 0.0001, 'USD', 'PROMPT'),
|
||||
('clnon9kfz000408ml1bg81o6z', 'text-embedding-ada-002', 'PER_1000_TOKENS', 0.0001, 'USD', 'COMPLETION'),
|
||||
('clqwniv8a000d08l2frjl7mmw', 'codechat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'PROMPT'),
|
||||
('clqwnj044000e08l2dfjm5g90', 'chat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'PROMPT'),
|
||||
('clqwnj47r000f08l2a16fekls', 'chat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'COMPLETION'),
|
||||
('clqwnj863000g08l2bwxgdapm', 'codechat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'COMPLETION')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "observations_model_idx" ON "observations"("model");
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "pricings_model_name_idx" ON "pricings"("model_name");
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "traces" ADD COLUMN "tags" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "traces_tags_idx" ON "traces" USING GIN ("tags" array_ops);
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `status` on the `datasets` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "datasets" DROP COLUMN "status";
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Migration to add gpt-35 spelling
|
||||
|
||||
INSERT INTO pricings (
|
||||
id,
|
||||
model_name,
|
||||
pricing_unit,
|
||||
price,
|
||||
currency,
|
||||
token_type
|
||||
)
|
||||
VALUES
|
||||
('clqqpc2pr000008l3hvy63gxy1','gpt-35-turbo-1106', 'PER_1000_TOKENS', 0.001, 'USD', 'PROMPT'),
|
||||
('clqqpcb6d000208l3atrfbmou1','gpt-35-turbo-1106', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION'),
|
||||
('clqqpdh45000008lfgrnx76cv1','gpt-35-turbo-instruct', 'PER_1000_TOKENS', 0.0015, 'USD', 'PROMPT'),
|
||||
('clqqpdjya000108lf3s4b4c4m1','gpt-35-turbo-instruct', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "traces_project_id_external_id_key";
|
||||
+48
-9
@@ -7,9 +7,18 @@ generator client {
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
}
|
||||
|
||||
generator erd {
|
||||
provider = "prisma-erd-generator"
|
||||
ignoreTables = ["_prisma_migrations", "Session", "Account", "Example"]
|
||||
disabled = true
|
||||
ignoreEnums = true
|
||||
output = "database.svg"
|
||||
}
|
||||
|
||||
model Example {
|
||||
@@ -28,6 +37,8 @@ model Account {
|
||||
refresh_token String? // @db.Text
|
||||
access_token String? // @db.Text
|
||||
expires_at Int?
|
||||
expires_in Int?
|
||||
ext_expires_in Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? // @db.Text
|
||||
@@ -79,6 +90,7 @@ model Project {
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
name String
|
||||
cloudConfig Json? @map("cloud_config") // Langfuse Cloud, for zod schema see projectsRouter.ts
|
||||
members Membership[]
|
||||
traces Trace[]
|
||||
observations Observation[]
|
||||
@@ -87,6 +99,7 @@ model Project {
|
||||
RawEvents Events[]
|
||||
invitations MembershipInvitation[]
|
||||
sessions TraceSession[]
|
||||
Prompt Prompt[]
|
||||
|
||||
@@map("projects")
|
||||
}
|
||||
@@ -173,18 +186,18 @@ model Trace {
|
||||
metadata Json?
|
||||
release String?
|
||||
version String?
|
||||
input Json?
|
||||
output Json?
|
||||
sessionId String? @map("session_id")
|
||||
session TraceSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull)
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
public Boolean @default(false)
|
||||
bookmarked Boolean @default(false)
|
||||
tags String[] @default([])
|
||||
input Json?
|
||||
output Json?
|
||||
sessionId String? @map("session_id")
|
||||
session TraceSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull)
|
||||
|
||||
scores Score[]
|
||||
|
||||
@@unique([projectId, externalId])
|
||||
@@index([projectId])
|
||||
@@index([sessionId])
|
||||
@@index([name])
|
||||
@@ -192,6 +205,7 @@ model Trace {
|
||||
@@index([externalId])
|
||||
@@index(timestamp)
|
||||
@@index(release)
|
||||
@@index([tags(ops: ArrayOps)], type: Gin)
|
||||
@@map("traces")
|
||||
}
|
||||
|
||||
@@ -225,6 +239,9 @@ model Observation {
|
||||
derivedDatasetItems DatasetItem[]
|
||||
datasetRunItems DatasetRunItems[]
|
||||
|
||||
promptId String? @map("prompt_id")
|
||||
prompt Prompt? @relation(fields: [promptId], onDelete: SetNull, references: [id])
|
||||
|
||||
@@unique([id, projectId])
|
||||
@@index([traceId])
|
||||
@@index([type])
|
||||
@@ -232,6 +249,7 @@ model Observation {
|
||||
@@index(createdAt)
|
||||
@@index(projectId)
|
||||
@@index(parentObservationId)
|
||||
@@index(model)
|
||||
@@map("observations")
|
||||
}
|
||||
|
||||
@@ -285,6 +303,7 @@ model Pricing {
|
||||
currency String @default("USD")
|
||||
tokenType TokenType @map("token_type")
|
||||
|
||||
@@index(modelName)
|
||||
@@map("pricings")
|
||||
}
|
||||
|
||||
@@ -300,7 +319,6 @@ model CronJobs {
|
||||
model Dataset {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
status DatasetStatus @default(ACTIVE)
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
@@ -381,3 +399,24 @@ model Events {
|
||||
@@index(projectId)
|
||||
@@map("events")
|
||||
}
|
||||
|
||||
model Prompt {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdBy String @map("created_by")
|
||||
|
||||
prompt String
|
||||
name String
|
||||
version Int
|
||||
isActive Boolean @map("is_active")
|
||||
Observation Observation[]
|
||||
|
||||
@@unique([projectId, name, version])
|
||||
@@index([projectId, name, version])
|
||||
@@map("prompts")
|
||||
}
|
||||
|
||||
+78
-39
@@ -53,6 +53,25 @@ async function main() {
|
||||
},
|
||||
});
|
||||
|
||||
const prompt = await prisma.prompt.upsert({
|
||||
where: {
|
||||
projectId_name_version: {
|
||||
projectId: seedProjectId,
|
||||
name: "summary-prompt",
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
name: "summary-prompt",
|
||||
project: { connect: { id: seedProjectId } },
|
||||
prompt: "prompt {{variable}} {{anotherVariable}}",
|
||||
isActive: true,
|
||||
version: 1,
|
||||
createdBy: "user-1",
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
const seedApiKey = {
|
||||
id: "seed-api-key",
|
||||
secret: process.env.SEED_SECRET_KEY ?? "sk-lf-1234567890",
|
||||
@@ -103,13 +122,15 @@ async function main() {
|
||||
});
|
||||
|
||||
const generationIds: string[] = [];
|
||||
const envTags = [null, "development", "staging", "production"];
|
||||
const colorTags = [null, "red", "blue", "yellow"];
|
||||
|
||||
for (let i = 0; i < TRACE_VOLUME; i++) {
|
||||
// print progress to console with a progress bar that refreshes every 10 iterations
|
||||
if (i % 10 === 0) {
|
||||
if ((i + 1) % 10 === 0 || i === TRACE_VOLUME - 1) {
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(`Seeding ${i} of ${TRACE_VOLUME}`);
|
||||
process.stdout.write(`Seeding ${i + 1} of ${TRACE_VOLUME}`);
|
||||
}
|
||||
// random date within last 90 days, with a linear bias towards more recent dates
|
||||
const traceTs = new Date(
|
||||
@@ -117,6 +138,11 @@ async function main() {
|
||||
Math.floor(Math.random() ** 1.5 * 90 * 24 * 60 * 60 * 1000),
|
||||
);
|
||||
|
||||
const envTag = envTags[Math.floor(Math.random() * envTags.length)];
|
||||
const colorTag = colorTags[Math.floor(Math.random() * colorTags.length)];
|
||||
|
||||
const tags = [envTag, colorTag].filter((tag) => tag !== null);
|
||||
|
||||
const trace = await prisma.trace.create({
|
||||
data: {
|
||||
id: `trace-${Math.floor(Math.random() * 1000000000)}`,
|
||||
@@ -127,6 +153,7 @@ async function main() {
|
||||
metadata: {
|
||||
user: `user-${i}@langfuse.com`,
|
||||
},
|
||||
tags: tags as string[],
|
||||
project: {
|
||||
connect: {
|
||||
id: [project1.id, project2.id][i % 2],
|
||||
@@ -326,6 +353,11 @@ async function main() {
|
||||
totalTokens: promptTokens + completionTokens,
|
||||
parentObservationId: span.id,
|
||||
traceId: trace.id,
|
||||
...{
|
||||
...(Math.random() > 0.5 && trace.projectId === prompt.projectId
|
||||
? { prompt: { connect: { id: prompt.id } } }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
if (Math.random() > 0.6)
|
||||
@@ -377,47 +409,54 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
const dataset = await prisma.dataset.create({
|
||||
data: {
|
||||
name: "demo-dataset",
|
||||
projectId: project2.id,
|
||||
},
|
||||
});
|
||||
|
||||
const datasetRun = await prisma.datasetRuns.create({
|
||||
data: {
|
||||
name: "demo-dataset-run",
|
||||
datasetId: dataset.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (let runNumber = 0; runNumber < 10; runNumber++) {
|
||||
//pick randomly from existingSpanIds
|
||||
const sourceObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
const runObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
|
||||
const datasetItem = await prisma.datasetItem.create({
|
||||
for (let datasetNumber = 0; datasetNumber < 2; datasetNumber++) {
|
||||
const dataset = await prisma.dataset.create({
|
||||
data: {
|
||||
datasetId: dataset.id,
|
||||
sourceObservationId:
|
||||
Math.random() > 0.5 ? sourceObservationId : undefined,
|
||||
input: [
|
||||
{ role: "user", content: "How can i create a React component?" },
|
||||
],
|
||||
expectedOutput:
|
||||
"Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both.",
|
||||
name: `demo-dataset-${datasetNumber}`,
|
||||
projectId: project2.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.datasetRunItems.create({
|
||||
data: {
|
||||
datasetItemId: datasetItem.id,
|
||||
observationId: runObservationId!,
|
||||
datasetRunId: datasetRun.id,
|
||||
},
|
||||
});
|
||||
for (let datasetRunNumber = 0; datasetRunNumber < 2; datasetRunNumber++) {
|
||||
const datasetRun = await prisma.datasetRuns.create({
|
||||
data: {
|
||||
name: `demo-dataset-run-${datasetRunNumber}`,
|
||||
datasetId: dataset.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (let runNumber = 0; runNumber < 10; runNumber++) {
|
||||
//pick randomly from existingSpanIds
|
||||
const sourceObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
const runObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
|
||||
const datasetItem = await prisma.datasetItem.create({
|
||||
data: {
|
||||
datasetId: dataset.id,
|
||||
sourceObservationId:
|
||||
Math.random() > 0.5 ? sourceObservationId : undefined,
|
||||
input: [
|
||||
{
|
||||
role: "user",
|
||||
content: "How can i create a React component?",
|
||||
},
|
||||
],
|
||||
expectedOutput:
|
||||
"Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both.",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.datasetRunItems.create({
|
||||
data: {
|
||||
datasetItemId: datasetItem.id,
|
||||
observationId: runObservationId!,
|
||||
datasetRunId: datasetRun.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 0.8,
|
||||
tracesSampleRate: 0.3,
|
||||
|
||||
// Capture Replay for 100% of all sessions,
|
||||
// plus for 100% of sessions with an error
|
||||
|
||||
@@ -7,7 +7,7 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 0.75,
|
||||
tracesSampleRate: 0.3,
|
||||
|
||||
// ...
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 0.8,
|
||||
tracesSampleRate: 0.3,
|
||||
|
||||
profilesSampleRate: 0.8, // Profiling sample rate is relative to tracesSampleRate
|
||||
profilesSampleRate: 0.3, // Profiling sample rate is relative to tracesSampleRate
|
||||
integrations: [
|
||||
// Add profiling integration to list of integrations
|
||||
new ProfilingIntegration(),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { test, expect } from "@playwright/test";
|
||||
|
||||
test("should redirect to sign-in if not signed in", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.waitForTimeout(2000);
|
||||
await expect(page).toHaveURL("/auth/sign-in");
|
||||
});
|
||||
|
||||
|
||||
@@ -3,28 +3,33 @@ import { test, expect } from "@playwright/test";
|
||||
test("should see new projects dialog open after clicking new project btn", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/auth/sign-in");
|
||||
await page.fill('input[name="email"]', "demo@langfuse.com");
|
||||
await page.goto("auth/sign-up");
|
||||
await page.fill('input[name="name"]', "demo user");
|
||||
await page.fill('input[name="email"]', randomEmailAddress());
|
||||
await page.fill('input[type="password"]', "password");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(2000);
|
||||
await page.waitForTimeout(2000);
|
||||
await page.isVisible("Create new project");
|
||||
expect(await page.innerHTML("data-testid=create-new-project-title")).toBe(
|
||||
"Create new project",
|
||||
);
|
||||
await page.click('[data-testid="create-project-btn"]');
|
||||
await page.waitForTimeout(2000);
|
||||
await page.isVisible('[data-testid="new-project-form"]');
|
||||
await expect(page.locator("data-testid=new-project-form")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Create a project with provided name", async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
|
||||
await page.goto("/auth/sign-in");
|
||||
await page.fill('input[name="email"]', "demo@langfuse.com");
|
||||
await page.fill('input[type="password"]', "password");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(2000);
|
||||
await page.isVisible("Create new project");
|
||||
await page.isVisible('[data-testid="new-project-form"]');
|
||||
await page.click('[data-testid="create-project-btn"]');
|
||||
await page.waitForTimeout(2000);
|
||||
await page.isVisible('[data-testid="new-project-form"]');
|
||||
await expect(page.locator("data-testid=new-project-form")).toBeVisible();
|
||||
await page.fill(
|
||||
'[data-testid="new-project-name-input"]',
|
||||
"my e2e demo project",
|
||||
@@ -33,7 +38,11 @@ test("Create a project with provided name", async ({ page }) => {
|
||||
await page.waitForTimeout(2000);
|
||||
expect(page.url()).toContain("/project/");
|
||||
await page.waitForTimeout(2000);
|
||||
expect(await page.getByTestId("project-title-span-1").textContent()).toBe(
|
||||
expect(await page.getByTestId("project-name").textContent()).toContain(
|
||||
"my e2e demo project",
|
||||
);
|
||||
});
|
||||
|
||||
// random email address to be used in tests
|
||||
const randomEmailAddress = () =>
|
||||
Math.random().toString(36).substring(2, 11) + "@example.com";
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
|
||||
import { TestRouter } from "@/src/__tests__/fixtures/TestRouter";
|
||||
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
|
||||
import { LocationMock } from "@jedmao/location";
|
||||
import NextAdapterPages from "next-query-params/pages";
|
||||
import { useRouter } from "next/router";
|
||||
import { QueryParamProvider } from "use-query-params";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
jest.mock("next/router", () => ({
|
||||
useRouter: jest.fn(),
|
||||
}));
|
||||
|
||||
const { location: savedLocation } = window;
|
||||
|
||||
// The test for the useOrderByState hook
|
||||
describe("useOrderByState hook", () => {
|
||||
let testRouter: TestRouter;
|
||||
let locationMock: LocationMock;
|
||||
|
||||
beforeAll(() => {
|
||||
// @ts-expect-error - TS only allows to delete optional params; but here we want to delete it
|
||||
// as it will be set in beforeEach
|
||||
delete window.location;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
locationMock = new LocationMock("https://langfuse.com");
|
||||
testRouter = new TestRouter(locationMock);
|
||||
window.location = locationMock;
|
||||
|
||||
(useRouter as jest.Mock).mockReturnValue(testRouter);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
window.location = savedLocation;
|
||||
});
|
||||
|
||||
test("orderBy takes the default value if no url param is given", () => {
|
||||
const result = render(
|
||||
<QueryParamProvider adapter={NextAdapterPages}>
|
||||
<UseOrderByStateExample />
|
||||
</QueryParamProvider>,
|
||||
);
|
||||
|
||||
expect(result.queryByText(/column Column 1, order DESC/)).toBeTruthy();
|
||||
});
|
||||
|
||||
test("setOrderBy updates orderBy query param", () => {
|
||||
const result = render(
|
||||
<QueryParamProvider adapter={NextAdapterPages}>
|
||||
<UseOrderByStateExample />
|
||||
</QueryParamProvider>,
|
||||
);
|
||||
|
||||
result.queryByText(/Set Order/)!.click();
|
||||
result.rerender(
|
||||
<QueryParamProvider adapter={NextAdapterPages}>
|
||||
<UseOrderByStateExample />
|
||||
</QueryParamProvider>,
|
||||
);
|
||||
|
||||
expect(result.queryByText(/column Column 2, order ASC/)).toBeTruthy();
|
||||
expect(locationMock.searchParams.get("orderBy")).toBe(
|
||||
"column-Column 2_order-ASC",
|
||||
);
|
||||
});
|
||||
|
||||
test("orderBy reads the given param from the url", () => {
|
||||
locationMock.replace(
|
||||
"https://langfuse.com?orderBy=column-Column 3_order-DESC",
|
||||
);
|
||||
|
||||
const result = render(
|
||||
<QueryParamProvider adapter={NextAdapterPages}>
|
||||
<UseOrderByStateExample />
|
||||
</QueryParamProvider>,
|
||||
);
|
||||
|
||||
expect(result.queryByText(/column Column 3, order DESC/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
const UseOrderByStateExample = () => {
|
||||
const [orderBy, setOrderBy] = useOrderByState({
|
||||
column: "Column 1",
|
||||
order: "DESC",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>{`column ${orderBy?.column}, order ${orderBy?.order}`}</h1>
|
||||
<button
|
||||
onClick={() => {
|
||||
setOrderBy({
|
||||
column: "Column 2",
|
||||
order: "ASC",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Set Order
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
|
||||
import { tracesTableCols } from "@/src/server/api/definitions/tracesTable";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
// The test for the orderByToPrisma function
|
||||
describe("orderByToPrisma (Convert orderBy to Prisma.sql)", () => {
|
||||
test("orderByToPrisma returns default sql when orderBy=null", () => {
|
||||
expect(orderByToPrismaSql(null, tracesTableCols)).toStrictEqual(
|
||||
Prisma.sql`ORDER BY t.timestamp DESC`,
|
||||
);
|
||||
});
|
||||
|
||||
test("orderByToPrisma returns correct clause for orderBy column included in column defs", () => {
|
||||
expect(
|
||||
orderByToPrismaSql(
|
||||
{
|
||||
column: "latency",
|
||||
order: "ASC",
|
||||
},
|
||||
tracesTableCols,
|
||||
),
|
||||
).toStrictEqual(Prisma.sql`ORDER BY tl.latency ASC`);
|
||||
});
|
||||
|
||||
test("orderByToPrisma throws error for orderBy column not included in column defs", () => {
|
||||
expect(() =>
|
||||
orderByToPrismaSql(
|
||||
{
|
||||
column: "InvalidCol",
|
||||
order: "ASC",
|
||||
},
|
||||
tracesTableCols,
|
||||
),
|
||||
).toThrow(/Invalid filter column: InvalidCol/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import { type Router } from "next/router";
|
||||
import type { UrlObject } from "url";
|
||||
import { type LocationMock } from "@jedmao/location";
|
||||
|
||||
type PartialRouter = Partial<Router>;
|
||||
|
||||
export const BASE_URL = "https://langfuse.com";
|
||||
|
||||
/**
|
||||
* A Router to be used for testing which provides the bare minimum needed
|
||||
* for the useQueryParam(s) hook and NextAdapter to work.
|
||||
*/
|
||||
export class TestRouter implements PartialRouter {
|
||||
isReady = true;
|
||||
pathname = "/";
|
||||
private currentUrl = "";
|
||||
private history: string[] = [];
|
||||
|
||||
constructor(private locationMock: LocationMock) {}
|
||||
|
||||
replace = (url: string | UrlObject) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
|
||||
this.locationMock.assign(`${BASE_URL}${url}`);
|
||||
this.currentUrl = TestRouter.getURLString(url);
|
||||
this.locationMock.assign(`${BASE_URL}${this.currentUrl}`);
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
push = (url: string | UrlObject) => {
|
||||
this.history.push(this.currentUrl);
|
||||
this.currentUrl = TestRouter.getURLString(url);
|
||||
this.locationMock.assign(`${BASE_URL}${this.currentUrl}`);
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
setIsReady = (isReady: boolean) => {
|
||||
this.isReady = isReady;
|
||||
};
|
||||
|
||||
get asPath() {
|
||||
return this.pathname;
|
||||
}
|
||||
|
||||
static getURLString(url: string | UrlObject): string {
|
||||
if (typeof url === "string") {
|
||||
return url;
|
||||
}
|
||||
return `${url.pathname}${url.search}`;
|
||||
}
|
||||
|
||||
getParams(): URLSearchParams {
|
||||
return new URL(`${BASE_URL}${this.currentUrl}`).searchParams;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
describe("/api/public/generations API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
[
|
||||
{
|
||||
|
||||
@@ -107,6 +107,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
tags: ["tag-1", "tag-2"],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -185,6 +186,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbTrace[0]?.projectId).toBe(
|
||||
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
);
|
||||
expect(dbTrace[0]?.tags).toEqual(["tag-1", "tag-2"]);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
@@ -253,6 +255,14 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
const eventId = v4();
|
||||
const scoreId = v4();
|
||||
|
||||
const exception = `
|
||||
ERROR langfuse:callback.py:677 'model_name'
|
||||
Traceback (most recent call last):
|
||||
File "/Users/maximiliandeichmann/development/github.com/langfuse/langfuse-python/langfuse/callback.py", line 674, in __on_llm_action
|
||||
model_name = kwargs["invocation_params"]["model_name"]
|
||||
KeyError: 'model_name'
|
||||
`;
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
@@ -290,6 +300,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
parentObservationId: spanId,
|
||||
modelParameters: { someKey: ["user-1", "user-2"] },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -324,6 +335,14 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
observationId: generationId,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "sdk-log",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
log: exception,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -358,6 +377,9 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbGeneration?.traceId).toBe(traceId);
|
||||
expect(dbGeneration?.name).toBe("generation-name");
|
||||
expect(dbGeneration?.parentObservationId).toBe(spanId);
|
||||
expect(dbGeneration?.modelParameters).toEqual({
|
||||
someKey: ["user-1", "user-2"],
|
||||
});
|
||||
|
||||
const dbEvent = await prisma.observation.findUnique({
|
||||
where: {
|
||||
@@ -380,6 +402,19 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbScore?.traceId).toBe(traceId);
|
||||
expect(dbScore?.observationId).toBe(generationId);
|
||||
expect(dbScore?.value).toBe(100.5);
|
||||
|
||||
const logEvent = await prisma.events.findFirst({
|
||||
where: {
|
||||
data: {
|
||||
path: ["body", "log"],
|
||||
string_contains: "ERROR",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(logEvent).toBeDefined();
|
||||
expect(logEvent).not.toBeFalsy();
|
||||
expect(JSON.stringify(logEvent?.data)).toContain("KeyError: 'model_name'");
|
||||
});
|
||||
|
||||
it("should upsert threats", async () => {
|
||||
@@ -759,6 +794,66 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbTrace[0]?.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should not override a trace from a different project", async () => {
|
||||
const traceId = v4();
|
||||
const newProjectId = v4();
|
||||
|
||||
await prisma.project.create({
|
||||
data: {
|
||||
id: newProjectId,
|
||||
name: "another-project",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.trace.create({
|
||||
data: {
|
||||
id: traceId,
|
||||
project: { connect: { id: newProjectId } },
|
||||
},
|
||||
});
|
||||
|
||||
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(responseOne.status).toBe(207);
|
||||
|
||||
console.log(responseOne.body);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const errors = responseOne.body.errors;
|
||||
|
||||
expect(errors).toBeDefined();
|
||||
console.log(errors);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
expect(errors.length).toBe(1);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toEqual(1);
|
||||
expect(dbTrace[0]?.name).toBeNull();
|
||||
expect(dbTrace[0]?.release).toBeNull();
|
||||
expect(dbTrace[0]?.metadata).toBeNull();
|
||||
expect(dbTrace[0]?.version).toBeNull();
|
||||
});
|
||||
|
||||
[
|
||||
{
|
||||
inputs: [{ a: "a" }, { b: "b" }],
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { v4 as uuidv4, v4 } from "uuid";
|
||||
import { type Prompt } from "@prisma/client";
|
||||
|
||||
describe("/api/public/prompts API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
|
||||
it("should fetch a prompt", async () => {
|
||||
const promptId = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name&version=1",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isPrompt(fetchedObservations.body)) {
|
||||
throw new Error("Expected body to be a prompt");
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.id).toBe(promptId);
|
||||
expect(fetchedObservations.body.name).toBe("prompt-name");
|
||||
expect(fetchedObservations.body.prompt).toBe("prompt");
|
||||
expect(fetchedObservations.body.version).toBe(1);
|
||||
expect(fetchedObservations.body.isActive).toBe(true);
|
||||
expect(fetchedObservations.body.createdBy).toBe("user-1");
|
||||
});
|
||||
|
||||
it("should fetch active prompt only if no prompt version is given", async () => {
|
||||
const promptId = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: false,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(404);
|
||||
});
|
||||
|
||||
it("should fetch inactive prompt if prompt version is given", async () => {
|
||||
const promptId = uuidv4();
|
||||
const promptTwoId = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt-one",
|
||||
isActive: false,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptTwoId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 2,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name&version=1",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isPrompt(fetchedObservations.body)) {
|
||||
throw new Error("Expected body to be a prompt");
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.id).toBe(promptId);
|
||||
expect(fetchedObservations.body.name).toBe("prompt-name");
|
||||
expect(fetchedObservations.body.prompt).toBe("prompt-one");
|
||||
expect(fetchedObservations.body.version).toBe(1);
|
||||
expect(fetchedObservations.body.isActive).toBe(false);
|
||||
expect(fetchedObservations.body.createdBy).toBe("user-1");
|
||||
});
|
||||
|
||||
it("should fetch active prompt when multiple exist", async () => {
|
||||
const promptIdOne = uuidv4();
|
||||
const promptIdTwo = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptIdOne,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: false,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptIdTwo,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 2,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isPrompt(fetchedObservations.body)) {
|
||||
throw new Error("Expected body to be a prompt");
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.id).toBe(promptIdTwo);
|
||||
expect(fetchedObservations.body.name).toBe("prompt-name");
|
||||
expect(fetchedObservations.body.prompt).toBe("prompt");
|
||||
expect(fetchedObservations.body.version).toBe(2);
|
||||
expect(fetchedObservations.body.isActive).toBe(true);
|
||||
expect(fetchedObservations.body.createdBy).toBe("user-1");
|
||||
});
|
||||
|
||||
it("should create and fetch a prompt", async () => {
|
||||
await makeAPICall("POST", "/api/public/prompts", {
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name&version=1",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isPrompt(fetchedObservations.body)) {
|
||||
throw new Error("Expected body to be an array of observations");
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.name).toBe("prompt-name");
|
||||
expect(fetchedObservations.body.prompt).toBe("prompt");
|
||||
expect(fetchedObservations.body.version).toBe(1);
|
||||
expect(fetchedObservations.body.isActive).toBe(true);
|
||||
expect(fetchedObservations.body.createdBy).toBe("API");
|
||||
});
|
||||
|
||||
it("should relate generation to prompt", async () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
|
||||
const promptId = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
metadata: {
|
||||
sdk_verion: "1.0.0",
|
||||
sdk_name: "python",
|
||||
},
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "generation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
promptName: "prompt-name",
|
||||
promptVersion: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
console.log("response body", response.body);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.promptId).toBe(promptId);
|
||||
});
|
||||
|
||||
it("should fail if prompt version is missing", async () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
|
||||
const promptId = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
metadata: {
|
||||
sdk_verion: "1.0.0",
|
||||
sdk_name: "python",
|
||||
},
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "generation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
promptName: "prompt-name",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
console.log("response body", response.body);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const isPrompt = (x: unknown): x is Prompt => {
|
||||
if (typeof x !== "object" || x === null) return false;
|
||||
const prompt = x as Prompt;
|
||||
return (
|
||||
typeof prompt.id === "string" &&
|
||||
typeof prompt.name === "string" &&
|
||||
typeof prompt.version === "number" &&
|
||||
typeof prompt.prompt === "string" &&
|
||||
typeof prompt.isActive === "boolean" &&
|
||||
typeof prompt.projectId === "string" &&
|
||||
typeof prompt.createdBy === "string"
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
/** @jest-environment node */
|
||||
import { pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { appRouter } from "@/src/server/api/root";
|
||||
import { createInnerTRPCContext } from "@/src/server/api/trpc";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import type { Session } from "next-auth";
|
||||
|
||||
describe("Traces TRPC Router", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
const session: Session = {
|
||||
expires: "1",
|
||||
user: {
|
||||
id: "clgb17vnp000008jjere5g15i",
|
||||
name: "John Doe",
|
||||
projects: [
|
||||
{
|
||||
id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
role: "ADMIN",
|
||||
name: "test",
|
||||
},
|
||||
],
|
||||
featureFlags: {
|
||||
templateFlag: true,
|
||||
},
|
||||
admin: true,
|
||||
},
|
||||
};
|
||||
|
||||
const ctx = createInnerTRPCContext({ session });
|
||||
const caller = appRouter.createCaller({ ...ctx, prisma });
|
||||
|
||||
test("traces.all RPC returns an array of traces", async () => {
|
||||
const trace = {
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
};
|
||||
await prisma.trace.create({
|
||||
data: { ...trace, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
});
|
||||
|
||||
const traces = await caller.traces.all({
|
||||
page: 0,
|
||||
limit: 10,
|
||||
// projectId from `seed.ts`
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: null,
|
||||
searchQuery: "",
|
||||
orderBy: null,
|
||||
});
|
||||
expect(traces).toBeDefined();
|
||||
expect(traces).toMatchObject({ traces: [trace] });
|
||||
});
|
||||
|
||||
test("traces.all RPC orders traces by userId", async () => {
|
||||
const traceTmpl = {
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
};
|
||||
const trace1 = traceTmpl;
|
||||
const trace2 = { ...traceTmpl, userId: "user-2" };
|
||||
await prisma.trace.create({
|
||||
data: { ...traceTmpl, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
});
|
||||
await prisma.trace.create({
|
||||
data: {
|
||||
...trace2,
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
},
|
||||
});
|
||||
|
||||
const tracesASC = await caller.traces.all({
|
||||
page: 0,
|
||||
limit: 10,
|
||||
// projectId from `seed.ts`
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: null,
|
||||
searchQuery: "",
|
||||
orderBy: {
|
||||
column: "userId",
|
||||
order: "ASC",
|
||||
},
|
||||
});
|
||||
expect(tracesASC).toMatchObject({ traces: [trace1, trace2] });
|
||||
|
||||
const tracesDESC = await caller.traces.all({
|
||||
page: 0,
|
||||
limit: 10,
|
||||
// projectId from `seed.ts`
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: null,
|
||||
searchQuery: "",
|
||||
orderBy: {
|
||||
column: "userId",
|
||||
order: "DESC",
|
||||
},
|
||||
});
|
||||
expect(tracesDESC).toMatchObject({ traces: [trace2, trace1] });
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,8 @@ export const pruneDatabase = async () => {
|
||||
await prisma.datasetItem.deleteMany();
|
||||
await prisma.dataset.deleteMany();
|
||||
await prisma.datasetRuns.deleteMany();
|
||||
await prisma.prompt.deleteMany();
|
||||
await prisma.events.deleteMany();
|
||||
};
|
||||
|
||||
export function createBasicAuthHeader(
|
||||
|
||||
@@ -40,7 +40,6 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
id: "trace-id",
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
@@ -65,7 +64,6 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-id",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "5.0.0",
|
||||
@@ -88,4 +86,54 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
userId: "user-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("should use tags correctly on POST and GET", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-1",
|
||||
tags: ["tag-1", "tag-2", "tag-3"],
|
||||
});
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-2",
|
||||
tags: ["tag-1"],
|
||||
});
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-3",
|
||||
tags: ["tag-2", "tag-3"],
|
||||
});
|
||||
|
||||
// multiple tags
|
||||
const traces = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/traces?tags=tag-2&tags=tag-3",
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const traceIds = traces.body.data.map((t: { id: string }) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds).toEqual(["trace-3", "trace-1"]);
|
||||
|
||||
// single tag
|
||||
const traces2 = await makeAPICall("GET", "/api/public/traces?tags=tag-1");
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const traceIds2 = traces2.body.data.map((t: { id: string }) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds2).toEqual(["trace-2", "trace-1"]);
|
||||
|
||||
// wrong tag
|
||||
const traces3 = await makeAPICall("GET", "/api/public/traces?tags=tag-10");
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const traceIds3 = traces3.body.data.map((t: { id: string }) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds3).toEqual([]);
|
||||
|
||||
// no tag
|
||||
const traces4 = await makeAPICall("GET", "/api/public/traces?tags=");
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const traceIds4 = traces4.body.data.map((t: { id: string }) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds4).toEqual(["trace-3", "trace-2", "trace-1"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,10 +4,13 @@ describe("Token Count Functions", () => {
|
||||
describe("token count for strings", () => {
|
||||
[
|
||||
{ model: "gpt-3.5", tokens: 114 },
|
||||
{ model: "gpt-35", tokens: 114 },
|
||||
{ model: "gpt-4-1106-preview", tokens: 114 },
|
||||
{ model: "gpt-4-vision-preview", tokens: 114 },
|
||||
{ model: "claude", tokens: 118 },
|
||||
{ model: "claude-instant-1.2", tokens: 118 },
|
||||
{ model: "gpt-3.5-turbo-1106", tokens: 114 },
|
||||
{ model: "gpt-35-turbo-1106", tokens: 114 },
|
||||
].forEach(({ model, tokens }) => {
|
||||
it(`should return token count ${tokens} for ${model}`, () => {
|
||||
const result = tokenCount({
|
||||
@@ -70,6 +73,7 @@ describe("Token Count Functions", () => {
|
||||
[
|
||||
{ model: "gpt-4", tokens: 44 },
|
||||
{ model: "gpt-3.5-turbo-16k-0613", tokens: 44 },
|
||||
{ model: "gpt-35-turbo-16k-0613", tokens: 44 },
|
||||
{ model: "claude-instant-1.2", tokens: 48 },
|
||||
].forEach(({ model, tokens }) => {
|
||||
it(`should return token count ${tokens} for ${model}`, () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { VERSION } from "@/src/constants/VERSION";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
export const LangfuseIcon = ({
|
||||
@@ -20,9 +21,11 @@ export const LangfuseIcon = ({
|
||||
export const LangfuseLogo = ({
|
||||
className,
|
||||
size = "sm",
|
||||
version = false,
|
||||
}: {
|
||||
size?: "sm" | "xl";
|
||||
className?: string;
|
||||
version?: boolean;
|
||||
}) => (
|
||||
<div className={cn("flex items-center", className)}>
|
||||
<LangfuseIcon size={size === "sm" ? 16 : 20} />
|
||||
@@ -34,5 +37,16 @@ export const LangfuseLogo = ({
|
||||
>
|
||||
Langfuse
|
||||
</span>
|
||||
{version && (
|
||||
<a
|
||||
href="https://github.com/langfuse/langfuse/releases"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
title="View releases on GitHub"
|
||||
className="ml-2 text-xs text-gray-400"
|
||||
>
|
||||
{VERSION}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -191,7 +191,7 @@ export function DatePickerWithRange({
|
||||
{DEFAULT_DATE_RANGE_SELECTION}
|
||||
</SelectItem>
|
||||
{dateTimeAggregationOptions.toReversed().map((item) => (
|
||||
<SelectItem key={item} value={`${item}`}>
|
||||
<SelectItem key={item} value={item}>
|
||||
{item}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -3,13 +3,19 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useSession } from "next-auth/react";
|
||||
import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
import { type Status, StatusBadge } from "./status-badge";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
export default function Header(props: {
|
||||
export default function Header({
|
||||
level = "h2",
|
||||
...props
|
||||
}: {
|
||||
title: string;
|
||||
breadcrumb?: { name: string; href?: string }[];
|
||||
live?: boolean;
|
||||
status?: Status;
|
||||
help?: { description: string; href: string };
|
||||
actionButtons?: React.ReactNode;
|
||||
level?: "h2" | "h3";
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const session = useSession();
|
||||
@@ -34,7 +40,7 @@ export default function Header(props: {
|
||||
[...props.breadcrumb.map((i) => i.href).filter(Boolean)].pop();
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className={cn(level === "h2" ? "mb-8" : "mb-1")}>
|
||||
<div>
|
||||
{backHref ? (
|
||||
<nav className="sm:hidden" aria-label="Back">
|
||||
@@ -50,7 +56,7 @@ export default function Header(props: {
|
||||
</Link>
|
||||
</nav>
|
||||
) : null}
|
||||
{breadcrumb.length ? (
|
||||
{(level === "h2" || props.breadcrumb) && breadcrumb.length ? (
|
||||
<nav className="hidden sm:flex" aria-label="Breadcrumb">
|
||||
<ol role="list" className="flex items-center space-x-4">
|
||||
{breadcrumb.map(({ name, href }, index) => (
|
||||
@@ -84,9 +90,15 @@ export default function Header(props: {
|
||||
<div className="mt-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-3 md:gap-5">
|
||||
<div className="flex min-w-0 flex-row">
|
||||
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||
{props.title}
|
||||
</h2>
|
||||
{level === "h2" ? (
|
||||
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||
{props.title}
|
||||
</h2>
|
||||
) : (
|
||||
<h3 className="text-lg font-bold leading-7 text-gray-900 sm:truncate sm:text-xl sm:tracking-tight">
|
||||
{props.title}
|
||||
</h3>
|
||||
)}
|
||||
{props.help ? (
|
||||
<DocPopup
|
||||
description={props.help.description}
|
||||
@@ -95,15 +107,7 @@ export default function Header(props: {
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{props.live ? (
|
||||
<div className="flex items-center gap-2 rounded-sm bg-green-100 px-3 text-green-600">
|
||||
<span className="relative flex h-2 w-2 ">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-600"></span>
|
||||
</span>
|
||||
Live
|
||||
</div>
|
||||
) : null}
|
||||
{props.status && <StatusBadge type={props.status} />}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{props.actionButtons ?? null}
|
||||
|
||||
+265
-185
@@ -1,12 +1,12 @@
|
||||
import { ROUTES } from "@/src/components/layouts/routes";
|
||||
import { ROUTES, type Route } from "@/src/components/layouts/routes";
|
||||
import { Fragment, type PropsWithChildren, useState } from "react";
|
||||
import { Dialog, Menu, Transition } from "@headlessui/react";
|
||||
import { Dialog, Disclosure, Menu, Transition } from "@headlessui/react";
|
||||
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import clsx from "clsx";
|
||||
import { Code, MessageSquarePlus, Info } from "lucide-react";
|
||||
import { Code, MessageSquarePlus, Info, ChevronRightIcon } from "lucide-react";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
@@ -23,6 +23,7 @@ import Head from "next/head";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { LangfuseLogo } from "@/src/components/LangfuseLogo";
|
||||
import { Spinner } from "@/src/components/layouts/spinner";
|
||||
import { hasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
|
||||
const userNavigation = [
|
||||
{
|
||||
@@ -34,9 +35,9 @@ const userNavigation = [
|
||||
},
|
||||
];
|
||||
|
||||
const pathsWithoutNavigation: string[] = [];
|
||||
const unauthenticatedPaths = ["/auth/sign-in", "/auth/sign-up"];
|
||||
const publishablePaths = [
|
||||
const pathsWithoutNavigation: string[] = ["/onboarding"];
|
||||
const unauthenticatedPaths: string[] = ["/auth/sign-in", "/auth/sign-up"];
|
||||
const publishablePaths: string[] = [
|
||||
"/project/[projectId]/sessions/[sessionId]",
|
||||
"/project/[projectId]/traces/[traceId]",
|
||||
];
|
||||
@@ -49,25 +50,47 @@ export default function Layout(props: PropsWithChildren) {
|
||||
api.environment.enableExperimentalFeatures.useQuery().data ?? false;
|
||||
|
||||
const projectId = router.query.projectId as string | undefined;
|
||||
const navigation = ROUTES.filter(
|
||||
({ pathname }) => projectId || !pathname.includes("[projectId]"),
|
||||
)
|
||||
.filter(
|
||||
({ featureFlag }) =>
|
||||
featureFlag === undefined ||
|
||||
|
||||
const mapNavigation = (route: Route): NavigationItem | null => {
|
||||
// Project-level routes
|
||||
if (!projectId && route.pathname?.includes("[projectId]")) return null;
|
||||
|
||||
// Feature Flags
|
||||
if (
|
||||
!(
|
||||
route.featureFlag === undefined ||
|
||||
enableExperimentalFeatures ||
|
||||
session.data?.user?.featureFlags[featureFlag],
|
||||
session.data?.user?.featureFlags[route.featureFlag]
|
||||
)
|
||||
)
|
||||
.map(({ pathname, ...rest }) => ({
|
||||
pathname,
|
||||
href: pathname.replace("[projectId]", projectId ?? ""),
|
||||
...rest,
|
||||
}))
|
||||
.map(({ pathname, ...rest }) => ({
|
||||
pathname,
|
||||
current: router.pathname === pathname,
|
||||
...rest,
|
||||
}));
|
||||
return null;
|
||||
|
||||
// RBAC
|
||||
if (
|
||||
route.rbacScope !== undefined &&
|
||||
(!projectId || !hasAccess({ projectId, scope: route.rbacScope, session }))
|
||||
)
|
||||
return null;
|
||||
|
||||
// apply to children as well
|
||||
const children: (NavigationItem | null)[] =
|
||||
route.children?.map((child) => mapNavigation(child)).filter(Boolean) ??
|
||||
[];
|
||||
return {
|
||||
...route,
|
||||
href: route.pathname?.replace("[projectId]", projectId ?? ""),
|
||||
current: router.pathname === route.pathname,
|
||||
children:
|
||||
children.length > 0
|
||||
? (children as NavigationItem[]) // does not include null due to filter
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const navigationMapped: (NavigationItem | null)[] = ROUTES.map((route) =>
|
||||
mapNavigation(route),
|
||||
).filter(Boolean);
|
||||
const navigation = navigationMapped.filter(Boolean) as NavigationItem[]; // does not include null due to filter
|
||||
|
||||
const currentPathName = navigation.find(({ current }) => current)?.name;
|
||||
|
||||
@@ -118,7 +141,6 @@ export default function Layout(props: PropsWithChildren) {
|
||||
{props.children}
|
||||
</main>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -198,58 +220,10 @@ export default function Layout(props: PropsWithChildren) {
|
||||
</Transition.Child>
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 py-4">
|
||||
<LangfuseLogo size="xl" />
|
||||
<LangfuseLogo version size="xl" />
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={clsx(
|
||||
item.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={clsx(
|
||||
item.current
|
||||
? "text-indigo-600"
|
||||
: "text-gray-400 group-hover:text-indigo-600",
|
||||
"h-6 w-6 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
<FeedbackButtonWrapper className="w-full">
|
||||
<li className="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
|
||||
<MessageSquarePlus
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Feedback
|
||||
</li>
|
||||
</FeedbackButtonWrapper>
|
||||
</ul>
|
||||
</li>
|
||||
<MainNavigation nav={navigation} />
|
||||
<li>
|
||||
<div className="flex flex-row place-content-between items-center">
|
||||
<div className="text-xs font-semibold leading-6 text-gray-400">
|
||||
@@ -312,59 +286,10 @@ export default function Layout(props: PropsWithChildren) {
|
||||
<div className="hidden xl:fixed xl:inset-y-0 xl:z-50 xl:flex xl:w-72 xl:flex-col">
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex h-screen grow flex-col gap-y-5 border-r border-gray-200 bg-white pt-7">
|
||||
<LangfuseLogo size="xl" className="mb-2 px-6" />
|
||||
<LangfuseLogo version size="xl" className="mb-2 px-6" />
|
||||
<nav className="flex h-full flex-1 flex-col overflow-y-auto px-6 pb-3">
|
||||
<ul role="list" className="flex h-full flex-col gap-y-4">
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={clsx(
|
||||
item.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={clsx(
|
||||
item.current
|
||||
? "text-indigo-600"
|
||||
: "text-gray-400 group-hover:text-indigo-600",
|
||||
"h-6 w-6 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
<FeedbackButtonWrapper className="w-full">
|
||||
<li className="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
|
||||
<MessageSquarePlus
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Feedback
|
||||
</li>
|
||||
</FeedbackButtonWrapper>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<MainNavigation nav={navigation} />
|
||||
<li className="mt-auto">
|
||||
<div className="flex flex-row place-content-between items-center">
|
||||
<div className="text-xs font-semibold leading-6 text-gray-400">
|
||||
@@ -417,64 +342,60 @@ export default function Layout(props: PropsWithChildren) {
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li className="-mx-6">
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button className="flex w-full items-center gap-x-4 p-1.5 px-6 py-3 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage
|
||||
src={session.data?.user?.image ?? undefined}
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{session.data?.user?.name
|
||||
? session.data.user.name
|
||||
.split(" ")
|
||||
.map((word) => word[0])
|
||||
.slice(0, 2)
|
||||
.concat("")
|
||||
: null}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="flex-shrink truncate text-sm font-semibold leading-6 text-gray-900">
|
||||
{session.data?.user?.name}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute -top-full right-0 z-10 mt-2.5 w-32 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||
{userNavigation.map((item) => (
|
||||
<Menu.Item key={item.name}>
|
||||
{({ active }) => (
|
||||
<a
|
||||
onClick={() => void item.onClick()}
|
||||
className={cn(
|
||||
active ? "bg-gray-50" : "",
|
||||
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<Menu as="div" className="relative left-1">
|
||||
<Menu.Button className="flex w-full items-center gap-x-4 p-1.5 px-6 py-3 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={session.data?.user?.image ?? undefined} />
|
||||
<AvatarFallback>
|
||||
{session.data?.user?.name
|
||||
? session.data.user.name
|
||||
.split(" ")
|
||||
.map((word) => word[0])
|
||||
.slice(0, 2)
|
||||
.concat("")
|
||||
: null}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="flex-shrink truncate text-sm font-semibold leading-6 text-gray-900">
|
||||
{session.data?.user?.name}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute -top-full right-0 z-10 mt-2.5 w-32 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||
{userNavigation.map((item) => (
|
||||
<Menu.Item key={item.name}>
|
||||
{({ active }) => (
|
||||
<a
|
||||
onClick={() => void item.onClick()}
|
||||
className={cn(
|
||||
active ? "bg-gray-50" : "",
|
||||
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -487,7 +408,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
<span className="sr-only">Open sidebar</span>
|
||||
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
<LangfuseLogo className="flex-1" />
|
||||
<LangfuseLogo version className="flex-1" />
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button className="flex items-center gap-x-4 text-sm font-semibold leading-6 text-gray-900">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
@@ -564,11 +485,170 @@ export default function Layout(props: PropsWithChildren) {
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<main className="py-4">
|
||||
<div className="px-4">{props.children}</div>
|
||||
</main>
|
||||
<main className="p-4">{props.children}</main>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type NavigationItem = NestedNavigationItem & {
|
||||
children?: NestedNavigationItem[];
|
||||
};
|
||||
|
||||
type NestedNavigationItem = Omit<Route, "children"> & {
|
||||
href?: string;
|
||||
current: boolean;
|
||||
};
|
||||
|
||||
const MainNavigation: React.FC<{
|
||||
nav: NavigationItem[];
|
||||
onNavitemClick?: () => void;
|
||||
}> = ({ nav, onNavitemClick }) => {
|
||||
const STORAGE_KEY = "sidebar-tracing-default-open";
|
||||
const getDefaultOpen = () => {
|
||||
const savedState = localStorage.getItem(STORAGE_KEY);
|
||||
if (savedState !== null) {
|
||||
try {
|
||||
return JSON.parse(savedState) as boolean;
|
||||
} catch (e) {
|
||||
console.error("Error parsing saved state: ", e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleDropDownClick = () => {
|
||||
const savedState = localStorage.getItem(STORAGE_KEY);
|
||||
const isOpen =
|
||||
savedState !== null ? (JSON.parse(savedState) as boolean) : false;
|
||||
const newState = !isOpen;
|
||||
localStorage.setItem(
|
||||
"sidebar-tracing-default-open",
|
||||
JSON.stringify(newState),
|
||||
);
|
||||
};
|
||||
return (
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{nav.map((item) => (
|
||||
<li key={item.name}>
|
||||
{(!item.children || item.children.length === 0) && item.href ? (
|
||||
<Link
|
||||
href={item.href}
|
||||
className={clsx(
|
||||
item.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
|
||||
)}
|
||||
onClick={onNavitemClick}
|
||||
>
|
||||
{item.icon && (
|
||||
<item.icon
|
||||
className={clsx(
|
||||
item.current
|
||||
? "text-indigo-600"
|
||||
: "text-gray-400 group-hover:text-indigo-600",
|
||||
"h-6 w-6 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{item.name}
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
) : item.children && item.children.length > 0 ? (
|
||||
<Disclosure
|
||||
as="div"
|
||||
defaultOpen={
|
||||
item.children.some((child) => child.current) ||
|
||||
getDefaultOpen()
|
||||
}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button
|
||||
className="group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold leading-6 hover:bg-gray-50 hover:text-indigo-600"
|
||||
onClick={handleDropDownClick}
|
||||
>
|
||||
{item.icon && (
|
||||
<item.icon
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{item.name}
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
<ChevronRightIcon
|
||||
className={clsx(
|
||||
open ? "rotate-90 text-gray-500" : "text-gray-400",
|
||||
"ml-auto h-5 w-5 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel as="ul" className="mt-1 px-2">
|
||||
{item.children?.map((subItem) => (
|
||||
<li key={subItem.name}>
|
||||
{/* 44px */}
|
||||
<Link
|
||||
href={subItem.href ?? "#"}
|
||||
className={clsx(
|
||||
subItem.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"flex w-full items-center gap-x-3 rounded-md py-2 pl-9 pr-2 text-sm leading-6",
|
||||
)}
|
||||
>
|
||||
{subItem.name}
|
||||
{subItem.label && (
|
||||
<span className="self-center whitespace-nowrap break-keep rounded-sm border border-gray-200 px-1 py-0.5 text-xs text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600">
|
||||
{subItem.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
<FeedbackButtonWrapper className="w-full">
|
||||
<li className="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
|
||||
<MessageSquarePlus
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Feedback
|
||||
</li>
|
||||
</FeedbackButtonWrapper>
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,54 +1,66 @@
|
||||
import { type Flag } from "@/src/features/feature-flags/types";
|
||||
import { type Scope } from "@/src/features/rbac/constants/roleAccessRights";
|
||||
import {
|
||||
Database,
|
||||
LayoutDashboard,
|
||||
LifeBuoy,
|
||||
LineChart,
|
||||
ListTree,
|
||||
type LucideIcon,
|
||||
Settings,
|
||||
TextSelect,
|
||||
UsersIcon,
|
||||
Route,
|
||||
PenSquareIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
export const ROUTES: Array<{
|
||||
export type Route = {
|
||||
name: string;
|
||||
pathname: string;
|
||||
icon: LucideIcon;
|
||||
featureFlag?: Flag;
|
||||
label?: string;
|
||||
}> = [
|
||||
rbacScope?: Scope;
|
||||
icon?: LucideIcon; // ignored for nested routes
|
||||
pathname?: string; // link, ignored if children
|
||||
children?: Array<Route>; // folder
|
||||
};
|
||||
|
||||
export const ROUTES: Route[] = [
|
||||
{
|
||||
name: "Dashboard",
|
||||
pathname: `/project/[projectId]`,
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{
|
||||
name: "Sessions",
|
||||
pathname: `/project/[projectId]/sessions`,
|
||||
icon: Route,
|
||||
},
|
||||
{
|
||||
name: "Traces",
|
||||
pathname: `/project/[projectId]/traces`,
|
||||
name: "Tracing",
|
||||
icon: ListTree,
|
||||
},
|
||||
{
|
||||
name: "Generations",
|
||||
pathname: `/project/[projectId]/generations`,
|
||||
icon: TextSelect,
|
||||
},
|
||||
{
|
||||
name: "Scores",
|
||||
pathname: `/project/[projectId]/scores`,
|
||||
icon: LineChart,
|
||||
children: [
|
||||
{
|
||||
name: "Traces",
|
||||
pathname: `/project/[projectId]/traces`,
|
||||
},
|
||||
{
|
||||
name: "Sessions",
|
||||
pathname: `/project/[projectId]/sessions`,
|
||||
},
|
||||
{
|
||||
name: "Generations",
|
||||
pathname: `/project/[projectId]/generations`,
|
||||
},
|
||||
{
|
||||
name: "Scores",
|
||||
pathname: `/project/[projectId]/scores`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Users",
|
||||
pathname: `/project/[projectId]/users`,
|
||||
icon: UsersIcon,
|
||||
},
|
||||
{
|
||||
name: "Prompts",
|
||||
pathname: "/project/[projectId]/prompts",
|
||||
icon: PenSquareIcon,
|
||||
label: "Beta",
|
||||
rbacScope: "prompts:read",
|
||||
},
|
||||
{
|
||||
name: "Datasets",
|
||||
pathname: `/project/[projectId]/datasets`,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
const statusCategories = {
|
||||
active: ["production", "live"],
|
||||
inactive: ["disabled", "inactive"],
|
||||
};
|
||||
|
||||
export type Status =
|
||||
(typeof statusCategories)[keyof typeof statusCategories][number];
|
||||
|
||||
export const StatusBadge = (props: { className?: string; type: Status }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 rounded-sm px-2 py-1 text-xs",
|
||||
statusCategories.active.includes(props.type)
|
||||
? " bg-green-100 text-green-600"
|
||||
: "bg-gray-100 text-gray-800",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<span className="relative inline-flex h-2 w-2">
|
||||
<span
|
||||
className={cn(
|
||||
"absolute inline-flex h-full w-full rounded-full opacity-75",
|
||||
statusCategories.active.includes(props.type)
|
||||
? "animate-ping bg-green-500"
|
||||
: "bg-gray-500",
|
||||
)}
|
||||
></span>
|
||||
<span
|
||||
className={cn(
|
||||
"relative inline-flex h-2 w-2 rounded-full ",
|
||||
statusCategories.active.includes(props.type)
|
||||
? "bg-green-600"
|
||||
: "bg-gray-600",
|
||||
)}
|
||||
></span>
|
||||
</span>
|
||||
<span>{props.type}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -8,13 +8,16 @@ import { Badge } from "@/src/components/ui/badge";
|
||||
import { Card } from "@/src/components/ui/card";
|
||||
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
|
||||
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
|
||||
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
|
||||
import { api } from "@/src/utils/api";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const SessionPage: React.FC<{
|
||||
sessionId: string;
|
||||
projectId: string;
|
||||
}> = ({ sessionId, projectId }) => {
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
const session = api.sessions.byId.useQuery(
|
||||
{
|
||||
sessionId,
|
||||
@@ -27,6 +30,15 @@ export const SessionPage: React.FC<{
|
||||
},
|
||||
},
|
||||
);
|
||||
useEffect(() => {
|
||||
if (session.isSuccess) {
|
||||
setDetailPageList(
|
||||
"traces",
|
||||
session.data.traces.map((t) => t.id),
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [session.isSuccess, session.data]);
|
||||
|
||||
if (session.error?.data?.code === "UNAUTHORIZED") return <NoAccessError />;
|
||||
|
||||
@@ -56,15 +68,22 @@ export const SessionPage: React.FC<{
|
||||
/>,
|
||||
<DetailPageNav
|
||||
key="nav"
|
||||
currentId={sessionId}
|
||||
path={(id) => `/project/${projectId}/sessions/${id}`}
|
||||
currentId={encodeURIComponent(sessionId)}
|
||||
path={(id) =>
|
||||
`/project/${projectId}/sessions/${encodeURIComponent(id)}`
|
||||
}
|
||||
listKey="sessions"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{session.data?.users.map((userId) => (
|
||||
<Link key={userId} href={`/project/${projectId}/users/${userId}`}>
|
||||
{session.data?.users.filter(Boolean).map((userId) => (
|
||||
<Link
|
||||
key={userId}
|
||||
href={`/project/${projectId}/users/${encodeURIComponent(
|
||||
userId ?? "",
|
||||
)}`}
|
||||
>
|
||||
<Badge>User ID: {userId}</Badge>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
+132
-24
@@ -1,55 +1,126 @@
|
||||
import { StarIcon } from "lucide-react";
|
||||
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
|
||||
import { useState } from "react";
|
||||
|
||||
export function StarToggle({
|
||||
value,
|
||||
disabled = false,
|
||||
onClick,
|
||||
size = "sm",
|
||||
isLoading,
|
||||
}: {
|
||||
value: boolean;
|
||||
disabled?: boolean;
|
||||
onClick: (value: boolean) => Promise<unknown>;
|
||||
size?: "sm" | "xs";
|
||||
isLoading: boolean;
|
||||
}) {
|
||||
const [cachedValue, setCachedValue] = useState<boolean | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const optimisticValue = cachedValue ?? value;
|
||||
|
||||
useEffect(() => {
|
||||
setCachedValue(null);
|
||||
}, [value]);
|
||||
|
||||
const handleBookmarkClick = async () => {
|
||||
if (disabled) return;
|
||||
setLoading(true);
|
||||
setCachedValue(!optimisticValue);
|
||||
await onClick(!optimisticValue);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size={size}
|
||||
onClick={() => void handleBookmarkClick()}
|
||||
onClick={() => void onClick(!value)}
|
||||
disabled={disabled}
|
||||
loading={loading}
|
||||
loading={isLoading}
|
||||
>
|
||||
<StarIcon
|
||||
className={`h-4 w-4 ${
|
||||
optimisticValue ? "fill-current text-yellow-500" : "text-gray-500"
|
||||
}`}
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
value ? "fill-current text-yellow-500" : "text-gray-500",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function StarTraceToggle({
|
||||
tracesFilter,
|
||||
projectId,
|
||||
traceId,
|
||||
value,
|
||||
size = "sm",
|
||||
}: {
|
||||
tracesFilter: RouterInput["traces"]["all"];
|
||||
projectId: string;
|
||||
traceId: string;
|
||||
value: boolean;
|
||||
size?: "sm" | "xs";
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const mutBookmarkTrace = api.traces.bookmark.useMutation({
|
||||
// Optimistic update
|
||||
// Tanstack docs: https://tanstack.com/query/v4/docs/react/guides/optimistic-updates
|
||||
|
||||
onMutate: async () => {
|
||||
// Cancel any outgoing refetches
|
||||
// (so they don't overwrite our optimistic update)
|
||||
await utils.traces.all.cancel();
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
// Snapshot the previous value
|
||||
const prev = utils.traces.all.getData(tracesFilter);
|
||||
|
||||
return { prev };
|
||||
},
|
||||
onError: (err, _newTodo, context) => {
|
||||
setIsLoading(false);
|
||||
// Rollback to the previous value if mutation fails
|
||||
console.log("error", err);
|
||||
utils.traces.all.setData(tracesFilter, context?.prev);
|
||||
},
|
||||
onSettled: () => {
|
||||
setIsLoading(false);
|
||||
utils.traces.all.setData(
|
||||
tracesFilter,
|
||||
(oldQueryData: RouterOutput["traces"]["all"] | undefined) => {
|
||||
return {
|
||||
totalCount: oldQueryData?.totalCount,
|
||||
traces: oldQueryData?.traces
|
||||
? oldQueryData.traces.map((trace) => {
|
||||
return {
|
||||
...trace,
|
||||
bookmarked:
|
||||
trace.id === traceId
|
||||
? !trace.bookmarked
|
||||
: trace.bookmarked,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
};
|
||||
},
|
||||
);
|
||||
void utils.traces.all.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<StarToggle
|
||||
value={value}
|
||||
size={size}
|
||||
disabled={!hasAccess}
|
||||
isLoading={isLoading}
|
||||
onClick={(value) =>
|
||||
mutBookmarkTrace.mutateAsync({
|
||||
projectId,
|
||||
traceId,
|
||||
bookmarked: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function StarTraceDetailsToggle({
|
||||
projectId,
|
||||
traceId,
|
||||
value,
|
||||
@@ -62,9 +133,44 @@ export function StarTraceToggle({
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const mutBookmarkTrace = api.traces.bookmark.useMutation({
|
||||
onSuccess: () => {
|
||||
void utils.traces.invalidate();
|
||||
onMutate: async () => {
|
||||
// Cancel any outgoing refetches
|
||||
// (so they don't overwrite our optimistic update)
|
||||
await utils.traces.byId.cancel();
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
// Snapshot the previous value
|
||||
const prevData = utils.traces.byId.getData({ traceId });
|
||||
|
||||
return { prevData };
|
||||
},
|
||||
onError: (err, _newTodo, context) => {
|
||||
setIsLoading(false);
|
||||
console.log("error", err);
|
||||
// Rollback to the previous value if mutation fails
|
||||
utils.traces.byId.setData({ traceId }, context?.prevData);
|
||||
},
|
||||
onSettled: () => {
|
||||
setIsLoading(false);
|
||||
|
||||
utils.traces.byId.setData(
|
||||
{ traceId },
|
||||
(oldQueryData: RouterOutput["traces"]["byId"] | undefined) => {
|
||||
return oldQueryData
|
||||
? {
|
||||
...oldQueryData,
|
||||
bookmarked: !oldQueryData.bookmarked,
|
||||
}
|
||||
: undefined;
|
||||
},
|
||||
);
|
||||
void utils.traces.byId.invalidate();
|
||||
void utils.traces.all.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -73,6 +179,7 @@ export function StarTraceToggle({
|
||||
value={value}
|
||||
size={size}
|
||||
disabled={!hasAccess}
|
||||
isLoading={isLoading}
|
||||
onClick={(value) =>
|
||||
mutBookmarkTrace.mutateAsync({
|
||||
projectId,
|
||||
@@ -107,6 +214,7 @@ export function StarSessionToggle({
|
||||
<StarToggle
|
||||
value={value}
|
||||
size={size}
|
||||
isLoading={mutBookmarkSession.isLoading}
|
||||
disabled={!hasAccess}
|
||||
onClick={(value) =>
|
||||
mutBookmarkSession.mutateAsync({
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type ColumnFiltersState,
|
||||
getFilteredRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
type VisibilityState,
|
||||
} from "@tanstack/react-table";
|
||||
import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
import { DataTablePagination } from "@/src/components/table/data-table-pagination";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -19,10 +11,20 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/src/components/ui/table";
|
||||
import { type OrderByState } from "@/src/features/orderBy/types";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
useReactTable,
|
||||
type ColumnFiltersState,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
type VisibilityState,
|
||||
} from "@tanstack/react-table";
|
||||
import { useState } from "react";
|
||||
import { DataTablePagination } from "@/src/components/table/data-table-pagination";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: LangfuseColumnDef<TData, TValue>[];
|
||||
@@ -36,6 +38,8 @@ interface DataTableProps<TData, TValue> {
|
||||
setRowSelection?: OnChangeFn<RowSelectionState>;
|
||||
columnVisibility?: VisibilityState;
|
||||
onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
|
||||
orderBy?: OrderByState;
|
||||
setOrderBy?: (s: OrderByState) => void;
|
||||
help?: { description: string; href: string };
|
||||
}
|
||||
|
||||
@@ -55,8 +59,11 @@ export function DataTable<TData extends object, TValue>({
|
||||
columnVisibility,
|
||||
onColumnVisibilityChange,
|
||||
help,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: data.data ?? [],
|
||||
columns,
|
||||
@@ -93,17 +100,58 @@ export function DataTable<TData extends object, TValue>({
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const sortingEnabled =
|
||||
header.column.columnDef.enableSorting;
|
||||
return header.column.getIsVisible() ? (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className="whitespace-nowrap p-2"
|
||||
className={cn(
|
||||
sortingEnabled ? "cursor-pointer" : null,
|
||||
"whitespace-nowrap p-2",
|
||||
)}
|
||||
title={sortingEnabled ? "Sort by this column" : ""}
|
||||
onClick={(event) => {
|
||||
event.preventDefault(); // Add this line
|
||||
|
||||
if (
|
||||
!setOrderBy ||
|
||||
!header.column.columnDef.id ||
|
||||
!sortingEnabled
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (orderBy?.column === header.column.columnDef.id) {
|
||||
if (orderBy.order === "DESC") {
|
||||
setOrderBy({
|
||||
column: header.column.columnDef.id,
|
||||
order: "ASC",
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
} else {
|
||||
setOrderBy({
|
||||
column: header.column.columnDef.id,
|
||||
order: "DESC",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
{header.isPlaceholder ? null : (
|
||||
<>
|
||||
<div className="select-none">
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
|
||||
{orderBy?.column === header.column.columnDef.id
|
||||
? renderOrderingIndicator(orderBy)
|
||||
: null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</TableHead>
|
||||
) : null;
|
||||
})}
|
||||
@@ -126,7 +174,7 @@ export function DataTable<TData extends object, TValue>({
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className="overflow-hidden whitespace-nowrap p-2 text-xs"
|
||||
className="overflow-hidden whitespace-nowrap px-2 py-1 text-xs first:pl-2"
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
@@ -163,3 +211,9 @@ export function DataTable<TData extends object, TValue>({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderOrderingIndicator(orderBy?: OrderByState) {
|
||||
if (!orderBy) return;
|
||||
if (orderBy.order === "ASC") return <span className="ml-1">▲</span>;
|
||||
else return <span className="ml-1">▼</span>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
export type TableLinkProps = {
|
||||
path: string;
|
||||
@@ -11,21 +11,15 @@ export default function TableLink({
|
||||
value,
|
||||
truncateAt = 7,
|
||||
}: TableLinkProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
key="openTrace"
|
||||
className="rounded bg-indigo-50 px-2 py-1 text-xs font-semibold text-blue-600 shadow-sm hover:bg-indigo-100"
|
||||
onClick={() => {
|
||||
void router.push(path);
|
||||
}}
|
||||
>
|
||||
{value.length > truncateAt
|
||||
? `...${value.substring(value.length - truncateAt)}`
|
||||
: value}
|
||||
</button>
|
||||
</div>
|
||||
<Link
|
||||
className="inline-block rounded bg-indigo-50 px-2 py-1 text-xs font-semibold text-blue-600 shadow-sm hover:bg-indigo-100"
|
||||
href={path}
|
||||
title={value}
|
||||
>
|
||||
{value.length > truncateAt
|
||||
? `...${value.substring(value.length - truncateAt)}`
|
||||
: value}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from "@/src/components/ui/dropdown-menu";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { ChevronDownIcon, Loader } from "lucide-react";
|
||||
import { type ExportFileFormats } from "@/src/server/api/routers/generations";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import {
|
||||
NumberParam,
|
||||
@@ -31,6 +30,11 @@ import { type ObservationLevel } from "@prisma/client";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { LevelColors } from "@/src/components/level-colors";
|
||||
import { usdFormatter } from "@/src/utils/numbers";
|
||||
import {
|
||||
exportOptions,
|
||||
type ExportFileFormats,
|
||||
} from "@/src/server/api/interfaces/exportTypes";
|
||||
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
|
||||
|
||||
export type GenerationsTableRow = {
|
||||
id: string;
|
||||
@@ -53,23 +57,6 @@ export type GenerationsTableRow = {
|
||||
};
|
||||
};
|
||||
|
||||
const exportOptions: Record<
|
||||
ExportFileFormats,
|
||||
{
|
||||
label: string;
|
||||
extension: string;
|
||||
fileType: string;
|
||||
}
|
||||
> = {
|
||||
CSV: { label: "CSV", extension: "csv", fileType: "text/csv" },
|
||||
JSON: { label: "JSON", extension: "json", fileType: "application/json" },
|
||||
"OPENAI-JSONL": {
|
||||
label: "OpenAI JSONL (fine-tuning)",
|
||||
extension: "jsonl",
|
||||
fileType: "application/json",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type GenerationsTableProps = {
|
||||
projectId: string;
|
||||
};
|
||||
@@ -96,14 +83,21 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
},
|
||||
]);
|
||||
|
||||
const [orderByState, setOrderByState] = useOrderByState({
|
||||
column: "startTime",
|
||||
order: "DESC",
|
||||
});
|
||||
|
||||
const generations = api.generations.all.useQuery({
|
||||
page: paginationState.pageIndex,
|
||||
limit: paginationState.pageSize,
|
||||
projectId,
|
||||
filter: filterState,
|
||||
orderBy: orderByState,
|
||||
searchQuery,
|
||||
});
|
||||
const totalCount = generations.data?.slice(1)[0]?.totalCount ?? 0;
|
||||
|
||||
const totalCount = generations.data?.totalCount ?? 0;
|
||||
|
||||
const filterOptions = api.generations.filterOptions.useQuery({
|
||||
projectId,
|
||||
@@ -114,42 +108,50 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
|
||||
setIsExporting(true);
|
||||
posthog.capture("generations:export", { file_format: fileFormat });
|
||||
const fileData = await directApi.generations.export.query({
|
||||
projectId,
|
||||
fileFormat,
|
||||
filter: filterState,
|
||||
searchQuery,
|
||||
});
|
||||
try {
|
||||
const fileData = await directApi.generations.export.query({
|
||||
projectId,
|
||||
fileFormat,
|
||||
filter: filterState,
|
||||
searchQuery,
|
||||
});
|
||||
|
||||
if (fileData) {
|
||||
const file = new File(
|
||||
[fileData],
|
||||
`generations.${exportOptions[fileFormat].extension}`,
|
||||
{
|
||||
let url: string;
|
||||
if (fileData.type === "s3") {
|
||||
url = fileData.url;
|
||||
} else {
|
||||
const file = new File([fileData.data], fileData.fileName, {
|
||||
type: exportOptions[fileFormat].fileType,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// create url from file
|
||||
const url = URL.createObjectURL(file);
|
||||
// create url from file
|
||||
url = URL.createObjectURL(file);
|
||||
}
|
||||
|
||||
// Use a dynamically created anchor element to trigger the download
|
||||
const a = document.createElement("a");
|
||||
document.body.appendChild(a);
|
||||
a.href = url;
|
||||
a.download = `generations.${exportOptions[fileFormat].extension}`; // name of the downloaded file
|
||||
a.download = fileData.fileName; // name of the downloaded file
|
||||
a.click();
|
||||
a.remove();
|
||||
|
||||
// Revoke the blob URL after using it
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
if (fileData.type === "data") {
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
}
|
||||
|
||||
setIsExporting(false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setIsExporting(false);
|
||||
}
|
||||
setIsExporting(false);
|
||||
};
|
||||
|
||||
const columns: LangfuseColumnDef<GenerationsTableRow>[] = [
|
||||
{
|
||||
accessorKey: "id",
|
||||
id: "id",
|
||||
header: "ID",
|
||||
cell: ({ row }) => {
|
||||
const observationId = row.getValue("id");
|
||||
@@ -162,13 +164,17 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
/>
|
||||
) : null;
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
id: "name",
|
||||
header: "name",
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "traceId",
|
||||
id: "traceId",
|
||||
header: "Trace ID",
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("traceId");
|
||||
@@ -179,19 +185,25 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "traceName",
|
||||
id: "traceName",
|
||||
header: "Trace Name",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "startTime",
|
||||
id: "startTime",
|
||||
header: "Start Time",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "latency",
|
||||
id: "latency",
|
||||
header: "Latency",
|
||||
cell: ({ row }) => {
|
||||
const value: number | undefined = row.getValue("latency");
|
||||
@@ -200,6 +212,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
) : undefined;
|
||||
},
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "cost",
|
||||
@@ -215,6 +228,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "level",
|
||||
id: "level",
|
||||
header: "Level",
|
||||
enableHiding: true,
|
||||
cell({ row }) {
|
||||
@@ -231,6 +245,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
</span>
|
||||
) : undefined;
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "statusMessage",
|
||||
@@ -240,8 +255,10 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "model",
|
||||
id: "model",
|
||||
header: "Model",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "usage",
|
||||
@@ -295,8 +312,10 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "version",
|
||||
id: "version",
|
||||
header: "Version",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
];
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
@@ -306,7 +325,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
);
|
||||
|
||||
const rows: GenerationsTableRow[] = generations.isSuccess
|
||||
? generations.data.map((generation) => {
|
||||
? generations.data.generations.map((generation) => {
|
||||
return {
|
||||
id: generation.id,
|
||||
traceId: generation.traceId,
|
||||
@@ -387,7 +406,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
data={
|
||||
generations.isLoading
|
||||
? { isLoading: true, isError: false }
|
||||
: generations.isError
|
||||
: generations.error
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
@@ -404,6 +423,8 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
onChange: setPaginationState,
|
||||
state: paginationState,
|
||||
}}
|
||||
setOrderBy={setOrderByState}
|
||||
orderBy={orderByState}
|
||||
columnVisibility={columnVisibility}
|
||||
onColumnVisibilityChange={setColumnVisibility}
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@ import useColumnVisibility from "@/src/features/column-visibility/hooks/useColum
|
||||
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
|
||||
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
|
||||
import { sessionsViewCols } from "@/src/server/api/definitions/sessionsView";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { formatInterval, utcDateOffsetByDays } from "@/src/utils/dates";
|
||||
@@ -63,12 +64,19 @@ export default function SessionsTable({
|
||||
pageSize: withDefault(NumberParam, 50),
|
||||
});
|
||||
|
||||
const [orderByState, setOrderByState] = useOrderByState({
|
||||
column: "createdAt",
|
||||
order: "DESC",
|
||||
});
|
||||
|
||||
const sessions = api.sessions.all.useQuery({
|
||||
page: paginationState.pageIndex,
|
||||
limit: paginationState.pageSize,
|
||||
projectId,
|
||||
filter: filterState,
|
||||
orderBy: orderByState,
|
||||
});
|
||||
|
||||
const totalCount = sessions.data?.slice(1)[0]?.totalCount ?? 0;
|
||||
useEffect(() => {
|
||||
if (sessions.isSuccess) {
|
||||
@@ -96,6 +104,7 @@ export default function SessionsTable({
|
||||
const columns: LangfuseColumnDef<SessionTableRow>[] = [
|
||||
{
|
||||
accessorKey: "bookmarked",
|
||||
id: "bookmarked",
|
||||
header: undefined,
|
||||
cell: ({ row }) => {
|
||||
const bookmarked = row.getValue("bookmarked");
|
||||
@@ -111,27 +120,33 @@ export default function SessionsTable({
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "id",
|
||||
id: "id",
|
||||
header: "ID",
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("id");
|
||||
return value && typeof value === "string" ? (
|
||||
<TableLink
|
||||
path={`/project/${projectId}/sessions/${value}`}
|
||||
path={`/project/${projectId}/sessions/${encodeURIComponent(value)}`}
|
||||
value={value}
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
id: "createdAt",
|
||||
header: "Created At",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "sessionDuration",
|
||||
id: "sessionDuration",
|
||||
header: "Duration",
|
||||
enableHiding: true,
|
||||
cell: ({ row }) => {
|
||||
@@ -140,6 +155,7 @@ export default function SessionsTable({
|
||||
? formatInterval(value)
|
||||
: undefined;
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "userIds",
|
||||
@@ -153,7 +169,7 @@ export default function SessionsTable({
|
||||
{(value as string[]).map((user) => (
|
||||
<TableLink
|
||||
key={user}
|
||||
path={`/project/${projectId}/users/${user}`}
|
||||
path={`/project/${projectId}/users/${encodeURIComponent(user)}`}
|
||||
value={user}
|
||||
truncateAt={40}
|
||||
/>
|
||||
@@ -164,8 +180,10 @@ export default function SessionsTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "countTraces",
|
||||
id: "countTraces",
|
||||
header: "Traces",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -204,6 +222,8 @@ export default function SessionsTable({
|
||||
onChange: setPaginationState,
|
||||
state: paginationState,
|
||||
}}
|
||||
setOrderBy={setOrderByState}
|
||||
orderBy={orderByState}
|
||||
columnVisibility={columnVisibility}
|
||||
onColumnVisibilityChange={setColumnVisibility}
|
||||
help={{
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { DeleteTrace } from "@/src/components/delete-trace";
|
||||
import { TraceTableMultiSelectAction } from "@/src/components/table/data-table-multi-select-actions/trace-table-multi-select-action";
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import { StarTraceToggle } from "@/src/components/star-toggle";
|
||||
import { DataTable } from "@/src/components/table/data-table";
|
||||
import { TraceTableMultiSelectAction } from "@/src/components/table/data-table-multi-select-actions/trace-table-multi-select-action";
|
||||
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import { TagTracePopver } from "@/src/features/tag/components/TagTracePopver";
|
||||
import { TokenUsageBadge } from "@/src/components/token-usage-badge";
|
||||
import { Checkbox } from "@/src/components/ui/checkbox";
|
||||
import { JSONView } from "@/src/components/ui/code";
|
||||
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
|
||||
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
|
||||
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
|
||||
import { tracesTableColsWithOptions } from "@/src/server/api/definitions/tracesTable";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { formatInterval, utcDateOffsetByDays } from "@/src/utils/dates";
|
||||
import { type RouterInput, type RouterOutput } from "@/src/utils/types";
|
||||
import { type Score } from "@prisma/client";
|
||||
import { type RowSelectionState } from "@tanstack/react-table";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
NumberParam,
|
||||
@@ -23,10 +29,6 @@ import {
|
||||
useQueryParams,
|
||||
withDefault,
|
||||
} from "use-query-params";
|
||||
import { StarTraceToggle } from "@/src/components/star-toggle";
|
||||
import { JSONView } from "@/src/components/ui/code";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import { type RowSelectionState } from "@tanstack/react-table";
|
||||
|
||||
export type TracesTableRow = {
|
||||
bookmarked: boolean;
|
||||
@@ -42,6 +44,7 @@ export type TracesTableRow = {
|
||||
output?: unknown;
|
||||
sessionId?: string;
|
||||
scores: Score[];
|
||||
tags: string[];
|
||||
usage: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
@@ -76,6 +79,10 @@ export default function TracesTable({
|
||||
value: utcDateOffsetByDays(-14),
|
||||
},
|
||||
]);
|
||||
const [orderByState, setOrderByState] = useOrderByState({
|
||||
column: "timestamp",
|
||||
order: "DESC",
|
||||
});
|
||||
|
||||
const userIdFilter: FilterState = userId
|
||||
? [
|
||||
@@ -93,31 +100,44 @@ export default function TracesTable({
|
||||
pageIndex: withDefault(NumberParam, 0),
|
||||
pageSize: withDefault(NumberParam, 50),
|
||||
});
|
||||
|
||||
const traces = api.traces.all.useQuery({
|
||||
const tracesAllQueryFilter = {
|
||||
page: paginationState.pageIndex,
|
||||
limit: paginationState.pageSize,
|
||||
projectId,
|
||||
filter: filterState,
|
||||
searchQuery,
|
||||
});
|
||||
const totalCount = traces.data?.slice(1)[0]?.totalCount ?? 0;
|
||||
orderBy: orderByState,
|
||||
};
|
||||
const traces = api.traces.all.useQuery(tracesAllQueryFilter);
|
||||
|
||||
const totalCount = traces.data?.totalCount ?? 0;
|
||||
useEffect(() => {
|
||||
if (traces.isSuccess) {
|
||||
setDetailPageList(
|
||||
"traces",
|
||||
traces.data.map((t) => t.id),
|
||||
traces.data.traces.map((t) => t.id),
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [traces.isSuccess, traces.data]);
|
||||
|
||||
const traceFilterOptions = api.traces.filterOptions.useQuery({
|
||||
projectId,
|
||||
});
|
||||
|
||||
// loading filter options individually from the remaining calls
|
||||
// traces.all should load first together with everything else.
|
||||
// This here happens in the background.
|
||||
const traceFilterOptions = api.traces.filterOptions.useQuery(
|
||||
{
|
||||
projectId,
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const convertToTableRow = (
|
||||
trace: RouterOutput["traces"]["all"][0],
|
||||
trace: RouterOutput["traces"]["all"]["traces"][0],
|
||||
): TracesTableRow => {
|
||||
return {
|
||||
bookmarked: trace.bookmarked,
|
||||
@@ -133,6 +153,7 @@ export default function TracesTable({
|
||||
input: trace.input,
|
||||
output: trace.output,
|
||||
latency: trace.latency === null ? undefined : trace.latency,
|
||||
tags: trace.tags,
|
||||
usage: {
|
||||
promptTokens: trace.promptTokens,
|
||||
completionTokens: trace.completionTokens,
|
||||
@@ -173,13 +194,14 @@ export default function TracesTable({
|
||||
{
|
||||
accessorKey: "bookmarked",
|
||||
header: undefined,
|
||||
id: "bookmarked",
|
||||
cell: ({ row }) => {
|
||||
const bookmarked = row.getValue("bookmarked");
|
||||
const traceId = row.getValue("id");
|
||||
|
||||
return typeof traceId === "string" &&
|
||||
typeof bookmarked === "boolean" ? (
|
||||
<StarTraceToggle
|
||||
tracesFilter={tracesAllQueryFilter}
|
||||
traceId={traceId}
|
||||
projectId={projectId}
|
||||
value={bookmarked}
|
||||
@@ -191,6 +213,7 @@ export default function TracesTable({
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
id: "id",
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("id");
|
||||
return value && typeof value === "string" ? (
|
||||
@@ -200,32 +223,38 @@ export default function TracesTable({
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "timestamp",
|
||||
header: "Timestamp",
|
||||
id: "timestamp",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
id: "name",
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "userId",
|
||||
enableColumnFilter: !omittedFilter.find((f) => f === "userId"),
|
||||
header: "User ID",
|
||||
id: "userId",
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("userId");
|
||||
return value && typeof value === "string" ? (
|
||||
<TableLink
|
||||
path={`/project/${projectId}/users/${value}`}
|
||||
path={`/project/${projectId}/users/${encodeURIComponent(value)}`}
|
||||
value={value}
|
||||
truncateAt={40}
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "sessionId",
|
||||
@@ -235,7 +264,7 @@ export default function TracesTable({
|
||||
const value = row.getValue("sessionId");
|
||||
return value && typeof value === "string" ? (
|
||||
<TableLink
|
||||
path={`/project/${projectId}/sessions/${value}`}
|
||||
path={`/project/${projectId}/sessions/${encodeURIComponent(value)}`}
|
||||
value={value}
|
||||
truncateAt={40}
|
||||
/>
|
||||
@@ -245,6 +274,7 @@ export default function TracesTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "latency",
|
||||
id: "latency",
|
||||
header: "Latency",
|
||||
// add seconds to the end of the latency
|
||||
cell: ({ row }) => {
|
||||
@@ -252,8 +282,10 @@ export default function TracesTable({
|
||||
return value !== undefined ? formatInterval(value) : undefined;
|
||||
},
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
// TODO: Enable Ordering By Usage (not covered by API yet)
|
||||
accessorKey: "usage",
|
||||
header: "Usage",
|
||||
cell: ({ row }) => {
|
||||
@@ -275,6 +307,7 @@ export default function TracesTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "scores",
|
||||
id: "scores",
|
||||
header: "Scores",
|
||||
enableColumnFilter: !omittedFilter.find((f) => f === "scores"),
|
||||
cell: ({ row }) => {
|
||||
@@ -314,13 +347,38 @@ export default function TracesTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "version",
|
||||
id: "version",
|
||||
header: "Version",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "release",
|
||||
id: "release",
|
||||
header: "Release",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "tags",
|
||||
id: "tags",
|
||||
header: "Tags",
|
||||
cell: ({ row }) => {
|
||||
const tags: string[] = row.getValue("tags");
|
||||
const traceId: string = row.getValue("id");
|
||||
const filterOptionTags = traceFilterOptions.data?.tags ?? [];
|
||||
const allTags = filterOptionTags.map((t) => t.value);
|
||||
return (
|
||||
<TagTracePopver
|
||||
tags={tags}
|
||||
availableTags={allTags}
|
||||
projectId={projectId}
|
||||
traceId={traceId}
|
||||
tracesFilter={tracesAllQueryFilter}
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "action",
|
||||
@@ -359,7 +417,8 @@ export default function TracesTable({
|
||||
<TraceTableMultiSelectAction
|
||||
// Exclude traces that are not in the current page
|
||||
selectedTraceIds={Object.keys(selectedRows).filter(
|
||||
(traceId) => traces.data?.map((t) => t.id).includes(traceId),
|
||||
(traceId) =>
|
||||
traces.data?.traces.map((t) => t.id).includes(traceId),
|
||||
)}
|
||||
projectId={projectId}
|
||||
onDeleteSuccess={() => {
|
||||
@@ -384,14 +443,16 @@ export default function TracesTable({
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: traces.data.map((t) => convertToTableRow(t)),
|
||||
data: traces.data.traces.map((t) => convertToTableRow(t)),
|
||||
}
|
||||
}
|
||||
pagination={{
|
||||
pageCount: Math.ceil(totalCount / paginationState.pageSize),
|
||||
pageCount: Math.ceil(Number(totalCount) / paginationState.pageSize),
|
||||
onChange: setPaginationState,
|
||||
state: paginationState,
|
||||
}}
|
||||
setOrderBy={setOrderByState}
|
||||
orderBy={orderByState}
|
||||
rowSelection={selectedRows}
|
||||
setRowSelection={setSelectedRows}
|
||||
columnVisibility={columnVisibility}
|
||||
|
||||
@@ -103,7 +103,8 @@ export const IOPreview: React.FC<{
|
||||
const OpenAiMessageSchema = z
|
||||
.object({
|
||||
role: z.enum(["system", "user", "assistant"]).optional(),
|
||||
content: z.string().nullable(),
|
||||
name: z.string().optional(),
|
||||
content: z.union([z.record(z.any()), z.string()]).nullable(),
|
||||
})
|
||||
.strict() // no additional properties
|
||||
.refine((value) => value.content !== null || value.role !== undefined);
|
||||
@@ -129,7 +130,7 @@ const OpenAiMessageView: React.FC<{
|
||||
.map((message, index) => (
|
||||
<Fragment key={index}>
|
||||
<JSONView
|
||||
title={message.role}
|
||||
title={message.name ?? message.role}
|
||||
json={message.content}
|
||||
className={cn(
|
||||
message.role === "system" && "bg-gray-100",
|
||||
|
||||
@@ -22,6 +22,8 @@ import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { IOPreview } from "@/src/components/trace/IOPreview";
|
||||
import { formatInterval } from "@/src/utils/dates";
|
||||
import Link from "next/link";
|
||||
import { usdFormatter } from "@/src/utils/numbers";
|
||||
|
||||
export const ObservationPreview = (props: {
|
||||
observations: Array<ObservationReturnType>;
|
||||
@@ -54,8 +56,25 @@ export const ObservationPreview = (props: {
|
||||
{preloadedObservation.startTime.toLocaleString()}
|
||||
</CardDescription>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{preloadedObservation.promptId ? (
|
||||
<PromptBadge
|
||||
promptId={preloadedObservation.promptId}
|
||||
projectId={preloadedObservation.projectId}
|
||||
/>
|
||||
) : undefined}
|
||||
{preloadedObservation.completionStartTime ? (
|
||||
<Badge variant="outline">
|
||||
Time to first token:{" "}
|
||||
{formatInterval(
|
||||
(preloadedObservation.completionStartTime.getTime() -
|
||||
preloadedObservation.startTime.getTime()) /
|
||||
1000,
|
||||
)}
|
||||
</Badge>
|
||||
) : null}
|
||||
{preloadedObservation.endTime ? (
|
||||
<Badge variant="outline">
|
||||
Latency:{" "}
|
||||
{formatInterval(
|
||||
(preloadedObservation.endTime.getTime() -
|
||||
preloadedObservation.startTime.getTime()) /
|
||||
@@ -80,9 +99,10 @@ export const ObservationPreview = (props: {
|
||||
) : null}
|
||||
{preloadedObservation.price ? (
|
||||
<Badge variant="outline">
|
||||
{preloadedObservation.price.toString()} USD
|
||||
{usdFormatter(preloadedObservation.price.toNumber())}
|
||||
</Badge>
|
||||
) : undefined}
|
||||
|
||||
{preloadedObservation.modelParameters &&
|
||||
typeof preloadedObservation.modelParameters === "object"
|
||||
? Object.entries(preloadedObservation.modelParameters)
|
||||
@@ -173,3 +193,22 @@ export const ObservationPreview = (props: {
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const PromptBadge = (props: { promptId: string; projectId: string }) => {
|
||||
const prompt = api.prompts.byId.useQuery({
|
||||
id: props.promptId,
|
||||
projectId: props.projectId,
|
||||
});
|
||||
|
||||
if (prompt.isLoading) return null;
|
||||
|
||||
return (
|
||||
<Link href={`/project/${props.projectId}/prompts/${props.promptId}`}>
|
||||
<Badge>
|
||||
Prompt: {prompt.data?.name}
|
||||
{" - "}
|
||||
{prompt.data?.version}
|
||||
</Badge>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,15 +13,20 @@ export const ObservationTree = (props: {
|
||||
scores: Score[];
|
||||
currentObservationId: string | undefined;
|
||||
setCurrentObservationId: (id: string | undefined) => void;
|
||||
showMetrics: boolean;
|
||||
showScores: boolean;
|
||||
className?: string;
|
||||
}) => {
|
||||
const nestedObservations = nestObservations(props.observations);
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className={props.className}>
|
||||
<ObservationTreeTraceNode
|
||||
trace={props.trace}
|
||||
scores={props.scores}
|
||||
currentObservationId={props.currentObservationId}
|
||||
setCurrentObservationId={props.setCurrentObservationId}
|
||||
showMetrics={props.showMetrics}
|
||||
showScores={props.showScores}
|
||||
/>
|
||||
<ObservationTreeNode
|
||||
observations={nestedObservations}
|
||||
@@ -29,6 +34,8 @@ export const ObservationTree = (props: {
|
||||
indentationLevel={1}
|
||||
currentObservationId={props.currentObservationId}
|
||||
setCurrentObservationId={props.setCurrentObservationId}
|
||||
showMetrics={props.showMetrics}
|
||||
showScores={props.showScores}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -38,10 +45,12 @@ const ObservationTreeTraceNode = (props: {
|
||||
scores: Score[];
|
||||
currentObservationId: string | undefined;
|
||||
setCurrentObservationId: (id: string | undefined) => void;
|
||||
showMetrics?: boolean;
|
||||
showScores?: boolean;
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
"group my-1 flex cursor-pointer flex-col gap-1 rounded-sm p-2",
|
||||
"group mb-0.5 flex cursor-pointer flex-col gap-1 rounded-sm p-1.5",
|
||||
props.currentObservationId === undefined ||
|
||||
props.currentObservationId === ""
|
||||
? "bg-gray-100"
|
||||
@@ -51,23 +60,23 @@ const ObservationTreeTraceNode = (props: {
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<span className={cn("rounded-sm bg-gray-200 p-1 text-xs")}>TRACE</span>
|
||||
<span>{props.trace.name}</span>
|
||||
<span className="text-sm">{props.trace.name}</span>
|
||||
</div>
|
||||
|
||||
{props.trace.latency ? (
|
||||
{props.showMetrics && props.trace.latency ? (
|
||||
<div className="flex gap-2">
|
||||
<span className="text-xs text-gray-500">
|
||||
{formatInterval(props.trace.latency)}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{props.scores.find((s) => s.observationId === null) ? (
|
||||
{props.showScores && props.scores.find((s) => s.observationId === null) ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<GroupedScoreBadges
|
||||
scores={props.scores.filter((s) => s.observationId === null)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
const ObservationTreeNode = (props: {
|
||||
@@ -76,6 +85,8 @@ const ObservationTreeNode = (props: {
|
||||
indentationLevel: number;
|
||||
currentObservationId: string | undefined;
|
||||
setCurrentObservationId: (id: string | undefined) => void;
|
||||
showMetrics?: boolean;
|
||||
showScores?: boolean;
|
||||
}) => (
|
||||
<>
|
||||
{props.observations
|
||||
@@ -84,11 +95,11 @@ const ObservationTreeNode = (props: {
|
||||
<Fragment key={observation.id}>
|
||||
<div className="flex">
|
||||
{Array.from({ length: props.indentationLevel }, (_, i) => (
|
||||
<div className="mx-2 border-r lg:mr-4" key={i} />
|
||||
<div className="mx-2 border-r" key={i} />
|
||||
))}
|
||||
<div
|
||||
className={cn(
|
||||
"group my-1 flex flex-1 cursor-pointer flex-col gap-1 rounded-sm p-2 ",
|
||||
"group my-0.5 flex flex-1 cursor-pointer flex-col gap-1 rounded-sm p-1.5",
|
||||
props.currentObservationId === observation.id
|
||||
? "bg-gray-100"
|
||||
: "hover:bg-gray-50",
|
||||
@@ -103,27 +114,34 @@ const ObservationTreeNode = (props: {
|
||||
>
|
||||
{observation.type}
|
||||
</span>
|
||||
<span className="line-clamp-1">{observation.name}</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{observation.endTime ? (
|
||||
<span className="text-xs text-gray-500">
|
||||
{formatInterval(
|
||||
(observation.endTime.getTime() -
|
||||
observation.startTime.getTime()) /
|
||||
1000,
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
{observation.promptTokens ||
|
||||
observation.completionTokens ||
|
||||
observation.totalTokens ? (
|
||||
<span className="text-xs text-gray-500">
|
||||
{observation.promptTokens} → {observation.completionTokens}{" "}
|
||||
(∑ {observation.totalTokens})
|
||||
</span>
|
||||
) : null}
|
||||
<span className="line-clamp-1 text-sm">{observation.name}</span>
|
||||
</div>
|
||||
{props.showMetrics &&
|
||||
(observation.promptTokens ||
|
||||
observation.completionTokens ||
|
||||
observation.totalTokens ||
|
||||
observation.endTime) && (
|
||||
<div className="flex gap-2">
|
||||
{observation.endTime ? (
|
||||
<span className="text-xs text-gray-500">
|
||||
{formatInterval(
|
||||
(observation.endTime.getTime() -
|
||||
observation.startTime.getTime()) /
|
||||
1000,
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
{observation.promptTokens ||
|
||||
observation.completionTokens ||
|
||||
observation.totalTokens ? (
|
||||
<span className="text-xs text-gray-500">
|
||||
{observation.promptTokens} →{" "}
|
||||
{observation.completionTokens} (∑{" "}
|
||||
{observation.totalTokens})
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{observation.level !== "DEFAULT" ? (
|
||||
<div className="flex">
|
||||
<span
|
||||
@@ -137,7 +155,8 @@ const ObservationTreeNode = (props: {
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{props.scores.find((s) => s.observationId === observation.id) ? (
|
||||
{props.showScores &&
|
||||
props.scores.find((s) => s.observationId === observation.id) ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<GroupedScoreBadges
|
||||
scores={props.scores.filter(
|
||||
@@ -154,6 +173,8 @@ const ObservationTreeNode = (props: {
|
||||
indentationLevel={props.indentationLevel + 1}
|
||||
currentObservationId={props.currentObservationId}
|
||||
setCurrentObservationId={props.setCurrentObservationId}
|
||||
showMetrics={props.showMetrics}
|
||||
showScores={props.showScores}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { TracePreview } from "./TracePreview";
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
import { TraceAggUsageBadge } from "@/src/components/token-usage-badge";
|
||||
import Decimal from "decimal.js";
|
||||
import { StringParam, useQueryParam } from "use-query-params";
|
||||
import { PublishTraceSwitch } from "@/src/components/publish-object-switch";
|
||||
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
|
||||
@@ -14,9 +13,16 @@ import { useRouter } from "next/router";
|
||||
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { DeleteTrace } from "@/src/components/delete-trace";
|
||||
import { StarTraceToggle } from "@/src/components/star-toggle";
|
||||
import { StarTraceDetailsToggle } from "@/src/components/star-toggle";
|
||||
import Link from "next/link";
|
||||
import { NoAccessError } from "@/src/components/no-access";
|
||||
import { TagTraceDetailsPopover } from "@/src/features/tag/components/TagTraceDetailsPopover";
|
||||
import useLocalStorage from "@/src/components/useLocalStorage";
|
||||
import { Toggle } from "@/src/components/ui/toggle";
|
||||
import { Award, ChevronsDownUp, ChevronsUpDown } from "lucide-react";
|
||||
import { ScrollArea } from "@/src/components/ui/scroll-area";
|
||||
import { usdFormatter } from "@/src/utils/numbers";
|
||||
import type Decimal from "decimal.js";
|
||||
|
||||
export function Trace(props: {
|
||||
observations: Array<ObservationReturnType>;
|
||||
@@ -28,10 +34,16 @@ export function Trace(props: {
|
||||
"observation",
|
||||
StringParam,
|
||||
);
|
||||
const [metricsOnObservationTree, setMetricsOnObservationTree] =
|
||||
useLocalStorage("metricsOnObservationTree", true);
|
||||
const [scoresOnObservationTree, setScoresOnObservationTree] = useLocalStorage(
|
||||
"scoresOnObservationTree",
|
||||
true,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid h-full gap-4 md:grid-cols-3">
|
||||
<div className="md:col-span-2 md:h-full md:overflow-y-auto">
|
||||
<div className="grid gap-4 md:h-full md:grid-cols-3">
|
||||
<ScrollArea className="md:col-span-2 md:h-full">
|
||||
{currentObservationId === undefined ||
|
||||
currentObservationId === "" ||
|
||||
currentObservationId === null ? (
|
||||
@@ -49,15 +61,45 @@ export function Trace(props: {
|
||||
traceId={props.trace.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="md:h-full md:overflow-y-auto">
|
||||
<ObservationTree
|
||||
observations={props.observations}
|
||||
trace={props.trace}
|
||||
scores={props.scores}
|
||||
currentObservationId={currentObservationId ?? undefined}
|
||||
setCurrentObservationId={setCurrentObservationId}
|
||||
/>
|
||||
</ScrollArea>
|
||||
<div className="md:flex md:h-full md:flex-col md:overflow-hidden">
|
||||
<div className="mb-2 flex flex-shrink-0 flex-row justify-end gap-2">
|
||||
<Toggle
|
||||
pressed={scoresOnObservationTree}
|
||||
onPressedChange={(e) => {
|
||||
setScoresOnObservationTree(e);
|
||||
}}
|
||||
size="sm"
|
||||
title="Show scores"
|
||||
>
|
||||
<Award className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
pressed={metricsOnObservationTree}
|
||||
onPressedChange={(e) => {
|
||||
setMetricsOnObservationTree(e);
|
||||
}}
|
||||
size="sm"
|
||||
title="Show metrics"
|
||||
>
|
||||
{metricsOnObservationTree ? (
|
||||
<ChevronsDownUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronsUpDown className="h-4 w-4" />
|
||||
)}
|
||||
</Toggle>
|
||||
</div>
|
||||
<ScrollArea className="flex flex-grow">
|
||||
<ObservationTree
|
||||
observations={props.observations}
|
||||
trace={props.trace}
|
||||
scores={props.scores}
|
||||
currentObservationId={currentObservationId ?? undefined}
|
||||
setCurrentObservationId={setCurrentObservationId}
|
||||
showMetrics={metricsOnObservationTree}
|
||||
showScores={scoresOnObservationTree}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -74,20 +116,36 @@ export function TracePage({ traceId }: { traceId: string }) {
|
||||
},
|
||||
},
|
||||
);
|
||||
const totalCost = trace.data?.observations.reduce(
|
||||
(acc, o) => {
|
||||
if (!o.price) return acc;
|
||||
|
||||
return acc ? acc.plus(o.price) : new Decimal(0).plus(o.price);
|
||||
const traceFilterOptions = api.traces.filterOptions.useQuery(
|
||||
{
|
||||
projectId: trace.data?.projectId ?? "",
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
enabled: !!trace.data?.projectId && trace.isSuccess,
|
||||
},
|
||||
undefined as Decimal | undefined,
|
||||
);
|
||||
|
||||
const filterOptionTags = traceFilterOptions.data?.tags ?? [];
|
||||
const allTags = filterOptionTags.map((t) => t.value);
|
||||
|
||||
const totalCost: Decimal | undefined = trace.data?.observations.reduce(
|
||||
(prev: Decimal | undefined, curr: ObservationReturnType) => {
|
||||
if (!curr.price) return prev;
|
||||
|
||||
return prev ? prev.plus(curr.price) : curr.price;
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
if (trace.error?.data?.code === "UNAUTHORIZED") return <NoAccessError />;
|
||||
if (!trace.data) return <div>loading...</div>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col overflow-hidden xl:container md:h-[calc(100vh-100px)] xl:h-[calc(100vh-40px)]">
|
||||
<div className="flex flex-col overflow-hidden xl:container md:h-[calc(100vh-2rem)]">
|
||||
<Header
|
||||
title="Trace Detail"
|
||||
breadcrumb={[
|
||||
@@ -99,7 +157,7 @@ export function TracePage({ traceId }: { traceId: string }) {
|
||||
]}
|
||||
actionButtons={
|
||||
<>
|
||||
<StarTraceToggle
|
||||
<StarTraceDetailsToggle
|
||||
traceId={trace.data.id}
|
||||
projectId={trace.data.projectId}
|
||||
value={trace.data.bookmarked}
|
||||
@@ -126,18 +184,18 @@ export function TracePage({ traceId }: { traceId: string }) {
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{trace.data.sessionId ? (
|
||||
<Link
|
||||
href={`/project/${router.query.projectId as string}/sessions/${
|
||||
trace.data.sessionId
|
||||
}`}
|
||||
href={`/project/${
|
||||
router.query.projectId as string
|
||||
}/sessions/${encodeURIComponent(trace.data.sessionId)}`}
|
||||
>
|
||||
<Badge>Session: {trace.data.sessionId}</Badge>
|
||||
</Link>
|
||||
) : null}
|
||||
{trace.data.userId ? (
|
||||
<Link
|
||||
href={`/project/${router.query.projectId as string}/users/${
|
||||
trace.data.userId
|
||||
}`}
|
||||
href={`/project/${
|
||||
router.query.projectId as string
|
||||
}/users/${encodeURIComponent(trace.data.userId)}`}
|
||||
>
|
||||
<Badge>User ID: {trace.data.userId}</Badge>
|
||||
</Link>
|
||||
@@ -145,10 +203,21 @@ export function TracePage({ traceId }: { traceId: string }) {
|
||||
<TraceAggUsageBadge observations={trace.data.observations} />
|
||||
{totalCost ? (
|
||||
<Badge variant="outline">
|
||||
Total cost: {totalCost.toString()} USD
|
||||
Total cost: {usdFormatter(totalCost.toNumber())}
|
||||
</Badge>
|
||||
) : undefined}
|
||||
</div>
|
||||
<div className="mt-5 rounded-lg border bg-card font-semibold text-card-foreground shadow-sm">
|
||||
<div className="flex flex-row items-center gap-3 p-2.5">
|
||||
Tags
|
||||
<TagTraceDetailsPopover
|
||||
tags={trace.data.tags}
|
||||
availableTags={allTags}
|
||||
traceId={trace.data.id}
|
||||
projectId={trace.data.projectId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex-1 overflow-hidden border-t pt-5">
|
||||
<Trace
|
||||
key={trace.data.id}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border p-4 [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as React from "react";
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
|
||||
@@ -4,16 +4,16 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// https://ui.shadcn.com/docs/components/button
|
||||
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium 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",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -15,18 +14,18 @@ const buttonVariants = cva(
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input hover:bg-accent hover:text-accent-foreground",
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "underline-offset-4 hover:underline text-primary",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 py-2 px-4",
|
||||
xs: "h-7 px-2 rounded-sm",
|
||||
sm: "h-9 px-3 rounded-md",
|
||||
lg: "h-11 px-8 rounded-md",
|
||||
icon: "h-9 w-9",
|
||||
default: "h-10 px-4 py-2",
|
||||
xs: "h-6 px-1 rounded-sm",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -36,15 +36,17 @@ function Calendar({
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: "text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
||||
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
||||
day: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"h-9 w-9 p-0 font-normal aria-selected:opacity-100",
|
||||
),
|
||||
day_range_end: "day-range-end",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside: "text-muted-foreground opacity-50",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// https://ui.shadcn.com/docs/components/card
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import * as React from "react";
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -37,7 +36,7 @@ const CardTitle = React.forwardRef<
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -49,7 +48,7 @@ const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
@@ -71,7 +70,7 @@ const CardFooter = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(" flex items-center p-6 pt-0", className)}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -5,8 +5,8 @@ import { type DialogProps } from "@radix-ui/react-dialog";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { Search } from "lucide-react";
|
||||
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { Dialog, DialogContent } from "@/src/components/ui/dialog";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
@@ -15,7 +15,7 @@ const Command = React.forwardRef<
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-white text-slate-950 dark:bg-slate-950 dark:text-slate-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -23,13 +23,13 @@ const Command = React.forwardRef<
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
type CommandDialogProps = DialogProps;
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-2xl">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-slate-500 dark:[&_[cmdk-group-heading]]:text-slate-400 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
@@ -41,12 +41,15 @@ const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<div
|
||||
className="flex items-center rounded-lg border px-3"
|
||||
cmdk-input-wrapper=""
|
||||
>
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-11 w-full rounded-md border-transparent bg-transparent py-3 text-sm outline-none placeholder:text-slate-500 focus:border-0 focus:border-none focus:border-transparent focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-slate-400 ",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -89,7 +92,7 @@ const CommandGroup = React.forwardRef<
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
"overflow-hidden p-1 text-slate-950 dark:text-slate-50 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-slate-500 dark:[&_[cmdk-group-heading]]:text-slate-400",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -104,7 +107,7 @@ const CommandSeparator = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
className={cn("-mx-1 h-px bg-slate-200 dark:bg-slate-800", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -117,7 +120,7 @@ const CommandItem = React.forwardRef<
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-slate-100 aria-selected:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:aria-selected:bg-slate-800 dark:aria-selected:text-slate-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -133,7 +136,7 @@ const CommandShortcut = ({
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
"ml-auto text-xs tracking-widest text-slate-500 dark:text-slate-400",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -3,31 +3,16 @@
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: DialogPrimitive.DialogPortalProps & {
|
||||
className?: string;
|
||||
}) => (
|
||||
<DialogPrimitive.Portal {...props}>
|
||||
<div
|
||||
className={cn(
|
||||
className,
|
||||
"fixed inset-0 z-50 flex items-start justify-center sm:items-center",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</DialogPrimitive.Portal>
|
||||
);
|
||||
DialogPortal.displayName = DialogPrimitive.Portal.displayName;
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
@@ -36,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -53,7 +38,7 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed z-50 grid w-full gap-4 overflow-hidden rounded-b-lg border bg-background p-6 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -125,6 +110,9 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
|
||||
@@ -47,7 +47,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// https://ui.shadcn.com/docs/forms/react-hook-form
|
||||
|
||||
import * as React from "react";
|
||||
import type * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
@@ -11,8 +9,9 @@ import {
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from "react-hook-form";
|
||||
import { Label } from "@/src/components/ui/label";
|
||||
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { Label } from "@/src/components/ui/label";
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
@@ -115,9 +114,7 @@ const FormControl = React.forwardRef<
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
!error ? formDescriptionId : `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
@@ -133,7 +130,7 @@ const FormDescription = React.forwardRef<
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<div
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
@@ -155,14 +152,14 @@ const FormMessage = React.forwardRef<
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-sm font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</div>
|
||||
</p>
|
||||
);
|
||||
});
|
||||
FormMessage.displayName = "FormMessage";
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import * as React from "react";
|
||||
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
@@ -9,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
const labelVariants = cva(
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
|
||||
export interface PasswordInputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 pr-10 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 transform cursor-pointer"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
title={showPassword ? "Hide password" : "Show password"}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-5 w-5 text-gray-400" />
|
||||
) : (
|
||||
<Eye className="h-5 w-5 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
PasswordInput.displayName = "PasswordInput";
|
||||
|
||||
export { PasswordInput };
|
||||
@@ -19,7 +19,7 @@ const PopoverContent = React.forwardRef<
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
));
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
));
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||
|
||||
export { ScrollArea, ScrollBar };
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
@@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -32,6 +32,41 @@ const SelectTrigger = React.forwardRef<
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
@@ -40,13 +75,15 @@ const SelectContent = React.forwardRef<
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-80",
|
||||
position === "popper" && "translate-y-1",
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
@@ -56,6 +93,7 @@ const SelectContent = React.forwardRef<
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
@@ -117,4 +155,6 @@ export {
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="w-full overflow-auto">
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
@@ -41,7 +42,10 @@ const TableFooter = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn("bg-primary font-medium text-primary-foreground", className)}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as React from "react";
|
||||
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
@@ -11,7 +10,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-3",
|
||||
sm: "h-9 px-2.5",
|
||||
lg: "h-11 px-5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName;
|
||||
|
||||
export { Toggle, toggleVariants };
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
/**
|
||||
* useLocalStorage is a hook for managing data with the localStorage API.
|
||||
*
|
||||
* @param {string} localStorageKey - The key under which the value is stored in localStorage.
|
||||
* @param {T} initialValue - The initial value of the data to be stored.
|
||||
*
|
||||
* Note: The object T should be able to be stringified, as it will be stored in localStorage as a string.
|
||||
*
|
||||
* @return An array with three elements:
|
||||
* value: Current value
|
||||
* setValue: Function to update the value
|
||||
* clearValue: Function to remove value from the local storage.
|
||||
* This function will also reset the value to initial value
|
||||
*
|
||||
* @template T - The type of the data to be stored in localStorage. It should be a type that can be stringified.
|
||||
*
|
||||
* @throws Will throw an error if the stringifying the value or accessing local storage fails.
|
||||
*/
|
||||
function useLocalStorage<T>(localStorageKey: string, initialValue: T) {
|
||||
const [value, setValue] = useState<T>(() => {
|
||||
try {
|
||||
const storedValue = localStorage.getItem(localStorageKey);
|
||||
return storedValue ? (JSON.parse(storedValue) as T) : initialValue;
|
||||
} catch (error) {
|
||||
console.error("Error reading from local storage", error);
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
const clearValue = () => {
|
||||
try {
|
||||
localStorage.removeItem(localStorageKey);
|
||||
setValue(initialValue);
|
||||
} catch (error) {
|
||||
console.error("Error clearing local storage", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(value));
|
||||
} catch (error) {
|
||||
console.error("Error writing to local storage", error);
|
||||
}
|
||||
}, [localStorageKey, value]);
|
||||
|
||||
return [value, setValue, clearValue] as [
|
||||
T,
|
||||
React.Dispatch<React.SetStateAction<T>>,
|
||||
() => void,
|
||||
];
|
||||
}
|
||||
|
||||
export default useLocalStorage;
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v1.19.4";
|
||||
export const VERSION = "v1.34.1";
|
||||
|
||||
+18
@@ -43,11 +43,20 @@ export const env = createEnv({
|
||||
AUTH_GOOGLE_CLIENT_SECRET: z.string().optional(),
|
||||
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
|
||||
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
|
||||
AUTH_AZURE_AD_CLIENT_ID: z.string().optional(),
|
||||
AUTH_AZURE_AD_CLIENT_SECRET: z.string().optional(),
|
||||
AUTH_AZURE_AD_TENANT_ID: z.string().optional(),
|
||||
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT: z.string().optional(),
|
||||
AUTH_DISABLE_USERNAME_PASSWORD: z.enum(["true", "false"]).optional(),
|
||||
// EMAIL
|
||||
EMAIL_FROM_ADDRESS: z.string().optional(),
|
||||
SMTP_CONNECTION_URL: z.string().optional(),
|
||||
// S3
|
||||
S3_ENDPOINT: z.string().optional(),
|
||||
S3_ACCESS_KEY_ID: z.string().optional(),
|
||||
S3_SECRET_ACCESS_KEY: z.string().optional(),
|
||||
S3_BUCKET_NAME: z.string().optional(),
|
||||
S3_REGION: z.string().optional(),
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -93,11 +102,20 @@ export const env = createEnv({
|
||||
AUTH_GOOGLE_CLIENT_SECRET: process.env.AUTH_GOOGLE_CLIENT_SECRET,
|
||||
AUTH_GITHUB_CLIENT_ID: process.env.AUTH_GITHUB_CLIENT_ID,
|
||||
AUTH_GITHUB_CLIENT_SECRET: process.env.AUTH_GITHUB_CLIENT_SECRET,
|
||||
AUTH_AZURE_AD_CLIENT_ID: process.env.AUTH_AZURE_AD_CLIENT_ID,
|
||||
AUTH_AZURE_AD_CLIENT_SECRET: process.env.AUTH_AZURE_AD_CLIENT_SECRET,
|
||||
AUTH_AZURE_AD_TENANT_ID: process.env.AUTH_AZURE_AD_TENANT_ID,
|
||||
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT:
|
||||
process.env.AUTH_DOMAINS_WITH_SSO_ENFORCEMENT,
|
||||
AUTH_DISABLE_USERNAME_PASSWORD: process.env.AUTH_DISABLE_USERNAME_PASSWORD,
|
||||
// Email
|
||||
EMAIL_FROM_ADDRESS: process.env.EMAIL_FROM_ADDRESS,
|
||||
SMTP_CONNECTION_URL: process.env.SMTP_CONNECTION_URL,
|
||||
// S3
|
||||
S3_ENDPOINT: process.env.S3_ENDPOINT,
|
||||
S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID,
|
||||
S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY,
|
||||
S3_BUCKET_NAME: process.env.S3_BUCKET_NAME,
|
||||
S3_REGION: process.env.S3_REGION,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { api } from "@/src/utils/api";
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
import { TotalMetric } from "./TotalMetric";
|
||||
import { compactNumberFormatter, usdFormatter } from "@/src/utils/numbers";
|
||||
import { DashboardTable } from "@/src/features/dashboard/components/cards/DashboardTable";
|
||||
import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
import { RightAlignedCell } from "@/src/features/dashboard/components/RightAlignedCell";
|
||||
import { DashboardCard } from "@/src/features/dashboard/components/cards/DashboardCard";
|
||||
import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
import { DashboardTable } from "@/src/features/dashboard/components/cards/DashboardTable";
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { compactNumberFormatter, usdFormatter } from "@/src/utils/numbers";
|
||||
import { TotalMetric } from "./TotalMetric";
|
||||
|
||||
export const MetricTable = ({
|
||||
className,
|
||||
@@ -54,14 +54,14 @@ export const MetricTable = ({
|
||||
.filter((item) => item.model !== null)
|
||||
.map((item, i) => [
|
||||
item.model as string,
|
||||
<RightAlignedCell key={i}>
|
||||
<RightAlignedCell key={`${i}-tokens`}>
|
||||
{item.sumTotalTokens
|
||||
? compactNumberFormatter(item.sumTotalTokens as number)
|
||||
: "0"}
|
||||
</RightAlignedCell>,
|
||||
<RightAlignedCell key={i}>
|
||||
<RightAlignedCell key={`${i}-cost`}>
|
||||
{item.totalTokenCost
|
||||
? usdFormatter(item.totalTokenCost as number)
|
||||
? usdFormatter(item.totalTokenCost as number, 2, 2)
|
||||
: "$0"}
|
||||
</RightAlignedCell>,
|
||||
])
|
||||
@@ -76,14 +76,14 @@ export const MetricTable = ({
|
||||
<DashboardTable
|
||||
headers={[
|
||||
"Model",
|
||||
<RightAlignedCell key={0}>Tokens</RightAlignedCell>,
|
||||
<RightAlignedCell key={0}>USD</RightAlignedCell>,
|
||||
<RightAlignedCell key="tokens">Tokens</RightAlignedCell>,
|
||||
<RightAlignedCell key="cost">USD</RightAlignedCell>,
|
||||
]}
|
||||
rows={metricsData}
|
||||
collapse={{ collapsed: 5, expanded: 20 }}
|
||||
>
|
||||
<TotalMetric
|
||||
metric={totalTokens ? usdFormatter(totalTokens) : "$0"}
|
||||
metric={totalTokens ? usdFormatter(totalTokens, 2, 2) : "$0"}
|
||||
description="Total cost"
|
||||
>
|
||||
<DocPopup
|
||||
|
||||
@@ -94,13 +94,19 @@ export const ModelUsageChart = ({
|
||||
0,
|
||||
);
|
||||
|
||||
// had to add this function as tremor under the hodd adds more variables
|
||||
// to the function call which would break usdFormatter.
|
||||
const oneValueUsdFormatter = (value: number) => {
|
||||
return usdFormatter(value, 2, 2);
|
||||
};
|
||||
|
||||
const data = [
|
||||
{
|
||||
tabTitle: "Total cost",
|
||||
data: transformedModelCost,
|
||||
totalMetric: totalCost ? usdFormatter(totalCost) : usdFormatter(0),
|
||||
totalMetric: totalCost ? usdFormatter(totalCost, 2, 2) : usdFormatter(0),
|
||||
metricDescription: `Token cost`,
|
||||
formatter: usdFormatter,
|
||||
formatter: oneValueUsdFormatter,
|
||||
},
|
||||
{
|
||||
tabTitle: "Total tokens",
|
||||
@@ -115,7 +121,7 @@ export const ModelUsageChart = ({
|
||||
return (
|
||||
<DashboardCard
|
||||
className={className}
|
||||
title={"Model Usage"}
|
||||
title="Model Usage"
|
||||
isLoading={tokens.isLoading}
|
||||
>
|
||||
<TabComponent
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import { type Key, type ReactNode } from "react";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { type ReactNode } from "react";
|
||||
|
||||
export const RightAlignedCell = ({
|
||||
children,
|
||||
className,
|
||||
key,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
key?: Key;
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn("text-right", className)} key={key}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}) => <div className={cn("text-right", className)}>{children}</div>;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { api } from "@/src/utils/api";
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
import { TotalMetric } from "./TotalMetric";
|
||||
import { compactNumberFormatter } from "@/src/utils/numbers";
|
||||
import { DashboardTable } from "@/src/features/dashboard/components/cards/DashboardTable";
|
||||
import { DashboardCard } from "@/src/features/dashboard/components/cards/DashboardCard";
|
||||
import { NoData } from "@/src/features/dashboard/components/NoData";
|
||||
import { RightAlignedCell } from "./RightAlignedCell";
|
||||
import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
import { NoData } from "@/src/features/dashboard/components/NoData";
|
||||
import { DashboardCard } from "@/src/features/dashboard/components/cards/DashboardCard";
|
||||
import { DashboardTable } from "@/src/features/dashboard/components/cards/DashboardTable";
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { compactNumberFormatter } from "@/src/utils/numbers";
|
||||
import { RightAlignedCell } from "./RightAlignedCell";
|
||||
import { TotalMetric } from "./TotalMetric";
|
||||
|
||||
export const ScoresTable = ({
|
||||
className,
|
||||
@@ -128,23 +128,23 @@ export const ScoresTable = ({
|
||||
<DashboardTable
|
||||
headers={[
|
||||
"Name",
|
||||
<RightAlignedCell key={0}>#</RightAlignedCell>,
|
||||
<RightAlignedCell key={0}>Avg</RightAlignedCell>,
|
||||
<RightAlignedCell key={0}>0</RightAlignedCell>,
|
||||
<RightAlignedCell key={0}>1</RightAlignedCell>,
|
||||
<RightAlignedCell key="count">#</RightAlignedCell>,
|
||||
<RightAlignedCell key="average">Avg</RightAlignedCell>,
|
||||
<RightAlignedCell key="zero">0</RightAlignedCell>,
|
||||
<RightAlignedCell key="one">1</RightAlignedCell>,
|
||||
]}
|
||||
rows={data.map((item, i) => [
|
||||
item.scoreName,
|
||||
<RightAlignedCell key={i}>
|
||||
<RightAlignedCell key={`${i}-count`}>
|
||||
{compactNumberFormatter(item.countScoreId as number)}
|
||||
</RightAlignedCell>,
|
||||
<RightAlignedCell key={i}>
|
||||
<RightAlignedCell key={`${i}-average`}>
|
||||
{compactNumberFormatter(item.avgValue)}
|
||||
</RightAlignedCell>,
|
||||
<RightAlignedCell key={i}>
|
||||
<RightAlignedCell key={`${i}-zero`}>
|
||||
{compactNumberFormatter(item.zeroValueScore as number)}
|
||||
</RightAlignedCell>,
|
||||
<RightAlignedCell key={i}>
|
||||
<RightAlignedCell key={`${i}-one`}>
|
||||
{compactNumberFormatter(item.oneValueScore)}
|
||||
</RightAlignedCell>,
|
||||
])}
|
||||
|
||||
@@ -84,7 +84,7 @@ export const TracesTimeSeriesChart = ({
|
||||
/>
|
||||
{!isEmptyTimeSeries(transformedTraces) ? (
|
||||
<BaseTimeSeriesChart
|
||||
className="min-h-80 h-full self-stretch"
|
||||
className="h-full min-h-80 self-stretch"
|
||||
agg={agg}
|
||||
data={transformedTraces}
|
||||
connectNulls={true}
|
||||
|
||||
@@ -114,15 +114,17 @@ export const UserChart = ({
|
||||
|
||||
const maxNumberOfEntries = { collapsed: 5, expanded: 20 } as const;
|
||||
|
||||
const localUsdFormatter = (value: number) => usdFormatter(value, 2, 2);
|
||||
|
||||
const data = [
|
||||
{
|
||||
tabTitle: "Token cost",
|
||||
data: isExpanded
|
||||
? transformedCost.slice(0, maxNumberOfEntries.expanded)
|
||||
: transformedCost.slice(0, maxNumberOfEntries.collapsed),
|
||||
totalMetric: totalCost ? usdFormatter(totalCost) : usdFormatter(0),
|
||||
totalMetric: totalCost ? usdFormatter(totalCost, 2, 2) : usdFormatter(0),
|
||||
metricDescription: "Total cost",
|
||||
formatter: usdFormatter,
|
||||
formatter: localUsdFormatter,
|
||||
},
|
||||
{
|
||||
tabTitle: "Count of Traces",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExpandListButton } from "@/src/features/dashboard/components/cards/ChevronButton";
|
||||
import { NoData } from "../NoData";
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { NoData } from "../NoData";
|
||||
|
||||
type TableHeaders = ReactNode[];
|
||||
type TableRows = ReactNode[][];
|
||||
@@ -57,11 +57,11 @@ export const DashboardTable = ({
|
||||
: collapse.collapsed
|
||||
: undefined,
|
||||
)
|
||||
.map((row) => (
|
||||
<tr key={"1"}>
|
||||
{row.map((cell, i) => (
|
||||
.map((row, i) => (
|
||||
<tr key={i}>
|
||||
{row.map((cell, j) => (
|
||||
<td
|
||||
key={i}
|
||||
key={j}
|
||||
className="whitespace-nowrap py-2 pl-3 pr-2 text-xs text-gray-500 sm:pl-0"
|
||||
>
|
||||
{cell}
|
||||
|
||||
@@ -15,6 +15,8 @@ import { Button } from "@/src/components/ui/button";
|
||||
import { DatasetStatus, type DatasetItem } from "@prisma/client";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
|
||||
import { useEffect } from "react";
|
||||
|
||||
type RowData = {
|
||||
id: string;
|
||||
@@ -31,12 +33,23 @@ export function DatasetItemsTable({
|
||||
projectId: string;
|
||||
datasetId: string;
|
||||
}) {
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
const utils = api.useUtils();
|
||||
const items = api.datasets.itemsByDatasetId.useQuery({
|
||||
projectId,
|
||||
datasetId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (items.isSuccess) {
|
||||
setDetailPageList(
|
||||
"datasetItems",
|
||||
items.data.map((t) => t.id),
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [items.isSuccess, items.data]);
|
||||
|
||||
const mutUpdate = api.datasets.updateDatasetItem.useMutation({
|
||||
onSuccess: () => utils.datasets.invalidate(),
|
||||
});
|
||||
|
||||
@@ -2,9 +2,11 @@ import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import { DataTable } from "@/src/components/table/data-table";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { formatInterval } from "@/src/utils/dates";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import { useEffect } from "react";
|
||||
|
||||
type RowData = {
|
||||
key: {
|
||||
@@ -25,7 +27,16 @@ export function DatasetRunsTable(props: {
|
||||
projectId: props.projectId,
|
||||
datasetId: props.datasetId,
|
||||
});
|
||||
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
useEffect(() => {
|
||||
if (runs.isSuccess) {
|
||||
setDetailPageList(
|
||||
"datasetRuns",
|
||||
runs.data.map((t) => t.id),
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [runs.isSuccess, runs.data]);
|
||||
const columns: LangfuseColumnDef<RowData>[] = [
|
||||
{
|
||||
accessorKey: "key",
|
||||
|
||||
@@ -10,18 +10,17 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/src/components/ui/dropdown-menu";
|
||||
import { NewDatasetButton } from "@/src/features/datasets/components/NewDatasetButton";
|
||||
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import { DatasetStatus } from "@prisma/client";
|
||||
import { Archive, MoreVertical } from "lucide-react";
|
||||
import { MoreVertical, Trash } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
type RowData = {
|
||||
key: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
status: DatasetStatus;
|
||||
createdAt: string;
|
||||
lastRunAt?: string;
|
||||
countItems: number;
|
||||
@@ -29,13 +28,23 @@ type RowData = {
|
||||
};
|
||||
|
||||
export function DatasetsTable(props: { projectId: string }) {
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
const utils = api.useUtils();
|
||||
const datasets = api.datasets.allDatasets.useQuery({
|
||||
projectId: props.projectId,
|
||||
});
|
||||
const mutArchive = api.datasets.updateDataset.useMutation({
|
||||
const mutDelete = api.datasets.deleteDataset.useMutation({
|
||||
onSuccess: () => utils.datasets.invalidate(),
|
||||
});
|
||||
useEffect(() => {
|
||||
if (datasets.isSuccess) {
|
||||
setDetailPageList(
|
||||
"datasets",
|
||||
datasets.data.map((t) => t.id),
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [datasets.isSuccess, datasets.data]);
|
||||
|
||||
const columns: LangfuseColumnDef<RowData>[] = [
|
||||
{
|
||||
@@ -52,26 +61,6 @@ export function DatasetsTable(props: { projectId: string }) {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: ({ row }) => {
|
||||
const status: DatasetStatus = row.getValue("status");
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"h-2 w-2 rounded-full",
|
||||
status === DatasetStatus.ACTIVE
|
||||
? "bg-green-600"
|
||||
: "bg-yellow-600",
|
||||
)}
|
||||
/>
|
||||
<span>{status}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "countItems",
|
||||
header: "Items",
|
||||
@@ -92,7 +81,6 @@ export function DatasetsTable(props: { projectId: string }) {
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const key: RowData["key"] = row.getValue("key");
|
||||
const status: DatasetStatus = row.getValue("status");
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -105,18 +93,14 @@ export function DatasetsTable(props: { projectId: string }) {
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
mutArchive.mutate({
|
||||
mutDelete.mutate({
|
||||
projectId: props.projectId,
|
||||
datasetId: key.id,
|
||||
status:
|
||||
status === DatasetStatus.ARCHIVED
|
||||
? DatasetStatus.ACTIVE
|
||||
: DatasetStatus.ARCHIVED,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Archive className="mr-2 h-4 w-4" />
|
||||
{status === DatasetStatus.ARCHIVED ? "Unarchive" : "Archive"}
|
||||
<Trash className="mr-2 h-4 w-4" />
|
||||
Delete permanently
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -132,7 +116,6 @@ export function DatasetsTable(props: { projectId: string }) {
|
||||
key: { id: item.id, name: item.name },
|
||||
createdAt: item.createdAt.toISOString(),
|
||||
lastRunAt: item.lastRunAt?.toISOString() ?? "",
|
||||
status: item.status,
|
||||
countItems: item.countDatasetItems,
|
||||
countRuns: item.countDatasetRuns,
|
||||
};
|
||||
|
||||
@@ -29,7 +29,6 @@ export const datasetRouter = createTRPCRouter({
|
||||
d.name,
|
||||
d.created_at "createdAt",
|
||||
d.updated_at "updatedAt",
|
||||
d.status,
|
||||
count(distinct di.id)::int "countDatasetItems",
|
||||
count(distinct dr.id)::int "countDatasetRuns",
|
||||
max(dr.created_at) "lastRunAt"
|
||||
@@ -37,8 +36,8 @@ export const datasetRouter = createTRPCRouter({
|
||||
LEFT JOIN dataset_items di ON di.dataset_id = d.id
|
||||
LEFT JOIN dataset_runs dr ON dr.dataset_id = d.id
|
||||
WHERE d.project_id = ${input.projectId}
|
||||
GROUP BY 1,2,3,4,5
|
||||
ORDER BY d.status ASC, d.created_at DESC
|
||||
GROUP BY 1,2,3,4
|
||||
ORDER BY d.created_at DESC
|
||||
`);
|
||||
}),
|
||||
byId: protectedProjectProcedure
|
||||
@@ -243,30 +242,19 @@ export const datasetRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
}),
|
||||
updateDataset: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
datasetId: z.string(),
|
||||
status: z.enum(["ACTIVE", "ARCHIVED"]).optional(),
|
||||
name: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
deleteDataset: protectedProjectProcedure
|
||||
.input(z.object({ projectId: z.string(), datasetId: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "datasets:CUD",
|
||||
});
|
||||
return ctx.prisma.dataset.update({
|
||||
return ctx.prisma.dataset.delete({
|
||||
where: {
|
||||
id: input.datasetId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
data: {
|
||||
status: input.status,
|
||||
name: input.name,
|
||||
},
|
||||
});
|
||||
}),
|
||||
createDatasetItem: protectedProjectProcedure
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user