User accounts
Allows for the creation of user accounts. A few notes on the specifics: - Experiments are the main access control objects. If you can view an experiment, you can view all its prompts/scenarios/evals. If you can edit it, you can edit or delete all of those as well. - Experiments are owned by Organizations in the database. Organizations can have multiple members and members can have roles of ADMIN, MEMBER or VIEWER. - Organizations can either be "personal" or general. Each user has a "personal" organization created as soon as they try to create an experiment. There's currently no UI support for creating general orgs or adding users to them; they're just in the database to future-proof all the ACL logic. - You can require that a user is signed-in to see a route using the `protectedProcedure` helper. When you use `protectedProcedure`, you also have to call `ctx.markAccessControlRun()` (or delegate to a function that does it for you; see accessControl.ts). This is to remind us to actually check for access control when we define a new endpoint.
This commit is contained in:
@@ -18,3 +18,11 @@ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/openpipe?schema=publ
|
|||||||
OPENAI_API_KEY=""
|
OPENAI_API_KEY=""
|
||||||
|
|
||||||
NEXT_PUBLIC_SOCKET_URL="http://localhost:3318"
|
NEXT_PUBLIC_SOCKET_URL="http://localhost:3318"
|
||||||
|
|
||||||
|
# Next Auth
|
||||||
|
NEXTAUTH_SECRET="your_secret"
|
||||||
|
NEXTAUTH_URL="http://localhost:3000"
|
||||||
|
|
||||||
|
# Next Auth Github Provider
|
||||||
|
GITHUB_CLIENT_ID="your_client_id"
|
||||||
|
GITHUB_CLIENT_SECRET="your_secret"
|
||||||
|
|||||||
1
@types/nextjs-routes.d.ts
vendored
1
@types/nextjs-routes.d.ts
vendored
@@ -11,6 +11,7 @@ declare module "nextjs-routes" {
|
|||||||
} from "next";
|
} from "next";
|
||||||
|
|
||||||
export type Route =
|
export type Route =
|
||||||
|
| StaticRoute<"/account/signin">
|
||||||
| DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }>
|
| DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }>
|
||||||
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
|
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
|
||||||
| DynamicRoute<"/experiments/[id]", { "id": string }>
|
| DynamicRoute<"/experiments/[id]", { "id": string }>
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -4,11 +4,18 @@
|
|||||||
|
|
||||||
OpenPipe is a flexible playground for comparing and optimizing LLM prompts. It lets you quickly generate, test and compare candidate prompts with realistic sample data.
|
OpenPipe is a flexible playground for comparing and optimizing LLM prompts. It lets you quickly generate, test and compare candidate prompts with realistic sample data.
|
||||||
|
|
||||||
**Live Demo:** https://openpipe.ai
|
## Sample Experiments
|
||||||
|
|
||||||
|
These are simple experiments users have created that show how OpenPipe works.
|
||||||
|
|
||||||
|
- [Country Capitals](https://openpipe.ai/experiments/11111111-1111-1111-1111-111111111111)
|
||||||
|
- [Reddit User Needs](https://openpipe.ai/experiments/22222222-2222-2222-2222-222222222222)
|
||||||
|
- [OpenAI Function Calls](https://openpipe.ai/experiments/2ebbdcb3-ed51-456e-87dc-91f72eaf3e2b)
|
||||||
|
- [Activity Classification](https://openpipe.ai/experiments/3950940f-ab6b-4b74-841d-7e9dbc4e4ff8)
|
||||||
|
|
||||||
<img src="https://github.com/openpipe/openpipe/assets/176426/fc7624c6-5b65-4d4d-82b7-4a816f3e5678" alt="demo" height="400px">
|
<img src="https://github.com/openpipe/openpipe/assets/176426/fc7624c6-5b65-4d4d-82b7-4a816f3e5678" alt="demo" height="400px">
|
||||||
|
|
||||||
Currently there's a public playground available at [https://openpipe.ai/](https://openpipe.ai/), but the recommended approach is to [run locally](#running-locally).
|
You can use our hosted version of OpenPipe at [https://openpipe.ai]. You can also clone this repository and [run it locally](#running-locally).
|
||||||
|
|
||||||
## High-Level Features
|
## High-Level Features
|
||||||
|
|
||||||
@@ -47,5 +54,6 @@ OpenPipe currently supports GPT-3.5 and GPT-4. Wider model support is planned.
|
|||||||
5. Install the dependencies: `cd openpipe && pnpm install`
|
5. Install the dependencies: `cd openpipe && pnpm install`
|
||||||
6. Create a `.env` file (`cp .env.example .env`) and enter your `OPENAI_API_KEY`.
|
6. Create a `.env` file (`cp .env.example .env`) and enter your `OPENAI_API_KEY`.
|
||||||
7. Update `DATABASE_URL` if necessary to point to your Postgres instance and run `pnpm prisma db push` to create the database.
|
7. Update `DATABASE_URL` if necessary to point to your Postgres instance and run `pnpm prisma db push` to create the database.
|
||||||
8. Start the app: `pnpm dev`.
|
8. Create a [GitHub OAuth App](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) and update the `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` values. (Note: a PR to make auth optional when running locally would be a great contribution!)
|
||||||
9. Navigate to [http://localhost:3000](http://localhost:3000)
|
9. Start the app: `pnpm dev`.
|
||||||
|
10. Navigate to [http://localhost:3000](http://localhost:3000)
|
||||||
|
|||||||
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@@ -1,4 +1,4 @@
|
|||||||
lockfileVersion: '6.0'
|
lockfileVersion: '6.1'
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
DROP TABLE "Account";
|
||||||
|
DROP TABLE "Session";
|
||||||
|
DROP TABLE "User";
|
||||||
|
DROP TABLE "VerificationToken";
|
||||||
|
|
||||||
|
CREATE TYPE "OrganizationUserRole" AS ENUM ('ADMIN', 'MEMBER', 'VIEWER');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Organization" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"personalOrgUserId" UUID,
|
||||||
|
|
||||||
|
CONSTRAINT "Organization_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "OrganizationUser" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"role" "OrganizationUserRole" NOT NULL,
|
||||||
|
"organizationId" UUID NOT NULL,
|
||||||
|
"userId" UUID NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "OrganizationUser_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Account" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"userId" UUID NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"provider" TEXT NOT NULL,
|
||||||
|
"providerAccountId" TEXT NOT NULL,
|
||||||
|
"refresh_token" TEXT,
|
||||||
|
"refresh_token_expires_in" INTEGER,
|
||||||
|
"access_token" TEXT,
|
||||||
|
"expires_at" INTEGER,
|
||||||
|
"token_type" TEXT,
|
||||||
|
"scope" TEXT,
|
||||||
|
"id_token" TEXT,
|
||||||
|
"session_state" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Session" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"sessionToken" TEXT NOT NULL,
|
||||||
|
"userId" UUID NOT NULL,
|
||||||
|
"expires" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"name" TEXT,
|
||||||
|
"email" TEXT,
|
||||||
|
"emailVerified" TIMESTAMP(3),
|
||||||
|
"image" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "VerificationToken" (
|
||||||
|
"identifier" TEXT NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"expires" TIMESTAMP(3) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO "Organization" ("id", "updatedAt") VALUES ('11111111-1111-1111-1111-111111111111', CURRENT_TIMESTAMP);
|
||||||
|
|
||||||
|
-- AlterTable add organizationId as NULLABLE
|
||||||
|
ALTER TABLE "Experiment" ADD COLUMN "organizationId" UUID;
|
||||||
|
|
||||||
|
-- Set default organization for existing experiments
|
||||||
|
UPDATE "Experiment" SET "organizationId" = '11111111-1111-1111-1111-111111111111';
|
||||||
|
|
||||||
|
-- AlterTable set organizationId as NOT NULL
|
||||||
|
ALTER TABLE "Experiment" ALTER COLUMN "organizationId" SET NOT NULL;
|
||||||
|
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "OrganizationUser_organizationId_userId_key" ON "OrganizationUser"("organizationId", "userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Experiment" ADD CONSTRAINT "Experiment_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "OrganizationUser" ADD CONSTRAINT "OrganizationUser_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "OrganizationUser" ADD CONSTRAINT "OrganizationUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "Organization_personalOrgUserId_key" ON "Organization"("personalOrgUserId");
|
||||||
|
|
||||||
|
ALTER TABLE "Organization" ADD CONSTRAINT "Organization_personalOrgUserId_fkey" FOREIGN KEY ("personalOrgUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -16,8 +16,12 @@ model Experiment {
|
|||||||
|
|
||||||
sortIndex Int @default(0)
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
|
organizationId String @db.Uuid
|
||||||
|
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
TemplateVariable TemplateVariable[]
|
TemplateVariable TemplateVariable[]
|
||||||
PromptVariant PromptVariant[]
|
PromptVariant PromptVariant[]
|
||||||
TestScenario TestScenario[]
|
TestScenario TestScenario[]
|
||||||
@@ -169,19 +173,53 @@ model OutputEvaluation {
|
|||||||
@@unique([modelOutputId, evaluationId])
|
@@unique([modelOutputId, evaluationId])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Necessary for Next auth
|
model Organization {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
personalOrgUserId String? @unique @db.Uuid
|
||||||
|
PersonalOrgUser User? @relation(fields: [personalOrgUserId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
OrganizationUser OrganizationUser[]
|
||||||
|
Experiment Experiment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
enum OrganizationUserRole {
|
||||||
|
ADMIN
|
||||||
|
MEMBER
|
||||||
|
VIEWER
|
||||||
|
}
|
||||||
|
|
||||||
|
model OrganizationUser {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
|
role OrganizationUserRole
|
||||||
|
|
||||||
|
organizationId String @db.Uuid
|
||||||
|
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
userId String @db.Uuid
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@unique([organizationId, userId])
|
||||||
|
}
|
||||||
|
|
||||||
model Account {
|
model Account {
|
||||||
id String @id @default(cuid())
|
id String @id @default(uuid()) @db.Uuid
|
||||||
userId String
|
userId String @db.Uuid
|
||||||
type String
|
type String
|
||||||
provider String
|
provider String
|
||||||
providerAccountId String
|
providerAccountId String
|
||||||
refresh_token String? // @db.Text
|
refresh_token String? @db.Text
|
||||||
access_token String? // @db.Text
|
refresh_token_expires_in Int?
|
||||||
|
access_token String? @db.Text
|
||||||
expires_at Int?
|
expires_at Int?
|
||||||
token_type String?
|
token_type String?
|
||||||
scope String?
|
scope String?
|
||||||
id_token String? // @db.Text
|
id_token String? @db.Text
|
||||||
session_state String?
|
session_state String?
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@ -189,21 +227,23 @@ model Account {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
id String @id @default(cuid())
|
id String @id @default(uuid()) @db.Uuid
|
||||||
sessionToken String @unique
|
sessionToken String @unique
|
||||||
userId String
|
userId String @db.Uuid
|
||||||
expires DateTime
|
expires DateTime
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(uuid()) @db.Uuid
|
||||||
name String?
|
name String?
|
||||||
email String? @unique
|
email String? @unique
|
||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
image String?
|
image String?
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
|
OrganizationUser OrganizationUser[]
|
||||||
|
Organization Organization[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model VerificationToken {
|
model VerificationToken {
|
||||||
|
|||||||
@@ -2,40 +2,47 @@ import { prisma } from "~/server/db";
|
|||||||
import dedent from "dedent";
|
import dedent from "dedent";
|
||||||
import { generateNewCell } from "~/server/utils/generateNewCell";
|
import { generateNewCell } from "~/server/utils/generateNewCell";
|
||||||
|
|
||||||
const experimentId = "11111111-1111-1111-1111-111111111111";
|
const defaultId = "11111111-1111-1111-1111-111111111111";
|
||||||
|
|
||||||
|
await prisma.organization.deleteMany({
|
||||||
|
where: { id: defaultId },
|
||||||
|
});
|
||||||
|
await prisma.organization.create({
|
||||||
|
data: { id: defaultId },
|
||||||
|
});
|
||||||
|
|
||||||
// Delete the existing experiment
|
|
||||||
await prisma.experiment.deleteMany({
|
await prisma.experiment.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: experimentId,
|
id: defaultId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.experiment.create({
|
await prisma.experiment.create({
|
||||||
data: {
|
data: {
|
||||||
id: experimentId,
|
id: defaultId,
|
||||||
label: "Country Capitals Example",
|
label: "Country Capitals Example",
|
||||||
|
organizationId: defaultId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.scenarioVariantCell.deleteMany({
|
await prisma.scenarioVariantCell.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
promptVariant: {
|
promptVariant: {
|
||||||
experimentId,
|
experimentId: defaultId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.promptVariant.deleteMany({
|
await prisma.promptVariant.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
experimentId,
|
experimentId: defaultId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.promptVariant.createMany({
|
await prisma.promptVariant.createMany({
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
experimentId,
|
experimentId: defaultId,
|
||||||
label: "Prompt Variant 1",
|
label: "Prompt Variant 1",
|
||||||
sortIndex: 0,
|
sortIndex: 0,
|
||||||
model: "gpt-3.5-turbo-0613",
|
model: "gpt-3.5-turbo-0613",
|
||||||
@@ -52,7 +59,7 @@ await prisma.promptVariant.createMany({
|
|||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
experimentId,
|
experimentId: defaultId,
|
||||||
label: "Prompt Variant 2",
|
label: "Prompt Variant 2",
|
||||||
sortIndex: 1,
|
sortIndex: 1,
|
||||||
model: "gpt-3.5-turbo-0613",
|
model: "gpt-3.5-turbo-0613",
|
||||||
@@ -73,14 +80,14 @@ await prisma.promptVariant.createMany({
|
|||||||
|
|
||||||
await prisma.templateVariable.deleteMany({
|
await prisma.templateVariable.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
experimentId,
|
experimentId: defaultId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.templateVariable.createMany({
|
await prisma.templateVariable.createMany({
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
experimentId,
|
experimentId: defaultId,
|
||||||
label: "country",
|
label: "country",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -88,28 +95,28 @@ await prisma.templateVariable.createMany({
|
|||||||
|
|
||||||
await prisma.testScenario.deleteMany({
|
await prisma.testScenario.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
experimentId,
|
experimentId: defaultId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.testScenario.createMany({
|
await prisma.testScenario.createMany({
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
experimentId,
|
experimentId: defaultId,
|
||||||
sortIndex: 0,
|
sortIndex: 0,
|
||||||
variableValues: {
|
variableValues: {
|
||||||
country: "Spain",
|
country: "Spain",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
experimentId,
|
experimentId: defaultId,
|
||||||
sortIndex: 1,
|
sortIndex: 1,
|
||||||
variableValues: {
|
variableValues: {
|
||||||
country: "USA",
|
country: "USA",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
experimentId,
|
experimentId: defaultId,
|
||||||
sortIndex: 2,
|
sortIndex: 2,
|
||||||
variableValues: {
|
variableValues: {
|
||||||
country: "Chile",
|
country: "Chile",
|
||||||
@@ -120,13 +127,13 @@ await prisma.testScenario.createMany({
|
|||||||
|
|
||||||
const variants = await prisma.promptVariant.findMany({
|
const variants = await prisma.promptVariant.findMany({
|
||||||
where: {
|
where: {
|
||||||
experimentId,
|
experimentId: defaultId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const scenarios = await prisma.testScenario.findMany({
|
const scenarios = await prisma.testScenario.findMany({
|
||||||
where: {
|
where: {
|
||||||
experimentId,
|
experimentId: defaultId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ services:
|
|||||||
dockerContext: .
|
dockerContext: .
|
||||||
plan: standard
|
plan: standard
|
||||||
domains:
|
domains:
|
||||||
- openpipe.ai
|
- app.openpipe.ai
|
||||||
envVars:
|
envVars:
|
||||||
- key: NODE_ENV
|
- key: NODE_ENV
|
||||||
value: production
|
value: production
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button, type ButtonProps, HStack, Spinner, Icon } from "@chakra-ui/react";
|
import { Button, type ButtonProps, HStack, Spinner, Icon } from "@chakra-ui/react";
|
||||||
import { BsPlus } from "react-icons/bs";
|
import { BsPlus } from "react-icons/bs";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useExperiment, useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
|
||||||
// Extracted Button styling into reusable component
|
// Extracted Button styling into reusable component
|
||||||
const StyledButton = ({ children, onClick }: ButtonProps) => (
|
const StyledButton = ({ children, onClick }: ButtonProps) => (
|
||||||
@@ -17,6 +17,8 @@ const StyledButton = ({ children, onClick }: ButtonProps) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export default function NewScenarioButton() {
|
export default function NewScenarioButton() {
|
||||||
|
const { canModify } = useExperimentAccess();
|
||||||
|
|
||||||
const experiment = useExperiment();
|
const experiment = useExperiment();
|
||||||
const mutation = api.scenarios.create.useMutation();
|
const mutation = api.scenarios.create.useMutation();
|
||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
@@ -38,6 +40,8 @@ export default function NewScenarioButton() {
|
|||||||
await utils.scenarios.list.invalidate();
|
await utils.scenarios.list.invalidate();
|
||||||
}, [mutation]);
|
}, [mutation]);
|
||||||
|
|
||||||
|
if (!canModify) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
<StyledButton onClick={onClick}>
|
<StyledButton onClick={onClick}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Button, Icon, Spinner } from "@chakra-ui/react";
|
import { Box, Button, Icon, Spinner } from "@chakra-ui/react";
|
||||||
import { BsPlus } from "react-icons/bs";
|
import { BsPlus } from "react-icons/bs";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useExperiment, useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
import { cellPadding, headerMinHeight } from "../constants";
|
import { cellPadding, headerMinHeight } from "../constants";
|
||||||
|
|
||||||
export default function NewVariantButton() {
|
export default function NewVariantButton() {
|
||||||
@@ -17,6 +17,9 @@ export default function NewVariantButton() {
|
|||||||
await utils.promptVariants.list.invalidate();
|
await utils.promptVariants.list.invalidate();
|
||||||
}, [mutation]);
|
}, [mutation]);
|
||||||
|
|
||||||
|
const { canModify } = useExperimentAccess();
|
||||||
|
if (!canModify) return <Box w={cellPadding.x} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
w="100%"
|
w="100%"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Button, HStack, Icon } from "@chakra-ui/react";
|
import { Button, HStack, Icon, Tooltip } from "@chakra-ui/react";
|
||||||
import { BsArrowClockwise } from "react-icons/bs";
|
import { BsArrowClockwise } from "react-icons/bs";
|
||||||
|
import { useExperimentAccess } from "~/utils/hooks";
|
||||||
|
|
||||||
export const CellOptions = ({
|
export const CellOptions = ({
|
||||||
refetchingOutput,
|
refetchingOutput,
|
||||||
@@ -8,9 +9,11 @@ export const CellOptions = ({
|
|||||||
refetchingOutput: boolean;
|
refetchingOutput: boolean;
|
||||||
refetchOutput: () => void;
|
refetchOutput: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { canModify } = useExperimentAccess();
|
||||||
return (
|
return (
|
||||||
<HStack justifyContent="flex-end" w="full">
|
<HStack justifyContent="flex-end" w="full">
|
||||||
{!refetchingOutput && (
|
{!refetchingOutput && canModify && (
|
||||||
|
<Tooltip label="Refetch output" aria-label="refetch output">
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
w={4}
|
w={4}
|
||||||
@@ -27,6 +30,7 @@ export const CellOptions = ({
|
|||||||
>
|
>
|
||||||
<Icon as={BsArrowClockwise} boxSize={4} />
|
<Icon as={BsArrowClockwise} boxSize={4} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { type DragEvent } from "react";
|
|||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { isEqual } from "lodash-es";
|
import { isEqual } from "lodash-es";
|
||||||
import { type Scenario } from "./types";
|
import { type Scenario } from "./types";
|
||||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useExperiment, useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { Box, Button, Flex, HStack, Icon, Spinner, Stack, Tooltip, VStack } from "@chakra-ui/react";
|
import { Box, Button, Flex, HStack, Icon, Spinner, Stack, Tooltip, VStack } from "@chakra-ui/react";
|
||||||
@@ -19,6 +19,8 @@ export default function ScenarioEditor({
|
|||||||
hovered: boolean;
|
hovered: boolean;
|
||||||
canHide: boolean;
|
canHide: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const { canModify } = useExperimentAccess();
|
||||||
|
|
||||||
const savedValues = scenario.variableValues as Record<string, string>;
|
const savedValues = scenario.variableValues as Record<string, string>;
|
||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
const [isDragTarget, setIsDragTarget] = useState(false);
|
const [isDragTarget, setIsDragTarget] = useState(false);
|
||||||
@@ -74,6 +76,7 @@ export default function ScenarioEditor({
|
|||||||
alignItems="flex-start"
|
alignItems="flex-start"
|
||||||
pr={cellPadding.x}
|
pr={cellPadding.x}
|
||||||
py={cellPadding.y}
|
py={cellPadding.y}
|
||||||
|
pl={canModify ? 0 : cellPadding.x}
|
||||||
height="100%"
|
height="100%"
|
||||||
draggable={!variableInputHovered}
|
draggable={!variableInputHovered}
|
||||||
onDragStart={(e) => {
|
onDragStart={(e) => {
|
||||||
@@ -93,6 +96,7 @@ export default function ScenarioEditor({
|
|||||||
onDrop={onReorder}
|
onDrop={onReorder}
|
||||||
backgroundColor={isDragTarget ? "gray.100" : "transparent"}
|
backgroundColor={isDragTarget ? "gray.100" : "transparent"}
|
||||||
>
|
>
|
||||||
|
{canModify && (
|
||||||
<Stack alignSelf="flex-start" opacity={props.hovered ? 1 : 0} spacing={0}>
|
<Stack alignSelf="flex-start" opacity={props.hovered ? 1 : 0} spacing={0}>
|
||||||
{props.canHide && (
|
{props.canHide && (
|
||||||
<>
|
<>
|
||||||
@@ -122,6 +126,8 @@ export default function ScenarioEditor({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
{variableLabels.length === 0 ? (
|
{variableLabels.length === 0 ? (
|
||||||
<Box color="gray.500">{vars.data ? "No scenario variables configured" : "Loading..."}</Box>
|
<Box color="gray.500">{vars.data ? "No scenario variables configured" : "Loading..."}</Box>
|
||||||
) : (
|
) : (
|
||||||
@@ -155,6 +161,8 @@ export default function ScenarioEditor({
|
|||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
lineHeight={1.2}
|
lineHeight={1.2}
|
||||||
value={value}
|
value={value}
|
||||||
|
isDisabled={!canModify}
|
||||||
|
_disabled={{ opacity: 1, cursor: "default" }}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setValues((prev) => ({ ...prev, [key]: e.target.value }));
|
setValues((prev) => ({ ...prev, [key]: e.target.value }));
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Button, GridItem, HStack, Heading } from "@chakra-ui/react";
|
import { Button, GridItem, HStack, Heading } from "@chakra-ui/react";
|
||||||
import { cellPadding } from "../constants";
|
import { cellPadding } from "../constants";
|
||||||
import { useElementDimensions } from "~/utils/hooks";
|
import { useElementDimensions, useExperimentAccess } from "~/utils/hooks";
|
||||||
import { stickyHeaderStyle } from "./styles";
|
import { stickyHeaderStyle } from "./styles";
|
||||||
import { BsPencil } from "react-icons/bs";
|
import { BsPencil } from "react-icons/bs";
|
||||||
import { useAppStore } from "~/state/store";
|
import { useAppStore } from "~/state/store";
|
||||||
@@ -13,6 +13,7 @@ export const ScenariosHeader = ({
|
|||||||
numScenarios: number;
|
numScenarios: number;
|
||||||
}) => {
|
}) => {
|
||||||
const openDrawer = useAppStore((s) => s.openDrawer);
|
const openDrawer = useAppStore((s) => s.openDrawer);
|
||||||
|
const { canModify } = useExperimentAccess();
|
||||||
|
|
||||||
const [ref, dimensions] = useElementDimensions();
|
const [ref, dimensions] = useElementDimensions();
|
||||||
const topValue = dimensions ? `-${dimensions.height - 24}px` : "-455px";
|
const topValue = dimensions ? `-${dimensions.height - 24}px` : "-455px";
|
||||||
@@ -33,6 +34,7 @@ export const ScenariosHeader = ({
|
|||||||
<Heading size="xs" fontWeight="bold" flex={1}>
|
<Heading size="xs" fontWeight="bold" flex={1}>
|
||||||
Scenarios ({numScenarios})
|
Scenarios ({numScenarios})
|
||||||
</Heading>
|
</Heading>
|
||||||
|
{canModify && (
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -43,6 +45,7 @@ export const ScenariosHeader = ({
|
|||||||
>
|
>
|
||||||
Edit Vars
|
Edit Vars
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Box, Button, HStack, Spinner, Tooltip, useToast, Text } from "@chakra-ui/react";
|
import { Box, Button, HStack, Spinner, Tooltip, useToast, Text } from "@chakra-ui/react";
|
||||||
import { useRef, useEffect, useState, useCallback } from "react";
|
import { useRef, useEffect, useState, useCallback } from "react";
|
||||||
import { useHandledAsyncCallback, useModifierKeyLabel } from "~/utils/hooks";
|
import { useExperimentAccess, useHandledAsyncCallback, useModifierKeyLabel } from "~/utils/hooks";
|
||||||
import { type PromptVariant } from "./types";
|
import { type PromptVariant } from "./types";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useAppStore } from "~/state/store";
|
import { useAppStore } from "~/state/store";
|
||||||
|
|
||||||
export default function VariantEditor(props: { variant: PromptVariant }) {
|
export default function VariantEditor(props: { variant: PromptVariant }) {
|
||||||
|
const { canModify } = useExperimentAccess();
|
||||||
const monaco = useAppStore.use.sharedVariantEditor.monaco();
|
const monaco = useAppStore.use.sharedVariantEditor.monaco();
|
||||||
const editorRef = useRef<ReturnType<NonNullable<typeof monaco>["editor"]["create"]> | null>(null);
|
const editorRef = useRef<ReturnType<NonNullable<typeof monaco>["editor"]["create"]> | null>(null);
|
||||||
const [editorId] = useState(() => `editor_${Math.random().toString(36).substring(7)}`);
|
const [editorId] = useState(() => `editor_${Math.random().toString(36).substring(7)}`);
|
||||||
@@ -40,18 +41,6 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
const model = editorRef.current.getModel();
|
const model = editorRef.current.getModel();
|
||||||
if (!model) return;
|
if (!model) return;
|
||||||
|
|
||||||
const markers = monaco?.editor.getModelMarkers({ resource: model.uri });
|
|
||||||
const hasErrors = markers?.some((m) => m.severity === monaco?.MarkerSeverity.Error);
|
|
||||||
|
|
||||||
if (hasErrors) {
|
|
||||||
toast({
|
|
||||||
title: "Invalid TypeScript",
|
|
||||||
description: "Please fix the TypeScript errors before saving.",
|
|
||||||
status: "error",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the user defined the prompt with the string "prompt\w*=" somewhere
|
// Make sure the user defined the prompt with the string "prompt\w*=" somewhere
|
||||||
const promptRegex = /prompt\s*=/;
|
const promptRegex = /prompt\s*=/;
|
||||||
if (!promptRegex.test(currentFn)) {
|
if (!promptRegex.test(currentFn)) {
|
||||||
@@ -103,6 +92,7 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
wordWrapBreakAfterCharacters: "",
|
wordWrapBreakAfterCharacters: "",
|
||||||
wordWrapBreakBeforeCharacters: "",
|
wordWrapBreakBeforeCharacters: "",
|
||||||
quickSuggestions: true,
|
quickSuggestions: true,
|
||||||
|
readOnly: !canModify,
|
||||||
});
|
});
|
||||||
|
|
||||||
editorRef.current.onDidFocusEditorText(() => {
|
editorRef.current.onDidFocusEditorText(() => {
|
||||||
@@ -130,6 +120,13 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||||
}, [monaco, editorId]);
|
}, [monaco, editorId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editorRef.current) return;
|
||||||
|
editorRef.current.updateOptions({
|
||||||
|
readOnly: !canModify,
|
||||||
|
});
|
||||||
|
}, [canModify]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box w="100%" pos="relative">
|
<Box w="100%" pos="relative">
|
||||||
<div id={editorId} style={{ height: "400px", width: "100%" }}></div>
|
<div id={editorId} style={{ height: "400px", width: "100%" }}></div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { type SystemStyleObject } from "@chakra-ui/react";
|
|||||||
|
|
||||||
export const stickyHeaderStyle: SystemStyleObject = {
|
export const stickyHeaderStyle: SystemStyleObject = {
|
||||||
position: "sticky",
|
position: "sticky",
|
||||||
top: "-1px",
|
top: "0",
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "#fff",
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { Flex, Icon, Link, Text } from "@chakra-ui/react";
|
|
||||||
import { BsExclamationTriangleFill } from "react-icons/bs";
|
|
||||||
import { env } from "~/env.mjs";
|
|
||||||
|
|
||||||
export default function PublicPlaygroundWarning() {
|
|
||||||
if (!env.NEXT_PUBLIC_IS_PUBLIC_PLAYGROUND) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex bgColor="red.600" color="whiteAlpha.900" p={2} align="center">
|
|
||||||
<Icon boxSize={4} mr={2} as={BsExclamationTriangleFill} />
|
|
||||||
<Text>
|
|
||||||
Warning: this is a public playground. Anyone can see, edit or delete your experiments. For
|
|
||||||
private use,{" "}
|
|
||||||
<Link textDecor="underline" href="https://github.com/openpipe/openpipe" target="_blank">
|
|
||||||
run a local copy
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
import { useState, type DragEvent } from "react";
|
import { useState, type DragEvent } from "react";
|
||||||
import { type PromptVariant } from "../OutputsTable/types";
|
import { type PromptVariant } from "../OutputsTable/types";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
|
||||||
import { HStack, Icon, GridItem } from "@chakra-ui/react"; // Changed here
|
|
||||||
import { RiDraggable } from "react-icons/ri";
|
import { RiDraggable } from "react-icons/ri";
|
||||||
|
import { useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
import { HStack, Icon, Text, GridItem } from "@chakra-ui/react"; // Changed here
|
||||||
import { cellPadding, headerMinHeight } from "../constants";
|
import { cellPadding, headerMinHeight } from "../constants";
|
||||||
import AutoResizeTextArea from "../AutoResizeTextArea";
|
import AutoResizeTextArea from "../AutoResizeTextArea";
|
||||||
import { stickyHeaderStyle } from "../OutputsTable/styles";
|
import { stickyHeaderStyle } from "../OutputsTable/styles";
|
||||||
import VariantHeaderMenuButton from "./VariantHeaderMenuButton";
|
import VariantHeaderMenuButton from "./VariantHeaderMenuButton";
|
||||||
|
|
||||||
export default function VariantHeader(props: { variant: PromptVariant; canHide: boolean }) {
|
export default function VariantHeader(props: { variant: PromptVariant; canHide: boolean }) {
|
||||||
|
const { canModify } = useExperimentAccess();
|
||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
const [isDragTarget, setIsDragTarget] = useState(false);
|
const [isDragTarget, setIsDragTarget] = useState(false);
|
||||||
const [isInputHovered, setIsInputHovered] = useState(false);
|
const [isInputHovered, setIsInputHovered] = useState(false);
|
||||||
@@ -44,6 +45,16 @@ export default function VariantHeader(props: { variant: PromptVariant; canHide:
|
|||||||
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
if (!canModify) {
|
||||||
|
return (
|
||||||
|
<GridItem padding={0} sx={stickyHeaderStyle} borderTopWidth={1}>
|
||||||
|
<Text fontSize={16} fontWeight="bold" px={cellPadding.x} py={cellPadding.y}>
|
||||||
|
{props.variant.label}
|
||||||
|
</Text>
|
||||||
|
</GridItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridItem
|
<GridItem
|
||||||
padding={0}
|
padding={0}
|
||||||
|
|||||||
@@ -14,17 +14,19 @@ import {
|
|||||||
Link,
|
Link,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { BsGithub, BsTwitter } from "react-icons/bs";
|
import { BsGithub, BsPersonCircle } from "react-icons/bs";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import PublicPlaygroundWarning from "../PublicPlaygroundWarning";
|
|
||||||
import { type IconType } from "react-icons";
|
import { type IconType } from "react-icons";
|
||||||
import { RiFlaskLine } from "react-icons/ri";
|
import { RiFlaskLine } from "react-icons/ri";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { signIn, useSession } from "next-auth/react";
|
||||||
|
import UserMenu from "./UserMenu";
|
||||||
|
|
||||||
type IconLinkProps = BoxProps & LinkProps & { label: string; icon: IconType; href: string };
|
type IconLinkProps = BoxProps & LinkProps & { label?: string; icon: IconType };
|
||||||
|
|
||||||
const IconLink = ({ icon, label, href, target, color, ...props }: IconLinkProps) => {
|
const IconLink = ({ icon, label, href, target, color, ...props }: IconLinkProps) => {
|
||||||
const isActive = useRouter().pathname.startsWith(href);
|
const router = useRouter();
|
||||||
|
const isActive = href && router.pathname.startsWith(href);
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as={Link}
|
as={Link}
|
||||||
@@ -32,7 +34,7 @@ const IconLink = ({ icon, label, href, target, color, ...props }: IconLinkProps)
|
|||||||
target={target}
|
target={target}
|
||||||
w="full"
|
w="full"
|
||||||
bgColor={isActive ? "gray.300" : "transparent"}
|
bgColor={isActive ? "gray.300" : "transparent"}
|
||||||
_hover={{ bgColor: "gray.300" }}
|
_hover={{ bgColor: "gray.200", textDecoration: "none" }}
|
||||||
py={4}
|
py={4}
|
||||||
justifyContent="start"
|
justifyContent="start"
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
@@ -47,6 +49,8 @@ const IconLink = ({ icon, label, href, target, color, ...props }: IconLinkProps)
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NavSidebar = () => {
|
const NavSidebar = () => {
|
||||||
|
const user = useSession().data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack align="stretch" bgColor="gray.100" py={2} pb={0} height="100%">
|
<VStack align="stretch" bgColor="gray.100" py={2} pb={0} height="100%">
|
||||||
<Link href="/" w="full" _hover={{ textDecoration: "none" }}>
|
<Link href="/" w="full" _hover={{ textDecoration: "none" }}>
|
||||||
@@ -59,26 +63,32 @@ const NavSidebar = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
<Divider />
|
<Divider />
|
||||||
<VStack spacing={0} align="flex-start" overflowY="auto" overflowX="hidden" flex={1}>
|
<VStack spacing={0} align="flex-start" overflowY="auto" overflowX="hidden" flex={1}>
|
||||||
|
{user != null && (
|
||||||
|
<>
|
||||||
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
|
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
|
||||||
</VStack>
|
</>
|
||||||
<Divider />
|
)}
|
||||||
<VStack w="full" spacing={0} pb={2}>
|
{user === null && (
|
||||||
<IconLink
|
<IconLink
|
||||||
icon={BsGithub}
|
icon={BsPersonCircle}
|
||||||
label="GitHub"
|
label="Sign In"
|
||||||
|
onClick={() => {
|
||||||
|
signIn("github").catch(console.error);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
{user ? <UserMenu user={user} /> : <Divider />}
|
||||||
|
<VStack spacing={0} align="center">
|
||||||
|
<Link
|
||||||
href="https://github.com/openpipe/openpipe"
|
href="https://github.com/openpipe/openpipe"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
color="gray.500"
|
color="gray.500"
|
||||||
_hover={{ color: "gray.800" }}
|
_hover={{ color: "gray.800" }}
|
||||||
/>
|
p={2}
|
||||||
<IconLink
|
>
|
||||||
icon={BsTwitter}
|
<Icon as={BsGithub} boxSize={6} />
|
||||||
label="Twitter"
|
</Link>
|
||||||
href="https://twitter.com/corbtt"
|
|
||||||
target="_blank"
|
|
||||||
color="gray.500"
|
|
||||||
_hover={{ color: "gray.800" }}
|
|
||||||
/>
|
|
||||||
</VStack>
|
</VStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
@@ -108,16 +118,13 @@ export default function AppShell(props: { children: React.ReactNode; title?: str
|
|||||||
<Grid
|
<Grid
|
||||||
h={vh}
|
h={vh}
|
||||||
w="100vw"
|
w="100vw"
|
||||||
templateColumns={{ base: "56px minmax(0, 1fr)", md: "200px minmax(0, 1fr)" }}
|
templateColumns={{ base: "56px minmax(0, 1fr)", md: "220px minmax(0, 1fr)" }}
|
||||||
templateRows="max-content 1fr"
|
templateRows="1fr"
|
||||||
templateAreas={'"warning warning"\n"sidebar main"'}
|
templateAreas={'"sidebar main"'}
|
||||||
>
|
>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{props.title ? `${props.title} | OpenPipe` : "OpenPipe"}</title>
|
<title>{props.title ? `${props.title} | OpenPipe` : "OpenPipe"}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<GridItem area="warning">
|
|
||||||
<PublicPlaygroundWarning />
|
|
||||||
</GridItem>
|
|
||||||
<GridItem area="sidebar" overflow="hidden">
|
<GridItem area="sidebar" overflow="hidden">
|
||||||
<NavSidebar />
|
<NavSidebar />
|
||||||
</GridItem>
|
</GridItem>
|
||||||
|
|||||||
72
src/components/nav/UserMenu.tsx
Normal file
72
src/components/nav/UserMenu.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import {
|
||||||
|
HStack,
|
||||||
|
Icon,
|
||||||
|
Image,
|
||||||
|
VStack,
|
||||||
|
Text,
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
Link,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { type Session } from "next-auth";
|
||||||
|
import { signOut } from "next-auth/react";
|
||||||
|
import { BsBoxArrowRight, BsChevronRight, BsPersonCircle } from "react-icons/bs";
|
||||||
|
|
||||||
|
export default function UserMenu({ user }: { user: Session }) {
|
||||||
|
const profileImage = user.user.image ? (
|
||||||
|
<Image src={user.user.image} alt="profile picture" w={8} h={8} borderRadius="50%" />
|
||||||
|
) : (
|
||||||
|
<Icon as={BsPersonCircle} boxSize="md" />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Popover placement="right">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<HStack
|
||||||
|
px={2}
|
||||||
|
py={2}
|
||||||
|
borderColor={"gray.200"}
|
||||||
|
borderTopWidth={1}
|
||||||
|
borderBottomWidth={1}
|
||||||
|
cursor="pointer"
|
||||||
|
_hover={{
|
||||||
|
bgColor: "gray.200",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{profileImage}
|
||||||
|
<VStack spacing={0} align="start" flex={1}>
|
||||||
|
<Text fontWeight="bold" fontSize="sm">
|
||||||
|
{user.user.name}
|
||||||
|
</Text>
|
||||||
|
<Text color="gray.500" fontSize="xs">
|
||||||
|
{user.user.email}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
<Icon as={BsChevronRight} boxSize={4} color="gray.500" />
|
||||||
|
</HStack>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent _focusVisible={{ boxShadow: "unset", outline: "unset" }} maxW="200px">
|
||||||
|
<VStack align="stretch" spacing={0}>
|
||||||
|
{/* sign out */}
|
||||||
|
<HStack
|
||||||
|
as={Link}
|
||||||
|
onClick={() => {
|
||||||
|
signOut().catch(console.error);
|
||||||
|
}}
|
||||||
|
px={4}
|
||||||
|
py={2}
|
||||||
|
spacing={4}
|
||||||
|
color="gray.500"
|
||||||
|
fontSize="sm"
|
||||||
|
>
|
||||||
|
<Icon as={BsBoxArrowRight} boxSize={6} />
|
||||||
|
<Text>Sign out</Text>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/env.mjs
10
src/env.mjs
@@ -15,6 +15,8 @@ export const env = createEnv({
|
|||||||
.optional()
|
.optional()
|
||||||
.default("false")
|
.default("false")
|
||||||
.transform((val) => val.toLowerCase() === "true"),
|
.transform((val) => val.toLowerCase() === "true"),
|
||||||
|
GITHUB_CLIENT_ID: z.string().min(1),
|
||||||
|
GITHUB_CLIENT_SECRET: z.string().min(1),
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,11 +26,6 @@ export const env = createEnv({
|
|||||||
*/
|
*/
|
||||||
client: {
|
client: {
|
||||||
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
|
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
|
||||||
NEXT_PUBLIC_IS_PUBLIC_PLAYGROUND: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.default("false")
|
|
||||||
.transform((val) => val.toLowerCase() === "true"),
|
|
||||||
NEXT_PUBLIC_SOCKET_URL: z.string().url().default("http://localhost:3318"),
|
NEXT_PUBLIC_SOCKET_URL: z.string().url().default("http://localhost:3318"),
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -42,8 +39,9 @@ export const env = createEnv({
|
|||||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||||
RESTRICT_PRISMA_LOGS: process.env.RESTRICT_PRISMA_LOGS,
|
RESTRICT_PRISMA_LOGS: process.env.RESTRICT_PRISMA_LOGS,
|
||||||
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
||||||
NEXT_PUBLIC_IS_PUBLIC_PLAYGROUND: process.env.NEXT_PUBLIC_IS_PUBLIC_PLAYGROUND,
|
|
||||||
NEXT_PUBLIC_SOCKET_URL: process.env.NEXT_PUBLIC_SOCKET_URL,
|
NEXT_PUBLIC_SOCKET_URL: process.env.NEXT_PUBLIC_SOCKET_URL,
|
||||||
|
GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
|
||||||
|
GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
|
||||||
|
|||||||
23
src/pages/account/signin.tsx
Normal file
23
src/pages/account/signin.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { signIn, useSession } from "next-auth/react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import AppShell from "~/components/nav/AppShell";
|
||||||
|
|
||||||
|
export default function SignIn() {
|
||||||
|
const session = useSession().data;
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (session) {
|
||||||
|
router.push("/experiments").catch(console.error);
|
||||||
|
} else if (session === null) {
|
||||||
|
signIn("github").catch(console.error);
|
||||||
|
}
|
||||||
|
}, [session, router]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell>
|
||||||
|
<div />
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -124,6 +124,8 @@ export default function Experiment() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canModify = experiment.data?.access.canModify ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell title={experiment.data?.label}>
|
<AppShell title={experiment.data?.label}>
|
||||||
<VStack h="full">
|
<VStack h="full">
|
||||||
@@ -143,6 +145,7 @@ export default function Experiment() {
|
|||||||
</Link>
|
</Link>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbItem isCurrentPage>
|
<BreadcrumbItem isCurrentPage>
|
||||||
|
{canModify ? (
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
value={label}
|
value={label}
|
||||||
@@ -157,8 +160,14 @@ export default function Experiment() {
|
|||||||
_hover={{ borderColor: "gray.300" }}
|
_hover={{ borderColor: "gray.300" }}
|
||||||
_focus={{ borderColor: "blue.500", outline: "none" }}
|
_focus={{ borderColor: "blue.500", outline: "none" }}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Text fontSize={16} px={0} minW={{ base: 100, lg: 300 }} flex={1}>
|
||||||
|
{experiment.data?.label}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
|
{canModify && (
|
||||||
<HStack>
|
<HStack>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -174,6 +183,7 @@ export default function Experiment() {
|
|||||||
</Button>
|
</Button>
|
||||||
<DeleteButton />
|
<DeleteButton />
|
||||||
</HStack>
|
</HStack>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
<SettingsDrawer />
|
<SettingsDrawer />
|
||||||
<Box w="100%" overflowX="auto" flex={1}>
|
<Box w="100%" overflowX="auto" flex={1}>
|
||||||
|
|||||||
@@ -6,18 +6,44 @@ import {
|
|||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
Flex,
|
Flex,
|
||||||
|
Center,
|
||||||
|
Text,
|
||||||
|
Link,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { RiFlaskLine } from "react-icons/ri";
|
import { RiFlaskLine } from "react-icons/ri";
|
||||||
import AppShell from "~/components/nav/AppShell";
|
import AppShell from "~/components/nav/AppShell";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { NewExperimentButton } from "~/components/experiments/NewExperimentButton";
|
import { NewExperimentButton } from "~/components/experiments/NewExperimentButton";
|
||||||
import { ExperimentCard } from "~/components/experiments/ExperimentCard";
|
import { ExperimentCard } from "~/components/experiments/ExperimentCard";
|
||||||
|
import { signIn, useSession } from "next-auth/react";
|
||||||
|
|
||||||
export default function ExperimentsPage() {
|
export default function ExperimentsPage() {
|
||||||
const experiments = api.experiments.list.useQuery();
|
const experiments = api.experiments.list.useQuery();
|
||||||
|
|
||||||
|
const user = useSession().data;
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
return (
|
return (
|
||||||
<AppShell>
|
<AppShell title="Experiments">
|
||||||
|
<Center h="100%">
|
||||||
|
<Text>
|
||||||
|
<Link
|
||||||
|
onClick={() => {
|
||||||
|
signIn("github").catch(console.error);
|
||||||
|
}}
|
||||||
|
textDecor="underline"
|
||||||
|
>
|
||||||
|
Sign in
|
||||||
|
</Link>{" "}
|
||||||
|
to view or create new experiments!
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell title="Experiments">
|
||||||
<VStack alignItems={"flex-start"} m={4} mt={1}>
|
<VStack alignItems={"flex-start"} m={4} mt={1}>
|
||||||
<HStack w="full" justifyContent="space-between" mb={4}>
|
<HStack w="full" justifyContent="space-between" mb={4}>
|
||||||
<Breadcrumb flex={1}>
|
<Breadcrumb flex={1}>
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { EvalType } from "@prisma/client";
|
import { EvalType } from "@prisma/client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc";
|
||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
import { runAllEvals } from "~/server/utils/evaluations";
|
import { runAllEvals } from "~/server/utils/evaluations";
|
||||||
|
import { requireCanModifyExperiment, requireCanViewExperiment } from "~/utils/accessControl";
|
||||||
|
|
||||||
export const evaluationsRouter = createTRPCRouter({
|
export const evaluationsRouter = createTRPCRouter({
|
||||||
list: publicProcedure.input(z.object({ experimentId: z.string() })).query(async ({ input }) => {
|
list: publicProcedure
|
||||||
|
.input(z.object({ experimentId: z.string() }))
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
await requireCanViewExperiment(input.experimentId, ctx);
|
||||||
|
|
||||||
return await prisma.evaluation.findMany({
|
return await prisma.evaluation.findMany({
|
||||||
where: {
|
where: {
|
||||||
experimentId: input.experimentId,
|
experimentId: input.experimentId,
|
||||||
@@ -14,7 +19,7 @@ export const evaluationsRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
create: publicProcedure
|
create: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
experimentId: z.string(),
|
experimentId: z.string(),
|
||||||
@@ -23,7 +28,9 @@ export const evaluationsRouter = createTRPCRouter({
|
|||||||
evalType: z.nativeEnum(EvalType),
|
evalType: z.nativeEnum(EvalType),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await requireCanModifyExperiment(input.experimentId, ctx);
|
||||||
|
|
||||||
await prisma.evaluation.create({
|
await prisma.evaluation.create({
|
||||||
data: {
|
data: {
|
||||||
experimentId: input.experimentId,
|
experimentId: input.experimentId,
|
||||||
@@ -38,7 +45,7 @@ export const evaluationsRouter = createTRPCRouter({
|
|||||||
await runAllEvals(input.experimentId);
|
await runAllEvals(input.experimentId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: publicProcedure
|
update: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -49,7 +56,12 @@ export const evaluationsRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const { experimentId } = await prisma.evaluation.findUniqueOrThrow({
|
||||||
|
where: { id: input.id },
|
||||||
|
});
|
||||||
|
await requireCanModifyExperiment(experimentId, ctx);
|
||||||
|
|
||||||
const evaluation = await prisma.evaluation.update({
|
const evaluation = await prisma.evaluation.update({
|
||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
data: {
|
data: {
|
||||||
@@ -69,7 +81,14 @@ export const evaluationsRouter = createTRPCRouter({
|
|||||||
await runAllEvals(evaluation.experimentId);
|
await runAllEvals(evaluation.experimentId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: publicProcedure.input(z.object({ id: z.string() })).mutation(async ({ input }) => {
|
delete: protectedProcedure
|
||||||
|
.input(z.object({ id: z.string() }))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const { experimentId } = await prisma.evaluation.findUniqueOrThrow({
|
||||||
|
where: { id: input.id },
|
||||||
|
});
|
||||||
|
await requireCanModifyExperiment(experimentId, ctx);
|
||||||
|
|
||||||
await prisma.evaluation.delete({
|
await prisma.evaluation.delete({
|
||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc";
|
||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
import dedent from "dedent";
|
import dedent from "dedent";
|
||||||
import { generateNewCell } from "~/server/utils/generateNewCell";
|
import { generateNewCell } from "~/server/utils/generateNewCell";
|
||||||
|
import {
|
||||||
|
canModifyExperiment,
|
||||||
|
requireCanModifyExperiment,
|
||||||
|
requireCanViewExperiment,
|
||||||
|
requireNothing,
|
||||||
|
} from "~/utils/accessControl";
|
||||||
|
import userOrg from "~/server/utils/userOrg";
|
||||||
|
|
||||||
export const experimentsRouter = createTRPCRouter({
|
export const experimentsRouter = createTRPCRouter({
|
||||||
list: publicProcedure.query(async () => {
|
list: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
// Anyone can list experiments
|
||||||
|
requireNothing(ctx);
|
||||||
|
|
||||||
const experiments = await prisma.experiment.findMany({
|
const experiments = await prisma.experiment.findMany({
|
||||||
|
where: {
|
||||||
|
organization: {
|
||||||
|
OrganizationUser: {
|
||||||
|
some: { userId: ctx.session.user.id },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
sortIndex: "asc",
|
sortIndex: "asc",
|
||||||
},
|
},
|
||||||
@@ -40,15 +57,29 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
return experimentsWithCounts;
|
return experimentsWithCounts;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => {
|
get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => {
|
||||||
return await prisma.experiment.findFirst({
|
await requireCanViewExperiment(input.id, ctx);
|
||||||
where: {
|
const experiment = await prisma.experiment.findFirstOrThrow({
|
||||||
id: input.id,
|
where: { id: input.id },
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canModify = ctx.session?.user.id
|
||||||
|
? await canModifyExperiment(experiment.id, ctx.session?.user.id)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...experiment,
|
||||||
|
access: {
|
||||||
|
canView: true,
|
||||||
|
canModify,
|
||||||
|
},
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
create: publicProcedure.input(z.object({})).mutation(async () => {
|
create: protectedProcedure.input(z.object({})).mutation(async ({ ctx }) => {
|
||||||
|
// Anyone can create an experiment
|
||||||
|
requireNothing(ctx);
|
||||||
|
|
||||||
const maxSortIndex =
|
const maxSortIndex =
|
||||||
(
|
(
|
||||||
await prisma.experiment.aggregate({
|
await prisma.experiment.aggregate({
|
||||||
@@ -62,6 +93,7 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
data: {
|
data: {
|
||||||
sortIndex: maxSortIndex + 1,
|
sortIndex: maxSortIndex + 1,
|
||||||
label: `Experiment ${maxSortIndex + 1}`,
|
label: `Experiment ${maxSortIndex + 1}`,
|
||||||
|
organizationId: (await userOrg(ctx.session.user.id)).id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,9 +149,10 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
return exp;
|
return exp;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: publicProcedure
|
update: protectedProcedure
|
||||||
.input(z.object({ id: z.string(), updates: z.object({ label: z.string() }) }))
|
.input(z.object({ id: z.string(), updates: z.object({ label: z.string() }) }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await requireCanModifyExperiment(input.id, ctx);
|
||||||
return await prisma.experiment.update({
|
return await prisma.experiment.update({
|
||||||
where: {
|
where: {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
@@ -130,7 +163,11 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: publicProcedure.input(z.object({ id: z.string() })).mutation(async ({ input }) => {
|
delete: protectedProcedure
|
||||||
|
.input(z.object({ id: z.string() }))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await requireCanModifyExperiment(input.id, ctx);
|
||||||
|
|
||||||
await prisma.experiment.delete({
|
await prisma.experiment.delete({
|
||||||
where: {
|
where: {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { isObject } from "lodash-es";
|
import { isObject } from "lodash-es";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc";
|
||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
import { generateNewCell } from "~/server/utils/generateNewCell";
|
import { generateNewCell } from "~/server/utils/generateNewCell";
|
||||||
import { OpenAIChatModel, type SupportedModel } from "~/server/types";
|
import { OpenAIChatModel, type SupportedModel } from "~/server/types";
|
||||||
@@ -11,9 +11,14 @@ import { calculateTokenCost } from "~/utils/calculateTokenCost";
|
|||||||
import { reorderPromptVariants } from "~/server/utils/reorderPromptVariants";
|
import { reorderPromptVariants } from "~/server/utils/reorderPromptVariants";
|
||||||
import { type PromptVariant } from "@prisma/client";
|
import { type PromptVariant } from "@prisma/client";
|
||||||
import { deriveNewConstructFn } from "~/server/utils/deriveNewContructFn";
|
import { deriveNewConstructFn } from "~/server/utils/deriveNewContructFn";
|
||||||
|
import { requireCanModifyExperiment, requireCanViewExperiment } from "~/utils/accessControl";
|
||||||
|
|
||||||
export const promptVariantsRouter = createTRPCRouter({
|
export const promptVariantsRouter = createTRPCRouter({
|
||||||
list: publicProcedure.input(z.object({ experimentId: z.string() })).query(async ({ input }) => {
|
list: publicProcedure
|
||||||
|
.input(z.object({ experimentId: z.string() }))
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
await requireCanViewExperiment(input.experimentId, ctx);
|
||||||
|
|
||||||
return await prisma.promptVariant.findMany({
|
return await prisma.promptVariant.findMany({
|
||||||
where: {
|
where: {
|
||||||
experimentId: input.experimentId,
|
experimentId: input.experimentId,
|
||||||
@@ -23,7 +28,9 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
stats: publicProcedure.input(z.object({ variantId: z.string() })).query(async ({ input }) => {
|
stats: publicProcedure
|
||||||
|
.input(z.object({ variantId: z.string() }))
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
const variant = await prisma.promptVariant.findUnique({
|
const variant = await prisma.promptVariant.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: input.variantId,
|
id: input.variantId,
|
||||||
@@ -34,6 +41,8 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
throw new Error(`Prompt Variant with id ${input.variantId} does not exist`);
|
throw new Error(`Prompt Variant with id ${input.variantId} does not exist`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await requireCanViewExperiment(variant.experimentId, ctx);
|
||||||
|
|
||||||
const outputEvals = await prisma.outputEvaluation.groupBy({
|
const outputEvals = await prisma.outputEvaluation.groupBy({
|
||||||
by: ["evaluationId"],
|
by: ["evaluationId"],
|
||||||
_sum: {
|
_sum: {
|
||||||
@@ -64,7 +73,9 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const evalResults = evals.map((evalItem) => {
|
const evalResults = evals.map((evalItem) => {
|
||||||
const evalResult = outputEvals.find((outputEval) => outputEval.evaluationId === evalItem.id);
|
const evalResult = outputEvals.find(
|
||||||
|
(outputEval) => outputEval.evaluationId === evalItem.id,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
id: evalItem.id,
|
id: evalItem.id,
|
||||||
label: evalItem.label,
|
label: evalItem.label,
|
||||||
@@ -133,7 +144,7 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
create: publicProcedure
|
create: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
experimentId: z.string(),
|
experimentId: z.string(),
|
||||||
@@ -141,7 +152,9 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
newModel: z.string().optional(),
|
newModel: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await requireCanViewExperiment(input.experimentId, ctx);
|
||||||
|
|
||||||
let originalVariant: PromptVariant | null = null;
|
let originalVariant: PromptVariant | null = null;
|
||||||
if (input.variantId) {
|
if (input.variantId) {
|
||||||
originalVariant = await prisma.promptVariant.findUnique({
|
originalVariant = await prisma.promptVariant.findUnique({
|
||||||
@@ -217,7 +230,7 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
return newVariant;
|
return newVariant;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: publicProcedure
|
update: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -226,7 +239,7 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const existing = await prisma.promptVariant.findUnique({
|
const existing = await prisma.promptVariant.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
@@ -237,6 +250,8 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
throw new Error(`Prompt Variant with id ${input.id} does not exist`);
|
throw new Error(`Prompt Variant with id ${input.id} does not exist`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await requireCanModifyExperiment(existing.experimentId, ctx);
|
||||||
|
|
||||||
const updatePromptVariantAction = prisma.promptVariant.update({
|
const updatePromptVariantAction = prisma.promptVariant.update({
|
||||||
where: {
|
where: {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
@@ -252,13 +267,18 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
return updatedPromptVariant;
|
return updatedPromptVariant;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
hide: publicProcedure
|
hide: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const { experimentId } = await prisma.promptVariant.findUniqueOrThrow({
|
||||||
|
where: { id: input.id },
|
||||||
|
});
|
||||||
|
await requireCanModifyExperiment(experimentId, ctx);
|
||||||
|
|
||||||
const updatedPromptVariant = await prisma.promptVariant.update({
|
const updatedPromptVariant = await prisma.promptVariant.update({
|
||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
data: { visible: false, experiment: { update: { updatedAt: new Date() } } },
|
data: { visible: false, experiment: { update: { updatedAt: new Date() } } },
|
||||||
@@ -267,19 +287,20 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
return updatedPromptVariant;
|
return updatedPromptVariant;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
replaceVariant: publicProcedure
|
replaceVariant: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
constructFn: z.string(),
|
constructFn: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const existing = await prisma.promptVariant.findUnique({
|
const existing = await prisma.promptVariant.findUniqueOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await requireCanModifyExperiment(existing.experimentId, ctx);
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new Error(`Prompt Variant with id ${input.id} does not exist`);
|
throw new Error(`Prompt Variant with id ${input.id} does not exist`);
|
||||||
@@ -347,14 +368,19 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
return { status: "ok" } as const;
|
return { status: "ok" } as const;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
reorder: publicProcedure
|
reorder: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
draggedId: z.string(),
|
draggedId: z.string(),
|
||||||
droppedId: z.string(),
|
droppedId: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const { experimentId } = await prisma.promptVariant.findUniqueOrThrow({
|
||||||
|
where: { id: input.draggedId },
|
||||||
|
});
|
||||||
|
await requireCanModifyExperiment(experimentId, ctx);
|
||||||
|
|
||||||
await reorderPromptVariants(input.draggedId, input.droppedId);
|
await reorderPromptVariants(input.draggedId, input.droppedId);
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc";
|
||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
import { generateNewCell } from "~/server/utils/generateNewCell";
|
import { generateNewCell } from "~/server/utils/generateNewCell";
|
||||||
import { queueLLMRetrievalTask } from "~/server/utils/queueLLMRetrievalTask";
|
import { queueLLMRetrievalTask } from "~/server/utils/queueLLMRetrievalTask";
|
||||||
|
import { requireCanModifyExperiment, requireCanViewExperiment } from "~/utils/accessControl";
|
||||||
|
|
||||||
export const scenarioVariantCellsRouter = createTRPCRouter({
|
export const scenarioVariantCellsRouter = createTRPCRouter({
|
||||||
get: publicProcedure
|
get: publicProcedure
|
||||||
@@ -12,7 +13,12 @@ export const scenarioVariantCellsRouter = createTRPCRouter({
|
|||||||
variantId: z.string(),
|
variantId: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
const { experimentId } = await prisma.testScenario.findUniqueOrThrow({
|
||||||
|
where: { id: input.scenarioId },
|
||||||
|
});
|
||||||
|
await requireCanViewExperiment(experimentId, ctx);
|
||||||
|
|
||||||
return await prisma.scenarioVariantCell.findUnique({
|
return await prisma.scenarioVariantCell.findUnique({
|
||||||
where: {
|
where: {
|
||||||
promptVariantId_testScenarioId: {
|
promptVariantId_testScenarioId: {
|
||||||
@@ -35,14 +41,20 @@ export const scenarioVariantCellsRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
forceRefetch: publicProcedure
|
forceRefetch: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
scenarioId: z.string(),
|
scenarioId: z.string(),
|
||||||
variantId: z.string(),
|
variantId: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const { experimentId } = await prisma.testScenario.findUniqueOrThrow({
|
||||||
|
where: { id: input.scenarioId },
|
||||||
|
});
|
||||||
|
|
||||||
|
await requireCanModifyExperiment(experimentId, ctx);
|
||||||
|
|
||||||
const cell = await prisma.scenarioVariantCell.findUnique({
|
const cell = await prisma.scenarioVariantCell.findUnique({
|
||||||
where: {
|
where: {
|
||||||
promptVariantId_testScenarioId: {
|
promptVariantId_testScenarioId: {
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc";
|
||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
import { autogenerateScenarioValues } from "../autogen";
|
import { autogenerateScenarioValues } from "../autogen";
|
||||||
import { recordExperimentUpdated } from "~/server/utils/recordExperimentUpdated";
|
import { recordExperimentUpdated } from "~/server/utils/recordExperimentUpdated";
|
||||||
import { runAllEvals } from "~/server/utils/evaluations";
|
import { runAllEvals } from "~/server/utils/evaluations";
|
||||||
import { generateNewCell } from "~/server/utils/generateNewCell";
|
import { generateNewCell } from "~/server/utils/generateNewCell";
|
||||||
|
import { requireCanModifyExperiment, requireCanViewExperiment } from "~/utils/accessControl";
|
||||||
|
|
||||||
export const scenariosRouter = createTRPCRouter({
|
export const scenariosRouter = createTRPCRouter({
|
||||||
list: publicProcedure.input(z.object({ experimentId: z.string() })).query(async ({ input }) => {
|
list: publicProcedure
|
||||||
|
.input(z.object({ experimentId: z.string() }))
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
await requireCanViewExperiment(input.experimentId, ctx);
|
||||||
|
|
||||||
return await prisma.testScenario.findMany({
|
return await prisma.testScenario.findMany({
|
||||||
where: {
|
where: {
|
||||||
experimentId: input.experimentId,
|
experimentId: input.experimentId,
|
||||||
@@ -19,14 +24,16 @@ export const scenariosRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
create: publicProcedure
|
create: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
experimentId: z.string(),
|
experimentId: z.string(),
|
||||||
autogenerate: z.boolean().optional(),
|
autogenerate: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await requireCanModifyExperiment(input.experimentId, ctx);
|
||||||
|
|
||||||
const maxSortIndex =
|
const maxSortIndex =
|
||||||
(
|
(
|
||||||
await prisma.testScenario.aggregate({
|
await prisma.testScenario.aggregate({
|
||||||
@@ -66,7 +73,14 @@ export const scenariosRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
hide: publicProcedure.input(z.object({ id: z.string() })).mutation(async ({ input }) => {
|
hide: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ input, ctx }) => {
|
||||||
|
const experimentId = (
|
||||||
|
await prisma.testScenario.findUniqueOrThrow({
|
||||||
|
where: { id: input.id },
|
||||||
|
})
|
||||||
|
).experimentId;
|
||||||
|
|
||||||
|
await requireCanModifyExperiment(experimentId, ctx);
|
||||||
const hiddenScenario = await prisma.testScenario.update({
|
const hiddenScenario = await prisma.testScenario.update({
|
||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
data: { visible: false, experiment: { update: { updatedAt: new Date() } } },
|
data: { visible: false, experiment: { update: { updatedAt: new Date() } } },
|
||||||
@@ -78,14 +92,14 @@ export const scenariosRouter = createTRPCRouter({
|
|||||||
return hiddenScenario;
|
return hiddenScenario;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
reorder: publicProcedure
|
reorder: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
draggedId: z.string(),
|
draggedId: z.string(),
|
||||||
droppedId: z.string(),
|
droppedId: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const dragged = await prisma.testScenario.findUnique({
|
const dragged = await prisma.testScenario.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: input.draggedId,
|
id: input.draggedId,
|
||||||
@@ -104,6 +118,8 @@ export const scenariosRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await requireCanModifyExperiment(dragged.experimentId, ctx);
|
||||||
|
|
||||||
const visibleItems = await prisma.testScenario.findMany({
|
const visibleItems = await prisma.testScenario.findMany({
|
||||||
where: {
|
where: {
|
||||||
experimentId: dragged.experimentId,
|
experimentId: dragged.experimentId,
|
||||||
@@ -147,14 +163,14 @@ export const scenariosRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
replaceWithValues: publicProcedure
|
replaceWithValues: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
values: z.record(z.string()),
|
values: z.record(z.string()),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const existing = await prisma.testScenario.findUnique({
|
const existing = await prisma.testScenario.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
@@ -165,6 +181,8 @@ export const scenariosRouter = createTRPCRouter({
|
|||||||
throw new Error(`Scenario with id ${input.id} does not exist`);
|
throw new Error(`Scenario with id ${input.id} does not exist`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await requireCanModifyExperiment(existing.experimentId, ctx);
|
||||||
|
|
||||||
const newScenario = await prisma.testScenario.create({
|
const newScenario = await prisma.testScenario.create({
|
||||||
data: {
|
data: {
|
||||||
experimentId: existing.experimentId,
|
experimentId: existing.experimentId,
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc";
|
||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
|
import { requireCanModifyExperiment, requireCanViewExperiment } from "~/utils/accessControl";
|
||||||
|
|
||||||
export const templateVarsRouter = createTRPCRouter({
|
export const templateVarsRouter = createTRPCRouter({
|
||||||
create: publicProcedure
|
create: protectedProcedure
|
||||||
.input(z.object({ experimentId: z.string(), label: z.string() }))
|
.input(z.object({ experimentId: z.string(), label: z.string() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await requireCanModifyExperiment(input.experimentId, ctx);
|
||||||
|
|
||||||
await prisma.templateVariable.create({
|
await prisma.templateVariable.create({
|
||||||
data: {
|
data: {
|
||||||
experimentId: input.experimentId,
|
experimentId: input.experimentId,
|
||||||
@@ -14,11 +17,22 @@ export const templateVarsRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: publicProcedure.input(z.object({ id: z.string() })).mutation(async ({ input }) => {
|
delete: protectedProcedure
|
||||||
|
.input(z.object({ id: z.string() }))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const { experimentId } = await prisma.templateVariable.findUniqueOrThrow({
|
||||||
|
where: { id: input.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
await requireCanModifyExperiment(experimentId, ctx);
|
||||||
|
|
||||||
await prisma.templateVariable.delete({ where: { id: input.id } });
|
await prisma.templateVariable.delete({ where: { id: input.id } });
|
||||||
}),
|
}),
|
||||||
|
|
||||||
list: publicProcedure.input(z.object({ experimentId: z.string() })).query(async ({ input }) => {
|
list: publicProcedure
|
||||||
|
.input(z.object({ experimentId: z.string() }))
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
await requireCanViewExperiment(input.experimentId, ctx);
|
||||||
return await prisma.templateVariable.findMany({
|
return await prisma.templateVariable.findMany({
|
||||||
where: {
|
where: {
|
||||||
experimentId: input.experimentId,
|
experimentId: input.experimentId,
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ type CreateContextOptions = {
|
|||||||
session: Session | null;
|
session: Session | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
const noOp = () => {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This helper generates the "internals" for a tRPC context. If you need to use it, you can export
|
* This helper generates the "internals" for a tRPC context. If you need to use it, you can export
|
||||||
* it from here.
|
* it from here.
|
||||||
@@ -41,6 +44,7 @@ const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
|||||||
return {
|
return {
|
||||||
session: opts.session,
|
session: opts.session,
|
||||||
prisma,
|
prisma,
|
||||||
|
markAccessControlRun: noOp,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,6 +73,8 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
|||||||
* errors on the backend.
|
* errors on the backend.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export type TRPCContext = Awaited<ReturnType<typeof createTRPCContext>>;
|
||||||
|
|
||||||
const t = initTRPC.context<typeof createTRPCContext>().create({
|
const t = initTRPC.context<typeof createTRPCContext>().create({
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
errorFormatter({ shape, error }) {
|
errorFormatter({ shape, error }) {
|
||||||
@@ -106,16 +112,29 @@ export const createTRPCRouter = t.router;
|
|||||||
export const publicProcedure = t.procedure;
|
export const publicProcedure = t.procedure;
|
||||||
|
|
||||||
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
||||||
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => {
|
||||||
if (!ctx.session || !ctx.session.user) {
|
if (!ctx.session || !ctx.session.user) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
}
|
}
|
||||||
return next({
|
|
||||||
|
let accessControlRun = false;
|
||||||
|
const resp = await next({
|
||||||
ctx: {
|
ctx: {
|
||||||
// infers the `session` as non-nullable
|
// infers the `session` as non-nullable
|
||||||
session: { ...ctx.session, user: ctx.session.user },
|
session: { ...ctx.session, user: ctx.session.user },
|
||||||
|
markAccessControlRun: () => {
|
||||||
|
accessControlRun = true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (!accessControlRun)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message:
|
||||||
|
"Protected routes must perform access control checks then explicitly invoke the `ctx.markAccessControlRun()` function to ensure we don't forget access control on a route.",
|
||||||
|
});
|
||||||
|
|
||||||
|
return resp;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { PrismaAdapter } from "@next-auth/prisma-adapter";
|
|||||||
import { type GetServerSidePropsContext } from "next";
|
import { type GetServerSidePropsContext } from "next";
|
||||||
import { getServerSession, type NextAuthOptions, type DefaultSession } from "next-auth";
|
import { getServerSession, type NextAuthOptions, type DefaultSession } from "next-auth";
|
||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
|
import GitHubProvider from "next-auth/providers/github";
|
||||||
|
import { env } from "~/env.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
|
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
|
||||||
@@ -41,20 +43,15 @@ export const authOptions: NextAuthOptions = {
|
|||||||
},
|
},
|
||||||
adapter: PrismaAdapter(prisma),
|
adapter: PrismaAdapter(prisma),
|
||||||
providers: [
|
providers: [
|
||||||
// DiscordProvider({
|
GitHubProvider({
|
||||||
// clientId: env.DISCORD_CLIENT_ID,
|
clientId: env.GITHUB_CLIENT_ID,
|
||||||
// clientSecret: env.DISCORD_CLIENT_SECRET,
|
clientSecret: env.GITHUB_CLIENT_SECRET,
|
||||||
// }),
|
}),
|
||||||
/**
|
|
||||||
* ...add more providers here.
|
|
||||||
*
|
|
||||||
* Most other providers require a bit more work than the Discord provider. For example, the
|
|
||||||
* GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
|
|
||||||
* model. Refer to the NextAuth.js docs for the provider you want to use. Example:
|
|
||||||
*
|
|
||||||
* @see https://next-auth.js.org/providers/github
|
|
||||||
*/
|
|
||||||
],
|
],
|
||||||
|
theme: {
|
||||||
|
logo: "/logo.svg",
|
||||||
|
brandColor: "#ff5733",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
19
src/server/utils/userOrg.ts
Normal file
19
src/server/utils/userOrg.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { prisma } from "~/server/db";
|
||||||
|
|
||||||
|
export default async function userOrg(userId: string) {
|
||||||
|
return await prisma.organization.upsert({
|
||||||
|
where: {
|
||||||
|
personalOrgUserId: userId,
|
||||||
|
},
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
personalOrgUserId: userId,
|
||||||
|
OrganizationUser: {
|
||||||
|
create: {
|
||||||
|
userId: userId,
|
||||||
|
role: "ADMIN",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
49
src/utils/accessControl.ts
Normal file
49
src/utils/accessControl.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { OrganizationUserRole } from "@prisma/client";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { type TRPCContext } from "~/server/api/trpc";
|
||||||
|
import { prisma } from "~/server/db";
|
||||||
|
|
||||||
|
// No-op method for protected routes that really should be accessible to anyone.
|
||||||
|
export const requireNothing = (ctx: TRPCContext) => {
|
||||||
|
ctx.markAccessControlRun();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requireCanViewExperiment = async (experimentId: string, ctx: TRPCContext) => {
|
||||||
|
await prisma.experiment.findFirst({
|
||||||
|
where: { id: experimentId },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Right now all experiments are publicly viewable, so this is a no-op.
|
||||||
|
ctx.markAccessControlRun();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const canModifyExperiment = async (experimentId: string, userId: string) => {
|
||||||
|
const experiment = await prisma.experiment.findFirst({
|
||||||
|
where: {
|
||||||
|
id: experimentId,
|
||||||
|
organization: {
|
||||||
|
OrganizationUser: {
|
||||||
|
some: {
|
||||||
|
role: { in: [OrganizationUserRole.ADMIN, OrganizationUserRole.MEMBER] },
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!experiment;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requireCanModifyExperiment = async (experimentId: string, ctx: TRPCContext) => {
|
||||||
|
const userId = ctx.session?.user.id;
|
||||||
|
if (!userId) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await canModifyExperiment(experimentId, userId))) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.markAccessControlRun();
|
||||||
|
};
|
||||||
@@ -12,6 +12,10 @@ export const useExperiment = () => {
|
|||||||
return experiment;
|
return experiment;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useExperimentAccess = () => {
|
||||||
|
return useExperiment().data?.access ?? { canView: false, canModify: false };
|
||||||
|
};
|
||||||
|
|
||||||
type AsyncFunction<T extends unknown[], U> = (...args: T) => Promise<U>;
|
type AsyncFunction<T extends unknown[], U> = (...args: T) => Promise<U>;
|
||||||
|
|
||||||
export function useHandledAsyncCallback<T extends unknown[], U>(
|
export function useHandledAsyncCallback<T extends unknown[], U>(
|
||||||
|
|||||||
Reference in New Issue
Block a user