Compare commits
56 Commits
pause-cham
...
move-api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
754e273049 | ||
|
|
c4cef35717 | ||
|
|
8552baf632 | ||
|
|
f41e2229ca | ||
|
|
e649f42c9c | ||
|
|
99f305483b | ||
|
|
b28f4cad57 | ||
|
|
df4a3a0950 | ||
|
|
e423ad656a | ||
|
|
7d0d94de3a | ||
|
|
344b257db4 | ||
|
|
28b43b6e6d | ||
|
|
8d373ec9b5 | ||
|
|
537525667d | ||
|
|
519367c553 | ||
|
|
1a338ec863 | ||
|
|
01d0b8f778 | ||
|
|
d99836ec30 | ||
|
|
33751c12d2 | ||
|
|
89815e1f7f | ||
|
|
5fa5109f34 | ||
|
|
b06ab2cbf9 | ||
|
|
35fb554038 | ||
|
|
f238177277 | ||
|
|
723c0f7505 | ||
|
|
ce6936f753 | ||
|
|
2a80cbf74a | ||
|
|
098805ef25 | ||
|
|
ed90bc5a99 | ||
|
|
de9be8c7ce | ||
|
|
3e02bcf9b8 | ||
|
|
cef2ee31fb | ||
|
|
d7cff0f52e | ||
|
|
228c547839 | ||
|
|
e1fcc8fb38 | ||
|
|
8ed47eb4dd | ||
|
|
3a908d51aa | ||
|
|
d9db6d80ea | ||
|
|
8d1ee62ff1 | ||
|
|
f270579283 | ||
|
|
81fbaeae44 | ||
|
|
5277afa199 | ||
|
|
76c34d64e6 | ||
|
|
454ac9a0d3 | ||
|
|
5ed7adadf9 | ||
|
|
b8e0f392ab | ||
|
|
b2af83341d | ||
|
|
e6d229d5f9 | ||
|
|
1a6ae3aef7 | ||
|
|
9051d80775 | ||
|
|
6c060c6ea0 | ||
|
|
f70e73e338 | ||
|
|
16aa6672fc | ||
|
|
ac99c8e0f7 | ||
|
|
df121db78c | ||
|
|
f09dfe18be |
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
**/node_modules/
|
||||||
|
.git
|
||||||
|
**/.venv/
|
||||||
|
**/.env*
|
||||||
|
**/.next/
|
||||||
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.env
|
||||||
|
.venv/
|
||||||
|
*.pyc
|
||||||
|
node_modules/
|
||||||
|
*.tsbuildinfo
|
||||||
@@ -65,7 +65,14 @@ OpenPipe includes a tool to generate new test scenarios based on your existing p
|
|||||||
4. Clone this repository: `git clone https://github.com/openpipe/openpipe`
|
4. Clone this repository: `git clone https://github.com/openpipe/openpipe`
|
||||||
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 migrate dev` to create the database.
|
||||||
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!)
|
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. Start the app: `pnpm dev`.
|
9. Start the app: `pnpm dev`.
|
||||||
10. Navigate to [http://localhost:3000](http://localhost:3000)
|
10. Navigate to [http://localhost:3000](http://localhost:3000)
|
||||||
|
|
||||||
|
## Testing Locally
|
||||||
|
|
||||||
|
1. Copy your `.env` file to `.env.test`.
|
||||||
|
2. Update the `DATABASE_URL` to have a different database name than your development one
|
||||||
|
3. Run `DATABASE_URL=[your new datatase url] pnpm prisma migrate dev --skip-seed --skip-generate`
|
||||||
|
4. Run `pnpm test`
|
||||||
@@ -32,5 +32,5 @@ NEXT_PUBLIC_HOST="http://localhost:3000"
|
|||||||
GITHUB_CLIENT_ID="your_client_id"
|
GITHUB_CLIENT_ID="your_client_id"
|
||||||
GITHUB_CLIENT_SECRET="your_secret"
|
GITHUB_CLIENT_SECRET="your_secret"
|
||||||
|
|
||||||
OPENPIPE_BASE_URL="http://localhost:3000/api"
|
OPENPIPE_BASE_URL="http://localhost:3000/api/v1"
|
||||||
OPENPIPE_API_KEY="your_key"
|
OPENPIPE_API_KEY="your_key"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const config = {
|
|||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
extends: ["plugin:@typescript-eslint/recommended-requiring-type-checking"],
|
extends: ["plugin:@typescript-eslint/recommended-requiring-type-checking"],
|
||||||
files: ["*.ts", "*.tsx"],
|
files: ["*.mts", "*.ts", "*.tsx"],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: path.join(__dirname, "tsconfig.json"),
|
project: path.join(__dirname, "tsconfig.json"),
|
||||||
},
|
},
|
||||||
|
|||||||
4
app/.gitignore
vendored
@@ -34,6 +34,7 @@ yarn-error.log*
|
|||||||
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
|
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
|
||||||
.env
|
.env
|
||||||
.env*.local
|
.env*.local
|
||||||
|
.env.test
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
@@ -43,3 +44,6 @@ yarn-error.log*
|
|||||||
|
|
||||||
# Sentry Auth Token
|
# Sentry Auth Token
|
||||||
.sentryclirc
|
.sentryclirc
|
||||||
|
|
||||||
|
# custom openai intialization
|
||||||
|
src/server/utils/openaiCustomConfig.json
|
||||||
|
|||||||
8
app/@types/nextjs-routes.d.ts
vendored
@@ -12,19 +12,19 @@ declare module "nextjs-routes" {
|
|||||||
|
|
||||||
export type Route =
|
export type Route =
|
||||||
| StaticRoute<"/account/signin">
|
| StaticRoute<"/account/signin">
|
||||||
| DynamicRoute<"/api/[...trpc]", { "trpc": string[] }>
|
|
||||||
| DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }>
|
| DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }>
|
||||||
| StaticRoute<"/api/experiments/og-image">
|
| StaticRoute<"/api/experiments/og-image">
|
||||||
| StaticRoute<"/api/openapi">
|
|
||||||
| StaticRoute<"/api/sentry-example-api">
|
|
||||||
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
|
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
|
||||||
|
| DynamicRoute<"/api/v1/[...trpc]", { "trpc": string[] }>
|
||||||
|
| StaticRoute<"/api/v1/openapi">
|
||||||
|
| StaticRoute<"/dashboard">
|
||||||
| DynamicRoute<"/data/[id]", { "id": string }>
|
| DynamicRoute<"/data/[id]", { "id": string }>
|
||||||
| StaticRoute<"/data">
|
| StaticRoute<"/data">
|
||||||
| DynamicRoute<"/experiments/[id]", { "id": string }>
|
| DynamicRoute<"/experiments/[id]", { "id": string }>
|
||||||
| StaticRoute<"/experiments">
|
| StaticRoute<"/experiments">
|
||||||
| StaticRoute<"/">
|
| StaticRoute<"/">
|
||||||
| StaticRoute<"/logged-calls">
|
|
||||||
| StaticRoute<"/project/settings">
|
| StaticRoute<"/project/settings">
|
||||||
|
| StaticRoute<"/request-logs">
|
||||||
| StaticRoute<"/sentry-example-page">
|
| StaticRoute<"/sentry-example-page">
|
||||||
| StaticRoute<"/world-champs">
|
| StaticRoute<"/world-champs">
|
||||||
| StaticRoute<"/world-champs/signup">;
|
| StaticRoute<"/world-champs/signup">;
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ RUN yarn global add pnpm
|
|||||||
# DEPS
|
# DEPS
|
||||||
FROM base as deps
|
FROM base as deps
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /code
|
||||||
|
|
||||||
COPY prisma ./
|
COPY app/prisma app/package.json ./app/
|
||||||
|
COPY client-libs/typescript/package.json ./client-libs/typescript/
|
||||||
|
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
|
||||||
COPY package.json pnpm-lock.yaml ./
|
RUN cd app && pnpm install --frozen-lockfile
|
||||||
|
|
||||||
RUN pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
# BUILDER
|
# BUILDER
|
||||||
FROM base as builder
|
FROM base as builder
|
||||||
@@ -25,22 +25,24 @@ ARG NEXT_PUBLIC_SENTRY_DSN
|
|||||||
ARG SENTRY_AUTH_TOKEN
|
ARG SENTRY_AUTH_TOKEN
|
||||||
ARG NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS
|
ARG NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /code
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /code/node_modules ./node_modules
|
||||||
|
COPY --from=deps /code/app/node_modules ./app/node_modules
|
||||||
|
COPY --from=deps /code/client-libs/typescript/node_modules ./client-libs/typescript/node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN SKIP_ENV_VALIDATION=1 pnpm build
|
RUN cd app && SKIP_ENV_VALIDATION=1 pnpm build
|
||||||
|
|
||||||
# RUNNER
|
# RUNNER
|
||||||
FROM base as runner
|
FROM base as runner
|
||||||
WORKDIR /app
|
WORKDIR /code/app
|
||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
COPY --from=builder /app/ ./
|
COPY --from=builder /code/ /code/
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
ENV PORT 3000
|
ENV PORT 3000
|
||||||
|
|
||||||
# Run the "run-prod.sh" script
|
# Run the "run-prod.sh" script
|
||||||
CMD /app/run-prod.sh
|
CMD /code/app/run-prod.sh
|
||||||
@@ -36,6 +36,8 @@ let config = {
|
|||||||
});
|
});
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
transpilePackages: ["openpipe"],
|
||||||
};
|
};
|
||||||
|
|
||||||
config = nextRoutes()(config);
|
config = nextRoutes()(config);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "openpipe",
|
"name": "openpipe-app",
|
||||||
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
@@ -16,15 +17,14 @@
|
|||||||
"postinstall": "prisma generate",
|
"postinstall": "prisma generate",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"codegen": "tsx src/server/scripts/client-codegen.ts",
|
"codegen:clients": "tsx src/server/scripts/client-codegen.ts",
|
||||||
"seed": "tsx prisma/seed.ts",
|
"seed": "tsx prisma/seed.ts",
|
||||||
"check": "concurrently 'pnpm lint' 'pnpm tsc' 'pnpm prettier . --check'",
|
"check": "concurrently 'pnpm lint' 'pnpm tsc' 'pnpm prettier . --check'",
|
||||||
"test": "pnpm vitest --no-threads"
|
"test": "pnpm vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.5.8",
|
"@anthropic-ai/sdk": "^0.5.8",
|
||||||
"@apidevtools/json-schema-ref-parser": "^10.1.0",
|
"@apidevtools/json-schema-ref-parser": "^10.1.0",
|
||||||
"@babel/preset-typescript": "^7.22.5",
|
|
||||||
"@babel/standalone": "^7.22.9",
|
"@babel/standalone": "^7.22.9",
|
||||||
"@chakra-ui/anatomy": "^2.2.0",
|
"@chakra-ui/anatomy": "^2.2.0",
|
||||||
"@chakra-ui/next-js": "^2.1.4",
|
"@chakra-ui/next-js": "^2.1.4",
|
||||||
@@ -100,7 +100,8 @@
|
|||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"vite-tsconfig-paths": "^4.2.0",
|
"vite-tsconfig-paths": "^4.2.0",
|
||||||
"zod": "^3.21.4",
|
"zod": "^3.21.4",
|
||||||
"zustand": "^4.3.9"
|
"zustand": "^4.3.9",
|
||||||
|
"openpipe": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@openapi-contrib/openapi-schema-to-json-schema": "^4.0.5",
|
"@openapi-contrib/openapi-schema-to-json-schema": "^4.0.5",
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
-- Rename Enum
|
||||||
|
ALTER TYPE "OrganizationUserRole" RENAME TO "ProjectUserRole";
|
||||||
|
|
||||||
|
-- Drop and recreate foreign keys
|
||||||
|
ALTER TABLE "ApiKey" DROP CONSTRAINT "ApiKey_organizationId_fkey";
|
||||||
|
ALTER TABLE "Dataset" DROP CONSTRAINT "Dataset_organizationId_fkey";
|
||||||
|
ALTER TABLE "Experiment" DROP CONSTRAINT "Experiment_organizationId_fkey";
|
||||||
|
ALTER TABLE "LoggedCall" DROP CONSTRAINT "LoggedCall_organizationId_fkey";
|
||||||
|
ALTER TABLE "OrganizationUser" DROP CONSTRAINT "OrganizationUser_organizationId_fkey";
|
||||||
|
ALTER TABLE "OrganizationUser" DROP CONSTRAINT "OrganizationUser_userId_fkey";
|
||||||
|
|
||||||
|
-- Rename columns
|
||||||
|
ALTER TABLE "ApiKey" RENAME COLUMN "organizationId" TO "projectId";
|
||||||
|
ALTER TABLE "Dataset" RENAME COLUMN "organizationId" TO "projectId";
|
||||||
|
ALTER TABLE "Experiment" RENAME COLUMN "organizationId" TO "projectId";
|
||||||
|
ALTER TABLE "LoggedCall" RENAME COLUMN "organizationId" TO "projectId";
|
||||||
|
ALTER TABLE "OrganizationUser" RENAME COLUMN "organizationId" TO "projectId";
|
||||||
|
ALTER TABLE "Organization" RENAME COLUMN "personalOrgUserId" TO "personalProjectUserId";
|
||||||
|
|
||||||
|
-- Rename table
|
||||||
|
ALTER TABLE "Organization" RENAME TO "Project";
|
||||||
|
ALTER TABLE "OrganizationUser" RENAME TO "ProjectUser";
|
||||||
|
|
||||||
|
-- Recreate foreign keys
|
||||||
|
ALTER TABLE "Experiment" ADD CONSTRAINT "Experiment_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "Dataset" ADD CONSTRAINT "Dataset_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "ProjectUser" ADD CONSTRAINT "ProjectUser_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "ProjectUser" ADD CONSTRAINT "ProjectUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "LoggedCall" ADD CONSTRAINT "LoggedCall_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Rename indexes
|
||||||
|
ALTER TABLE "Project" RENAME CONSTRAINT "Organization_pkey" TO "Project_pkey";
|
||||||
|
ALTER TABLE "ProjectUser" RENAME CONSTRAINT "OrganizationUser_pkey" TO "ProjectUser_pkey";
|
||||||
|
ALTER TABLE "Project" RENAME CONSTRAINT "Organization_personalOrgUserId_fkey" TO "Project_personalProjectUserId_fkey";
|
||||||
|
ALTER INDEX "Organization_personalOrgUserId_key" RENAME TO "Project_personalProjectUserId_key";
|
||||||
|
ALTER INDEX "OrganizationUser_organizationId_userId_key" RENAME TO "ProjectUser_projectId_userId_key";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to rename the column `completionTokens` to `outputTokens` on the `ModelResponse` table.
|
||||||
|
- You are about to rename the column `promptTokens` to `inputTokens` on the `ModelResponse` table.
|
||||||
|
- You are about to rename the column `startTime` on the `LoggedCall` table to `requestedAt`. Ensure compatibility with application logic.
|
||||||
|
- You are about to rename the column `startTime` on the `LoggedCallModelResponse` table to `requestedAt`. Ensure compatibility with application logic.
|
||||||
|
- You are about to rename the column `endTime` on the `LoggedCallModelResponse` table to `receivedAt`. Ensure compatibility with application logic.
|
||||||
|
- You are about to rename the column `error` on the `LoggedCallModelResponse` table to `errorMessage`. Ensure compatibility with application logic.
|
||||||
|
- You are about to rename the column `respStatus` on the `LoggedCallModelResponse` table to `statusCode`. Ensure compatibility with application logic.
|
||||||
|
- You are about to rename the column `totalCost` on the `LoggedCallModelResponse` table to `cost`. Ensure compatibility with application logic.
|
||||||
|
- You are about to rename the column `inputHash` on the `ModelResponse` table to `cacheKey`. Ensure compatibility with application logic.
|
||||||
|
- You are about to rename the column `output` on the `ModelResponse` table to `respPayload`. Ensure compatibility with application logic.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "LoggedCall_startTime_idx";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "ModelResponse_inputHash_idx";
|
||||||
|
|
||||||
|
-- Rename completionTokens to outputTokens
|
||||||
|
ALTER TABLE "ModelResponse"
|
||||||
|
RENAME COLUMN "completionTokens" TO "outputTokens";
|
||||||
|
|
||||||
|
-- Rename promptTokens to inputTokens
|
||||||
|
ALTER TABLE "ModelResponse"
|
||||||
|
RENAME COLUMN "promptTokens" TO "inputTokens";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LoggedCall"
|
||||||
|
RENAME COLUMN "startTime" TO "requestedAt";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LoggedCallModelResponse"
|
||||||
|
RENAME COLUMN "startTime" TO "requestedAt";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LoggedCallModelResponse"
|
||||||
|
RENAME COLUMN "endTime" TO "receivedAt";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LoggedCallModelResponse"
|
||||||
|
RENAME COLUMN "error" TO "errorMessage";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LoggedCallModelResponse"
|
||||||
|
RENAME COLUMN "respStatus" TO "statusCode";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LoggedCallModelResponse"
|
||||||
|
RENAME COLUMN "totalCost" TO "cost";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ModelResponse"
|
||||||
|
RENAME COLUMN "inputHash" TO "cacheKey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ModelResponse"
|
||||||
|
RENAME COLUMN "output" TO "respPayload";
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "LoggedCall_requestedAt_idx" ON "LoggedCall"("requestedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ModelResponse_cacheKey_idx" ON "ModelResponse"("cacheKey");
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LoggedCall" ADD COLUMN "model" TEXT;
|
||||||
@@ -16,8 +16,8 @@ model Experiment {
|
|||||||
|
|
||||||
sortIndex Int @default(0)
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
organizationId String @db.Uuid
|
projectId String @db.Uuid
|
||||||
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -112,17 +112,17 @@ model ScenarioVariantCell {
|
|||||||
model ModelResponse {
|
model ModelResponse {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
inputHash String
|
cacheKey String
|
||||||
requestedAt DateTime?
|
requestedAt DateTime?
|
||||||
receivedAt DateTime?
|
receivedAt DateTime?
|
||||||
output Json?
|
respPayload Json?
|
||||||
cost Float?
|
cost Float?
|
||||||
promptTokens Int?
|
inputTokens Int?
|
||||||
completionTokens Int?
|
outputTokens Int?
|
||||||
statusCode Int?
|
statusCode Int?
|
||||||
errorMessage String?
|
errorMessage String?
|
||||||
retryTime DateTime?
|
retryTime DateTime?
|
||||||
outdated Boolean @default(false)
|
outdated Boolean @default(false)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -131,7 +131,7 @@ model ModelResponse {
|
|||||||
scenarioVariantCell ScenarioVariantCell @relation(fields: [scenarioVariantCellId], references: [id], onDelete: Cascade)
|
scenarioVariantCell ScenarioVariantCell @relation(fields: [scenarioVariantCellId], references: [id], onDelete: Cascade)
|
||||||
outputEvaluations OutputEvaluation[]
|
outputEvaluations OutputEvaluation[]
|
||||||
|
|
||||||
@@index([inputHash])
|
@@index([cacheKey])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum EvalType {
|
enum EvalType {
|
||||||
@@ -180,8 +180,8 @@ model Dataset {
|
|||||||
name String
|
name String
|
||||||
datasetEntries DatasetEntry[]
|
datasetEntries DatasetEntry[]
|
||||||
|
|
||||||
organizationId String @db.Uuid
|
projectId String @db.Uuid
|
||||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -200,36 +200,35 @@ model DatasetEntry {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO rename Organization to Project
|
model Project {
|
||||||
model Organization {
|
id String @id @default(uuid()) @db.Uuid
|
||||||
id String @id @default(uuid()) @db.Uuid
|
name String @default("Project 1")
|
||||||
name String @default("Project 1")
|
|
||||||
|
|
||||||
personalOrgUserId String? @unique @db.Uuid
|
personalProjectUserId String? @unique @db.Uuid
|
||||||
personalOrgUser User? @relation(fields: [personalOrgUserId], references: [id], onDelete: Cascade)
|
personalProjectUser User? @relation(fields: [personalProjectUserId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
organizationUsers OrganizationUser[]
|
projectUsers ProjectUser[]
|
||||||
experiments Experiment[]
|
experiments Experiment[]
|
||||||
datasets Dataset[]
|
datasets Dataset[]
|
||||||
loggedCalls LoggedCall[]
|
loggedCalls LoggedCall[]
|
||||||
apiKeys ApiKey[]
|
apiKeys ApiKey[]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum OrganizationUserRole {
|
enum ProjectUserRole {
|
||||||
ADMIN
|
ADMIN
|
||||||
MEMBER
|
MEMBER
|
||||||
VIEWER
|
VIEWER
|
||||||
}
|
}
|
||||||
|
|
||||||
model OrganizationUser {
|
model ProjectUser {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
role OrganizationUserRole
|
role ProjectUserRole
|
||||||
|
|
||||||
organizationId String @db.Uuid
|
projectId String @db.Uuid
|
||||||
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
userId String @db.Uuid
|
userId String @db.Uuid
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
@@ -237,7 +236,7 @@ model OrganizationUser {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@unique([organizationId, userId])
|
@@unique([projectId, userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model WorldChampEntrant {
|
model WorldChampEntrant {
|
||||||
@@ -257,7 +256,7 @@ model WorldChampEntrant {
|
|||||||
model LoggedCall {
|
model LoggedCall {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
startTime DateTime
|
requestedAt DateTime
|
||||||
|
|
||||||
// True if this call was served from the cache, false otherwise
|
// True if this call was served from the cache, false otherwise
|
||||||
cacheHit Boolean
|
cacheHit Boolean
|
||||||
@@ -265,21 +264,22 @@ model LoggedCall {
|
|||||||
// A LoggedCall is always associated with a LoggedCallModelResponse. If this
|
// A LoggedCall is always associated with a LoggedCallModelResponse. If this
|
||||||
// is a cache miss, we create a new LoggedCallModelResponse.
|
// is a cache miss, we create a new LoggedCallModelResponse.
|
||||||
// If it's a cache hit, it's a pre-existing LoggedCallModelResponse.
|
// If it's a cache hit, it's a pre-existing LoggedCallModelResponse.
|
||||||
modelResponseId String? @db.Uuid
|
modelResponseId String? @db.Uuid
|
||||||
modelResponse LoggedCallModelResponse? @relation(fields: [modelResponseId], references: [id], onDelete: Cascade)
|
modelResponse LoggedCallModelResponse? @relation(fields: [modelResponseId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
// The responses created by this LoggedCall. Will be empty if this LoggedCall was a cache hit.
|
// The responses created by this LoggedCall. Will be empty if this LoggedCall was a cache hit.
|
||||||
createdResponses LoggedCallModelResponse[] @relation(name: "ModelResponseOriginalCall")
|
createdResponses LoggedCallModelResponse[] @relation(name: "ModelResponseOriginalCall")
|
||||||
|
|
||||||
organizationId String @db.Uuid
|
projectId String @db.Uuid
|
||||||
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
tags LoggedCallTag[]
|
model String?
|
||||||
|
tags LoggedCallTag[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@index([startTime])
|
@@index([requestedAt])
|
||||||
}
|
}
|
||||||
|
|
||||||
model LoggedCallModelResponse {
|
model LoggedCallModelResponse {
|
||||||
@@ -288,14 +288,14 @@ model LoggedCallModelResponse {
|
|||||||
reqPayload Json
|
reqPayload Json
|
||||||
|
|
||||||
// The HTTP status returned by the model provider
|
// The HTTP status returned by the model provider
|
||||||
respStatus Int?
|
statusCode Int?
|
||||||
respPayload Json?
|
respPayload Json?
|
||||||
|
|
||||||
// Should be null if the request was successful, and some string if the request failed.
|
// Should be null if the request was successful, and some string if the request failed.
|
||||||
error String?
|
errorMessage String?
|
||||||
|
|
||||||
startTime DateTime
|
requestedAt DateTime
|
||||||
endTime DateTime
|
receivedAt DateTime
|
||||||
|
|
||||||
// Note: the function to calculate the cacheKey should include the project
|
// Note: the function to calculate the cacheKey should include the project
|
||||||
// ID so we don't share cached responses between projects, which could be an
|
// ID so we don't share cached responses between projects, which could be an
|
||||||
@@ -309,7 +309,7 @@ model LoggedCallModelResponse {
|
|||||||
outputTokens Int?
|
outputTokens Int?
|
||||||
finishReason String?
|
finishReason String?
|
||||||
completionId String?
|
completionId String?
|
||||||
totalCost Decimal? @db.Decimal(18, 12)
|
cost Decimal? @db.Decimal(18, 12)
|
||||||
|
|
||||||
// The LoggedCall that created this LoggedCallModelResponse
|
// The LoggedCall that created this LoggedCallModelResponse
|
||||||
originalLoggedCallId String @unique @db.Uuid
|
originalLoggedCallId String @unique @db.Uuid
|
||||||
@@ -323,11 +323,11 @@ model LoggedCallModelResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model LoggedCallTag {
|
model LoggedCallTag {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
name String
|
name String
|
||||||
value String?
|
value String?
|
||||||
|
|
||||||
loggedCallId String @db.Uuid
|
loggedCallId String @db.Uuid
|
||||||
loggedCall LoggedCall @relation(fields: [loggedCallId], references: [id], onDelete: Cascade)
|
loggedCall LoggedCall @relation(fields: [loggedCallId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@index([name])
|
@@index([name])
|
||||||
@@ -340,8 +340,8 @@ model ApiKey {
|
|||||||
name String
|
name String
|
||||||
apiKey String @unique
|
apiKey String @unique
|
||||||
|
|
||||||
organizationId String @db.Uuid
|
projectId String @db.Uuid
|
||||||
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -390,8 +390,8 @@ model User {
|
|||||||
|
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
organizationUsers OrganizationUser[]
|
projectUsers ProjectUser[]
|
||||||
organizations Organization[]
|
projects Project[]
|
||||||
worldChampEntrant WorldChampEntrant?
|
worldChampEntrant WorldChampEntrant?
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|||||||
@@ -2,20 +2,31 @@ 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 { promptConstructorVersion } from "~/promptConstructor/version";
|
import { promptConstructorVersion } from "~/promptConstructor/version";
|
||||||
|
import { env } from "~/env.mjs";
|
||||||
|
|
||||||
const defaultId = "11111111-1111-1111-1111-111111111111";
|
const defaultId = "11111111-1111-1111-1111-111111111111";
|
||||||
|
|
||||||
await prisma.organization.deleteMany({
|
await prisma.project.deleteMany({
|
||||||
where: { id: defaultId },
|
where: { id: defaultId },
|
||||||
});
|
});
|
||||||
|
|
||||||
// If there's an existing org, just seed into it
|
// If there's an existing project, just seed into it
|
||||||
const org =
|
const project =
|
||||||
(await prisma.organization.findFirst({})) ??
|
(await prisma.project.findFirst({})) ??
|
||||||
(await prisma.organization.create({
|
(await prisma.project.create({
|
||||||
data: { id: defaultId },
|
data: { id: defaultId },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (env.OPENPIPE_API_KEY) {
|
||||||
|
await prisma.apiKey.create({
|
||||||
|
data: {
|
||||||
|
projectId: project.id,
|
||||||
|
name: "Default API Key",
|
||||||
|
apiKey: env.OPENPIPE_API_KEY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.experiment.deleteMany({
|
await prisma.experiment.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: defaultId,
|
id: defaultId,
|
||||||
@@ -26,7 +37,7 @@ await prisma.experiment.create({
|
|||||||
data: {
|
data: {
|
||||||
id: defaultId,
|
id: defaultId,
|
||||||
label: "Country Capitals Example",
|
label: "Country Capitals Example",
|
||||||
organizationId: org.id,
|
projectId: project.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import { promptConstructorVersion } from "~/promptConstructor/version";
|
|||||||
|
|
||||||
const defaultId = "11111111-1111-1111-1111-111111111112";
|
const defaultId = "11111111-1111-1111-1111-111111111112";
|
||||||
|
|
||||||
await prisma.organization.deleteMany({
|
await prisma.project.deleteMany({
|
||||||
where: { id: defaultId },
|
where: { id: defaultId },
|
||||||
});
|
});
|
||||||
|
|
||||||
// If there's an existing org, just seed into it
|
// If there's an existing project, just seed into it
|
||||||
const org =
|
const project =
|
||||||
(await prisma.organization.findFirst({})) ??
|
(await prisma.project.findFirst({})) ??
|
||||||
(await prisma.organization.create({
|
(await prisma.project.create({
|
||||||
data: { id: defaultId },
|
data: { id: defaultId },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ for (const dataset of datasets) {
|
|||||||
const oldExperiment = await prisma.experiment.findFirst({
|
const oldExperiment = await prisma.experiment.findFirst({
|
||||||
where: {
|
where: {
|
||||||
label: experimentName,
|
label: experimentName,
|
||||||
organizationId: org.id,
|
projectId: project.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (oldExperiment) {
|
if (oldExperiment) {
|
||||||
@@ -60,7 +60,7 @@ for (const dataset of datasets) {
|
|||||||
data: {
|
data: {
|
||||||
id: oldExperiment?.id ?? undefined,
|
id: oldExperiment?.id ?? undefined,
|
||||||
label: experimentName,
|
label: experimentName,
|
||||||
organizationId: org.id,
|
projectId: project.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -311,9 +311,9 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
|
|
||||||
await prisma.loggedCallModelResponse.deleteMany();
|
await prisma.loggedCallModelResponse.deleteMany();
|
||||||
|
|
||||||
const org = await prisma.organization.findFirst({
|
const project = await prisma.project.findFirst({
|
||||||
where: {
|
where: {
|
||||||
personalOrgUserId: {
|
personalProjectUserId: {
|
||||||
not: null,
|
not: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -322,8 +322,8 @@ const org = await prisma.organization.findFirst({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!org) {
|
if (!project) {
|
||||||
console.error("No org found. Sign up to create your first org.");
|
console.error("No project found. Sign up to create your first project.");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,17 +339,17 @@ for (let i = 0; i < 1437; i++) {
|
|||||||
MODEL_RESPONSE_TEMPLATES[Math.floor(Math.random() * MODEL_RESPONSE_TEMPLATES.length)]!;
|
MODEL_RESPONSE_TEMPLATES[Math.floor(Math.random() * MODEL_RESPONSE_TEMPLATES.length)]!;
|
||||||
const model = template.reqPayload.model;
|
const model = template.reqPayload.model;
|
||||||
// choose random time in the last two weeks, with a bias towards the last few days
|
// choose random time in the last two weeks, with a bias towards the last few days
|
||||||
const startTime = new Date(Date.now() - Math.pow(Math.random(), 2) * 1000 * 60 * 60 * 24 * 14);
|
const requestedAt = new Date(Date.now() - Math.pow(Math.random(), 2) * 1000 * 60 * 60 * 24 * 14);
|
||||||
// choose random delay anywhere from 2 to 10 seconds later for gpt-4, or 1 to 5 seconds for gpt-3.5
|
// choose random delay anywhere from 2 to 10 seconds later for gpt-4, or 1 to 5 seconds for gpt-3.5
|
||||||
const delay =
|
const delay =
|
||||||
model === "gpt-4" ? 1000 * 2 + Math.random() * 1000 * 8 : 1000 + Math.random() * 1000 * 4;
|
model === "gpt-4" ? 1000 * 2 + Math.random() * 1000 * 8 : 1000 + Math.random() * 1000 * 4;
|
||||||
const endTime = new Date(startTime.getTime() + delay);
|
const receivedAt = new Date(requestedAt.getTime() + delay);
|
||||||
loggedCallsToCreate.push({
|
loggedCallsToCreate.push({
|
||||||
id: loggedCallId,
|
id: loggedCallId,
|
||||||
cacheHit: false,
|
cacheHit: false,
|
||||||
startTime,
|
requestedAt,
|
||||||
organizationId: org.id,
|
projectId: project.id,
|
||||||
createdAt: startTime,
|
createdAt: requestedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { promptTokenPrice, completionTokenPrice } =
|
const { promptTokenPrice, completionTokenPrice } =
|
||||||
@@ -365,21 +365,20 @@ for (let i = 0; i < 1437; i++) {
|
|||||||
|
|
||||||
loggedCallModelResponsesToCreate.push({
|
loggedCallModelResponsesToCreate.push({
|
||||||
id: loggedCallModelResponseId,
|
id: loggedCallModelResponseId,
|
||||||
startTime,
|
requestedAt,
|
||||||
endTime,
|
receivedAt,
|
||||||
originalLoggedCallId: loggedCallId,
|
originalLoggedCallId: loggedCallId,
|
||||||
reqPayload: template.reqPayload,
|
reqPayload: template.reqPayload,
|
||||||
respPayload: template.respPayload,
|
respPayload: template.respPayload,
|
||||||
respStatus: template.respStatus,
|
statusCode: template.respStatus,
|
||||||
error: template.error,
|
errorMessage: template.error,
|
||||||
createdAt: startTime,
|
createdAt: requestedAt,
|
||||||
cacheKey: hashRequest(org.id, template.reqPayload as JsonValue),
|
cacheKey: hashRequest(project.id, template.reqPayload as JsonValue),
|
||||||
durationMs: endTime.getTime() - startTime.getTime(),
|
durationMs: receivedAt.getTime() - requestedAt.getTime(),
|
||||||
inputTokens: template.inputTokens,
|
inputTokens: template.inputTokens,
|
||||||
outputTokens: template.outputTokens,
|
outputTokens: template.outputTokens,
|
||||||
finishReason: template.finishReason,
|
finishReason: template.finishReason,
|
||||||
totalCost:
|
cost: template.inputTokens * promptTokenPrice + template.outputTokens * completionTokenPrice,
|
||||||
template.inputTokens * promptTokenPrice + template.outputTokens * completionTokenPrice,
|
|
||||||
});
|
});
|
||||||
loggedCallsToUpdate.push({
|
loggedCallsToUpdate.push({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import { promptConstructorVersion } from "~/promptConstructor/version";
|
|||||||
|
|
||||||
const defaultId = "11111111-1111-1111-1111-111111111112";
|
const defaultId = "11111111-1111-1111-1111-111111111112";
|
||||||
|
|
||||||
await prisma.organization.deleteMany({
|
await prisma.project.deleteMany({
|
||||||
where: { id: defaultId },
|
where: { id: defaultId },
|
||||||
});
|
});
|
||||||
|
|
||||||
// If there's an existing org, just seed into it
|
// If there's an existing project, just seed into it
|
||||||
const org =
|
const project =
|
||||||
(await prisma.organization.findFirst({})) ??
|
(await prisma.project.findFirst({})) ??
|
||||||
(await prisma.organization.create({
|
(await prisma.project.create({
|
||||||
data: { id: defaultId },
|
data: { id: defaultId },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ const experimentName = `Twitter Sentiment Analysis`;
|
|||||||
const oldExperiment = await prisma.experiment.findFirst({
|
const oldExperiment = await prisma.experiment.findFirst({
|
||||||
where: {
|
where: {
|
||||||
label: experimentName,
|
label: experimentName,
|
||||||
organizationId: org.id,
|
projectId: project.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (oldExperiment) {
|
if (oldExperiment) {
|
||||||
@@ -40,7 +40,7 @@ const experiment = await prisma.experiment.create({
|
|||||||
data: {
|
data: {
|
||||||
id: oldExperiment?.id ?? undefined,
|
id: oldExperiment?.id ?? undefined,
|
||||||
label: experimentName,
|
label: experimentName,
|
||||||
organizationId: org.id,
|
projectId: project.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 800 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.4 KiB |
@@ -9,10 +9,9 @@ Created by potrace 1.14, written by Peter Selinger 2001-2017
|
|||||||
</metadata>
|
</metadata>
|
||||||
<g transform="translate(0.000000,550.000000) scale(0.100000,-0.100000)"
|
<g transform="translate(0.000000,550.000000) scale(0.100000,-0.100000)"
|
||||||
fill="#000000" stroke="none">
|
fill="#000000" stroke="none">
|
||||||
<path d="M813 5478 c-18 -13 -37 -36 -43 -52 -6 -19 -10 -236 -10 -603 0 -638
|
<path d="M785 5474 l-25 -27 0 -622 0 -622 25 -27 24 -26 171 0 170 0 0 -2050
|
||||||
-1 -626 65 -657 25 -12 67 -16 179 -16 l146 0 0 -2032 0 -2032 23 -33 c12 -18
|
0 -2051 25 -25 24 -24 1557 2 1556 3 19 24 c19 23 19 70 19 2072 l0 2049 169
|
||||||
35 -37 51 -43 19 -7 539 -10 1528 -10 1663 0 1549 -5 1582 65 14 30 16 235 16
|
0 c165 0 169 1 195 25 l26 24 0 626 0 626 -26 24 -27 25 -1939 0 -1939 0 -24
|
||||||
2059 l0 2026 156 0 156 0 39 39 39 39 0 587 c0 651 1 638 -65 669 -30 14 -223
|
-26z"/>
|
||||||
16 -1932 16 l-1898 0 -32 -22z"/>
|
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 858 B After Width: | Height: | Size: 755 B |
@@ -1,5 +1,28 @@
|
|||||||
<svg width="380" height="320" viewBox="0 0 380 320" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="398" height="550" viewBox="0 0 398 550" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M72 320L122.5 231L130.5 150.5L115 73L72 0H312L265 64.5L257 158.5L265 249L312 320H72Z" fill="#FF5733"/>
|
<path d="M39 125H359V542C359 546.418 355.418 550 351 550H47C42.5817 550 39 546.418 39 542V125Z" fill="black"/>
|
||||||
<path d="M67.027 9.5C72.9909 9.5 79.5196 12.3449 86.3672 19.2588C93.2495 26.2075 99.8845 36.7468 105.66 50.5336C117.194 78.0671 124.554 116.764 124.554 160C124.554 203.236 117.194 241.933 105.66 269.466C99.8845 283.253 93.2495 293.793 86.3672 300.741C79.5196 307.655 72.9909 310.5 67.027 310.5C61.0632 310.5 54.5345 307.655 47.6868 300.741C40.8045 293.793 34.1695 283.253 28.394 269.466C16.8596 241.933 9.5 203.236 9.5 160C9.5 116.764 16.8596 78.0671 28.394 50.5336C34.1695 36.7468 40.8045 26.2075 47.6868 19.2588C54.5345 12.3449 61.0632 9.5 67.027 9.5Z" stroke="#FF5733" stroke-width="19"/>
|
<path d="M0 8C0 3.58172 3.58172 0 8 0H390C394.418 0 398 3.58172 398 8V127C398 131.418 394.418 135 390 135H7.99999C3.58171 135 0 131.418 0 127V8Z" fill="black"/>
|
||||||
<path d="M312.027 9.5C317.991 9.5 324.52 12.3449 331.367 19.2588C338.25 26.2075 344.885 36.7468 350.66 50.5336C362.194 78.0671 369.554 116.764 369.554 160C369.554 203.236 362.194 241.933 350.66 269.466C344.885 283.253 338.25 293.793 331.367 300.741C324.52 307.655 317.991 310.5 312.027 310.5C306.063 310.5 299.534 307.655 292.687 300.741C285.805 293.793 279.17 283.253 273.394 269.466C261.86 241.933 254.5 203.236 254.5 160C254.5 116.764 261.86 78.0671 273.394 50.5336C279.17 36.7468 285.805 26.2075 292.687 19.2588C299.534 12.3449 306.063 9.5 312.027 9.5Z" stroke="#FF5733" stroke-width="19"/>
|
<path d="M50 135H348V535C348 537.209 346.209 539 344 539H54C51.7909 539 50 537.209 50 535V135Z" fill="#FF5733"/>
|
||||||
|
<path d="M11 14.0001C11 11.791 12.7909 10.0001 15 10.0001H384C386.209 10.0001 388 11.791 388 14.0001V120C388 122.209 386.209 124 384 124H15C12.7909 124 11 122.209 11 120V14.0001Z" fill="#FF5733"/>
|
||||||
|
<path d="M11 14.0001C11 11.791 12.7909 10.0001 15 10.0001H384C386.209 10.0001 388 11.791 388 14.0001V120C388 122.209 386.209 124 384 124H15C12.7909 124 11 122.209 11 120V14.0001Z" fill="url(#paint0_linear_102_49)"/>
|
||||||
|
<path d="M50 134H348V535C348 537.209 346.209 539 344 539H54C51.7909 539 50 537.209 50 535V134Z" fill="url(#paint1_linear_102_49)"/>
|
||||||
|
<path d="M108 142H156V535H108V142Z" fill="white"/>
|
||||||
|
<path d="M300 135H348V535C348 537.209 346.209 539 344 539H300V135Z" fill="white" fill-opacity="0.25"/>
|
||||||
|
<path d="M96 142H108V535H96V142Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M84 10.0001H133V120H84V10.0001Z" fill="white"/>
|
||||||
|
<path d="M339 10.0001H384C386.209 10.0001 388 11.791 388 14.0001V120C388 122.209 386.209 124 384 124H339V10.0001Z" fill="white" fill-opacity="0.25"/>
|
||||||
|
<path d="M71.9995 10.0001H83.9995V120H71.9995V10.0001Z" fill="white" fill-opacity="0.5"/>
|
||||||
|
<path d="M108 534.529H156V539.019H108V534.529Z" fill="#AAAAAA"/>
|
||||||
|
<path opacity="0.5" d="M95.9927 534.529H107.982V539.019H95.9927V534.529Z" fill="#AAAAAA"/>
|
||||||
|
<path d="M84.0029 119.887H133.007V124.027H84.0029V119.887Z" fill="#AAAAAA"/>
|
||||||
|
<path opacity="0.5" d="M71.9883 119.887H83.978V124.027H71.9883V119.887Z" fill="#AAAAAA"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_102_49" x1="335" y1="67.0001" x2="137" y2="67.0001" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#D62600"/>
|
||||||
|
<stop offset="1" stop-color="#FF5733" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint1_linear_102_49" x1="306.106" y1="336.5" x2="149.597" y2="336.5" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#D62600"/>
|
||||||
|
<stop offset="1" stop-color="#FF5733" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 26 KiB |
@@ -19,7 +19,7 @@ const CopiableCode = ({ code }: { code: string }) => {
|
|||||||
w="full"
|
w="full"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
>
|
>
|
||||||
<Text fontFamily="inconsolata" fontWeight="bold" letterSpacing={0.5}>
|
<Text fontFamily="inconsolata" fontWeight="bold" letterSpacing={0.5} overflowX="auto">
|
||||||
{code}
|
{code}
|
||||||
</Text>
|
</Text>
|
||||||
<Tooltip closeOnClick={false} label={copied ? "Copied!" : "Copy to clipboard"}>
|
<Tooltip closeOnClick={false} label={copied ? "Copied!" : "Copy to clipboard"}>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ export default function Favicon() {
|
|||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png" />
|
||||||
<link rel="manifest" href="/favicons/site.webmanifest" />
|
<link rel="manifest" href="/favicons/site.webmanifest" />
|
||||||
<link rel="shortcut icon" href="/favicons/favicon.ico" />
|
<link rel="shortcut icon" href="/favicons/favicon.ico" />
|
||||||
|
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#5bbad5" />
|
||||||
<meta name="msapplication-TileColor" content="#da532c" />
|
<meta name="msapplication-TileColor" content="#da532c" />
|
||||||
<meta name="msapplication-config" content="/favicons/browserconfig.xml" />
|
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#ffffff" />
|
||||||
</Head>
|
</Head>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,25 +33,11 @@ export default function AddVariantButton() {
|
|||||||
<Flex w="100%" justifyContent="flex-end">
|
<Flex w="100%" justifyContent="flex-end">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
py={5}
|
py={7}
|
||||||
leftIcon={<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />}
|
leftIcon={<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />}
|
||||||
>
|
>
|
||||||
<Text display={{ base: "none", md: "flex" }}>Add Variant</Text>
|
<Text display={{ base: "none", md: "flex" }}>Add Variant</Text>
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
{/* <Button
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
fontWeight="normal"
|
|
||||||
bgColor="transparent"
|
|
||||||
_hover={{ bgColor: "gray.100" }}
|
|
||||||
px={cellPadding.x}
|
|
||||||
onClick={onClick}
|
|
||||||
height="unset"
|
|
||||||
minH={headerMinHeight}
|
|
||||||
>
|
|
||||||
<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />
|
|
||||||
<Text display={{ base: "none", md: "flex" }}>Add Variant</Text>
|
|
||||||
</Button> */}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
FormHelperText,
|
FormHelperText,
|
||||||
Code,
|
Code,
|
||||||
|
IconButton,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { type Evaluation, EvalType } from "@prisma/client";
|
import { type Evaluation, EvalType } from "@prisma/client";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
@@ -183,46 +184,37 @@ export default function EditEvaluations() {
|
|||||||
<Text flex={1}>
|
<Text flex={1}>
|
||||||
{evaluation.evalType}: "{evaluation.value}"
|
{evaluation.evalType}: "{evaluation.value}"
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
|
||||||
|
<IconButton
|
||||||
|
aria-label="Edit"
|
||||||
variant="unstyled"
|
variant="unstyled"
|
||||||
color="gray.400"
|
|
||||||
height="unset"
|
|
||||||
width="unset"
|
|
||||||
minW="unset"
|
minW="unset"
|
||||||
|
color="gray.400"
|
||||||
onClick={() => setEditingId(evaluation.id)}
|
onClick={() => setEditingId(evaluation.id)}
|
||||||
_hover={{
|
_hover={{ color: "gray.800", cursor: "pointer" }}
|
||||||
color: "gray.800",
|
icon={<Icon as={BsPencil} />}
|
||||||
cursor: "pointer",
|
/>
|
||||||
}}
|
<IconButton
|
||||||
>
|
aria-label="Delete"
|
||||||
<Icon as={BsPencil} boxSize={4} />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="unstyled"
|
variant="unstyled"
|
||||||
color="gray.400"
|
|
||||||
height="unset"
|
|
||||||
width="unset"
|
|
||||||
minW="unset"
|
minW="unset"
|
||||||
|
color="gray.400"
|
||||||
onClick={() => onDelete(evaluation.id)}
|
onClick={() => onDelete(evaluation.id)}
|
||||||
_hover={{
|
_hover={{ color: "gray.800", cursor: "pointer" }}
|
||||||
color: "gray.800",
|
icon={<Icon as={BsX} boxSize={6} />}
|
||||||
cursor: "pointer",
|
/>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon as={BsX} boxSize={6} />
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
{editingId == null && (
|
{editingId == null && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setEditingId("new")}
|
onClick={() => setEditingId("new")}
|
||||||
alignSelf="flex-start"
|
alignSelf="end"
|
||||||
size="sm"
|
size="sm"
|
||||||
mt={4}
|
mt={4}
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
>
|
>
|
||||||
Add Evaluation
|
New Evaluation
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{editingId == "new" && (
|
{editingId == "new" && (
|
||||||
|
|||||||
@@ -1,103 +1,185 @@
|
|||||||
import { Text, Button, HStack, Heading, Icon, Input, Stack } from "@chakra-ui/react";
|
import { Text, Button, HStack, Heading, Icon, IconButton, Stack, VStack } from "@chakra-ui/react";
|
||||||
import { useState } from "react";
|
import { type TemplateVariable } from "@prisma/client";
|
||||||
import { BsCheck, BsX } from "react-icons/bs";
|
import { useEffect, useState } from "react";
|
||||||
|
import { BsPencil, BsX } from "react-icons/bs";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useExperiment, useHandledAsyncCallback, useScenarioVars } from "~/utils/hooks";
|
||||||
|
import { maybeReportError } from "~/utils/errorHandling/maybeReportError";
|
||||||
|
import { FloatingLabelInput } from "./FloatingLabelInput";
|
||||||
|
|
||||||
|
export const ScenarioVar = ({
|
||||||
|
variable,
|
||||||
|
isEditing,
|
||||||
|
setIsEditing,
|
||||||
|
}: {
|
||||||
|
variable: Pick<TemplateVariable, "id" | "label">;
|
||||||
|
isEditing: boolean;
|
||||||
|
setIsEditing: (isEditing: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
const utils = api.useContext();
|
||||||
|
|
||||||
|
const [label, setLabel] = useState(variable.label);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLabel(variable.label);
|
||||||
|
}, [variable.label]);
|
||||||
|
|
||||||
|
const renameVarMutation = api.scenarioVars.rename.useMutation();
|
||||||
|
const [onRename] = useHandledAsyncCallback(async () => {
|
||||||
|
const resp = await renameVarMutation.mutateAsync({ id: variable.id, label });
|
||||||
|
if (maybeReportError(resp)) return;
|
||||||
|
|
||||||
|
setIsEditing(false);
|
||||||
|
await utils.scenarioVars.list.invalidate();
|
||||||
|
await utils.scenarios.list.invalidate();
|
||||||
|
}, [label, variable.id]);
|
||||||
|
|
||||||
|
const deleteMutation = api.scenarioVars.delete.useMutation();
|
||||||
|
const [onDeleteVar] = useHandledAsyncCallback(async () => {
|
||||||
|
await deleteMutation.mutateAsync({ id: variable.id });
|
||||||
|
await utils.scenarioVars.list.invalidate();
|
||||||
|
}, [variable.id]);
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<HStack w="full">
|
||||||
|
<FloatingLabelInput
|
||||||
|
flex={1}
|
||||||
|
label="Renamed Variable"
|
||||||
|
value={label}
|
||||||
|
onChange={(e) => setLabel(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
onRename();
|
||||||
|
}
|
||||||
|
// If the user types a space, replace it with an underscore
|
||||||
|
if (e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
setLabel((label) => label && `${label}_`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button size="sm" onClick={() => setIsEditing(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" colorScheme="blue" onClick={onRename}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<HStack w="full" borderTopWidth={1} borderColor="gray.200">
|
||||||
|
<Text flex={1}>{variable.label}</Text>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Edit"
|
||||||
|
variant="unstyled"
|
||||||
|
minW="unset"
|
||||||
|
color="gray.400"
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
_hover={{ color: "gray.800", cursor: "pointer" }}
|
||||||
|
icon={<Icon as={BsPencil} />}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Delete"
|
||||||
|
variant="unstyled"
|
||||||
|
minW="unset"
|
||||||
|
color="gray.400"
|
||||||
|
onClick={onDeleteVar}
|
||||||
|
_hover={{ color: "gray.800", cursor: "pointer" }}
|
||||||
|
icon={<Icon as={BsX} boxSize={6} />}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function EditScenarioVars() {
|
export default function EditScenarioVars() {
|
||||||
const experiment = useExperiment();
|
const experiment = useExperiment();
|
||||||
const vars =
|
const vars = useScenarioVars();
|
||||||
api.templateVars.list.useQuery({ experimentId: experiment.data?.id ?? "" }).data ?? [];
|
|
||||||
|
const [currentlyEditingId, setCurrentlyEditingId] = useState<string | null>(null);
|
||||||
|
|
||||||
const [newVariable, setNewVariable] = useState<string>("");
|
const [newVariable, setNewVariable] = useState<string>("");
|
||||||
const newVarIsValid = newVariable.length > 0 && !vars.map((v) => v.label).includes(newVariable);
|
const newVarIsValid = newVariable?.length ?? 0 > 0;
|
||||||
|
|
||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
const addVarMutation = api.templateVars.create.useMutation();
|
const addVarMutation = api.scenarioVars.create.useMutation();
|
||||||
const [onAddVar] = useHandledAsyncCallback(async () => {
|
const [onAddVar] = useHandledAsyncCallback(async () => {
|
||||||
if (!experiment.data?.id) return;
|
if (!experiment.data?.id) return;
|
||||||
if (!newVarIsValid) return;
|
if (!newVariable) return;
|
||||||
await addVarMutation.mutateAsync({
|
const resp = await addVarMutation.mutateAsync({
|
||||||
experimentId: experiment.data.id,
|
experimentId: experiment.data.id,
|
||||||
label: newVariable,
|
label: newVariable,
|
||||||
});
|
});
|
||||||
await utils.templateVars.list.invalidate();
|
if (maybeReportError(resp)) return;
|
||||||
|
|
||||||
|
await utils.scenarioVars.list.invalidate();
|
||||||
setNewVariable("");
|
setNewVariable("");
|
||||||
}, [addVarMutation, experiment.data?.id, newVarIsValid, newVariable]);
|
}, [addVarMutation, experiment.data?.id, newVarIsValid, newVariable]);
|
||||||
|
|
||||||
const deleteMutation = api.templateVars.delete.useMutation();
|
|
||||||
const [onDeleteVar] = useHandledAsyncCallback(async (id: string) => {
|
|
||||||
await deleteMutation.mutateAsync({ id });
|
|
||||||
await utils.templateVars.list.invalidate();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Heading size="sm">Scenario Variables</Heading>
|
<Heading size="sm">Scenario Variables</Heading>
|
||||||
<Stack spacing={2}>
|
<VStack spacing={4}>
|
||||||
<Text fontSize="sm">
|
<Text fontSize="sm">
|
||||||
Scenario variables can be used in your prompt variants as well as evaluations.
|
Scenario variables can be used in your prompt variants as well as evaluations.
|
||||||
</Text>
|
</Text>
|
||||||
<HStack spacing={0}>
|
<VStack spacing={0} w="full">
|
||||||
<Input
|
{vars.data?.map((variable) => (
|
||||||
placeholder="Add Scenario Variable"
|
<ScenarioVar
|
||||||
size="sm"
|
variable={variable}
|
||||||
borderTopRadius={0}
|
|
||||||
borderRightRadius={0}
|
|
||||||
value={newVariable}
|
|
||||||
onChange={(e) => setNewVariable(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
onAddVar();
|
|
||||||
}
|
|
||||||
// If the user types a space, replace it with an underscore
|
|
||||||
if (e.key === " ") {
|
|
||||||
e.preventDefault();
|
|
||||||
setNewVariable((v) => v + "_");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
height="100%"
|
|
||||||
borderLeftRadius={0}
|
|
||||||
isDisabled={!newVarIsValid}
|
|
||||||
onClick={onAddVar}
|
|
||||||
>
|
|
||||||
<Icon as={BsCheck} boxSize={8} />
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
<HStack spacing={2} py={4} wrap="wrap">
|
|
||||||
{vars.map((variable) => (
|
|
||||||
<HStack
|
|
||||||
key={variable.id}
|
key={variable.id}
|
||||||
spacing={0}
|
isEditing={currentlyEditingId === variable.id}
|
||||||
bgColor="blue.100"
|
setIsEditing={(isEditing) => {
|
||||||
color="blue.600"
|
if (isEditing) {
|
||||||
pl={2}
|
setCurrentlyEditingId(variable.id);
|
||||||
pr={0}
|
} else {
|
||||||
fontWeight="bold"
|
setCurrentlyEditingId(null);
|
||||||
>
|
}
|
||||||
<Text fontSize="sm" flex={1}>
|
}}
|
||||||
{variable.label}
|
/>
|
||||||
</Text>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant="ghost"
|
|
||||||
colorScheme="blue"
|
|
||||||
p="unset"
|
|
||||||
minW="unset"
|
|
||||||
px="unset"
|
|
||||||
onClick={() => onDeleteVar(variable.id)}
|
|
||||||
>
|
|
||||||
<Icon as={BsX} boxSize={6} color="blue.800" />
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
))}
|
))}
|
||||||
</HStack>
|
</VStack>
|
||||||
</Stack>
|
{currentlyEditingId !== "new" && (
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentlyEditingId("new")}
|
||||||
|
alignSelf="end"
|
||||||
|
>
|
||||||
|
New Variable
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{currentlyEditingId === "new" && (
|
||||||
|
<HStack w="full">
|
||||||
|
<FloatingLabelInput
|
||||||
|
flex={1}
|
||||||
|
label="New Variable"
|
||||||
|
value={newVariable}
|
||||||
|
onChange={(e) => setNewVariable(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
onAddVar();
|
||||||
|
}
|
||||||
|
// If the user types a space, replace it with an underscore
|
||||||
|
if (e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
setNewVariable((v) => v && `${v}_`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button size="sm" onClick={() => setCurrentlyEditingId(null)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" colorScheme="blue" onClick={onAddVar}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { type PromptVariant, type Scenario } from "../types";
|
import { type PromptVariant, type Scenario } from "../types";
|
||||||
import { type StackProps, Text, VStack } from "@chakra-ui/react";
|
import { type StackProps, Text, VStack } from "@chakra-ui/react";
|
||||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useScenarioVars, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||||
import { docco } from "react-syntax-highlighter/dist/cjs/styles/hljs";
|
import { docco } from "react-syntax-highlighter/dist/cjs/styles/hljs";
|
||||||
import stringify from "json-stringify-pretty-compact";
|
import stringify from "json-stringify-pretty-compact";
|
||||||
@@ -23,10 +23,7 @@ export default function OutputCell({
|
|||||||
variant: PromptVariant;
|
variant: PromptVariant;
|
||||||
}): ReactElement | null {
|
}): ReactElement | null {
|
||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
const experiment = useExperiment();
|
const vars = useScenarioVars().data;
|
||||||
const vars = api.templateVars.list.useQuery({
|
|
||||||
experimentId: experiment.data?.id ?? "",
|
|
||||||
}).data;
|
|
||||||
|
|
||||||
const scenarioVariables = scenario.variableValues as Record<string, string>;
|
const scenarioVariables = scenario.variableValues as Record<string, string>;
|
||||||
const templateHasVariables =
|
const templateHasVariables =
|
||||||
@@ -36,7 +33,7 @@ export default function OutputCell({
|
|||||||
|
|
||||||
if (!templateHasVariables) disabledReason = "Add a value to the scenario variables to see output";
|
if (!templateHasVariables) disabledReason = "Add a value to the scenario variables to see output";
|
||||||
|
|
||||||
const [refetchInterval, setRefetchInterval] = useState(0);
|
const [refetchInterval, setRefetchInterval] = useState<number | false>(false);
|
||||||
const { data: cell, isLoading: queryLoading } = api.scenarioVariantCells.get.useQuery(
|
const { data: cell, isLoading: queryLoading } = api.scenarioVariantCells.get.useQuery(
|
||||||
{ scenarioId: scenario.id, variantId: variant.id },
|
{ scenarioId: scenario.id, variantId: variant.id },
|
||||||
{ refetchInterval },
|
{ refetchInterval },
|
||||||
@@ -67,7 +64,8 @@ export default function OutputCell({
|
|||||||
cell.retrievalStatus === "PENDING" ||
|
cell.retrievalStatus === "PENDING" ||
|
||||||
cell.retrievalStatus === "IN_PROGRESS" ||
|
cell.retrievalStatus === "IN_PROGRESS" ||
|
||||||
hardRefetching;
|
hardRefetching;
|
||||||
useEffect(() => setRefetchInterval(awaitingOutput ? 1000 : 0), [awaitingOutput]);
|
|
||||||
|
useEffect(() => setRefetchInterval(awaitingOutput ? 1000 : false), [awaitingOutput]);
|
||||||
|
|
||||||
// TODO: disconnect from socket if we're not streaming anymore
|
// TODO: disconnect from socket if we're not streaming anymore
|
||||||
const streamedMessage = useSocket<OutputSchema>(cell?.id);
|
const streamedMessage = useSocket<OutputSchema>(cell?.id);
|
||||||
@@ -110,7 +108,7 @@ export default function OutputCell({
|
|||||||
|
|
||||||
if (disabledReason) return <Text color="gray.500">{disabledReason}</Text>;
|
if (disabledReason) return <Text color="gray.500">{disabledReason}</Text>;
|
||||||
|
|
||||||
const showLogs = !streamedMessage && !mostRecentResponse?.output;
|
const showLogs = !streamedMessage && !mostRecentResponse?.respPayload;
|
||||||
|
|
||||||
if (showLogs)
|
if (showLogs)
|
||||||
return (
|
return (
|
||||||
@@ -123,8 +121,13 @@ export default function OutputCell({
|
|||||||
? response.receivedAt.getTime()
|
? response.receivedAt.getTime()
|
||||||
: Date.now();
|
: Date.now();
|
||||||
if (response.requestedAt) {
|
if (response.requestedAt) {
|
||||||
numWaitingMessages = Math.floor(
|
numWaitingMessages = Math.min(
|
||||||
(relativeWaitingTime - response.requestedAt.getTime()) / WAITING_MESSAGE_INTERVAL,
|
Math.floor(
|
||||||
|
(relativeWaitingTime - response.requestedAt.getTime()) / WAITING_MESSAGE_INTERVAL,
|
||||||
|
),
|
||||||
|
// Don't try to render more than 15, it'll use too much CPU and
|
||||||
|
// break the page
|
||||||
|
15,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -163,13 +166,13 @@ export default function OutputCell({
|
|||||||
</CellWrapper>
|
</CellWrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
const normalizedOutput = mostRecentResponse?.output
|
const normalizedOutput = mostRecentResponse?.respPayload
|
||||||
? provider.normalizeOutput(mostRecentResponse?.output)
|
? provider.normalizeOutput(mostRecentResponse?.respPayload)
|
||||||
: streamedMessage
|
: streamedMessage
|
||||||
? provider.normalizeOutput(streamedMessage)
|
? provider.normalizeOutput(streamedMessage)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (mostRecentResponse?.output && normalizedOutput?.type === "json") {
|
if (mostRecentResponse?.respPayload && normalizedOutput?.type === "json") {
|
||||||
return (
|
return (
|
||||||
<CellWrapper>
|
<CellWrapper>
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
@@ -191,7 +194,7 @@ export default function OutputCell({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CellWrapper>
|
<CellWrapper>
|
||||||
<Text>{contentToDisplay}</Text>
|
<Text whiteSpace="pre-wrap">{contentToDisplay}</Text>
|
||||||
</CellWrapper>
|
</CellWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export const OutputStats = ({
|
|||||||
? modelResponse.receivedAt.getTime() - modelResponse.requestedAt.getTime()
|
? modelResponse.receivedAt.getTime() - modelResponse.requestedAt.getTime()
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const promptTokens = modelResponse.promptTokens;
|
const inputTokens = modelResponse.inputTokens;
|
||||||
const completionTokens = modelResponse.completionTokens;
|
const outputTokens = modelResponse.outputTokens;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
@@ -55,8 +55,8 @@ export const OutputStats = ({
|
|||||||
</HStack>
|
</HStack>
|
||||||
{modelResponse.cost && (
|
{modelResponse.cost && (
|
||||||
<CostTooltip
|
<CostTooltip
|
||||||
promptTokens={promptTokens}
|
inputTokens={inputTokens}
|
||||||
completionTokens={completionTokens}
|
outputTokens={outputTokens}
|
||||||
cost={modelResponse.cost}
|
cost={modelResponse.cost}
|
||||||
>
|
>
|
||||||
<HStack spacing={0}>
|
<HStack spacing={0}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { isEqual } from "lodash-es";
|
import { isEqual } from "lodash-es";
|
||||||
import { useEffect, useState, type DragEvent } from "react";
|
import { useEffect, useState, type DragEvent } from "react";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useExperiment, useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useExperimentAccess, useHandledAsyncCallback, useScenarioVars } from "~/utils/hooks";
|
||||||
import { type Scenario } from "./types";
|
import { type Scenario } from "./types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -41,8 +41,7 @@ export default function ScenarioEditor({
|
|||||||
if (savedValues) setValues(savedValues);
|
if (savedValues) setValues(savedValues);
|
||||||
}, [savedValues]);
|
}, [savedValues]);
|
||||||
|
|
||||||
const experiment = useExperiment();
|
const vars = useScenarioVars();
|
||||||
const vars = api.templateVars.list.useQuery({ experimentId: experiment.data?.id ?? "" });
|
|
||||||
|
|
||||||
const variableLabels = vars.data?.map((v) => v.label) ?? [];
|
const variableLabels = vars.data?.map((v) => v.label) ?? [];
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const ScenarioEditorModal = ({
|
|||||||
await utils.scenarios.list.invalidate();
|
await utils.scenarios.list.invalidate();
|
||||||
}, [mutation, values]);
|
}, [mutation, values]);
|
||||||
|
|
||||||
const vars = api.templateVars.list.useQuery({ experimentId: experiment.data?.id ?? "" });
|
const vars = api.scenarioVars.list.useQuery({ experimentId: experiment.data?.id ?? "" });
|
||||||
const variableLabels = vars.data?.map((v) => v.label) ?? [];
|
const variableLabels = vars.data?.map((v) => v.label) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
|
import { type StackProps } from "@chakra-ui/react";
|
||||||
|
|
||||||
import { useScenarios } from "~/utils/hooks";
|
import { useScenarios } from "~/utils/hooks";
|
||||||
import Paginator from "../Paginator";
|
import Paginator from "../Paginator";
|
||||||
|
|
||||||
const ScenarioPaginator = () => {
|
const ScenarioPaginator = (props: StackProps) => {
|
||||||
const { data } = useScenarios();
|
const { data } = useScenarios();
|
||||||
|
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
const { scenarios, startIndex, lastPage, count } = data;
|
const { count } = data;
|
||||||
|
|
||||||
return (
|
return <Paginator count={count} condense {...props} />;
|
||||||
<Paginator
|
|
||||||
numItemsLoaded={scenarios.length}
|
|
||||||
startIndex={startIndex}
|
|
||||||
lastPage={lastPage}
|
|
||||||
count={count}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ScenarioPaginator;
|
export default ScenarioPaginator;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const ScenarioRow = (props: {
|
|||||||
variants: PromptVariant[];
|
variants: PromptVariant[];
|
||||||
canHide: boolean;
|
canHide: boolean;
|
||||||
rowStart: number;
|
rowStart: number;
|
||||||
|
isFirst: boolean;
|
||||||
|
isLast: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
@@ -21,10 +23,14 @@ const ScenarioRow = (props: {
|
|||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
sx={isHovered ? highlightStyle : undefined}
|
sx={isHovered ? highlightStyle : undefined}
|
||||||
borderLeftWidth={1}
|
bgColor="white"
|
||||||
{...borders}
|
|
||||||
rowStart={props.rowStart}
|
rowStart={props.rowStart}
|
||||||
colStart={1}
|
colStart={1}
|
||||||
|
borderLeftWidth={1}
|
||||||
|
borderTopWidth={props.isFirst ? 1 : 0}
|
||||||
|
borderTopLeftRadius={props.isFirst ? 8 : 0}
|
||||||
|
borderBottomLeftRadius={props.isLast ? 8 : 0}
|
||||||
|
{...borders}
|
||||||
>
|
>
|
||||||
<ScenarioEditor scenario={props.scenario} hovered={isHovered} canHide={props.canHide} />
|
<ScenarioEditor scenario={props.scenario} hovered={isHovered} canHide={props.canHide} />
|
||||||
</GridItem>
|
</GridItem>
|
||||||
@@ -34,8 +40,12 @@ const ScenarioRow = (props: {
|
|||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
sx={isHovered ? highlightStyle : undefined}
|
sx={isHovered ? highlightStyle : undefined}
|
||||||
|
bgColor="white"
|
||||||
rowStart={props.rowStart}
|
rowStart={props.rowStart}
|
||||||
colStart={i + 2}
|
colStart={i + 2}
|
||||||
|
borderTopWidth={props.isFirst ? 1 : 0}
|
||||||
|
borderTopRightRadius={props.isFirst && i === props.variants.length - 1 ? 8 : 0}
|
||||||
|
borderBottomRightRadius={props.isLast && i === props.variants.length - 1 ? 8 : 0}
|
||||||
{...borders}
|
{...borders}
|
||||||
>
|
>
|
||||||
<OutputCell key={variant.id} scenario={props.scenario} variant={variant} />
|
<OutputCell key={variant.id} scenario={props.scenario} variant={variant} />
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const ScenariosHeader = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack w="100%" pb={cellPadding.y} pt={0} align="center" spacing={0}>
|
<HStack w="100%" py={cellPadding.y} px={cellPadding.x} align="center" spacing={0}>
|
||||||
<Text fontSize={16} fontWeight="bold">
|
<Text fontSize={16} fontWeight="bold">
|
||||||
Scenarios ({scenarios.data?.count})
|
Scenarios ({scenarios.data?.count})
|
||||||
</Text>
|
</Text>
|
||||||
@@ -57,11 +57,16 @@ export const ScenariosHeader = () => {
|
|||||||
<MenuButton
|
<MenuButton
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
mt={1}
|
mt={1}
|
||||||
|
ml={2}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
aria-label="Edit Scenarios"
|
aria-label="Edit Scenarios"
|
||||||
icon={<Icon as={loading ? Spinner : BsGear} />}
|
icon={<Icon as={loading ? Spinner : BsGear} />}
|
||||||
|
maxW={8}
|
||||||
|
minW={8}
|
||||||
|
minH={8}
|
||||||
|
maxH={8}
|
||||||
/>
|
/>
|
||||||
<MenuList fontSize="md" zIndex="dropdown" mt={-3}>
|
<MenuList fontSize="md" zIndex="dropdown" mt={-1}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<Icon as={BsPlus} boxSize={6} mx="-5px" />}
|
icon={<Icon as={BsPlus} boxSize={6} mx="-5px" />}
|
||||||
onClick={() => onAddScenario(false)}
|
onClick={() => onAddScenario(false)}
|
||||||
@@ -72,7 +77,7 @@ export const ScenariosHeader = () => {
|
|||||||
Autogenerate Scenario
|
Autogenerate Scenario
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem icon={<BsPencil />} onClick={openDrawer}>
|
<MenuItem icon={<BsPencil />} onClick={openDrawer}>
|
||||||
Edit Vars
|
Add or Remove Variables
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -17,18 +17,22 @@ export default function VariantStats(props: { variant: PromptVariant }) {
|
|||||||
initialData: {
|
initialData: {
|
||||||
evalResults: [],
|
evalResults: [],
|
||||||
overallCost: 0,
|
overallCost: 0,
|
||||||
promptTokens: 0,
|
inputTokens: 0,
|
||||||
completionTokens: 0,
|
outputTokens: 0,
|
||||||
scenarioCount: 0,
|
scenarioCount: 0,
|
||||||
outputCount: 0,
|
outputCount: 0,
|
||||||
|
awaitingCompletions: false,
|
||||||
awaitingEvals: false,
|
awaitingEvals: false,
|
||||||
},
|
},
|
||||||
refetchInterval,
|
refetchInterval,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Poll every two seconds while we are waiting for LLM retrievals to finish
|
// Poll every five seconds while we are waiting for LLM retrievals to finish
|
||||||
useEffect(() => setRefetchInterval(data.awaitingEvals ? 5000 : 0), [data.awaitingEvals]);
|
useEffect(
|
||||||
|
() => setRefetchInterval(data.awaitingCompletions || data.awaitingEvals ? 5000 : 0),
|
||||||
|
[data.awaitingCompletions, data.awaitingEvals],
|
||||||
|
);
|
||||||
|
|
||||||
const [passColor, neutralColor, failColor] = useToken("colors", [
|
const [passColor, neutralColor, failColor] = useToken("colors", [
|
||||||
"green.500",
|
"green.500",
|
||||||
@@ -68,8 +72,8 @@ export default function VariantStats(props: { variant: PromptVariant }) {
|
|||||||
</HStack>
|
</HStack>
|
||||||
{data.overallCost && (
|
{data.overallCost && (
|
||||||
<CostTooltip
|
<CostTooltip
|
||||||
promptTokens={data.promptTokens}
|
inputTokens={data.inputTokens}
|
||||||
completionTokens={data.completionTokens}
|
outputTokens={data.outputTokens}
|
||||||
cost={data.overallCost}
|
cost={data.overallCost}
|
||||||
>
|
>
|
||||||
<HStack spacing={0} align="center" color="gray.500">
|
<HStack spacing={0} align="center" color="gray.500">
|
||||||
|
|||||||
@@ -53,20 +53,29 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
|||||||
colStart: i + 2,
|
colStart: i + 2,
|
||||||
borderLeftWidth: i === 0 ? 1 : 0,
|
borderLeftWidth: i === 0 ? 1 : 0,
|
||||||
marginLeft: i === 0 ? "-1px" : 0,
|
marginLeft: i === 0 ? "-1px" : 0,
|
||||||
backgroundColor: "gray.100",
|
backgroundColor: "white",
|
||||||
};
|
};
|
||||||
|
const isFirst = i === 0;
|
||||||
|
const isLast = i === variants.data.length - 1;
|
||||||
return (
|
return (
|
||||||
<Fragment key={variant.uiId}>
|
<Fragment key={variant.uiId}>
|
||||||
<VariantHeader
|
<VariantHeader
|
||||||
variant={variant}
|
variant={variant}
|
||||||
canHide={variants.data.length > 1}
|
canHide={variants.data.length > 1}
|
||||||
rowStart={1}
|
rowStart={1}
|
||||||
|
borderTopLeftRadius={isFirst ? 8 : 0}
|
||||||
|
borderTopRightRadius={isLast ? 8 : 0}
|
||||||
{...sharedProps}
|
{...sharedProps}
|
||||||
/>
|
/>
|
||||||
<GridItem rowStart={2} {...sharedProps}>
|
<GridItem rowStart={2} {...sharedProps}>
|
||||||
<VariantEditor variant={variant} />
|
<VariantEditor variant={variant} />
|
||||||
</GridItem>
|
</GridItem>
|
||||||
<GridItem rowStart={3} {...sharedProps}>
|
<GridItem
|
||||||
|
rowStart={3}
|
||||||
|
{...sharedProps}
|
||||||
|
borderBottomLeftRadius={isFirst ? 8 : 0}
|
||||||
|
borderBottomRightRadius={isLast ? 8 : 0}
|
||||||
|
>
|
||||||
<VariantStats variant={variant} />
|
<VariantStats variant={variant} />
|
||||||
</GridItem>
|
</GridItem>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@@ -77,7 +86,6 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
|||||||
colSpan={allCols - 1}
|
colSpan={allCols - 1}
|
||||||
rowStart={variantHeaderRows + 1}
|
rowStart={variantHeaderRows + 1}
|
||||||
colStart={1}
|
colStart={1}
|
||||||
{...borders}
|
|
||||||
borderRightWidth={0}
|
borderRightWidth={0}
|
||||||
>
|
>
|
||||||
<ScenariosHeader />
|
<ScenariosHeader />
|
||||||
@@ -90,6 +98,8 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
|||||||
scenario={scenario}
|
scenario={scenario}
|
||||||
variants={variants.data}
|
variants={variants.data}
|
||||||
canHide={visibleScenariosCount > 1}
|
canHide={visibleScenariosCount > 1}
|
||||||
|
isFirst={i === 0}
|
||||||
|
isLast={i === visibleScenariosCount - 1}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<GridItem
|
<GridItem
|
||||||
|
|||||||
@@ -1,77 +1,117 @@
|
|||||||
import { Box, HStack, IconButton } from "@chakra-ui/react";
|
import { HStack, IconButton, Text, Select, type StackProps, Icon } from "@chakra-ui/react";
|
||||||
import {
|
import React, { useCallback } from "react";
|
||||||
BsChevronDoubleLeft,
|
import { FiChevronsLeft, FiChevronsRight, FiChevronLeft, FiChevronRight } from "react-icons/fi";
|
||||||
BsChevronDoubleRight,
|
import { usePageParams } from "~/utils/hooks";
|
||||||
BsChevronLeft,
|
|
||||||
BsChevronRight,
|
const pageSizeOptions = [10, 25, 50, 100];
|
||||||
} from "react-icons/bs";
|
|
||||||
import { usePage } from "~/utils/hooks";
|
|
||||||
|
|
||||||
const Paginator = ({
|
const Paginator = ({
|
||||||
numItemsLoaded,
|
|
||||||
startIndex,
|
|
||||||
lastPage,
|
|
||||||
count,
|
count,
|
||||||
}: {
|
condense,
|
||||||
numItemsLoaded: number;
|
...props
|
||||||
startIndex: number;
|
}: { count: number; condense?: boolean } & StackProps) => {
|
||||||
lastPage: number;
|
const { page, pageSize, setPageParams } = usePageParams();
|
||||||
count: number;
|
|
||||||
}) => {
|
const lastPage = Math.ceil(count / pageSize);
|
||||||
const [page, setPage] = usePage();
|
|
||||||
|
const updatePageSize = useCallback(
|
||||||
|
(newPageSize: number) => {
|
||||||
|
const newPage = Math.floor(((page - 1) * pageSize) / newPageSize) + 1;
|
||||||
|
setPageParams({ page: newPage, pageSize: newPageSize }, "replace");
|
||||||
|
},
|
||||||
|
[page, pageSize, setPageParams],
|
||||||
|
);
|
||||||
|
|
||||||
const nextPage = () => {
|
const nextPage = () => {
|
||||||
if (page < lastPage) {
|
if (page < lastPage) {
|
||||||
setPage(page + 1, "replace");
|
setPageParams({ page: page + 1 }, "replace");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const prevPage = () => {
|
const prevPage = () => {
|
||||||
if (page > 1) {
|
if (page > 1) {
|
||||||
setPage(page - 1, "replace");
|
setPageParams({ page: page - 1 }, "replace");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToLastPage = () => setPage(lastPage, "replace");
|
const goToLastPage = () => setPageParams({ page: lastPage }, "replace");
|
||||||
const goToFirstPage = () => setPage(1, "replace");
|
const goToFirstPage = () => setPageParams({ page: 1 }, "replace");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack pt={4}>
|
<HStack
|
||||||
<IconButton
|
pt={4}
|
||||||
variant="ghost"
|
spacing={8}
|
||||||
size="sm"
|
justifyContent={condense ? "flex-start" : "space-between"}
|
||||||
onClick={goToFirstPage}
|
alignItems="center"
|
||||||
isDisabled={page === 1}
|
w="full"
|
||||||
aria-label="Go to first page"
|
{...props}
|
||||||
icon={<BsChevronDoubleLeft />}
|
>
|
||||||
/>
|
{!condense && (
|
||||||
<IconButton
|
<>
|
||||||
variant="ghost"
|
<HStack>
|
||||||
size="sm"
|
<Text>Rows</Text>
|
||||||
onClick={prevPage}
|
<Select
|
||||||
isDisabled={page === 1}
|
value={pageSize}
|
||||||
aria-label="Previous page"
|
onChange={(e) => updatePageSize(parseInt(e.target.value))}
|
||||||
icon={<BsChevronLeft />}
|
w={20}
|
||||||
/>
|
backgroundColor="white"
|
||||||
<Box>
|
>
|
||||||
{startIndex}-{startIndex + numItemsLoaded - 1} / {count}
|
{pageSizeOptions.map((option) => (
|
||||||
</Box>
|
<option key={option} value={option}>
|
||||||
<IconButton
|
{option}
|
||||||
variant="ghost"
|
</option>
|
||||||
size="sm"
|
))}
|
||||||
onClick={nextPage}
|
</Select>
|
||||||
isDisabled={page === lastPage}
|
</HStack>
|
||||||
aria-label="Next page"
|
<Text>
|
||||||
icon={<BsChevronRight />}
|
Page {page} of {lastPage}
|
||||||
/>
|
</Text>
|
||||||
<IconButton
|
</>
|
||||||
variant="ghost"
|
)}
|
||||||
size="sm"
|
|
||||||
onClick={goToLastPage}
|
<HStack>
|
||||||
isDisabled={page === lastPage}
|
<IconButton
|
||||||
aria-label="Go to last page"
|
variant="outline"
|
||||||
icon={<BsChevronDoubleRight />}
|
size="sm"
|
||||||
/>
|
onClick={goToFirstPage}
|
||||||
|
isDisabled={page === 1}
|
||||||
|
aria-label="Go to first page"
|
||||||
|
icon={<Icon as={FiChevronsLeft} boxSize={5} strokeWidth={1.5} />}
|
||||||
|
bgColor="white"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={prevPage}
|
||||||
|
isDisabled={page === 1}
|
||||||
|
aria-label="Previous page"
|
||||||
|
icon={<Icon as={FiChevronLeft} boxSize={5} strokeWidth={1.5} />}
|
||||||
|
bgColor="white"
|
||||||
|
/>
|
||||||
|
{condense && (
|
||||||
|
<Text>
|
||||||
|
Page {page} of {lastPage}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={nextPage}
|
||||||
|
isDisabled={page === lastPage}
|
||||||
|
aria-label="Next page"
|
||||||
|
icon={<Icon as={FiChevronRight} boxSize={5} strokeWidth={1.5} />}
|
||||||
|
bgColor="white"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={goToLastPage}
|
||||||
|
isDisabled={page === lastPage}
|
||||||
|
aria-label="Go to last page"
|
||||||
|
icon={<Icon as={FiChevronsRight} boxSize={5} strokeWidth={1.5} />}
|
||||||
|
bgColor="white"
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export default function VariantHeader(
|
|||||||
padding={0}
|
padding={0}
|
||||||
sx={{
|
sx={{
|
||||||
position: "sticky",
|
position: "sticky",
|
||||||
top: "0",
|
top: "-2",
|
||||||
// Ensure that the menu always appears above the sticky header of other variants
|
// Ensure that the menu always appears above the sticky header of other variants
|
||||||
zIndex: menuOpen ? "dropdown" : 10,
|
zIndex: menuOpen ? "dropdown" : 10,
|
||||||
}}
|
}}
|
||||||
@@ -84,6 +84,7 @@ export default function VariantHeader(
|
|||||||
>
|
>
|
||||||
<HStack
|
<HStack
|
||||||
spacing={2}
|
spacing={2}
|
||||||
|
py={2}
|
||||||
alignItems="flex-start"
|
alignItems="flex-start"
|
||||||
minH={headerMinHeight}
|
minH={headerMinHeight}
|
||||||
draggable={!isInputHovered}
|
draggable={!isInputHovered}
|
||||||
@@ -102,7 +103,9 @@ export default function VariantHeader(
|
|||||||
setIsDragTarget(false);
|
setIsDragTarget(false);
|
||||||
}}
|
}}
|
||||||
onDrop={onReorder}
|
onDrop={onReorder}
|
||||||
backgroundColor={isDragTarget ? "gray.200" : "gray.100"}
|
backgroundColor={isDragTarget ? "gray.200" : "white"}
|
||||||
|
borderTopLeftRadius={gridItemProps.borderTopLeftRadius}
|
||||||
|
borderTopRightRadius={gridItemProps.borderTopRightRadius}
|
||||||
h="full"
|
h="full"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -1,201 +0,0 @@
|
|||||||
import {
|
|
||||||
Box,
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
Heading,
|
|
||||||
Table,
|
|
||||||
Tbody,
|
|
||||||
Td,
|
|
||||||
Th,
|
|
||||||
Thead,
|
|
||||||
Tr,
|
|
||||||
Tooltip,
|
|
||||||
Collapse,
|
|
||||||
HStack,
|
|
||||||
VStack,
|
|
||||||
IconButton,
|
|
||||||
useToast,
|
|
||||||
Icon,
|
|
||||||
Button,
|
|
||||||
ButtonGroup,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
|
||||||
import { ChevronUpIcon, ChevronDownIcon, CopyIcon } from "lucide-react";
|
|
||||||
import { useMemo, useState } from "react";
|
|
||||||
import { type RouterOutputs, api } from "~/utils/api";
|
|
||||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
|
||||||
import { atelierCaveLight } from "react-syntax-highlighter/dist/cjs/styles/hljs";
|
|
||||||
import stringify from "json-stringify-pretty-compact";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
type LoggedCall = RouterOutputs["dashboard"]["loggedCalls"][0];
|
|
||||||
|
|
||||||
const FormattedJson = ({ json }: { json: any }) => {
|
|
||||||
const jsonString = stringify(json, { maxLength: 40 });
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const copyToClipboard = async (text: string) => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
toast({
|
|
||||||
title: "Copied to clipboard",
|
|
||||||
status: "success",
|
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
|
||||||
title: "Failed to copy to clipboard",
|
|
||||||
status: "error",
|
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box position="relative" fontSize="sm" borderRadius="md" overflow="hidden">
|
|
||||||
<SyntaxHighlighter
|
|
||||||
customStyle={{ overflowX: "unset" }}
|
|
||||||
language="json"
|
|
||||||
style={atelierCaveLight}
|
|
||||||
lineProps={{
|
|
||||||
style: { wordBreak: "break-all", whiteSpace: "pre-wrap" },
|
|
||||||
}}
|
|
||||||
wrapLines
|
|
||||||
>
|
|
||||||
{jsonString}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
<IconButton
|
|
||||||
aria-label="Copy"
|
|
||||||
icon={<CopyIcon />}
|
|
||||||
position="absolute"
|
|
||||||
top={1}
|
|
||||||
right={1}
|
|
||||||
size="xs"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => void copyToClipboard(jsonString)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function TableRow({
|
|
||||||
loggedCall,
|
|
||||||
isExpanded,
|
|
||||||
onToggle,
|
|
||||||
}: {
|
|
||||||
loggedCall: LoggedCall;
|
|
||||||
isExpanded: boolean;
|
|
||||||
onToggle: () => void;
|
|
||||||
}) {
|
|
||||||
const isError = loggedCall.modelResponse?.respStatus !== 200;
|
|
||||||
const timeAgo = dayjs(loggedCall.startTime).fromNow();
|
|
||||||
const fullTime = dayjs(loggedCall.startTime).toString();
|
|
||||||
|
|
||||||
const model = useMemo(
|
|
||||||
() => loggedCall.tags.find((tag) => tag.name.startsWith("$model"))?.value,
|
|
||||||
[loggedCall.tags],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tr
|
|
||||||
onClick={onToggle}
|
|
||||||
key={loggedCall.id}
|
|
||||||
_hover={{ bgColor: "gray.100", cursor: "pointer" }}
|
|
||||||
sx={{
|
|
||||||
"> td": { borderBottom: "none" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Td>
|
|
||||||
<Icon boxSize={6} as={isExpanded ? ChevronUpIcon : ChevronDownIcon} />
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<Tooltip label={fullTime} placement="top">
|
|
||||||
<Box whiteSpace="nowrap" minW="120px">
|
|
||||||
{timeAgo}
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
</Td>
|
|
||||||
<Td width="100%">{model}</Td>
|
|
||||||
<Td isNumeric>{((loggedCall.modelResponse?.durationMs ?? 0) / 1000).toFixed(2)}s</Td>
|
|
||||||
<Td isNumeric>{loggedCall.modelResponse?.inputTokens}</Td>
|
|
||||||
<Td isNumeric>{loggedCall.modelResponse?.outputTokens}</Td>
|
|
||||||
<Td sx={{ color: isError ? "red.500" : "green.500", fontWeight: "semibold" }} isNumeric>
|
|
||||||
{loggedCall.modelResponse?.respStatus ?? "No response"}
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={8} p={0}>
|
|
||||||
<Collapse in={isExpanded} unmountOnExit={true}>
|
|
||||||
<VStack p={4} align="stretch">
|
|
||||||
<HStack align="stretch">
|
|
||||||
<VStack flex={1} align="stretch">
|
|
||||||
<Heading size="sm">Input</Heading>
|
|
||||||
<FormattedJson json={loggedCall.modelResponse?.reqPayload} />
|
|
||||||
</VStack>
|
|
||||||
<VStack flex={1} align="stretch">
|
|
||||||
<Heading size="sm">Output</Heading>
|
|
||||||
<FormattedJson json={loggedCall.modelResponse?.respPayload} />
|
|
||||||
</VStack>
|
|
||||||
</HStack>
|
|
||||||
<ButtonGroup alignSelf="flex-end">
|
|
||||||
<Button as={Link} colorScheme="blue" href={{ pathname: "/experiments" }}>
|
|
||||||
Experiments
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</VStack>
|
|
||||||
</Collapse>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LoggedCallTable() {
|
|
||||||
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
|
||||||
const loggedCalls = api.dashboard.loggedCalls.useQuery({});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card variant="outline" width="100%" overflow="hidden">
|
|
||||||
<CardHeader>
|
|
||||||
<Heading as="h3" size="sm">
|
|
||||||
Logged Calls
|
|
||||||
</Heading>
|
|
||||||
</CardHeader>
|
|
||||||
<Table>
|
|
||||||
<Thead>
|
|
||||||
<Tr>
|
|
||||||
<Th />
|
|
||||||
<Th>Time</Th>
|
|
||||||
<Th>Model</Th>
|
|
||||||
<Th isNumeric>Duration</Th>
|
|
||||||
<Th isNumeric>Input tokens</Th>
|
|
||||||
<Th isNumeric>Output tokens</Th>
|
|
||||||
<Th isNumeric>Status</Th>
|
|
||||||
</Tr>
|
|
||||||
</Thead>
|
|
||||||
<Tbody>
|
|
||||||
{loggedCalls.data?.map((loggedCall) => {
|
|
||||||
return (
|
|
||||||
<TableRow
|
|
||||||
key={loggedCall.id}
|
|
||||||
loggedCall={loggedCall}
|
|
||||||
isExpanded={loggedCall.id === expandedRow}
|
|
||||||
onToggle={() => {
|
|
||||||
if (loggedCall.id === expandedRow) {
|
|
||||||
setExpandedRow(null);
|
|
||||||
} else {
|
|
||||||
setExpandedRow(loggedCall.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
46
app/src/components/dashboard/LoggedCallsTable.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Card, CardHeader, Heading, Table, Tbody, HStack, Button, Text } from "@chakra-ui/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useLoggedCalls } from "~/utils/hooks";
|
||||||
|
import { TableHeader, TableRow } from "../requestLogs/TableRow";
|
||||||
|
|
||||||
|
export default function LoggedCallsTable() {
|
||||||
|
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
||||||
|
const { data: loggedCalls } = useLoggedCalls();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card width="100%" overflow="hidden">
|
||||||
|
<CardHeader>
|
||||||
|
<HStack justifyContent="space-between">
|
||||||
|
<Heading as="h3" size="sm">
|
||||||
|
Request Logs
|
||||||
|
</Heading>
|
||||||
|
<Button as={Link} href="/request-logs" variant="ghost" colorScheme="blue">
|
||||||
|
<Text>View All</Text>
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</CardHeader>
|
||||||
|
<Table>
|
||||||
|
<TableHeader />
|
||||||
|
<Tbody>
|
||||||
|
{loggedCalls?.calls.map((loggedCall) => {
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={loggedCall.id}
|
||||||
|
loggedCall={loggedCall}
|
||||||
|
isExpanded={loggedCall.id === expandedRow}
|
||||||
|
onToggle={() => {
|
||||||
|
if (loggedCall.id === expandedRow) {
|
||||||
|
setExpandedRow(null);
|
||||||
|
} else {
|
||||||
|
setExpandedRow(loggedCall.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
61
app/src/components/dashboard/UsageGraph.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import {
|
||||||
|
ResponsiveContainer,
|
||||||
|
LineChart,
|
||||||
|
Line,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
} from "recharts";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { useSelectedProject } from "~/utils/hooks";
|
||||||
|
import dayjs from "~/utils/dayjs";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
|
export default function UsageGraph() {
|
||||||
|
const { data: selectedProject } = useSelectedProject();
|
||||||
|
|
||||||
|
const stats = api.dashboard.stats.useQuery(
|
||||||
|
{ projectId: selectedProject?.id ?? "" },
|
||||||
|
{ enabled: !!selectedProject },
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
return (
|
||||||
|
stats.data?.periods.map(({ period, numQueries, cost }) => ({
|
||||||
|
period,
|
||||||
|
Requests: numQueries,
|
||||||
|
"Total Spent (USD)": parseFloat(cost.toString()),
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
}, [stats.data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
|
<LineChart data={data} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
|
||||||
|
<XAxis dataKey="period" tickFormatter={(str: string) => dayjs(str).format("MMM D")} />
|
||||||
|
<YAxis yAxisId="left" dataKey="Requests" orientation="left" stroke="#8884d8" />
|
||||||
|
<YAxis
|
||||||
|
yAxisId="right"
|
||||||
|
dataKey="Total Spent (USD)"
|
||||||
|
orientation="right"
|
||||||
|
unit="$"
|
||||||
|
stroke="#82ca9d"
|
||||||
|
/>
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<CartesianGrid stroke="#f5f5f5" />
|
||||||
|
<Line dataKey="Requests" stroke="#8884d8" yAxisId="left" dot={false} strokeWidth={2} />
|
||||||
|
<Line
|
||||||
|
dataKey="Total Spent (USD)"
|
||||||
|
stroke="#82ca9d"
|
||||||
|
yAxisId="right"
|
||||||
|
dot={false}
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -72,12 +72,12 @@ const CountLabel = ({ label, count }: { label: string; count: number }) => {
|
|||||||
|
|
||||||
export const NewDatasetCard = () => {
|
export const NewDatasetCard = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const selectedOrgId = useAppStore((s) => s.selectedOrgId);
|
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
|
||||||
const createMutation = api.datasets.create.useMutation();
|
const createMutation = api.datasets.create.useMutation();
|
||||||
const [createDataset, isLoading] = useHandledAsyncCallback(async () => {
|
const [createDataset, isLoading] = useHandledAsyncCallback(async () => {
|
||||||
const newDataset = await createMutation.mutateAsync({ organizationId: selectedOrgId ?? "" });
|
const newDataset = await createMutation.mutateAsync({ projectId: selectedProjectId ?? "" });
|
||||||
await router.push({ pathname: "/data/[id]", query: { id: newDataset.id } });
|
await router.push({ pathname: "/data/[id]", query: { id: newDataset.id } });
|
||||||
}, [createMutation, router, selectedOrgId]);
|
}, [createMutation, router, selectedProjectId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AspectRatio ratio={1.2} w="full">
|
<AspectRatio ratio={1.2} w="full">
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
|
import { type StackProps } from "@chakra-ui/react";
|
||||||
|
|
||||||
import { useDatasetEntries } from "~/utils/hooks";
|
import { useDatasetEntries } from "~/utils/hooks";
|
||||||
import Paginator from "../Paginator";
|
import Paginator from "../Paginator";
|
||||||
|
|
||||||
const DatasetEntriesPaginator = () => {
|
const DatasetEntriesPaginator = (props: StackProps) => {
|
||||||
const { data } = useDatasetEntries();
|
const { data } = useDatasetEntries();
|
||||||
|
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
const { entries, startIndex, lastPage, count } = data;
|
const { count } = data;
|
||||||
|
|
||||||
return (
|
return <Paginator count={count} {...props} />;
|
||||||
<Paginator
|
|
||||||
numItemsLoaded={entries.length}
|
|
||||||
startIndex={startIndex}
|
|
||||||
lastPage={lastPage}
|
|
||||||
count={count}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DatasetEntriesPaginator;
|
export default DatasetEntriesPaginator;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
AspectRatio,
|
AspectRatio,
|
||||||
SkeletonText,
|
SkeletonText,
|
||||||
|
Card,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { RiFlaskLine } from "react-icons/ri";
|
import { RiFlaskLine } from "react-icons/ri";
|
||||||
import { formatTimePast } from "~/utils/dayjs";
|
import { formatTimePast } from "~/utils/dayjs";
|
||||||
@@ -29,17 +30,22 @@ type ExperimentData = {
|
|||||||
|
|
||||||
export const ExperimentCard = ({ exp }: { exp: ExperimentData }) => {
|
export const ExperimentCard = ({ exp }: { exp: ExperimentData }) => {
|
||||||
return (
|
return (
|
||||||
<AspectRatio ratio={1.2} w="full">
|
<Card
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
cursor="pointer"
|
||||||
|
p={4}
|
||||||
|
bg="white"
|
||||||
|
borderRadius={4}
|
||||||
|
_hover={{ bg: "gray.100" }}
|
||||||
|
transition="background 0.2s"
|
||||||
|
aspectRatio={1.2}
|
||||||
|
>
|
||||||
<VStack
|
<VStack
|
||||||
as={Link}
|
as={Link}
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
href={{ pathname: "/experiments/[id]", query: { id: exp.id } }}
|
href={{ pathname: "/experiments/[id]", query: { id: exp.id } }}
|
||||||
bg="gray.50"
|
|
||||||
_hover={{ bg: "gray.100" }}
|
|
||||||
transition="background 0.2s"
|
|
||||||
cursor="pointer"
|
|
||||||
borderColor="gray.200"
|
|
||||||
borderWidth={1}
|
|
||||||
p={4}
|
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
>
|
>
|
||||||
<HStack w="full" color="gray.700" justify="center">
|
<HStack w="full" color="gray.700" justify="center">
|
||||||
@@ -57,7 +63,7 @@ export const ExperimentCard = ({ exp }: { exp: ExperimentData }) => {
|
|||||||
<Text flex={1}>Updated {formatTimePast(exp.updatedAt)}</Text>
|
<Text flex={1}>Updated {formatTimePast(exp.updatedAt)}</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
</AspectRatio>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,43 +82,43 @@ const CountLabel = ({ label, count }: { label: string; count: number }) => {
|
|||||||
|
|
||||||
export const NewExperimentCard = () => {
|
export const NewExperimentCard = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const selectedOrgId = useAppStore((s) => s.selectedOrgId);
|
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
|
||||||
const createMutation = api.experiments.create.useMutation();
|
const createMutation = api.experiments.create.useMutation();
|
||||||
const [createExperiment, isLoading] = useHandledAsyncCallback(async () => {
|
const [createExperiment, isLoading] = useHandledAsyncCallback(async () => {
|
||||||
const newExperiment = await createMutation.mutateAsync({
|
const newExperiment = await createMutation.mutateAsync({
|
||||||
organizationId: selectedOrgId ?? "",
|
projectId: selectedProjectId ?? "",
|
||||||
});
|
});
|
||||||
await router.push({
|
await router.push({
|
||||||
pathname: "/experiments/[id]",
|
pathname: "/experiments/[id]",
|
||||||
query: { id: newExperiment.id },
|
query: { id: newExperiment.id },
|
||||||
});
|
});
|
||||||
}, [createMutation, router, selectedOrgId]);
|
}, [createMutation, router, selectedProjectId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AspectRatio ratio={1.2} w="full">
|
<Card
|
||||||
<VStack
|
w="full"
|
||||||
align="center"
|
h="full"
|
||||||
justify="center"
|
cursor="pointer"
|
||||||
_hover={{ cursor: "pointer", bg: "gray.50" }}
|
p={4}
|
||||||
transition="background 0.2s"
|
bg="white"
|
||||||
cursor="pointer"
|
borderRadius={4}
|
||||||
borderColor="gray.200"
|
_hover={{ bg: "gray.100" }}
|
||||||
borderWidth={1}
|
transition="background 0.2s"
|
||||||
p={4}
|
aspectRatio={1.2}
|
||||||
onClick={createExperiment}
|
>
|
||||||
>
|
<VStack align="center" justify="center" w="full" h="full" p={4} onClick={createExperiment}>
|
||||||
<Icon as={isLoading ? Spinner : BsPlusSquare} boxSize={8} />
|
<Icon as={isLoading ? Spinner : BsPlusSquare} boxSize={8} />
|
||||||
<Text display={{ base: "none", md: "block" }} ml={2}>
|
<Text display={{ base: "none", md: "block" }} ml={2}>
|
||||||
New Experiment
|
New Experiment
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</AspectRatio>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExperimentCardSkeleton = () => (
|
export const ExperimentCardSkeleton = () => (
|
||||||
<AspectRatio ratio={1.2} w="full">
|
<AspectRatio ratio={1.2} w="full">
|
||||||
<VStack align="center" borderColor="gray.200" borderWidth={1} p={4} bg="gray.50">
|
<VStack align="center" borderColor="gray.200" borderWidth={1} p={4} bg="white">
|
||||||
<SkeletonText noOfLines={1} w="80%" />
|
<SkeletonText noOfLines={1} w="80%" />
|
||||||
<SkeletonText noOfLines={2} w="60%" />
|
<SkeletonText noOfLines={2} w="60%" />
|
||||||
<SkeletonText noOfLines={1} w="80%" />
|
<SkeletonText noOfLines={1} w="80%" />
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ export const useOnForkButtonPressed = () => {
|
|||||||
|
|
||||||
const user = useSession().data;
|
const user = useSession().data;
|
||||||
const experiment = useExperiment();
|
const experiment = useExperiment();
|
||||||
const selectedOrgId = useAppStore((state) => state.selectedOrgId);
|
const selectedProjectId = useAppStore((state) => state.selectedProjectId);
|
||||||
|
|
||||||
const forkMutation = api.experiments.fork.useMutation();
|
const forkMutation = api.experiments.fork.useMutation();
|
||||||
|
|
||||||
const [onFork, isForking] = useHandledAsyncCallback(async () => {
|
const [onFork, isForking] = useHandledAsyncCallback(async () => {
|
||||||
if (!experiment.data?.id || !selectedOrgId) return;
|
if (!experiment.data?.id || !selectedProjectId) return;
|
||||||
const forkedExperimentId = await forkMutation.mutateAsync({
|
const forkedExperimentId = await forkMutation.mutateAsync({
|
||||||
id: experiment.data.id,
|
id: experiment.data.id,
|
||||||
organizationId: selectedOrgId,
|
projectId: selectedProjectId,
|
||||||
});
|
});
|
||||||
await router.push({ pathname: "/experiments/[id]", query: { id: forkedExperimentId } });
|
await router.push({ pathname: "/experiments/[id]", query: { id: forkedExperimentId } });
|
||||||
}, [forkMutation, experiment.data?.id, router]);
|
}, [forkMutation, experiment.data?.id, router]);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
Heading,
|
Heading,
|
||||||
VStack,
|
VStack,
|
||||||
@@ -9,14 +9,14 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Link as ChakraLink,
|
Link as ChakraLink,
|
||||||
Flex,
|
Flex,
|
||||||
|
useBreakpointValue,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { BsGearFill, BsGithub, BsPersonCircle } from "react-icons/bs";
|
import { BsGearFill, BsGithub, BsPersonCircle } from "react-icons/bs";
|
||||||
import { IoStatsChartOutline } from "react-icons/io5";
|
import { IoStatsChartOutline } from "react-icons/io5";
|
||||||
import { RiDatabase2Line, RiFlaskLine } from "react-icons/ri";
|
import { RiHome3Line, RiDatabase2Line, RiFlaskLine } from "react-icons/ri";
|
||||||
import { signIn, useSession } from "next-auth/react";
|
import { signIn, useSession } from "next-auth/react";
|
||||||
import UserMenu from "./UserMenu";
|
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env.mjs";
|
||||||
import ProjectMenu from "./ProjectMenu";
|
import ProjectMenu from "./ProjectMenu";
|
||||||
import NavSidebarOption from "./NavSidebarOption";
|
import NavSidebarOption from "./NavSidebarOption";
|
||||||
@@ -27,10 +27,16 @@ const Divider = () => <Box h="1px" bgColor="gray.300" w="full" />;
|
|||||||
const NavSidebar = () => {
|
const NavSidebar = () => {
|
||||||
const user = useSession().data;
|
const user = useSession().data;
|
||||||
|
|
||||||
|
// Hack to get around initial flash, see https://github.com/chakra-ui/chakra-ui/issues/6452
|
||||||
|
const isMobile = useBreakpointValue({ base: true, md: false, ssr: false });
|
||||||
|
const renderCount = useRef(0);
|
||||||
|
renderCount.current++;
|
||||||
|
|
||||||
|
const displayLogo = isMobile && renderCount.current > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack
|
<VStack
|
||||||
align="stretch"
|
align="stretch"
|
||||||
bgColor="gray.50"
|
|
||||||
py={2}
|
py={2}
|
||||||
px={2}
|
px={2}
|
||||||
pb={0}
|
pb={0}
|
||||||
@@ -40,25 +46,59 @@ const NavSidebar = () => {
|
|||||||
borderRightWidth={1}
|
borderRightWidth={1}
|
||||||
borderColor="gray.300"
|
borderColor="gray.300"
|
||||||
>
|
>
|
||||||
<HStack as={Link} href="/" _hover={{ textDecoration: "none" }} spacing={0} px={2} py={2}>
|
{displayLogo && (
|
||||||
<Image src="/logo.svg" alt="" boxSize={6} mr={4} />
|
<>
|
||||||
<Heading size="md" fontFamily="inconsolata, monospace">
|
<HStack
|
||||||
OpenPipe
|
as={Link}
|
||||||
</Heading>
|
href="/"
|
||||||
</HStack>
|
_hover={{ textDecoration: "none" }}
|
||||||
<Divider />
|
spacing={{ base: 1, md: 0 }}
|
||||||
|
mx={2}
|
||||||
|
py={{ base: 1, md: 2 }}
|
||||||
|
>
|
||||||
|
<Image src="/logo.svg" alt="" boxSize={6} mr={4} ml={{ base: 0.5, md: 0 }} />
|
||||||
|
<Heading size="md" fontFamily="inconsolata, monospace">
|
||||||
|
OpenPipe
|
||||||
|
</Heading>
|
||||||
|
</HStack>
|
||||||
|
<Divider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<VStack align="flex-start" overflowY="auto" overflowX="hidden" flex={1}>
|
<VStack align="flex-start" overflowY="auto" overflowX="hidden" flex={1}>
|
||||||
{user != null && (
|
{user != null && (
|
||||||
<>
|
<>
|
||||||
<ProjectMenu />
|
<ProjectMenu />
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
{env.NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS && (
|
{env.NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS && (
|
||||||
<IconLink icon={IoStatsChartOutline} label="Logged Calls" href="/logged-calls" beta />
|
<>
|
||||||
|
<IconLink icon={RiHome3Line} label="Dashboard" href="/dashboard" beta />
|
||||||
|
<IconLink
|
||||||
|
icon={IoStatsChartOutline}
|
||||||
|
label="Request Logs"
|
||||||
|
href="/request-logs"
|
||||||
|
beta
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
|
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
|
||||||
{env.NEXT_PUBLIC_SHOW_DATA && (
|
{env.NEXT_PUBLIC_SHOW_DATA && (
|
||||||
<IconLink icon={RiDatabase2Line} label="Data" href="/data" />
|
<IconLink icon={RiDatabase2Line} label="Data" href="/data" />
|
||||||
)}
|
)}
|
||||||
|
<VStack w="full" alignItems="flex-start" spacing={0} pt={8}>
|
||||||
|
<Text
|
||||||
|
pl={2}
|
||||||
|
pb={2}
|
||||||
|
fontSize="xs"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="gray.500"
|
||||||
|
display={{ base: "none", md: "flex" }}
|
||||||
|
>
|
||||||
|
CONFIGURATION
|
||||||
|
</Text>
|
||||||
|
<IconLink icon={BsGearFill} label="Project Settings" href="/project/settings" />
|
||||||
|
</VStack>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{user === null && (
|
{user === null && (
|
||||||
@@ -80,20 +120,7 @@ const NavSidebar = () => {
|
|||||||
</NavSidebarOption>
|
</NavSidebarOption>
|
||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack w="full" alignItems="flex-start" spacing={0}>
|
|
||||||
<Text
|
|
||||||
pl={2}
|
|
||||||
pb={2}
|
|
||||||
fontSize="xs"
|
|
||||||
fontWeight="bold"
|
|
||||||
color="gray.500"
|
|
||||||
display={{ base: "none", md: "flex" }}
|
|
||||||
>
|
|
||||||
CONFIGURATION
|
|
||||||
</Text>
|
|
||||||
<IconLink icon={BsGearFill} label="Project Settings" href="/project/settings" />
|
|
||||||
</VStack>
|
|
||||||
{user && <UserMenu user={user} borderColor={"gray.200"} />}
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<VStack spacing={0} align="center">
|
<VStack spacing={0} align="center">
|
||||||
<ChakraLink
|
<ChakraLink
|
||||||
@@ -153,7 +180,7 @@ export default function AppShell({
|
|||||||
<title>{title ? `${title} | OpenPipe` : "OpenPipe"}</title>
|
<title>{title ? `${title} | OpenPipe` : "OpenPipe"}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<NavSidebar />
|
<NavSidebar />
|
||||||
<Box h="100%" flex={1} overflowY="auto">
|
<Box h="100%" flex={1} overflowY="auto" bgColor="gray.50">
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Box, type BoxProps } from "@chakra-ui/react";
|
import { Box, type BoxProps, forwardRef } from "@chakra-ui/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
const NavSidebarOption = ({
|
const NavSidebarOption = forwardRef<
|
||||||
activeHrefPattern,
|
{ activeHrefPattern?: string; disableHoverEffect?: boolean } & BoxProps,
|
||||||
disableHoverEffect,
|
"div"
|
||||||
...props
|
>(({ activeHrefPattern, disableHoverEffect, ...props }, ref) => {
|
||||||
}: { activeHrefPattern?: string; disableHoverEffect?: boolean } & BoxProps) => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isActive = activeHrefPattern && router.pathname.startsWith(activeHrefPattern);
|
const isActive = activeHrefPattern && router.pathname.startsWith(activeHrefPattern);
|
||||||
return (
|
return (
|
||||||
@@ -18,10 +17,13 @@ const NavSidebarOption = ({
|
|||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
borderRadius={4}
|
borderRadius={4}
|
||||||
{...props}
|
{...props}
|
||||||
|
ref={ref}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
NavSidebarOption.displayName = "NavSidebarOption";
|
||||||
|
|
||||||
export default NavSidebarOption;
|
export default NavSidebarOption;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { HStack, Flex, Text } from "@chakra-ui/react";
|
import { HStack, Flex, Text } from "@chakra-ui/react";
|
||||||
import { useSelectedOrg } from "~/utils/hooks";
|
import { useSelectedProject } from "~/utils/hooks";
|
||||||
|
|
||||||
// Have to export only contents here instead of full BreadcrumbItem because Chakra doesn't
|
// Have to export only contents here instead of full BreadcrumbItem because Chakra doesn't
|
||||||
// recognize a BreadcrumbItem exported with this component as a valid child of Breadcrumb.
|
// recognize a BreadcrumbItem exported with this component as a valid child of Breadcrumb.
|
||||||
export default function ProjectBreadcrumbContents({ orgName = "" }: { orgName?: string }) {
|
export default function ProjectBreadcrumbContents({ projectName = "" }: { projectName?: string }) {
|
||||||
const { data: selectedOrg } = useSelectedOrg();
|
const { data: selectedProject } = useSelectedProject();
|
||||||
|
|
||||||
orgName = orgName || selectedOrg?.name || "";
|
projectName = projectName || selectedProject?.name || "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack w="full">
|
<HStack w="full">
|
||||||
@@ -18,10 +18,10 @@ export default function ProjectBreadcrumbContents({ orgName = "" }: { orgName?:
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
>
|
>
|
||||||
<Text>{orgName[0]?.toUpperCase()}</Text>
|
<Text>{projectName[0]?.toUpperCase()}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Text display={{ base: "none", md: "block" }} py={1}>
|
<Text display={{ base: "none", md: "block" }} py={1}>
|
||||||
{orgName}
|
{projectName}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,74 +6,78 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
Flex,
|
Flex,
|
||||||
IconButton,
|
|
||||||
Icon,
|
Icon,
|
||||||
Divider,
|
Divider,
|
||||||
Button,
|
Button,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
Link as ChakraLink,
|
||||||
|
Image,
|
||||||
|
Box,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AiFillCaretDown } from "react-icons/ai";
|
import { BsPlus, BsPersonCircle } from "react-icons/bs";
|
||||||
import { BsGear, BsPlus } from "react-icons/bs";
|
import { type Project } from "@prisma/client";
|
||||||
import { type Organization } from "@prisma/client";
|
|
||||||
|
|
||||||
import { useAppStore } from "~/state/store";
|
import { useAppStore } from "~/state/store";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import NavSidebarOption from "./NavSidebarOption";
|
import NavSidebarOption from "./NavSidebarOption";
|
||||||
import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks";
|
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useSession, signOut } from "next-auth/react";
|
||||||
|
|
||||||
export default function ProjectMenu() {
|
export default function ProjectMenu() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isActive = router.pathname.startsWith("/home");
|
|
||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
|
|
||||||
const selectedOrgId = useAppStore((s) => s.selectedOrgId);
|
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
|
||||||
const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId);
|
const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId);
|
||||||
|
|
||||||
const { data: orgs } = api.organizations.list.useQuery();
|
const { data: projects } = api.projects.list.useQuery();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (orgs && orgs[0] && (!selectedOrgId || !orgs.find((org) => org.id === selectedOrgId))) {
|
if (
|
||||||
setSelectedOrgId(orgs[0].id);
|
projects &&
|
||||||
|
projects[0] &&
|
||||||
|
(!selectedProjectId || !projects.find((proj) => proj.id === selectedProjectId))
|
||||||
|
) {
|
||||||
|
setSelectedProjectId(projects[0].id);
|
||||||
}
|
}
|
||||||
}, [selectedOrgId, setSelectedOrgId, orgs]);
|
}, [selectedProjectId, setSelectedProjectId, projects]);
|
||||||
|
|
||||||
const { data: selectedOrg } = useSelectedOrg();
|
const { data: selectedProject } = useSelectedProject();
|
||||||
|
|
||||||
const popover = useDisclosure();
|
const popover = useDisclosure();
|
||||||
|
|
||||||
const createMutation = api.organizations.create.useMutation();
|
const createMutation = api.projects.create.useMutation();
|
||||||
const [createProject, isLoading] = useHandledAsyncCallback(async () => {
|
const [createProject, isLoading] = useHandledAsyncCallback(async () => {
|
||||||
const newOrg = await createMutation.mutateAsync({ name: "New Project" });
|
const newProj = await createMutation.mutateAsync({ name: "Untitled Project" });
|
||||||
await utils.organizations.list.invalidate();
|
await utils.projects.list.invalidate();
|
||||||
setSelectedOrgId(newOrg.id);
|
setSelectedProjectId(newProj.id);
|
||||||
await router.push({ pathname: "/project/settings" });
|
await router.push({ pathname: "/project/settings" });
|
||||||
}, [createMutation, router]);
|
}, [createMutation, router]);
|
||||||
|
|
||||||
|
const user = useSession().data;
|
||||||
|
|
||||||
|
const profileImage = user?.user.image ? (
|
||||||
|
<Image src={user.user.image} alt="profile picture" boxSize={6} borderRadius="50%" />
|
||||||
|
) : (
|
||||||
|
<Icon as={BsPersonCircle} boxSize={6} />
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack w="full" alignItems="flex-start" spacing={0}>
|
<VStack w="full" alignItems="flex-start" spacing={0} py={1}>
|
||||||
<Text
|
<Popover
|
||||||
pl={2}
|
placement="bottom"
|
||||||
pb={2}
|
isOpen={popover.isOpen}
|
||||||
fontSize="xs"
|
onOpen={popover.onOpen}
|
||||||
fontWeight="bold"
|
onClose={popover.onClose}
|
||||||
color="gray.500"
|
closeOnBlur
|
||||||
display={{ base: "none", md: "flex" }}
|
|
||||||
>
|
>
|
||||||
PROJECT
|
<PopoverTrigger>
|
||||||
</Text>
|
<NavSidebarOption>
|
||||||
<NavSidebarOption>
|
<HStack w="full">
|
||||||
<Popover
|
|
||||||
placement="bottom-start"
|
|
||||||
isOpen={popover.isOpen}
|
|
||||||
onClose={popover.onClose}
|
|
||||||
closeOnBlur
|
|
||||||
>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<HStack w="full" justifyContent="space-between" onClick={popover.onToggle}>
|
|
||||||
<Flex
|
<Flex
|
||||||
p={1}
|
p={1}
|
||||||
borderRadius={4}
|
borderRadius={4}
|
||||||
@@ -83,96 +87,115 @@ export default function ProjectMenu() {
|
|||||||
m={{ base: 0, md: 1 }}
|
m={{ base: 0, md: 1 }}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
// onClick={sidebarExpanded ? undefined : openMenu}
|
|
||||||
>
|
>
|
||||||
<Text>{selectedOrg?.name[0]?.toUpperCase()}</Text>
|
<Text>{selectedProject?.name[0]?.toUpperCase()}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Text fontSize="sm" display={{ base: "none", md: "block" }} py={1}>
|
<Text
|
||||||
{selectedOrg?.name}
|
fontSize="sm"
|
||||||
|
display={{ base: "none", md: "block" }}
|
||||||
|
py={1}
|
||||||
|
flex={1}
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
|
{selectedProject?.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Icon as={AiFillCaretDown} boxSize={3} size="xs" color="gray.500" mr={2} />
|
<Box mr={2}>{profileImage}</Box>
|
||||||
</HStack>
|
</HStack>
|
||||||
</PopoverTrigger>
|
</NavSidebarOption>
|
||||||
<PopoverContent
|
</PopoverTrigger>
|
||||||
_focusVisible={{ boxShadow: "unset" }}
|
<PopoverContent
|
||||||
minW={0}
|
_focusVisible={{ outline: "unset" }}
|
||||||
borderColor="blue.400"
|
ml={-1}
|
||||||
w="full"
|
w={224}
|
||||||
>
|
boxShadow="0 0 40px 4px rgba(0, 0, 0, 0.1);"
|
||||||
<VStack alignItems="flex-start" spacing={2} py={4} px={2}>
|
fontSize="sm"
|
||||||
<Text color="gray.500" fontSize="xs" fontWeight="bold" pb={1}>
|
>
|
||||||
PROJECTS
|
<VStack alignItems="flex-start" spacing={1} py={1}>
|
||||||
</Text>
|
<Text px={3} py={2}>
|
||||||
<Divider />
|
{user?.user.email}
|
||||||
<VStack spacing={0} w="full">
|
</Text>
|
||||||
{orgs?.map((org) => (
|
<Divider />
|
||||||
<ProjectOption
|
<Text alignSelf="flex-start" fontWeight="bold" px={3} pt={2}>
|
||||||
key={org.id}
|
Your Projects
|
||||||
org={org}
|
</Text>
|
||||||
isActive={org.id === selectedOrgId}
|
<VStack spacing={0} w="full" px={1}>
|
||||||
onClose={popover.onClose}
|
{projects?.map((proj) => (
|
||||||
/>
|
<ProjectOption
|
||||||
))}
|
key={proj.id}
|
||||||
</VStack>
|
proj={proj}
|
||||||
|
isActive={proj.id === selectedProjectId}
|
||||||
|
onClose={popover.onClose}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
<HStack
|
<HStack
|
||||||
as={Button}
|
as={Button}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
color="blue.400"
|
color="blue.400"
|
||||||
pr={8}
|
fontSize="sm"
|
||||||
w="full"
|
justifyContent="flex-start"
|
||||||
onClick={createProject}
|
onClick={createProject}
|
||||||
|
w="full"
|
||||||
|
borderRadius={4}
|
||||||
|
spacing={0}
|
||||||
>
|
>
|
||||||
<Icon as={isLoading ? Spinner : BsPlus} boxSize={6} />
|
<Text>Add project</Text>
|
||||||
<Text>New project</Text>
|
<Icon as={isLoading ? Spinner : BsPlus} boxSize={4} strokeWidth={0.5} />
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
<Divider />
|
||||||
</NavSidebarOption>
|
<VStack w="full" px={1}>
|
||||||
|
<ChakraLink
|
||||||
|
onClick={() => {
|
||||||
|
signOut().catch(console.error);
|
||||||
|
}}
|
||||||
|
_hover={{ bgColor: "gray.200", textDecoration: "none" }}
|
||||||
|
w="full"
|
||||||
|
py={2}
|
||||||
|
px={2}
|
||||||
|
borderRadius={4}
|
||||||
|
>
|
||||||
|
<Text>Sign out</Text>
|
||||||
|
</ChakraLink>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProjectOption = ({
|
const ProjectOption = ({
|
||||||
org,
|
proj,
|
||||||
isActive,
|
isActive,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
org: Organization;
|
proj: Project;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId);
|
const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId);
|
||||||
const [gearHovered, setGearHovered] = useState(false);
|
const [gearHovered, setGearHovered] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
as={Link}
|
as={Link}
|
||||||
href="/experiments"
|
href="/experiments"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedOrgId(org.id);
|
setSelectedProjectId(proj.id);
|
||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
w="full"
|
w="full"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
bgColor={isActive ? "gray.100" : "transparent"}
|
|
||||||
_hover={gearHovered ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
|
_hover={gearHovered ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
|
||||||
p={2}
|
color={isActive ? "blue.400" : undefined}
|
||||||
|
py={2}
|
||||||
|
px={4}
|
||||||
|
borderRadius={4}
|
||||||
|
spacing={4}
|
||||||
>
|
>
|
||||||
<Text>{org.name}</Text>
|
<Text>{proj.name}</Text>
|
||||||
<IconButton
|
|
||||||
as={Link}
|
|
||||||
href="/project/settings"
|
|
||||||
aria-label={`Open ${org.name} settings`}
|
|
||||||
icon={<Icon as={BsGear} boxSize={5} strokeWidth={0.5} color="gray.500" />}
|
|
||||||
variant="ghost"
|
|
||||||
size="xs"
|
|
||||||
p={0}
|
|
||||||
onMouseEnter={() => setGearHovered(true)}
|
|
||||||
onMouseLeave={() => setGearHovered(false)}
|
|
||||||
_hover={{ bgColor: isActive ? "gray.300" : "gray.100", transitionDelay: 0 }}
|
|
||||||
borderRadius={4}
|
|
||||||
/>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
Link,
|
Link,
|
||||||
type StackProps,
|
type StackProps,
|
||||||
Box,
|
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { type Session } from "next-auth";
|
import { type Session } from "next-auth";
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
@@ -27,30 +26,28 @@ export default function UserMenu({ user, ...rest }: { user: Session } & StackPro
|
|||||||
<>
|
<>
|
||||||
<Popover placement="right">
|
<Popover placement="right">
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Box>
|
<NavSidebarOption>
|
||||||
<NavSidebarOption>
|
<HStack
|
||||||
<HStack
|
// Weird values to make mobile look right; can clean up when we make the sidebar disappear on mobile
|
||||||
// Weird values to make mobile look right; can clean up when we make the sidebar disappear on mobile
|
py={2}
|
||||||
py={2}
|
px={1}
|
||||||
px={1}
|
spacing={3}
|
||||||
spacing={3}
|
{...rest}
|
||||||
{...rest}
|
>
|
||||||
>
|
{profileImage}
|
||||||
{profileImage}
|
<VStack spacing={0} align="start" flex={1} flexShrink={1}>
|
||||||
<VStack spacing={0} align="start" flex={1} flexShrink={1}>
|
<Text fontWeight="bold" fontSize="sm">
|
||||||
<Text fontWeight="bold" fontSize="sm">
|
{user.user.name}
|
||||||
{user.user.name}
|
</Text>
|
||||||
</Text>
|
<Text color="gray.500" fontSize="xs">
|
||||||
<Text color="gray.500" fontSize="xs">
|
{/* {user.user.email} */}
|
||||||
{/* {user.user.email} */}
|
</Text>
|
||||||
</Text>
|
</VStack>
|
||||||
</VStack>
|
<Icon as={BsChevronRight} boxSize={4} color="gray.500" />
|
||||||
<Icon as={BsChevronRight} boxSize={4} color="gray.500" />
|
</HStack>
|
||||||
</HStack>
|
</NavSidebarOption>
|
||||||
</NavSidebarOption>
|
|
||||||
</Box>
|
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent _focusVisible={{ boxShadow: "unset", outline: "unset" }} maxW="200px">
|
<PopoverContent _focusVisible={{ outline: "unset" }} ml={-1} minW={48} w="full">
|
||||||
<VStack align="stretch" spacing={0}>
|
<VStack align="stretch" spacing={0}>
|
||||||
{/* sign out */}
|
{/* sign out */}
|
||||||
<HStack
|
<HStack
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks";
|
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
|
||||||
|
|
||||||
export const DeleteProjectDialog = ({
|
export const DeleteProjectDialog = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
@@ -25,20 +25,20 @@ export const DeleteProjectDialog = ({
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const selectedOrg = useSelectedOrg();
|
const selectedProject = useSelectedProject();
|
||||||
const deleteMutation = api.organizations.delete.useMutation();
|
const deleteMutation = api.projects.delete.useMutation();
|
||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const [onDeleteConfirm, isDeleting] = useHandledAsyncCallback(async () => {
|
const [onDeleteConfirm, isDeleting] = useHandledAsyncCallback(async () => {
|
||||||
if (!selectedOrg.data?.id) return;
|
if (!selectedProject.data?.id) return;
|
||||||
await deleteMutation.mutateAsync({ id: selectedOrg.data.id });
|
await deleteMutation.mutateAsync({ id: selectedProject.data.id });
|
||||||
await utils.organizations.list.invalidate();
|
await utils.projects.list.invalidate();
|
||||||
await router.push({ pathname: "/experiments" });
|
await router.push({ pathname: "/experiments" });
|
||||||
onClose();
|
onClose();
|
||||||
}, [deleteMutation, selectedOrg, router]);
|
}, [deleteMutation, selectedProject, router]);
|
||||||
|
|
||||||
const [nameToDelete, setNameToDelete] = useState("");
|
const [nameToDelete, setNameToDelete] = useState("");
|
||||||
|
|
||||||
@@ -58,10 +58,10 @@ export const DeleteProjectDialog = ({
|
|||||||
of the project below.
|
of the project below.
|
||||||
</Text>
|
</Text>
|
||||||
<Box bgColor="orange.100" w="full" p={2} borderRadius={4}>
|
<Box bgColor="orange.100" w="full" p={2} borderRadius={4}>
|
||||||
<Text fontFamily="inconsolata">{selectedOrg.data?.name}</Text>
|
<Text fontFamily="inconsolata">{selectedProject.data?.name}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Input
|
<Input
|
||||||
placeholder={selectedOrg.data?.name}
|
placeholder={selectedProject.data?.name}
|
||||||
value={nameToDelete}
|
value={nameToDelete}
|
||||||
onChange={(e) => setNameToDelete(e.target.value)}
|
onChange={(e) => setNameToDelete(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -76,7 +76,7 @@ export const DeleteProjectDialog = ({
|
|||||||
colorScheme="red"
|
colorScheme="red"
|
||||||
onClick={onDeleteConfirm}
|
onClick={onDeleteConfirm}
|
||||||
ml={3}
|
ml={3}
|
||||||
isDisabled={nameToDelete !== selectedOrg.data?.name}
|
isDisabled={nameToDelete !== selectedProject.data?.name}
|
||||||
w={20}
|
w={20}
|
||||||
>
|
>
|
||||||
{isDeleting ? <Spinner /> : "Delete"}
|
{isDeleting ? <Spinner /> : "Delete"}
|
||||||
|
|||||||
30
app/src/components/requestLogs/ActionButton.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Button, HStack, type ButtonProps, Icon, Text } from "@chakra-ui/react";
|
||||||
|
import { type IconType } from "react-icons";
|
||||||
|
|
||||||
|
const ActionButton = ({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
...buttonProps
|
||||||
|
}: { icon: IconType; label: string } & ButtonProps) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
color="black"
|
||||||
|
bgColor="white"
|
||||||
|
borderColor="gray.300"
|
||||||
|
borderRadius={4}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
fontSize="sm"
|
||||||
|
fontWeight="normal"
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
{icon && <Icon as={icon} />}
|
||||||
|
<Text>{label}</Text>
|
||||||
|
</HStack>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActionButton;
|
||||||
55
app/src/components/requestLogs/FormattedJson.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Box, IconButton, useToast } from "@chakra-ui/react";
|
||||||
|
import { CopyIcon } from "lucide-react";
|
||||||
|
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||||
|
import { atelierCaveLight } from "react-syntax-highlighter/dist/cjs/styles/hljs";
|
||||||
|
import stringify from "json-stringify-pretty-compact";
|
||||||
|
|
||||||
|
const FormattedJson = ({ json }: { json: any }) => {
|
||||||
|
const jsonString = stringify(json, { maxLength: 40 });
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const copyToClipboard = async (text: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
toast({
|
||||||
|
title: "Copied to clipboard",
|
||||||
|
status: "success",
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
toast({
|
||||||
|
title: "Failed to copy to clipboard",
|
||||||
|
status: "error",
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box position="relative" fontSize="sm" borderRadius="md" overflow="hidden">
|
||||||
|
<SyntaxHighlighter
|
||||||
|
customStyle={{ overflowX: "unset" }}
|
||||||
|
language="json"
|
||||||
|
style={atelierCaveLight}
|
||||||
|
lineProps={{
|
||||||
|
style: { wordBreak: "break-all", whiteSpace: "pre-wrap" },
|
||||||
|
}}
|
||||||
|
wrapLines
|
||||||
|
>
|
||||||
|
{jsonString}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Copy"
|
||||||
|
icon={<CopyIcon />}
|
||||||
|
position="absolute"
|
||||||
|
top={1}
|
||||||
|
right={1}
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => void copyToClipboard(jsonString)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { FormattedJson };
|
||||||
16
app/src/components/requestLogs/LoggedCallsPaginator.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { type StackProps } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { useLoggedCalls } from "~/utils/hooks";
|
||||||
|
import Paginator from "../Paginator";
|
||||||
|
|
||||||
|
const LoggedCallsPaginator = (props: StackProps) => {
|
||||||
|
const { data } = useLoggedCalls();
|
||||||
|
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
const { count } = data;
|
||||||
|
|
||||||
|
return <Paginator count={count} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoggedCallsPaginator;
|
||||||
36
app/src/components/requestLogs/LoggedCallsTable.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Card, Table, Tbody } from "@chakra-ui/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useLoggedCalls } from "~/utils/hooks";
|
||||||
|
import { TableHeader, TableRow } from "./TableRow";
|
||||||
|
|
||||||
|
export default function LoggedCallsTable() {
|
||||||
|
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
||||||
|
const { data: loggedCalls } = useLoggedCalls();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card width="100%" overflow="hidden">
|
||||||
|
<Table>
|
||||||
|
<TableHeader showCheckbox />
|
||||||
|
<Tbody>
|
||||||
|
{loggedCalls?.calls.map((loggedCall) => {
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={loggedCall.id}
|
||||||
|
loggedCall={loggedCall}
|
||||||
|
isExpanded={loggedCall.id === expandedRow}
|
||||||
|
onToggle={() => {
|
||||||
|
if (loggedCall.id === expandedRow) {
|
||||||
|
setExpandedRow(null);
|
||||||
|
} else {
|
||||||
|
setExpandedRow(loggedCall.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
showCheckbox
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
164
app/src/components/requestLogs/TableRow.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Heading,
|
||||||
|
Td,
|
||||||
|
Tr,
|
||||||
|
Thead,
|
||||||
|
Th,
|
||||||
|
Tooltip,
|
||||||
|
Collapse,
|
||||||
|
HStack,
|
||||||
|
VStack,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Text,
|
||||||
|
Checkbox,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { type RouterOutputs } from "~/utils/api";
|
||||||
|
import { FormattedJson } from "./FormattedJson";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
import { useLoggedCalls } from "~/utils/hooks";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
type LoggedCall = RouterOutputs["loggedCalls"]["list"]["calls"][0];
|
||||||
|
|
||||||
|
export const TableHeader = ({ showCheckbox }: { showCheckbox?: boolean }) => {
|
||||||
|
const matchingLogIds = useLoggedCalls().data?.matchingLogIds;
|
||||||
|
const selectedLogIds = useAppStore((s) => s.selectedLogs.selectedLogIds);
|
||||||
|
const addAll = useAppStore((s) => s.selectedLogs.addSelectedLogIds);
|
||||||
|
const clearAll = useAppStore((s) => s.selectedLogs.clearSelectedLogIds);
|
||||||
|
const allSelected = useMemo(() => {
|
||||||
|
if (!matchingLogIds) return false;
|
||||||
|
return matchingLogIds.every((id) => selectedLogIds.has(id));
|
||||||
|
}, [selectedLogIds, matchingLogIds]);
|
||||||
|
return (
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
{showCheckbox && (
|
||||||
|
<Th>
|
||||||
|
<HStack w={8}>
|
||||||
|
<Checkbox
|
||||||
|
isChecked={allSelected}
|
||||||
|
onChange={() => {
|
||||||
|
allSelected ? clearAll() : addAll(matchingLogIds || []);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text>({selectedLogIds.size})</Text>
|
||||||
|
</HStack>
|
||||||
|
</Th>
|
||||||
|
)}
|
||||||
|
<Th>Time</Th>
|
||||||
|
<Th>Model</Th>
|
||||||
|
<Th isNumeric>Duration</Th>
|
||||||
|
<Th isNumeric>Input tokens</Th>
|
||||||
|
<Th isNumeric>Output tokens</Th>
|
||||||
|
<Th isNumeric>Status</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TableRow = ({
|
||||||
|
loggedCall,
|
||||||
|
isExpanded,
|
||||||
|
onToggle,
|
||||||
|
showCheckbox,
|
||||||
|
}: {
|
||||||
|
loggedCall: LoggedCall;
|
||||||
|
isExpanded: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
showCheckbox?: boolean;
|
||||||
|
}) => {
|
||||||
|
const isError = loggedCall.modelResponse?.statusCode !== 200;
|
||||||
|
const timeAgo = dayjs(loggedCall.requestedAt).fromNow();
|
||||||
|
const fullTime = dayjs(loggedCall.requestedAt).toString();
|
||||||
|
|
||||||
|
const durationCell = (
|
||||||
|
<Td isNumeric>
|
||||||
|
{loggedCall.cacheHit ? (
|
||||||
|
<Text color="gray.500">Cached</Text>
|
||||||
|
) : (
|
||||||
|
((loggedCall.modelResponse?.durationMs ?? 0) / 1000).toFixed(2) + "s"
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
);
|
||||||
|
|
||||||
|
const isChecked = useAppStore((s) => s.selectedLogs.selectedLogIds.has(loggedCall.id));
|
||||||
|
const toggleChecked = useAppStore((s) => s.selectedLogs.toggleSelectedLogId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tr
|
||||||
|
onClick={onToggle}
|
||||||
|
key={loggedCall.id}
|
||||||
|
_hover={{ bgColor: "gray.50", cursor: "pointer" }}
|
||||||
|
sx={{
|
||||||
|
"> td": { borderBottom: "none" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showCheckbox && (
|
||||||
|
<Td>
|
||||||
|
<Checkbox isChecked={isChecked} onChange={() => toggleChecked(loggedCall.id)} />
|
||||||
|
</Td>
|
||||||
|
)}
|
||||||
|
<Td>
|
||||||
|
<Tooltip label={fullTime} placement="top">
|
||||||
|
<Box whiteSpace="nowrap" minW="120px">
|
||||||
|
{timeAgo}
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
</Td>
|
||||||
|
<Td width="100%">
|
||||||
|
<HStack justifyContent="flex-start">
|
||||||
|
<Text
|
||||||
|
colorScheme="purple"
|
||||||
|
color="purple.500"
|
||||||
|
borderColor="purple.500"
|
||||||
|
px={1}
|
||||||
|
borderRadius={4}
|
||||||
|
borderWidth={1}
|
||||||
|
fontSize="xs"
|
||||||
|
>
|
||||||
|
{loggedCall.model}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
{durationCell}
|
||||||
|
<Td isNumeric>{loggedCall.modelResponse?.inputTokens}</Td>
|
||||||
|
<Td isNumeric>{loggedCall.modelResponse?.outputTokens}</Td>
|
||||||
|
<Td sx={{ color: isError ? "red.500" : "green.500", fontWeight: "semibold" }} isNumeric>
|
||||||
|
{loggedCall.modelResponse?.statusCode ?? "No response"}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={8} p={0}>
|
||||||
|
<Collapse in={isExpanded} unmountOnExit={true}>
|
||||||
|
<VStack p={4} align="stretch">
|
||||||
|
<HStack align="stretch">
|
||||||
|
<VStack flex={1} align="stretch">
|
||||||
|
<Heading size="sm">Input</Heading>
|
||||||
|
<FormattedJson json={loggedCall.modelResponse?.reqPayload} />
|
||||||
|
</VStack>
|
||||||
|
<VStack flex={1} align="stretch">
|
||||||
|
<Heading size="sm">Output</Heading>
|
||||||
|
<FormattedJson json={loggedCall.modelResponse?.respPayload} />
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
<ButtonGroup alignSelf="flex-end">
|
||||||
|
<Button as={Link} colorScheme="blue" href={{ pathname: "/experiments" }}>
|
||||||
|
Experiments
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</VStack>
|
||||||
|
</Collapse>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,14 +2,14 @@ import { HStack, Icon, Text, Tooltip, type TooltipProps, VStack, Divider } from
|
|||||||
import { BsCurrencyDollar } from "react-icons/bs";
|
import { BsCurrencyDollar } from "react-icons/bs";
|
||||||
|
|
||||||
type CostTooltipProps = {
|
type CostTooltipProps = {
|
||||||
promptTokens: number | null;
|
inputTokens: number | null;
|
||||||
completionTokens: number | null;
|
outputTokens: number | null;
|
||||||
cost: number;
|
cost: number;
|
||||||
} & TooltipProps;
|
} & TooltipProps;
|
||||||
|
|
||||||
export const CostTooltip = ({
|
export const CostTooltip = ({
|
||||||
promptTokens,
|
inputTokens,
|
||||||
completionTokens,
|
outputTokens,
|
||||||
cost,
|
cost,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
@@ -36,12 +36,12 @@ export const CostTooltip = ({
|
|||||||
<HStack>
|
<HStack>
|
||||||
<VStack w="28" spacing={1}>
|
<VStack w="28" spacing={1}>
|
||||||
<Text>Prompt</Text>
|
<Text>Prompt</Text>
|
||||||
<Text>{promptTokens ?? 0}</Text>
|
<Text>{inputTokens ?? 0}</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<Divider borderColor="gray.200" h={8} orientation="vertical" />
|
<Divider borderColor="gray.200" h={8} orientation="vertical" />
|
||||||
<VStack w="28" spacing={1}>
|
<VStack w="28" spacing={1}>
|
||||||
<Text whiteSpace="nowrap">Completion</Text>
|
<Text whiteSpace="nowrap">Completion</Text>
|
||||||
<Text>{completionTokens ?? 0}</Text>
|
<Text>{outputTokens ?? 0}</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
"claude-2",
|
"claude-2",
|
||||||
"claude-2.0",
|
"claude-2.0",
|
||||||
"claude-instant-1",
|
"claude-instant-1",
|
||||||
"claude-instant-1.1"
|
"claude-instant-1.1",
|
||||||
|
"claude-instant-1.2"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"prompt": {
|
"prompt": {
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ const modelProvider: AnthropicProvider = {
|
|||||||
inputSchema: inputSchema as JSONSchema4,
|
inputSchema: inputSchema as JSONSchema4,
|
||||||
canStream: true,
|
canStream: true,
|
||||||
getCompletion,
|
getCompletion,
|
||||||
|
getUsage: (input, output) => {
|
||||||
|
// TODO: add usage logic
|
||||||
|
return null;
|
||||||
|
},
|
||||||
...frontendModelProvider,
|
...frontendModelProvider,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,10 @@ import {
|
|||||||
type ChatCompletion,
|
type ChatCompletion,
|
||||||
type CompletionCreateParams,
|
type CompletionCreateParams,
|
||||||
} from "openai/resources/chat";
|
} from "openai/resources/chat";
|
||||||
import { countOpenAIChatTokens } from "~/utils/countTokens";
|
|
||||||
import { type CompletionResponse } from "../types";
|
import { type CompletionResponse } from "../types";
|
||||||
import { isArray, isString, omit } from "lodash-es";
|
import { isArray, isString, omit } from "lodash-es";
|
||||||
import { openai } from "~/server/utils/openai";
|
import { openai } from "~/server/utils/openai";
|
||||||
import { truthyFilter } from "~/utils/utils";
|
|
||||||
import { APIError } from "openai";
|
import { APIError } from "openai";
|
||||||
import frontendModelProvider from "./frontend";
|
|
||||||
import modelProvider, { type SupportedModel } from ".";
|
|
||||||
|
|
||||||
const mergeStreamedChunks = (
|
const mergeStreamedChunks = (
|
||||||
base: ChatCompletion | null,
|
base: ChatCompletion | null,
|
||||||
@@ -60,9 +56,6 @@ export async function getCompletion(
|
|||||||
): Promise<CompletionResponse<ChatCompletion>> {
|
): Promise<CompletionResponse<ChatCompletion>> {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
let finalCompletion: ChatCompletion | null = null;
|
let finalCompletion: ChatCompletion | null = null;
|
||||||
let promptTokens: number | undefined = undefined;
|
|
||||||
let completionTokens: number | undefined = undefined;
|
|
||||||
const modelName = modelProvider.getModel(input) as SupportedModel;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (onStream) {
|
if (onStream) {
|
||||||
@@ -86,16 +79,6 @@ export async function getCompletion(
|
|||||||
autoRetry: false,
|
autoRetry: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
promptTokens = countOpenAIChatTokens(modelName, input.messages);
|
|
||||||
completionTokens = countOpenAIChatTokens(
|
|
||||||
modelName,
|
|
||||||
finalCompletion.choices.map((c) => c.message).filter(truthyFilter),
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
// TODO handle this, library seems like maybe it doesn't work with function calls?
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const resp = await openai.chat.completions.create(
|
const resp = await openai.chat.completions.create(
|
||||||
{ ...input, stream: false },
|
{ ...input, stream: false },
|
||||||
@@ -104,25 +87,14 @@ export async function getCompletion(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
finalCompletion = resp;
|
finalCompletion = resp;
|
||||||
promptTokens = resp.usage?.prompt_tokens ?? 0;
|
|
||||||
completionTokens = resp.usage?.completion_tokens ?? 0;
|
|
||||||
}
|
}
|
||||||
const timeToComplete = Date.now() - start;
|
const timeToComplete = Date.now() - start;
|
||||||
|
|
||||||
const { promptTokenPrice, completionTokenPrice } = frontendModelProvider.models[modelName];
|
|
||||||
let cost = undefined;
|
|
||||||
if (promptTokenPrice && completionTokenPrice && promptTokens && completionTokens) {
|
|
||||||
cost = promptTokens * promptTokenPrice + completionTokens * completionTokenPrice;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "success",
|
type: "success",
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
value: finalCompletion,
|
value: finalCompletion,
|
||||||
timeToComplete,
|
timeToComplete,
|
||||||
promptTokens,
|
|
||||||
completionTokens,
|
|
||||||
cost,
|
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof APIError) {
|
if (error instanceof APIError) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import inputSchema from "./codegen/input.schema.json";
|
|||||||
import { type ChatCompletion, type CompletionCreateParams } from "openai/resources/chat";
|
import { type ChatCompletion, type CompletionCreateParams } from "openai/resources/chat";
|
||||||
import { getCompletion } from "./getCompletion";
|
import { getCompletion } from "./getCompletion";
|
||||||
import frontendModelProvider from "./frontend";
|
import frontendModelProvider from "./frontend";
|
||||||
|
import { countOpenAIChatTokens } from "~/utils/countTokens";
|
||||||
|
import { truthyFilter } from "~/utils/utils";
|
||||||
|
|
||||||
const supportedModels = [
|
const supportedModels = [
|
||||||
"gpt-4-0613",
|
"gpt-4-0613",
|
||||||
@@ -39,6 +41,41 @@ const modelProvider: OpenaiChatModelProvider = {
|
|||||||
inputSchema: inputSchema as JSONSchema4,
|
inputSchema: inputSchema as JSONSchema4,
|
||||||
canStream: true,
|
canStream: true,
|
||||||
getCompletion,
|
getCompletion,
|
||||||
|
getUsage: (input, output) => {
|
||||||
|
if (output.choices.length === 0) return null;
|
||||||
|
|
||||||
|
const model = modelProvider.getModel(input);
|
||||||
|
if (!model) return null;
|
||||||
|
|
||||||
|
let inputTokens: number;
|
||||||
|
let outputTokens: number;
|
||||||
|
|
||||||
|
if (output.usage) {
|
||||||
|
inputTokens = output.usage.prompt_tokens;
|
||||||
|
outputTokens = output.usage.completion_tokens;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
inputTokens = countOpenAIChatTokens(model, input.messages);
|
||||||
|
outputTokens = countOpenAIChatTokens(
|
||||||
|
model,
|
||||||
|
output.choices.map((c) => c.message).filter(truthyFilter),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
inputTokens = 0;
|
||||||
|
outputTokens = 0;
|
||||||
|
// TODO handle this, library seems like maybe it doesn't work with function calls?
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { promptTokenPrice, completionTokenPrice } = frontendModelProvider.models[model];
|
||||||
|
let cost = undefined;
|
||||||
|
if (promptTokenPrice && completionTokenPrice && inputTokens && outputTokens) {
|
||||||
|
cost = inputTokens * promptTokenPrice + outputTokens * completionTokenPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { inputTokens: inputTokens, outputTokens: outputTokens, cost };
|
||||||
|
},
|
||||||
...frontendModelProvider,
|
...frontendModelProvider,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -120,9 +120,9 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
"Convert to function call": {
|
"Convert to function call": {
|
||||||
icon: TfiThought,
|
icon: TfiThought,
|
||||||
description: "Use function calls to get output from the model in a more structured way.",
|
description: "Use function calls to get output from the model in a more structured way.",
|
||||||
instructions: `OpenAI functions are a specialized way for an LLM to return output.
|
instructions: `OpenAI functions are a specialized way for an LLM to return its final output.
|
||||||
|
|
||||||
This is what a prompt looks like before adding a function:
|
Example 1 before:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-4",
|
model: "gpt-4",
|
||||||
@@ -139,7 +139,7 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
This is what one looks like after adding a function:
|
Example 1 after:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-4",
|
model: "gpt-4",
|
||||||
@@ -156,7 +156,7 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
],
|
],
|
||||||
functions: [
|
functions: [
|
||||||
{
|
{
|
||||||
name: "extract_sentiment",
|
name: "log_extracted_sentiment",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object", // parameters must always be an object with a properties key
|
type: "object", // parameters must always be an object with a properties key
|
||||||
properties: { // properties key is required
|
properties: { // properties key is required
|
||||||
@@ -169,13 +169,13 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
function_call: {
|
function_call: {
|
||||||
name: "extract_sentiment",
|
name: "log_extracted_sentiment",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Here's another example of adding a function:
|
=========
|
||||||
|
|
||||||
Before:
|
Example 2 before:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-3.5-turbo",
|
model: "gpt-3.5-turbo",
|
||||||
@@ -197,7 +197,7 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
temperature: 0,
|
temperature: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
After:
|
Example 2 after:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-3.5-turbo",
|
model: "gpt-3.5-turbo",
|
||||||
@@ -215,7 +215,7 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
temperature: 0,
|
temperature: 0,
|
||||||
functions: [
|
functions: [
|
||||||
{
|
{
|
||||||
name: "score_post",
|
name: "log_post_score",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -227,13 +227,13 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
function_call: {
|
function_call: {
|
||||||
name: "score_post",
|
name: "log_post_score",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Another example
|
=========
|
||||||
|
|
||||||
Before:
|
Example 3 before:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-3.5-turbo",
|
model: "gpt-3.5-turbo",
|
||||||
@@ -246,7 +246,7 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
After:
|
Example 3 after:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-3.5-turbo",
|
model: "gpt-3.5-turbo",
|
||||||
@@ -258,21 +258,24 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
],
|
],
|
||||||
functions: [
|
functions: [
|
||||||
{
|
{
|
||||||
name: "write_in_language",
|
name: "log_translated_text",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
text: {
|
translated_text: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
description: "The text, written in the language specified in the prompt",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
function_call: {
|
function_call: {
|
||||||
name: "write_in_language",
|
name: "log_translated_text",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
=========
|
||||||
|
|
||||||
Add an OpenAI function that takes one or more nested parameters that match the expected output from this prompt.`,
|
Add an OpenAI function that takes one or more nested parameters that match the expected output from this prompt.`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ const modelProvider: ReplicateLlama2Provider = {
|
|||||||
},
|
},
|
||||||
canStream: true,
|
canStream: true,
|
||||||
getCompletion,
|
getCompletion,
|
||||||
|
getUsage: (input, output) => {
|
||||||
|
// TODO: add usage logic
|
||||||
|
return null;
|
||||||
|
},
|
||||||
...frontendModelProvider,
|
...frontendModelProvider,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -43,9 +43,6 @@ export type CompletionResponse<T> =
|
|||||||
value: T;
|
value: T;
|
||||||
timeToComplete: number;
|
timeToComplete: number;
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
promptTokens?: number;
|
|
||||||
completionTokens?: number;
|
|
||||||
cost?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModelProvider<SupportedModels extends string, InputSchema, OutputSchema> = {
|
export type ModelProvider<SupportedModels extends string, InputSchema, OutputSchema> = {
|
||||||
@@ -56,6 +53,10 @@ export type ModelProvider<SupportedModels extends string, InputSchema, OutputSch
|
|||||||
input: InputSchema,
|
input: InputSchema,
|
||||||
onStream: ((partialOutput: OutputSchema) => void) | null,
|
onStream: ((partialOutput: OutputSchema) => void) | null,
|
||||||
) => Promise<CompletionResponse<OutputSchema>>;
|
) => Promise<CompletionResponse<OutputSchema>>;
|
||||||
|
getUsage: (
|
||||||
|
input: InputSchema,
|
||||||
|
output: OutputSchema,
|
||||||
|
) => { gpuRuntime?: number; inputTokens?: number; outputTokens?: number; cost?: number } | null;
|
||||||
|
|
||||||
// This is just a convenience for type inference, don't use it at runtime
|
// This is just a convenience for type inference, don't use it at runtime
|
||||||
_outputSchema?: OutputSchema | null;
|
_outputSchema?: OutputSchema | null;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { ChakraThemeProvider } from "~/theme/ChakraThemeProvider";
|
|||||||
import { SyncAppStore } from "~/state/sync";
|
import { SyncAppStore } from "~/state/sync";
|
||||||
import NextAdapterApp from "next-query-params/app";
|
import NextAdapterApp from "next-query-params/app";
|
||||||
import { QueryParamProvider } from "use-query-params";
|
import { QueryParamProvider } from "use-query-params";
|
||||||
import { SessionIdentifier } from "~/utils/analytics/clientAnalytics";
|
import { PosthogAppProvider } from "~/utils/analytics/posthog";
|
||||||
|
|
||||||
const MyApp: AppType<{ session: Session | null }> = ({
|
const MyApp: AppType<{ session: Session | null }> = ({
|
||||||
Component,
|
Component,
|
||||||
@@ -34,14 +34,15 @@ const MyApp: AppType<{ session: Session | null }> = ({
|
|||||||
<meta name="twitter:image" content="/og.png" />
|
<meta name="twitter:image" content="/og.png" />
|
||||||
</Head>
|
</Head>
|
||||||
<SessionProvider session={session}>
|
<SessionProvider session={session}>
|
||||||
<SyncAppStore />
|
<PosthogAppProvider>
|
||||||
<Favicon />
|
<SyncAppStore />
|
||||||
<SessionIdentifier />
|
<Favicon />
|
||||||
<ChakraThemeProvider>
|
<ChakraThemeProvider>
|
||||||
<QueryParamProvider adapter={NextAdapterApp}>
|
<QueryParamProvider adapter={NextAdapterApp}>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</ChakraThemeProvider>
|
</ChakraThemeProvider>
|
||||||
|
</PosthogAppProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
// A faulty API route to test Sentry's error monitoring
|
|
||||||
// @ts-expect-error just a test file, don't care about types
|
|
||||||
export default function handler(_req, res) {
|
|
||||||
throw new Error("Sentry Example API Route Error");
|
|
||||||
res.status(200).json({ name: "John Doe" });
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||||
import cors from "nextjs-cors";
|
import cors from "nextjs-cors";
|
||||||
import { createOpenApiNextHandler } from "trpc-openapi";
|
import { createOpenApiNextHandler } from "trpc-openapi";
|
||||||
import { createProcedureCache } from "trpc-openapi/dist/adapters/node-http/procedures";
|
import { v1ApiRouter } from "~/server/api/external/v1Api.router";
|
||||||
import { appRouter } from "~/server/api/root.router";
|
import { createOpenApiContext } from "~/server/api/external/openApiTrpc";
|
||||||
import { createTRPCContext } from "~/server/api/trpc";
|
|
||||||
|
|
||||||
const openApiHandler = createOpenApiNextHandler({
|
const openApiHandler = createOpenApiNextHandler({
|
||||||
router: appRouter,
|
router: v1ApiRouter,
|
||||||
createContext: createTRPCContext,
|
createContext: createOpenApiContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cache = createProcedureCache(appRouter);
|
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
// Setup CORS
|
// Setup CORS
|
||||||
await cors(req, res);
|
await cors(req, res);
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||||
import { generateOpenApiDocument } from "trpc-openapi";
|
import { generateOpenApiDocument } from "trpc-openapi";
|
||||||
import { appRouter } from "~/server/api/root.router";
|
import { v1ApiRouter } from "~/server/api/external/v1Api.router";
|
||||||
|
|
||||||
export const openApiDocument = generateOpenApiDocument(appRouter, {
|
export const openApiDocument = generateOpenApiDocument(v1ApiRouter, {
|
||||||
title: "OpenPipe API",
|
title: "OpenPipe API",
|
||||||
description: "The public API for reporting API calls to OpenPipe",
|
description: "The public API for reporting API calls to OpenPipe",
|
||||||
version: "0.1.0",
|
version: "0.1.1",
|
||||||
baseUrl: "https://app.openpipe.ai/api",
|
baseUrl: "https://app.openpipe.ai/api/v1",
|
||||||
});
|
});
|
||||||
// Respond with our OpenAPI schema
|
// Respond with our OpenAPI schema
|
||||||
const hander = (req: NextApiRequest, res: NextApiResponse) => {
|
const hander = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
114
app/src/pages/dashboard/index.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import {
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Stat,
|
||||||
|
StatLabel,
|
||||||
|
StatNumber,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardHeader,
|
||||||
|
Icon,
|
||||||
|
Table,
|
||||||
|
Tbody,
|
||||||
|
Tr,
|
||||||
|
Td,
|
||||||
|
Divider,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { Ban, DollarSign, Hash } from "lucide-react";
|
||||||
|
|
||||||
|
import AppShell from "~/components/nav/AppShell";
|
||||||
|
import { useSelectedProject } from "~/utils/hooks";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import LoggedCallsTable from "~/components/dashboard/LoggedCallsTable";
|
||||||
|
import UsageGraph from "~/components/dashboard/UsageGraph";
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const { data: selectedProject } = useSelectedProject();
|
||||||
|
|
||||||
|
const stats = api.dashboard.stats.useQuery(
|
||||||
|
{ projectId: selectedProject?.id ?? "" },
|
||||||
|
{ enabled: !!selectedProject },
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell title="Dashboard" requireAuth>
|
||||||
|
<VStack px={8} py={8} alignItems="flex-start" spacing={4}>
|
||||||
|
<Text fontSize="2xl" fontWeight="bold">
|
||||||
|
Dashboard
|
||||||
|
</Text>
|
||||||
|
<Divider />
|
||||||
|
<VStack margin="auto" spacing={4} align="stretch" w="full">
|
||||||
|
<HStack gap={4} align="start">
|
||||||
|
<Card flex={1}>
|
||||||
|
<CardHeader>
|
||||||
|
<Heading as="h3" size="sm">
|
||||||
|
Usage Statistics
|
||||||
|
</Heading>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody>
|
||||||
|
<UsageGraph />
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
<VStack spacing="4" width="300px" align="stretch">
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<Stat>
|
||||||
|
<HStack>
|
||||||
|
<StatLabel flex={1}>Total Spent</StatLabel>
|
||||||
|
<Icon as={DollarSign} boxSize={4} color="gray.500" />
|
||||||
|
</HStack>
|
||||||
|
<StatNumber>
|
||||||
|
${parseFloat(stats.data?.totals?.cost?.toString() ?? "0").toFixed(3)}
|
||||||
|
</StatNumber>
|
||||||
|
</Stat>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<Stat>
|
||||||
|
<HStack>
|
||||||
|
<StatLabel flex={1}>Total Requests</StatLabel>
|
||||||
|
<Icon as={Hash} boxSize={4} color="gray.500" />
|
||||||
|
</HStack>
|
||||||
|
<StatNumber>
|
||||||
|
{stats.data?.totals?.numQueries
|
||||||
|
? parseInt(stats.data?.totals?.numQueries.toString())?.toLocaleString()
|
||||||
|
: undefined}
|
||||||
|
</StatNumber>
|
||||||
|
</Stat>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
<Card overflow="hidden">
|
||||||
|
<Stat>
|
||||||
|
<CardHeader>
|
||||||
|
<HStack>
|
||||||
|
<StatLabel flex={1}>Errors</StatLabel>
|
||||||
|
<Icon as={Ban} boxSize={4} color="gray.500" />
|
||||||
|
</HStack>
|
||||||
|
</CardHeader>
|
||||||
|
<Table variant="simple">
|
||||||
|
<Tbody>
|
||||||
|
{stats.data?.errors?.map((error) => (
|
||||||
|
<Tr key={error.code}>
|
||||||
|
<Td>
|
||||||
|
{error.name} ({error.code})
|
||||||
|
</Td>
|
||||||
|
<Td isNumeric color="red.600">
|
||||||
|
{parseInt(error.count.toString()).toLocaleString()}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</Stat>
|
||||||
|
</Card>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
<LoggedCallsTable />
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -60,7 +60,7 @@ export default function Dataset() {
|
|||||||
<PageHeaderContainer>
|
<PageHeaderContainer>
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<ProjectBreadcrumbContents orgName={dataset.data?.organization?.name} />
|
<ProjectBreadcrumbContents projectName={dataset.data?.project?.name} />
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<Link href="/data">
|
<Link href="/data">
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default function Experiment() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
useAppStore.getState().sharedVariantEditor.loadMonaco().catch(console.error);
|
useAppStore.getState().sharedVariantEditor.loadMonaco().catch(console.error);
|
||||||
});
|
}, []);
|
||||||
|
|
||||||
const [label, setLabel] = useState(experiment.data?.label || "");
|
const [label, setLabel] = useState(experiment.data?.label || "");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -109,7 +109,7 @@ export default function Experiment() {
|
|||||||
<PageHeaderContainer>
|
<PageHeaderContainer>
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<ProjectBreadcrumbContents orgName={experiment.data?.organization?.name} />
|
<ProjectBreadcrumbContents projectName={experiment.data?.project?.name} />
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<Link href="/experiments">
|
<Link href="/experiments">
|
||||||
|
|||||||
@@ -1,181 +0,0 @@
|
|||||||
import {
|
|
||||||
Heading,
|
|
||||||
Text,
|
|
||||||
Stat,
|
|
||||||
StatLabel,
|
|
||||||
StatNumber,
|
|
||||||
VStack,
|
|
||||||
HStack,
|
|
||||||
Card,
|
|
||||||
CardBody,
|
|
||||||
CardHeader,
|
|
||||||
Icon,
|
|
||||||
Table,
|
|
||||||
Tbody,
|
|
||||||
Tr,
|
|
||||||
Td,
|
|
||||||
Divider,
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import {
|
|
||||||
LineChart,
|
|
||||||
Line,
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
CartesianGrid,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
ResponsiveContainer,
|
|
||||||
} from "recharts";
|
|
||||||
import { Ban, DollarSign, Hash } from "lucide-react";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
import AppShell from "~/components/nav/AppShell";
|
|
||||||
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
|
|
||||||
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
|
|
||||||
import { useSelectedOrg } from "~/utils/hooks";
|
|
||||||
import dayjs from "~/utils/dayjs";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import LoggedCallTable from "~/components/dashboard/LoggedCallTable";
|
|
||||||
|
|
||||||
export default function LoggedCalls() {
|
|
||||||
const { data: selectedOrg } = useSelectedOrg();
|
|
||||||
|
|
||||||
const stats = api.dashboard.stats.useQuery(
|
|
||||||
{ organizationId: selectedOrg?.id ?? "" },
|
|
||||||
{ enabled: !!selectedOrg },
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = useMemo(() => {
|
|
||||||
return (
|
|
||||||
stats.data?.periods.map(({ period, numQueries, totalCost }) => ({
|
|
||||||
period,
|
|
||||||
Requests: numQueries,
|
|
||||||
"Total Spent (USD)": parseFloat(totalCost.toString()),
|
|
||||||
})) || []
|
|
||||||
);
|
|
||||||
}, [stats.data]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppShell requireAuth>
|
|
||||||
<PageHeaderContainer>
|
|
||||||
<Breadcrumb>
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<ProjectBreadcrumbContents />
|
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbItem isCurrentPage>
|
|
||||||
<Text>Logged Calls</Text>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</Breadcrumb>
|
|
||||||
</PageHeaderContainer>
|
|
||||||
<VStack px={8} pt={4} alignItems="flex-start" spacing={4}>
|
|
||||||
<Text fontSize="2xl" fontWeight="bold">
|
|
||||||
{selectedOrg?.name}
|
|
||||||
</Text>
|
|
||||||
<Divider />
|
|
||||||
<VStack margin="auto" spacing={4} align="stretch" w="full">
|
|
||||||
<HStack gap={4} align="start">
|
|
||||||
<Card variant="outline" flex={1}>
|
|
||||||
<CardHeader>
|
|
||||||
<Heading as="h3" size="sm">
|
|
||||||
Usage Statistics
|
|
||||||
</Heading>
|
|
||||||
</CardHeader>
|
|
||||||
<CardBody>
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={data} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
|
|
||||||
<XAxis
|
|
||||||
dataKey="period"
|
|
||||||
tickFormatter={(str: string) => dayjs(str).format("MMM D")}
|
|
||||||
/>
|
|
||||||
<YAxis yAxisId="left" dataKey="Requests" orientation="left" stroke="#8884d8" />
|
|
||||||
<YAxis
|
|
||||||
yAxisId="right"
|
|
||||||
dataKey="Total Spent (USD)"
|
|
||||||
orientation="right"
|
|
||||||
unit="$"
|
|
||||||
stroke="#82ca9d"
|
|
||||||
/>
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<CartesianGrid stroke="#f5f5f5" />
|
|
||||||
<Line
|
|
||||||
dataKey="Requests"
|
|
||||||
stroke="#8884d8"
|
|
||||||
yAxisId="left"
|
|
||||||
dot={false}
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
dataKey="Total Spent (USD)"
|
|
||||||
stroke="#82ca9d"
|
|
||||||
yAxisId="right"
|
|
||||||
dot={false}
|
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
<VStack spacing="4" width="300px" align="stretch">
|
|
||||||
<Card variant="outline">
|
|
||||||
<CardBody>
|
|
||||||
<Stat>
|
|
||||||
<HStack>
|
|
||||||
<StatLabel flex={1}>Total Spent</StatLabel>
|
|
||||||
<Icon as={DollarSign} boxSize={4} color="gray.500" />
|
|
||||||
</HStack>
|
|
||||||
<StatNumber>
|
|
||||||
${parseFloat(stats.data?.totals?.totalCost?.toString() ?? "0").toFixed(2)}
|
|
||||||
</StatNumber>
|
|
||||||
</Stat>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
<Card variant="outline">
|
|
||||||
<CardBody>
|
|
||||||
<Stat>
|
|
||||||
<HStack>
|
|
||||||
<StatLabel flex={1}>Total Requests</StatLabel>
|
|
||||||
<Icon as={Hash} boxSize={4} color="gray.500" />
|
|
||||||
</HStack>
|
|
||||||
<StatNumber>
|
|
||||||
{stats.data?.totals?.numQueries
|
|
||||||
? parseInt(stats.data?.totals?.numQueries.toString())?.toLocaleString()
|
|
||||||
: undefined}
|
|
||||||
</StatNumber>
|
|
||||||
</Stat>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
<Card variant="outline" overflow="hidden">
|
|
||||||
<Stat>
|
|
||||||
<CardHeader>
|
|
||||||
<HStack>
|
|
||||||
<StatLabel flex={1}>Errors</StatLabel>
|
|
||||||
<Icon as={Ban} boxSize={4} color="gray.500" />
|
|
||||||
</HStack>
|
|
||||||
</CardHeader>
|
|
||||||
<Table variant="simple">
|
|
||||||
<Tbody>
|
|
||||||
{stats.data?.errors?.map((error) => (
|
|
||||||
<Tr key={error.code}>
|
|
||||||
<Td>
|
|
||||||
{error.name} ({error.code})
|
|
||||||
</Td>
|
|
||||||
<Td isNumeric color="red.600">
|
|
||||||
{parseInt(error.count.toString()).toLocaleString()}
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
))}
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
</Stat>
|
|
||||||
</Card>
|
|
||||||
</VStack>
|
|
||||||
</HStack>
|
|
||||||
<LoggedCallTable />
|
|
||||||
</VStack>
|
|
||||||
</VStack>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
type TextProps,
|
type TextProps,
|
||||||
VStack,
|
VStack,
|
||||||
HStack,
|
HStack,
|
||||||
Input,
|
|
||||||
Button,
|
Button,
|
||||||
Divider,
|
Divider,
|
||||||
Icon,
|
Icon,
|
||||||
@@ -17,33 +16,39 @@ import { BsTrash } from "react-icons/bs";
|
|||||||
import AppShell from "~/components/nav/AppShell";
|
import AppShell from "~/components/nav/AppShell";
|
||||||
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
|
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks";
|
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
|
||||||
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
|
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
|
||||||
import CopiableCode from "~/components/CopiableCode";
|
import CopiableCode from "~/components/CopiableCode";
|
||||||
import { DeleteProjectDialog } from "~/components/projectSettings/DeleteProjectDialog";
|
import { DeleteProjectDialog } from "~/components/projectSettings/DeleteProjectDialog";
|
||||||
|
import AutoResizeTextArea from "~/components/AutoResizeTextArea";
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
const { data: selectedOrg } = useSelectedOrg();
|
const { data: selectedProject } = useSelectedProject();
|
||||||
|
|
||||||
const apiKey =
|
const apiKey =
|
||||||
selectedOrg?.apiKeys?.length && selectedOrg?.apiKeys[0] ? selectedOrg?.apiKeys[0].apiKey : "";
|
selectedProject?.apiKeys?.length && selectedProject?.apiKeys[0]
|
||||||
|
? selectedProject?.apiKeys[0].apiKey
|
||||||
|
: "";
|
||||||
|
|
||||||
const updateMutation = api.organizations.update.useMutation();
|
const updateMutation = api.projects.update.useMutation();
|
||||||
const [onSaveName] = useHandledAsyncCallback(async () => {
|
const [onSaveName] = useHandledAsyncCallback(async () => {
|
||||||
if (name && name !== selectedOrg?.name && selectedOrg?.id) {
|
if (name && name !== selectedProject?.name && selectedProject?.id) {
|
||||||
await updateMutation.mutateAsync({
|
await updateMutation.mutateAsync({
|
||||||
id: selectedOrg.id,
|
id: selectedProject.id,
|
||||||
updates: { name },
|
updates: { name },
|
||||||
});
|
});
|
||||||
await Promise.all([utils.organizations.get.invalidate({ id: selectedOrg.id })]);
|
await Promise.all([
|
||||||
|
utils.projects.get.invalidate({ id: selectedProject.id }),
|
||||||
|
utils.projects.list.invalidate(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}, [updateMutation, selectedOrg]);
|
}, [updateMutation, selectedProject]);
|
||||||
|
|
||||||
const [name, setName] = useState(selectedOrg?.name);
|
const [name, setName] = useState(selectedProject?.name);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setName(selectedOrg?.name);
|
setName(selectedProject?.name);
|
||||||
}, [selectedOrg?.name]);
|
}, [selectedProject?.name]);
|
||||||
|
|
||||||
const deleteProjectOpen = useDisclosure();
|
const deleteProjectOpen = useDisclosure();
|
||||||
|
|
||||||
@@ -60,13 +65,13 @@ export default function Settings() {
|
|||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</PageHeaderContainer>
|
</PageHeaderContainer>
|
||||||
<VStack px={8} pt={4} alignItems="flex-start" spacing={4}>
|
<VStack px={8} py={4} alignItems="flex-start" spacing={4}>
|
||||||
<VStack spacing={0} alignItems="flex-start">
|
<VStack spacing={0} alignItems="flex-start">
|
||||||
<Text fontSize="2xl" fontWeight="bold">
|
<Text fontSize="2xl" fontWeight="bold">
|
||||||
Project Settings
|
Project Settings
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="sm">
|
<Text fontSize="sm">
|
||||||
Configure your project settings. These settings only apply to {selectedOrg?.name}.
|
Configure your project settings. These settings only apply to {selectedProject?.name}.
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack
|
<VStack
|
||||||
@@ -75,6 +80,7 @@ export default function Settings() {
|
|||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
borderRadius={4}
|
borderRadius={4}
|
||||||
borderColor="gray.300"
|
borderColor="gray.300"
|
||||||
|
bgColor="white"
|
||||||
p={6}
|
p={6}
|
||||||
spacing={6}
|
spacing={6}
|
||||||
>
|
>
|
||||||
@@ -82,7 +88,7 @@ export default function Settings() {
|
|||||||
<Text fontWeight="bold" fontSize="xl">
|
<Text fontWeight="bold" fontSize="xl">
|
||||||
Display Name
|
Display Name
|
||||||
</Text>
|
</Text>
|
||||||
<Input
|
<AutoResizeTextArea
|
||||||
w="full"
|
w="full"
|
||||||
maxW={600}
|
maxW={600}
|
||||||
value={name}
|
value={name}
|
||||||
@@ -90,7 +96,7 @@ export default function Settings() {
|
|||||||
borderColor="gray.300"
|
borderColor="gray.300"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
isDisabled={!name || name === selectedOrg?.name}
|
isDisabled={!name || name === selectedProject?.name}
|
||||||
colorScheme="orange"
|
colorScheme="orange"
|
||||||
borderRadius={4}
|
borderRadius={4}
|
||||||
mt={2}
|
mt={2}
|
||||||
@@ -113,12 +119,12 @@ export default function Settings() {
|
|||||||
</VStack>
|
</VStack>
|
||||||
<CopiableCode code={apiKey} />
|
<CopiableCode code={apiKey} />
|
||||||
<Divider />
|
<Divider />
|
||||||
{selectedOrg?.personalOrgUserId ? (
|
{selectedProject?.personalProjectUserId ? (
|
||||||
<VStack alignItems="flex-start">
|
<VStack alignItems="flex-start">
|
||||||
<Subtitle>Personal Project</Subtitle>
|
<Subtitle>Personal Project</Subtitle>
|
||||||
<Text fontSize="sm">
|
<Text fontSize="sm">
|
||||||
This project is {selectedOrg?.personalOrgUser?.name}'s personal project. It cannot
|
This project is {selectedProject?.personalProjectUser?.name}'s personal project.
|
||||||
be deleted.
|
It cannot be deleted.
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
) : (
|
) : (
|
||||||
@@ -129,15 +135,18 @@ export default function Settings() {
|
|||||||
</Text>
|
</Text>
|
||||||
<HStack
|
<HStack
|
||||||
as={Button}
|
as={Button}
|
||||||
isDisabled={selectedOrg?.role !== "ADMIN"}
|
isDisabled={selectedProject?.role !== "ADMIN"}
|
||||||
colorScheme="red"
|
colorScheme="red"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
borderRadius={4}
|
borderRadius={4}
|
||||||
mt={2}
|
mt={2}
|
||||||
|
height="auto"
|
||||||
onClick={deleteProjectOpen.onOpen}
|
onClick={deleteProjectOpen.onOpen}
|
||||||
>
|
>
|
||||||
<Icon as={BsTrash} />
|
<Icon as={BsTrash} />
|
||||||
<Text>Delete {selectedOrg?.name}</Text>
|
<Text overflowWrap="break-word" whiteSpace="normal" py={2}>
|
||||||
|
Delete {selectedProject?.name}
|
||||||
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
)}
|
)}
|
||||||
|
|||||||
34
app/src/pages/request-logs/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Text, VStack, Divider, HStack } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import AppShell from "~/components/nav/AppShell";
|
||||||
|
import LoggedCallTable from "~/components/requestLogs/LoggedCallsTable";
|
||||||
|
import LoggedCallsPaginator from "~/components/requestLogs/LoggedCallsPaginator";
|
||||||
|
import ActionButton from "~/components/requestLogs/ActionButton";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
import { RiFlaskLine } from "react-icons/ri";
|
||||||
|
|
||||||
|
export default function LoggedCalls() {
|
||||||
|
const selectedLogIds = useAppStore((s) => s.selectedLogs.selectedLogIds);
|
||||||
|
return (
|
||||||
|
<AppShell title="Request Logs" requireAuth>
|
||||||
|
<VStack px={8} py={8} alignItems="flex-start" spacing={4} w="full">
|
||||||
|
<Text fontSize="2xl" fontWeight="bold">
|
||||||
|
Request Logs
|
||||||
|
</Text>
|
||||||
|
<Divider />
|
||||||
|
<HStack w="full" justifyContent="flex-end">
|
||||||
|
<ActionButton
|
||||||
|
onClick={() => {
|
||||||
|
console.log("experimenting with these ids", selectedLogIds);
|
||||||
|
}}
|
||||||
|
label="Experiment"
|
||||||
|
icon={RiFlaskLine}
|
||||||
|
isDisabled={selectedLogIds.size === 0}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
<LoggedCallTable />
|
||||||
|
<LoggedCallsPaginator />
|
||||||
|
</VStack>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,11 +4,15 @@ import parserTypescript from "prettier/plugins/typescript";
|
|||||||
// @ts-expect-error for some reason missing from types
|
// @ts-expect-error for some reason missing from types
|
||||||
import parserEstree from "prettier/plugins/estree";
|
import parserEstree from "prettier/plugins/estree";
|
||||||
|
|
||||||
|
// This emits a warning in the browser "Critical dependency: the request of a
|
||||||
|
// dependency is an expression". Unfortunately doesn't seem to be a way to get
|
||||||
|
// around it if we want to use Babel client-side for now. One solution would be
|
||||||
|
// to just do the formatting server-side in a trpc call.
|
||||||
|
// https://github.com/babel/babel/issues/14301
|
||||||
import * as babel from "@babel/standalone";
|
import * as babel from "@babel/standalone";
|
||||||
|
|
||||||
export function stripTypes(tsCode: string): string {
|
export function stripTypes(tsCode: string): string {
|
||||||
const options = {
|
const options = {
|
||||||
presets: ["typescript"],
|
|
||||||
filename: "file.ts",
|
filename: "file.ts",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
95
app/src/server/api/external/openApiTrpc.ts
vendored
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import type { ApiKey, Project } from "@prisma/client";
|
||||||
|
import { TRPCError, initTRPC } from "@trpc/server";
|
||||||
|
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||||
|
import superjson from "superjson";
|
||||||
|
import { type OpenApiMeta } from "trpc-openapi";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
import { prisma } from "~/server/db";
|
||||||
|
|
||||||
|
type CreateContextOptions = {
|
||||||
|
key:
|
||||||
|
| (ApiKey & {
|
||||||
|
project: Project;
|
||||||
|
})
|
||||||
|
| null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This helper generates the "internals" for a tRPC context. If you need to use it, you can export
|
||||||
|
* it from here.
|
||||||
|
*
|
||||||
|
* Examples of things you may need it for:
|
||||||
|
* - testing, so we don't have to mock Next.js' req/res
|
||||||
|
* - tRPC's `createSSGHelpers`, where we don't have req/res
|
||||||
|
*
|
||||||
|
* @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
|
||||||
|
*/
|
||||||
|
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||||
|
return {
|
||||||
|
key: opts.key,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createOpenApiContext = async (opts: CreateNextContextOptions) => {
|
||||||
|
const { req, res } = opts;
|
||||||
|
|
||||||
|
const apiKey = req.headers.authorization?.split(" ")[1] as string | null;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
const key = await prisma.apiKey.findUnique({
|
||||||
|
where: { apiKey },
|
||||||
|
include: { project: true },
|
||||||
|
});
|
||||||
|
if (!key) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return createInnerTRPCContext({
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRPCContext = Awaited<ReturnType<typeof createOpenApiContext>>;
|
||||||
|
|
||||||
|
const t = initTRPC
|
||||||
|
.context<typeof createOpenApiContext>()
|
||||||
|
.meta<OpenApiMeta>()
|
||||||
|
.create({
|
||||||
|
transformer: superjson,
|
||||||
|
errorFormatter({ shape, error }) {
|
||||||
|
return {
|
||||||
|
...shape,
|
||||||
|
data: {
|
||||||
|
...shape.data,
|
||||||
|
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createOpenApiRouter = t.router;
|
||||||
|
|
||||||
|
export const openApiPublicProc = t.procedure;
|
||||||
|
|
||||||
|
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
||||||
|
const enforceApiKey = t.middleware(async ({ ctx, next }) => {
|
||||||
|
if (!ctx.key) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return next({
|
||||||
|
ctx: { key: ctx.key },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protected (authenticated) procedure
|
||||||
|
*
|
||||||
|
* If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
|
||||||
|
* the session is valid and guarantees `ctx.session.user` is not null.
|
||||||
|
*
|
||||||
|
* @see https://trpc.io/docs/procedures
|
||||||
|
*/
|
||||||
|
export const openApiProtectedProc = t.procedure.use(enforceApiKey);
|
||||||
240
app/src/server/api/external/v1Api.router.ts
vendored
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
import { type Prisma } from "@prisma/client";
|
||||||
|
import { type JsonValue } from "type-fest";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { prisma } from "~/server/db";
|
||||||
|
import { hashRequest } from "~/server/utils/hashObject";
|
||||||
|
import modelProvider from "~/modelProviders/openai-ChatCompletion";
|
||||||
|
import {
|
||||||
|
type ChatCompletion,
|
||||||
|
type CompletionCreateParams,
|
||||||
|
} from "openai/resources/chat/completions";
|
||||||
|
import { createOpenApiRouter, openApiProtectedProc } from "./openApiTrpc";
|
||||||
|
|
||||||
|
const reqValidator = z.object({
|
||||||
|
model: z.string(),
|
||||||
|
messages: z.array(z.any()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const respValidator = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
model: z.string(),
|
||||||
|
choices: z.array(
|
||||||
|
z.object({
|
||||||
|
finish_reason: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const v1ApiRouter = createOpenApiRouter({
|
||||||
|
checkCache: openApiProtectedProc
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "POST",
|
||||||
|
path: "/check-cache",
|
||||||
|
description: "Check if a prompt is cached",
|
||||||
|
protect: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
requestedAt: z.number().describe("Unix timestamp in milliseconds"),
|
||||||
|
reqPayload: z.unknown().describe("JSON-encoded request payload"),
|
||||||
|
tags: z
|
||||||
|
.record(z.string())
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }',
|
||||||
|
)
|
||||||
|
.default({}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
respPayload: z.unknown().optional().describe("JSON-encoded response payload"),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const reqPayload = await reqValidator.spa(input.reqPayload);
|
||||||
|
const cacheKey = hashRequest(ctx.key.projectId, reqPayload as JsonValue);
|
||||||
|
|
||||||
|
const existingResponse = await prisma.loggedCallModelResponse.findFirst({
|
||||||
|
where: { cacheKey },
|
||||||
|
include: { originalLoggedCall: true },
|
||||||
|
orderBy: { requestedAt: "desc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingResponse) return { respPayload: null };
|
||||||
|
|
||||||
|
await prisma.loggedCall.create({
|
||||||
|
data: {
|
||||||
|
projectId: ctx.key.projectId,
|
||||||
|
requestedAt: new Date(input.requestedAt),
|
||||||
|
cacheHit: true,
|
||||||
|
modelResponseId: existingResponse.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await createTags(existingResponse.originalLoggedCallId, input.tags);
|
||||||
|
return {
|
||||||
|
respPayload: existingResponse.respPayload,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
report: openApiProtectedProc
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "POST",
|
||||||
|
path: "/report",
|
||||||
|
description: "Report an API call",
|
||||||
|
protect: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
requestedAt: z.number().describe("Unix timestamp in milliseconds"),
|
||||||
|
receivedAt: z.number().describe("Unix timestamp in milliseconds"),
|
||||||
|
reqPayload: z.unknown().describe("JSON-encoded request payload"),
|
||||||
|
respPayload: z.unknown().optional().describe("JSON-encoded response payload"),
|
||||||
|
statusCode: z.number().optional().describe("HTTP status code of response"),
|
||||||
|
errorMessage: z.string().optional().describe("User-friendly error message"),
|
||||||
|
tags: z
|
||||||
|
.record(z.string())
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }',
|
||||||
|
)
|
||||||
|
.default({}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.output(z.void())
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const reqPayload = await reqValidator.spa(input.reqPayload);
|
||||||
|
const respPayload = await respValidator.spa(input.respPayload);
|
||||||
|
|
||||||
|
const requestHash = hashRequest(ctx.key.projectId, reqPayload as JsonValue);
|
||||||
|
|
||||||
|
const newLoggedCallId = uuidv4();
|
||||||
|
const newModelResponseId = uuidv4();
|
||||||
|
|
||||||
|
let usage;
|
||||||
|
let model;
|
||||||
|
if (reqPayload.success && respPayload.success) {
|
||||||
|
usage = modelProvider.getUsage(
|
||||||
|
input.reqPayload as CompletionCreateParams,
|
||||||
|
input.respPayload as ChatCompletion,
|
||||||
|
);
|
||||||
|
model = reqPayload.data.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.loggedCall.create({
|
||||||
|
data: {
|
||||||
|
id: newLoggedCallId,
|
||||||
|
projectId: ctx.key.projectId,
|
||||||
|
requestedAt: new Date(input.requestedAt),
|
||||||
|
cacheHit: false,
|
||||||
|
model,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.loggedCallModelResponse.create({
|
||||||
|
data: {
|
||||||
|
id: newModelResponseId,
|
||||||
|
originalLoggedCallId: newLoggedCallId,
|
||||||
|
requestedAt: new Date(input.requestedAt),
|
||||||
|
receivedAt: new Date(input.receivedAt),
|
||||||
|
reqPayload: input.reqPayload as Prisma.InputJsonValue,
|
||||||
|
respPayload: input.respPayload as Prisma.InputJsonValue,
|
||||||
|
statusCode: input.statusCode,
|
||||||
|
errorMessage: input.errorMessage,
|
||||||
|
durationMs: input.receivedAt - input.requestedAt,
|
||||||
|
cacheKey: respPayload.success ? requestHash : null,
|
||||||
|
inputTokens: usage?.inputTokens,
|
||||||
|
outputTokens: usage?.outputTokens,
|
||||||
|
cost: usage?.cost,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// Avoid foreign key constraint error by updating the logged call after the model response is created
|
||||||
|
prisma.loggedCall.update({
|
||||||
|
where: {
|
||||||
|
id: newLoggedCallId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
modelResponseId: newModelResponseId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await createTags(newLoggedCallId, input.tags);
|
||||||
|
}),
|
||||||
|
localTestingOnlyGetLatestLoggedCall: openApiProtectedProc
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "GET",
|
||||||
|
path: "/local-testing-only-get-latest-logged-call",
|
||||||
|
description: "Get the latest logged call (only for local testing)",
|
||||||
|
protect: true, // Make sure to protect this endpoint
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(z.void())
|
||||||
|
.output(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
createdAt: z.date(),
|
||||||
|
cacheHit: z.boolean(),
|
||||||
|
tags: z.record(z.string().nullable()),
|
||||||
|
modelResponse: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
statusCode: z.number().nullable(),
|
||||||
|
errorMessage: z.string().nullable(),
|
||||||
|
reqPayload: z.unknown(),
|
||||||
|
respPayload: z.unknown(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx }) => {
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
throw new Error("This operation is not allowed in production environment");
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestLoggedCall = await prisma.loggedCall.findFirst({
|
||||||
|
where: { projectId: ctx.key.projectId },
|
||||||
|
orderBy: { requestedAt: "desc" },
|
||||||
|
select: {
|
||||||
|
createdAt: true,
|
||||||
|
cacheHit: true,
|
||||||
|
tags: true,
|
||||||
|
modelResponse: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
statusCode: true,
|
||||||
|
errorMessage: true,
|
||||||
|
reqPayload: true,
|
||||||
|
respPayload: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
latestLoggedCall && {
|
||||||
|
...latestLoggedCall,
|
||||||
|
tags: Object.fromEntries(latestLoggedCall.tags.map((tag) => [tag.name, tag.value])),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createTags(loggedCallId: string, tags: Record<string, string>) {
|
||||||
|
const tagsToCreate = Object.entries(tags).map(([name, value]) => ({
|
||||||
|
loggedCallId,
|
||||||
|
name: name.replaceAll(/[^a-zA-Z0-9_$]/g, "_"),
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
await prisma.loggedCallTag.createMany({
|
||||||
|
data: tagsToCreate,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,14 +3,14 @@ import { createTRPCRouter } from "~/server/api/trpc";
|
|||||||
import { experimentsRouter } from "./routers/experiments.router";
|
import { experimentsRouter } from "./routers/experiments.router";
|
||||||
import { scenariosRouter } from "./routers/scenarios.router";
|
import { scenariosRouter } from "./routers/scenarios.router";
|
||||||
import { scenarioVariantCellsRouter } from "./routers/scenarioVariantCells.router";
|
import { scenarioVariantCellsRouter } from "./routers/scenarioVariantCells.router";
|
||||||
import { templateVarsRouter } from "./routers/templateVariables.router";
|
import { scenarioVarsRouter } from "./routers/scenarioVariables.router";
|
||||||
import { evaluationsRouter } from "./routers/evaluations.router";
|
import { evaluationsRouter } from "./routers/evaluations.router";
|
||||||
import { worldChampsRouter } from "./routers/worldChamps.router";
|
import { worldChampsRouter } from "./routers/worldChamps.router";
|
||||||
import { datasetsRouter } from "./routers/datasets.router";
|
import { datasetsRouter } from "./routers/datasets.router";
|
||||||
import { datasetEntries } from "./routers/datasetEntries.router";
|
import { datasetEntries } from "./routers/datasetEntries.router";
|
||||||
import { externalApiRouter } from "./routers/externalApi.router";
|
import { projectsRouter } from "./routers/projects.router";
|
||||||
import { organizationsRouter } from "./routers/organizations.router";
|
|
||||||
import { dashboardRouter } from "./routers/dashboard.router";
|
import { dashboardRouter } from "./routers/dashboard.router";
|
||||||
|
import { loggedCallsRouter } from "./routers/loggedCalls.router";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
@@ -22,14 +22,14 @@ export const appRouter = createTRPCRouter({
|
|||||||
experiments: experimentsRouter,
|
experiments: experimentsRouter,
|
||||||
scenarios: scenariosRouter,
|
scenarios: scenariosRouter,
|
||||||
scenarioVariantCells: scenarioVariantCellsRouter,
|
scenarioVariantCells: scenarioVariantCellsRouter,
|
||||||
templateVars: templateVarsRouter,
|
scenarioVars: scenarioVarsRouter,
|
||||||
evaluations: evaluationsRouter,
|
evaluations: evaluationsRouter,
|
||||||
worldChamps: worldChampsRouter,
|
worldChamps: worldChampsRouter,
|
||||||
datasets: datasetsRouter,
|
datasets: datasetsRouter,
|
||||||
datasetEntries: datasetEntries,
|
datasetEntries: datasetEntries,
|
||||||
organizations: organizationsRouter,
|
projects: projectsRouter,
|
||||||
dashboard: dashboardRouter,
|
dashboard: dashboardRouter,
|
||||||
externalApi: externalApiRouter,
|
loggedCalls: loggedCallsRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import { sql } from "kysely";
|
import { sql } from "kysely";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||||
import { kysely, prisma } from "~/server/db";
|
import { kysely } from "~/server/db";
|
||||||
|
import { requireCanViewProject } from "~/utils/accessControl";
|
||||||
import dayjs from "~/utils/dayjs";
|
import dayjs from "~/utils/dayjs";
|
||||||
|
|
||||||
export const dashboardRouter = createTRPCRouter({
|
export const dashboardRouter = createTRPCRouter({
|
||||||
stats: publicProcedure
|
stats: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
// TODO: actually take startDate into account
|
// TODO: actually take startDate into account
|
||||||
startDate: z.string().optional(),
|
startDate: z.string().optional(),
|
||||||
organizationId: z.string(),
|
projectId: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
await requireCanViewProject(input.projectId, ctx);
|
||||||
// Return the stats group by hour
|
// Return the stats group by hour
|
||||||
const periods = await kysely
|
const periods = await kysely
|
||||||
.selectFrom("LoggedCall")
|
.selectFrom("LoggedCall")
|
||||||
@@ -22,11 +24,11 @@ export const dashboardRouter = createTRPCRouter({
|
|||||||
"LoggedCall.id",
|
"LoggedCall.id",
|
||||||
"LoggedCallModelResponse.originalLoggedCallId",
|
"LoggedCallModelResponse.originalLoggedCallId",
|
||||||
)
|
)
|
||||||
.where("organizationId", "=", input.organizationId)
|
.where("projectId", "=", input.projectId)
|
||||||
.select(({ fn }) => [
|
.select(({ fn }) => [
|
||||||
sql<Date>`date_trunc('day', "LoggedCallModelResponse"."startTime")`.as("period"),
|
sql<Date>`date_trunc('day', "LoggedCallModelResponse"."requestedAt")`.as("period"),
|
||||||
sql<number>`count("LoggedCall"."id")::int`.as("numQueries"),
|
sql<number>`count("LoggedCall"."id")::int`.as("numQueries"),
|
||||||
fn.sum(fn.coalesce("LoggedCallModelResponse.totalCost", sql<number>`0`)).as("totalCost"),
|
fn.sum(fn.coalesce("LoggedCallModelResponse.cost", sql<number>`0`)).as("cost"),
|
||||||
])
|
])
|
||||||
.groupBy("period")
|
.groupBy("period")
|
||||||
.orderBy("period")
|
.orderBy("period")
|
||||||
@@ -57,7 +59,7 @@ export const dashboardRouter = createTRPCRouter({
|
|||||||
backfilledPeriods.unshift({
|
backfilledPeriods.unshift({
|
||||||
period: dayjs(dayToMatch).toDate(),
|
period: dayjs(dayToMatch).toDate(),
|
||||||
numQueries: 0,
|
numQueries: 0,
|
||||||
totalCost: 0,
|
cost: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
dayToMatch = dayToMatch.subtract(1, "day");
|
dayToMatch = dayToMatch.subtract(1, "day");
|
||||||
@@ -70,23 +72,23 @@ export const dashboardRouter = createTRPCRouter({
|
|||||||
"LoggedCall.id",
|
"LoggedCall.id",
|
||||||
"LoggedCallModelResponse.originalLoggedCallId",
|
"LoggedCallModelResponse.originalLoggedCallId",
|
||||||
)
|
)
|
||||||
.where("organizationId", "=", input.organizationId)
|
.where("projectId", "=", input.projectId)
|
||||||
.select(({ fn }) => [
|
.select(({ fn }) => [
|
||||||
fn.sum(fn.coalesce("LoggedCallModelResponse.totalCost", sql<number>`0`)).as("totalCost"),
|
fn.sum(fn.coalesce("LoggedCallModelResponse.cost", sql<number>`0`)).as("cost"),
|
||||||
fn.count("LoggedCall.id").as("numQueries"),
|
fn.count("LoggedCall.id").as("numQueries"),
|
||||||
])
|
])
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
const errors = await kysely
|
const errors = await kysely
|
||||||
.selectFrom("LoggedCall")
|
.selectFrom("LoggedCall")
|
||||||
.where("organizationId", "=", input.organizationId)
|
.where("projectId", "=", input.projectId)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
"LoggedCallModelResponse",
|
"LoggedCallModelResponse",
|
||||||
"LoggedCall.id",
|
"LoggedCall.id",
|
||||||
"LoggedCallModelResponse.originalLoggedCallId",
|
"LoggedCallModelResponse.originalLoggedCallId",
|
||||||
)
|
)
|
||||||
.select(({ fn }) => [fn.count("LoggedCall.id").as("count"), "respStatus as code"])
|
.select(({ fn }) => [fn.count("LoggedCall.id").as("count"), "statusCode as code"])
|
||||||
.where("respStatus", ">", 200)
|
.where("statusCode", ">", 200)
|
||||||
.groupBy("code")
|
.groupBy("code")
|
||||||
.orderBy("count", "desc")
|
.orderBy("count", "desc")
|
||||||
.execute();
|
.execute();
|
||||||
@@ -103,16 +105,4 @@ export const dashboardRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return { periods: backfilledPeriods, totals, errors: namedErrors };
|
return { periods: backfilledPeriods, totals, errors: namedErrors };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// TODO useInfiniteQuery
|
|
||||||
// https://discord.com/channels/966627436387266600/1122258443886153758/1122258443886153758
|
|
||||||
loggedCalls: publicProcedure.input(z.object({})).query(async ({ input }) => {
|
|
||||||
const loggedCalls = await prisma.loggedCall.findMany({
|
|
||||||
orderBy: { startTime: "desc" },
|
|
||||||
include: { tags: true, modelResponse: true },
|
|
||||||
take: 20,
|
|
||||||
});
|
|
||||||
|
|
||||||
return loggedCalls;
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,23 +4,21 @@ import { prisma } from "~/server/db";
|
|||||||
import { requireCanModifyDataset, requireCanViewDataset } from "~/utils/accessControl";
|
import { requireCanModifyDataset, requireCanViewDataset } from "~/utils/accessControl";
|
||||||
import { autogenerateDatasetEntries } from "../autogenerate/autogenerateDatasetEntries";
|
import { autogenerateDatasetEntries } from "../autogenerate/autogenerateDatasetEntries";
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
|
||||||
|
|
||||||
export const datasetEntries = createTRPCRouter({
|
export const datasetEntries = createTRPCRouter({
|
||||||
list: protectedProcedure
|
list: protectedProcedure
|
||||||
.input(z.object({ datasetId: z.string(), page: z.number() }))
|
.input(z.object({ datasetId: z.string(), page: z.number(), pageSize: z.number() }))
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
await requireCanViewDataset(input.datasetId, ctx);
|
await requireCanViewDataset(input.datasetId, ctx);
|
||||||
|
|
||||||
const { datasetId, page } = input;
|
const { datasetId, page, pageSize } = input;
|
||||||
|
|
||||||
const entries = await prisma.datasetEntry.findMany({
|
const entries = await prisma.datasetEntry.findMany({
|
||||||
where: {
|
where: {
|
||||||
datasetId,
|
datasetId,
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { createdAt: "desc" },
|
||||||
skip: (page - 1) * PAGE_SIZE,
|
skip: (page - 1) * pageSize,
|
||||||
take: PAGE_SIZE,
|
take: pageSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
const count = await prisma.datasetEntry.count({
|
const count = await prisma.datasetEntry.count({
|
||||||
@@ -31,8 +29,6 @@ export const datasetEntries = createTRPCRouter({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
entries,
|
entries,
|
||||||
startIndex: (page - 1) * PAGE_SIZE + 1,
|
|
||||||
lastPage: Math.ceil(count / PAGE_SIZE),
|
|
||||||
count,
|
count,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -3,20 +3,20 @@ import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/
|
|||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
import {
|
import {
|
||||||
requireCanModifyDataset,
|
requireCanModifyDataset,
|
||||||
requireCanModifyOrganization,
|
requireCanModifyProject,
|
||||||
requireCanViewDataset,
|
requireCanViewDataset,
|
||||||
requireCanViewOrganization,
|
requireCanViewProject,
|
||||||
} from "~/utils/accessControl";
|
} from "~/utils/accessControl";
|
||||||
|
|
||||||
export const datasetsRouter = createTRPCRouter({
|
export const datasetsRouter = createTRPCRouter({
|
||||||
list: protectedProcedure
|
list: protectedProcedure
|
||||||
.input(z.object({ organizationId: z.string() }))
|
.input(z.object({ projectId: z.string() }))
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
await requireCanViewOrganization(input.organizationId, ctx);
|
await requireCanViewProject(input.projectId, ctx);
|
||||||
|
|
||||||
const datasets = await prisma.dataset.findMany({
|
const datasets = await prisma.dataset.findMany({
|
||||||
where: {
|
where: {
|
||||||
organizationId: input.organizationId,
|
projectId: input.projectId,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: "desc",
|
createdAt: "desc",
|
||||||
@@ -36,26 +36,26 @@ export const datasetsRouter = createTRPCRouter({
|
|||||||
return await prisma.dataset.findFirstOrThrow({
|
return await prisma.dataset.findFirstOrThrow({
|
||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
include: {
|
include: {
|
||||||
organization: true,
|
project: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(z.object({ organizationId: z.string() }))
|
.input(z.object({ projectId: z.string() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await requireCanModifyOrganization(input.organizationId, ctx);
|
await requireCanModifyProject(input.projectId, ctx);
|
||||||
|
|
||||||
const numDatasets = await prisma.dataset.count({
|
const numDatasets = await prisma.dataset.count({
|
||||||
where: {
|
where: {
|
||||||
organizationId: input.organizationId,
|
projectId: input.projectId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return await prisma.dataset.create({
|
return await prisma.dataset.create({
|
||||||
data: {
|
data: {
|
||||||
name: `Dataset ${numDatasets + 1}`,
|
name: `Dataset ${numDatasets + 1}`,
|
||||||
organizationId: input.organizationId,
|
projectId: input.projectId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import { generateNewCell } from "~/server/utils/generateNewCell";
|
|||||||
import {
|
import {
|
||||||
canModifyExperiment,
|
canModifyExperiment,
|
||||||
requireCanModifyExperiment,
|
requireCanModifyExperiment,
|
||||||
requireCanModifyOrganization,
|
requireCanModifyProject,
|
||||||
requireCanViewExperiment,
|
requireCanViewExperiment,
|
||||||
requireCanViewOrganization,
|
requireCanViewProject,
|
||||||
} from "~/utils/accessControl";
|
} from "~/utils/accessControl";
|
||||||
import generateTypes from "~/modelProviders/generateTypes";
|
import generateTypes from "~/modelProviders/generateTypes";
|
||||||
import { promptConstructorVersion } from "~/promptConstructor/version";
|
import { promptConstructorVersion } from "~/promptConstructor/version";
|
||||||
@@ -44,13 +44,13 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
list: protectedProcedure
|
list: protectedProcedure
|
||||||
.input(z.object({ organizationId: z.string() }))
|
.input(z.object({ projectId: z.string() }))
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
await requireCanViewOrganization(input.organizationId, ctx);
|
await requireCanViewProject(input.projectId, ctx);
|
||||||
|
|
||||||
const experiments = await prisma.experiment.findMany({
|
const experiments = await prisma.experiment.findMany({
|
||||||
where: {
|
where: {
|
||||||
organizationId: input.organizationId,
|
projectId: input.projectId,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
sortIndex: "desc",
|
sortIndex: "desc",
|
||||||
@@ -90,7 +90,7 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
const experiment = await prisma.experiment.findFirstOrThrow({
|
const experiment = await prisma.experiment.findFirstOrThrow({
|
||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
include: {
|
include: {
|
||||||
organization: true,
|
project: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,10 +108,10 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
fork: protectedProcedure
|
fork: protectedProcedure
|
||||||
.input(z.object({ id: z.string(), organizationId: z.string() }))
|
.input(z.object({ id: z.string(), projectId: z.string() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await requireCanViewExperiment(input.id, ctx);
|
await requireCanViewExperiment(input.id, ctx);
|
||||||
await requireCanModifyOrganization(input.organizationId, ctx);
|
await requireCanModifyProject(input.projectId, ctx);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
existingExp,
|
existingExp,
|
||||||
@@ -227,7 +227,7 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
...modelResponseData,
|
...modelResponseData,
|
||||||
id: newModelResponseId,
|
id: newModelResponseId,
|
||||||
scenarioVariantCellId: newCellId,
|
scenarioVariantCellId: newCellId,
|
||||||
output: (modelResponse.output as Prisma.InputJsonValue) ?? undefined,
|
respPayload: (modelResponse.respPayload as Prisma.InputJsonValue) ?? undefined,
|
||||||
});
|
});
|
||||||
for (const evaluation of outputEvaluations) {
|
for (const evaluation of outputEvaluations) {
|
||||||
outputEvaluationsToCreate.push({
|
outputEvaluationsToCreate.push({
|
||||||
@@ -264,7 +264,7 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
id: newExperimentId,
|
id: newExperimentId,
|
||||||
sortIndex: maxSortIndex + 1,
|
sortIndex: maxSortIndex + 1,
|
||||||
label: `${existingExp.label} (forked)`,
|
label: `${existingExp.label} (forked)`,
|
||||||
organizationId: input.organizationId,
|
projectId: input.projectId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.promptVariant.createMany({
|
prisma.promptVariant.createMany({
|
||||||
@@ -294,9 +294,9 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(z.object({ organizationId: z.string() }))
|
.input(z.object({ projectId: z.string() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await requireCanModifyOrganization(input.organizationId, ctx);
|
await requireCanModifyProject(input.projectId, ctx);
|
||||||
|
|
||||||
const maxSortIndex =
|
const maxSortIndex =
|
||||||
(
|
(
|
||||||
@@ -304,7 +304,7 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
_max: {
|
_max: {
|
||||||
sortIndex: true,
|
sortIndex: true,
|
||||||
},
|
},
|
||||||
where: { organizationId: input.organizationId },
|
where: { projectId: input.projectId },
|
||||||
})
|
})
|
||||||
)._max?.sortIndex ?? 0;
|
)._max?.sortIndex ?? 0;
|
||||||
|
|
||||||
@@ -312,7 +312,7 @@ export const experimentsRouter = createTRPCRouter({
|
|||||||
data: {
|
data: {
|
||||||
sortIndex: maxSortIndex + 1,
|
sortIndex: maxSortIndex + 1,
|
||||||
label: `Experiment ${maxSortIndex + 1}`,
|
label: `Experiment ${maxSortIndex + 1}`,
|
||||||
organizationId: input.organizationId,
|
projectId: input.projectId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,205 +0,0 @@
|
|||||||
import { type Prisma } from "@prisma/client";
|
|
||||||
import { type JsonValue } from "type-fest";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
|
|
||||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
|
||||||
import { prisma } from "~/server/db";
|
|
||||||
import { hashRequest } from "~/server/utils/hashObject";
|
|
||||||
|
|
||||||
const reqValidator = z.object({
|
|
||||||
model: z.string(),
|
|
||||||
messages: z.array(z.any()),
|
|
||||||
});
|
|
||||||
|
|
||||||
const respValidator = z.object({
|
|
||||||
id: z.string(),
|
|
||||||
model: z.string(),
|
|
||||||
usage: z.object({
|
|
||||||
total_tokens: z.number(),
|
|
||||||
prompt_tokens: z.number(),
|
|
||||||
completion_tokens: z.number(),
|
|
||||||
}),
|
|
||||||
choices: z.array(
|
|
||||||
z.object({
|
|
||||||
finish_reason: z.string(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const externalApiRouter = createTRPCRouter({
|
|
||||||
checkCache: publicProcedure
|
|
||||||
.meta({
|
|
||||||
openapi: {
|
|
||||||
method: "POST",
|
|
||||||
path: "/v1/check-cache",
|
|
||||||
description: "Check if a prompt is cached",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
startTime: z.number().describe("Unix timestamp in milliseconds"),
|
|
||||||
reqPayload: z.unknown().describe("JSON-encoded request payload"),
|
|
||||||
tags: z
|
|
||||||
.record(z.string())
|
|
||||||
.optional()
|
|
||||||
.describe(
|
|
||||||
'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }',
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.output(
|
|
||||||
z.object({
|
|
||||||
respPayload: z.unknown().optional().describe("JSON-encoded response payload"),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
const apiKey = ctx.apiKey;
|
|
||||||
if (!apiKey) {
|
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
||||||
}
|
|
||||||
const key = await prisma.apiKey.findUnique({
|
|
||||||
where: { apiKey },
|
|
||||||
});
|
|
||||||
if (!key) {
|
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
||||||
}
|
|
||||||
const reqPayload = await reqValidator.spa(input.reqPayload);
|
|
||||||
const cacheKey = hashRequest(key.organizationId, reqPayload as JsonValue);
|
|
||||||
|
|
||||||
const existingResponse = await prisma.loggedCallModelResponse.findFirst({
|
|
||||||
where: {
|
|
||||||
cacheKey,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
originalLoggedCall: true,
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
startTime: "desc",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!existingResponse) return { respPayload: null };
|
|
||||||
|
|
||||||
await prisma.loggedCall.create({
|
|
||||||
data: {
|
|
||||||
organizationId: key.organizationId,
|
|
||||||
startTime: new Date(input.startTime),
|
|
||||||
cacheHit: true,
|
|
||||||
modelResponseId: existingResponse.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
respPayload: existingResponse.respPayload,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
|
|
||||||
report: publicProcedure
|
|
||||||
.meta({
|
|
||||||
openapi: {
|
|
||||||
method: "POST",
|
|
||||||
path: "/v1/report",
|
|
||||||
description: "Report an API call",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
startTime: z.number().describe("Unix timestamp in milliseconds"),
|
|
||||||
endTime: z.number().describe("Unix timestamp in milliseconds"),
|
|
||||||
reqPayload: z.unknown().describe("JSON-encoded request payload"),
|
|
||||||
respPayload: z.unknown().optional().describe("JSON-encoded response payload"),
|
|
||||||
respStatus: z.number().optional().describe("HTTP status code of response"),
|
|
||||||
error: z.string().optional().describe("User-friendly error message"),
|
|
||||||
tags: z
|
|
||||||
.record(z.string())
|
|
||||||
.optional()
|
|
||||||
.describe(
|
|
||||||
'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }',
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.output(z.void())
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
const apiKey = ctx.apiKey;
|
|
||||||
if (!apiKey) {
|
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
||||||
}
|
|
||||||
const key = await prisma.apiKey.findUnique({
|
|
||||||
where: { apiKey },
|
|
||||||
});
|
|
||||||
if (!key) {
|
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
||||||
}
|
|
||||||
const reqPayload = await reqValidator.spa(input.reqPayload);
|
|
||||||
const respPayload = await respValidator.spa(input.respPayload);
|
|
||||||
|
|
||||||
const requestHash = hashRequest(key.organizationId, reqPayload as JsonValue);
|
|
||||||
|
|
||||||
const newLoggedCallId = uuidv4();
|
|
||||||
const newModelResponseId = uuidv4();
|
|
||||||
|
|
||||||
const usage = respPayload.success ? respPayload.data.usage : undefined;
|
|
||||||
|
|
||||||
await prisma.$transaction([
|
|
||||||
prisma.loggedCall.create({
|
|
||||||
data: {
|
|
||||||
id: newLoggedCallId,
|
|
||||||
organizationId: key.organizationId,
|
|
||||||
startTime: new Date(input.startTime),
|
|
||||||
cacheHit: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
prisma.loggedCallModelResponse.create({
|
|
||||||
data: {
|
|
||||||
id: newModelResponseId,
|
|
||||||
originalLoggedCallId: newLoggedCallId,
|
|
||||||
startTime: new Date(input.startTime),
|
|
||||||
endTime: new Date(input.endTime),
|
|
||||||
reqPayload: input.reqPayload as Prisma.InputJsonValue,
|
|
||||||
respPayload: input.respPayload as Prisma.InputJsonValue,
|
|
||||||
respStatus: input.respStatus,
|
|
||||||
error: input.error,
|
|
||||||
durationMs: input.endTime - input.startTime,
|
|
||||||
...(respPayload.success
|
|
||||||
? {
|
|
||||||
cacheKey: requestHash,
|
|
||||||
inputTokens: usage ? usage.prompt_tokens : undefined,
|
|
||||||
outputTokens: usage ? usage.completion_tokens : undefined,
|
|
||||||
}
|
|
||||||
: null),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
// Avoid foreign key constraint error by updating the logged call after the model response is created
|
|
||||||
prisma.loggedCall.update({
|
|
||||||
where: {
|
|
||||||
id: newLoggedCallId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
modelResponseId: newModelResponseId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (input.tags) {
|
|
||||||
const tagsToCreate = Object.entries(input.tags).map(([name, value]) => ({
|
|
||||||
loggedCallId: newLoggedCallId,
|
|
||||||
// sanitize tags
|
|
||||||
name: name.replaceAll(/[^a-zA-Z0-9_]/g, "_"),
|
|
||||||
value,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (reqPayload.success) {
|
|
||||||
tagsToCreate.push({
|
|
||||||
loggedCallId: newLoggedCallId,
|
|
||||||
name: "$model",
|
|
||||||
value: reqPayload.data.model,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await prisma.loggedCallTag.createMany({
|
|
||||||
data: tagsToCreate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
33
app/src/server/api/routers/loggedCalls.router.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||||
|
import { prisma } from "~/server/db";
|
||||||
|
import { requireCanViewProject } from "~/utils/accessControl";
|
||||||
|
|
||||||
|
export const loggedCallsRouter = createTRPCRouter({
|
||||||
|
list: protectedProcedure
|
||||||
|
.input(z.object({ projectId: z.string(), page: z.number(), pageSize: z.number() }))
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const { projectId, page, pageSize } = input;
|
||||||
|
|
||||||
|
await requireCanViewProject(projectId, ctx);
|
||||||
|
|
||||||
|
const calls = await prisma.loggedCall.findMany({
|
||||||
|
where: { projectId },
|
||||||
|
orderBy: { requestedAt: "desc" },
|
||||||
|
include: { tags: true, modelResponse: true },
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const matchingLogs = await prisma.loggedCall.findMany({
|
||||||
|
where: { projectId },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const count = await prisma.loggedCall.count({
|
||||||
|
where: { projectId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { calls, count, matchingLogIds: matchingLogs.map((log) => log.id) };
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -5,15 +5,15 @@ import { z } from "zod";
|
|||||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
import { generateApiKey } from "~/server/utils/generateApiKey";
|
import { generateApiKey } from "~/server/utils/generateApiKey";
|
||||||
import userOrg from "~/server/utils/userOrg";
|
import userProject from "~/server/utils/userProject";
|
||||||
import {
|
import {
|
||||||
requireCanModifyOrganization,
|
requireCanModifyProject,
|
||||||
requireCanViewOrganization,
|
requireCanViewProject,
|
||||||
requireIsOrgAdmin,
|
requireIsProjectAdmin,
|
||||||
requireNothing,
|
requireNothing,
|
||||||
} from "~/utils/accessControl";
|
} from "~/utils/accessControl";
|
||||||
|
|
||||||
export const organizationsRouter = createTRPCRouter({
|
export const projectsRouter = createTRPCRouter({
|
||||||
list: protectedProcedure.query(async ({ ctx }) => {
|
list: protectedProcedure.query(async ({ ctx }) => {
|
||||||
const userId = ctx.session.user.id;
|
const userId = ctx.session.user.id;
|
||||||
requireNothing(ctx);
|
requireNothing(ctx);
|
||||||
@@ -22,9 +22,9 @@ export const organizationsRouter = createTRPCRouter({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const organizations = await prisma.organization.findMany({
|
const projects = await prisma.project.findMany({
|
||||||
where: {
|
where: {
|
||||||
organizationUsers: {
|
projectUsers: {
|
||||||
some: { userId: ctx.session.user.id },
|
some: { userId: ctx.session.user.id },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -33,30 +33,30 @@ export const organizationsRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!organizations.length) {
|
if (!projects.length) {
|
||||||
// TODO: We should move this to a separate endpoint that is called on sign up
|
// TODO: We should move this to a separate endpoint that is called on sign up
|
||||||
const personalOrg = await userOrg(userId);
|
const personalProject = await userProject(userId);
|
||||||
organizations.push(personalOrg);
|
projects.push(personalProject);
|
||||||
}
|
}
|
||||||
|
|
||||||
return organizations;
|
return projects;
|
||||||
}),
|
}),
|
||||||
get: protectedProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => {
|
get: protectedProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => {
|
||||||
await requireCanViewOrganization(input.id, ctx);
|
await requireCanViewProject(input.id, ctx);
|
||||||
const [org, userRole] = await prisma.$transaction([
|
const [proj, userRole] = await prisma.$transaction([
|
||||||
prisma.organization.findUnique({
|
prisma.project.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
apiKeys: true,
|
apiKeys: true,
|
||||||
personalOrgUser: true,
|
personalProjectUser: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.organizationUser.findFirst({
|
prisma.projectUser.findFirst({
|
||||||
where: {
|
where: {
|
||||||
userId: ctx.session.user.id,
|
userId: ctx.session.user.id,
|
||||||
organizationId: input.id,
|
projectId: input.id,
|
||||||
role: {
|
role: {
|
||||||
in: ["ADMIN", "MEMBER"],
|
in: ["ADMIN", "MEMBER"],
|
||||||
},
|
},
|
||||||
@@ -64,20 +64,20 @@ export const organizationsRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!org) {
|
if (!proj) {
|
||||||
throw new TRPCError({ code: "NOT_FOUND" });
|
throw new TRPCError({ code: "NOT_FOUND" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...org,
|
...proj,
|
||||||
role: userRole?.role ?? null,
|
role: userRole?.role ?? null,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(z.object({ id: z.string(), updates: z.object({ name: z.string() }) }))
|
.input(z.object({ id: z.string(), updates: z.object({ name: z.string() }) }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await requireCanModifyOrganization(input.id, ctx);
|
await requireCanModifyProject(input.id, ctx);
|
||||||
return await prisma.organization.update({
|
return await prisma.project.update({
|
||||||
where: {
|
where: {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
},
|
},
|
||||||
@@ -90,36 +90,36 @@ export const organizationsRouter = createTRPCRouter({
|
|||||||
.input(z.object({ name: z.string() }))
|
.input(z.object({ name: z.string() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
requireNothing(ctx);
|
requireNothing(ctx);
|
||||||
const newOrgId = uuidv4();
|
const newProjectId = uuidv4();
|
||||||
const [newOrg] = await prisma.$transaction([
|
const [newProject] = await prisma.$transaction([
|
||||||
prisma.organization.create({
|
prisma.project.create({
|
||||||
data: {
|
data: {
|
||||||
id: newOrgId,
|
id: newProjectId,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.organizationUser.create({
|
prisma.projectUser.create({
|
||||||
data: {
|
data: {
|
||||||
userId: ctx.session.user.id,
|
userId: ctx.session.user.id,
|
||||||
organizationId: newOrgId,
|
projectId: newProjectId,
|
||||||
role: "ADMIN",
|
role: "ADMIN",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.apiKey.create({
|
prisma.apiKey.create({
|
||||||
data: {
|
data: {
|
||||||
name: "Default API Key",
|
name: "Default API Key",
|
||||||
organizationId: newOrgId,
|
projectId: newProjectId,
|
||||||
apiKey: generateApiKey(),
|
apiKey: generateApiKey(),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
return newOrg;
|
return newProject;
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(z.object({ id: z.string() }))
|
.input(z.object({ id: z.string() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await requireIsOrgAdmin(input.id, ctx);
|
await requireIsProjectAdmin(input.id, ctx);
|
||||||
return await prisma.organization.delete({
|
return await prisma.project.delete({
|
||||||
where: {
|
where: {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
},
|
},
|
||||||
@@ -3,7 +3,7 @@ import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/
|
|||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { generateNewCell } from "~/server/utils/generateNewCell";
|
import { generateNewCell } from "~/server/utils/generateNewCell";
|
||||||
import userError from "~/server/utils/error";
|
import { error, success } from "~/utils/errorHandling/standardResponses";
|
||||||
import { recordExperimentUpdated } from "~/server/utils/recordExperimentUpdated";
|
import { recordExperimentUpdated } from "~/server/utils/recordExperimentUpdated";
|
||||||
import { reorderPromptVariants } from "~/server/utils/reorderPromptVariants";
|
import { reorderPromptVariants } from "~/server/utils/reorderPromptVariants";
|
||||||
import { type PromptVariant } from "@prisma/client";
|
import { type PromptVariant } from "@prisma/client";
|
||||||
@@ -55,7 +55,7 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
where: {
|
where: {
|
||||||
modelResponse: {
|
modelResponse: {
|
||||||
outdated: false,
|
outdated: false,
|
||||||
output: { not: Prisma.AnyNull },
|
respPayload: { not: Prisma.AnyNull },
|
||||||
scenarioVariantCell: {
|
scenarioVariantCell: {
|
||||||
promptVariant: {
|
promptVariant: {
|
||||||
id: input.variantId,
|
id: input.variantId,
|
||||||
@@ -100,7 +100,7 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
modelResponses: {
|
modelResponses: {
|
||||||
some: {
|
some: {
|
||||||
outdated: false,
|
outdated: false,
|
||||||
output: {
|
respPayload: {
|
||||||
not: Prisma.AnyNull,
|
not: Prisma.AnyNull,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -111,7 +111,7 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
const overallTokens = await prisma.modelResponse.aggregate({
|
const overallTokens = await prisma.modelResponse.aggregate({
|
||||||
where: {
|
where: {
|
||||||
outdated: false,
|
outdated: false,
|
||||||
output: {
|
respPayload: {
|
||||||
not: Prisma.AnyNull,
|
not: Prisma.AnyNull,
|
||||||
},
|
},
|
||||||
scenarioVariantCell: {
|
scenarioVariantCell: {
|
||||||
@@ -123,13 +123,15 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
_sum: {
|
_sum: {
|
||||||
cost: true,
|
cost: true,
|
||||||
promptTokens: true,
|
inputTokens: true,
|
||||||
completionTokens: true,
|
outputTokens: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const promptTokens = overallTokens._sum?.promptTokens ?? 0;
|
const inputTokens = overallTokens._sum?.inputTokens ?? 0;
|
||||||
const completionTokens = overallTokens._sum?.completionTokens ?? 0;
|
const outputTokens = overallTokens._sum?.outputTokens ?? 0;
|
||||||
|
|
||||||
|
const awaitingCompletions = outputCount < scenarioCount;
|
||||||
|
|
||||||
const awaitingEvals = !!evalResults.find(
|
const awaitingEvals = !!evalResults.find(
|
||||||
(result) => result.totalCount < scenarioCount * evals.length,
|
(result) => result.totalCount < scenarioCount * evals.length,
|
||||||
@@ -137,11 +139,12 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
evalResults,
|
evalResults,
|
||||||
promptTokens,
|
inputTokens,
|
||||||
completionTokens,
|
outputTokens,
|
||||||
overallCost: overallTokens._sum?.cost ?? 0,
|
overallCost: overallTokens._sum?.cost ?? 0,
|
||||||
scenarioCount,
|
scenarioCount,
|
||||||
outputCount,
|
outputCount,
|
||||||
|
awaitingCompletions,
|
||||||
awaitingEvals,
|
awaitingEvals,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -315,7 +318,7 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
const constructedPrompt = await parsePromptConstructor(existing.promptConstructor);
|
const constructedPrompt = await parsePromptConstructor(existing.promptConstructor);
|
||||||
|
|
||||||
if ("error" in constructedPrompt) {
|
if ("error" in constructedPrompt) {
|
||||||
return userError(constructedPrompt.error);
|
return error(constructedPrompt.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const model = input.newModel
|
const model = input.newModel
|
||||||
@@ -353,7 +356,7 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
const parsedPrompt = await parsePromptConstructor(input.promptConstructor);
|
const parsedPrompt = await parsePromptConstructor(input.promptConstructor);
|
||||||
|
|
||||||
if ("error" in parsedPrompt) {
|
if ("error" in parsedPrompt) {
|
||||||
return userError(parsedPrompt.error);
|
return error(parsedPrompt.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a duplicate with only the config changed
|
// Create a duplicate with only the config changed
|
||||||
@@ -398,7 +401,7 @@ export const promptVariantsRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { status: "ok" } as const;
|
return success();
|
||||||
}),
|
}),
|
||||||
|
|
||||||
reorder: protectedProcedure
|
reorder: protectedProcedure
|
||||||
|
|||||||
143
app/src/server/api/routers/scenarioVariables.router.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { type TemplateVariable } from "@prisma/client";
|
||||||
|
import { sql } from "kysely";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc";
|
||||||
|
import { kysely, prisma } from "~/server/db";
|
||||||
|
import { error, success } from "~/utils/errorHandling/standardResponses";
|
||||||
|
import { requireCanModifyExperiment, requireCanViewExperiment } from "~/utils/accessControl";
|
||||||
|
|
||||||
|
export const scenarioVarsRouter = createTRPCRouter({
|
||||||
|
create: protectedProcedure
|
||||||
|
.input(z.object({ experimentId: z.string(), label: z.string() }))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await requireCanModifyExperiment(input.experimentId, ctx);
|
||||||
|
|
||||||
|
// Make sure there isn't an existing variable with the same name
|
||||||
|
const existingVariable = await prisma.templateVariable.findFirst({
|
||||||
|
where: {
|
||||||
|
experimentId: input.experimentId,
|
||||||
|
label: input.label,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (existingVariable) {
|
||||||
|
return error(`A variable named ${input.label} already exists.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.templateVariable.create({
|
||||||
|
data: {
|
||||||
|
experimentId: input.experimentId,
|
||||||
|
label: input.label,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return success();
|
||||||
|
}),
|
||||||
|
|
||||||
|
rename: protectedProcedure
|
||||||
|
.input(z.object({ id: z.string(), label: z.string() }))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const templateVariable = await prisma.templateVariable.findUniqueOrThrow({
|
||||||
|
where: { id: input.id },
|
||||||
|
});
|
||||||
|
await requireCanModifyExperiment(templateVariable.experimentId, ctx);
|
||||||
|
|
||||||
|
// Make sure there isn't an existing variable with the same name
|
||||||
|
const existingVariable = await prisma.templateVariable.findFirst({
|
||||||
|
where: {
|
||||||
|
experimentId: templateVariable.experimentId,
|
||||||
|
label: input.label,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (existingVariable) {
|
||||||
|
return error(`A variable named ${input.label} already exists.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await renameTemplateVariable(templateVariable, input.label);
|
||||||
|
return success();
|
||||||
|
}),
|
||||||
|
|
||||||
|
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 } });
|
||||||
|
}),
|
||||||
|
|
||||||
|
list: publicProcedure
|
||||||
|
.input(z.object({ experimentId: z.string() }))
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
await requireCanViewExperiment(input.experimentId, ctx);
|
||||||
|
return await prisma.templateVariable.findMany({
|
||||||
|
where: {
|
||||||
|
experimentId: input.experimentId,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "asc",
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
label: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const renameTemplateVariable = async (
|
||||||
|
templateVariable: TemplateVariable,
|
||||||
|
newLabel: string,
|
||||||
|
) => {
|
||||||
|
const { experimentId } = templateVariable;
|
||||||
|
|
||||||
|
await kysely.transaction().execute(async (trx) => {
|
||||||
|
await trx
|
||||||
|
.updateTable("TemplateVariable")
|
||||||
|
.set({
|
||||||
|
label: newLabel,
|
||||||
|
})
|
||||||
|
.where("id", "=", templateVariable.id)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await sql`
|
||||||
|
CREATE TEMP TABLE "TempTestScenario" AS
|
||||||
|
SELECT *
|
||||||
|
FROM "TestScenario"
|
||||||
|
WHERE "experimentId" = ${experimentId}
|
||||||
|
|
||||||
|
-- Only copy the rows that actually have a value for the variable, no reason to churn the rest and simplifies the update.
|
||||||
|
AND "variableValues"->${templateVariable.label} IS NOT NULL
|
||||||
|
`.execute(trx);
|
||||||
|
|
||||||
|
await sql`
|
||||||
|
UPDATE "TempTestScenario"
|
||||||
|
SET "variableValues" = jsonb_set(
|
||||||
|
"variableValues",
|
||||||
|
${`{${newLabel}}`},
|
||||||
|
"variableValues"->${templateVariable.label}
|
||||||
|
) - ${templateVariable.label},
|
||||||
|
"updatedAt" = NOW(),
|
||||||
|
"id" = uuid_generate_v4()
|
||||||
|
`.execute(trx);
|
||||||
|
|
||||||
|
// Print the contents of the temp table
|
||||||
|
const results = await sql`SELECT * FROM "TempTestScenario"`.execute(trx);
|
||||||
|
console.log(results.rows);
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.updateTable("TestScenario")
|
||||||
|
.set({
|
||||||
|
visible: false,
|
||||||
|
})
|
||||||
|
.where("experimentId", "=", experimentId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await sql`
|
||||||
|
INSERT INTO "TestScenario" (id, "variableValues", "uiId", visible, "sortIndex", "experimentId", "createdAt", "updatedAt")
|
||||||
|
SELECT * FROM "TempTestScenario";
|
||||||
|
`.execute(trx);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -7,15 +7,13 @@ import { runAllEvals } from "~/server/utils/evaluations";
|
|||||||
import { generateNewCell } from "~/server/utils/generateNewCell";
|
import { generateNewCell } from "~/server/utils/generateNewCell";
|
||||||
import { requireCanModifyExperiment, requireCanViewExperiment } from "~/utils/accessControl";
|
import { requireCanModifyExperiment, requireCanViewExperiment } from "~/utils/accessControl";
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
|
||||||
|
|
||||||
export const scenariosRouter = createTRPCRouter({
|
export const scenariosRouter = createTRPCRouter({
|
||||||
list: publicProcedure
|
list: publicProcedure
|
||||||
.input(z.object({ experimentId: z.string(), page: z.number() }))
|
.input(z.object({ experimentId: z.string(), page: z.number(), pageSize: z.number() }))
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
await requireCanViewExperiment(input.experimentId, ctx);
|
await requireCanViewExperiment(input.experimentId, ctx);
|
||||||
|
|
||||||
const { experimentId, page } = input;
|
const { experimentId, page, pageSize } = input;
|
||||||
|
|
||||||
const scenarios = await prisma.testScenario.findMany({
|
const scenarios = await prisma.testScenario.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -23,8 +21,8 @@ export const scenariosRouter = createTRPCRouter({
|
|||||||
visible: true,
|
visible: true,
|
||||||
},
|
},
|
||||||
orderBy: { sortIndex: "asc" },
|
orderBy: { sortIndex: "asc" },
|
||||||
skip: (page - 1) * PAGE_SIZE,
|
skip: (page - 1) * pageSize,
|
||||||
take: PAGE_SIZE,
|
take: pageSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
const count = await prisma.testScenario.count({
|
const count = await prisma.testScenario.count({
|
||||||
@@ -36,8 +34,6 @@ export const scenariosRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
scenarios,
|
scenarios,
|
||||||
startIndex: (page - 1) * PAGE_SIZE + 1,
|
|
||||||
lastPage: Math.ceil(count / PAGE_SIZE),
|
|
||||||
count,
|
count,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
110
app/src/server/api/routers/templateVariables.router.test.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { expect, it } from "vitest";
|
||||||
|
import { prisma } from "~/server/db";
|
||||||
|
import { renameTemplateVariable } from "./scenarioVariables.router";
|
||||||
|
|
||||||
|
const createExperiment = async () => {
|
||||||
|
return await prisma.experiment.create({
|
||||||
|
data: {
|
||||||
|
label: "Test Experiment",
|
||||||
|
project: {
|
||||||
|
create: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTemplateVar = async (experimentId: string, label: string) => {
|
||||||
|
return await prisma.templateVariable.create({
|
||||||
|
data: {
|
||||||
|
experimentId,
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
it("renames templateVariables", async () => {
|
||||||
|
// Create experiments concurrently
|
||||||
|
const [exp1, exp2] = await Promise.all([createExperiment(), createExperiment()]);
|
||||||
|
|
||||||
|
// Create template variables concurrently
|
||||||
|
const [exp1Var, exp2Var1, exp2Var2] = await Promise.all([
|
||||||
|
createTemplateVar(exp1.id, "input1"),
|
||||||
|
createTemplateVar(exp2.id, "input1"),
|
||||||
|
createTemplateVar(exp2.id, "input2"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create test scenarios concurrently
|
||||||
|
const [exp1Scenario, exp2Scenario, exp2HiddenScenario] = await Promise.all([
|
||||||
|
prisma.testScenario.create({
|
||||||
|
data: {
|
||||||
|
experimentId: exp1.id,
|
||||||
|
visible: true,
|
||||||
|
variableValues: { input1: "test" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.testScenario.create({
|
||||||
|
data: {
|
||||||
|
experimentId: exp2.id,
|
||||||
|
visible: true,
|
||||||
|
variableValues: { input1: "test1", otherInput: "otherTest" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.testScenario.create({
|
||||||
|
data: {
|
||||||
|
experimentId: exp2.id,
|
||||||
|
visible: false,
|
||||||
|
variableValues: { otherInput: "otherTest2" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await renameTemplateVariable(exp2Var1, "input1-renamed");
|
||||||
|
|
||||||
|
expect(await prisma.templateVariable.findUnique({ where: { id: exp2Var1.id } })).toMatchObject({
|
||||||
|
label: "input1-renamed",
|
||||||
|
});
|
||||||
|
|
||||||
|
// It shouldn't mess with unrelated experiments
|
||||||
|
expect(await prisma.testScenario.findUnique({ where: { id: exp1Scenario.id } })).toMatchObject({
|
||||||
|
visible: true,
|
||||||
|
variableValues: { input1: "test" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make sure there are a total of 4 scenarios for exp2
|
||||||
|
expect(
|
||||||
|
await prisma.testScenario.count({
|
||||||
|
where: {
|
||||||
|
experimentId: exp2.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toBe(3);
|
||||||
|
|
||||||
|
// It shouldn't mess with the existing scenarios, except to hide them
|
||||||
|
expect(await prisma.testScenario.findUnique({ where: { id: exp2Scenario.id } })).toMatchObject({
|
||||||
|
visible: false,
|
||||||
|
variableValues: { input1: "test1", otherInput: "otherTest" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// It should create a new scenario with the new variable name
|
||||||
|
const newScenario1 = await prisma.testScenario.findFirst({
|
||||||
|
where: {
|
||||||
|
experimentId: exp2.id,
|
||||||
|
variableValues: { equals: { "input1-renamed": "test1", otherInput: "otherTest" } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newScenario1).toMatchObject({
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newScenario2 = await prisma.testScenario.findFirst({
|
||||||
|
where: {
|
||||||
|
experimentId: exp2.id,
|
||||||
|
variableValues: { equals: { otherInput: "otherTest2" } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newScenario2).toMatchObject({
|
||||||
|
visible: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc";
|
|
||||||
import { prisma } from "~/server/db";
|
|
||||||
import { requireCanModifyExperiment, requireCanViewExperiment } from "~/utils/accessControl";
|
|
||||||
|
|
||||||
export const templateVarsRouter = createTRPCRouter({
|
|
||||||
create: protectedProcedure
|
|
||||||
.input(z.object({ experimentId: z.string(), label: z.string() }))
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
await requireCanModifyExperiment(input.experimentId, ctx);
|
|
||||||
|
|
||||||
await prisma.templateVariable.create({
|
|
||||||
data: {
|
|
||||||
experimentId: input.experimentId,
|
|
||||||
label: input.label,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
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 } });
|
|
||||||
}),
|
|
||||||
|
|
||||||
list: publicProcedure
|
|
||||||
.input(z.object({ experimentId: z.string() }))
|
|
||||||
.query(async ({ input, ctx }) => {
|
|
||||||
await requireCanViewExperiment(input.experimentId, ctx);
|
|
||||||
return await prisma.templateVariable.findMany({
|
|
||||||
where: {
|
|
||||||
experimentId: input.experimentId,
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
createdAt: "asc",
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
label: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -27,7 +27,6 @@ import { capturePath } from "~/utils/analytics/serverAnalytics";
|
|||||||
|
|
||||||
type CreateContextOptions = {
|
type CreateContextOptions = {
|
||||||
session: Session | null;
|
session: Session | null;
|
||||||
apiKey: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
@@ -46,7 +45,6 @@ const noOp = () => {};
|
|||||||
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||||
return {
|
return {
|
||||||
session: opts.session,
|
session: opts.session,
|
||||||
apiKey: opts.apiKey,
|
|
||||||
prisma,
|
prisma,
|
||||||
markAccessControlRun: noOp,
|
markAccessControlRun: noOp,
|
||||||
};
|
};
|
||||||
@@ -64,11 +62,8 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
|||||||
// Get the session from the server using the getServerSession wrapper function
|
// Get the session from the server using the getServerSession wrapper function
|
||||||
const session = await getServerAuthSession({ req, res });
|
const session = await getServerAuthSession({ req, res });
|
||||||
|
|
||||||
const apiKey = req.headers["x-openpipe-api-key"] as string | null;
|
|
||||||
|
|
||||||
return createInnerTRPCContext({
|
return createInnerTRPCContext({
|
||||||
session,
|
session,
|
||||||
apiKey,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
type OutputEvaluation,
|
type OutputEvaluation,
|
||||||
type Dataset,
|
type Dataset,
|
||||||
type DatasetEntry,
|
type DatasetEntry,
|
||||||
type Organization,
|
type Project,
|
||||||
type OrganizationUser,
|
type ProjectUser,
|
||||||
type WorldChampEntrant,
|
type WorldChampEntrant,
|
||||||
type LoggedCall,
|
type LoggedCall,
|
||||||
type LoggedCallModelResponse,
|
type LoggedCallModelResponse,
|
||||||
@@ -43,8 +43,8 @@ interface DB {
|
|||||||
OutputEvaluation: OutputEvaluation;
|
OutputEvaluation: OutputEvaluation;
|
||||||
Dataset: Dataset;
|
Dataset: Dataset;
|
||||||
DatasetEntry: DatasetEntry;
|
DatasetEntry: DatasetEntry;
|
||||||
Organization: Organization;
|
Project: Project;
|
||||||
OrganizationUser: OrganizationUser;
|
ProjectUser: ProjectUser;
|
||||||
WorldChampEntrant: WorldChampEntrant;
|
WorldChampEntrant: WorldChampEntrant;
|
||||||
LoggedCall: LoggedCall;
|
LoggedCall: LoggedCall;
|
||||||
LoggedCallModelResponse: LoggedCallModelResponse;
|
LoggedCallModelResponse: LoggedCallModelResponse;
|
||||||
|
|||||||