Compare commits

...
28 Commits
Author SHA1 Message Date
Max Deichmann fa26275634 chore: release v2.46.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-28 16:04:42 +02:00
Max DeichmannandGitHub 7587c75459 chore: do not log io (#2164) 2024-05-28 11:27:12 +00:00
ClemoandGitHub a956d0115c docs: update 2024-05-27 19:14:33 -07:00
Marc KlingenandGitHub cb82835489 fix(ui): overflow behavior of tag manager (#2156) 2024-05-27 17:13:32 +02:00
Hassieb PakzadandGitHub d1c1e60eca feat: use custom keys for playground (#2139) 2024-05-27 14:29:05 +02:00
Marc Klingen 81b612273a chore: release v2.45.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-27 13:10:28 +02:00
Marc KlingenandGitHub 2c1abf2f6e feat(auth): add AWS Cognito provider (#2151)
Fixes #2107
2024-05-27 10:21:04 +00:00
Marc KlingenandGitHub db1a662f7f fix(ui): default hide version and release on trace table (#2150) 2024-05-27 10:06:22 +00:00
Richard KrümmelandGitHub 1b0e3de709 fix(ui): table header tooltips (#2148) 2024-05-27 11:20:48 +02:00
Richard KrümmelandGitHub be8aec2f8f feat(theme): add posthog (#2147) 2024-05-27 11:20:11 +02:00
adc01014c0 fix(scores): table source filter (#2144)
Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-05-24 19:29:39 +00:00
Marc KlingenandGitHub 215af995fb perf: add indicies to foreign keys on job executions table (#2143) 2024-05-24 21:12:52 +02:00
marliessophieandGitHub 74ff237145 feat(scores): rename source REVIEW to ANNOTATION (#2141)
* feat(scores): add 'ANNOTATION' to `ScoreSource`

* feat: adjust seeder

* feat: adjust trpc router

* add: backfill scores source `REVIEW` to `ANNOTATION`

* add: drop `REVIEW` from scores source enum

* update yml

* refactor: rename trpc procedures
2024-05-24 19:58:30 +02:00
porter-deployment-app[bot]GitHubporter-deployment-app[bot] <87230664+porter-deployment-app[bot]@users.noreply.github.com>
5c2ae0e678 Enable Porter Application worker (#2138)
Create porter_stack_worker.yml file

Co-authored-by: porter-deployment-app[bot] <87230664+porter-deployment-app[bot]@users.noreply.github.com>
2024-05-24 14:42:46 +00:00
marliessophieandGitHub a6d8ce8c96 feat(score_config): add table via migration (#2126)
* feat: add author user id column to scores table

* feat: add author user id to scores created via trpc router

* feat: add author user id to scores created in seeder

* fixup

* add: index to author user id on scores

* feat: annotation config schema

* add: migration

* add: migrations in order

* fixup after merge

* fix: replicate to worker/types

* fix: address feedback

* rn: to scoreConfig

* push

* refactor: rn `ScoreDataType` to `ScoreConfigDataType`

* fix

* fix: scores server test

* context: order of returned data can vary based on how data is retrieved. Likely re-ordered due to new index on configId in scores table

* fix: re-generate types for correct alphabetical order

* fix: remodel migrations to match new schema

* feat: rm backfill migration for data_type on scores

* make data_type column optional on scores

* push: regenerate types

* push

* feat: make dataType on scores required again

* rename: scores add data type migration

* re-run migrations

* push
2024-05-23 19:26:16 +02:00
Marc Klingen fa4ba016b5 chore: release v2.45.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-23 15:49:07 +02:00
Hassieb PakzadandGitHub 9b7e0abfbc feat: adds links in and out of playground (#2123) 2024-05-23 11:35:39 +00:00
dependabot[bot]andGitHub 5556772388 chore(deps): bump the patches group across 1 directory with 14 updates (#2120) 2024-05-23 09:46:10 +02:00
Richard KrümmelandGitHub 68e0270051 fix(ui): improve orange hue (#2128)
Improve dark:muted-orange hue
2024-05-22 23:12:34 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5bd7d879ad chore(deps): bump @t3-oss/env-nextjs from 0.8.0 to 0.10.1 (#1962)
Bumps [@t3-oss/env-nextjs](https://github.com/t3-oss/t3-env/tree/HEAD/packages/nextjs) from 0.8.0 to 0.10.1.
- [Release notes](https://github.com/t3-oss/t3-env/releases)
- [Changelog](https://github.com/t3-oss/t3-env/blob/main/packages/nextjs/CHANGELOG.md)
- [Commits](https://github.com/t3-oss/t3-env/commits/@t3-oss/env-nextjs@0.10.1/packages/nextjs)

---
updated-dependencies:
- dependency-name: "@t3-oss/env-nextjs"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-22 17:17:57 +00:00
Richard KrümmelandGitHub 87d34c05d9 feat(ui): add dark mode improvements (#2127) 2024-05-22 18:26:21 +02:00
ae27144346 feat(scores): add nullable authorUserId to scores (#2121)
* feat: add author user id column to scores table

* feat: add author user id to scores created via trpc router

* feat: add author user id to scores created in seeder

* fixup

* add: index to author user id on scores

* replicate to worker/types

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-05-22 15:47:56 +02:00
marliessophieandGitHub f5101083a2 fix: prompt metrics router add early return if no valid promptIds array input (#2125)
fix: metric router add early return if no valid promptIds array input
2024-05-22 12:16:00 +00:00
124c9e8107 feat: move scores table on trace and observation preview into seperate tab (#2078)
* feat: move scores on trace and observation preview to different tab

* refactor: extract current table logic into `ScoresTablePreview`

* fix: rm unused footer

* refactor: scores preview table

* fix: tab state needs to be reset upon changing currentObservationId

* refactor: reorder for readability

* refactor: rm column visibility as not needed

* fix: rm pagination

* refactor: rn details to preview

* feat: restyle tabs

* feat: reuse scores table

* feat: hide columns

* feat: add source column to scores table

* feat: add user name and avatar to scores table

* adjust seeder

* feat: show score value in preview tab

* fix: don't show comment if empty string

* feat: add score source to preview UI

* feat: add tooltip

* feat: don't use user image and name in scores table

* feat: hide userId column in trace and observation preview

* adjust seeder to reuse image in bucket

* fix: pass table name to save column visibility in local storage

* revert: changes to seed file

* feat: misc UI improvements

* fixup: first draft add `is not` filter to table

* feat: show all nested scores for trace scores

* fix: misc UI

* ui: use `primary-accent` color

* adjust padding/margins

* adjust paddings

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-05-22 09:57:24 +00:00
Marc KlingenandGitHub fabbb75e40 fix(api): dedupe and sort trace tags (#2122) 2024-05-22 09:00:34 +00:00
Richard KrümmelandGitHub 857507195b feat(ui): add dark mode (#1998) 2024-05-22 10:52:53 +02:00
Marc KlingenandGitHub cd7864cad4 fix(ui): hide playground button if not available (#2118) 2024-05-21 14:43:07 +02:00
Marc KlingenandGitHub cd9fd110e4 perf(api): optimize get traces api & fix totalCost (#2117) 2024-05-21 14:40:21 +02:00
153 changed files with 2600 additions and 1261 deletions
+5
View File
@@ -58,6 +58,11 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
# AUTH_AUTH0_CLIENT_SECRET=
# AUTH_AUTH0_ISSUER=
# AUTH_AUTH0_ALLOW_ACCOUNT_LINKING=false
# AUTH_COGNITO_CLIENT_ID=
# AUTH_COGNITO_CLIENT_SECRET=
# AUTH_COGNITO_ISSUER=
# AUTH_COGNITO_ALLOW_ACCOUNT_LINKING=false
# Transactional email, optional
# Defines the email address to use as the from address.
+29
View File
@@ -0,0 +1,29 @@
"on":
push:
branches:
- main
name: Deploy to worker
jobs:
porter-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set Github tag
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Setup porter
uses: porter-dev/setup-porter@v0.1.0
- name: Deploy stack
timeout-minutes: 30
run: exec porter apply
env:
PORTER_CLUSTER: "3959"
PORTER_DEPLOYMENT_TARGET_ID: d2bb23e4-1d77-48f2-a113-383d076959b3
PORTER_HOST: https://dashboard.getporter.dev
PORTER_PR_NUMBER: ${{ github.event.number }}
PORTER_PROJECT: "12565"
PORTER_REPO_NAME: ${{ github.event.repository.name }}
PORTER_STACK_NAME: worker
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_3959 }}
+68
View File
@@ -290,6 +290,74 @@ When a new release is tagged on the `main` branch (excluding prereleases), it tr
1. The Docker image is published to GitHub Packages with the version number and `latest` tag.
2. The deployment is carried out on Langfuse Cloud. This is done by force pushing the `main` branch to the `production` branch during every release, using the [`release.yml`](.github/workflows/release.yml) GitHub Action.
## Theming
At Langfuse, we utilize CSS variables to manage our theme settings across the platform.
Our approach leverages separate CSS variables for backgrounds (--background) and foregrounds (--foreground), fully adhering to the [shadcn/ui](https://ui.shadcn.com/docs/theming) color conventions. The background suffix can be omitted if the variable is used for the background color of the component. We recommend using HSL values for these colors to enhance consistency and customization. There is no need to manually handle dark mode styling with "dark:" prefixes, as next-themes automatically manages the theme switching.
Given the following CSS variables:
```
--primary: 222.2 47.4% 11.2%; // e.g. background-color
--primary-foreground: 210 40% 98%; // e.g. text-color
```
The background color of the following component will be `hsl(var(--primary))` and the foreground color will be `hsl(var(--primary-foreground))`.
```
<div class="bg-primary text-primary-foreground">Hello</div>
```
### Color Variables
| Variable | Description | Examples |
| ------------------------ | ------------------------------------------------------------------ | -------------------------------- |
| --background | Background color | Default background color of body |
| --foreground | Foreground color | Default text color of body |
| --muted | Muted background color | TabsList, Skeleton and Switch |
| --muted-foreground | Muted foreground color | |
| --popover | Popover background color | DropdownMenu, HoverCard, Popover |
| --popover-foreground | Popover foreground color | |
| --card | Card background color | Card |
| --card-foreground | Card foreground color | |
| --border | Border color | Default border color |
| --input | Input field border color | Input, Select, Textarea |
| --primary | Primary button background colors | Button variant="primary" |
| --primary-foreground | Primary button foreground color | |
| --secondary | Secondary button background color | Button variant="secondary" |
| --secondary-foreground | Secondary button foreground color | |
| --accent | Used for accents such as hover effects | DropdownMenuItem, SelectItem |
| --accent-foreground | Used for texts on hover effects | DropdownMenuItem, SelectItem |
| --destructive | Destructive action color for background | Button variant="destructive" |
| --destructive-foreground | Destructive action color for text | |
| --ring | Focus ring color | MultiSelect |
| --primary-accent | Primary accent color used for branding | Layout |
| --hover-primary-accent | Primary accent color used for hover effects for links | SignIn and AuthCloudRegionSwitch |
| --muted-green | Muted green for Event label | ObservationTree |
| --muted-orange | Muted orange for Generation label | ObservationTree |
| --muted-blue | Muted blue for Span label | ObservationTree |
| --muted-gray | Muted gray for disabled status badges | StatusBadge |
| --accent-light-green | Light green accent for background of output and assistant messages | IOPreview, Generations, Traces |
| --accent-dark-green | Dark green accent for border of output and assistant messages | CodeJsonViewer and IOPReview |
| --light-red | Light red for error background | level-color and StatusBadge |
| --dark-red | Dark red for error text and error badge dot color | level-color and ErrorPage |
| --light-yellow | Light yellow for warning background | LevelColor |
| --dark-yellow | Dark yellow for warning text | LevelColor |
| --light-green | Light green for success status badge background | StatusBadge |
| --dark-green | Dark green for success status badge text and dot | StatusBadge |
| --light-blue | Light blue for background of Staging label | LangfuseLogo |
| --dark-blue | Dark blue for text and border of Staging label | LangfuseLogo |
| --accent-light-blue | Light blue accent for table link hover effect | TableLink |
| --accent-dark-blue | Dark blue accent for table link text | TableLink |
### Adding New Colors
1. Global Definitions: Add new CSS variable definitions in the global.css file.
2. Tailwind Configuration: Reflect these new colors in the tailwind.config.js to maintain alignment with Tailwind's utility classes.
By following these guidelines, you can ensure that any contributions to our theme are consistent, maintainable, and aligned with our design system.
## Using secrets stored in 1Password
When applying changes to non-local environments, you may need to use secrets stored in 1Password. We use the 1Password CLI for this purpose.
+3 -3
View File
@@ -3,9 +3,9 @@ Join us in scaling Langfuse in Berlin, Germany. We are an open source company, w
_Open Roles_
- Backend Engineer, 70-110k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/mnrdwla-backend-engineer
- Product Engineer, 70-110k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
- Developer Advocate, 60-100k EUR, 0.25-0.5% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
- Backend Engineer, 70-130k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/mnrdwla-backend-engineer
- Product Engineer, 70-130k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
- Developer Advocate, 60-110k EUR, 0.25-0.5% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
_More Info_
+7
View File
@@ -2,6 +2,7 @@ import { type Provider } from "next-auth/providers/index";
import GoogleProvider from "next-auth/providers/google";
import GitHubProvider from "next-auth/providers/github";
import OktaProvider from "next-auth/providers/okta";
import CognitoProvider from "next-auth/providers/cognito";
import Auth0Provider from "next-auth/providers/auth0";
import AzureADProvider from "next-auth/providers/azure-ad";
import { isEeAvailable } from "..";
@@ -144,6 +145,12 @@ const dbToNextAuthProvider = (provider: SsoProviderSchema): Provider | null => {
...provider.authConfig,
clientSecret: decrypt(provider.authConfig.clientSecret),
});
else if (provider.authProvider === "cognito")
return CognitoProvider({
id: getAuthProviderIdForSsoConfig(provider), // use the domain as the provider id as we use domain-specific credentials
...provider.authConfig,
clientSecret: decrypt(provider.authConfig.clientSecret),
});
else {
// Type check to ensure we handle all providers
// eslint-disable-next-line no-unused-vars
+14
View File
@@ -64,11 +64,24 @@ export const AzureAdProviderSchema = base.extend({
.nullish(),
});
export const CognitoProviderSchema = base.extend({
authProvider: z.literal("cognito"),
authConfig: z
.object({
clientId: z.string(),
clientSecret: z.string(),
issuer: z.string(),
allowDangerousEmailAccountLinking: z.boolean().optional().default(false),
})
.nullish(),
});
export type GoogleProviderSchema = z.infer<typeof GoogleProviderSchema>;
export type GithubProviderSchema = z.infer<typeof GithubProviderSchema>;
export type Auth0ProviderSchema = z.infer<typeof Auth0ProviderSchema>;
export type OktaProviderSchema = z.infer<typeof OktaProviderSchema>;
export type AzureAdProviderSchema = z.infer<typeof AzureAdProviderSchema>;
export type CognitoProviderSchema = z.infer<typeof CognitoProviderSchema>;
export const SsoProviderSchema = z.discriminatedUnion("authProvider", [
GoogleProviderSchema,
@@ -76,6 +89,7 @@ export const SsoProviderSchema = z.discriminatedUnion("authProvider", [
Auth0ProviderSchema,
OktaProviderSchema,
AzureAdProviderSchema,
CognitoProviderSchema,
]);
export type SsoProviderSchema = z.infer<typeof SsoProviderSchema>;
+1 -1
View File
@@ -209,8 +209,8 @@ types:
- ARCHIVED
ScoreSource:
enum:
- ANNOTATION
- API
- REVIEW
- EVAL
errors:
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "2.44.0",
"version": "2.46.0",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
@@ -28,7 +28,7 @@
},
"devDependencies": {
"@release-it/bumper": "^6.0.1",
"dotenv-cli": "^7.4.1",
"dotenv-cli": "^7.4.2",
"prettier": "^3.2.5",
"release-it": "^17.2.1",
"turbo": "^1.13.3"
+4 -4
View File
@@ -44,9 +44,9 @@
"seed": "ts-node -r tsconfig-paths/register -r dotenv/config --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
},
"dependencies": {
"@langchain/anthropic": "^0.1.16",
"@langchain/anthropic": "^0.1.21",
"@langchain/core": "^0.1.61",
"@langchain/openai": "^0.0.28",
"@langchain/openai": "^0.0.33",
"@prisma/client": "^5.13.0",
"@types/bcryptjs": "^2.4.6",
"bcryptjs": "^2.4.3",
@@ -59,9 +59,9 @@
"devDependencies": {
"@repo/eslint-config": "*",
"@repo/typescript-config": "*",
"@types/lodash": "^4.14.202",
"@types/lodash": "^4.17.4",
"@types/node": "^20.11.29",
"@types/pg": "^8.11.5",
"@types/pg": "^8.11.6",
"@types/uuid": "^9.0.8",
"@typescript-eslint/parser": "^7.7.0",
"eslint": "^8.57.0",
+24 -1
View File
@@ -25,11 +25,16 @@ export const ObservationLevel = {
} as const;
export type ObservationLevel = (typeof ObservationLevel)[keyof typeof ObservationLevel];
export const ScoreSource = {
ANNOTATION: "ANNOTATION",
API: "API",
REVIEW: "REVIEW",
EVAL: "EVAL"
} as const;
export type ScoreSource = (typeof ScoreSource)[keyof typeof ScoreSource];
export const ScoreDataType = {
CATEGORICAL: "CATEGORICAL",
NUMERIC: "NUMERIC"
} as const;
export type ScoreDataType = (typeof ScoreDataType)[keyof typeof ScoreDataType];
export const PricingUnit = {
PER_1000_TOKENS: "PER_1000_TOKENS",
PER_1000_CHARS: "PER_1000_CHARS"
@@ -347,9 +352,26 @@ export type Score = {
name: string;
value: number;
source: ScoreSource;
author_user_id: string | null;
comment: string | null;
trace_id: string;
observation_id: string | null;
config_id: string | null;
string_value: string | null;
data_type: Generated<ScoreDataType>;
};
export type ScoreConfig = {
id: string;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
project_id: string;
name: string;
data_type: ScoreDataType;
is_archived: Generated<boolean>;
min_value: number | null;
max_value: number | null;
categories: unknown | null;
description: string | null;
};
export type Session = {
id: string;
@@ -449,6 +471,7 @@ export type DB = {
project_memberships: ProjectMembership;
projects: Project;
prompts: Prompt;
score_configs: ScoreConfig;
scores: Score;
Session: Session;
sso_configs: SsoConfig;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "scores" ADD COLUMN "author_user_id" TEXT;
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "scores_author_user_id_idx" ON "scores"("author_user_id");
@@ -0,0 +1,42 @@
-- CreateEnum
CREATE TYPE "ScoreDataType" AS ENUM ('CATEGORICAL', 'NUMERIC');
-- AlterTable
ALTER TABLE "scores" ADD COLUMN "config_id" TEXT,
ADD COLUMN "data_type" "ScoreDataType" NOT NULL DEFAULT 'NUMERIC',
ADD COLUMN "string_value" TEXT;
-- CreateTable
CREATE TABLE "score_configs" (
"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,
"name" TEXT NOT NULL,
"data_type" "ScoreDataType" NOT NULL,
"is_archived" BOOLEAN NOT NULL DEFAULT false,
"min_value" DOUBLE PRECISION,
"max_value" DOUBLE PRECISION,
"categories" JSONB,
"description" TEXT,
CONSTRAINT "score_configs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "score_configs_data_type_idx" ON "score_configs"("data_type");
-- CreateIndex
CREATE INDEX "score_configs_is_archived_idx" ON "score_configs"("is_archived");
-- CreateIndex
CREATE INDEX "score_configs_project_id_idx" ON "score_configs"("project_id");
-- CreateIndex
CREATE INDEX "score_configs_categories_idx" ON "score_configs"("categories");
-- CreateIndex
CREATE UNIQUE INDEX "score_configs_id_project_id_key" ON "score_configs"("id", "project_id");
-- AddForeignKey
ALTER TABLE "score_configs" ADD CONSTRAINT "score_configs_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "scores_config_id_idx" ON "scores"("config_id");
@@ -0,0 +1,2 @@
-- AddForeignKey
ALTER TABLE "scores" ADD CONSTRAINT "scores_config_id_fkey" FOREIGN KEY ("config_id") REFERENCES "score_configs"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "ScoreSource" ADD VALUE 'ANNOTATION';
@@ -0,0 +1,4 @@
-- Backfill the scores source for 'REVIEW' to be 'ANNOTATION'
UPDATE "scores"
SET "source" = 'ANNOTATION'::"ScoreSource"
WHERE "source" = 'REVIEW'::"ScoreSource";
@@ -0,0 +1,14 @@
/*
Warnings:
- The values [REVIEW] on the enum `ScoreSource` will be removed. If these variants are still used in the database, this will fail.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "ScoreSource_new" AS ENUM ('ANNOTATION', 'API', 'EVAL');
ALTER TABLE "scores" ALTER COLUMN "source" TYPE "ScoreSource_new" USING ("source"::text::"ScoreSource_new");
ALTER TYPE "ScoreSource" RENAME TO "ScoreSource_old";
ALTER TYPE "ScoreSource_new" RENAME TO "ScoreSource";
DROP TYPE "ScoreSource_old";
COMMIT;
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_job_configuration_id_idx" ON "job_executions"("job_configuration_id");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_job_output_score_id_idx" ON "job_executions"("job_output_score_id");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_job_input_trace_id_idx" ON "job_executions"("job_input_trace_id");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_created_at_idx" ON "job_executions"("created_at");
+41 -1
View File
@@ -114,6 +114,7 @@ model Project {
LlmApiKeys LlmApiKeys[]
PosthogIntegration PosthogIntegration[]
Score Score[]
scoreConfig ScoreConfig[]
@@map("projects")
}
@@ -399,15 +400,22 @@ model Score {
name String
value Float
source ScoreSource
authorUserId String? @map("author_user_id")
comment String?
traceId String @map("trace_id")
observationId String? @map("observation_id")
configId String? @map("config_id")
stringValue String? @map("string_value")
dataType ScoreDataType @default(NUMERIC) @map("data_type")
JobExecution JobExecution[]
scoreConfig ScoreConfig? @relation(fields: [configId], references: [id], onDelete: SetNull)
@@unique([id, projectId]) // used for upserts via prisma
@@index(timestamp)
@@index([value])
@@index([projectId])
@@index([authorUserId])
@@index([configId])
@@index([traceId], type: Hash)
@@index([observationId], type: Hash)
@@index([source])
@@ -415,11 +423,39 @@ model Score {
}
enum ScoreSource {
ANNOTATION
API
REVIEW
EVAL
}
model ScoreConfig {
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)
name String
dataType ScoreDataType @map("data_type")
isArchived Boolean @default(false) @map("is_archived")
minValue Float? @map("min_value")
maxValue Float? @map("max_value")
categories Json? @map("categories")
description String?
score Score[]
@@unique([id, projectId]) // used for upserts via prisma
@@index([dataType])
@@index([isArchived])
@@index([projectId])
@@index([categories])
@@map("score_configs")
}
enum ScoreDataType {
CATEGORICAL
NUMERIC
}
enum PricingUnit {
PER_1000_TOKENS
PER_1000_CHARS
@@ -710,6 +746,10 @@ model JobExecution {
@@index([projectId, status])
@@index([projectId, id])
@@index([projectId])
@@index([jobConfigurationId])
@@index([jobOutputScoreId])
@@index([jobInputTraceId])
@@index([createdAt])
@@map("job_executions")
}
+21 -1
View File
@@ -12,6 +12,7 @@ import { chunk } from "lodash";
import { v4 } from "uuid";
import { ModelUsageUnit } from "../src";
import { getDisplaySecretKey, hashSecretKey } from "../src/server/auth";
import { encrypt } from "../src/encryption";
const LOAD_TRACE_VOLUME = 10_000;
@@ -165,6 +166,24 @@ async function main() {
await uploadObjects(traces, observations, scores, sessions, events);
// If openai key is in environment, add it to the projects LLM API keys
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
if (OPENAI_API_KEY) {
await prisma.llmApiKeys.create({
data: {
projectId: project1.id,
secretKey: encrypt(OPENAI_API_KEY),
displaySecretKey: getDisplaySecretKey(OPENAI_API_KEY),
provider: "openai",
},
});
} else {
console.warn(
"No OPENAI_API_KEY found in environment. Skipping seeding LLM API key."
);
}
// add eval objects
const evalTemplate = await prisma.evalTemplate.upsert({
where: {
@@ -500,8 +519,9 @@ function createObjects(
name: "manual-score",
value: Math.floor(Math.random() * 3) - 1,
timestamp: traceTs,
source: ScoreSource.REVIEW,
source: ScoreSource.ANNOTATION,
projectId,
authorUserId: `user-${i}`,
},
]
: []),
+3 -3
View File
@@ -34,9 +34,9 @@ export type UIModelParams = RecordWithEnabledFlag<
export type ModelConfig = z.infer<typeof ZodModelConfig>;
export const ZodModelConfig = z.object({
max_tokens: z.number().optional(),
temperature: z.number().optional(),
top_p: z.number().optional(),
max_tokens: z.coerce.number().optional(),
temperature: z.coerce.number().optional(),
top_p: z.coerce.number().optional(),
});
// OpenAI
+390 -249
View File
File diff suppressed because it is too large Load Diff
+21 -17
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.44.0",
"version": "2.46.0",
"private": true,
"license": "MIT",
"engines": {
@@ -25,12 +25,13 @@
"@aws-sdk/lib-storage": "^3.568.0",
"@aws-sdk/s3-request-presigner": "^3.554.0",
"@codemirror/lang-json": "^6.0.1",
"@headlessui/react": "^1.7.19",
"@headlessui/react": "1.7.18",
"@headlessui/tailwindcss": "0.2.0",
"@heroicons/react": "^2.1.3",
"@hookform/resolvers": "^3.3.4",
"@langchain/anthropic": "^0.1.16",
"@langchain/anthropic": "^0.1.21",
"@langchain/core": "^0.1.61",
"@langchain/openai": "^0.0.28",
"@langchain/openai": "^0.0.33",
"@langfuse/ee": "workspace:*",
"@langfuse/shared": "workspace:*",
"@marsidev/react-turnstile": "^0.5.4",
@@ -55,26 +56,27 @@
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@react-email/components": "^0.0.17",
"@react-email/render": "^0.0.13",
"@react-email/components": "^0.0.18",
"@react-email/render": "^0.0.14",
"@remixicon/react": "^4.2.0",
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@sentry/nextjs": "^7.113.0",
"@sentry/node": "^7.113.0",
"@sentry/profiling-node": "^7.113.0",
"@sentry/types": "^7.113.0",
"@t3-oss/env-nextjs": "^0.8.0",
"@t3-oss/env-nextjs": "^0.10.1",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.7",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.11.8",
"@tanstack/react-virtual": "^3.5.0",
"@tremor/react": "3.11.1",
"@tremor/react": "3.16.2",
"@trpc/client": "^10.45.0",
"@trpc/next": "^10.45.0",
"@trpc/react-query": "^10.45.0",
"@trpc/server": "^10.45.0",
"@uiw/codemirror-theme-github": "^4.21.25",
"@uiw/codemirror-theme-tokyo-night": "^4.22.1",
"@uiw/react-codemirror": "^4.21.25",
"ai": "^3.0.23",
"bcryptjs": "^2.4.3",
@@ -85,7 +87,7 @@
"cors": "^2.8.5",
"date-fns": "^3.3.1",
"decimal.js": "^10.4.3",
"dompurify": "^3.1.2",
"dompurify": "^3.1.4",
"exponential-backoff": "^3.1.1",
"js-tiktoken": "^1.0.12",
"kysely": "^0.27.3",
@@ -95,6 +97,7 @@
"next": "^14.2.3",
"next-auth": "^4.24.7",
"next-query-params": "^5.0.0",
"next-themes": "^0.3.0",
"nodemailer": "^6.9.13",
"posthog-js": "^1.122.0",
"posthog-node": "^3.6.3",
@@ -102,8 +105,8 @@
"react": "18.2.0",
"react-day-picker": "^8.10.1",
"react-dom": "18.2.0",
"react-hook-form": "^7.51.3",
"react-icons": "^5.0.1",
"react-hook-form": "^7.51.5",
"react-icons": "^5.2.1",
"react-responsive": "^10.0.0",
"react18-json-view": "^0.2.8-canary.6",
"sonner": "^1.4.41",
@@ -118,23 +121,24 @@
"@jedmao/location": "^3.0.0",
"@mermaid-js/mermaid-cli": "^10.7.0",
"@playwright/test": "^1.43.1",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^15.0.6",
"@tailwindcss/forms": "^0.5.7",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.17",
"@types/dompurify": "^3.0.5",
"@types/eslint": "^8.56.7",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.14.202",
"@types/lodash": "^4.17.4",
"@types/node": "20.10.5",
"@types/nodemailer": "^6.4.14",
"@types/nodemailer": "^6.4.15",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^7.7.0",
"autoprefixer": "^10.4.19",
"dotenv-cli": "^7.3.0",
"dotenv-cli": "^7.4.2",
"eslint": "^8.56.0",
"eslint-config-next": "^14.2.3",
"jest": "^29.7.0",
+1 -1
View File
@@ -1825,8 +1825,8 @@ components:
title: ScoreSource
type: string
enum:
- ANNOTATION
- API
- REVIEW
- EVAL
CreateDatasetItemRequest:
title: CreateDatasetItemRequest
+3 -2
View File
@@ -727,7 +727,7 @@ describe("/api/public/ingestion API Endpoint", () => {
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
tags: ["tag-1", "tag-2"],
tags: ["tag-1", "tag-2", "tag-2"],
},
},
],
@@ -744,7 +744,7 @@ describe("/api/public/ingestion API Endpoint", () => {
id: traceId,
name: "trace-name",
userId: "user-2",
tags: ["tag-3", "tag-4"],
tags: ["tag-1", "tag-4", "tag-3"],
},
},
],
@@ -766,6 +766,7 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbTrace[0]?.version).toBe("2.0.0");
expect(dbTrace[0]?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
expect(dbTrace[0]?.tags).toEqual(["tag-1", "tag-2", "tag-3", "tag-4"]);
expect(dbTrace[0]?.tags.length).toBe(4);
});
it("should fail for wrong event formats", async () => {
+15 -15
View File
@@ -489,16 +489,16 @@ describe("/api/public/scores API Endpoint", () => {
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
{
id: scoreId_2,
name: scoreName,
value: 50.5,
},
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
]);
});
it("test operator >=", async () => {
@@ -514,16 +514,16 @@ describe("/api/public/scores API Endpoint", () => {
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_2,
name: scoreName,
value: 50.5,
},
{
id: scoreId_3,
name: scoreName,
value: 100.8,
},
{
id: scoreId_2,
name: scoreName,
value: 50.5,
},
]);
});
it("test operator !=", async () => {
@@ -539,16 +539,16 @@ describe("/api/public/scores API Endpoint", () => {
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
{
id: scoreId_3,
name: scoreName,
value: 100.8,
},
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
]);
});
it("test operator =", async () => {
+26
View File
@@ -182,6 +182,32 @@ describe("/api/public/traces API Endpoint", () => {
endTime: "2021-01-01T00:20:00.000Z",
});
// Simulate scores on the trace
const scoreId1 = uuidv4();
await makeAPICall("POST", "/api/public/scores", {
id: scoreId1,
name: "score-1",
value: 75.0,
traceId: traceId,
comment: "First score",
});
const scoreId2 = uuidv4();
await makeAPICall("POST", "/api/public/scores", {
id: scoreId2,
name: "score-2",
value: 85.5,
traceId: traceId,
comment: "Second score",
});
const scoreId3 = uuidv4();
await makeAPICall("POST", "/api/public/scores", {
id: scoreId3,
name: "score-3",
value: 95.0,
traceId: traceId,
comment: "Third score",
});
// GET traces
// Retrieve the trace with totalCost and latency
const traces = await makeAPICall<GetTracesAPIResponse>(
+6
View File
@@ -0,0 +1,6 @@
import chatCompletionHandler from "@/src/ee/features/playground/server/chatCompletionHandler";
export const dynamic = "force-dynamic";
export const maxDuration = 60;
export const POST = chatCompletionHandler;
@@ -63,6 +63,7 @@ export const ChatMessageComponent: React.FC<ChatMessageProps> = ({
<Textarea
ref={textAreaRef}
id={message.id}
className="height-[auto] min-h-8 w-full pt-3 font-mono text-xs focus:outline-none"
placeholder={placeholder}
value={message.content}
+4 -4
View File
@@ -41,10 +41,10 @@ export const LangfuseLogo = ({
className={cn(
"flex items-center gap-2 self-stretch rounded-md px-1 py-1 text-xs ring-1 sm:px-3 sm:py-2 lg:-mx-2",
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING"
? "bg-blue-100 text-blue-500 ring-blue-500"
? "bg-light-blue text-dark-blue ring-dark-blue"
: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV"
? "bg-green-100 text-green-500 ring-green-500"
: "bg-red-100 text-red-500 ring-red-500",
? "bg-light-green text-dark-green ring-dark-green"
: "bg-light-red text-dark-red ring-dark-red",
)}
>
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV" ? (
@@ -76,7 +76,7 @@ export const LangfuseLogo = ({
target="_blank"
rel="noopener"
title="View releases on GitHub"
className="ml-2 text-xs text-gray-400"
className="ml-2 text-xs text-muted-foreground"
>
{VERSION}
</a>
@@ -0,0 +1,77 @@
import Link from "next/link";
import { Label } from "@/src/components/ui/label";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { api } from "@/src/utils/api";
import { supportedModels, type UIModelParams } from "@langfuse/shared";
import { ArrowTopRightIcon } from "@radix-ui/react-icons";
export const LLMApiKeyComponent = (p: {
projectId: string;
modelParams: UIModelParams;
}) => {
const hasAccess = useHasAccess({
projectId: p.projectId,
scope: "llmApiKeys:read",
});
if (!hasAccess) {
return (
<div>
<Label className="text-xs font-semibold">API key</Label>
<p className="text-sm text-muted-foreground">
LLM API Key only visible to Owner and Admin roles.
</p>
</div>
);
}
const apiKeys = api.llmApiKey.all.useQuery({
projectId: p.projectId,
});
if (apiKeys.isLoading) {
return (
<div>
<Label className="text-xs font-semibold">API key</Label>
<p className="text-sm text-muted-foreground">Loading...</p>
</div>
);
}
const model = p.modelParams.model.value;
const modelProvider = Object.entries(supportedModels).find((providerData) =>
(providerData[1] as any as string[]).includes(model),
)?.[0];
const apiKey = apiKeys.data?.data.find((k) => k.provider === modelProvider);
return (
<div className="space-y-2 text-xs">
<Label className="text-xs font-semibold">API key</Label>
<div>
{apiKey ? (
<span className="mr-2 rounded-sm bg-input p-1 text-xs">
{apiKey.displaySecretKey}
</span>
) : undefined}
</div>
{/* Custom form message to include a link to the already existing prompt */}
{!apiKey ? (
<div className="flex flex-col font-medium text-destructive">
{`No LLM API key found for provider ${modelProvider}.`}
<Link
href={`/project/${p.projectId}/settings`}
className="flex flex-row"
>
Create a new LLM API key here. <ArrowTopRightIcon />
</Link>
</div>
) : undefined}
<p className="text-muted-foreground">
The LLM API key is used for each execution and will incur costs.
</p>
</div>
);
};
@@ -8,6 +8,7 @@ import {
} from "@/src/components/ui/select";
import { Slider } from "@/src/components/ui/slider";
import { Switch } from "@/src/components/ui/switch";
import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL";
import { cn } from "@/src/utils/tailwind";
import {
ModelProvider,
@@ -15,6 +16,8 @@ import {
type UIModelParams,
} from "@langfuse/shared";
import { LLMApiKeyComponent } from "./LLMApiKeyComponent";
export type ModelParamsContext = {
modelParams: UIModelParams;
availableModels?: UIModelParams[];
@@ -33,10 +36,14 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
setModelParamEnabled,
formDisabled = false,
}) => {
const projectId = useProjectIdFromURL();
if (!projectId) return null;
return (
<div className="flex flex-col space-y-4">
<p className="font-semibold">Model</p>
<div className="space-y-6">
<div className="space-y-4">
<ModelParamsSelect
title="Provider"
modelParamsKey="provider"
@@ -104,6 +111,7 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
tooltip="An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both."
updateModelParam={updateModelParamValue}
/>
<LLMApiKeyComponent {...{ projectId, modelParams }} />
</div>
</div>
);
@@ -127,7 +135,12 @@ const ModelParamsSelect = ({
}: ModelParamsSelectProps) => {
return (
<div className="space-y-2">
<p className={cn("text-xs font-semibold", disabled && "text-gray-400")}>
<p
className={cn(
"text-xs font-semibold",
disabled && "text-muted-foreground",
)}
>
{title}
</p>
<Select
@@ -187,7 +200,7 @@ const ModelParamsSlider = ({
<p
className={cn(
"flex-1 text-xs font-semibold",
(!enabled || formDisabled) && "text-gray-400",
(!enabled || formDisabled) && "text-muted-foreground",
)}
>
{title}
+18 -22
View File
@@ -7,27 +7,23 @@ export const Slider = (props: {
loading?: boolean;
onChecked?: (checked: boolean) => void;
isChecked?: boolean; // whether the slider is active
}) => {
console.log("props", props);
return (
<Switch
checked={props.isChecked}
disabled={props.loading || props.disabled}
onChange={props.onChecked}
}) => (
<Switch
checked={props.isChecked}
disabled={props.loading || props.disabled}
onChange={props.onChecked}
className={cn(
props.isChecked ? "bg-background" : "bg-input",
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-foreground focus:ring-offset-2",
)}
>
<span className="sr-only">Use setting</span>
<span
aria-hidden="true"
className={cn(
props.isChecked ? "bg-black" : "bg-gray-200",
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2",
props.isChecked ? "translate-x-5" : "translate-x-0",
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-background shadow ring-0 transition duration-200 ease-in-out",
)}
>
<span className="sr-only">Use setting</span>
<span
aria-hidden="true"
className={cn(
props.isChecked ? "translate-x-5" : "translate-x-0",
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out",
)}
/>
</Switch>
);
};
/>
</Switch>
);
+1 -1
View File
@@ -18,7 +18,7 @@ export const ErrorPage = ({
return (
<div className="flex h-screen flex-col items-center justify-center">
<AlertCircle className="mb-4 h-12 w-12 text-red-500" />
<AlertCircle className="text-dark-red mb-4 h-12 w-12" />
<h1 className="mb-4 text-xl font-bold">{title}</h1>
<p className="mb-8 text-center">{message}</p>
{session.status === "unauthenticated" ? (
+1 -1
View File
@@ -58,7 +58,7 @@ export const GroupedScoreBadges = ({
.sort(([a], [b]) => (a < b ? -1 : 1))
.map(([name, scores]) => (
<div key={name}>
<div className="text-xs text-gray-500">{name}</div>
<div className="text-xs text-muted-foreground">{name}</div>
<ScoresOfGroup scores={scores} />
</div>
))}
+5 -1
View File
@@ -1,6 +1,8 @@
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
import { githubLight } from "@uiw/codemirror-theme-github";
import { tokyoNight } from "@uiw/codemirror-theme-tokyo-night";
import { json } from "@codemirror/lang-json";
import { useTheme } from "next-themes";
import { cn } from "@/src/utils/tailwind";
// todo: add json linting
@@ -18,10 +20,12 @@ export function JsonEditor({
lineWrapping?: boolean;
className?: string;
}) {
const { theme } = useTheme();
const codeMirrorTheme = theme === "dark" ? tokyoNight : githubLight;
return (
<CodeMirror
value={defaultValue}
theme={githubLight}
theme={codeMirrorTheme}
basicSetup={{
foldGutter: true,
}}
+9 -5
View File
@@ -4,6 +4,7 @@ import {
HoverCardTrigger,
} from "@/src/components/ui/hover-card";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { cn } from "@/src/utils/tailwind";
import { HelpCircle, Info } from "lucide-react";
import Link from "next/link";
@@ -40,13 +41,16 @@ export default function DocPopup({
}
}}
>
<HoverCardTrigger className="mx-1 cursor-pointer" asChild>
<HoverCardTrigger
className={cn("mx-1", href ? "cursor-pointer" : "cursor-default")}
asChild
>
{href ? (
<Link
href={href}
rel="noopener"
target="_blank"
className="inline-block whitespace-nowrap text-gray-500 sm:pl-0"
className="inline-block whitespace-nowrap text-muted-foreground sm:pl-0"
onClick={() => {
capture("help_popup:href_clicked", {
href: href,
@@ -62,7 +66,7 @@ export default function DocPopup({
}
</Link>
) : (
<div className="inline-block whitespace-nowrap text-gray-500 sm:pl-0">
<div className="inline-block whitespace-nowrap text-muted-foreground sm:pl-0">
{
{
question: <HelpCircle className={sizes[size]} />,
@@ -74,7 +78,7 @@ export default function DocPopup({
</HoverCardTrigger>
<HoverCardContent>
{typeof description === "string" ? (
<div className="whitespace-break-spaces text-xs font-normal text-gray-800 sm:pl-0">
<div className="whitespace-break-spaces text-xs font-normal text-primary sm:pl-0">
{description}
</div>
) : (
@@ -98,7 +102,7 @@ export function Popup({ triggerContent, description }: PopupProps) {
</HoverCardTrigger>
<HoverCardContent>
{typeof description === "string" ? (
<div className="whitespace-break-spaces text-xs font-normal text-gray-800 sm:pl-0">
<div className="whitespace-break-spaces text-xs font-normal text-primary sm:pl-0">
{description}
</div>
) : (
+7 -7
View File
@@ -49,10 +49,10 @@ export default function Header({
<nav className="sm:hidden" aria-label="Back">
<Link
href={backHref}
className="flex items-center text-sm font-medium text-gray-500 hover:text-gray-700"
className="flex items-center text-sm font-medium text-muted-foreground hover:text-primary"
>
<ChevronLeftIcon
className="-ml-1 mr-1 h-5 w-5 flex-shrink-0 text-gray-400"
className="-ml-1 mr-1 h-5 w-5 flex-shrink-0 text-muted-foreground"
aria-hidden="true"
/>
Back
@@ -67,19 +67,19 @@ export default function Header({
<div className="flex items-center">
{index !== 0 && (
<ChevronRightIcon
className="mr-4 h-5 w-5 flex-shrink-0 text-gray-400"
className="mr-4 h-5 w-5 flex-shrink-0 text-muted-foreground"
aria-hidden="true"
/>
)}
{href ? (
<Link
href={href}
className="text-sm font-medium text-gray-500 hover:text-gray-700"
className="text-sm font-medium text-muted-foreground hover:text-primary"
>
{name}
</Link>
) : (
<div className="text-sm font-medium text-gray-500">
<div className="text-sm font-medium text-muted-foreground">
{name}
</div>
)}
@@ -94,11 +94,11 @@ export default function Header({
<div className="flex items-center gap-3 md:gap-5">
<div className="flex min-w-0 flex-row justify-center align-middle">
{level === "h2" ? (
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
<h2 className="text-2xl font-bold leading-7 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">
<h3 className="text-lg font-bold leading-7 sm:truncate sm:text-xl sm:tracking-tight">
{props.title}
</h3>
)}
+61 -46
View File
@@ -31,14 +31,26 @@ import { ChevronDownIcon } from "@heroicons/react/20/solid";
import useLocalStorage from "@/src/components/useLocalStorage";
import { ProjectNavigation } from "@/src/components/projectNavigation";
import DOMPurify from "dompurify";
import { ThemeToggle } from "@/src/features/theming/ThemeToggle";
const signOutUser = async () => {
localStorage.clear();
sessionStorage.clear();
await signOut({
callbackUrl: "/auth/sign-in",
});
};
const userNavigation = [
{
name: "Theme",
onClick: () => {},
content: <ThemeToggle />,
},
{
name: "Sign out",
onClick: () =>
signOut({
callbackUrl: "/auth/sign-in",
}),
onClick: signOutUser,
},
];
@@ -135,9 +147,8 @@ export default function Layout(props: PropsWithChildren) {
!publishablePaths.includes(router.pathname) &&
!router.pathname.startsWith("/public/")
) {
void signOut({
callbackUrl: "/auth/sign-in",
});
signOutUser();
return <Spinner message="Redirecting" />;
}
@@ -178,7 +189,7 @@ export default function Layout(props: PropsWithChildren) {
router.pathname.startsWith("/public/");
if (hideNavigation)
return (
<main className="min-h-screen bg-gray-50 px-4 py-4 sm:px-6 lg:px-8">
<main className="min-h-screen bg-primary-foreground px-4 py-4 sm:px-6 lg:px-8">
{props.children}
</main>
);
@@ -222,7 +233,7 @@ export default function Layout(props: PropsWithChildren) {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-900/80" />
<div className="fixed inset-0 bg-primary/80" />
</Transition.Child>
<div className="fixed inset-0 flex">
@@ -253,14 +264,14 @@ export default function Layout(props: PropsWithChildren) {
>
<span className="sr-only">Close sidebar</span>
<XMarkIcon
className="h-5 w-5 text-white"
className="h-5 w-5 text-background"
aria-hidden="true"
/>
</button>
</div>
</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">
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-background px-6 py-4">
<LangfuseLogo
version
size="xl"
@@ -273,7 +284,7 @@ export default function Layout(props: PropsWithChildren) {
<MainNavigation nav={navigation} />
</ul>
<div className="mb-2 flex flex-row place-content-between items-center">
<div className="text-xs font-semibold text-gray-400">
<div className="text-xs font-semibold text-muted-foreground">
Project
</div>
<NewProjectButton size="xs" />
@@ -293,7 +304,7 @@ export default function Layout(props: PropsWithChildren) {
{/* Static sidebar for desktop */}
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-56 lg:flex-col">
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex h-screen grow flex-col border-r border-gray-200 bg-white pt-7">
<div className="flex h-screen grow flex-col border-r border-border bg-background pt-7">
<LangfuseLogo
version
size="xl"
@@ -312,16 +323,16 @@ export default function Layout(props: PropsWithChildren) {
description="What do you think about this project? What can be improved?"
type="feedback"
>
<li className="group -mx-2 my-1 flex cursor-pointer gap-x-3 rounded-md p-1.5 text-sm font-semibold text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
<li className="group -mx-2 my-1 flex cursor-pointer gap-x-3 rounded-md p-1.5 text-sm font-semibold text-primary hover:bg-primary-foreground hover:text-primary-accent">
<MessageSquarePlus
className="h-5 w-5 shrink-0 text-gray-400 group-hover:text-indigo-600"
className="h-5 w-5 shrink-0 text-muted-foreground group-hover:text-primary-accent"
aria-hidden="true"
/>
Feedback
</li>
</FeedbackButtonWrapper>
<div className="mb-2 flex flex-row place-content-between items-center">
<div className="text-xs font-semibold text-gray-400">
<div className="text-xs font-semibold text-muted-foreground">
Project
</div>
<NewProjectButton size="xs" />
@@ -334,7 +345,7 @@ export default function Layout(props: PropsWithChildren) {
</nav>
<Menu as="div" className="relative">
<Menu.Button className="flex w-full items-center gap-x-2 overflow-hidden p-1.5 py-3 pl-6 pr-8 text-sm font-semibold text-gray-900 hover:bg-gray-50">
<Menu.Button className="flex w-full items-center gap-x-2 overflow-hidden p-1.5 py-3 pl-6 pr-8 text-sm font-semibold text-primary hover:bg-primary-foreground">
<span className="sr-only">Open user menu</span>
<Avatar className="h-7 w-7">
<AvatarImage src={session.data?.user?.image ?? undefined} />
@@ -348,12 +359,12 @@ export default function Layout(props: PropsWithChildren) {
: null}
</AvatarFallback>
</Avatar>
<span className="flex-shrink truncate text-sm font-semibold text-gray-900">
<span className="flex-shrink truncate text-sm font-semibold text-primary">
{session.data?.user?.name}
</span>
<div className="flex-1" />
<ChevronDownIcon
className="h-5 w-5 text-gray-400"
className="h-5 w-5 text-muted-foreground"
aria-hidden="true"
/>
</Menu.Button>
@@ -366,8 +377,8 @@ export default function Layout(props: PropsWithChildren) {
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 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
<span className="mb-1 block border-b px-3 pb-2 text-sm text-gray-500">
<Menu.Items className="absolute -top-full bottom-1 right-0 z-10 overflow-hidden rounded-md bg-background py-2 shadow-lg ring-1 ring-border focus:outline-none">
<span className="block border-b px-3 pb-2 text-sm leading-6 text-muted-foreground">
{session.data?.user?.email}
</span>
{userNavigation.map((item) => (
@@ -376,11 +387,12 @@ export default function Layout(props: PropsWithChildren) {
<a
onClick={() => void item.onClick()}
className={cn(
active ? "bg-gray-50" : "",
"block cursor-pointer px-3 py-1 text-sm text-gray-900",
active ? "bg-primary-foreground" : "",
"flex cursor-pointer items-center justify-between px-2 py-0.5 text-sm leading-6 text-primary",
)}
>
{item.name}
{item.content}
</a>
)}
</Menu.Item>
@@ -391,10 +403,10 @@ export default function Layout(props: PropsWithChildren) {
</div>
</div>
<div className="sticky top-0 z-40 flex items-center gap-x-6 bg-white px-4 py-4 shadow-sm sm:px-6 lg:hidden">
<div className="sticky top-0 z-40 flex items-center gap-x-6 bg-background px-4 py-4 shadow-sm sm:px-6 lg:hidden">
<button
type="button"
className="-m-2.5 p-2.5 text-gray-700 lg:hidden"
className="-m-2.5 p-2.5 text-primary lg:hidden"
onClick={() => setSidebarOpen(true)}
>
<span className="sr-only">Open sidebar</span>
@@ -406,7 +418,7 @@ export default function Layout(props: PropsWithChildren) {
showEnvLabel={session.data?.user?.email?.endsWith("@langfuse.com")}
/>
<Menu as="div" className="relative">
<Menu.Button className="flex items-center gap-x-4 text-sm font-semibold text-gray-900">
<Menu.Button className="flex items-center gap-x-4 text-sm font-semibold text-primary">
<span className="sr-only">Open user menu</span>
<Avatar className="h-8 w-8">
<AvatarImage src={session.data?.user?.image ?? undefined} />
@@ -430,8 +442,8 @@ export default function Layout(props: PropsWithChildren) {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 mt-2.5 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
<span className="mb-1 block border-b px-3 pb-2 text-sm text-gray-500">
<Menu.Items className="absolute right-0 z-10 mt-2.5 rounded-md bg-background py-2 pb-1 shadow-lg ring-1 ring-border focus:outline-none">
<span className="mb-1 block border-b px-3 pb-2 text-sm leading-6 text-muted-foreground">
{session.data?.user?.email}
</span>
{userNavigation.map((item) => (
@@ -440,11 +452,12 @@ export default function Layout(props: PropsWithChildren) {
<a
onClick={() => void item.onClick()}
className={cn(
active ? "bg-gray-50" : "",
"block cursor-pointer px-3 py-1 text-sm text-gray-900",
active ? "bg-primary-foreground" : "",
"flex cursor-pointer items-center justify-between px-2 py-1 text-sm leading-6 text-primary",
)}
>
{item.name}
{item.content}
</a>
)}
</Menu.Item>
@@ -459,7 +472,7 @@ export default function Layout(props: PropsWithChildren) {
(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING" ||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU") &&
!session.data?.user?.email?.endsWith("@langfuse.com") ? (
<div className="flex w-full items-center border-b border-yellow-500 bg-yellow-100 px-4 py-2 lg:sticky lg:top-0 lg:z-40">
<div className="flex w-full items-center border-b border-dark-yellow bg-light-yellow px-4 py-2 lg:sticky lg:top-0 lg:z-40">
<div className="flex flex-1 flex-wrap gap-1">
<div className="flex items-center gap-1">
<Info className="h-4 w-4" />
@@ -523,9 +536,9 @@ const MainNavigation: React.FC<{
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-1.5 text-sm font-semibold",
? "bg-primary-foreground text-primary-accent"
: "text-primary hover:bg-primary-foreground hover:text-primary-accent",
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold",
)}
onClick={onNavitemClick}
target={item.newTab ? "_blank" : undefined}
@@ -534,8 +547,8 @@ const MainNavigation: React.FC<{
<item.icon
className={clsx(
item.current
? "text-indigo-600"
: "text-gray-400 group-hover:text-indigo-600",
? "text-primary-accent"
: "text-muted-foreground group-hover:text-primary-accent",
"h-5 w-5 shrink-0",
)}
aria-hidden="true"
@@ -547,8 +560,8 @@ const MainNavigation: React.FC<{
className={cn(
"-my-0.5 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",
? "border-primary-accent text-primary-accent"
: "border-border text-muted-foreground group-hover:border-primary-accent group-hover:text-primary-accent",
)}
>
{item.label}
@@ -565,12 +578,12 @@ const MainNavigation: React.FC<{
{({ open }) => (
<>
<Disclosure.Button
className="group flex w-full items-center gap-x-3 rounded-md p-1.5 text-left text-sm font-semibold hover:bg-gray-50 hover:text-indigo-600"
className="group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold hover:bg-primary-foreground hover:text-primary-accent"
onClick={() => setIsOpen(!isOpen)}
>
{item.icon && (
<item.icon
className="h-5 w-5 shrink-0 text-gray-400 group-hover:text-indigo-600"
className="h-5 w-5 shrink-0 text-muted-foreground group-hover:text-primary-accent"
aria-hidden="true"
/>
)}
@@ -580,8 +593,8 @@ const MainNavigation: React.FC<{
className={cn(
"-my-0.5 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",
? "border-primary-accent text-primary-accent"
: "border-border text-muted-foreground group-hover:border-primary-accent group-hover:text-primary-accent",
)}
>
{item.label}
@@ -589,7 +602,9 @@ const MainNavigation: React.FC<{
)}
<ChevronRightIcon
className={clsx(
open ? "rotate-90 text-gray-500" : "text-gray-400",
open
? "rotate-90 text-muted-foreground"
: "text-muted-foreground",
"ml-auto h-5 w-5 shrink-0",
)}
aria-hidden="true"
@@ -603,15 +618,15 @@ const MainNavigation: React.FC<{
href={subItem.href ?? "#"}
className={clsx(
subItem.current
? "bg-gray-50 text-indigo-600"
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
? "bg-primary-foreground text-primary-accent"
: "text-primary hover:bg-primary-foreground hover:text-primary-accent",
"ml-0.5 flex w-full items-center gap-x-3 rounded-md p-1.5 pl-7 pr-2 text-sm",
)}
target={subItem.newTab ? "_blank" : undefined}
>
{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">
<span className="self-center whitespace-nowrap break-keep rounded-sm border border-border px-1 py-0.5 text-xs text-muted-foreground group-hover:border-primary-accent group-hover:text-primary-accent">
{subItem.label}
</span>
)}
+1 -1
View File
@@ -5,7 +5,7 @@ export function Spinner(props: { message: string }) {
<div className="flex min-h-full flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<LangfuseIcon className="mx-auto motion-safe:animate-spin" size={42} />
<h2 className="mt-5 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
<h2 className="mt-5 text-center text-2xl font-bold leading-9 tracking-tight text-primary">
{props.message} ...
</h2>
</div>
+10 -10
View File
@@ -11,22 +11,22 @@ export type Status =
(typeof statusCategories)[keyof typeof statusCategories][number];
export const StatusBadge = (props: { className?: string; type: Status }) => {
let badgeColor = "bg-gray-100 text-gray-800";
let dotColor = "bg-gray-500";
let dotPingColor = "bg-gray-600";
let badgeColor = "bg-muted-gray text-primary";
let dotColor = "bg-muted-foreground";
let dotPingColor = "bg-muted-foreground";
let showDot = true;
if (statusCategories.active.includes(props.type)) {
badgeColor = "bg-green-100 text-green-600";
dotColor = "animate-ping bg-green-500";
dotPingColor = "bg-green-600";
badgeColor = "bg-light-green text-dark-green";
dotColor = "animate-ping bg-dark-green";
dotPingColor = "bg-dark-green";
} else if (statusCategories.error.includes(props.type)) {
badgeColor = "bg-red-100 text-red-600";
dotColor = "bg-red-500";
dotPingColor = "bg-red-600";
badgeColor = "bg-light-red text-dark-red";
dotColor = "animate-ping bg-dark-red";
dotPingColor = "bg-dark-red";
showDot = false;
} else if (statusCategories.completed.includes(props.type)) {
badgeColor = "bg-green-100 text-green-600";
badgeColor = "bg-light-green text-dark-green";
showDot = false;
}
+3 -3
View File
@@ -1,6 +1,6 @@
export const LevelColors = {
DEFAULT: { text: "", bg: "" },
DEBUG: { text: "text-gray-500", bg: "bg-gray-50" },
WARNING: { text: "text-yellow-800", bg: "bg-yellow-50" },
ERROR: { text: "text-red-800", bg: "bg-red-50" },
DEBUG: { text: "text-muted-foreground", bg: "bg-primary-foreground" },
WARNING: { text: "text-dark-yellow", bg: "bg-light-yellow" },
ERROR: { text: "text-dark-red", bg: "bg-light-red" },
};
+6 -6
View File
@@ -32,9 +32,9 @@ export const ProjectNavigation: React.FC<ProjectNavigationProps> = ({
router.push(`/project/${value}`);
}}
>
<SelectTrigger className="h-8 text-gray-700 ring-transparent focus:ring-0 focus:ring-offset-0">
<SelectTrigger className="h-8 text-primary ring-transparent focus:ring-0 focus:ring-offset-0">
<SelectValue
className="text-sm font-semibold text-gray-700"
className="text-sm font-semibold text-primary"
placeholder={currentProjectId}
/>
</SelectTrigger>
@@ -46,8 +46,8 @@ export const ProjectNavigation: React.FC<ProjectNavigationProps> = ({
className={cn(
"truncate",
currentProjectId === project.id
? "border-gray-700 text-gray-700"
: "text-gray-400",
? "border-primary text-primary"
: "text-muted-foreground",
)}
>
{project.name}
@@ -57,8 +57,8 @@ export const ProjectNavigation: React.FC<ProjectNavigationProps> = ({
className={cn(
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
currentProjectId === project.id
? "border-gray-700 text-gray-700"
: "border-gray-200 text-gray-400 group-hover:border-gray-700 group-hover:text-gray-700",
? "border-primary text-primary"
: "border-border text-muted-foreground group-hover:border-primary group-hover:text-primary",
)}
>
view-only
+1 -1
View File
@@ -93,7 +93,7 @@ const Base = (props: {
"Loading.."
) : props.isPublic ? (
<div
className="flex cursor-pointer items-center gap-1 text-green-800"
className="text-dark-green flex cursor-pointer items-center gap-1"
onClick={() => copyUrl()}
>
{isCopied ? "Link copied ..." : "Public"}
+6 -4
View File
@@ -171,7 +171,7 @@ const TraceCardList = ({
}))
.map(({ virtualItem, trace }) => (
<Card
className="border-border-gray-150 group grid w-full gap-3 overflow-hidden p-2 shadow-none hover:border-gray-300 md:grid-cols-3"
className="group grid w-full gap-3 overflow-hidden border-border p-2 shadow-none hover:border-ring md:grid-cols-3"
key={virtualItem.key}
data-index={virtualItem.index}
style={{
@@ -193,10 +193,12 @@ const TraceCardList = ({
>
Trace: {trace.name} ({trace.id})&nbsp;
</Link>
<div className="text-xs text-gray-500">
<div className="text-xs text-muted-foreground">
{trace.timestamp.toLocaleString()}
</div>
<div className="mb-1 mt-2 text-xs text-gray-500">Scores</div>
<div className="mb-1 mt-2 text-xs text-muted-foreground">
Scores
</div>
<div className="flex flex-wrap content-start items-start gap-1">
<GroupedScoreBadges scores={trace.scores} />
</div>
@@ -244,7 +246,7 @@ const SessionIO = ({ traceId }: { traceId: string }) => {
hideIfNull
/>
) : (
<div className="p-2 text-xs text-gray-500">
<div className="p-2 text-xs text-muted-foreground">
This trace has no input or output.
</div>
)}
+1 -1
View File
@@ -32,7 +32,7 @@ export function StarToggle({
<StarIcon
className={cn(
"h-4 w-4",
value ? "fill-current text-yellow-500" : "text-gray-500",
value ? "fill-current text-yellow-500" : "text-muted-foreground",
)}
/>
</Button>
+4 -4
View File
@@ -6,19 +6,19 @@ interface Statistic {
export default function StatsCards({ stats }: { stats: Statistic[] }) {
return (
<div>
<h3 className="text-base font-semibold leading-6 text-gray-900">
<h3 className="text-base font-semibold leading-6 text-primary">
Model configuration
</h3>
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-4">
{stats.map((item) => (
<div
key={item.name}
className="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6"
className="overflow-hidden rounded-lg bg-background px-4 py-5 shadow sm:p-6"
>
<dt className="truncate text-sm font-medium text-gray-500">
<dt className="truncate text-sm font-medium text-muted-foreground">
{item.name}
</dt>
<dd className="mt-1 text-3xl font-semibold tracking-tight text-gray-900">
<dd className="mt-1 text-3xl font-semibold tracking-tight text-primary">
{item.stat}
</dd>
</div>
@@ -83,7 +83,7 @@ export function DataTableColumnVisibilityFilter<TData, TValue>({
>
<Button variant="outline" title="Show/hide columns">
<Columns className="mr-2 h-4 w-4" />
<span className="text-xs text-gray-500">{`(${count}/${total})`}</span>
<span className="text-xs text-muted-foreground">{`(${count}/${total})`}</span>
<ChevronDown className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
@@ -55,7 +55,7 @@ export const DataTableRowHeightSwitch = ({
<TabsTrigger
key={id}
value={id}
className="px-2 shadow-none data-[state=active]:bg-slate-200 data-[state=active]:ring-border"
className="px-2 shadow-none data-[state=active]:bg-input data-[state=active]:ring-border"
>
<span role="img" aria-label={`${label} size`}>
{icon}
+1 -2
View File
@@ -12,7 +12,6 @@ import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
@@ -243,7 +242,7 @@ export function DataTable<TData extends object, TValue>({
<div className="grow"></div>
</div>
{pagination !== undefined ? (
<div className="sticky bottom-0 z-10 flex w-full justify-end bg-white font-medium">
<div className="bg:background sticky bottom-0 z-10 flex w-full justify-end font-medium">
<DataTablePagination
table={table}
paginationOptions={pagination.options}
+2 -1
View File
@@ -19,7 +19,8 @@ export default function TableLink({
: value;
return (
<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"
className="inline-block rounded bg-primary-accent/20 px-2
py-1 text-xs font-semibold text-accent-dark-blue shadow-sm hover:bg-accent-light-blue/45"
href={path}
title={value}
>
@@ -750,7 +750,7 @@ const GenerationsIOCell = ({
data={
io === "output" ? observation.data?.output : observation.data?.input
}
className={cn(io === "output" && "bg-green-50")}
className={cn(io === "output" && "bg-accent-light-green")}
singleLine={singleLine}
/>
);
@@ -111,7 +111,7 @@ export default function ModelTable({ projectId }: { projectId: string }) {
return (
<>
Input Price{" "}
<span className="text-xs text-gray-400">/ 1k units</span>
<span className="text-xs text-muted-foreground">/ 1k units</span>
</>
);
},
@@ -140,7 +140,7 @@ export default function ModelTable({ projectId }: { projectId: string }) {
return (
<>
Output Price{" "}
<span className="text-xs text-gray-400">/ 1k units</span>
<span className="text-xs text-muted-foreground">/ 1k units</span>
</>
);
},
@@ -163,7 +163,7 @@ export default function ModelTable({ projectId }: { projectId: string }) {
return (
<>
Total Price{" "}
<span className="text-xs text-gray-400">/ 1k units</span>
<span className="text-xs text-muted-foreground">/ 1k units</span>
</>
);
},
+48 -16
View File
@@ -13,12 +13,14 @@ import {
} from "@/src/server/api/definitions/scoresTable";
import { api } from "@/src/utils/api";
import type { RouterOutput, RouterInput } from "@/src/utils/types";
import type { FilterState } from "@langfuse/shared";
import { useQueryParams, withDefault, NumberParam } from "use-query-params";
export type ScoresTableRow = {
id: string;
traceId: string;
timestamp: string;
source: string;
name: string;
value: number;
comment?: string;
@@ -33,14 +35,38 @@ export type ScoreFilterInput = Omit<
"projectId" | "userId"
>;
function createFilterState(
userFilterState: FilterState,
omittedFilters: Record<string, string>[],
): FilterState {
return omittedFilters.reduce((filterState, { key, value }) => {
return filterState.concat([
{
column: `${key}`,
type: "string",
operator: "=",
value: value,
},
]);
}, userFilterState);
}
export default function ScoresTable({
projectId,
userId,
traceId,
observationId,
omittedFilter = [],
hiddenColumns = [],
tableColumnVisibilityName = "scoresColumnVisibility",
}: {
projectId: string;
userId?: string;
traceId?: string;
observationId?: string;
omittedFilter?: string[];
hiddenColumns?: string[];
tableColumnVisibilityName?: string;
}) {
const [paginationState, setPaginationState] = useQueryParams({
pageIndex: withDefault(NumberParam, 0),
@@ -53,16 +79,12 @@ export default function ScoresTable({
[],
"scores",
);
const filterState = userId
? userFilterState.concat([
{
column: "User ID",
type: "string",
operator: "=",
value: userId,
},
])
: userFilterState;
const filterState = createFilterState(userFilterState, [
...(userId ? [{ key: "User ID", value: userId }] : []),
...(traceId ? [{ key: "Trace ID", value: traceId }] : []),
...(observationId ? [{ key: "Observation ID", value: observationId }] : []),
]);
const [orderByState, setOrderByState] = useOrderByState({
column: "timestamp",
@@ -82,7 +104,7 @@ export default function ScoresTable({
projectId,
});
const columns: LangfuseColumnDef<ScoresTableRow>[] = [
const rawColumns: LangfuseColumnDef<ScoresTableRow>[] = [
{
accessorKey: "traceId",
id: "traceId",
@@ -146,6 +168,13 @@ export default function ScoresTable({
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "source",
header: "Source",
id: "source",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "name",
header: "Name",
@@ -218,16 +247,18 @@ export default function ScoresTable({
cell: ({ row }) => {
const value = row.getValue("comment") as ScoresTableRow["comment"];
return (
value !== undefined && (
<IOTableCell data={value} singleLine={rowHeight === "s"} />
)
!!value && <IOTableCell data={value} singleLine={rowHeight === "s"} />
);
},
},
];
const columns = rawColumns.filter(
(c) => !!c.id && !hiddenColumns.includes(c.id),
);
const [columnVisibility, setColumnVisibility] =
useColumnVisibility<ScoresTableRow>("scoresColumnVisibility", columns);
useColumnVisibility<ScoresTableRow>(tableColumnVisibilityName, columns);
const convertToTableRow = (
score: RouterOutput["scores"]["all"]["scores"][0],
@@ -235,6 +266,7 @@ export default function ScoresTable({
return {
id: score.id,
timestamp: score.timestamp.toLocaleString(),
source: score.source,
name: score.name,
value: score.value,
comment: score.comment ?? undefined,
@@ -250,7 +282,7 @@ export default function ScoresTable({
traceFilterOptions: ScoreOptions | undefined,
) => {
return scoresTableColsWithOptions(traceFilterOptions).filter(
(c) => !omittedFilter?.includes(c.name),
(c) => !omittedFilter?.includes(c.name) && !hiddenColumns.includes(c.id),
);
};
@@ -547,6 +547,7 @@ export default function TracesTable({
header: "Version",
enableHiding: true,
enableSorting: true,
defaultHidden: true,
},
{
accessorKey: "release",
@@ -554,6 +555,7 @@ export default function TracesTable({
header: "Release",
enableHiding: true,
enableSorting: true,
defaultHidden: true,
},
{
accessorKey: "tags",
@@ -690,7 +692,7 @@ const TracesIOCell = ({
<IOTableCell
isLoading={trace.isLoading}
data={io === "output" ? trace.data?.output : trace.data?.input}
className={cn(io === "output" && "bg-green-50")}
className={cn(io === "output" && "bg-accent-light-green")}
singleLine={singleLine}
/>
);
+11 -9
View File
@@ -112,7 +112,7 @@ export const IOPreview: React.FC<{
title="Output"
json={outputClean}
isLoading={isLoading}
className="flex-1 bg-green-50"
className="flex-1 bg-accent-light-green dark:border-accent-dark-green"
/>
) : null}
</>
@@ -187,10 +187,11 @@ export const OpenAiMessageView: React.FC<{
title={message.name ?? message.role}
json={message.content}
className={cn(
"bg-gray-100",
message.role === "system" && "bg-gray-100",
message.role === "assistant" && "bg-green-50",
message.role === "user" && "bg-white",
"bg-muted",
message.role === "system" && "bg-primary-foreground",
message.role === "assistant" &&
"bg-accent-light-green dark:border-accent-dark-green",
message.role === "user" && "bg-background",
!!message.json && "rounded-b-none",
)}
/>
@@ -202,10 +203,11 @@ export const OpenAiMessageView: React.FC<{
}
json={message.json}
className={cn(
"bg-gray-100",
message.role === "system" && "bg-gray-100",
message.role === "assistant" && "bg-green-50",
message.role === "user" && "bg-white",
"bg-muted",
message.role === "system" && "bg-primary-foreground",
message.role === "assistant" &&
"bg-accent-light-green dark:border-accent-dark-green",
message.role === "user" && "bg-foreground",
!!message.content && "rounded-t-none border-t-0",
)}
/>
+180 -149
View File
@@ -1,5 +1,5 @@
import { JSONView } from "@/src/components/ui/CodeJsonViewer";
import { type Score } from "@langfuse/shared";
import { type ScoreSource, type Score } from "@langfuse/shared";
import {
Card,
CardContent,
@@ -8,14 +8,6 @@ import {
CardTitle,
} from "@/src/components/ui/card";
import { Badge } from "@/src/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/src/components/ui/table";
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
import { NewDatasetItemFromTrace } from "@/src/features/datasets/components/NewDatasetItemFromObservationButton";
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
@@ -25,6 +17,11 @@ import { formatIntervalSeconds } from "@/src/utils/dates";
import Link from "next/link";
import { usdFormatter } from "@/src/utils/numbers";
import { calculateDisplayTotalCost } from "@/src/components/trace";
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import { withDefault, StringParam, useQueryParam } from "use-query-params";
import ScoresTable from "@/src/components/table/use-cases/scores";
import { ScoresPreview } from "@/src/components/trace/ScoresPreview";
import { JumpToPlaygroundButton } from "@/src/ee/features/playground/page/components/JumpToPlaygroundButton";
export const ObservationPreview = (props: {
observations: Array<ObservationReturnType>;
@@ -33,6 +30,11 @@ export const ObservationPreview = (props: {
currentObservationId: string;
traceId: string;
}) => {
const [selectedTab, setSelectedTab] = useQueryParam(
"view",
withDefault(StringParam, "preview"),
);
const observationWithInputAndOutput = api.observations.byId.useQuery({
observationId: props.currentObservationId,
traceId: props.traceId,
@@ -47,152 +49,181 @@ export const ObservationPreview = (props: {
);
if (!preloadedObservation) return <div className="flex-1">Not found</div>;
return (
<Card className="flex-1">
<CardHeader className="flex flex-row flex-wrap justify-between gap-2">
<div className="flex flex-col gap-1">
<CardTitle>
<span className="mr-2 rounded-sm bg-gray-200 p-1 text-xs">
{preloadedObservation.type}
</span>
<span>{preloadedObservation.name}</span>
</CardTitle>
<CardDescription className="flex gap-2">
{preloadedObservation.startTime.toLocaleString()}
</CardDescription>
<div className="flex flex-wrap gap-2">
{preloadedObservation.promptId ? (
<PromptBadge
promptId={preloadedObservation.promptId}
projectId={preloadedObservation.projectId}
/>
) : undefined}
{preloadedObservation.timeToFirstToken ? (
<Badge variant="outline">
Time to first token:{" "}
{formatIntervalSeconds(preloadedObservation.timeToFirstToken)}
</Badge>
) : null}
{preloadedObservation.endTime ? (
<Badge variant="outline">
Latency:{" "}
{formatIntervalSeconds(
(preloadedObservation.endTime.getTime() -
preloadedObservation.startTime.getTime()) /
1000,
)}
</Badge>
) : null}
{preloadedObservation.type === "GENERATION" && (
<Badge variant="outline">
{preloadedObservation.promptTokens} prompt {" "}
{preloadedObservation.completionTokens} completion ({" "}
{preloadedObservation.totalTokens})
</Badge>
)}
{preloadedObservation.version ? (
<Badge variant="outline">
Version: {preloadedObservation.version}
</Badge>
) : undefined}
{preloadedObservation.model ? (
<Badge variant="outline">{preloadedObservation.model}</Badge>
) : null}
{totalCost ? (
<Badge variant="outline">
{usdFormatter(totalCost.toNumber())}
</Badge>
) : undefined}
{preloadedObservation.modelParameters &&
typeof preloadedObservation.modelParameters === "object"
? Object.entries(preloadedObservation.modelParameters)
.filter(Boolean)
.map(([key, value]) => (
<Badge variant="outline" key={key}>
{key}: {value?.toString()}
</Badge>
))
: null}
const observationScores = props.scores.filter(
(s) => s.observationId === preloadedObservation.id,
);
const observationScoresBySource = observationScores.reduce((acc, score) => {
if (!acc.get(score.source)) {
acc.set(score.source, []);
}
acc.get(score.source)?.push(score);
return acc;
}, new Map<ScoreSource, Score[]>());
return (
<Card className="col-span-2 flex max-h-full flex-col overflow-hidden">
<div className="flex flex-shrink-0 flex-row justify-end gap-2">
<Tabs
value={selectedTab}
onValueChange={setSelectedTab}
className="flex w-full justify-end border-b bg-background"
>
<TabsList className="bg-background py-0">
<TabsTrigger
value="preview"
className="h-full rounded-none border-b-4 border-transparent data-[state=active]:border-primary-accent data-[state=active]:shadow-none"
>
Preview
</TabsTrigger>
<TabsTrigger
value="scores"
className="h-full rounded-none border-b-4 border-transparent data-[state=active]:border-primary-accent data-[state=active]:shadow-none"
>
Scores
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="flex w-full flex-col overflow-y-auto">
<CardHeader className="flex flex-row flex-wrap justify-between gap-2">
<div className="flex flex-col gap-1">
<CardTitle>
<span className="mr-2 rounded-sm bg-input p-1 text-xs">
{preloadedObservation.type}
</span>
<span>{preloadedObservation.name}</span>
</CardTitle>
<CardDescription className="flex gap-2">
{preloadedObservation.startTime.toLocaleString()}
</CardDescription>
<div className="flex flex-wrap gap-2">
{preloadedObservation.promptId ? (
<PromptBadge
promptId={preloadedObservation.promptId}
projectId={preloadedObservation.projectId}
/>
) : undefined}
{preloadedObservation.timeToFirstToken ? (
<Badge variant="outline">
Time to first token:{" "}
{formatIntervalSeconds(preloadedObservation.timeToFirstToken)}
</Badge>
) : null}
{preloadedObservation.endTime ? (
<Badge variant="outline">
Latency:{" "}
{formatIntervalSeconds(
(preloadedObservation.endTime.getTime() -
preloadedObservation.startTime.getTime()) /
1000,
)}
</Badge>
) : null}
{preloadedObservation.type === "GENERATION" && (
<Badge variant="outline">
{preloadedObservation.promptTokens} prompt {" "}
{preloadedObservation.completionTokens} completion ({" "}
{preloadedObservation.totalTokens})
</Badge>
)}
{preloadedObservation.version ? (
<Badge variant="outline">
Version: {preloadedObservation.version}
</Badge>
) : undefined}
{preloadedObservation.model ? (
<Badge variant="outline">{preloadedObservation.model}</Badge>
) : null}
{totalCost ? (
<Badge variant="outline">
{usdFormatter(totalCost.toNumber())}
</Badge>
) : undefined}
{preloadedObservation.modelParameters &&
typeof preloadedObservation.modelParameters === "object"
? Object.entries(preloadedObservation.modelParameters)
.filter(Boolean)
.map(([key, value]) => (
<Badge variant="outline" key={key}>
{key}: {value?.toString()}
</Badge>
))
: null}
</div>
</div>
</div>
<div className="flex gap-2">
<ManualScoreButton
projectId={props.projectId}
traceId={preloadedObservation.traceId}
observationId={preloadedObservation.id}
scores={props.scores}
/>
{observationWithInputAndOutput.data ? (
<NewDatasetItemFromTrace
<div className="flex flex-wrap gap-2">
<ManualScoreButton
projectId={props.projectId}
traceId={preloadedObservation.traceId}
observationId={preloadedObservation.id}
projectId={props.projectId}
input={observationWithInputAndOutput.data.input}
output={observationWithInputAndOutput.data.output}
metadata={preloadedObservation.metadata}
key={preloadedObservation.id}
scores={props.scores}
/>
) : null}
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<IOPreview
key={preloadedObservation.id + "-input"}
input={observationWithInputAndOutput.data?.input ?? undefined}
output={observationWithInputAndOutput.data?.output ?? undefined}
isLoading={observationWithInputAndOutput.isLoading}
/>
{preloadedObservation.statusMessage ? (
<JSONView
key={preloadedObservation.id + "-status"}
title="Status Message"
json={preloadedObservation.statusMessage}
/>
) : null}
{observationWithInputAndOutput.data?.metadata ? (
<JSONView
key={observationWithInputAndOutput.data.id + "-metadata"}
title="Metadata"
json={observationWithInputAndOutput.data.metadata}
/>
) : null}
{props.scores.find(
(s) => s.observationId === preloadedObservation.id,
) ? (
<div className="flex flex-col gap-2">
<h3>Scores</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Timestamp</TableHead>
<TableHead>Name</TableHead>
<TableHead className="text-right">Value</TableHead>
<TableHead>Comment</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{props.scores
.filter((s) => s.observationId === preloadedObservation.id)
.map((s) => (
<TableRow key={s.id}>
<TableCell className="text-xs">
{s.timestamp.toLocaleString()}
</TableCell>
<TableCell className="text-xs">{s.name}</TableCell>
<TableCell className="text-right text-xs">
{s.value}
</TableCell>
<TableCell className="text-xs">{s.comment}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{observationWithInputAndOutput.data?.type === "GENERATION" && (
<JumpToPlaygroundButton
source="generation"
generation={observationWithInputAndOutput.data}
analyticsEventName="trace_detail:test_in_playground_button_click"
fullWidth
/>
)}
{observationWithInputAndOutput.data ? (
<NewDatasetItemFromTrace
traceId={preloadedObservation.traceId}
observationId={preloadedObservation.id}
projectId={props.projectId}
input={observationWithInputAndOutput.data.input}
output={observationWithInputAndOutput.data.output}
metadata={preloadedObservation.metadata}
key={preloadedObservation.id}
/>
) : null}
</div>
) : null}
</CardContent>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{selectedTab === "preview" && (
<>
<IOPreview
key={preloadedObservation.id + "-input"}
input={observationWithInputAndOutput.data?.input ?? undefined}
output={observationWithInputAndOutput.data?.output ?? undefined}
isLoading={observationWithInputAndOutput.isLoading}
/>
{preloadedObservation.statusMessage ? (
<JSONView
key={preloadedObservation.id + "-status"}
title="Status Message"
json={preloadedObservation.statusMessage}
/>
) : null}
{observationWithInputAndOutput.data?.metadata ? (
<JSONView
key={observationWithInputAndOutput.data.id + "-metadata"}
title="Metadata"
json={observationWithInputAndOutput.data.metadata}
/>
) : null}
<ScoresPreview itemScoresBySource={observationScoresBySource} />
</>
)}
{selectedTab === "scores" && (
<ScoresTable
projectId={props.projectId}
omittedFilter={["Observation ID"]}
observationId={preloadedObservation.id}
hiddenColumns={[
"traceId",
"observationId",
"traceName",
"jobConfigurationId",
"userId",
]}
tableColumnVisibilityName="scoresColumnVisibilityObservationPreview"
/>
)}
</CardContent>
</div>
</Card>
);
};
+11 -11
View File
@@ -68,13 +68,13 @@ const ObservationTreeTraceNode = (props: {
"group mb-0.5 flex cursor-pointer flex-col gap-1 rounded-sm p-1",
props.currentObservationId === undefined ||
props.currentObservationId === ""
? "bg-gray-100"
: "hover:bg-gray-50",
? "bg-muted"
: "hover:bg-primary-foreground",
)}
onClick={() => props.setCurrentObservationId(undefined)}
>
<div className="flex gap-2">
<span className={cn("rounded-sm bg-gray-200 p-1 text-xs")}>TRACE</span>
<span className={cn("rounded-sm bg-input p-1 text-xs")}>TRACE</span>
<span className="flex-1 break-all text-sm">{props.trace.name}</span>
<Button
onClick={(ev) => (ev.stopPropagation(), props.expandAll())}
@@ -96,7 +96,7 @@ const ObservationTreeTraceNode = (props: {
{props.showMetrics && props.trace.latency ? (
<div className="flex gap-2">
<span className="text-xs text-gray-500">
<span className="text-xs text-muted-foreground">
{formatIntervalSeconds(props.trace.latency)}
</span>
</div>
@@ -141,8 +141,8 @@ const ObservationTreeNode = (props: {
className={cn(
"group my-0.5 flex flex-1 cursor-pointer flex-col gap-1 rounded-sm p-1",
props.currentObservationId === observation.id
? "bg-gray-100"
: "hover:bg-gray-50",
? "bg-muted"
: "hover:bg-primary-foreground",
)}
onClick={() => props.setCurrentObservationId(observation.id)}
>
@@ -188,7 +188,7 @@ const ObservationTreeNode = (props: {
observation.endTime) && (
<div className="flex gap-2">
{observation.endTime ? (
<span className="text-xs text-gray-500">
<span className="text-xs text-muted-foreground">
{formatIntervalSeconds(
(observation.endTime.getTime() -
observation.startTime.getTime()) /
@@ -199,7 +199,7 @@ const ObservationTreeNode = (props: {
{observation.promptTokens ||
observation.completionTokens ||
observation.totalTokens ? (
<span className="text-xs text-gray-500">
<span className="text-xs text-muted-foreground">
{observation.promptTokens} {" "}
{observation.completionTokens} ({" "}
{observation.totalTokens})
@@ -258,9 +258,9 @@ const ColorCodedObservationType = (props: {
observationType: $Enums.ObservationType;
}) => {
const colors: Record<$Enums.ObservationType, string> = {
[$Enums.ObservationType.SPAN]: "bg-blue-100",
[$Enums.ObservationType.GENERATION]: "bg-orange-100",
[$Enums.ObservationType.EVENT]: "bg-green-100",
[$Enums.ObservationType.SPAN]: "bg-muted-blue",
[$Enums.ObservationType.GENERATION]: "bg-muted-orange",
[$Enums.ObservationType.EVENT]: "bg-muted-green",
};
return (
@@ -0,0 +1,29 @@
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
import { type Score } from "@langfuse/shared";
export const ScoresPreview = ({
itemScoresBySource,
}: {
itemScoresBySource: Map<string, Score[]>;
}) => {
if (!Boolean(itemScoresBySource.size)) return null;
return (
<div className="flex flex-col rounded-md border">
<span className="border-b px-3 py-1 text-xs font-medium">Scores</span>
<div
key={itemScoresBySource.size}
className="grid grid-flow-row gap-2 overflow-x-auto px-3 pb-3 pt-1"
>
{Array.from(itemScoresBySource).map(([source, scores]) => (
<div key={source} className="flex flex-col align-middle text-xs">
<span className="min-w-16 p-1 font-medium">{source}</span>
<div className="flex flex-col content-start items-start gap-1 text-nowrap">
<GroupedScoreBadges scores={scores} />
</div>
</div>
))}
</div>
</div>
);
};
+111 -92
View File
@@ -1,5 +1,5 @@
import { JSONView } from "@/src/components/ui/CodeJsonViewer";
import { type Trace, type Score } from "@langfuse/shared";
import { type Trace, type Score, type ScoreSource } from "@langfuse/shared";
import {
Card,
CardContent,
@@ -8,14 +8,6 @@ import {
CardHeader,
CardTitle,
} from "@/src/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/src/components/ui/table";
import { TraceAggUsageBadge } from "@/src/components/token-usage-badge";
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
import { Badge } from "@/src/components/ui/badge";
@@ -23,6 +15,10 @@ import { type ObservationReturnType } from "@/src/server/api/routers/traces";
import { IOPreview } from "@/src/components/trace/IOPreview";
import { formatIntervalSeconds } from "@/src/utils/dates";
import { NewDatasetItemFromTrace } from "@/src/features/datasets/components/NewDatasetItemFromObservationButton";
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import { withDefault, StringParam, useQueryParam } from "use-query-params";
import ScoresTable from "@/src/components/table/use-cases/scores";
import { ScoresPreview } from "@/src/components/trace/ScoresPreview";
export const TracePreview = ({
trace,
@@ -33,92 +29,115 @@ export const TracePreview = ({
observations: ObservationReturnType[];
scores: Score[];
}) => {
const [selectedTab, setSelectedTab] = useQueryParam(
"view",
withDefault(StringParam, "preview"),
);
const traceScores = scores.filter((s) => s.observationId === null);
const traceScoresBySource = traceScores.reduce((acc, score) => {
if (!acc.get(score.source)) {
acc.set(score.source, []);
}
acc.get(score.source)?.push(score);
return acc;
}, new Map<ScoreSource, Score[]>());
return (
<Card className="flex-1">
<CardHeader className="flex flex-row flex-wrap justify-between gap-2">
<div className="flex flex-col gap-1">
<CardTitle>
<span className="mr-2 rounded-sm bg-gray-200 p-1 text-xs">
TRACE
</span>
<span>{trace.name}</span>
</CardTitle>
<CardDescription>{trace.timestamp.toLocaleString()}</CardDescription>
<Card className="col-span-2 flex max-h-full flex-col overflow-hidden">
<div className="flex flex-shrink-0 flex-row justify-end gap-2">
<Tabs
value={selectedTab}
onValueChange={setSelectedTab}
className="flex w-full justify-end border-b bg-background"
>
<TabsList className="bg-background py-0">
<TabsTrigger
value="preview"
className="data-[state=active]:border-primary-accent h-full rounded-none border-b-4 border-transparent data-[state=active]:shadow-none"
>
Preview
</TabsTrigger>
<TabsTrigger
value="scores"
className="data-[state=active]:border-primary-accent h-full rounded-none border-b-4 border-transparent data-[state=active]:shadow-none"
>
Scores
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="flex w-full flex-col overflow-y-auto">
<CardHeader className="flex flex-row flex-wrap justify-between gap-2">
<div className="flex flex-col gap-1">
<CardTitle>
<span className="mr-2 rounded-sm bg-input p-1 text-xs">
TRACE
</span>
<span>{trace.name}</span>
</CardTitle>
<CardDescription>
{trace.timestamp.toLocaleString()}
</CardDescription>
<div className="flex flex-wrap gap-2">
{!!trace.latency && (
<Badge variant="outline">
{formatIntervalSeconds(trace.latency)}
</Badge>
)}
<TraceAggUsageBadge observations={observations} />
{!!trace.release && (
<Badge variant="outline">Release: {trace.release}</Badge>
)}
{!!trace.version && (
<Badge variant="outline">Version: {trace.version}</Badge>
)}
</div>
</div>
<div className="flex flex-wrap gap-2">
{!!trace.latency && (
<Badge variant="outline">
{formatIntervalSeconds(trace.latency)}
</Badge>
)}
<TraceAggUsageBadge observations={observations} />
{!!trace.release && (
<Badge variant="outline">Release: {trace.release}</Badge>
)}
{!!trace.version && (
<Badge variant="outline">Version: {trace.version}</Badge>
)}
<ManualScoreButton
projectId={trace.projectId}
traceId={trace.id}
scores={scores}
/>
<NewDatasetItemFromTrace
traceId={trace.id}
projectId={trace.projectId}
input={trace.input}
output={trace.output}
metadata={trace.metadata}
key={trace.id}
/>
</div>
</div>
<div className="flex gap-2">
<ManualScoreButton
projectId={trace.projectId}
traceId={trace.id}
scores={scores}
/>
<NewDatasetItemFromTrace
traceId={trace.id}
projectId={trace.projectId}
input={trace.input}
output={trace.output}
metadata={trace.metadata}
key={trace.id}
/>
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<IOPreview
key={trace.id + "-io"}
input={trace.input ?? undefined}
output={trace.output ?? undefined}
/>
<JSONView
key={trace.id + "-metadata"}
title="Metadata"
json={trace.metadata}
/>
{scores.find((s) => s.observationId === null) ? (
<div className="mt-5 flex flex-col gap-2">
<h3>Scores</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Timestamp</TableHead>
<TableHead>Name</TableHead>
<TableHead className="text-right">Value</TableHead>
<TableHead>Comment</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{scores
.filter((s) => s.observationId === null)
.map((s) => (
<TableRow key={s.id}>
<TableCell className="text-xs">
{s.timestamp.toLocaleString()}
</TableCell>
<TableCell className="text-xs">{s.name}</TableCell>
<TableCell className="text-right text-xs">
{s.value}
</TableCell>
<TableCell className="text-xs">{s.comment}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : null}
</CardContent>
<CardFooter></CardFooter>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{selectedTab === "preview" && (
<>
<IOPreview
key={trace.id + "-io"}
input={trace.input ?? undefined}
output={trace.output ?? undefined}
/>
<JSONView
key={trace.id + "-metadata"}
title="Metadata"
json={trace.metadata}
/>
<ScoresPreview itemScoresBySource={traceScoresBySource} />
</>
)}
{selectedTab === "scores" && (
<ScoresTable
projectId={trace.projectId}
omittedFilter={["Trace ID"]}
traceId={trace.id}
hiddenColumns={["traceName", "jobConfigurationId", "userId"]}
tableColumnVisibilityName="scoresColumnVisibilityTracePreview"
/>
)}
</CardContent>
<CardFooter></CardFooter>
</div>
</Card>
);
};
+12 -2
View File
@@ -3,8 +3,10 @@ import { Button } from "@/src/components/ui/button";
import { Check, ChevronsDownUp, ChevronsUpDown, Copy } from "lucide-react";
import { cn } from "@/src/utils/tailwind";
import { default as React18JsonView } from "react18-json-view";
import "react18-json-view/src/dark.css";
import { deepParseJson } from "@/src/utils/json";
import { Skeleton } from "@/src/components/ui/skeleton";
import { useTheme } from "next-themes";
export function JSONView(props: {
json?: unknown;
@@ -15,11 +17,18 @@ export function JSONView(props: {
}) {
// some users ingest stringified json nested in json, parse it
const parsedJson = deepParseJson(props.json);
const { theme } = useTheme();
return (
<div className={cn("rounded-md border", props.className)}>
{props.title ? (
<div className="border-b px-3 py-1 text-xs font-medium">
<div
className={cn(
props.title === "assistant" || props.title === "Output"
? "dark:border-accent-dark-green"
: "",
"border-b px-3 py-1 text-xs font-medium",
)}
>
{props.title}
</div>
) : undefined}
@@ -35,6 +44,7 @@ export function JSONView(props: {
<React18JsonView
src={parsedJson}
theme="github"
dark={theme === "dark"}
collapseObjectsAfterLength={20}
collapseStringsAfterLength={500}
displaySize={"collapsed"}
@@ -1,50 +0,0 @@
// https://tailwindui.com/components/application-ui/data-display/description-lists
import clsx from "clsx";
import { type ReactNode } from "react";
export default function DescriptionList(props: {
header?: {
title: string;
description: string;
};
items: { label: string; value: string | ReactNode }[];
descriptionColumns?: number;
valueColumns?: number;
}) {
const { descriptionColumns = 1, valueColumns = 2 } = props;
const totalColumns = descriptionColumns + valueColumns;
return (
<div>
{props.header ? (
<div className="px-4 sm:px-0">
<h3 className="text-base font-semibold leading-7 text-gray-900">
{props.header.title}
</h3>
<p className="mt-1 max-w-2xl text-sm leading-6 text-gray-500">
{props.header.description}
</p>
</div>
) : null}
<div className={clsx(props.header && "mt-6 border-t border-gray-100")}>
<dl className="divide-y divide-gray-100">
{props.items.map((item) => (
<div
key={item.label}
className={`sm:grid-cols-${totalColumns} px-4 py-3 sm:grid sm:gap-4 sm:px-0`}
>
<dt className="text-sm font-medium leading-6 text-gray-900">
{item.label}
</dt>
<dd
className={`mt-1 text-sm leading-6 text-gray-700 sm:col-span-${valueColumns} sm:mt-0`}
>
{item.value}
</dd>
</div>
))}
</dl>
</div>
</div>
);
}
+1 -1
View File
@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"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",
"fixed inset-0 z-50 bg-foreground/40 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
+1 -1
View File
@@ -11,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-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",
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
+2 -2
View File
@@ -27,9 +27,9 @@ const PasswordInput = React.forwardRef<HTMLInputElement, PasswordInputProps>(
title={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? (
<EyeOff className="h-5 w-5 text-gray-400" />
<EyeOff className="h-5 w-5 text-muted-foreground" />
) : (
<Eye className="h-5 w-5 text-gray-400" />
<Eye className="h-5 w-5 text-muted-foreground" />
)}
</button>
</div>
+1 -1
View File
@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"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",
"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 dark:data-[state=unchecked]:bg-muted-foreground",
className,
)}
{...props}
+1 -1
View File
@@ -74,7 +74,7 @@ const TableHead = React.forwardRef<
<th
ref={ref}
className={cn(
"sticky top-0 z-10 border-b bg-white px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
"sticky top-0 z-10 border-b bg-background px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.44.0";
export const VERSION = "v2.46.0";
@@ -318,7 +318,7 @@ export const InnerEvalConfigForm = (props: {
<JSONView
title={"Eval Template"}
json={props.evalTemplate.prompt ?? null}
className={"min-h-48 bg-gray-100 lg:w-1/2"}
className={"min-h-48 bg-muted lg:w-1/2"}
/>
<div className=" flex flex-col gap-2 lg:w-1/3">
{fields.map((mappingField, index) => (
@@ -33,8 +33,6 @@ import {
type ModelParams,
} from "@langfuse/shared";
import { PromptDescription } from "@/src/features/prompts/components/prompt-description";
import Link from "next/dist/client/link";
import { ArrowTopRightIcon } from "@radix-ui/react-icons";
import {
Select,
SelectContent,
@@ -43,8 +41,6 @@ import {
SelectValue,
} from "@/src/components/ui/select";
import { TEMPLATES } from "@/src/ee/features/evals/components/templates";
import { Label } from "@/src/components/ui/label";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { getFinalModelParams } from "@/src/ee/utils/getFinalModelParams";
@@ -73,9 +69,9 @@ export const EvalTemplateForm = (props: {
value={langfuseTemplate ?? ""}
onValueChange={updateLangfuseTemplate}
>
<SelectTrigger className="text-gray-700 ring-transparent focus:ring-0 focus:ring-offset-0">
<SelectTrigger className="text-primary ring-transparent focus:ring-0 focus:ring-offset-0">
<SelectValue
className="text-sm font-semibold text-gray-700"
className="text-sm font-semibold text-primary"
placeholder={"Select a Langfuse managed template"}
/>
</SelectTrigger>
@@ -439,10 +435,6 @@ export const InnerEvalTemplateForm = (props: {
availableModels={[...evalLLMModels]}
formDisabled={!props.isEditing}
/>
<LLMApiKeyComponent
projectId={props.projectId}
modelParams={modelParams}
/>
</div>
</div>
@@ -465,78 +457,6 @@ export const InnerEvalTemplateForm = (props: {
);
};
export const LLMApiKeyComponent = (p: {
projectId: string;
modelParams: UIModelParams;
}) => {
const hasAccess = useHasAccess({
projectId: p.projectId,
scope: "llmApiKeys:read",
});
if (!hasAccess) {
return (
<div>
<Label>API key</Label>
<p className="text-sm text-muted-foreground">
LLM API Key only visible to Owner and Admin roles.
</p>
</div>
);
}
const apiKeys = api.llmApiKey.all.useQuery({
projectId: p.projectId,
});
if (apiKeys.isLoading) {
return (
<div>
<Label>API key</Label>
<p className="text-sm text-muted-foreground">Loading...</p>
</div>
);
}
const getModelProvider = (model: string) => {
return evalLLMModels.find((m) => m.model.value === model)?.provider.value;
};
const getApiKeyForModel = (model: string) => {
const modelProvider = getModelProvider(model);
return apiKeys.data?.data.find((k) => k.provider === modelProvider);
};
return (
<div>
<Label>API key</Label>
<div>
{getApiKeyForModel(p.modelParams.model.value) ? (
<span className="mr-2 rounded-sm bg-gray-200 p-1 text-xs">
{getApiKeyForModel(p.modelParams.model.value)?.displaySecretKey}
</span>
) : undefined}
</div>
{/* Custom form message to include a link to the already existing prompt */}
{!getApiKeyForModel(p.modelParams.model.value) ? (
<div className="flex flex-col text-sm font-medium text-destructive">
{"No LLM API key found."}
<Link
href={`/project/${p.projectId}/settings`}
className="flex flex-row"
>
Create a new API key here. <ArrowTopRightIcon />
</Link>
</div>
) : undefined}
<p className="text-sm text-muted-foreground">
The API key is used for each evaluation and will incur costs.
</p>
</div>
);
};
function getModelParamsWithEnabledFlag(
evalPreFill?: EvalTemplateFormPreFill,
): UIModelParams {
@@ -67,7 +67,7 @@ export const GenerationOutput = () => {
return (
<div className="relative h-full overflow-auto">
<div
className="h-full overflow-auto rounded-lg bg-gray-100 p-4"
className="h-full overflow-auto rounded-lg bg-muted p-4"
ref={scrollAreaRef}
>
<div className="mb-4 flex w-full items-center">
@@ -0,0 +1,170 @@
import { Terminal } from "lucide-react";
import Link from "next/link";
import { Button } from "@/src/components/ui/button";
import usePlaygroundCache from "@/src/ee/features/playground/page/hooks/usePlaygroundCache";
import { type PlaygroundCache } from "@/src/ee/features/playground/page/types";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL";
import {
ChatMessageRole,
supportedModels as playgroundSupportedModels,
type Observation,
type Prompt,
type UIModelParams,
ZodModelConfig,
} from "@langfuse/shared";
import { useEffect, useState } from "react";
import { PromptType } from "@/src/features/prompts/server/utils/validation";
import { ChatMessageListSchema } from "@/src/features/prompts/components/NewPromptForm/validation";
import { createEmptyMessage } from "@/src/components/ChatMessages/utils/createEmptyMessage";
import { getIsCloudEnvironment } from "@/src/ee/utils/getIsCloudEnvironment";
type JumpToPlaygroundButtonProps = (
| {
source: "prompt";
prompt: Prompt;
analyticsEventName: "prompt_detail:test_in_playground_button_click";
}
| {
source: "generation";
generation: Observation;
analyticsEventName: "trace_detail:test_in_playground_button_click";
}
) & {
fullWidth?: boolean;
};
export const JumpToPlaygroundButton: React.FC<JumpToPlaygroundButtonProps> = (
props,
) => {
const capture = usePostHogClientCapture();
const projectId = useProjectIdFromURL();
const { setPlaygroundCache } = usePlaygroundCache();
const [capturedState, setCapturedState] = useState<PlaygroundCache>(null);
useEffect(() => {
if (props.source === "prompt") {
setCapturedState(parsePrompt(props.prompt));
} else if (props.source === "generation") {
setCapturedState(parseGeneration(props.generation));
}
}, [props]);
const handleClick = () => {
capture(props.analyticsEventName);
setPlaygroundCache(capturedState);
};
if (!getIsCloudEnvironment()) return null;
return (
<Button
variant={props.fullWidth ? "secondary" : "outline"}
title="Test in LLM playground"
size={!props.fullWidth ? "icon" : undefined}
onClick={handleClick}
asChild
>
<Link href={`/project/${projectId}/playground`}>
<Terminal className="h-5 w-5" />
{props.fullWidth ? (
<span className="ml-2">Test in playground</span>
) : null}
</Link>
</Button>
);
};
const parsePrompt = (prompt: Prompt): PlaygroundCache => {
if (prompt.type === PromptType.Chat) {
const parsedMessages = ChatMessageListSchema.safeParse(prompt.prompt);
return parsedMessages.success ? { messages: parsedMessages.data } : null;
} else {
const promptString = prompt.prompt?.valueOf();
return {
messages: [
createEmptyMessage(
ChatMessageRole.System,
typeof promptString === "string" ? promptString : "",
),
],
};
}
};
const parseGeneration = (generation: Observation): PlaygroundCache => {
if (generation.type !== "GENERATION") return null;
const modelParams = parseModelParams(generation);
const input = generation.input?.valueOf();
if (typeof input === "string") {
return {
messages: [createEmptyMessage(ChatMessageRole.System, input)],
modelParams,
};
}
if (typeof input === "object") {
const parsedMessages = ChatMessageListSchema.safeParse(input);
if (parsedMessages.success)
return { messages: parsedMessages.data, modelParams };
}
if (typeof input === "object" && "messages" in input) {
const parsedMessages = ChatMessageListSchema.safeParse(input["messages"]);
if (parsedMessages.success)
return { messages: parsedMessages.data, modelParams };
}
return null;
};
function parseModelParams(
generation: Observation,
):
| (Partial<UIModelParams> & Pick<UIModelParams, "provider" | "model">)
| undefined {
const generationModel = generation.model?.valueOf();
let modelParams:
| (Partial<UIModelParams> & Pick<UIModelParams, "provider" | "model">)
| undefined = undefined;
if (generationModel) {
const provider = Object.entries(playgroundSupportedModels).find(
([_, models]) =>
generationModel ? models.some((m) => m === generationModel) : false,
)?.[0];
if (!provider) return;
modelParams = {
provider: { value: provider, enabled: true },
model: { value: generationModel, enabled: true },
} as Partial<UIModelParams> & Pick<UIModelParams, "provider" | "model">;
const generationModelParams = generation.modelParameters?.valueOf();
if (generationModelParams && typeof generationModelParams === "object") {
const parsedParams = ZodModelConfig.safeParse(generationModelParams);
if (parsedParams.success) {
Object.entries(parsedParams.data).forEach(([key, value]) => {
if (!modelParams) return;
modelParams[key as keyof typeof parsedParams.data] = {
value,
enabled: true,
};
});
}
}
}
return modelParams;
}
@@ -0,0 +1,28 @@
import { ListRestartIcon } from "lucide-react";
import { useRouter } from "next/router";
import { Button } from "@/src/components/ui/button";
import usePlaygroundCache from "@/src/ee/features/playground/page/hooks/usePlaygroundCache";
import { getIsCloudEnvironment } from "@/src/ee/utils/getIsCloudEnvironment";
export const ResetPlaygroundButton: React.FC = () => {
const router = useRouter();
const { setPlaygroundCache } = usePlaygroundCache();
const handleClick = () => {
setPlaygroundCache(null);
router.reload();
};
return getIsCloudEnvironment() ? (
<Button
variant={"outline"}
title="Reset playground state"
onClick={handleClick}
>
<ListRestartIcon className="mr-1 h-5 w-5" />
<span>Reset playground</span>
</Button>
) : null;
};
@@ -0,0 +1,161 @@
import { Check, FileInput } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { Button } from "@/src/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/src/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/src/components/ui/popover";
import { usePlaygroundContext } from "@/src/ee/features/playground/page/context";
import usePlaygroundCache from "@/src/ee/features/playground/page/hooks/usePlaygroundCache";
import { getIsCloudEnvironment } from "@/src/ee/utils/getIsCloudEnvironment";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { PromptType } from "@/src/features/prompts/server/utils/validation";
import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL";
import { api } from "@/src/utils/api";
import { cn } from "@/src/utils/tailwind";
export const SaveToPromptButton: React.FC = () => {
const [open, setOpen] = useState(false);
const [selectedPromptId, setSelectedPromptId] = useState("");
const { modelParams, messages, output, promptVariables } =
usePlaygroundContext();
const capture = usePostHogClientCapture();
const router = useRouter();
const projectId = useProjectIdFromURL();
const { setPlaygroundCache } = usePlaygroundCache();
const allPromptNames =
api.prompts.all
.useQuery(
{
projectId: projectId as string, // Typecast as query is enabled only when projectId is present
filter: [],
orderBy: { column: "name", order: "ASC" },
page: 0,
},
{ enabled: Boolean(projectId) },
)
.data?.prompts.filter((prompt) => prompt.type === PromptType.Chat)
.map((prompt) => ({
label:
prompt.name.slice(0, 20) + (prompt.name.length > 25 ? "..." : ""),
value: prompt.id,
})) ?? [];
const handleNewPrompt = async () => {
capture("playground:save_to_new_prompt_button_click", { projectId });
setPlaygroundCache({
modelParams,
messages,
output,
promptVariables,
});
await router.push(
`/project/${projectId}/prompts/new?loadPlaygroundCache=true`,
);
};
const handleNewPromptVersion = async () => {
capture("playground:save_to_prompt_version_button_click", { projectId });
setPlaygroundCache({
modelParams,
messages,
output,
promptVariables,
});
await router.push(
`/project/${projectId}/prompts/new?promptId=${selectedPromptId}&loadPlaygroundCache=true`,
);
};
if (!getIsCloudEnvironment()) return null;
return (
<Popover>
<PopoverTrigger asChild>
<Button variant={"outline"} title="Save to prompt" asChild>
<Link href={`/project/${projectId}/playground`}>
<FileInput className="mr-1 h-5 w-5" />
<span>Save as prompt</span>
</Link>
</Button>
</PopoverTrigger>
<PopoverContent>
<Button className="mt-2 w-full" onClick={handleNewPrompt}>
Save as new prompt
</Button>
<Divider />
<Command className="min-h-[8rem]">
<CommandInput placeholder="Search chat prompts..." />
<CommandEmpty>No chat prompt found.</CommandEmpty>
<CommandGroup className="mt-2">
<CommandList>
{allPromptNames.map((promptName) => (
<CommandItem
key={promptName.value}
title={promptName.label}
value={promptName.value}
onSelect={(currentValue) => {
setSelectedPromptId(
currentValue === selectedPromptId ? "" : currentValue,
);
setOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedPromptId === promptName.value
? "opacity-100"
: "opacity-0",
)}
/>
{promptName.label}
</CommandItem>
))}
</CommandList>
</CommandGroup>
</Command>
<Button
className="mt-2 w-full"
disabled={!Boolean(selectedPromptId)}
onClick={handleNewPromptVersion}
>
Save as new prompt version
</Button>
</PopoverContent>
</Popover>
);
};
export function Divider() {
return (
<div className="my-6 flex flex-row justify-center align-middle">
<div className="flex flex-1 flex-col">
<div className="flex-1 border-b-2 border-gray-200" />
<div className="flex-1" />
</div>
<p className="mx-2 text-sm text-gray-400">or</p>
<div className="flex flex-1 flex-col">
<div className="flex-1 border-b-2 border-gray-200" />
<div className="flex-1" />
</div>
</div>
);
}
@@ -31,7 +31,7 @@ export const Variables = () => {
key={promptVariable.name}
/>
{index !== promptVariables.length - 1 ? (
<Divider className="my-2 text-gray-400" />
<Divider className="my-2 text-muted-foreground" />
) : null}
</>
);
@@ -7,17 +7,14 @@ import React, {
useState,
} from "react";
import { StringParam, useQueryParam } from "use-query-params";
import { v4 as uuidv4 } from "uuid";
import { createEmptyMessage } from "@/src/components/ChatMessages/utils/createEmptyMessage";
import useCommandEnter from "@/src/ee/features/playground/page/hooks/useCommandEnter";
import usePlaygroundCache from "@/src/ee/features/playground/page/hooks/usePlaygroundCache";
import { getFinalModelParams } from "@/src/ee/utils/getFinalModelParams";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { ChatMessageListSchema } from "@/src/features/prompts/components/NewPromptForm/validation";
import { PromptType } from "@/src/features/prompts/server/utils/validation";
import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL";
import { api } from "@/src/utils/api";
import { extractVariables } from "@/src/utils/string";
import {
ChatMessageRole,
@@ -40,7 +37,6 @@ type PlaygroundContextType = {
handleSubmit: () => Promise<void>;
isStreaming: boolean;
isInitializing: boolean;
} & ModelParamsContext &
MessagesContext;
@@ -61,9 +57,9 @@ export const usePlaygroundContext = () => {
export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
children,
}) => {
const projectId = useProjectIdFromURL();
const capture = usePostHogClientCapture();
const [initialPromptId] = useQueryParam("promptId", StringParam);
const projectId = useProjectIdFromURL();
const { playgroundCache, setPlaygroundCache } = usePlaygroundCache();
const [promptVariables, setPromptVariables] = useState<PromptVariable[]>([]);
const [output, setOutput] = useState("");
const [outputJson, setOutputJson] = useState("");
@@ -76,35 +72,32 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
getDefaultModelParams(ModelProvider.OpenAI),
);
const { data: initialPrompt, isInitialLoading } = api.prompts.byId.useQuery(
{
projectId: projectId as string, // Typecast as query is enabled only when projectId is present
id: initialPromptId ?? "",
},
{ enabled: Boolean(initialPromptId && projectId), staleTime: Infinity }, // do not refetch as this would overwrite the user's input
);
// Load state from cache
useEffect(() => {
if (!initialPrompt) return;
if (initialPrompt.type === PromptType.Chat) {
try {
const initialMessages = ChatMessageListSchema.parse(
initialPrompt.prompt,
);
setMessages(initialMessages.map((m) => ({ ...m, id: uuidv4() })));
} catch (err) {
console.warn("Failed to parse initial chat messages", err);
}
} else {
const promptString = initialPrompt.prompt?.valueOf();
setMessages([
createEmptyMessage(
ChatMessageRole.System,
typeof promptString === "string" ? promptString : "",
),
]);
if (!playgroundCache) return;
const {
messages: cachedMessages,
modelParams: cachedModelParams,
output: cachedOutput,
promptVariables: cachedPromptVariables,
} = playgroundCache;
setMessages(cachedMessages.map((m) => ({ ...m, id: uuidv4() })));
if (cachedOutput) {
setOutput(cachedOutput);
setOutputJson("");
}
}, [initialPrompt]);
if (cachedModelParams) {
setModelParams((prev) => ({ ...prev, ...cachedModelParams }));
}
if (cachedPromptVariables) {
setPromptVariables(cachedPromptVariables);
}
}, [playgroundCache]);
useEffect(() => {
setModelParams(getDefaultModelParams(modelParams.provider.value));
@@ -181,6 +174,7 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
}
const completionStream = getChatCompletionStream(
projectId,
finalMessages,
modelParams,
);
@@ -191,6 +185,12 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
setOutput(response);
}
setOutputJson(getOutputJson(response, finalMessages, modelParams));
setPlaygroundCache({
messages,
modelParams,
output: response,
promptVariables,
});
capture("playground:execute_button_click", {
inputLength: finalMessages.length,
modelName: modelParams.model,
@@ -258,7 +258,6 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
outputJson,
handleSubmit,
isStreaming,
isInitializing: isInitialLoading,
}}
>
{children}
@@ -267,10 +266,17 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
};
async function* getChatCompletionStream(
projectId: string | undefined,
messages: ChatMessageWithId[],
modelParams: UIModelParams,
) {
if (!projectId) {
console.error("Project ID is not set");
return;
}
const body = JSON.stringify({
projectId,
messages,
modelParams: getFinalModelParams(modelParams),
});
@@ -0,0 +1,30 @@
import { useEffect, useState } from "react";
import { getIsCloudEnvironment } from "@/src/ee/utils/getIsCloudEnvironment";
import { type PlaygroundCache } from "../types";
const playgroundCacheKey = "playgroundCache";
export default function usePlaygroundCache() {
const [cache, setCache] = useState<PlaygroundCache>(null);
const setPlaygroundCache = (cache: PlaygroundCache) => {
sessionStorage.setItem(playgroundCacheKey, JSON.stringify(cache));
};
useEffect(() => {
const savedCache = sessionStorage.getItem(playgroundCacheKey);
if (savedCache) {
try {
setCache(JSON.parse(savedCache));
} catch (e) {
console.error("Failed to parse playground cache", e);
}
}
}, []);
return {
playgroundCache: getIsCloudEnvironment() ? cache : null,
setPlaygroundCache: getIsCloudEnvironment() ? setPlaygroundCache : () => {},
};
}
+22 -14
View File
@@ -1,24 +1,32 @@
import Header from "@/src/components/layouts/header";
import Playground from "@/src/ee/features/playground/page/playground";
import { ResetPlaygroundButton } from "@/src/ee/features/playground/page/components/ResetPlaygroundButton";
import { SaveToPromptButton } from "@/src/ee/features/playground/page/components/SaveToPromptButton";
import { PlaygroundProvider } from "@/src/ee/features/playground/page/context";
import Playground from "@/src/ee/features/playground/page/playground";
import { getIsCloudEnvironment } from "@/src/ee/utils/getIsCloudEnvironment";
export default function PlaygroundPage() {
return getIsCloudEnvironment() ? (
<div className="flex h-[95vh] flex-col">
<Header
title="Playground"
help={{
description: "A sandbox to test and iterate your prompts",
href: "https://langfuse.com/docs/playground",
}}
featureBetaURL="https://github.com/orgs/langfuse/discussions/1170"
/>
<div className="flex-1 overflow-auto">
<PlaygroundProvider>
<PlaygroundProvider>
<div className="flex h-[95vh] flex-col">
<Header
title="Playground"
help={{
description: "A sandbox to test and iterate your prompts",
href: "https://langfuse.com/docs/playground",
}}
featureBetaURL="https://github.com/orgs/langfuse/discussions/1170"
actionButtons={
<>
<SaveToPromptButton />
<ResetPlaygroundButton />
</>
}
/>
<div className="flex-1 overflow-auto">
<Playground />
</PlaygroundProvider>
</div>
</div>
</div>
</PlaygroundProvider>
) : null;
}
@@ -6,10 +6,6 @@ import { Messages } from "./components/Messages";
export default function Playground() {
const playgroundContext = usePlaygroundContext();
if (playgroundContext.isInitializing) {
return <div>Loading...</div>;
}
return (
<div className="flex h-full flex-row space-x-8">
<div className="h-full basis-3/4 overflow-auto">
@@ -20,7 +16,7 @@ export default function Playground() {
<div className="basis-[55%] ">
<ModelParameters {...playgroundContext} />
</div>
<div className="basis-[45%] overflow-auto">
<div className="mt-4 basis-[45%] overflow-auto">
<Variables />
</div>
</div>
@@ -0,0 +1,13 @@
import {
type PromptVariable,
type ChatMessage,
type UIModelParams,
} from "@langfuse/shared";
export type PlaygroundCache = {
messages: ChatMessage[];
modelParams?: Partial<UIModelParams> &
Pick<UIModelParams, "provider" | "model">;
output?: string | null;
promptVariables?: PromptVariable[];
} | null;
@@ -0,0 +1,26 @@
import { getServerSession } from "next-auth";
import { getIsCloudEnvironment } from "@/src/ee/utils/getIsCloudEnvironment";
import { getAuthOptions } from "@/src/server/auth";
import { isProjectMemberOrAdmin } from "@/src/server/utils/checkProjectMembershipOrAdmin";
import { ApiError, ForbiddenError, UnauthorizedError } from "@langfuse/shared";
export type AuthorizeRequestResult = {
userId: string;
};
export const authorizeRequestOrThrow = async (
projectId: string,
): Promise<AuthorizeRequestResult> => {
if (!getIsCloudEnvironment())
throw new ApiError("This endpoint is available in Langfuse cloud only.");
const authOptions = await getAuthOptions();
const session = await getServerSession(authOptions);
if (!session?.user) throw new UnauthorizedError("Unauthenticated");
if (!isProjectMemberOrAdmin(session.user, projectId))
throw new ForbiddenError("User is not a member of this project");
return { userId: session.user.id };
};
@@ -1,75 +1,60 @@
import { StreamingTextResponse } from "ai";
import { getToken } from "next-auth/jwt";
import { type NextRequest, NextResponse } from "next/server";
import { NextResponse, type NextRequest } from "next/server";
import { fetchLLMCompletion } from "@langfuse/shared";
import {
BaseError,
ValidationError,
fetchLLMCompletion,
} from "@langfuse/shared";
import { PosthogCallbackHandler } from "./analytics/posthogCallback";
import {
validateChatCompletionBody,
type ValidatedChatCompletionBody,
} from "./validateChatCompletionBody";
import { getCookieName } from "@/src/server/utils/cookies";
import { env } from "@/src/env.mjs";
import { getIsCloudEnvironment } from "@/src/ee/utils/getIsCloudEnvironment";
import { authorizeRequestOrThrow } from "./authorizeRequest";
import { validateChatCompletionBody } from "./validateChatCompletionBody";
import { prisma } from "@langfuse/shared/src/db";
import { decrypt } from "@langfuse/shared/encryption";
export default async function chatCompletionHandler(req: NextRequest) {
if (!getIsCloudEnvironment()) {
return NextResponse.json(
{ message: "This endpoint is available in Langfuse cloud only." },
{ status: 501 },
);
}
const token = await getToken({
req,
cookieName: getCookieName("next-auth.session-token"),
secret: env.NEXTAUTH_SECRET,
});
if (!token || !token.sub)
// sub is the user id
return NextResponse.json({ message: "Unauthenticated" }, { status: 401 });
if (req.method !== "POST")
return NextResponse.json(
{ message: "Method not allowed" },
{ status: 405 },
);
let body: ValidatedChatCompletionBody;
try {
body = validateChatCompletionBody(await req.json());
} catch (err) {
console.error(err);
const body = validateChatCompletionBody(await req.json());
const { userId } = await authorizeRequestOrThrow(body.projectId);
return NextResponse.json(
{
message: "Invalid request body",
error: err,
},
{ status: 400 },
);
}
try {
const { messages, modelParams } = body;
const LLMApiKey = await prisma.llmApiKeys.findFirst({
where: {
projectId: body.projectId,
provider: modelParams.provider,
},
});
if (!LLMApiKey)
throw new ValidationError(
`No ${modelParams.provider} API key found in project. Please add one in the project settings.`,
);
const stream = await fetchLLMCompletion({
messages,
modelParams,
streaming: true,
callbacks: [new PosthogCallbackHandler("playground", body, token.sub)],
apiKey:
modelParams.provider === "openai"
? env.OPENAI_API_KEY
: env.ANTHROPIC_API_KEY,
callbacks: [new PosthogCallbackHandler("playground", body, userId)],
apiKey: decrypt(LLMApiKey.secretKey),
});
return new StreamingTextResponse(stream);
} catch (err) {
console.error(err);
if (err instanceof BaseError) {
return NextResponse.json(
{
error: err.name,
message: err.message,
},
{ status: err.httpCode },
);
}
if (err instanceof Error) {
return NextResponse.json(
{
@@ -32,6 +32,7 @@ const MessageSchema = z.object({
id: z.string().optional(),
});
export const ChatCompletionBodySchema = z.object({
projectId: z.string(),
messages: z.array(MessageSchema),
modelParams: ModelParamsSchema,
});
+2 -2
View File
@@ -1,4 +1,4 @@
import { env } from "@/src/env.mjs";
export const getIsCloudEnvironment = () =>
Boolean(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION)
export const getIsCloudEnvironment = () =>
Boolean(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION);
+8
View File
@@ -67,6 +67,10 @@ export const env = createEnv({
AUTH_AUTH0_CLIENT_SECRET: z.string().optional(),
AUTH_AUTH0_ISSUER: z.string().url().optional(),
AUTH_AUTH0_ALLOW_ACCOUNT_LINKING: z.enum(["true", "false"]).optional(),
AUTH_COGNITO_CLIENT_ID: z.string().optional(),
AUTH_COGNITO_CLIENT_SECRET: z.string().optional(),
AUTH_COGNITO_ISSUER: z.string().url().optional(),
AUTH_COGNITO_ALLOW_ACCOUNT_LINKING: z.enum(["true", "false"]).optional(),
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT: z.string().optional(),
AUTH_DISABLE_USERNAME_PASSWORD: z.enum(["true", "false"]).optional(),
AUTH_DISABLE_SIGNUP: z.enum(["true", "false"]).optional(),
@@ -167,6 +171,10 @@ export const env = createEnv({
AUTH_AUTH0_ISSUER: process.env.AUTH_AUTH0_ISSUER,
AUTH_AUTH0_ALLOW_ACCOUNT_LINKING:
process.env.AUTH_AUTH0_ALLOW_ACCOUNT_LINKING,
AUTH_COGNITO_CLIENT_ID: process.env.AUTH_COGNITO_CLIENT_ID,
AUTH_COGNITO_CLIENT_SECRET: process.env.AUTH_COGNITO_CLIENT_SECRET,
AUTH_COGNITO_ISSUER: process.env.AUTH_COGNITO_ISSUER,
AUTH_COGNITO_ALLOW_ACCOUNT_LINKING: process.env.AUTH_COGNITO_ALLOW_ACCOUNT_LINKING,
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT:
process.env.AUTH_DOMAINS_WITH_SSO_ENFORCEMENT,
AUTH_DISABLE_USERNAME_PASSWORD: process.env.AUTH_DISABLE_USERNAME_PASSWORD,
@@ -2,7 +2,7 @@ import { env } from "@/src/env.mjs";
export const CloudPrivacyNotice = ({ action }: { action: string }) =>
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ? (
<div className="mx-auto mt-10 max-w-lg text-center text-xs text-gray-500">
<div className="mx-auto mt-10 max-w-lg text-center text-xs text-muted-foreground">
By {action} you are agreeing to our{" "}
<a
href="https://langfuse.com/terms"
@@ -67,7 +67,7 @@ export function CloudRegionSwitch({
<DataRegionInfo />
</span>
{isSignUpPage && env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "US" ? (
<p className="text-xs text-gray-500">
<p className="text-xs text-muted-foreground">
Demo project is only available in the EU region.
</p>
) : null}
@@ -113,7 +113,7 @@ const DataRegionInfo = () => (
<DialogTrigger asChild>
<a
href="#"
className="ml-1 text-xs text-indigo-600 hover:text-indigo-500"
className="hover:text-hover-primary-accent ml-1 text-xs text-primary-accent"
title="What is this?"
>
(what is this?)
@@ -144,7 +144,7 @@ const DataRegionInfo = () => (
href="https://langfuse.com/docs/data-security-privacy"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 underline"
className="text-primary-accent underline"
>
langfuse.com/security
</a>
@@ -1,4 +1,5 @@
import { type DateTimeAggregationOption } from "@/src/features/dashboard/lib/timeseries-aggregation";
import { getColorsForCategories } from "@/src/features/dashboard/utils/getColorsForCategories";
import { compactNumberFormatter } from "@/src/utils/numbers";
import { cn } from "@/src/utils/tailwind";
import { AreaChart, LineChart } from "@tremor/react";
@@ -57,6 +58,8 @@ export function BaseTimeSeriesChart(props: {
};
const ChartComponent = props.chartType === "area" ? AreaChart : LineChart;
const colors = getColorsForCategories(Array.from(labels));
return (
<ChartComponent
className={cn("mt-4", props.className)}
@@ -64,10 +67,8 @@ export function BaseTimeSeriesChart(props: {
index="timestamp"
categories={Array.from(labels)}
connectNulls={props.connectNulls}
colors={["indigo", "cyan", "zinc", "purple"]}
valueFormatter={
props.valueFormatter ? props.valueFormatter : compactNumberFormatter
}
colors={colors}
valueFormatter={props.valueFormatter ?? compactNumberFormatter}
noDataText="No data"
showLegend={props.showLegend}
showAnimation={true}
@@ -17,7 +17,7 @@ export const NoData = ({
justifyContent="center"
className={cn(
className,
"min-h-[9rem] w-full flex-1 rounded-tremor-default border border-dashed border-tremor-border",
"min-h-[9rem] w-full flex-1 rounded-tremor-default border border-dashed",
)}
>
<Text className="text-tremor-content">{noDataText}</Text>
@@ -21,7 +21,7 @@ export const TabComponent = ({ tabs }: TabComponentProps) => {
<select
id="tabs"
name="tabs"
className="block w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
className="focus:border-primary-accent focus:ring-primary-accent block w-full rounded-md border-border py-2 pl-3 pr-10 text-base focus:outline-none sm:text-sm"
defaultValue={0}
onChange={(e) => setSelectedIndex(Number(e.target.selectedIndex))}
>
@@ -31,7 +31,7 @@ export const TabComponent = ({ tabs }: TabComponentProps) => {
</select>
</div>
<div className="hidden sm:block">
<div className="border-b border-gray-200">
<div className="border-b border-border">
<nav
className="-mb-px flex space-x-2 md:space-x-4 lg:space-x-6 xl:space-x-8"
aria-label="Tabs"
@@ -41,8 +41,8 @@ export const TabComponent = ({ tabs }: TabComponentProps) => {
key={tab.tabTitle}
className={cn(
index === selectedIndex
? "border-indigo-500 text-indigo-600"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
? "border-primary-accent text-primary-accent"
: "border-transparent text-muted-foreground hover:border-border hover:text-primary",
"cursor-pointer whitespace-nowrap border-b-2 px-1 py-3 text-sm font-medium",
)}
aria-current={index === selectedIndex ? "page" : undefined}
@@ -32,14 +32,14 @@ export const DashboardTable = ({
<div className="mt-4">
<div className="overflow-x-auto">
<div className="inline-block min-w-full align-middle">
<table className="min-w-full divide-y divide-gray-300 animate-in animate-out">
<table className="min-w-full divide-y divide-border animate-in animate-out">
<thead>
<tr>
{headers.map((header, i) => (
<th
key={i}
scope="col"
className="whitespace-nowrap py-3.5 pl-4 pr-3 text-left text-xs font-semibold text-gray-900 sm:pl-0"
className="whitespace-nowrap py-3.5 pl-4 pr-3 text-left text-xs font-semibold text-primary sm:pl-0"
>
{header}
</th>
@@ -47,7 +47,7 @@ export const DashboardTable = ({
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
<tbody className="divide-y divide-accent bg-background">
{rows
.slice(
0,
@@ -62,7 +62,7 @@ export const DashboardTable = ({
{row.map((cell, j) => (
<td
key={j}
className="whitespace-nowrap py-2 pl-3 pr-2 text-xs text-gray-500 sm:pl-0"
className="whitespace-nowrap py-2 pl-3 pr-2 text-xs text-muted-foreground sm:pl-0"
>
{cell}
</td>
@@ -0,0 +1,63 @@
type Color =
| "indigo"
| "cyan"
| "zinc"
| "purple"
| "slate"
| "gray"
| "neutral"
| "stone"
| "red"
| "orange"
| "amber"
| "yellow"
| "lime"
| "green"
| "emerald"
| "teal"
| "sky"
| "blue"
| "violet"
| "fuchsia"
| "pink"
| "rose";
const predefinedColors: Color[] = [
"indigo",
"cyan",
"zinc",
"purple",
"yellow",
"red",
"lime",
"pink",
"emerald",
"teal",
"fuchsia",
"sky",
"blue",
"orange",
"violet",
"rose",
"green",
"amber",
"slate",
"gray",
"neutral",
"stone",
];
function getRandomColor() {
return predefinedColors[Math.floor(Math.random() * predefinedColors.length)];
}
export function getColorsForCategories(categories: string[]): Color[] {
if (categories.length <= predefinedColors.length) {
return predefinedColors.slice(0, categories.length);
}
const colors: Color[] = predefinedColors;
while (colors.length < categories.length) {
colors.push(getRandomColor());
}
return colors;
}
@@ -72,7 +72,7 @@ export const DatasetActionButton = (props: DatasetActionButtonProps) => {
</Button>
) : (
<div
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
onClick={() => {
setOpen(true);
capture("datasets:update_form_open", {
@@ -90,7 +90,7 @@ export const DatasetActionButton = (props: DatasetActionButtonProps) => {
)
) : props.mode === "delete" ? (
<div
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
onClick={() => {
setOpen(true);
capture("datasets:delete_form_open", {
@@ -134,8 +134,8 @@ export function DatasetItemsTable({
className={cn(
"h-2 w-2 rounded-full",
status === DatasetStatus.ACTIVE
? "bg-green-600"
: "bg-yellow-600",
? "bg-dark-green"
: "bg-dark-yellow",
)}
/>
<span>{status}</span>
@@ -173,7 +173,7 @@ export function DatasetItemsTable({
return !!expectedOutput ? (
<IOTableCell
data={expectedOutput}
className="bg-green-50"
className="bg-accent-light-green"
singleLine={rowHeight === "s"}
/>
) : null;
@@ -316,7 +316,7 @@ const TraceObservationIOCell = ({
<IOTableCell
isLoading={!!!observationId ? trace.isLoading : observation.isLoading}
data={io === "output" ? data?.output : data?.input}
className={cn(io === "output" && "bg-green-50")}
className={cn(io === "output" && "bg-accent-light-green")}
singleLine={singleLine}
/>
);
@@ -11,7 +11,7 @@ const langfuseUrls = {
STAGING: "https://staging.langfuse.com",
};
const authUrl =
const getAuthURL = () =>
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "US" ||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU" ||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING"
@@ -40,7 +40,7 @@ export const sendProjectInvitation = async (
invitedByUserEmail: inviterEmail,
projectName: projectName,
recieverEmail: to,
inviteLink: authUrl,
inviteLink: getAuthURL(),
langfuseCloudRegion: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION,
}),
);

Some files were not shown because too many files have changed in this diff Show More