Compare commits
112 Commits
add-lib
...
pause-cham
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
816b41adad | ||
|
|
868d7084f0 | ||
|
|
f6f04e537e | ||
|
|
4feb3e5829 | ||
|
|
c62ced867a | ||
|
|
7bb414026e | ||
|
|
1b2b6c1456 | ||
|
|
760bfbbe32 | ||
|
|
3424aa36ba | ||
|
|
ded86cba08 | ||
|
|
65a0f9065f | ||
|
|
2861c64428 | ||
|
|
ca33bb0b08 | ||
|
|
72e680e77c | ||
|
|
5dd7e67396 | ||
|
|
fd286f6874 | ||
|
|
7a4aa5f0aa | ||
|
|
cb791e3c73 | ||
|
|
a2c322ff43 | ||
|
|
a2ace63f25 | ||
|
|
41d06596cb | ||
|
|
49c68fdbf2 | ||
|
|
6188f55569 | ||
|
|
ea91d692d3 | ||
|
|
ae7acbfdd4 | ||
|
|
b9396e63cc | ||
|
|
753a48f6e9 | ||
|
|
bd7c8b43b0 | ||
|
|
a1249f17c9 | ||
|
|
6f8db40f74 | ||
|
|
8c5345a291 | ||
|
|
f47010a6e7 | ||
|
|
6d32f1c06e | ||
|
|
8fed9730da | ||
|
|
0f9a83cf45 | ||
|
|
9f17d98736 | ||
|
|
74029e5478 | ||
|
|
d220cd30e8 | ||
|
|
c0f10cd522 | ||
|
|
dc497dbd99 | ||
|
|
f8f855adf4 | ||
|
|
8f49bace53 | ||
|
|
c9f59bfb79 | ||
|
|
57166e96b4 | ||
|
|
1a838824ae | ||
|
|
6b304f8456 | ||
|
|
a53d70d8b2 | ||
|
|
109a9ddb1e | ||
|
|
7f8b574c9f | ||
|
|
9e859c199e | ||
|
|
deabbb094b | ||
|
|
7637b94ea7 | ||
|
|
721f1726eb | ||
|
|
cfeb4dfa92 | ||
|
|
21ef67ed4c | ||
|
|
7707d451e0 | ||
|
|
d82782adb4 | ||
|
|
e10589abff | ||
|
|
01dcbfc896 | ||
|
|
50e0b34d30 | ||
|
|
44bb9fc58d | ||
|
|
c0d3784f0c | ||
|
|
e522026b71 | ||
|
|
46b13d85b7 | ||
|
|
c12aa82a3e | ||
|
|
b98bce8944 | ||
|
|
f045c80dfd | ||
|
|
3b460dff2a | ||
|
|
5fa5732804 | ||
|
|
28e6e2b9df | ||
|
|
54d1df4442 | ||
|
|
f69c2b5f23 | ||
|
|
51f0666f6a | ||
|
|
b67d974f4c | ||
|
|
33fb2db981 | ||
|
|
e391379c3e | ||
|
|
8d1609dd52 | ||
|
|
f3380f302d | ||
|
|
3dba9c7ee1 | ||
|
|
e0e4f7a9d6 | ||
|
|
48293dc579 | ||
|
|
38ac6243a0 | ||
|
|
bd2f58e2a5 | ||
|
|
808e47c6b9 | ||
|
|
5945f0ed6b | ||
|
|
6bc7d76d15 | ||
|
|
e9ed173e34 | ||
|
|
75d58d7021 | ||
|
|
896c8c5c57 | ||
|
|
ec5547d0b0 | ||
|
|
77e4e3b8c3 | ||
|
|
a1b03ddad1 | ||
|
|
6be32bea4c | ||
|
|
72c70e2a55 | ||
|
|
026532f2c2 | ||
|
|
f88538336f | ||
|
|
3c7178115e | ||
|
|
292aaf090a | ||
|
|
d9915dc41b | ||
|
|
3560bcff14 | ||
|
|
6982339a1a | ||
|
|
d348b130d5 | ||
|
|
bf67580991 | ||
|
|
156f248c3a | ||
|
|
6184498810 | ||
|
|
65a76cddc5 | ||
|
|
c88266bcd4 | ||
|
|
1bf9554eca | ||
|
|
1fb428ef4a | ||
|
|
6316eaae6d | ||
|
|
8513924ea5 | ||
|
|
51d64baae9 |
@@ -6,6 +6,10 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: app
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run-checks:
|
run-checks:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -1,52 +1,62 @@
|
|||||||
<img src="https://github.com/openpipe/openpipe/assets/41524992/ca59596e-eb80-40f9-921f-6d67f6e6d8fa" width="72px" />
|
<!-- <img src="https://github.com/openpipe/openpipe/assets/41524992/ca59596e-eb80-40f9-921f-6d67f6e6d8fa" width="72px" /> -->
|
||||||
|
|
||||||
# OpenPipe
|
# OpenPipe
|
||||||
|
|
||||||
OpenPipe is a flexible playground for comparing and optimizing LLM prompts. It lets you quickly generate, test and compare candidate prompts with realistic sample data.
|
OpenPipe is a flexible playground for comparing and optimizing LLM prompts. It lets you quickly generate, test and compare candidate prompts, and can automatically [translate](#-translate-between-model-apis) those prompts between models.
|
||||||
|
|
||||||
|
<img src="https://github.com/openpipe/openpipe/assets/41524992/219a844e-3f4e-4f6b-8066-41348b42977b" alt="demo">
|
||||||
|
|
||||||
|
You can use our hosted version of OpenPipe at https://openpipe.ai. You can also clone this repository and [run it locally](#running-locally).
|
||||||
|
|
||||||
## Sample Experiments
|
## Sample Experiments
|
||||||
|
|
||||||
These are simple experiments users have created that show how OpenPipe works.
|
These are simple experiments users have created that show how OpenPipe works. Feel free to fork them and start experimenting yourself.
|
||||||
|
|
||||||
- [Country Capitals](https://app.openpipe.ai/experiments/11111111-1111-1111-1111-111111111111)
|
- [Twitter Sentiment Analysis](https://app.openpipe.ai/experiments/62c20a73-2012-4a64-973c-4b665ad46a57)
|
||||||
- [Reddit User Needs](https://app.openpipe.ai/experiments/22222222-2222-2222-2222-222222222222)
|
- [Reddit User Needs](https://app.openpipe.ai/experiments/22222222-2222-2222-2222-222222222222)
|
||||||
- [OpenAI Function Calls](https://app.openpipe.ai/experiments/2ebbdcb3-ed51-456e-87dc-91f72eaf3e2b)
|
- [OpenAI Function Calls](https://app.openpipe.ai/experiments/2ebbdcb3-ed51-456e-87dc-91f72eaf3e2b)
|
||||||
- [Activity Classification](https://app.openpipe.ai/experiments/3950940f-ab6b-4b74-841d-7e9dbc4e4ff8)
|
- [Activity Classification](https://app.openpipe.ai/experiments/3950940f-ab6b-4b74-841d-7e9dbc4e4ff8)
|
||||||
|
|
||||||
<img src="https://github.com/openpipe/openpipe/assets/176426/fc7624c6-5b65-4d4d-82b7-4a816f3e5678" alt="demo" height="400px">
|
|
||||||
|
|
||||||
You can use our hosted version of OpenPipe at [https://openpipe.ai]. You can also clone this repository and [run it locally](#running-locally).
|
|
||||||
|
|
||||||
## High-Level Features
|
|
||||||
|
|
||||||
**Configure Multiple Prompts**
|
|
||||||
Set up multiple prompt configurations and compare their output side-by-side. Each configuration can be configured independently.
|
|
||||||
|
|
||||||
**Visualize Responses**
|
|
||||||
Inspect prompt completions side-by-side.
|
|
||||||
|
|
||||||
**Test Many Inputs**
|
|
||||||
OpenPipe lets you _template_ a prompt. Use the templating feature to run the prompts you're testing against many potential inputs for broader coverage of your problem space than you'd get with manual testing.
|
|
||||||
|
|
||||||
**🪄 Auto-generate Test Scenarios**
|
|
||||||
OpenPipe includes a tool to generate new test scenarios based on your existing prompts and scenarios. Just click "Autogenerate Scenario" to try it out!
|
|
||||||
|
|
||||||
**Prompt Validation and Typeahead**
|
|
||||||
We use OpenAI's OpenAPI spec to automatically provide typeahead and validate prompts.
|
|
||||||
|
|
||||||
<img alt="typeahead" src="https://github.com/openpipe/openpipe/assets/176426/acc638f8-d851-4742-8d01-fe6f98890840" height="300px">
|
|
||||||
|
|
||||||
**Function Call Support**
|
|
||||||
Natively supports [OpenAI function calls](https://openai.com/blog/function-calling-and-other-api-updates) on supported models.
|
|
||||||
|
|
||||||
<img height="300px" alt="function calls" src="https://github.com/openpipe/openpipe/assets/176426/48ad13fe-af2f-4294-bf32-62015597fd9b">
|
|
||||||
|
|
||||||
## Supported Models
|
## Supported Models
|
||||||
|
|
||||||
- All models available through the OpenAI [chat completion API](https://platform.openai.com/docs/guides/gpt/chat-completions-api)
|
- All models available through the OpenAI [chat completion API](https://platform.openai.com/docs/guides/gpt/chat-completions-api)
|
||||||
- Llama2 [7b chat](https://replicate.com/a16z-infra/llama7b-v2-chat), [13b chat](https://replicate.com/a16z-infra/llama13b-v2-chat), [70b chat](https://replicate.com/replicate/llama70b-v2-chat).
|
- Llama2 [7b chat](https://replicate.com/a16z-infra/llama7b-v2-chat), [13b chat](https://replicate.com/a16z-infra/llama13b-v2-chat), [70b chat](https://replicate.com/replicate/llama70b-v2-chat).
|
||||||
- Anthropic's [Claude 1 Instant](https://www.anthropic.com/index/introducing-claude) and [Claude 2](https://www.anthropic.com/index/claude-2)
|
- Anthropic's [Claude 1 Instant](https://www.anthropic.com/index/introducing-claude) and [Claude 2](https://www.anthropic.com/index/claude-2)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 🔍 Visualize Responses
|
||||||
|
|
||||||
|
Inspect prompt completions side-by-side.
|
||||||
|
|
||||||
|
### 🧪 Bulk-Test
|
||||||
|
|
||||||
|
OpenPipe lets you _template_ a prompt. Use the templating feature to run the prompts you're testing against many potential inputs for broad coverage of your problem space.
|
||||||
|
|
||||||
|
### 📟 Translate between Model APIs
|
||||||
|
|
||||||
|
Write your prompt in one format and automatically convert it to work with any other model.
|
||||||
|
|
||||||
|
<img width="480" alt="Screenshot 2023-08-01 at 11 55 38 PM" src="https://github.com/OpenPipe/OpenPipe/assets/41524992/1e19ccf2-96b6-4e93-a3a5-1449710d1b5b" alt="translate between models">
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
### 🛠️ Refine Your Prompts Automatically
|
||||||
|
|
||||||
|
Use a growing database of best-practice refinements to improve your prompts automatically.
|
||||||
|
|
||||||
|
<img width="480" alt="Screenshot 2023-08-01 at 11 55 38 PM" src="https://github.com/OpenPipe/OpenPipe/assets/41524992/87a27fe7-daef-445c-a5e2-1c82b23f9f99" alt="add function call">
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
### 🪄 Auto-generate Test Scenarios
|
||||||
|
|
||||||
|
OpenPipe includes a tool to generate new test scenarios based on your existing prompts and scenarios. Just click "Autogenerate Scenario" to try it out!
|
||||||
|
|
||||||
|
<img width="600" src="https://github.com/openpipe/openpipe/assets/41524992/219a844e-3f4e-4f6b-8066-41348b42977b" alt="auto-generate">
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
|
||||||
## Running Locally
|
## Running Locally
|
||||||
|
|
||||||
1. Install [Postgresql](https://www.postgresql.org/download/).
|
1. Install [Postgresql](https://www.postgresql.org/download/).
|
||||||
@@ -26,6 +26,11 @@ NEXT_PUBLIC_SOCKET_URL="http://localhost:3318"
|
|||||||
NEXTAUTH_SECRET="your_secret"
|
NEXTAUTH_SECRET="your_secret"
|
||||||
NEXTAUTH_URL="http://localhost:3000"
|
NEXTAUTH_URL="http://localhost:3000"
|
||||||
|
|
||||||
|
NEXT_PUBLIC_HOST="http://localhost:3000"
|
||||||
|
|
||||||
# Next Auth Github Provider
|
# Next Auth Github Provider
|
||||||
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_API_KEY="your_key"
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const config = {
|
|||||||
"warn",
|
"warn",
|
||||||
{ vars: "all", varsIgnorePattern: "^_", args: "after-used", argsIgnorePattern: "^_" },
|
{ vars: "all", varsIgnorePattern: "^_", args: "after-used", argsIgnorePattern: "^_" },
|
||||||
],
|
],
|
||||||
|
"react/no-unescaped-entities": "off",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
29
app/.gitignore
vendored
29
app/.gitignore
vendored
@@ -1,11 +1,9 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
# App files
|
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
/.pnp.js
|
.pnp.js
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
@@ -17,28 +15,31 @@
|
|||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
/next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
/.DS_Store
|
.DS_Store
|
||||||
/*.pem
|
*.pem
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
/npm-debug.log*
|
npm-debug.log*
|
||||||
/yarn-debug.log*
|
yarn-debug.log*
|
||||||
/yarn-error.log*
|
yarn-error.log*
|
||||||
/.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
# 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
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
/.vercel
|
.vercel
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
/*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Sentry Auth Token
|
||||||
|
.sentryclirc
|
||||||
|
|||||||
11
app/@types/nextjs-routes.d.ts
vendored
11
app/@types/nextjs-routes.d.ts
vendored
@@ -14,11 +14,20 @@ declare module "nextjs-routes" {
|
|||||||
| StaticRoute<"/account/signin">
|
| StaticRoute<"/account/signin">
|
||||||
| DynamicRoute<"/api/[...trpc]", { "trpc": string[] }>
|
| DynamicRoute<"/api/[...trpc]", { "trpc": string[] }>
|
||||||
| DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }>
|
| DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }>
|
||||||
|
| StaticRoute<"/api/experiments/og-image">
|
||||||
| StaticRoute<"/api/openapi">
|
| StaticRoute<"/api/openapi">
|
||||||
|
| StaticRoute<"/api/sentry-example-api">
|
||||||
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
|
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
|
||||||
|
| DynamicRoute<"/data/[id]", { "id": string }>
|
||||||
|
| 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<"/sentry-example-page">
|
||||||
|
| StaticRoute<"/world-champs">
|
||||||
|
| StaticRoute<"/world-champs/signup">;
|
||||||
|
|
||||||
interface StaticRoute<Pathname> {
|
interface StaticRoute<Pathname> {
|
||||||
pathname: Pathname;
|
pathname: Pathname;
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ FROM base as builder
|
|||||||
# Include all NEXT_PUBLIC_* env vars here
|
# Include all NEXT_PUBLIC_* env vars here
|
||||||
ARG NEXT_PUBLIC_POSTHOG_KEY
|
ARG NEXT_PUBLIC_POSTHOG_KEY
|
||||||
ARG NEXT_PUBLIC_SOCKET_URL
|
ARG NEXT_PUBLIC_SOCKET_URL
|
||||||
|
ARG NEXT_PUBLIC_HOST
|
||||||
|
ARG NEXT_PUBLIC_SENTRY_DSN
|
||||||
|
ARG SENTRY_AUTH_TOKEN
|
||||||
|
ARG NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
|||||||
1
app/dist/tsconfig.tsbuildinfo
vendored
1
app/dist/tsconfig.tsbuildinfo
vendored
File diff suppressed because one or more lines are too long
@@ -1,13 +1,14 @@
|
|||||||
import nextRoutes from "nextjs-routes/config";
|
import nextRoutes from "nextjs-routes/config";
|
||||||
|
import { withSentryConfig } from "@sentry/nextjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
|
||||||
* for Docker builds.
|
* for Docker builds.
|
||||||
*/
|
*/
|
||||||
await import("./src/env.mjs");
|
const { env } = await import("./src/env.mjs");
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const config = {
|
let config = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,6 +22,13 @@ const config = {
|
|||||||
defaultLocale: "en",
|
defaultLocale: "en",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
rewrites: async () => [
|
||||||
|
{
|
||||||
|
source: "/ingest/:path*",
|
||||||
|
destination: "https://app.posthog.com/:path*",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
webpack: (config) => {
|
webpack: (config) => {
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.txt$/,
|
test: /\.txt$/,
|
||||||
@@ -30,4 +38,24 @@ const config = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextRoutes()(config);
|
config = nextRoutes()(config);
|
||||||
|
|
||||||
|
if (env.NEXT_PUBLIC_SENTRY_DSN && env.SENTRY_AUTH_TOKEN) {
|
||||||
|
// @ts-expect-error - `withSentryConfig` is not typed correctly
|
||||||
|
config = withSentryConfig(
|
||||||
|
config,
|
||||||
|
{
|
||||||
|
authToken: env.SENTRY_AUTH_TOKEN,
|
||||||
|
silent: true,
|
||||||
|
org: "openpipe",
|
||||||
|
project: "openpipe",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
widenClientFileUpload: true,
|
||||||
|
tunnelRoute: "/monitoring",
|
||||||
|
disableLogger: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|||||||
@@ -16,17 +16,20 @@
|
|||||||
"postinstall": "prisma generate",
|
"postinstall": "prisma generate",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"codegen": "tsx src/codegen/export-client-types.ts",
|
"codegen": "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"
|
||||||
},
|
},
|
||||||
"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/preset-typescript": "^7.22.5",
|
||||||
"@babel/standalone": "^7.22.9",
|
"@babel/standalone": "^7.22.9",
|
||||||
|
"@chakra-ui/anatomy": "^2.2.0",
|
||||||
"@chakra-ui/next-js": "^2.1.4",
|
"@chakra-ui/next-js": "^2.1.4",
|
||||||
"@chakra-ui/react": "^2.7.1",
|
"@chakra-ui/react": "^2.7.1",
|
||||||
|
"@chakra-ui/styled-system": "^2.9.1",
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/server": "^11.11.0",
|
"@emotion/server": "^11.11.0",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
@@ -34,6 +37,7 @@
|
|||||||
"@monaco-editor/loader": "^1.3.3",
|
"@monaco-editor/loader": "^1.3.3",
|
||||||
"@next-auth/prisma-adapter": "^1.0.5",
|
"@next-auth/prisma-adapter": "^1.0.5",
|
||||||
"@prisma/client": "^4.14.0",
|
"@prisma/client": "^4.14.0",
|
||||||
|
"@sentry/nextjs": "^7.61.0",
|
||||||
"@t3-oss/env-nextjs": "^0.3.1",
|
"@t3-oss/env-nextjs": "^0.3.1",
|
||||||
"@tabler/icons-react": "^2.22.0",
|
"@tabler/icons-react": "^2.22.0",
|
||||||
"@tanstack/react-query": "^4.29.7",
|
"@tanstack/react-query": "^4.29.7",
|
||||||
@@ -41,10 +45,12 @@
|
|||||||
"@trpc/next": "^10.26.0",
|
"@trpc/next": "^10.26.0",
|
||||||
"@trpc/react-query": "^10.26.0",
|
"@trpc/react-query": "^10.26.0",
|
||||||
"@trpc/server": "^10.26.0",
|
"@trpc/server": "^10.26.0",
|
||||||
|
"@vercel/og": "^0.5.9",
|
||||||
"ast-types": "^0.14.2",
|
"ast-types": "^0.14.2",
|
||||||
"chroma-js": "^2.4.2",
|
"chroma-js": "^2.4.2",
|
||||||
"concurrently": "^8.2.0",
|
"concurrently": "^8.2.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"crypto-random-string": "^5.0.0",
|
||||||
"dayjs": "^1.11.8",
|
"dayjs": "^1.11.8",
|
||||||
"dedent": "^1.0.1",
|
"dedent": "^1.0.1",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
@@ -57,25 +63,32 @@
|
|||||||
"json-schema-to-typescript": "^13.0.2",
|
"json-schema-to-typescript": "^13.0.2",
|
||||||
"json-stringify-pretty-compact": "^4.0.0",
|
"json-stringify-pretty-compact": "^4.0.0",
|
||||||
"jsonschema": "^1.4.1",
|
"jsonschema": "^1.4.1",
|
||||||
|
"kysely": "^0.26.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"lucide-react": "^0.265.0",
|
||||||
"next": "^13.4.2",
|
"next": "^13.4.2",
|
||||||
"next-auth": "^4.22.1",
|
"next-auth": "^4.22.1",
|
||||||
"next-query-params": "^4.2.3",
|
"next-query-params": "^4.2.3",
|
||||||
"nextjs-cors": "^2.1.2",
|
"nextjs-cors": "^2.1.2",
|
||||||
"nextjs-routes": "^2.0.1",
|
"nextjs-routes": "^2.0.1",
|
||||||
"openai": "4.0.0-beta.2",
|
"openai": "4.0.0-beta.7",
|
||||||
|
"pg": "^8.11.2",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"posthog-js": "^1.68.4",
|
"posthog-js": "^1.75.3",
|
||||||
|
"posthog-node": "^3.1.1",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.0.0",
|
||||||
"prismjs": "^1.29.0",
|
"prismjs": "^1.29.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-diff-viewer": "^3.1.1",
|
"react-diff-viewer": "^3.1.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-github-btn": "^1.4.0",
|
||||||
"react-icons": "^4.10.1",
|
"react-icons": "^4.10.1",
|
||||||
|
"react-json-tree": "^0.18.0",
|
||||||
"react-select": "^5.7.4",
|
"react-select": "^5.7.4",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"react-textarea-autosize": "^8.5.0",
|
"react-textarea-autosize": "^8.5.0",
|
||||||
"recast": "^0.23.3",
|
"recast": "^0.23.3",
|
||||||
|
"recharts": "^2.7.2",
|
||||||
"replicate": "^0.12.3",
|
"replicate": "^0.12.3",
|
||||||
"socket.io": "^4.7.1",
|
"socket.io": "^4.7.1",
|
||||||
"socket.io-client": "^4.7.1",
|
"socket.io-client": "^4.7.1",
|
||||||
@@ -100,6 +113,7 @@
|
|||||||
"@types/json-schema": "^7.0.12",
|
"@types/json-schema": "^7.0.12",
|
||||||
"@types/lodash-es": "^4.17.8",
|
"@types/lodash-es": "^4.17.8",
|
||||||
"@types/node": "^18.16.0",
|
"@types/node": "^18.16.0",
|
||||||
|
"@types/pg": "^8.10.2",
|
||||||
"@types/pluralize": "^0.0.30",
|
"@types/pluralize": "^0.0.30",
|
||||||
"@types/prismjs": "^1.26.0",
|
"@types/prismjs": "^1.26.0",
|
||||||
"@types/react": "^18.2.6",
|
"@types/react": "^18.2.6",
|
||||||
@@ -108,6 +122,7 @@
|
|||||||
"@types/uuid": "^9.0.2",
|
"@types/uuid": "^9.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.6",
|
"@typescript-eslint/eslint-plugin": "^5.59.6",
|
||||||
"@typescript-eslint/parser": "^5.59.6",
|
"@typescript-eslint/parser": "^5.59.6",
|
||||||
|
"csv-parse": "^5.4.0",
|
||||||
"eslint": "^8.40.0",
|
"eslint": "^8.40.0",
|
||||||
"eslint-config-next": "^13.4.2",
|
"eslint-config-next": "^13.4.2",
|
||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
|
|||||||
1503
app/pnpm-lock.yaml
generated
1503
app/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
84
app/prisma/datasets/validated_tweets.csv
Normal file
84
app/prisma/datasets/validated_tweets.csv
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
Text,sentiment,emotion
|
||||||
|
@dell your customer service is horrible especially agent syedfaisal who has made this experience of purchasing a new computer downright awful and I’ll reconsider ever buying a Dell in the future @DellTech,negative,anger
|
||||||
|
@zacokalo @Dell @DellCares @Dell give the man what he paid for!,neutral,anger
|
||||||
|
"COOKING STREAM DAY!!! Ty to @Alienware for sponsoring this stream! I’ll be making a bunch of Japanese Alien themed foods hehe
|
||||||
|
|
||||||
|
Come check it out! https://t.co/m06tJQ06zk
|
||||||
|
|
||||||
|
#alienwarepartner #intelgaming @Dell @IntelGaming https://t.co/qOdQX2E8VD",positive,joy
|
||||||
|
@emijuju_ @Alienware @Dell @intel Beautiful 😍❤️😻,positive,joy
|
||||||
|
"What's your biggest data management challenge? • Cloud complexity? • Lengthy tech refresh cycles? • Capital budget constraints? Solve your challenges with as-a-Storage. Get simplicity, agility & control with @Dell #APEX. https://t.co/mCblMtH931 https://t.co/eepKNZ4Ai3",neutral,optimism
|
||||||
|
"This week we were at the ""Top Gun"" themed @Dell Product Expo. Eddie Muñoz met Maverick look-alike, California Tom Cruise (Jerome LeBlanc)!
|
||||||
|
|
||||||
|
""I feel the need, the need for speed."" - Maverick
|
||||||
|
#topgun #topgunmaverick #dell #delltechnologies #lockncharge https://t.co/QHYH2EbMjq",positive,joy
|
||||||
|
"Itsss been more than a week...i m following up with dell for troubleshootings...my https://t.co/lWhg2YKhQa suffering so as my hard earned money...hightly disappointed...contd..
|
||||||
|
@DellCares @Dell",negative,sadness
|
||||||
|
"@ashu_k7 @Dell Pathetic!!!!! I Dont mind taking legal action, this is deficency of service for which the customer is nt getting help..",negative,anger
|
||||||
|
@ashu_k7 @Dell Making life unhappy is the new tag line of #Dell,negative,sadness
|
||||||
|
"@Dell If you are buying a Dell, make sure you are making your life hell.
|
||||||
|
Better buy other laptops. If you wanted to opt for Dell better opt for garbage on the streets.",negative,anger
|
||||||
|
"MY DESK'S FINAL FORM? Seriously, I'm finally happy with my monitor setup here... and I'll keep this setup whenever I move... FOREVER. What do you think?
|
||||||
|
https://t.co/WJZ2JXtOnX
|
||||||
|
@Alienware @Dell cheers. https://t.co/6Whhldfpv0",positive,joy
|
||||||
|
"@Dell Dell Alienware computer has had software problems with SupportAssist since purchase. Dell, despite paying for Premium Support, has never fixed issues. Latest solution was to erase everything and reload....SupportAssist still doesn't work.",negative,anger
|
||||||
|
"HUGE congratulations to Startup Battle 3.0 winner ➡️ @Ox_Fulfillment x @cyborgcharu for being featured in @BusinessInsider & @Dell showcasing the journey at Ox! 🚀🚀🚀
|
||||||
|
|
||||||
|
We love to see our portfolio companies continuing to BUILD SOMETHING FROM NOTHING! 🔥 https://t.co/awBkn5ippB",positive,joy
|
||||||
|
@Dell happy Friday!,positive,joy
|
||||||
|
"@intel Core i5 1135G7 - 4732 points
|
||||||
|
@intel Core i5 1235 - 6619 points
|
||||||
|
@Dell Latitude 5420 x 5430.
|
||||||
|
Cinebench R23. Good job Intel!",positive,joy
|
||||||
|
@Dell india we purchased 52 docking station and we have around 100 users using dell laptop as well as dell monitor now they are refusing to replace my faulty product and disconnecting my every call....,negative,anger
|
||||||
|
"It's another year ans another day But cant fill it in yet the child hood dreams.
|
||||||
|
It's my birthdy today. Can anyone of you guys bless me with a simplest gaming oc that can run
|
||||||
|
@DOTA2 ?
|
||||||
|
@Dell @HP @VastGG @Acer @Alienware @Lenovo @toshiba @IBM @Fujitsu_Global @NEC https://t.co/69G8tL9sN8",neutral,joy
|
||||||
|
"@idoccor @Dell That's always the decision—wait, or, look elsewhere. In this case, I think I unfortunately need to wait since there are only two monitors with these specs and I don't like the other one 😂",negative,sadness
|
||||||
|
"@MichaelDell @Dell @DellCares For how long this will continue. It is high time you either fix the problem for good or replace the complete laptop. Spent over 60+ hours with Customer Care teams, which is not helping. Cannot keep going on like this.",negative,anger
|
||||||
|
"@Dell @DellCares but no, not really",neutral,sadness
|
||||||
|
"Business innovation requires insight, agility and efficiency. How do you get there? RP PRO, LLC recommends starting by proactively managing IT infrastructure with #OpenManage Systems from @Dell. https://t.co/fBcK1lfFMu https://t.co/xWHLkkHCjn",neutral,optimism
|
||||||
|
@Dell Yessirrrrr #NationalCoffeeDay,positive,joy
|
||||||
|
"New blog post from @Dell shared on https://t.co/EgfPChB8AT
|
||||||
|
|
||||||
|
Re-routing Our Connected and Autonomous Future https://t.co/AW8EHQrbd6
|
||||||
|
|
||||||
|
#future #futuretech #techinnovation https://t.co/koX8stKPsr",neutral,joy
|
||||||
|
"In a free-market economy, the folks @IronMountain can set prices as they see fit. Their customers are also free to find better prices at competitors like @Dell
|
||||||
|
@H3CGlobal @HPE
|
||||||
|
https://t.co/reZ56DNTBI",neutral,optimism
|
||||||
|
"Delighted to chat with many of our partners here in person at @Intel Innovation! @Dell, @Lenovo, @Supermicro_SMCI, @QuantaQCT #IntelON https://t.co/BxIeGW8deN",positive,joy
|
||||||
|
"A special gracias to our Startup Chica San Antonio 2022 sponsors @eBay, @jcpenney, @Barbie, @HEB, @Dell, @Honda, @SouthsideSATX💜✨ https://t.co/lZ6WWkziHl",positive,joy
|
||||||
|
"When your team decides to start supporting developers, your #ops must change too. More from @cote and @Dell Developer Community Manager @barton808: https://t.co/W6f1oMiTgV",neutral,optimism
|
||||||
|
@EmDStowers @LASERGIANT1 @ohwormongod @Ludovician_Vega @Dell our boy snitchin,neutral,anger
|
||||||
|
A 1st place dmi:Design Value Award goes to @Dell for a packaging modernization initiative that helped them get closer to their corporate Moonshot Sustainability Goal of 100% recycled or renewable packaging by 2030. More at https://t.co/dnhZWWLCQC #designvalue #DVA22,positive,optimism
|
||||||
|
Reducing deployment and maintenance complexity is the goal behind @dell and @WindRiver's new collaboration. https://t.co/2PxQgPuHUU,positive,optimism
|
||||||
|
@jaserhunter @Dell Love the sales pitch lol,positive,joy
|
||||||
|
@Dell india we purchased 52 docking station and we have around 100 users using dell laptop as well as dell monitor now they are refusing to replace my faulty product and disconnecting my every call....,negative,anger
|
||||||
|
@ashu_k7 @Dell One more example.. their technical support is also worse. https://t.co/20atSgI4fg,negative,anger
|
||||||
|
*angry screeches about @Dell proprietary MBR windows 8.1 partitions not being able to save as an img in clonezilla *,negative,anger
|
||||||
|
@socialitebooks @BBYC_Gamers @Dell @Alienware @BestBuyCanada @intelcanada Congratulations!!!,positive,joy
|
||||||
|
"Thank you to the @dell team for coming out to volunteer today! We truly appreciate your hard work and look forward to seeing you again soon!
|
||||||
|
|
||||||
|
If you and your team are interested in helping out at the UMLAUF, visit our website for more information: https://t.co/lVfsZT2ogS https://t.co/eLz0FY0y4M",positive,joy
|
||||||
|
"@TheCaramelGamer @intel @bravadogaming @Intel_Africa @Dell @DellTech @DellTechMEA @Alienware @IntelUK we love to see it.
|
||||||
|
|
||||||
|
Also also actually actually whoever did that artwork? 🔥🔥🔥 am a fan.",positive,joy
|
||||||
|
"LOVING MY DELL 2 IN 1 LAPTOP
|
||||||
|
YAYY 🥳🥳
|
||||||
|
@Dell #DellInspiron #DellLaptop https://t.co/vib96jf3tC",positive,joy
|
||||||
|
@Azure @OracleItalia @AWS_Italy @lenovoitalia @Dell discussing the future of #HPC during the #hpcroundtable22 in Turin today #highperformancecomputing https://t.co/jJ1WqBulPF,neutral,joy
|
||||||
|
Attracting talent @AmericanChamber. @marg_cola @Dell speaks of quality of life connectivity and the Opportunity for development being so crucial. Housing availability is now impacting on decision making for potential candidates. #WhyCork,positive,optimism
|
||||||
|
.@Dell partners with @WindRiver on modular cloud-native telecommunications infrastructure https://t.co/4SWATspwCP @SiliconANGLE @Mike_Wheatley @holgermu @constellationr,neutral,joy
|
||||||
|
@Dell Not buy Dell Inspiron laptop,neutral,sadness
|
||||||
|
"@dell #delltechforum reminding us IDC have predicted that by 2024, 50% of everything we consume in technology will be as a service https://t.co/3UBiZJX0LE",neutral,optimism
|
||||||
|
@RachMurph @HETTShow @Dell Thank you for coming! Great evening,positive,joy
|
||||||
|
Congratulations to Jason M of Moncton NB on winning a @Dell @Alienware m15 R7 15.6″ gaming laptop from @BestBuyCanada and @intelcanada's gaming days #contest on the blog. Visit https://t.co/VryaY5Rvv9 to learn about tech and for chances to win new tech. https://t.co/T6n0dzF6oL,positive,joy
|
||||||
|
@MattVisiwig @Dell Sour taste for sure 😶 But don't let ego distract you from what you really want to buy 😁,neutral,optimism
|
||||||
|
"Massive thank you goes to sponsors @HendersonLoggie @lindsaysnews @Dell @unity, all of our fantastic judges and mentors and the team at @EGX and @ExCeLLondon.
|
||||||
|
|
||||||
|
Big congratulations also to all of our other @AbertayDare teams - an amazing year! #Dare2022 https://t.co/jYe4agO7lW",positive,joy
|
||||||
|
"@timetcetera @rahaug Nah, I just need @Dell to start paying me comissions 😂",neutral,joy
|
||||||
|
"""Whether you’re an engineer, a designer, or work in supply chain management or sales, there are always opportunities to think about sustainability and how you can do things more efficiently."" 👏 — Oliver Campbell, Director of Packaging Engineering, @Dell https://t.co/vUJLTWNFwP https://t.co/GJWAzGfAxJ",positive,optimism
|
||||||
|
"Hi, my name is @listerepvp and I support @Dell, always.",positive,joy
|
||||||
|
@@ -1,64 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "Experiment" ADD COLUMN "dataFlowId" UUID;
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "ModelResponse" ADD COLUMN "loggedCallId" UUID,
|
|
||||||
ALTER COLUMN "scenarioVariantCellId" DROP NOT NULL;
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "LoggedCall" (
|
|
||||||
"id" UUID NOT NULL,
|
|
||||||
"promptFunctionHash" TEXT NOT NULL,
|
|
||||||
"promptFunction" TEXT NOT NULL,
|
|
||||||
"prompt" JSONB NOT NULL,
|
|
||||||
"responsePayload" JSONB,
|
|
||||||
"scenarioVariables" JSONB NOT NULL,
|
|
||||||
"model" TEXT NOT NULL,
|
|
||||||
"modelProvider" TEXT NOT NULL,
|
|
||||||
"dataFlowId" UUID NOT NULL,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "LoggedCall_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "DataFlow" (
|
|
||||||
"id" UUID NOT NULL,
|
|
||||||
"label" TEXT NOT NULL,
|
|
||||||
"organizationId" UUID NOT NULL,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "DataFlow_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "ApiKey" (
|
|
||||||
"id" UUID NOT NULL,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"key" TEXT NOT NULL,
|
|
||||||
"organizationId" UUID NOT NULL,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "ApiKey_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "ApiKey_key_key" ON "ApiKey"("key");
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "Experiment" ADD CONSTRAINT "Experiment_dataFlowId_fkey" FOREIGN KEY ("dataFlowId") REFERENCES "DataFlow"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "ModelResponse" ADD CONSTRAINT "ModelResponse_loggedCallId_fkey" FOREIGN KEY ("loggedCallId") REFERENCES "LoggedCall"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "LoggedCall" ADD CONSTRAINT "LoggedCall_dataFlowId_fkey" FOREIGN KEY ("dataFlowId") REFERENCES "DataFlow"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "DataFlow" ADD CONSTRAINT "DataFlow_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "WorldChampEntrant" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"userId" UUID NOT NULL,
|
||||||
|
"approved" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "WorldChampEntrant_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "WorldChampEntrant_userId_key" ON "WorldChampEntrant"("userId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "WorldChampEntrant" ADD CONSTRAINT "WorldChampEntrant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'USER');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "role" "UserRole" NOT NULL DEFAULT 'USER';
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Dataset" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"organizationId" UUID NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Dataset_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "DatasetEntry" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"input" TEXT NOT NULL,
|
||||||
|
"output" TEXT,
|
||||||
|
"datasetId" UUID NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "DatasetEntry_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Dataset" ADD CONSTRAINT "Dataset_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "DatasetEntry" ADD CONSTRAINT "DatasetEntry_datasetId_fkey" FOREIGN KEY ("datasetId") REFERENCES "Dataset"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `constructFn` on the `PromptVariant` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `constructFnVersion` on the `PromptVariant` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `promptConstructor` to the `PromptVariant` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `promptConstructorVersion` to the `PromptVariant` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
|
||||||
|
ALTER TABLE "PromptVariant" RENAME COLUMN "constructFn" TO "promptConstructor";
|
||||||
|
ALTER TABLE "PromptVariant" RENAME COLUMN "constructFnVersion" TO "promptConstructorVersion";
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "LoggedCall" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"startTime" TIMESTAMP(3) NOT NULL,
|
||||||
|
"cacheHit" BOOLEAN NOT NULL,
|
||||||
|
"modelResponseId" UUID NOT NULL,
|
||||||
|
"organizationId" UUID NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "LoggedCall_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "LoggedCallModelResponse" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"reqPayload" JSONB NOT NULL,
|
||||||
|
"respStatus" INTEGER,
|
||||||
|
"respPayload" JSONB,
|
||||||
|
"error" TEXT,
|
||||||
|
"startTime" TIMESTAMP(3) NOT NULL,
|
||||||
|
"endTime" TIMESTAMP(3) NOT NULL,
|
||||||
|
"cacheKey" TEXT,
|
||||||
|
"durationMs" INTEGER,
|
||||||
|
"inputTokens" INTEGER,
|
||||||
|
"outputTokens" INTEGER,
|
||||||
|
"finishReason" TEXT,
|
||||||
|
"completionId" TEXT,
|
||||||
|
"totalCost" DECIMAL(18,12),
|
||||||
|
"originalLoggedCallId" UUID NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "LoggedCallModelResponse_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "LoggedCallTag" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"value" TEXT,
|
||||||
|
"loggedCallId" UUID NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "LoggedCallTag_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ApiKey" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"apiKey" TEXT NOT NULL,
|
||||||
|
"organizationId" UUID NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ApiKey_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "LoggedCall_startTime_idx" ON "LoggedCall"("startTime");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "LoggedCallModelResponse_originalLoggedCallId_key" ON "LoggedCallModelResponse"("originalLoggedCallId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "LoggedCallModelResponse_cacheKey_idx" ON "LoggedCallModelResponse"("cacheKey");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "LoggedCallTag_name_idx" ON "LoggedCallTag"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "LoggedCallTag_name_value_idx" ON "LoggedCallTag"("name", "value");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ApiKey_apiKey_key" ON "ApiKey"("apiKey");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "LoggedCall" ADD CONSTRAINT "LoggedCall_modelResponseId_fkey" FOREIGN KEY ("modelResponseId") REFERENCES "LoggedCallModelResponse"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "LoggedCall" ADD CONSTRAINT "LoggedCall_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "LoggedCallModelResponse" ADD CONSTRAINT "LoggedCallModelResponse_originalLoggedCallId_fkey" FOREIGN KEY ("originalLoggedCallId") REFERENCES "LoggedCall"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "LoggedCallTag" ADD CONSTRAINT "LoggedCallTag_loggedCallId_fkey" FOREIGN KEY ("loggedCallId") REFERENCES "LoggedCall"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Organization" ADD COLUMN "name" TEXT NOT NULL DEFAULT 'Project 1';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LoggedCall" ALTER COLUMN "modelResponseId" DROP NOT NULL;
|
||||||
@@ -19,9 +19,6 @@ model Experiment {
|
|||||||
organizationId String @db.Uuid
|
organizationId String @db.Uuid
|
||||||
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
dataFlowId String? @db.Uuid
|
|
||||||
dataFlow DataFlow? @relation(fields: [dataFlowId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@ -35,8 +32,8 @@ model PromptVariant {
|
|||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
label String
|
label String
|
||||||
constructFn String
|
promptConstructor String
|
||||||
constructFnVersion Int
|
promptConstructorVersion Int
|
||||||
model String
|
model String
|
||||||
modelProvider String
|
modelProvider String
|
||||||
|
|
||||||
@@ -130,13 +127,10 @@ model ModelResponse {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
scenarioVariantCellId String? @db.Uuid
|
scenarioVariantCellId String @db.Uuid
|
||||||
scenarioVariantCell ScenarioVariantCell? @relation(fields: [scenarioVariantCellId], references: [id], onDelete: Cascade)
|
scenarioVariantCell ScenarioVariantCell @relation(fields: [scenarioVariantCellId], references: [id], onDelete: Cascade)
|
||||||
outputEvaluations OutputEvaluation[]
|
outputEvaluations OutputEvaluation[]
|
||||||
|
|
||||||
loggedCallId String? @db.Uuid
|
|
||||||
loggedCall LoggedCall? @relation(fields: [loggedCallId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@index([inputHash])
|
@@index([inputHash])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,62 +174,47 @@ model OutputEvaluation {
|
|||||||
@@unique([modelResponseId, evaluationId])
|
@@unique([modelResponseId, evaluationId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model LoggedCall {
|
model Dataset {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
|
||||||
|
|
||||||
promptFunctionHash String
|
|
||||||
promptFunction String
|
|
||||||
prompt Json
|
|
||||||
responsePayload Json?
|
|
||||||
scenarioVariables Json
|
|
||||||
model String
|
|
||||||
modelProvider String
|
|
||||||
|
|
||||||
dataFlowId String @db.Uuid
|
|
||||||
dataFlow DataFlow @relation(fields: [dataFlowId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
modelResponse ModelResponse[]
|
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
model DataFlow {
|
|
||||||
id String @id @default(uuid()) @db.Uuid
|
|
||||||
|
|
||||||
label String
|
|
||||||
experiments Experiment[]
|
|
||||||
loggedCalls LoggedCall[]
|
|
||||||
organizationId String @db.Uuid
|
|
||||||
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
model ApiKey {
|
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
name String
|
name String
|
||||||
key String @unique
|
datasetEntries DatasetEntry[]
|
||||||
|
|
||||||
organizationId String @db.Uuid
|
organizationId String @db.Uuid
|
||||||
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model DatasetEntry {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
|
input String
|
||||||
|
output String?
|
||||||
|
|
||||||
|
datasetId String @db.Uuid
|
||||||
|
dataset Dataset? @relation(fields: [datasetId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO rename Organization to Project
|
||||||
model Organization {
|
model Organization {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
name String @default("Project 1")
|
||||||
|
|
||||||
personalOrgUserId String? @unique @db.Uuid
|
personalOrgUserId String? @unique @db.Uuid
|
||||||
personalOrgUser User? @relation(fields: [personalOrgUserId], references: [id], onDelete: Cascade)
|
personalOrgUser User? @relation(fields: [personalOrgUserId], references: [id], onDelete: Cascade)
|
||||||
apiKeys ApiKey[]
|
|
||||||
dataFlows DataFlow[]
|
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
organizationUsers OrganizationUser[]
|
organizationUsers OrganizationUser[]
|
||||||
experiments Experiment[]
|
experiments Experiment[]
|
||||||
|
datasets Dataset[]
|
||||||
|
loggedCalls LoggedCall[]
|
||||||
|
apiKeys ApiKey[]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum OrganizationUserRole {
|
enum OrganizationUserRole {
|
||||||
@@ -261,6 +240,113 @@ model OrganizationUser {
|
|||||||
@@unique([organizationId, userId])
|
@@unique([organizationId, userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model WorldChampEntrant {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
|
userId String @db.Uuid
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
approved Boolean @default(false)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@unique([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model LoggedCall {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
|
startTime DateTime
|
||||||
|
|
||||||
|
// True if this call was served from the cache, false otherwise
|
||||||
|
cacheHit Boolean
|
||||||
|
|
||||||
|
// A LoggedCall is always associated with a LoggedCallModelResponse. If this
|
||||||
|
// is a cache miss, we create a new LoggedCallModelResponse.
|
||||||
|
// If it's a cache hit, it's a pre-existing LoggedCallModelResponse.
|
||||||
|
modelResponseId String? @db.Uuid
|
||||||
|
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.
|
||||||
|
createdResponses LoggedCallModelResponse[] @relation(name: "ModelResponseOriginalCall")
|
||||||
|
|
||||||
|
organizationId String @db.Uuid
|
||||||
|
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
tags LoggedCallTag[]
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([startTime])
|
||||||
|
}
|
||||||
|
|
||||||
|
model LoggedCallModelResponse {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
|
reqPayload Json
|
||||||
|
|
||||||
|
// The HTTP status returned by the model provider
|
||||||
|
respStatus Int?
|
||||||
|
respPayload Json?
|
||||||
|
|
||||||
|
// Should be null if the request was successful, and some string if the request failed.
|
||||||
|
error String?
|
||||||
|
|
||||||
|
startTime DateTime
|
||||||
|
endTime DateTime
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// attack vector. Also, we should only set the cacheKey on the model if the
|
||||||
|
// request was successful.
|
||||||
|
cacheKey String?
|
||||||
|
|
||||||
|
// Derived fields
|
||||||
|
durationMs Int?
|
||||||
|
inputTokens Int?
|
||||||
|
outputTokens Int?
|
||||||
|
finishReason String?
|
||||||
|
completionId String?
|
||||||
|
totalCost Decimal? @db.Decimal(18, 12)
|
||||||
|
|
||||||
|
// The LoggedCall that created this LoggedCallModelResponse
|
||||||
|
originalLoggedCallId String @unique @db.Uuid
|
||||||
|
originalLoggedCall LoggedCall @relation(name: "ModelResponseOriginalCall", fields: [originalLoggedCallId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
loggedCalls LoggedCall[]
|
||||||
|
|
||||||
|
@@index([cacheKey])
|
||||||
|
}
|
||||||
|
|
||||||
|
model LoggedCallTag {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
name String
|
||||||
|
value String?
|
||||||
|
|
||||||
|
loggedCallId String @db.Uuid
|
||||||
|
loggedCall LoggedCall @relation(fields: [loggedCallId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([name])
|
||||||
|
@@index([name, value])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ApiKey {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
|
name String
|
||||||
|
apiKey String @unique
|
||||||
|
|
||||||
|
organizationId String @db.Uuid
|
||||||
|
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
model Account {
|
model Account {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
userId String @db.Uuid
|
userId String @db.Uuid
|
||||||
@@ -288,16 +374,28 @@ model Session {
|
|||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum UserRole {
|
||||||
|
ADMIN
|
||||||
|
USER
|
||||||
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
name String?
|
name String?
|
||||||
email String? @unique
|
email String? @unique
|
||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
image String?
|
image String?
|
||||||
|
|
||||||
|
role UserRole @default(USER)
|
||||||
|
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
organizationUsers OrganizationUser[]
|
organizationUsers OrganizationUser[]
|
||||||
organizations Organization[]
|
organizations Organization[]
|
||||||
|
worldChampEntrant WorldChampEntrant?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model VerificationToken {
|
model VerificationToken {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
import dedent from "dedent";
|
import dedent from "dedent";
|
||||||
import { generateNewCell } from "~/server/utils/generateNewCell";
|
import { generateNewCell } from "~/server/utils/generateNewCell";
|
||||||
|
import { promptConstructorVersion } from "~/promptConstructor/version";
|
||||||
|
|
||||||
const defaultId = "11111111-1111-1111-1111-111111111111";
|
const defaultId = "11111111-1111-1111-1111-111111111111";
|
||||||
|
|
||||||
@@ -51,8 +52,8 @@ await prisma.promptVariant.createMany({
|
|||||||
sortIndex: 0,
|
sortIndex: 0,
|
||||||
model: "gpt-3.5-turbo-0613",
|
model: "gpt-3.5-turbo-0613",
|
||||||
modelProvider: "openai/ChatCompletion",
|
modelProvider: "openai/ChatCompletion",
|
||||||
constructFnVersion: 1,
|
promptConstructorVersion,
|
||||||
constructFn: dedent`
|
promptConstructor: dedent`
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-3.5-turbo-0613",
|
model: "gpt-3.5-turbo-0613",
|
||||||
messages: [
|
messages: [
|
||||||
@@ -70,8 +71,8 @@ await prisma.promptVariant.createMany({
|
|||||||
sortIndex: 1,
|
sortIndex: 1,
|
||||||
model: "gpt-3.5-turbo-0613",
|
model: "gpt-3.5-turbo-0613",
|
||||||
modelProvider: "openai/ChatCompletion",
|
modelProvider: "openai/ChatCompletion",
|
||||||
constructFnVersion: 1,
|
promptConstructorVersion,
|
||||||
constructFn: dedent`
|
promptConstructor: dedent`
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-3.5-turbo-0613",
|
model: "gpt-3.5-turbo-0613",
|
||||||
messages: [
|
messages: [
|
||||||
|
|||||||
128
app/prisma/seedAgiEval.ts
Normal file
128
app/prisma/seedAgiEval.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { prisma } from "~/server/db";
|
||||||
|
import { generateNewCell } from "~/server/utils/generateNewCell";
|
||||||
|
import dedent from "dedent";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import fs from "fs";
|
||||||
|
import { promptConstructorVersion } from "~/promptConstructor/version";
|
||||||
|
|
||||||
|
const defaultId = "11111111-1111-1111-1111-111111111112";
|
||||||
|
|
||||||
|
await prisma.organization.deleteMany({
|
||||||
|
where: { id: defaultId },
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there's an existing org, just seed into it
|
||||||
|
const org =
|
||||||
|
(await prisma.organization.findFirst({})) ??
|
||||||
|
(await prisma.organization.create({
|
||||||
|
data: { id: defaultId },
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Clone the repo from git@github.com:microsoft/AGIEval.git into a tmp dir if it doesn't exist
|
||||||
|
const tmpDir = "/tmp/agi-eval";
|
||||||
|
if (!fs.existsSync(tmpDir)) {
|
||||||
|
execSync(`git clone git@github.com:microsoft/AGIEval.git ${tmpDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const datasets = [
|
||||||
|
"sat-en",
|
||||||
|
"sat-math",
|
||||||
|
"lsat-rc",
|
||||||
|
"lsat-ar",
|
||||||
|
"aqua-rat",
|
||||||
|
"logiqa-en",
|
||||||
|
"lsat-lr",
|
||||||
|
"math",
|
||||||
|
];
|
||||||
|
|
||||||
|
type Scenario = {
|
||||||
|
passage: string | null;
|
||||||
|
question: string;
|
||||||
|
options: string[] | null;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const dataset of datasets) {
|
||||||
|
const experimentName = `AGI-Eval: ${dataset}`;
|
||||||
|
const oldExperiment = await prisma.experiment.findFirst({
|
||||||
|
where: {
|
||||||
|
label: experimentName,
|
||||||
|
organizationId: org.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (oldExperiment) {
|
||||||
|
await prisma.experiment.deleteMany({
|
||||||
|
where: { id: oldExperiment.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const experiment = await prisma.experiment.create({
|
||||||
|
data: {
|
||||||
|
id: oldExperiment?.id ?? undefined,
|
||||||
|
label: experimentName,
|
||||||
|
organizationId: org.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const scenarios: Scenario[] = fs
|
||||||
|
.readFileSync(`${tmpDir}/data/v1/${dataset}.jsonl`, "utf8")
|
||||||
|
.split("\n")
|
||||||
|
.filter((line) => line.length > 0)
|
||||||
|
.map((line) => JSON.parse(line) as Scenario);
|
||||||
|
console.log("scenarios", scenarios.length);
|
||||||
|
|
||||||
|
await prisma.testScenario.createMany({
|
||||||
|
data: scenarios.slice(0, 30).map((scenario, i) => ({
|
||||||
|
experimentId: experiment.id,
|
||||||
|
sortIndex: i,
|
||||||
|
variableValues: {
|
||||||
|
passage: scenario.passage,
|
||||||
|
question: scenario.question,
|
||||||
|
options: scenario.options?.join("\n"),
|
||||||
|
label: scenario.label,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.templateVariable.createMany({
|
||||||
|
data: ["passage", "question", "options", "label"].map((label) => ({
|
||||||
|
experimentId: experiment.id,
|
||||||
|
label,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.promptVariant.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
experimentId: experiment.id,
|
||||||
|
label: "Prompt Variant 1",
|
||||||
|
sortIndex: 0,
|
||||||
|
model: "gpt-3.5-turbo-0613",
|
||||||
|
modelProvider: "openai/ChatCompletion",
|
||||||
|
promptConstructorVersion,
|
||||||
|
promptConstructor: dedent`
|
||||||
|
definePrompt("openai/ChatCompletion", {
|
||||||
|
model: "gpt-3.5-turbo-0613",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: \`Passage: ${"$"}{scenario.passage}\n\nQuestion: ${"$"}{scenario.question}\n\nOptions: ${"$"}{scenario.options}\n\n Respond with just the letter of the best option in the format Answer: (A).\`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
temperature: 0,
|
||||||
|
})`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.evaluation.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
experimentId: experiment.id,
|
||||||
|
label: "Eval",
|
||||||
|
evalType: "CONTAINS",
|
||||||
|
value: "Answer: ({{label}})",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
410
app/prisma/seedDashboard.ts
Normal file
410
app/prisma/seedDashboard.ts
Normal file
File diff suppressed because one or more lines are too long
114
app/prisma/seedTwitterSentiment.ts
Normal file
114
app/prisma/seedTwitterSentiment.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { prisma } from "~/server/db";
|
||||||
|
import dedent from "dedent";
|
||||||
|
import fs from "fs";
|
||||||
|
import { parse } from "csv-parse/sync";
|
||||||
|
import { promptConstructorVersion } from "~/promptConstructor/version";
|
||||||
|
|
||||||
|
const defaultId = "11111111-1111-1111-1111-111111111112";
|
||||||
|
|
||||||
|
await prisma.organization.deleteMany({
|
||||||
|
where: { id: defaultId },
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there's an existing org, just seed into it
|
||||||
|
const org =
|
||||||
|
(await prisma.organization.findFirst({})) ??
|
||||||
|
(await prisma.organization.create({
|
||||||
|
data: { id: defaultId },
|
||||||
|
}));
|
||||||
|
|
||||||
|
type Scenario = {
|
||||||
|
text: string;
|
||||||
|
sentiment: string;
|
||||||
|
emotion: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const experimentName = `Twitter Sentiment Analysis`;
|
||||||
|
const oldExperiment = await prisma.experiment.findFirst({
|
||||||
|
where: {
|
||||||
|
label: experimentName,
|
||||||
|
organizationId: org.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (oldExperiment) {
|
||||||
|
await prisma.experiment.deleteMany({
|
||||||
|
where: { id: oldExperiment.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const experiment = await prisma.experiment.create({
|
||||||
|
data: {
|
||||||
|
id: oldExperiment?.id ?? undefined,
|
||||||
|
label: experimentName,
|
||||||
|
organizationId: org.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = fs.readFileSync("./prisma/datasets/validated_tweets.csv", "utf8");
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const records: any[] = parse(content, { delimiter: ",", from_line: 2 });
|
||||||
|
|
||||||
|
console.log("records", records);
|
||||||
|
|
||||||
|
const scenarios: Scenario[] = records.map((row) => ({
|
||||||
|
text: row[0],
|
||||||
|
sentiment: row[1],
|
||||||
|
emotion: row[2],
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log("scenarios", scenarios.length);
|
||||||
|
|
||||||
|
await prisma.testScenario.createMany({
|
||||||
|
data: scenarios.slice(0, 30).map((scenario, i) => ({
|
||||||
|
experimentId: experiment.id,
|
||||||
|
sortIndex: i,
|
||||||
|
variableValues: {
|
||||||
|
text: scenario.text,
|
||||||
|
sentiment: scenario.sentiment,
|
||||||
|
emotion: scenario.emotion,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.templateVariable.createMany({
|
||||||
|
data: ["text", "sentiment", "emotion"].map((label) => ({
|
||||||
|
experimentId: experiment.id,
|
||||||
|
label,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.promptVariant.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
experimentId: experiment.id,
|
||||||
|
label: "Prompt Variant 1",
|
||||||
|
sortIndex: 0,
|
||||||
|
model: "gpt-3.5-turbo-0613",
|
||||||
|
modelProvider: "openai/ChatCompletion",
|
||||||
|
promptConstructorVersion,
|
||||||
|
promptConstructor: dedent`
|
||||||
|
definePrompt("openai/ChatCompletion", {
|
||||||
|
model: "gpt-3.5-turbo-0613",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: \`Text: ${"$"}{scenario.text}\n\nRespond with the sentiment (negative|neutral|positive) and emotion (optimism|joy|anger|sadness) of the tweet in this format: "answer: <sentiment>-<emotion>".\`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
temperature: 0,
|
||||||
|
})`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.evaluation.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
experimentId: experiment.id,
|
||||||
|
label: "Eval",
|
||||||
|
evalType: "CONTAINS",
|
||||||
|
value: "answer: {{sentiment}}-{{emotion}}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
BIN
app/public/fonts/Inconsolata_SemiExpanded-Medium.ttf
Normal file
BIN
app/public/fonts/Inconsolata_SemiExpanded-Medium.ttf
Normal file
Binary file not shown.
BIN
app/public/og.png
Normal file
BIN
app/public/og.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
@@ -5,6 +5,9 @@ set -e
|
|||||||
echo "Migrating the database"
|
echo "Migrating the database"
|
||||||
pnpm prisma migrate deploy
|
pnpm prisma migrate deploy
|
||||||
|
|
||||||
|
echo "Migrating promptConstructors"
|
||||||
|
pnpm tsx src/promptConstructor/migrate.ts
|
||||||
|
|
||||||
echo "Starting the server"
|
echo "Starting the server"
|
||||||
|
|
||||||
pnpm concurrently --kill-others \
|
pnpm concurrently --kill-others \
|
||||||
|
|||||||
33
app/sentry.client.config.ts
Normal file
33
app/sentry.client.config.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// This file configures the initialization of Sentry on the client.
|
||||||
|
// The config you add here will be used whenever a users loads a page in their browser.
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
import { env } from "~/env.mjs";
|
||||||
|
|
||||||
|
if (env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
|
||||||
|
// Adjust this value in production, or use tracesSampler for greater control
|
||||||
|
tracesSampleRate: 1,
|
||||||
|
|
||||||
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
|
debug: false,
|
||||||
|
|
||||||
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
|
||||||
|
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||||
|
// in development and sample at a lower rate in production
|
||||||
|
replaysSessionSampleRate: 0.1,
|
||||||
|
|
||||||
|
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
|
||||||
|
integrations: [
|
||||||
|
new Sentry.Replay({
|
||||||
|
// Additional Replay configuration goes in here, for example:
|
||||||
|
maskAllText: true,
|
||||||
|
blockAllMedia: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
19
app/sentry.edge.config.ts
Normal file
19
app/sentry.edge.config.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
|
||||||
|
// The config you add here will be used whenever one of the edge features is loaded.
|
||||||
|
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
import { env } from "~/env.mjs";
|
||||||
|
|
||||||
|
if (env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
|
||||||
|
// Adjust this value in production, or use tracesSampler for greater control
|
||||||
|
tracesSampleRate: 1,
|
||||||
|
|
||||||
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
|
debug: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
18
app/sentry.server.config.ts
Normal file
18
app/sentry.server.config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// This file configures the initialization of Sentry on the server.
|
||||||
|
// The config you add here will be used whenever the server handles a request.
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
import { env } from "~/env.mjs";
|
||||||
|
|
||||||
|
if (env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||||
|
Sentry.init({
|
||||||
|
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
|
||||||
|
// Adjust this value in production, or use tracesSampler for greater control
|
||||||
|
tracesSampleRate: 1,
|
||||||
|
|
||||||
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
|
debug: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -68,7 +68,7 @@ export const ChangeModelModal = ({
|
|||||||
return;
|
return;
|
||||||
await replaceVariantMutation.mutateAsync({
|
await replaceVariantMutation.mutateAsync({
|
||||||
id: variant.id,
|
id: variant.id,
|
||||||
constructFn: modifiedPromptFn,
|
promptConstructor: modifiedPromptFn,
|
||||||
streamScenarios: visibleScenarios,
|
streamScenarios: visibleScenarios,
|
||||||
});
|
});
|
||||||
await utils.promptVariants.list.invalidate();
|
await utils.promptVariants.list.invalidate();
|
||||||
@@ -107,7 +107,7 @@ export const ChangeModelModal = ({
|
|||||||
<ModelSearch selectedModel={selectedModel} setSelectedModel={setSelectedModel} />
|
<ModelSearch selectedModel={selectedModel} setSelectedModel={setSelectedModel} />
|
||||||
{isString(modifiedPromptFn) && (
|
{isString(modifiedPromptFn) && (
|
||||||
<CompareFunctions
|
<CompareFunctions
|
||||||
originalFunction={variant.constructFn}
|
originalFunction={variant.promptConstructor}
|
||||||
newFunction={modifiedPromptFn}
|
newFunction={modifiedPromptFn}
|
||||||
leftTitle={originalLabel}
|
leftTitle={originalLabel}
|
||||||
rightTitle={convertedLabel}
|
rightTitle={convertedLabel}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export const ModelSearch = (props: {
|
|||||||
const [containerRef, containerDimensions] = useElementDimensions();
|
const [containerRef, containerDimensions] = useElementDimensions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack ref={containerRef as LegacyRef<HTMLDivElement>} w="full">
|
<VStack ref={containerRef as LegacyRef<HTMLDivElement>} w="full" fontFamily="inconsolata">
|
||||||
<Text>Browse Models</Text>
|
<Text fontWeight="bold">Browse Models</Text>
|
||||||
<Select<ProviderModel>
|
<Select<ProviderModel>
|
||||||
styles={{ control: (provided) => ({ ...provided, width: containerDimensions?.width }) }}
|
styles={{ control: (provided) => ({ ...provided, width: containerDimensions?.width }) }}
|
||||||
getOptionLabel={(data) => modelLabel(data.provider, data.model)}
|
getOptionLabel={(data) => modelLabel(data.provider, data.model)}
|
||||||
|
|||||||
@@ -23,16 +23,24 @@ export const ModelStatsCard = ({
|
|||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<VStack w="full" spacing={6} bgColor="gray.100" p={4} borderRadius={4}>
|
<VStack
|
||||||
|
w="full"
|
||||||
|
spacing={6}
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor="gray.300"
|
||||||
|
p={4}
|
||||||
|
borderRadius={8}
|
||||||
|
fontFamily="inconsolata"
|
||||||
|
>
|
||||||
<HStack w="full" align="flex-start">
|
<HStack w="full" align="flex-start">
|
||||||
<Text flex={1} fontSize="lg">
|
<VStack flex={1} fontSize="lg" alignItems="flex-start">
|
||||||
<Text as="span" color="gray.600">
|
|
||||||
{model.provider} /{" "}
|
|
||||||
</Text>
|
|
||||||
<Text as="span" fontWeight="bold" color="gray.900">
|
<Text as="span" fontWeight="bold" color="gray.900">
|
||||||
{model.name}
|
{model.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text as="span" color="gray.600" fontSize="sm">
|
||||||
|
Provider: {model.provider}
|
||||||
</Text>
|
</Text>
|
||||||
|
</VStack>
|
||||||
<Link
|
<Link
|
||||||
href={model.learnMoreUrl}
|
href={model.learnMoreUrl}
|
||||||
isExternal
|
isExternal
|
||||||
|
|||||||
40
app/src/components/CopiableCode.tsx
Normal file
40
app/src/components/CopiableCode.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { HStack, Icon, IconButton, Tooltip, Text } from "@chakra-ui/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { MdContentCopy } from "react-icons/md";
|
||||||
|
import { useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
|
||||||
|
const CopiableCode = ({ code }: { code: string }) => {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const [copyToClipboard] = useHandledAsyncCallback(async () => {
|
||||||
|
await navigator.clipboard.writeText(code);
|
||||||
|
setCopied(true);
|
||||||
|
}, [code]);
|
||||||
|
return (
|
||||||
|
<HStack
|
||||||
|
backgroundColor="blackAlpha.800"
|
||||||
|
color="white"
|
||||||
|
borderRadius={4}
|
||||||
|
padding={3}
|
||||||
|
w="full"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Text fontFamily="inconsolata" fontWeight="bold" letterSpacing={0.5}>
|
||||||
|
{code}
|
||||||
|
</Text>
|
||||||
|
<Tooltip closeOnClick={false} label={copied ? "Copied!" : "Copy to clipboard"}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Copy"
|
||||||
|
icon={<Icon as={MdContentCopy} boxSize={5} />}
|
||||||
|
size="xs"
|
||||||
|
colorScheme="white"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
onMouseLeave={() => setCopied(false)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CopiableCode;
|
||||||
@@ -1,18 +1,29 @@
|
|||||||
import { Button, Spinner, InputGroup, InputRightElement, Icon, HStack } from "@chakra-ui/react";
|
import {
|
||||||
|
Button,
|
||||||
|
Spinner,
|
||||||
|
InputGroup,
|
||||||
|
InputRightElement,
|
||||||
|
Icon,
|
||||||
|
HStack,
|
||||||
|
type InputGroupProps,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
import { IoMdSend } from "react-icons/io";
|
import { IoMdSend } from "react-icons/io";
|
||||||
import AutoResizeTextArea from "../AutoResizeTextArea";
|
import AutoResizeTextArea from "./AutoResizeTextArea";
|
||||||
|
|
||||||
export const CustomInstructionsInput = ({
|
export const CustomInstructionsInput = ({
|
||||||
instructions,
|
instructions,
|
||||||
setInstructions,
|
setInstructions,
|
||||||
loading,
|
loading,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
placeholder = "Send custom instructions",
|
||||||
|
...props
|
||||||
}: {
|
}: {
|
||||||
instructions: string;
|
instructions: string;
|
||||||
setInstructions: (instructions: string) => void;
|
setInstructions: (instructions: string) => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
}) => {
|
placeholder?: string;
|
||||||
|
} & InputGroupProps) => {
|
||||||
return (
|
return (
|
||||||
<InputGroup
|
<InputGroup
|
||||||
size="md"
|
size="md"
|
||||||
@@ -22,6 +33,7 @@ export const CustomInstructionsInput = ({
|
|||||||
borderRadius={8}
|
borderRadius={8}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
colorScheme="orange"
|
colorScheme="orange"
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<AutoResizeTextArea
|
<AutoResizeTextArea
|
||||||
value={instructions}
|
value={instructions}
|
||||||
@@ -33,7 +45,7 @@ export const CustomInstructionsInput = ({
|
|||||||
onSubmit();
|
onSubmit();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="Send custom instructions"
|
placeholder={placeholder}
|
||||||
py={4}
|
py={4}
|
||||||
pl={4}
|
pl={4}
|
||||||
pr={12}
|
pr={12}
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { BsTrash } from "react-icons/bs";
|
import { BsTrash } from "react-icons/bs";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
|
||||||
@@ -23,6 +24,8 @@ export const DeleteButton = () => {
|
|||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const closeDrawer = useAppStore((s) => s.closeDrawer);
|
||||||
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
@@ -31,6 +34,8 @@ export const DeleteButton = () => {
|
|||||||
await mutation.mutateAsync({ id: experiment.data.id });
|
await mutation.mutateAsync({ id: experiment.data.id });
|
||||||
await utils.experiments.list.invalidate();
|
await utils.experiments.list.invalidate();
|
||||||
await router.push({ pathname: "/experiments" });
|
await router.push({ pathname: "/experiments" });
|
||||||
|
closeDrawer();
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
}, [mutation, experiment.data?.id, router]);
|
}, [mutation, experiment.data?.id, router]);
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export const FloatingLabelInput = ({
|
|||||||
borderColor={isFocused ? "blue.500" : "gray.400"}
|
borderColor={isFocused ? "blue.500" : "gray.400"}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
value={value}
|
value={value}
|
||||||
maxHeight={32}
|
|
||||||
overflowY="auto"
|
overflowY="auto"
|
||||||
overflowX="hidden"
|
overflowX="hidden"
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { type StackProps, VStack } from "@chakra-ui/react";
|
|
||||||
import { CellOptions } from "./CellOptions";
|
|
||||||
|
|
||||||
export const CellContent = ({
|
|
||||||
hardRefetch,
|
|
||||||
hardRefetching,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
hardRefetch: () => void;
|
|
||||||
hardRefetching: boolean;
|
|
||||||
} & StackProps) => (
|
|
||||||
<VStack w="full" alignItems="flex-start" {...props}>
|
|
||||||
<CellOptions refetchingOutput={hardRefetching} refetchOutput={hardRefetch} />
|
|
||||||
<VStack w="full" alignItems="flex-start" maxH={500} overflowY="auto">
|
|
||||||
{children}
|
|
||||||
</VStack>
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { Button, HStack, Icon, Spinner, Tooltip } from "@chakra-ui/react";
|
|
||||||
import { BsArrowClockwise } from "react-icons/bs";
|
|
||||||
import { useExperimentAccess } from "~/utils/hooks";
|
|
||||||
|
|
||||||
export const CellOptions = ({
|
|
||||||
refetchingOutput,
|
|
||||||
refetchOutput,
|
|
||||||
}: {
|
|
||||||
refetchingOutput: boolean;
|
|
||||||
refetchOutput: () => void;
|
|
||||||
}) => {
|
|
||||||
const { canModify } = useExperimentAccess();
|
|
||||||
return (
|
|
||||||
<HStack justifyContent="flex-end" w="full">
|
|
||||||
{canModify && (
|
|
||||||
<Tooltip label="Refetch output" aria-label="refetch output">
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
w={4}
|
|
||||||
h={4}
|
|
||||||
py={4}
|
|
||||||
px={4}
|
|
||||||
minW={0}
|
|
||||||
borderRadius={8}
|
|
||||||
color="gray.500"
|
|
||||||
variant="ghost"
|
|
||||||
cursor="pointer"
|
|
||||||
onClick={refetchOutput}
|
|
||||||
aria-label="refetch output"
|
|
||||||
>
|
|
||||||
<Icon as={refetchingOutput ? Spinner : BsArrowClockwise} boxSize={4} />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
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 { Text, VStack } from "@chakra-ui/react";
|
import { type StackProps, Text, VStack } from "@chakra-ui/react";
|
||||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useExperiment, 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";
|
||||||
import { type ReactElement, useState, useEffect, Fragment } from "react";
|
import { type ReactElement, useState, useEffect, Fragment, useCallback } from "react";
|
||||||
import useSocket from "~/utils/useSocket";
|
import useSocket from "~/utils/useSocket";
|
||||||
import { OutputStats } from "./OutputStats";
|
import { OutputStats } from "./OutputStats";
|
||||||
import { RetryCountdown } from "./RetryCountdown";
|
import { RetryCountdown } from "./RetryCountdown";
|
||||||
import frontendModelProviders from "~/modelProviders/frontendModelProviders";
|
import frontendModelProviders from "~/modelProviders/frontendModelProviders";
|
||||||
import { ResponseLog } from "./ResponseLog";
|
import { ResponseLog } from "./ResponseLog";
|
||||||
import { CellContent } from "./CellContent";
|
import { CellOptions } from "./TopActions";
|
||||||
|
|
||||||
const WAITING_MESSAGE_INTERVAL = 20000;
|
const WAITING_MESSAGE_INTERVAL = 20000;
|
||||||
|
|
||||||
@@ -72,37 +72,49 @@ export default function OutputCell({
|
|||||||
// 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);
|
||||||
|
|
||||||
|
const mostRecentResponse = cell?.modelResponses[cell.modelResponses.length - 1];
|
||||||
|
|
||||||
|
const CellWrapper = useCallback(
|
||||||
|
({ children, ...props }: StackProps) => (
|
||||||
|
<VStack w="full" alignItems="flex-start" {...props} px={2} py={2} h="100%">
|
||||||
|
{cell && (
|
||||||
|
<CellOptions refetchingOutput={hardRefetching} refetchOutput={hardRefetch} cell={cell} />
|
||||||
|
)}
|
||||||
|
<VStack w="full" alignItems="flex-start" maxH={500} overflowY="auto" flex={1}>
|
||||||
|
{children}
|
||||||
|
</VStack>
|
||||||
|
{mostRecentResponse && (
|
||||||
|
<OutputStats modelResponse={mostRecentResponse} scenario={scenario} />
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
),
|
||||||
|
[hardRefetching, hardRefetch, mostRecentResponse, scenario, cell],
|
||||||
|
);
|
||||||
|
|
||||||
if (!vars) return null;
|
if (!vars) return null;
|
||||||
|
|
||||||
if (!cell && !fetchingOutput)
|
if (!cell && !fetchingOutput)
|
||||||
return (
|
return (
|
||||||
<CellContent hardRefetching={hardRefetching} hardRefetch={hardRefetch}>
|
<CellWrapper>
|
||||||
<Text color="gray.500">Error retrieving output</Text>
|
<Text color="gray.500">Error retrieving output</Text>
|
||||||
</CellContent>
|
</CellWrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cell && cell.errorMessage) {
|
if (cell && cell.errorMessage) {
|
||||||
return (
|
return (
|
||||||
<CellContent hardRefetching={hardRefetching} hardRefetch={hardRefetch}>
|
<CellWrapper>
|
||||||
<Text color="red.500">{cell.errorMessage}</Text>
|
<Text color="red.500">{cell.errorMessage}</Text>
|
||||||
</CellContent>
|
</CellWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (disabledReason) return <Text color="gray.500">{disabledReason}</Text>;
|
if (disabledReason) return <Text color="gray.500">{disabledReason}</Text>;
|
||||||
|
|
||||||
const mostRecentResponse = cell?.modelResponses[cell.modelResponses.length - 1];
|
|
||||||
const showLogs = !streamedMessage && !mostRecentResponse?.output;
|
const showLogs = !streamedMessage && !mostRecentResponse?.output;
|
||||||
|
|
||||||
if (showLogs)
|
if (showLogs)
|
||||||
return (
|
return (
|
||||||
<CellContent
|
<CellWrapper alignItems="flex-start" fontFamily="inconsolata, monospace" spacing={0}>
|
||||||
hardRefetching={hardRefetching}
|
|
||||||
hardRefetch={hardRefetch}
|
|
||||||
alignItems="flex-start"
|
|
||||||
fontFamily="inconsolata, monospace"
|
|
||||||
spacing={0}
|
|
||||||
>
|
|
||||||
{cell?.jobQueuedAt && <ResponseLog time={cell.jobQueuedAt} title="Job queued" />}
|
{cell?.jobQueuedAt && <ResponseLog time={cell.jobQueuedAt} title="Job queued" />}
|
||||||
{cell?.jobStartedAt && <ResponseLog time={cell.jobStartedAt} title="Job started" />}
|
{cell?.jobStartedAt && <ResponseLog time={cell.jobStartedAt} title="Job started" />}
|
||||||
{cell?.modelResponses?.map((response) => {
|
{cell?.modelResponses?.map((response) => {
|
||||||
@@ -124,9 +136,13 @@ export default function OutputCell({
|
|||||||
Array.from({ length: numWaitingMessages }, (_, i) => (
|
Array.from({ length: numWaitingMessages }, (_, i) => (
|
||||||
<ResponseLog
|
<ResponseLog
|
||||||
key={`waiting-${i}`}
|
key={`waiting-${i}`}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
time={
|
||||||
time={new Date(response.requestedAt!.getTime() + i * WAITING_MESSAGE_INTERVAL)}
|
new Date(
|
||||||
title="Waiting for response"
|
(response.requestedAt?.getTime?.() ?? 0) +
|
||||||
|
(i + 1) * WAITING_MESSAGE_INTERVAL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
title="Waiting for response..."
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{response.receivedAt && (
|
{response.receivedAt && (
|
||||||
@@ -144,7 +160,7 @@ export default function OutputCell({
|
|||||||
{mostRecentResponse?.retryTime && (
|
{mostRecentResponse?.retryTime && (
|
||||||
<RetryCountdown retryTime={mostRecentResponse.retryTime} />
|
<RetryCountdown retryTime={mostRecentResponse.retryTime} />
|
||||||
)}
|
)}
|
||||||
</CellContent>
|
</CellWrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
const normalizedOutput = mostRecentResponse?.output
|
const normalizedOutput = mostRecentResponse?.output
|
||||||
@@ -155,21 +171,7 @@ export default function OutputCell({
|
|||||||
|
|
||||||
if (mostRecentResponse?.output && normalizedOutput?.type === "json") {
|
if (mostRecentResponse?.output && normalizedOutput?.type === "json") {
|
||||||
return (
|
return (
|
||||||
<VStack
|
<CellWrapper>
|
||||||
w="100%"
|
|
||||||
h="100%"
|
|
||||||
fontSize="xs"
|
|
||||||
flexWrap="wrap"
|
|
||||||
overflowX="hidden"
|
|
||||||
justifyContent="space-between"
|
|
||||||
>
|
|
||||||
<CellContent
|
|
||||||
hardRefetching={hardRefetching}
|
|
||||||
hardRefetch={hardRefetch}
|
|
||||||
w="full"
|
|
||||||
flex={1}
|
|
||||||
spacing={0}
|
|
||||||
>
|
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
customStyle={{ overflowX: "unset", width: "100%", flex: 1 }}
|
customStyle={{ overflowX: "unset", width: "100%", flex: 1 }}
|
||||||
language="json"
|
language="json"
|
||||||
@@ -181,24 +183,15 @@ export default function OutputCell({
|
|||||||
>
|
>
|
||||||
{stringify(normalizedOutput.value, { maxLength: 40 })}
|
{stringify(normalizedOutput.value, { maxLength: 40 })}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</CellContent>
|
</CellWrapper>
|
||||||
<OutputStats modelResponse={mostRecentResponse} scenario={scenario} />
|
|
||||||
</VStack>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentToDisplay = (normalizedOutput?.type === "text" && normalizedOutput.value) || "";
|
const contentToDisplay = (normalizedOutput?.type === "text" && normalizedOutput.value) || "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack w="100%" h="100%" justifyContent="space-between" whiteSpace="pre-wrap">
|
<CellWrapper>
|
||||||
<VStack w="full" alignItems="flex-start" spacing={0}>
|
|
||||||
<CellContent hardRefetching={hardRefetching} hardRefetch={hardRefetch}>
|
|
||||||
<Text>{contentToDisplay}</Text>
|
<Text>{contentToDisplay}</Text>
|
||||||
</CellContent>
|
</CellWrapper>
|
||||||
</VStack>
|
|
||||||
{mostRecentResponse?.output && (
|
|
||||||
<OutputStats modelResponse={mostRecentResponse} scenario={scenario} />
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,15 @@ export const OutputStats = ({
|
|||||||
const completionTokens = modelResponse.completionTokens;
|
const completionTokens = modelResponse.completionTokens;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack w="full" align="center" color="gray.500" fontSize="2xs" mt={{ base: 0, md: 1 }}>
|
<HStack
|
||||||
<HStack flex={1}>
|
w="full"
|
||||||
|
align="center"
|
||||||
|
color="gray.500"
|
||||||
|
fontSize="2xs"
|
||||||
|
mt={{ base: 0, md: 1 }}
|
||||||
|
alignItems="flex-end"
|
||||||
|
>
|
||||||
|
<HStack flex={1} flexWrap="wrap">
|
||||||
{modelResponse.outputEvaluations.map((evaluation) => {
|
{modelResponse.outputEvaluations.map((evaluation) => {
|
||||||
const passed = evaluation.result > 0.5;
|
const passed = evaluation.result > 0.5;
|
||||||
return (
|
return (
|
||||||
|
|||||||
36
app/src/components/OutputsTable/OutputCell/PromptModal.tsx
Normal file
36
app/src/components/OutputsTable/OutputCell/PromptModal.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
type UseDisclosureReturn,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { type RouterOutputs } from "~/utils/api";
|
||||||
|
import { JSONTree } from "react-json-tree";
|
||||||
|
|
||||||
|
export default function ExpandedModal(props: {
|
||||||
|
cell: NonNullable<RouterOutputs["scenarioVariantCells"]["get"]>;
|
||||||
|
disclosure: UseDisclosureReturn;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={props.disclosure.isOpen} onClose={props.disclosure.onClose} size="2xl">
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>Prompt</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<JSONTree
|
||||||
|
data={props.cell.prompt}
|
||||||
|
invertTheme={true}
|
||||||
|
theme="chalk"
|
||||||
|
shouldExpandNodeInitially={() => true}
|
||||||
|
getItemString={() => ""}
|
||||||
|
hideRoot
|
||||||
|
/>
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
app/src/components/OutputsTable/OutputCell/TopActions.tsx
Normal file
53
app/src/components/OutputsTable/OutputCell/TopActions.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { HStack, Icon, IconButton, Spinner, Tooltip, useDisclosure } from "@chakra-ui/react";
|
||||||
|
import { BsArrowClockwise, BsInfoCircle } from "react-icons/bs";
|
||||||
|
import { useExperimentAccess } from "~/utils/hooks";
|
||||||
|
import ExpandedModal from "./PromptModal";
|
||||||
|
import { type RouterOutputs } from "~/utils/api";
|
||||||
|
|
||||||
|
export const CellOptions = ({
|
||||||
|
cell,
|
||||||
|
refetchingOutput,
|
||||||
|
refetchOutput,
|
||||||
|
}: {
|
||||||
|
cell: RouterOutputs["scenarioVariantCells"]["get"];
|
||||||
|
refetchingOutput: boolean;
|
||||||
|
refetchOutput: () => void;
|
||||||
|
}) => {
|
||||||
|
const { canModify } = useExperimentAccess();
|
||||||
|
|
||||||
|
const modalDisclosure = useDisclosure();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack justifyContent="flex-end" w="full" spacing={1}>
|
||||||
|
{cell && (
|
||||||
|
<>
|
||||||
|
<Tooltip label="See Prompt">
|
||||||
|
<IconButton
|
||||||
|
aria-label="See Prompt"
|
||||||
|
icon={<Icon as={BsInfoCircle} boxSize={3.5} />}
|
||||||
|
onClick={modalDisclosure.onOpen}
|
||||||
|
size="xs"
|
||||||
|
colorScheme="gray"
|
||||||
|
color="gray.500"
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<ExpandedModal cell={cell} disclosure={modalDisclosure} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{canModify && (
|
||||||
|
<Tooltip label="Refetch output">
|
||||||
|
<IconButton
|
||||||
|
size="xs"
|
||||||
|
color="gray.500"
|
||||||
|
variant="ghost"
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={refetchOutput}
|
||||||
|
aria-label="refetch output"
|
||||||
|
icon={<Icon as={refetchingOutput ? Spinner : BsArrowClockwise} boxSize={4} />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,15 +1,24 @@
|
|||||||
import { type DragEvent } from "react";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { isEqual } from "lodash-es";
|
import { isEqual } from "lodash-es";
|
||||||
import { type Scenario } from "./types";
|
import { useEffect, useState, type DragEvent } from "react";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
import { useExperiment, useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useExperiment, useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
import { useState } from "react";
|
import { type Scenario } from "./types";
|
||||||
|
|
||||||
import { Box, Button, Flex, HStack, Icon, Spinner, Stack, Tooltip, VStack } from "@chakra-ui/react";
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
HStack,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Spinner,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
VStack,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { BsArrowsAngleExpand, BsX } from "react-icons/bs";
|
||||||
import { cellPadding } from "../constants";
|
import { cellPadding } from "../constants";
|
||||||
import { BsX } from "react-icons/bs";
|
|
||||||
import { RiDraggable } from "react-icons/ri";
|
|
||||||
import { FloatingLabelInput } from "./FloatingLabelInput";
|
import { FloatingLabelInput } from "./FloatingLabelInput";
|
||||||
|
import { ScenarioEditorModal } from "./ScenarioEditorModal";
|
||||||
|
|
||||||
export default function ScenarioEditor({
|
export default function ScenarioEditor({
|
||||||
scenario,
|
scenario,
|
||||||
@@ -28,6 +37,10 @@ export default function ScenarioEditor({
|
|||||||
|
|
||||||
const [values, setValues] = useState<Record<string, string>>(savedValues);
|
const [values, setValues] = useState<Record<string, string>>(savedValues);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (savedValues) setValues(savedValues);
|
||||||
|
}, [savedValues]);
|
||||||
|
|
||||||
const experiment = useExperiment();
|
const experiment = useExperiment();
|
||||||
const vars = api.templateVars.list.useQuery({ experimentId: experiment.data?.id ?? "" });
|
const vars = api.templateVars.list.useQuery({ experimentId: experiment.data?.id ?? "" });
|
||||||
|
|
||||||
@@ -71,7 +84,10 @@ export default function ScenarioEditor({
|
|||||||
[reorderMutation, scenario.id],
|
[reorderMutation, scenario.id],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [scenarioEditorModalOpen, setScenarioEditorModalOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<HStack
|
<HStack
|
||||||
alignItems="flex-start"
|
alignItems="flex-start"
|
||||||
px={cellPadding.x}
|
px={cellPadding.x}
|
||||||
@@ -96,58 +112,54 @@ export default function ScenarioEditor({
|
|||||||
onDrop={onReorder}
|
onDrop={onReorder}
|
||||||
backgroundColor={isDragTarget ? "gray.100" : "transparent"}
|
backgroundColor={isDragTarget ? "gray.100" : "transparent"}
|
||||||
>
|
>
|
||||||
{canModify && props.canHide && (
|
|
||||||
<Stack
|
|
||||||
alignSelf="flex-start"
|
|
||||||
opacity={props.hovered ? 1 : 0}
|
|
||||||
spacing={0}
|
|
||||||
ml={-cellPadding.x}
|
|
||||||
>
|
|
||||||
<Tooltip label="Hide scenario" hasArrow>
|
|
||||||
{/* for some reason the tooltip can't position itself properly relative to the icon without the wrapping box */}
|
|
||||||
<Button
|
|
||||||
variant="unstyled"
|
|
||||||
color="gray.400"
|
|
||||||
height="unset"
|
|
||||||
width="unset"
|
|
||||||
minW="unset"
|
|
||||||
onClick={onHide}
|
|
||||||
_hover={{
|
|
||||||
color: "gray.800",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon as={hidingInProgress ? Spinner : BsX} boxSize={hidingInProgress ? 4 : 6} />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Icon
|
|
||||||
as={RiDraggable}
|
|
||||||
boxSize={6}
|
|
||||||
color="gray.400"
|
|
||||||
_hover={{ color: "gray.800", cursor: "pointer" }}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{variableLabels.length === 0 ? (
|
{variableLabels.length === 0 ? (
|
||||||
<Box color="gray.500">{vars.data ? "No scenario variables configured" : "Loading..."}</Box>
|
<Box color="gray.500">
|
||||||
|
{vars.data ? "No scenario variables configured" : "Loading..."}
|
||||||
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<VStack spacing={4} flex={1} py={2}>
|
<VStack spacing={4} flex={1} py={2}>
|
||||||
|
<HStack justifyContent="space-between" w="100%" align="center" spacing={0}>
|
||||||
|
<Text flex={1}>Scenario</Text>
|
||||||
|
<Tooltip label="Expand" hasArrow>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Expand"
|
||||||
|
icon={<Icon as={BsArrowsAngleExpand} boxSize={3} />}
|
||||||
|
onClick={() => setScenarioEditorModalOpen(true)}
|
||||||
|
size="xs"
|
||||||
|
colorScheme="gray"
|
||||||
|
color="gray.500"
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
{canModify && props.canHide && (
|
||||||
|
<Tooltip label="Delete" hasArrow>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Delete"
|
||||||
|
icon={
|
||||||
|
<Icon
|
||||||
|
as={hidingInProgress ? Spinner : BsX}
|
||||||
|
boxSize={hidingInProgress ? 4 : 6}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={onHide}
|
||||||
|
size="xs"
|
||||||
|
display="flex"
|
||||||
|
colorScheme="gray"
|
||||||
|
color="gray.500"
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
{variableLabels.map((key) => {
|
{variableLabels.map((key) => {
|
||||||
const value = values[key] ?? "";
|
const value = values[key] ?? "";
|
||||||
const layoutDirection = value.length > 20 ? "column" : "row";
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
|
||||||
key={key}
|
|
||||||
direction={layoutDirection}
|
|
||||||
alignItems={layoutDirection === "column" ? "flex-start" : "center"}
|
|
||||||
flexWrap="wrap"
|
|
||||||
width="full"
|
|
||||||
>
|
|
||||||
<FloatingLabelInput
|
<FloatingLabelInput
|
||||||
|
key={key}
|
||||||
label={key}
|
label={key}
|
||||||
isDisabled={!canModify}
|
isDisabled={!canModify}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
|
maxHeight={32}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setValues((prev) => ({ ...prev, [key]: e.target.value }));
|
setValues((prev) => ({ ...prev, [key]: e.target.value }));
|
||||||
@@ -162,7 +174,6 @@ export default function ScenarioEditor({
|
|||||||
onMouseEnter={() => setVariableInputHovered(true)}
|
onMouseEnter={() => setVariableInputHovered(true)}
|
||||||
onMouseLeave={() => setVariableInputHovered(false)}
|
onMouseLeave={() => setVariableInputHovered(false)}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{hasChanged && (
|
{hasChanged && (
|
||||||
@@ -184,5 +195,13 @@ export default function ScenarioEditor({
|
|||||||
</VStack>
|
</VStack>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
{scenarioEditorModalOpen && (
|
||||||
|
<ScenarioEditorModal
|
||||||
|
scenarioId={scenario.id}
|
||||||
|
initialValues={savedValues}
|
||||||
|
onClose={() => setScenarioEditorModalOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
123
app/src/components/OutputsTable/ScenarioEditorModal.tsx
Normal file
123
app/src/components/OutputsTable/ScenarioEditorModal.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
HStack,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
Spinner,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { isEqual } from "lodash-es";
|
||||||
|
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import {
|
||||||
|
useScenario,
|
||||||
|
useHandledAsyncCallback,
|
||||||
|
useExperiment,
|
||||||
|
useExperimentAccess,
|
||||||
|
} from "~/utils/hooks";
|
||||||
|
import { FloatingLabelInput } from "./FloatingLabelInput";
|
||||||
|
|
||||||
|
export const ScenarioEditorModal = ({
|
||||||
|
scenarioId,
|
||||||
|
initialValues,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
scenarioId: string;
|
||||||
|
initialValues: Record<string, string>;
|
||||||
|
onClose: () => void;
|
||||||
|
}) => {
|
||||||
|
const utils = api.useContext();
|
||||||
|
const experiment = useExperiment();
|
||||||
|
const { canModify } = useExperimentAccess();
|
||||||
|
const scenario = useScenario(scenarioId);
|
||||||
|
|
||||||
|
const savedValues = scenario.data?.variableValues as Record<string, string>;
|
||||||
|
|
||||||
|
const [values, setValues] = useState<Record<string, string>>(initialValues);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (savedValues) setValues(savedValues);
|
||||||
|
}, [savedValues]);
|
||||||
|
|
||||||
|
const hasChanged = !isEqual(savedValues, values);
|
||||||
|
|
||||||
|
const mutation = api.scenarios.replaceWithValues.useMutation();
|
||||||
|
|
||||||
|
const [onSave, saving] = useHandledAsyncCallback(async () => {
|
||||||
|
await mutation.mutateAsync({
|
||||||
|
id: scenarioId,
|
||||||
|
values,
|
||||||
|
});
|
||||||
|
await utils.scenarios.list.invalidate();
|
||||||
|
}, [mutation, values]);
|
||||||
|
|
||||||
|
const vars = api.templateVars.list.useQuery({ experimentId: experiment.data?.id ?? "" });
|
||||||
|
const variableLabels = vars.data?.map((v) => v.label) ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen
|
||||||
|
onClose={onClose}
|
||||||
|
size={{ base: "xl", sm: "2xl", md: "3xl", lg: "5xl", xl: "7xl" }}
|
||||||
|
>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent w={1200}>
|
||||||
|
<ModalHeader />
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody maxW="unset">
|
||||||
|
<VStack spacing={8}>
|
||||||
|
{values &&
|
||||||
|
variableLabels.map((key) => {
|
||||||
|
const value = values[key] ?? "";
|
||||||
|
return (
|
||||||
|
<FloatingLabelInput
|
||||||
|
key={key}
|
||||||
|
label={key}
|
||||||
|
isDisabled={!canModify}
|
||||||
|
_disabled={{ opacity: 1 }}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
setValues((prev) => ({ ...prev, [key]: e.target.value }));
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.currentTarget.blur();
|
||||||
|
onSave();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</VStack>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
{canModify && (
|
||||||
|
<HStack>
|
||||||
|
<Button
|
||||||
|
colorScheme="gray"
|
||||||
|
onClick={() => setValues(savedValues)}
|
||||||
|
minW={24}
|
||||||
|
isDisabled={!hasChanged}
|
||||||
|
>
|
||||||
|
<Text>Reset</Text>
|
||||||
|
</Button>
|
||||||
|
<Button colorScheme="blue" onClick={onSave} minW={24} isDisabled={!hasChanged}>
|
||||||
|
{saving ? <Spinner boxSize={4} /> : <Text>Save</Text>}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,73 +1,20 @@
|
|||||||
import { Box, HStack, IconButton } from "@chakra-ui/react";
|
import { useScenarios } from "~/utils/hooks";
|
||||||
import {
|
import Paginator from "../Paginator";
|
||||||
BsChevronDoubleLeft,
|
|
||||||
BsChevronDoubleRight,
|
|
||||||
BsChevronLeft,
|
|
||||||
BsChevronRight,
|
|
||||||
} from "react-icons/bs";
|
|
||||||
import { usePage, useScenarios } from "~/utils/hooks";
|
|
||||||
|
|
||||||
const ScenarioPaginator = () => {
|
const ScenarioPaginator = () => {
|
||||||
const [page, setPage] = usePage();
|
|
||||||
const { data } = useScenarios();
|
const { data } = useScenarios();
|
||||||
|
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
const { scenarios, startIndex, lastPage, count } = data;
|
const { scenarios, startIndex, lastPage, count } = data;
|
||||||
|
|
||||||
const nextPage = () => {
|
|
||||||
if (page < lastPage) {
|
|
||||||
setPage(page + 1, "replace");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const prevPage = () => {
|
|
||||||
if (page > 1) {
|
|
||||||
setPage(page - 1, "replace");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const goToLastPage = () => setPage(lastPage, "replace");
|
|
||||||
const goToFirstPage = () => setPage(1, "replace");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack pt={4}>
|
<Paginator
|
||||||
<IconButton
|
numItemsLoaded={scenarios.length}
|
||||||
variant="ghost"
|
startIndex={startIndex}
|
||||||
size="sm"
|
lastPage={lastPage}
|
||||||
onClick={goToFirstPage}
|
count={count}
|
||||||
isDisabled={page === 1}
|
|
||||||
aria-label="Go to first page"
|
|
||||||
icon={<BsChevronDoubleLeft />}
|
|
||||||
/>
|
/>
|
||||||
<IconButton
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={prevPage}
|
|
||||||
isDisabled={page === 1}
|
|
||||||
aria-label="Previous page"
|
|
||||||
icon={<BsChevronLeft />}
|
|
||||||
/>
|
|
||||||
<Box>
|
|
||||||
{startIndex}-{startIndex + scenarios.length - 1} / {count}
|
|
||||||
</Box>
|
|
||||||
<IconButton
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={nextPage}
|
|
||||||
isDisabled={page === lastPage}
|
|
||||||
aria-label="Next page"
|
|
||||||
icon={<BsChevronRight />}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={goToLastPage}
|
|
||||||
isDisabled={page === lastPage}
|
|
||||||
aria-label="Go to last page"
|
|
||||||
icon={<BsChevronDoubleRight />}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Box, GridItem } from "@chakra-ui/react";
|
import { GridItem } from "@chakra-ui/react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { cellPadding } from "../constants";
|
|
||||||
import OutputCell from "./OutputCell/OutputCell";
|
import OutputCell from "./OutputCell/OutputCell";
|
||||||
import ScenarioEditor from "./ScenarioEditor";
|
import ScenarioEditor from "./ScenarioEditor";
|
||||||
import type { PromptVariant, Scenario } from "./types";
|
import type { PromptVariant, Scenario } from "./types";
|
||||||
@@ -39,9 +38,7 @@ const ScenarioRow = (props: {
|
|||||||
colStart={i + 2}
|
colStart={i + 2}
|
||||||
{...borders}
|
{...borders}
|
||||||
>
|
>
|
||||||
<Box h="100%" w="100%" px={cellPadding.x} py={cellPadding.y}>
|
|
||||||
<OutputCell key={variant.id} scenario={props.scenario} variant={variant} />
|
<OutputCell key={variant.id} scenario={props.scenario} variant={variant} />
|
||||||
</Box>
|
|
||||||
</GridItem>
|
</GridItem>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
return () => window.removeEventListener("keydown", handleEsc);
|
return () => window.removeEventListener("keydown", handleEsc);
|
||||||
}, [isFullscreen, toggleFullscreen]);
|
}, [isFullscreen, toggleFullscreen]);
|
||||||
|
|
||||||
const lastSavedFn = props.variant.constructFn;
|
const lastSavedFn = props.variant.promptConstructor;
|
||||||
|
|
||||||
const modifierKey = useModifierKeyLabel();
|
const modifierKey = useModifierKeyLabel();
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
|
|
||||||
const resp = await replaceVariant.mutateAsync({
|
const resp = await replaceVariant.mutateAsync({
|
||||||
id: props.variant.id,
|
id: props.variant.id,
|
||||||
constructFn: currentFn,
|
promptConstructor: currentFn,
|
||||||
streamScenarios: visibleScenarios,
|
streamScenarios: visibleScenarios,
|
||||||
});
|
});
|
||||||
if (resp.status === "error") {
|
if (resp.status === "error") {
|
||||||
|
|||||||
@@ -43,17 +43,17 @@ export default function VariantStats(props: { variant: PromptVariant }) {
|
|||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="flex-end"
|
||||||
mx="2"
|
mx="2"
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
py={cellPadding.y}
|
py={cellPadding.y}
|
||||||
>
|
>
|
||||||
|
<HStack px={cellPadding.x} flexWrap="wrap">
|
||||||
{showNumFinished && (
|
{showNumFinished && (
|
||||||
<Text>
|
<Text>
|
||||||
{data.outputCount} / {data.scenarioCount}
|
{data.outputCount} / {data.scenarioCount}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<HStack px={cellPadding.x}>
|
|
||||||
{data.evalResults.map((result) => {
|
{data.evalResults.map((result) => {
|
||||||
const passedFrac = result.passCount / result.totalCount;
|
const passedFrac = result.passCount / result.totalCount;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
|||||||
pb={24}
|
pb={24}
|
||||||
pl={8}
|
pl={8}
|
||||||
display="grid"
|
display="grid"
|
||||||
gridTemplateColumns={`250px repeat(${variants.data.length}, minmax(300px, 1fr)) auto`}
|
gridTemplateColumns={`250px repeat(${variants.data.length}, minmax(320px, 1fr)) auto`}
|
||||||
sx={{
|
sx={{
|
||||||
"> *": {
|
"> *": {
|
||||||
borderColor: "gray.300",
|
borderColor: "gray.300",
|
||||||
|
|||||||
79
app/src/components/Paginator.tsx
Normal file
79
app/src/components/Paginator.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { Box, HStack, IconButton } from "@chakra-ui/react";
|
||||||
|
import {
|
||||||
|
BsChevronDoubleLeft,
|
||||||
|
BsChevronDoubleRight,
|
||||||
|
BsChevronLeft,
|
||||||
|
BsChevronRight,
|
||||||
|
} from "react-icons/bs";
|
||||||
|
import { usePage } from "~/utils/hooks";
|
||||||
|
|
||||||
|
const Paginator = ({
|
||||||
|
numItemsLoaded,
|
||||||
|
startIndex,
|
||||||
|
lastPage,
|
||||||
|
count,
|
||||||
|
}: {
|
||||||
|
numItemsLoaded: number;
|
||||||
|
startIndex: number;
|
||||||
|
lastPage: number;
|
||||||
|
count: number;
|
||||||
|
}) => {
|
||||||
|
const [page, setPage] = usePage();
|
||||||
|
|
||||||
|
const nextPage = () => {
|
||||||
|
if (page < lastPage) {
|
||||||
|
setPage(page + 1, "replace");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevPage = () => {
|
||||||
|
if (page > 1) {
|
||||||
|
setPage(page - 1, "replace");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToLastPage = () => setPage(lastPage, "replace");
|
||||||
|
const goToFirstPage = () => setPage(1, "replace");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack pt={4}>
|
||||||
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={goToFirstPage}
|
||||||
|
isDisabled={page === 1}
|
||||||
|
aria-label="Go to first page"
|
||||||
|
icon={<BsChevronDoubleLeft />}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={prevPage}
|
||||||
|
isDisabled={page === 1}
|
||||||
|
aria-label="Previous page"
|
||||||
|
icon={<BsChevronLeft />}
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
{startIndex}-{startIndex + numItemsLoaded - 1} / {count}
|
||||||
|
</Box>
|
||||||
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={nextPage}
|
||||||
|
isDisabled={page === lastPage}
|
||||||
|
aria-label="Next page"
|
||||||
|
icon={<BsChevronRight />}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={goToLastPage}
|
||||||
|
isDisabled={page === lastPage}
|
||||||
|
aria-label="Go to last page"
|
||||||
|
icon={<BsChevronDoubleRight />}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Paginator;
|
||||||
@@ -20,7 +20,7 @@ import { useHandledAsyncCallback, useVisibleScenarioIds } from "~/utils/hooks";
|
|||||||
import { type PromptVariant } from "@prisma/client";
|
import { type PromptVariant } from "@prisma/client";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import CompareFunctions from "./CompareFunctions";
|
import CompareFunctions from "./CompareFunctions";
|
||||||
import { CustomInstructionsInput } from "./CustomInstructionsInput";
|
import { CustomInstructionsInput } from "../CustomInstructionsInput";
|
||||||
import { RefineAction } from "./RefineAction";
|
import { RefineAction } from "./RefineAction";
|
||||||
import { isObject, isString } from "lodash-es";
|
import { isObject, isString } from "lodash-es";
|
||||||
import { type RefinementAction, type SupportedProvider } from "~/modelProviders/types";
|
import { type RefinementAction, type SupportedProvider } from "~/modelProviders/types";
|
||||||
@@ -73,7 +73,7 @@ export const RefinePromptModal = ({
|
|||||||
return;
|
return;
|
||||||
await replaceVariantMutation.mutateAsync({
|
await replaceVariantMutation.mutateAsync({
|
||||||
id: variant.id,
|
id: variant.id,
|
||||||
constructFn: refinedPromptFn,
|
promptConstructor: refinedPromptFn,
|
||||||
streamScenarios: visibleScenarios,
|
streamScenarios: visibleScenarios,
|
||||||
});
|
});
|
||||||
await utils.promptVariants.list.invalidate();
|
await utils.promptVariants.list.invalidate();
|
||||||
@@ -97,7 +97,7 @@ export const RefinePromptModal = ({
|
|||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody maxW="unset">
|
<ModalBody maxW="unset">
|
||||||
<VStack spacing={8}>
|
<VStack spacing={8}>
|
||||||
<VStack spacing={4}>
|
<VStack spacing={4} w="full">
|
||||||
{Object.keys(refinementActions).length && (
|
{Object.keys(refinementActions).length && (
|
||||||
<>
|
<>
|
||||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={8}>
|
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={8}>
|
||||||
@@ -122,11 +122,11 @@ export const RefinePromptModal = ({
|
|||||||
instructions={instructions}
|
instructions={instructions}
|
||||||
setInstructions={setInstructions}
|
setInstructions={setInstructions}
|
||||||
loading={modificationInProgress}
|
loading={modificationInProgress}
|
||||||
onSubmit={getModifiedPromptFn}
|
onSubmit={() => getModifiedPromptFn()}
|
||||||
/>
|
/>
|
||||||
</VStack>
|
</VStack>
|
||||||
<CompareFunctions
|
<CompareFunctions
|
||||||
originalFunction={variant.constructFn}
|
originalFunction={variant.promptConstructor}
|
||||||
newFunction={isString(refinedPromptFn) ? refinedPromptFn : undefined}
|
newFunction={isString(refinedPromptFn) ? refinedPromptFn : undefined}
|
||||||
maxH="40vh"
|
maxH="40vh"
|
||||||
/>
|
/>
|
||||||
|
|||||||
26
app/src/components/StatsCard.tsx
Normal file
26
app/src/components/StatsCard.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { VStack, HStack, type StackProps, Text, Divider } from "@chakra-ui/react";
|
||||||
|
import Link, { type LinkProps } from "next/link";
|
||||||
|
|
||||||
|
const StatsCard = ({
|
||||||
|
title,
|
||||||
|
href,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: { title: string; href: string } & StackProps & LinkProps) => {
|
||||||
|
return (
|
||||||
|
<VStack flex={1} borderWidth={1} padding={4} borderRadius={4} borderColor="gray.300" {...rest}>
|
||||||
|
<HStack w="full" justifyContent="space-between">
|
||||||
|
<Text fontSize="md" fontWeight="bold">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Link href={href}>
|
||||||
|
<Text color="blue">View all</Text>
|
||||||
|
</Link>
|
||||||
|
</HStack>
|
||||||
|
<Divider />
|
||||||
|
{children}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatsCard;
|
||||||
201
app/src/components/dashboard/LoggedCallTable.tsx
Normal file
201
app/src/components/dashboard/LoggedCallTable.tsx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
112
app/src/components/datasets/DatasetCard.tsx
Normal file
112
app/src/components/datasets/DatasetCard.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import {
|
||||||
|
HStack,
|
||||||
|
Icon,
|
||||||
|
VStack,
|
||||||
|
Text,
|
||||||
|
Divider,
|
||||||
|
Spinner,
|
||||||
|
AspectRatio,
|
||||||
|
SkeletonText,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { RiDatabase2Line } from "react-icons/ri";
|
||||||
|
import { formatTimePast } from "~/utils/dayjs";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { BsPlusSquare } from "react-icons/bs";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
|
||||||
|
type DatasetData = {
|
||||||
|
name: string;
|
||||||
|
numEntries: number;
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DatasetCard = ({ dataset }: { dataset: DatasetData }) => {
|
||||||
|
return (
|
||||||
|
<AspectRatio ratio={1.2} w="full">
|
||||||
|
<VStack
|
||||||
|
as={Link}
|
||||||
|
href={{ pathname: "/data/[id]", query: { id: dataset.id } }}
|
||||||
|
bg="gray.50"
|
||||||
|
_hover={{ bg: "gray.100" }}
|
||||||
|
transition="background 0.2s"
|
||||||
|
cursor="pointer"
|
||||||
|
borderColor="gray.200"
|
||||||
|
borderWidth={1}
|
||||||
|
p={4}
|
||||||
|
justify="space-between"
|
||||||
|
>
|
||||||
|
<HStack w="full" color="gray.700" justify="center">
|
||||||
|
<Icon as={RiDatabase2Line} boxSize={4} />
|
||||||
|
<Text fontWeight="bold">{dataset.name}</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack h="full" spacing={4} flex={1} align="center">
|
||||||
|
<CountLabel label="Rows" count={dataset.numEntries} />
|
||||||
|
</HStack>
|
||||||
|
<HStack w="full" color="gray.500" fontSize="xs" textAlign="center">
|
||||||
|
<Text flex={1}>Created {formatTimePast(dataset.createdAt)}</Text>
|
||||||
|
<Divider h={4} orientation="vertical" />
|
||||||
|
<Text flex={1}>Updated {formatTimePast(dataset.updatedAt)}</Text>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</AspectRatio>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CountLabel = ({ label, count }: { label: string; count: number }) => {
|
||||||
|
return (
|
||||||
|
<VStack alignItems="center" flex={1}>
|
||||||
|
<Text color="gray.500" fontWeight="bold">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" color="gray.500">
|
||||||
|
{count}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NewDatasetCard = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const selectedOrgId = useAppStore((s) => s.selectedOrgId);
|
||||||
|
const createMutation = api.datasets.create.useMutation();
|
||||||
|
const [createDataset, isLoading] = useHandledAsyncCallback(async () => {
|
||||||
|
const newDataset = await createMutation.mutateAsync({ organizationId: selectedOrgId ?? "" });
|
||||||
|
await router.push({ pathname: "/data/[id]", query: { id: newDataset.id } });
|
||||||
|
}, [createMutation, router, selectedOrgId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AspectRatio ratio={1.2} w="full">
|
||||||
|
<VStack
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
_hover={{ cursor: "pointer", bg: "gray.50" }}
|
||||||
|
transition="background 0.2s"
|
||||||
|
cursor="pointer"
|
||||||
|
borderColor="gray.200"
|
||||||
|
borderWidth={1}
|
||||||
|
p={4}
|
||||||
|
onClick={createDataset}
|
||||||
|
>
|
||||||
|
<Icon as={isLoading ? Spinner : BsPlusSquare} boxSize={8} />
|
||||||
|
<Text display={{ base: "none", md: "block" }} ml={2}>
|
||||||
|
New Dataset
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</AspectRatio>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DatasetCardSkeleton = () => (
|
||||||
|
<AspectRatio ratio={1.2} w="full">
|
||||||
|
<VStack align="center" borderColor="gray.200" borderWidth={1} p={4} bg="gray.50">
|
||||||
|
<SkeletonText noOfLines={1} w="80%" />
|
||||||
|
<SkeletonText noOfLines={2} w="60%" />
|
||||||
|
<SkeletonText noOfLines={1} w="80%" />
|
||||||
|
</VStack>
|
||||||
|
</AspectRatio>
|
||||||
|
);
|
||||||
21
app/src/components/datasets/DatasetEntriesPaginator.tsx
Normal file
21
app/src/components/datasets/DatasetEntriesPaginator.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useDatasetEntries } from "~/utils/hooks";
|
||||||
|
import Paginator from "../Paginator";
|
||||||
|
|
||||||
|
const DatasetEntriesPaginator = () => {
|
||||||
|
const { data } = useDatasetEntries();
|
||||||
|
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
const { entries, startIndex, lastPage, count } = data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paginator
|
||||||
|
numItemsLoaded={entries.length}
|
||||||
|
startIndex={startIndex}
|
||||||
|
lastPage={lastPage}
|
||||||
|
count={count}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DatasetEntriesPaginator;
|
||||||
31
app/src/components/datasets/DatasetEntriesTable.tsx
Normal file
31
app/src/components/datasets/DatasetEntriesTable.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { type StackProps, VStack, Table, Th, Tr, Thead, Tbody, Text } from "@chakra-ui/react";
|
||||||
|
import { useDatasetEntries } from "~/utils/hooks";
|
||||||
|
import TableRow from "./TableRow";
|
||||||
|
import DatasetEntriesPaginator from "./DatasetEntriesPaginator";
|
||||||
|
|
||||||
|
const DatasetEntriesTable = (props: StackProps) => {
|
||||||
|
const { data } = useDatasetEntries();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack justifyContent="space-between" {...props}>
|
||||||
|
<Table variant="simple" sx={{ "table-layout": "fixed", width: "full" }}>
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Input</Th>
|
||||||
|
<Th>Output</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>{data?.entries.map((entry) => <TableRow key={entry.id} entry={entry} />)}</Tbody>
|
||||||
|
</Table>
|
||||||
|
{(!data || data.entries.length) === 0 ? (
|
||||||
|
<Text alignSelf="flex-start" pl={6} color="gray.500">
|
||||||
|
No entries found
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<DatasetEntriesPaginator />
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DatasetEntriesTable;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Button, HStack, useDisclosure } from "@chakra-ui/react";
|
||||||
|
import { BiImport } from "react-icons/bi";
|
||||||
|
import { BsStars } from "react-icons/bs";
|
||||||
|
|
||||||
|
import { GenerateDataModal } from "./GenerateDataModal";
|
||||||
|
|
||||||
|
export const DatasetHeaderButtons = () => {
|
||||||
|
const generateModalDisclosure = useDisclosure();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HStack>
|
||||||
|
<Button leftIcon={<BiImport />} colorScheme="blue" variant="ghost">
|
||||||
|
Import Data
|
||||||
|
</Button>
|
||||||
|
<Button leftIcon={<BsStars />} colorScheme="blue" onClick={generateModalDisclosure.onOpen}>
|
||||||
|
Generate Data
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
<GenerateDataModal
|
||||||
|
isOpen={generateModalDisclosure.isOpen}
|
||||||
|
onClose={generateModalDisclosure.onClose}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalFooter,
|
||||||
|
Text,
|
||||||
|
HStack,
|
||||||
|
VStack,
|
||||||
|
Icon,
|
||||||
|
NumberInput,
|
||||||
|
NumberInputField,
|
||||||
|
NumberInputStepper,
|
||||||
|
NumberIncrementStepper,
|
||||||
|
NumberDecrementStepper,
|
||||||
|
Button,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { BsStars } from "react-icons/bs";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useDataset, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import AutoResizeTextArea from "~/components/AutoResizeTextArea";
|
||||||
|
|
||||||
|
export const GenerateDataModal = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}) => {
|
||||||
|
const utils = api.useContext();
|
||||||
|
|
||||||
|
const datasetId = useDataset().data?.id;
|
||||||
|
|
||||||
|
const [numToGenerate, setNumToGenerate] = useState<number>(20);
|
||||||
|
const [inputDescription, setInputDescription] = useState<string>(
|
||||||
|
"Each input should contain an email body. Half of the emails should contain event details, and the other half should not.",
|
||||||
|
);
|
||||||
|
const [outputDescription, setOutputDescription] = useState<string>(
|
||||||
|
`Each output should contain "true" or "false", where "true" indicates that the email contains event details.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const generateEntriesMutation = api.datasetEntries.autogenerateEntries.useMutation();
|
||||||
|
|
||||||
|
const [generateEntries, generateEntriesInProgress] = useHandledAsyncCallback(async () => {
|
||||||
|
if (!inputDescription || !outputDescription || !numToGenerate || !datasetId) return;
|
||||||
|
await generateEntriesMutation.mutateAsync({
|
||||||
|
datasetId,
|
||||||
|
inputDescription,
|
||||||
|
outputDescription,
|
||||||
|
numToGenerate,
|
||||||
|
});
|
||||||
|
await utils.datasetEntries.list.invalidate();
|
||||||
|
onClose();
|
||||||
|
}, [
|
||||||
|
generateEntriesMutation,
|
||||||
|
onClose,
|
||||||
|
inputDescription,
|
||||||
|
outputDescription,
|
||||||
|
numToGenerate,
|
||||||
|
datasetId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} size={{ base: "xl", sm: "2xl", md: "3xl" }}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent w={1200}>
|
||||||
|
<ModalHeader>
|
||||||
|
<HStack>
|
||||||
|
<Icon as={BsStars} />
|
||||||
|
<Text>Generate Data</Text>
|
||||||
|
</HStack>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody maxW="unset">
|
||||||
|
<VStack w="full" spacing={8} padding={8} alignItems="flex-start">
|
||||||
|
<VStack alignItems="flex-start" spacing={2}>
|
||||||
|
<Text fontWeight="bold">Number of Rows:</Text>
|
||||||
|
<NumberInput
|
||||||
|
step={5}
|
||||||
|
defaultValue={15}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
onChange={(valueString) => setNumToGenerate(parseInt(valueString) || 0)}
|
||||||
|
value={numToGenerate}
|
||||||
|
w="24"
|
||||||
|
>
|
||||||
|
<NumberInputField />
|
||||||
|
<NumberInputStepper>
|
||||||
|
<NumberIncrementStepper />
|
||||||
|
<NumberDecrementStepper />
|
||||||
|
</NumberInputStepper>
|
||||||
|
</NumberInput>
|
||||||
|
</VStack>
|
||||||
|
<VStack alignItems="flex-start" w="full" spacing={2}>
|
||||||
|
<Text fontWeight="bold">Input Description:</Text>
|
||||||
|
<AutoResizeTextArea
|
||||||
|
value={inputDescription}
|
||||||
|
onChange={(e) => setInputDescription(e.target.value)}
|
||||||
|
placeholder="Each input should contain..."
|
||||||
|
/>
|
||||||
|
</VStack>
|
||||||
|
<VStack alignItems="flex-start" w="full" spacing={2}>
|
||||||
|
<Text fontWeight="bold">Output Description (optional):</Text>
|
||||||
|
<AutoResizeTextArea
|
||||||
|
value={outputDescription}
|
||||||
|
onChange={(e) => setOutputDescription(e.target.value)}
|
||||||
|
placeholder="The output should contain..."
|
||||||
|
/>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
isLoading={generateEntriesInProgress}
|
||||||
|
isDisabled={!numToGenerate || !inputDescription || !outputDescription}
|
||||||
|
onClick={generateEntries}
|
||||||
|
>
|
||||||
|
Generate
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
13
app/src/components/datasets/TableRow.tsx
Normal file
13
app/src/components/datasets/TableRow.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Td, Tr } from "@chakra-ui/react";
|
||||||
|
import { type DatasetEntry } from "@prisma/client";
|
||||||
|
|
||||||
|
const TableRow = ({ entry }: { entry: DatasetEntry }) => {
|
||||||
|
return (
|
||||||
|
<Tr key={entry.id}>
|
||||||
|
<Td>{entry.input}</Td>
|
||||||
|
<Td>{entry.output}</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableRow;
|
||||||
@@ -15,6 +15,7 @@ import { useRouter } from "next/router";
|
|||||||
import { BsPlusSquare } from "react-icons/bs";
|
import { BsPlusSquare } from "react-icons/bs";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
import { useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
|
||||||
type ExperimentData = {
|
type ExperimentData = {
|
||||||
testScenarioCount: number;
|
testScenarioCount: number;
|
||||||
@@ -75,11 +76,17 @@ 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 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({ label: "New Experiment" });
|
const newExperiment = await createMutation.mutateAsync({
|
||||||
await router.push({ pathname: "/experiments/[id]", query: { id: newExperiment.id } });
|
organizationId: selectedOrgId ?? "",
|
||||||
}, [createMutation, router]);
|
});
|
||||||
|
await router.push({
|
||||||
|
pathname: "/experiments/[id]",
|
||||||
|
query: { id: newExperiment.id },
|
||||||
|
});
|
||||||
|
}, [createMutation, router, selectedOrgId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AspectRatio ratio={1.2} w="full">
|
<AspectRatio ratio={1.2} w="full">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { BsGearFill } from "react-icons/bs";
|
|||||||
import { TbGitFork } from "react-icons/tb";
|
import { TbGitFork } from "react-icons/tb";
|
||||||
import { useAppStore } from "~/state/store";
|
import { useAppStore } from "~/state/store";
|
||||||
|
|
||||||
export const HeaderButtons = () => {
|
export const ExperimentHeaderButtons = () => {
|
||||||
const experiment = useExperiment();
|
const experiment = useExperiment();
|
||||||
|
|
||||||
const canModify = experiment.data?.access.canModify ?? false;
|
const canModify = experiment.data?.access.canModify ?? false;
|
||||||
@@ -3,18 +3,23 @@ import { api } from "~/utils/api";
|
|||||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
import { signIn, useSession } from "next-auth/react";
|
import { signIn, useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
|
||||||
export const useOnForkButtonPressed = () => {
|
export const useOnForkButtonPressed = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const user = useSession().data;
|
const user = useSession().data;
|
||||||
const experiment = useExperiment();
|
const experiment = useExperiment();
|
||||||
|
const selectedOrgId = useAppStore((state) => state.selectedOrgId);
|
||||||
|
|
||||||
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) return;
|
if (!experiment.data?.id || !selectedOrgId) return;
|
||||||
const forkedExperimentId = await forkMutation.mutateAsync({ id: experiment.data.id });
|
const forkedExperimentId = await forkMutation.mutateAsync({
|
||||||
|
id: experiment.data.id,
|
||||||
|
organizationId: selectedOrgId,
|
||||||
|
});
|
||||||
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]);
|
||||||
|
|
||||||
@@ -7,47 +7,22 @@ import {
|
|||||||
Image,
|
Image,
|
||||||
Text,
|
Text,
|
||||||
Box,
|
Box,
|
||||||
type BoxProps,
|
Link as ChakraLink,
|
||||||
type LinkProps,
|
|
||||||
Link,
|
|
||||||
Flex,
|
Flex,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { BsGithub, BsPersonCircle } from "react-icons/bs";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { BsGearFill, BsGithub, BsPersonCircle } from "react-icons/bs";
|
||||||
import { type IconType } from "react-icons";
|
import { IoStatsChartOutline } from "react-icons/io5";
|
||||||
import { RiFlaskLine } from "react-icons/ri";
|
import { 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 UserMenu from "./UserMenu";
|
||||||
|
import { env } from "~/env.mjs";
|
||||||
|
import ProjectMenu from "./ProjectMenu";
|
||||||
|
import NavSidebarOption from "./NavSidebarOption";
|
||||||
|
import IconLink from "./IconLink";
|
||||||
|
|
||||||
type IconLinkProps = BoxProps & LinkProps & { label?: string; icon: IconType };
|
const Divider = () => <Box h="1px" bgColor="gray.300" w="full" />;
|
||||||
|
|
||||||
const IconLink = ({ icon, label, href, target, color, ...props }: IconLinkProps) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const isActive = href && router.pathname.startsWith(href);
|
|
||||||
return (
|
|
||||||
<HStack
|
|
||||||
w="full"
|
|
||||||
p={4}
|
|
||||||
color={color}
|
|
||||||
as={Link}
|
|
||||||
href={href}
|
|
||||||
target={target}
|
|
||||||
bgColor={isActive ? "gray.200" : "transparent"}
|
|
||||||
_hover={{ bgColor: "gray.200", textDecoration: "none" }}
|
|
||||||
justifyContent="start"
|
|
||||||
cursor="pointer"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Icon as={icon} boxSize={6} mr={2} />
|
|
||||||
<Text fontWeight="bold" fontSize="sm">
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Divider = () => <Box h="1px" bgColor="gray.200" />;
|
|
||||||
|
|
||||||
const NavSidebar = () => {
|
const NavSidebar = () => {
|
||||||
const user = useSession().data;
|
const user = useSession().data;
|
||||||
@@ -55,38 +30,73 @@ const NavSidebar = () => {
|
|||||||
return (
|
return (
|
||||||
<VStack
|
<VStack
|
||||||
align="stretch"
|
align="stretch"
|
||||||
bgColor="gray.100"
|
bgColor="gray.50"
|
||||||
py={2}
|
py={2}
|
||||||
|
px={2}
|
||||||
pb={0}
|
pb={0}
|
||||||
height="100%"
|
height="100%"
|
||||||
w={{ base: "56px", md: "200px" }}
|
w={{ base: "56px", md: "240px" }}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
|
borderRightWidth={1}
|
||||||
|
borderColor="gray.300"
|
||||||
>
|
>
|
||||||
<HStack as={Link} href="/" _hover={{ textDecoration: "none" }} spacing={0} px={4} py={2}>
|
<HStack as={Link} href="/" _hover={{ textDecoration: "none" }} spacing={0} px={2} py={2}>
|
||||||
<Image src="/logo.svg" alt="" boxSize={6} mr={4} />
|
<Image src="/logo.svg" alt="" boxSize={6} mr={4} />
|
||||||
<Heading size="md" fontFamily="inconsolata, monospace">
|
<Heading size="md" fontFamily="inconsolata, monospace">
|
||||||
OpenPipe
|
OpenPipe
|
||||||
</Heading>
|
</Heading>
|
||||||
</HStack>
|
</HStack>
|
||||||
<VStack spacing={0} align="flex-start" overflowY="auto" overflowX="hidden" flex={1}>
|
<Divider />
|
||||||
|
<VStack align="flex-start" overflowY="auto" overflowX="hidden" flex={1}>
|
||||||
{user != null && (
|
{user != null && (
|
||||||
<>
|
<>
|
||||||
|
<ProjectMenu />
|
||||||
|
<Divider />
|
||||||
|
{env.NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS && (
|
||||||
|
<IconLink icon={IoStatsChartOutline} label="Logged Calls" href="/logged-calls" beta />
|
||||||
|
)}
|
||||||
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
|
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
|
||||||
|
{env.NEXT_PUBLIC_SHOW_DATA && (
|
||||||
|
<IconLink icon={RiDatabase2Line} label="Data" href="/data" />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{user === null && (
|
{user === null && (
|
||||||
<IconLink
|
<NavSidebarOption>
|
||||||
icon={BsPersonCircle}
|
<HStack
|
||||||
label="Sign In"
|
w="full"
|
||||||
|
p={4}
|
||||||
|
as={ChakraLink}
|
||||||
|
justifyContent="start"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
signIn("github").catch(console.error);
|
signIn("github").catch(console.error);
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<Icon as={BsPersonCircle} boxSize={6} mr={2} />
|
||||||
|
<Text fontWeight="bold" fontSize="sm">
|
||||||
|
Sign In
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</NavSidebarOption>
|
||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
{user ? <UserMenu user={user} /> : <Divider />}
|
<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 />
|
||||||
<VStack spacing={0} align="center">
|
<VStack spacing={0} align="center">
|
||||||
<Link
|
<ChakraLink
|
||||||
href="https://github.com/openpipe/openpipe"
|
href="https://github.com/openpipe/openpipe"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
color="gray.500"
|
color="gray.500"
|
||||||
@@ -94,13 +104,21 @@ const NavSidebar = () => {
|
|||||||
p={2}
|
p={2}
|
||||||
>
|
>
|
||||||
<Icon as={BsGithub} boxSize={6} />
|
<Icon as={BsGithub} boxSize={6} />
|
||||||
</Link>
|
</ChakraLink>
|
||||||
</VStack>
|
</VStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AppShell(props: { children: React.ReactNode; title?: string }) {
|
export default function AppShell({
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
requireAuth,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
title?: string;
|
||||||
|
requireAuth?: boolean;
|
||||||
|
}) {
|
||||||
const [vh, setVh] = useState("100vh"); // Default height to prevent flicker on initial render
|
const [vh, setVh] = useState("100vh"); // Default height to prevent flicker on initial render
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -120,14 +138,23 @@ export default function AppShell(props: { children: React.ReactNode; title?: str
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const user = useSession().data;
|
||||||
|
const authLoading = useSession().status === "loading";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requireAuth && user === null && !authLoading) {
|
||||||
|
signIn("github").catch(console.error);
|
||||||
|
}
|
||||||
|
}, [requireAuth, user, authLoading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex h={vh} w="100vw">
|
<Flex h={vh} w="100vw">
|
||||||
<Head>
|
<Head>
|
||||||
<title>{props.title ? `${props.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">
|
||||||
{props.children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
31
app/src/components/nav/IconLink.tsx
Normal file
31
app/src/components/nav/IconLink.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Icon, HStack, Text, type BoxProps } from "@chakra-ui/react";
|
||||||
|
import Link, { type LinkProps } from "next/link";
|
||||||
|
import { type IconType } from "react-icons";
|
||||||
|
import NavSidebarOption from "./NavSidebarOption";
|
||||||
|
|
||||||
|
type IconLinkProps = BoxProps &
|
||||||
|
LinkProps & { label?: string; icon: IconType; href: string; beta?: boolean };
|
||||||
|
|
||||||
|
const IconLink = ({ icon, label, href, color, beta, ...props }: IconLinkProps) => {
|
||||||
|
return (
|
||||||
|
<Link href={href} style={{ width: "100%" }}>
|
||||||
|
<NavSidebarOption activeHrefPattern={href}>
|
||||||
|
<HStack w="full" justifyContent="space-between" p={2} color={color} {...props}>
|
||||||
|
<HStack w="full" justifyContent="start">
|
||||||
|
<Icon as={icon} boxSize={6} mr={2} />
|
||||||
|
<Text fontSize="sm" display={{ base: "none", md: "block" }}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
{beta && (
|
||||||
|
<Text fontSize="xs" ml={2} fontWeight="bold" color="orange.400">
|
||||||
|
BETA
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</NavSidebarOption>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IconLink;
|
||||||
27
app/src/components/nav/NavSidebarOption.tsx
Normal file
27
app/src/components/nav/NavSidebarOption.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Box, type BoxProps } from "@chakra-ui/react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
const NavSidebarOption = ({
|
||||||
|
activeHrefPattern,
|
||||||
|
disableHoverEffect,
|
||||||
|
...props
|
||||||
|
}: { activeHrefPattern?: string; disableHoverEffect?: boolean } & BoxProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const isActive = activeHrefPattern && router.pathname.startsWith(activeHrefPattern);
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
w="full"
|
||||||
|
fontWeight={isActive ? "bold" : "500"}
|
||||||
|
bgColor={isActive ? "gray.200" : "transparent"}
|
||||||
|
_hover={disableHoverEffect ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
|
||||||
|
justifyContent="start"
|
||||||
|
cursor="pointer"
|
||||||
|
borderRadius={4}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavSidebarOption;
|
||||||
19
app/src/components/nav/PageHeaderContainer.tsx
Normal file
19
app/src/components/nav/PageHeaderContainer.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Flex, type FlexProps } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
const PageHeaderContainer = (props: FlexProps) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
px={8}
|
||||||
|
py={2}
|
||||||
|
minH={16}
|
||||||
|
w="full"
|
||||||
|
direction={{ base: "column", sm: "row" }}
|
||||||
|
alignItems={{ base: "flex-start", sm: "center" }}
|
||||||
|
justifyContent="space-between"
|
||||||
|
fontWeight="500"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageHeaderContainer;
|
||||||
28
app/src/components/nav/ProjectBreadcrumbContents.tsx
Normal file
28
app/src/components/nav/ProjectBreadcrumbContents.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { HStack, Flex, Text } from "@chakra-ui/react";
|
||||||
|
import { useSelectedOrg } from "~/utils/hooks";
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
export default function ProjectBreadcrumbContents({ orgName = "" }: { orgName?: string }) {
|
||||||
|
const { data: selectedOrg } = useSelectedOrg();
|
||||||
|
|
||||||
|
orgName = orgName || selectedOrg?.name || "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack w="full">
|
||||||
|
<Flex
|
||||||
|
p={1}
|
||||||
|
borderRadius={4}
|
||||||
|
backgroundColor="orange.100"
|
||||||
|
boxSize={6}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Text>{orgName[0]?.toUpperCase()}</Text>
|
||||||
|
</Flex>
|
||||||
|
<Text display={{ base: "none", md: "block" }} py={1}>
|
||||||
|
{orgName}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
178
app/src/components/nav/ProjectMenu.tsx
Normal file
178
app/src/components/nav/ProjectMenu.tsx
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import {
|
||||||
|
HStack,
|
||||||
|
VStack,
|
||||||
|
Text,
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
Flex,
|
||||||
|
IconButton,
|
||||||
|
Icon,
|
||||||
|
Divider,
|
||||||
|
Button,
|
||||||
|
useDisclosure,
|
||||||
|
Spinner,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { AiFillCaretDown } from "react-icons/ai";
|
||||||
|
import { BsGear, BsPlus } from "react-icons/bs";
|
||||||
|
import { type Organization } from "@prisma/client";
|
||||||
|
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import NavSidebarOption from "./NavSidebarOption";
|
||||||
|
import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
export default function ProjectMenu() {
|
||||||
|
const router = useRouter();
|
||||||
|
const isActive = router.pathname.startsWith("/home");
|
||||||
|
const utils = api.useContext();
|
||||||
|
|
||||||
|
const selectedOrgId = useAppStore((s) => s.selectedOrgId);
|
||||||
|
const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId);
|
||||||
|
|
||||||
|
const { data: orgs } = api.organizations.list.useQuery();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (orgs && orgs[0] && (!selectedOrgId || !orgs.find((org) => org.id === selectedOrgId))) {
|
||||||
|
setSelectedOrgId(orgs[0].id);
|
||||||
|
}
|
||||||
|
}, [selectedOrgId, setSelectedOrgId, orgs]);
|
||||||
|
|
||||||
|
const { data: selectedOrg } = useSelectedOrg();
|
||||||
|
|
||||||
|
const popover = useDisclosure();
|
||||||
|
|
||||||
|
const createMutation = api.organizations.create.useMutation();
|
||||||
|
const [createProject, isLoading] = useHandledAsyncCallback(async () => {
|
||||||
|
const newOrg = await createMutation.mutateAsync({ name: "New Project" });
|
||||||
|
await utils.organizations.list.invalidate();
|
||||||
|
setSelectedOrgId(newOrg.id);
|
||||||
|
await router.push({ pathname: "/project/settings" });
|
||||||
|
}, [createMutation, router]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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" }}
|
||||||
|
>
|
||||||
|
PROJECT
|
||||||
|
</Text>
|
||||||
|
<NavSidebarOption>
|
||||||
|
<Popover
|
||||||
|
placement="bottom-start"
|
||||||
|
isOpen={popover.isOpen}
|
||||||
|
onClose={popover.onClose}
|
||||||
|
closeOnBlur
|
||||||
|
>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<HStack w="full" justifyContent="space-between" onClick={popover.onToggle}>
|
||||||
|
<Flex
|
||||||
|
p={1}
|
||||||
|
borderRadius={4}
|
||||||
|
backgroundColor="orange.100"
|
||||||
|
minW={{ base: 10, md: 8 }}
|
||||||
|
minH={{ base: 10, md: 8 }}
|
||||||
|
m={{ base: 0, md: 1 }}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
// onClick={sidebarExpanded ? undefined : openMenu}
|
||||||
|
>
|
||||||
|
<Text>{selectedOrg?.name[0]?.toUpperCase()}</Text>
|
||||||
|
</Flex>
|
||||||
|
<Text fontSize="sm" display={{ base: "none", md: "block" }} py={1}>
|
||||||
|
{selectedOrg?.name}
|
||||||
|
</Text>
|
||||||
|
<Icon as={AiFillCaretDown} boxSize={3} size="xs" color="gray.500" mr={2} />
|
||||||
|
</HStack>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
_focusVisible={{ boxShadow: "unset" }}
|
||||||
|
minW={0}
|
||||||
|
borderColor="blue.400"
|
||||||
|
w="full"
|
||||||
|
>
|
||||||
|
<VStack alignItems="flex-start" spacing={2} py={4} px={2}>
|
||||||
|
<Text color="gray.500" fontSize="xs" fontWeight="bold" pb={1}>
|
||||||
|
PROJECTS
|
||||||
|
</Text>
|
||||||
|
<Divider />
|
||||||
|
<VStack spacing={0} w="full">
|
||||||
|
{orgs?.map((org) => (
|
||||||
|
<ProjectOption
|
||||||
|
key={org.id}
|
||||||
|
org={org}
|
||||||
|
isActive={org.id === selectedOrgId}
|
||||||
|
onClose={popover.onClose}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
<HStack
|
||||||
|
as={Button}
|
||||||
|
variant="ghost"
|
||||||
|
colorScheme="blue"
|
||||||
|
color="blue.400"
|
||||||
|
pr={8}
|
||||||
|
w="full"
|
||||||
|
onClick={createProject}
|
||||||
|
>
|
||||||
|
<Icon as={isLoading ? Spinner : BsPlus} boxSize={6} />
|
||||||
|
<Text>New project</Text>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</NavSidebarOption>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectOption = ({
|
||||||
|
org,
|
||||||
|
isActive,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
org: Organization;
|
||||||
|
isActive: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}) => {
|
||||||
|
const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId);
|
||||||
|
const [gearHovered, setGearHovered] = useState(false);
|
||||||
|
return (
|
||||||
|
<HStack
|
||||||
|
as={Link}
|
||||||
|
href="/experiments"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedOrgId(org.id);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
w="full"
|
||||||
|
justifyContent="space-between"
|
||||||
|
bgColor={isActive ? "gray.100" : "transparent"}
|
||||||
|
_hover={gearHovered ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
|
||||||
|
p={2}
|
||||||
|
>
|
||||||
|
<Text>{org.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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,12 +8,15 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
Link,
|
Link,
|
||||||
|
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";
|
||||||
import { BsBoxArrowRight, BsChevronRight, BsPersonCircle } from "react-icons/bs";
|
import { BsBoxArrowRight, BsChevronRight, BsPersonCircle } from "react-icons/bs";
|
||||||
|
import NavSidebarOption from "./NavSidebarOption";
|
||||||
|
|
||||||
export default function UserMenu({ user }: { user: Session }) {
|
export default function UserMenu({ user, ...rest }: { user: Session } & StackProps) {
|
||||||
const profileImage = user.user.image ? (
|
const profileImage = user.user.image ? (
|
||||||
<Image src={user.user.image} alt="profile picture" boxSize={8} borderRadius="50%" />
|
<Image src={user.user.image} alt="profile picture" boxSize={8} borderRadius="50%" />
|
||||||
) : (
|
) : (
|
||||||
@@ -24,18 +27,14 @@ export default function UserMenu({ user }: { user: Session }) {
|
|||||||
<>
|
<>
|
||||||
<Popover placement="right">
|
<Popover placement="right">
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
|
<Box>
|
||||||
|
<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
|
||||||
px={3}
|
|
||||||
spacing={3}
|
|
||||||
py={2}
|
py={2}
|
||||||
borderColor={"gray.200"}
|
px={1}
|
||||||
borderTopWidth={1}
|
spacing={3}
|
||||||
borderBottomWidth={1}
|
{...rest}
|
||||||
cursor="pointer"
|
|
||||||
_hover={{
|
|
||||||
bgColor: "gray.200",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{profileImage}
|
{profileImage}
|
||||||
<VStack spacing={0} align="start" flex={1} flexShrink={1}>
|
<VStack spacing={0} align="start" flex={1} flexShrink={1}>
|
||||||
@@ -43,11 +42,13 @@ export default function UserMenu({ user }: { user: Session }) {
|
|||||||
{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>
|
||||||
|
</Box>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent _focusVisible={{ boxShadow: "unset", outline: "unset" }} maxW="200px">
|
<PopoverContent _focusVisible={{ boxShadow: "unset", outline: "unset" }} maxW="200px">
|
||||||
<VStack align="stretch" spacing={0}>
|
<VStack align="stretch" spacing={0}>
|
||||||
|
|||||||
89
app/src/components/projectSettings/DeleteProjectDialog.tsx
Normal file
89
app/src/components/projectSettings/DeleteProjectDialog.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogBody,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
Input,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
Box,
|
||||||
|
Spinner,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks";
|
||||||
|
|
||||||
|
export const DeleteProjectDialog = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}) => {
|
||||||
|
const selectedOrg = useSelectedOrg();
|
||||||
|
const deleteMutation = api.organizations.delete.useMutation();
|
||||||
|
const utils = api.useContext();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const [onDeleteConfirm, isDeleting] = useHandledAsyncCallback(async () => {
|
||||||
|
if (!selectedOrg.data?.id) return;
|
||||||
|
await deleteMutation.mutateAsync({ id: selectedOrg.data.id });
|
||||||
|
await utils.organizations.list.invalidate();
|
||||||
|
await router.push({ pathname: "/experiments" });
|
||||||
|
onClose();
|
||||||
|
}, [deleteMutation, selectedOrg, router]);
|
||||||
|
|
||||||
|
const [nameToDelete, setNameToDelete] = useState("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
||||||
|
<AlertDialogOverlay>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||||
|
Delete Project
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<AlertDialogBody>
|
||||||
|
<VStack spacing={4} alignItems="flex-start">
|
||||||
|
<Text>
|
||||||
|
If you delete this project all the associated data and experiments will be deleted
|
||||||
|
as well. If you are sure that you want to delete this project, please type the name
|
||||||
|
of the project below.
|
||||||
|
</Text>
|
||||||
|
<Box bgColor="orange.100" w="full" p={2} borderRadius={4}>
|
||||||
|
<Text fontFamily="inconsolata">{selectedOrg.data?.name}</Text>
|
||||||
|
</Box>
|
||||||
|
<Input
|
||||||
|
placeholder={selectedOrg.data?.name}
|
||||||
|
value={nameToDelete}
|
||||||
|
onChange={(e) => setNameToDelete(e.target.value)}
|
||||||
|
/>
|
||||||
|
</VStack>
|
||||||
|
</AlertDialogBody>
|
||||||
|
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<Button ref={cancelRef} onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorScheme="red"
|
||||||
|
onClick={onDeleteConfirm}
|
||||||
|
ml={3}
|
||||||
|
isDisabled={nameToDelete !== selectedOrg.data?.name}
|
||||||
|
w={20}
|
||||||
|
>
|
||||||
|
{isDeleting ? <Spinner /> : "Delete"}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogOverlay>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -19,6 +19,8 @@ export const env = createEnv({
|
|||||||
OPENAI_API_KEY: z.string().min(1),
|
OPENAI_API_KEY: z.string().min(1),
|
||||||
REPLICATE_API_TOKEN: z.string().default("placeholder"),
|
REPLICATE_API_TOKEN: z.string().default("placeholder"),
|
||||||
ANTHROPIC_API_KEY: z.string().default("placeholder"),
|
ANTHROPIC_API_KEY: z.string().default("placeholder"),
|
||||||
|
SENTRY_AUTH_TOKEN: z.string().optional(),
|
||||||
|
OPENPIPE_API_KEY: z.string().optional(),
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +31,10 @@ export const env = createEnv({
|
|||||||
client: {
|
client: {
|
||||||
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
|
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
|
||||||
NEXT_PUBLIC_SOCKET_URL: z.string().url().default("http://localhost:3318"),
|
NEXT_PUBLIC_SOCKET_URL: z.string().url().default("http://localhost:3318"),
|
||||||
|
NEXT_PUBLIC_HOST: z.string().url().default("http://localhost:3000"),
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN: z.string().optional(),
|
||||||
|
NEXT_PUBLIC_SHOW_DATA: z.string().optional(),
|
||||||
|
NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS: z.string().optional(),
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,10 +48,16 @@ export const env = createEnv({
|
|||||||
RESTRICT_PRISMA_LOGS: process.env.RESTRICT_PRISMA_LOGS,
|
RESTRICT_PRISMA_LOGS: process.env.RESTRICT_PRISMA_LOGS,
|
||||||
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
||||||
NEXT_PUBLIC_SOCKET_URL: process.env.NEXT_PUBLIC_SOCKET_URL,
|
NEXT_PUBLIC_SOCKET_URL: process.env.NEXT_PUBLIC_SOCKET_URL,
|
||||||
|
NEXT_PUBLIC_HOST: process.env.NEXT_PUBLIC_HOST,
|
||||||
|
NEXT_PUBLIC_SHOW_DATA: process.env.NEXT_PUBLIC_SHOW_DATA,
|
||||||
GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
|
GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
|
||||||
GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
|
GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
|
||||||
REPLICATE_API_TOKEN: process.env.REPLICATE_API_TOKEN,
|
REPLICATE_API_TOKEN: process.env.REPLICATE_API_TOKEN,
|
||||||
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
||||||
|
OPENPIPE_API_KEY: process.env.OPENPIPE_API_KEY,
|
||||||
|
NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS: process.env.NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"model": {
|
||||||
|
"description": "The model that will complete your prompt.",
|
||||||
|
"x-oaiTypeLabel": "string",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"claude-2",
|
||||||
|
"claude-2.0",
|
||||||
|
"claude-instant-1",
|
||||||
|
"claude-instant-1.1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"description": "The prompt that you want Claude to complete.\n\nFor proper response generation you will need to format your prompt as follows:\n\"\\n\\nHuman: all instructions for the assistant\\n\\nAssistant:\". The prompt string should begin with the characters \"Human:\" and end with \"Assistant:\".",
|
||||||
|
"default": "<|endoftext|>",
|
||||||
|
"example": "\\n\\nHuman: What is the correct translation of ${scenario.input}? I would like a long analysis followed by a short answer.\\n\\nAssistant:",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"max_tokens_to_sample": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"default": 256,
|
||||||
|
"nullable": true,
|
||||||
|
"description": "The maximum number of tokens to generate before stopping."
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 1,
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Amount of randomness injected into the response.\n\nDefaults to 1."
|
||||||
|
},
|
||||||
|
"top_p": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 1,
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Use nucleus sampling.\n\nYou should either alter temperature or top_p, but not both.\n"
|
||||||
|
},
|
||||||
|
"top_k": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"default": 5,
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Only sample from the top K options for each subsequent token."
|
||||||
|
},
|
||||||
|
"stream": {
|
||||||
|
"description": "Whether to incrementally stream the response using server-sent events.",
|
||||||
|
"type": "boolean",
|
||||||
|
"nullable": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"stop_sequences": {
|
||||||
|
"description": "Sequences that will cause the model to stop generating completion text.\nBy default, our models stop on \"\\n\\nHuman:\".",
|
||||||
|
"default": null,
|
||||||
|
"nullable": true,
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["model", "prompt", "max_tokens_to_sample"]
|
||||||
|
}
|
||||||
@@ -13,8 +13,9 @@ const frontendModelProvider: FrontendModelProvider<SupportedModel, Completion> =
|
|||||||
promptTokenPrice: 11.02 / 1000000,
|
promptTokenPrice: 11.02 / 1000000,
|
||||||
completionTokenPrice: 32.68 / 1000000,
|
completionTokenPrice: 32.68 / 1000000,
|
||||||
speed: "medium",
|
speed: "medium",
|
||||||
provider: "anthropic",
|
provider: "anthropic/completion",
|
||||||
learnMoreUrl: "https://www.anthropic.com/product",
|
learnMoreUrl: "https://www.anthropic.com/product",
|
||||||
|
apiDocsUrl: "https://docs.anthropic.com/claude/reference/complete_post",
|
||||||
},
|
},
|
||||||
"claude-instant-1.1": {
|
"claude-instant-1.1": {
|
||||||
name: "Claude Instant 1.1",
|
name: "Claude Instant 1.1",
|
||||||
@@ -22,8 +23,9 @@ const frontendModelProvider: FrontendModelProvider<SupportedModel, Completion> =
|
|||||||
promptTokenPrice: 1.63 / 1000000,
|
promptTokenPrice: 1.63 / 1000000,
|
||||||
completionTokenPrice: 5.51 / 1000000,
|
completionTokenPrice: 5.51 / 1000000,
|
||||||
speed: "fast",
|
speed: "fast",
|
||||||
provider: "anthropic",
|
provider: "anthropic/completion",
|
||||||
learnMoreUrl: "https://www.anthropic.com/product",
|
learnMoreUrl: "https://www.anthropic.com/product",
|
||||||
|
apiDocsUrl: "https://docs.anthropic.com/claude/reference/complete_post",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"model": {
|
|
||||||
"description": "The model that will complete your prompt.\nAs we improve Claude, we develop new versions of it that you can query.\nThis parameter controls which version of Claude answers your request.\nRight now we are offering two model families: Claude, and Claude Instant.\nYou can use them by setting model to \"claude-2\" or \"claude-instant-1\", respectively.\nSee models for additional details.\n",
|
|
||||||
"x-oaiTypeLabel": "string",
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"claude-2",
|
|
||||||
"claude-2.0",
|
|
||||||
"claude-instant-1",
|
|
||||||
"claude-instant-1.1"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"prompt": {
|
|
||||||
"description": "The prompt that you want Claude to complete.\n\nFor proper response generation you will need to format your prompt as follows:\n\\n\\nHuman: ${userQuestion}\\n\\nAssistant:\nSee our comments on prompts for more context.\n",
|
|
||||||
"default": "<|endoftext|>",
|
|
||||||
"nullable": true,
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"default": "",
|
|
||||||
"example": "This is a test."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "",
|
|
||||||
"example": "This is a test."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"items": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"example": "[1212, 318, 257, 1332, 13]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"items": {
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"items": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"example": "[[1212, 318, 257, 1332, 13]]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"max_tokens_to_sample": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 1,
|
|
||||||
"default": 256,
|
|
||||||
"example": 256,
|
|
||||||
"nullable": true,
|
|
||||||
"description": "The maximum number of tokens to generate before stopping.\n\nNote that our models may stop before reaching this maximum. This parameter only specifies the absolute maximum number of tokens to generate.\n"
|
|
||||||
},
|
|
||||||
"temperature": {
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 0,
|
|
||||||
"maximum": 1,
|
|
||||||
"default": 1,
|
|
||||||
"example": 1,
|
|
||||||
"nullable": true,
|
|
||||||
"description": "Amount of randomness injected into the response.\n\nDefaults to 1. Ranges from 0 to 1. Use temp closer to 0 for analytical / multiple choice, and closer to 1 for creative and generative tasks.\n"
|
|
||||||
},
|
|
||||||
"top_p": {
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 0,
|
|
||||||
"maximum": 1,
|
|
||||||
"default": 1,
|
|
||||||
"example": 1,
|
|
||||||
"nullable": true,
|
|
||||||
"description": "Use nucleus sampling.\n\nIn nucleus sampling, we compute the cumulative distribution over all the options \nfor each subsequent token in decreasing probability order and cut it off once \nit reaches a particular probability specified by top_p. You should either alter temperature or top_p, but not both.\n"
|
|
||||||
},
|
|
||||||
"top_k": {
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 0,
|
|
||||||
"default": 5,
|
|
||||||
"example": 5,
|
|
||||||
"nullable": true,
|
|
||||||
"description": "Only sample from the top K options for each subsequent token.\n\nUsed to remove \"long tail\" low probability responses. Learn more technical details here.\n"
|
|
||||||
},
|
|
||||||
"stream": {
|
|
||||||
"description": "Whether to incrementally stream the response using server-sent events.\nSee this guide to SSE events for details.type: boolean\n",
|
|
||||||
"nullable": true,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"stop_sequences": {
|
|
||||||
"description": "Sequences that will cause the model to stop generating completion text.\nOur models stop on \"\\n\\nHuman:\", and may include additional built-in stop sequences in the future. By providing the stop_sequences parameter, you may include additional strings that will cause the model to stop generating.\n",
|
|
||||||
"default": null,
|
|
||||||
"nullable": true,
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"default": "<|endoftext|>",
|
|
||||||
"example": "\n",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"maxItems": 4,
|
|
||||||
"items": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "[\"\\n\"]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"user_id": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "13803d75-b4b5-4c3e-b2a2-6f21399b021b",
|
|
||||||
"description": "An external identifier for the user who is associated with the request.\n\nThis should be a uuid, hash value, or other opaque identifier. Anthropic may use this id to help detect abuse. \nDo not include any identifying information such as name, email address, or phone number.\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "An object describing metadata about the request.\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["model", "prompt", "max_tokens_to_sample"]
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import openaiChatCompletionFrontend from "./openai-ChatCompletion/frontend";
|
import openaiChatCompletionFrontend from "./openai-ChatCompletion/frontend";
|
||||||
import replicateLlama2Frontend from "./replicate-llama2/frontend";
|
import replicateLlama2Frontend from "./replicate-llama2/frontend";
|
||||||
import anthropicFrontend from "./anthropic/frontend";
|
import anthropicFrontend from "./anthropic-completion/frontend";
|
||||||
import { type SupportedProvider, type FrontendModelProvider } from "./types";
|
import { type SupportedProvider, type FrontendModelProvider } from "./types";
|
||||||
|
|
||||||
// Keep attributes here that need to be accessible from the frontend. We can't
|
// Keep attributes here that need to be accessible from the frontend. We can't
|
||||||
@@ -9,7 +9,7 @@ import { type SupportedProvider, type FrontendModelProvider } from "./types";
|
|||||||
const frontendModelProviders: Record<SupportedProvider, FrontendModelProvider<any, any>> = {
|
const frontendModelProviders: Record<SupportedProvider, FrontendModelProvider<any, any>> = {
|
||||||
"openai/ChatCompletion": openaiChatCompletionFrontend,
|
"openai/ChatCompletion": openaiChatCompletionFrontend,
|
||||||
"replicate/llama2": replicateLlama2Frontend,
|
"replicate/llama2": replicateLlama2Frontend,
|
||||||
anthropic: anthropicFrontend,
|
"anthropic/completion": anthropicFrontend,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default frontendModelProviders;
|
export default frontendModelProviders;
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import { type JSONSchema4Object } from "json-schema";
|
|
||||||
import modelProviders from "./modelProviders";
|
|
||||||
import { compile } from "json-schema-to-typescript";
|
|
||||||
import dedent from "dedent";
|
|
||||||
|
|
||||||
export const declarePromptTypes = async () => {
|
|
||||||
const promptTypes = (await generatePromptTypes()).replace(
|
|
||||||
/export interface PromptTypes/g,
|
|
||||||
"interface PromptTypes",
|
|
||||||
);
|
|
||||||
|
|
||||||
return dedent`
|
|
||||||
${promptTypes}
|
|
||||||
|
|
||||||
declare function definePrompt<T extends keyof PromptTypes>(modelProvider: T, input: PromptTypes[T])
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generatePromptTypes = async () => {
|
|
||||||
const combinedSchema = {
|
|
||||||
type: "object",
|
|
||||||
properties: {} as Record<string, JSONSchema4Object>,
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.entries(modelProviders).forEach(([id, provider]) => {
|
|
||||||
combinedSchema.properties[id] = provider.inputSchema;
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.entries(modelProviders).forEach(([id, provider]) => {
|
|
||||||
combinedSchema.properties[id] = provider.inputSchema;
|
|
||||||
});
|
|
||||||
|
|
||||||
return await compile(combinedSchema as JSONSchema4Object, "PromptTypes", {
|
|
||||||
additionalProperties: false,
|
|
||||||
bannerComment: dedent`
|
|
||||||
/**
|
|
||||||
* This type map defines the input types for each model provider.
|
|
||||||
*/
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
36
app/src/modelProviders/generateTypes.ts
Normal file
36
app/src/modelProviders/generateTypes.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { type JSONSchema4Object } from "json-schema";
|
||||||
|
import modelProviders from "./modelProviders";
|
||||||
|
import { compile } from "json-schema-to-typescript";
|
||||||
|
import dedent from "dedent";
|
||||||
|
|
||||||
|
export default async function generateTypes() {
|
||||||
|
const combinedSchema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {} as Record<string, JSONSchema4Object>,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(modelProviders).forEach(([id, provider]) => {
|
||||||
|
combinedSchema.properties[id] = provider.inputSchema;
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.entries(modelProviders).forEach(([id, provider]) => {
|
||||||
|
combinedSchema.properties[id] = provider.inputSchema;
|
||||||
|
});
|
||||||
|
|
||||||
|
const promptTypes = (
|
||||||
|
await compile(combinedSchema as JSONSchema4Object, "PromptTypes", {
|
||||||
|
additionalProperties: false,
|
||||||
|
bannerComment: dedent`
|
||||||
|
/**
|
||||||
|
* This type map defines the input types for each model provider.
|
||||||
|
*/
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
).replace(/export interface PromptTypes/g, "interface PromptTypes");
|
||||||
|
|
||||||
|
return dedent`
|
||||||
|
${promptTypes}
|
||||||
|
|
||||||
|
declare function definePrompt<T extends keyof PromptTypes>(modelProvider: T, input: PromptTypes[T])
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import openaiChatCompletion from "./openai-ChatCompletion";
|
import openaiChatCompletion from "./openai-ChatCompletion";
|
||||||
import replicateLlama2 from "./replicate-llama2";
|
import replicateLlama2 from "./replicate-llama2";
|
||||||
import anthropic from "./anthropic";
|
import anthropicCompletion from "./anthropic-completion";
|
||||||
import { type SupportedProvider, type ModelProvider } from "./types";
|
import { type SupportedProvider, type ModelProvider } from "./types";
|
||||||
|
|
||||||
const modelProviders: Record<SupportedProvider, ModelProvider<any, any, any>> = {
|
const modelProviders: Record<SupportedProvider, ModelProvider<any, any, any>> = {
|
||||||
"openai/ChatCompletion": openaiChatCompletion,
|
"openai/ChatCompletion": openaiChatCompletion,
|
||||||
"replicate/llama2": replicateLlama2,
|
"replicate/llama2": replicateLlama2,
|
||||||
anthropic,
|
"anthropic/completion": anthropicCompletion,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default modelProviders;
|
export default modelProviders;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from "openai/resources/chat";
|
} from "openai/resources/chat";
|
||||||
import { countOpenAIChatTokens } from "~/utils/countTokens";
|
import { countOpenAIChatTokens } from "~/utils/countTokens";
|
||||||
import { type CompletionResponse } from "../types";
|
import { type CompletionResponse } from "../types";
|
||||||
import { 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 { truthyFilter } from "~/utils/utils";
|
||||||
import { APIError } from "openai";
|
import { APIError } from "openai";
|
||||||
@@ -40,6 +40,8 @@ const mergeStreamedChunks = (
|
|||||||
((choice.delta.function_call.arguments as string) ?? "");
|
((choice.delta.function_call.arguments as string) ?? "");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-expect-error the types are correctly telling us that finish_reason
|
||||||
|
// could be null, but don't want to fix it right now.
|
||||||
choices.push({ ...omit(choice, "delta"), message: { role: "assistant", ...choice.delta } });
|
choices.push({ ...omit(choice, "delta"), message: { role: "assistant", ...choice.delta } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,6 +66,7 @@ export async function getCompletion(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (onStream) {
|
if (onStream) {
|
||||||
|
console.log("got started");
|
||||||
const resp = await openai.chat.completions.create(
|
const resp = await openai.chat.completions.create(
|
||||||
{ ...input, stream: true },
|
{ ...input, stream: true },
|
||||||
{
|
{
|
||||||
@@ -71,9 +74,11 @@ export async function getCompletion(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
for await (const part of resp) {
|
for await (const part of resp) {
|
||||||
|
console.log("got part", part);
|
||||||
finalCompletion = mergeStreamedChunks(finalCompletion, part);
|
finalCompletion = mergeStreamedChunks(finalCompletion, part);
|
||||||
onStream(finalCompletion);
|
onStream(finalCompletion);
|
||||||
}
|
}
|
||||||
|
console.log("got final", finalCompletion);
|
||||||
if (!finalCompletion) {
|
if (!finalCompletion) {
|
||||||
return {
|
return {
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -121,9 +126,17 @@ export async function getCompletion(
|
|||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof APIError) {
|
if (error instanceof APIError) {
|
||||||
|
// The types from the sdk are wrong
|
||||||
|
const rawMessage = error.message as string | string[];
|
||||||
|
// If the message is not a string, stringify it
|
||||||
|
const message = isString(rawMessage)
|
||||||
|
? rawMessage
|
||||||
|
: isArray(rawMessage)
|
||||||
|
? rawMessage.map((m) => m.toString()).join("\n")
|
||||||
|
: (rawMessage as any).toString();
|
||||||
return {
|
return {
|
||||||
type: "error",
|
type: "error",
|
||||||
message: error.message,
|
message,
|
||||||
autoRetry: error.status === 429 || error.status === 503,
|
autoRetry: error.status === 429 || error.status === 503,
|
||||||
statusCode: error.status,
|
statusCode: error.status,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ const replicate = new Replicate({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const modelIds: Record<ReplicateLlama2Input["model"], string> = {
|
const modelIds: Record<ReplicateLlama2Input["model"], string> = {
|
||||||
"7b-chat": "5ec5fdadd80ace49f5a2b2178cceeb9f2f77c493b85b1131002c26e6b2b13184",
|
"7b-chat": "4f0b260b6a13eb53a6b1891f089d57c08f41003ae79458be5011303d81a394dc",
|
||||||
"13b-chat": "6b4da803a2382c08868c5af10a523892f38e2de1aafb2ee55b020d9efef2fdb8",
|
"13b-chat": "2a7f981751ec7fdf87b5b91ad4db53683a98082e9ff7bfd12c8cd5ea85980a52",
|
||||||
"70b-chat": "2d19859030ff705a87c746f7e96eea03aefb71f166725aee39692f1476566d48",
|
"70b-chat": "2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1",
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getCompletion(
|
export async function getCompletion(
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const modelProvider: ReplicateLlama2Provider = {
|
|||||||
temperature: {
|
temperature: {
|
||||||
type: "number",
|
type: "number",
|
||||||
description:
|
description:
|
||||||
"Adjusts randomness of outputs, greater than 1 is random and 0 is deterministic, 0.75 is a good starting value. (minimum: 0.01; maximum: 5)",
|
"Adjusts randomness of outputs, 0.1 is a good starting value. (minimum: 0.01; maximum: 5)",
|
||||||
},
|
},
|
||||||
top_p: {
|
top_p: {
|
||||||
type: "number",
|
type: "number",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { z } from "zod";
|
|||||||
export const ZodSupportedProvider = z.union([
|
export const ZodSupportedProvider = z.union([
|
||||||
z.literal("openai/ChatCompletion"),
|
z.literal("openai/ChatCompletion"),
|
||||||
z.literal("replicate/llama2"),
|
z.literal("replicate/llama2"),
|
||||||
z.literal("anthropic"),
|
z.literal("anthropic/completion"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type SupportedProvider = z.infer<typeof ZodSupportedProvider>;
|
export type SupportedProvider = z.infer<typeof ZodSupportedProvider>;
|
||||||
@@ -21,6 +21,7 @@ export type Model = {
|
|||||||
provider: SupportedProvider;
|
provider: SupportedProvider;
|
||||||
description?: string;
|
description?: string;
|
||||||
learnMoreUrl?: string;
|
learnMoreUrl?: string;
|
||||||
|
apiDocsUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProviderModel = { provider: z.infer<typeof ZodSupportedProvider>; model: string };
|
export type ProviderModel = { provider: z.infer<typeof ZodSupportedProvider>; model: string };
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { SessionProvider } from "next-auth/react";
|
|||||||
import { type AppType } from "next/app";
|
import { type AppType } from "next/app";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import Favicon from "~/components/Favicon";
|
import Favicon from "~/components/Favicon";
|
||||||
import "~/utils/analytics";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { ChakraThemeProvider } from "~/theme/ChakraThemeProvider";
|
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";
|
||||||
|
|
||||||
const MyApp: AppType<{ session: Session | null }> = ({
|
const MyApp: AppType<{ session: Session | null }> = ({
|
||||||
Component,
|
Component,
|
||||||
@@ -21,10 +21,22 @@ const MyApp: AppType<{ session: Session | null }> = ({
|
|||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
|
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
|
||||||
/>
|
/>
|
||||||
|
<meta name="og:title" content="OpenPipe: Open-Source Lab for LLMs" key="title" />
|
||||||
|
<meta
|
||||||
|
name="og:description"
|
||||||
|
content="OpenPipe is a powerful playground for quickly optimizing performance, cost, and speed across models."
|
||||||
|
key="description"
|
||||||
|
/>
|
||||||
|
<meta name="og:image" content="/og.png" key="og-image" />
|
||||||
|
<meta property="og:image:height" content="630" />
|
||||||
|
<meta property="og:image:width" content="1200" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:image" content="/og.png" />
|
||||||
</Head>
|
</Head>
|
||||||
<SessionProvider session={session}>
|
<SessionProvider session={session}>
|
||||||
<SyncAppStore />
|
<SyncAppStore />
|
||||||
<Favicon />
|
<Favicon />
|
||||||
|
<SessionIdentifier />
|
||||||
<ChakraThemeProvider>
|
<ChakraThemeProvider>
|
||||||
<QueryParamProvider adapter={NextAdapterApp}>
|
<QueryParamProvider adapter={NextAdapterApp}>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
|
|||||||
81
app/src/pages/api/experiments/og-image.tsx
Normal file
81
app/src/pages/api/experiments/og-image.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { ImageResponse } from "@vercel/og";
|
||||||
|
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
runtime: "experimental-edge",
|
||||||
|
};
|
||||||
|
|
||||||
|
const inconsolataRegularFontP = fetch(
|
||||||
|
new URL("../../../../public/fonts/Inconsolata_SemiExpanded-Medium.ttf", import.meta.url),
|
||||||
|
).then((res) => res.arrayBuffer());
|
||||||
|
|
||||||
|
const OgImage = async (req: NextApiRequest, _res: NextApiResponse) => {
|
||||||
|
// @ts-expect-error - nextUrl is not defined on NextApiRequest for some reason
|
||||||
|
const searchParams = req.nextUrl?.searchParams as URLSearchParams;
|
||||||
|
const experimentLabel = searchParams.get("experimentLabel");
|
||||||
|
const variantsCount = searchParams.get("variantsCount");
|
||||||
|
const scenariosCount = searchParams.get("scenariosCount");
|
||||||
|
|
||||||
|
const inconsolataRegularFont = await inconsolataRegularFontP;
|
||||||
|
|
||||||
|
return new ImageResponse(
|
||||||
|
(
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: 48,
|
||||||
|
padding: "48px",
|
||||||
|
background: "white",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: 48,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src="https://app.openpipe.ai/logo.svg"
|
||||||
|
alt="OpenPipe Logo"
|
||||||
|
height={100}
|
||||||
|
width={120}
|
||||||
|
/>
|
||||||
|
<div style={{ marginLeft: 24, fontSize: 64, fontFamily: "Inconsolata" }}>OpenPipe</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", fontSize: 72, marginTop: 108 }}>{experimentLabel}</div>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", marginTop: 36 }}>
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
<span style={{ width: 320 }}>Variants:</span> {variantsCount}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", marginTop: 24 }}>
|
||||||
|
<span style={{ width: 320 }}>Scenarios:</span> {scenariosCount}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
fonts: [
|
||||||
|
{
|
||||||
|
name: "inconsolata",
|
||||||
|
data: inconsolataRegularFont,
|
||||||
|
style: "normal",
|
||||||
|
weight: 400,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OgImage;
|
||||||
@@ -9,8 +9,8 @@ export const openApiDocument = generateOpenApiDocument(appRouter, {
|
|||||||
baseUrl: "https://app.openpipe.ai/api",
|
baseUrl: "https://app.openpipe.ai/api",
|
||||||
});
|
});
|
||||||
// Respond with our OpenAPI schema
|
// Respond with our OpenAPI schema
|
||||||
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
const hander = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
res.status(200).send(openApiDocument);
|
res.status(200).send(openApiDocument);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handler;
|
export default hander;
|
||||||
|
|||||||
6
app/src/pages/api/sentry-example-api.js
Normal file
6
app/src/pages/api/sentry-example-api.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// 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" });
|
||||||
|
}
|
||||||
97
app/src/pages/data/[id].tsx
Normal file
97
app/src/pages/data/[id].tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
Center,
|
||||||
|
Flex,
|
||||||
|
Icon,
|
||||||
|
Input,
|
||||||
|
VStack,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { RiDatabase2Line } from "react-icons/ri";
|
||||||
|
import AppShell from "~/components/nav/AppShell";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { useDataset, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
import DatasetEntriesTable from "~/components/datasets/DatasetEntriesTable";
|
||||||
|
import { DatasetHeaderButtons } from "~/components/datasets/DatasetHeaderButtons/DatasetHeaderButtons";
|
||||||
|
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
|
||||||
|
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
|
||||||
|
|
||||||
|
export default function Dataset() {
|
||||||
|
const router = useRouter();
|
||||||
|
const utils = api.useContext();
|
||||||
|
|
||||||
|
const dataset = useDataset();
|
||||||
|
const datasetId = router.query.id as string;
|
||||||
|
|
||||||
|
const [name, setName] = useState(dataset.data?.name || "");
|
||||||
|
useEffect(() => {
|
||||||
|
setName(dataset.data?.name || "");
|
||||||
|
}, [dataset.data?.name]);
|
||||||
|
|
||||||
|
const updateMutation = api.datasets.update.useMutation();
|
||||||
|
const [onSaveName] = useHandledAsyncCallback(async () => {
|
||||||
|
if (name && name !== dataset.data?.name && dataset.data?.id) {
|
||||||
|
await updateMutation.mutateAsync({
|
||||||
|
id: dataset.data.id,
|
||||||
|
updates: { name: name },
|
||||||
|
});
|
||||||
|
await Promise.all([utils.datasets.list.invalidate(), utils.datasets.get.invalidate()]);
|
||||||
|
}
|
||||||
|
}, [updateMutation, dataset.data?.id, dataset.data?.name, name]);
|
||||||
|
|
||||||
|
if (!dataset.isLoading && !dataset.data) {
|
||||||
|
return (
|
||||||
|
<AppShell title="Dataset not found">
|
||||||
|
<Center h="100%">
|
||||||
|
<div>Dataset not found 😕</div>
|
||||||
|
</Center>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell title={dataset.data?.name}>
|
||||||
|
<VStack h="full">
|
||||||
|
<PageHeaderContainer>
|
||||||
|
<Breadcrumb>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<ProjectBreadcrumbContents orgName={dataset.data?.organization?.name} />
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<Link href="/data">
|
||||||
|
<Flex alignItems="center" _hover={{ textDecoration: "underline" }}>
|
||||||
|
<Icon as={RiDatabase2Line} boxSize={4} mr={2} /> Datasets
|
||||||
|
</Flex>
|
||||||
|
</Link>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbItem isCurrentPage>
|
||||||
|
<Input
|
||||||
|
size="sm"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
onBlur={onSaveName}
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor="transparent"
|
||||||
|
fontSize={16}
|
||||||
|
px={0}
|
||||||
|
minW={{ base: 100, lg: 300 }}
|
||||||
|
flex={1}
|
||||||
|
_hover={{ borderColor: "gray.300" }}
|
||||||
|
_focus={{ borderColor: "blue.500", outline: "none" }}
|
||||||
|
/>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</Breadcrumb>
|
||||||
|
<DatasetHeaderButtons />
|
||||||
|
</PageHeaderContainer>
|
||||||
|
<Box w="full" overflowX="auto" flex={1} px={8} pt={8} pb={16}>
|
||||||
|
{datasetId && <DatasetEntriesTable />}
|
||||||
|
</Box>
|
||||||
|
</VStack>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
app/src/pages/data/index.tsx
Normal file
49
app/src/pages/data/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { SimpleGrid, Icon, Breadcrumb, BreadcrumbItem, Flex } from "@chakra-ui/react";
|
||||||
|
import AppShell from "~/components/nav/AppShell";
|
||||||
|
import { RiDatabase2Line } from "react-icons/ri";
|
||||||
|
import {
|
||||||
|
DatasetCard,
|
||||||
|
DatasetCardSkeleton,
|
||||||
|
NewDatasetCard,
|
||||||
|
} from "~/components/datasets/DatasetCard";
|
||||||
|
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
|
||||||
|
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
|
||||||
|
import { useDatasets } from "~/utils/hooks";
|
||||||
|
|
||||||
|
export default function DatasetsPage() {
|
||||||
|
const datasets = useDatasets();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell title="Data" requireAuth>
|
||||||
|
<PageHeaderContainer>
|
||||||
|
<Breadcrumb>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<ProjectBreadcrumbContents />
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbItem minH={8}>
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<Icon as={RiDatabase2Line} boxSize={4} mr={2} /> Datasets
|
||||||
|
</Flex>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</Breadcrumb>
|
||||||
|
</PageHeaderContainer>
|
||||||
|
<SimpleGrid w="full" columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing={8} py={4} px={8}>
|
||||||
|
<NewDatasetCard />
|
||||||
|
{datasets.data && !datasets.isLoading ? (
|
||||||
|
datasets?.data?.map((dataset) => (
|
||||||
|
<DatasetCard
|
||||||
|
key={dataset.id}
|
||||||
|
dataset={{ ...dataset, numEntries: dataset._count.datasetEntries }}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<DatasetCardSkeleton />
|
||||||
|
<DatasetCardSkeleton />
|
||||||
|
<DatasetCardSkeleton />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SimpleGrid>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,14 +21,45 @@ import { api } from "~/utils/api";
|
|||||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
import { useAppStore } from "~/state/store";
|
import { useAppStore } from "~/state/store";
|
||||||
import { useSyncVariantEditor } from "~/state/sync";
|
import { useSyncVariantEditor } from "~/state/sync";
|
||||||
import { HeaderButtons } from "~/components/experiments/HeaderButtons/HeaderButtons";
|
import { ExperimentHeaderButtons } from "~/components/experiments/ExperimentHeaderButtons/ExperimentHeaderButtons";
|
||||||
|
import Head from "next/head";
|
||||||
|
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
|
||||||
|
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
|
||||||
|
|
||||||
|
// TODO: import less to fix deployment with server side props
|
||||||
|
// export const getServerSideProps = async (context: GetServerSidePropsContext<{ id: string }>) => {
|
||||||
|
// const experimentId = context.params?.id as string;
|
||||||
|
|
||||||
|
// const helpers = createServerSideHelpers({
|
||||||
|
// router: appRouter,
|
||||||
|
// ctx: createInnerTRPCContext({ session: null }),
|
||||||
|
// transformer: superjson, // optional - adds superjson serialization
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // prefetch query
|
||||||
|
// await helpers.experiments.stats.prefetch({ id: experimentId });
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// props: {
|
||||||
|
// trpcState: helpers.dehydrate(),
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
|
||||||
export default function Experiment() {
|
export default function Experiment() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const experiment = useExperiment();
|
|
||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
useSyncVariantEditor();
|
useSyncVariantEditor();
|
||||||
|
|
||||||
|
const experiment = useExperiment();
|
||||||
|
const experimentStats = api.experiments.stats.useQuery(
|
||||||
|
{ id: router.query.id as string },
|
||||||
|
{
|
||||||
|
enabled: !!router.query.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const stats = experimentStats.data;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
useAppStore.getState().sharedVariantEditor.loadMonaco().catch(console.error);
|
useAppStore.getState().sharedVariantEditor.loadMonaco().catch(console.error);
|
||||||
});
|
});
|
||||||
@@ -62,16 +93,24 @@ export default function Experiment() {
|
|||||||
const canModify = experiment.data?.access.canModify ?? false;
|
const canModify = experiment.data?.access.canModify ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{stats && (
|
||||||
|
<Head>
|
||||||
|
<meta property="og:title" content={stats.experimentLabel} key="title" />
|
||||||
|
<meta
|
||||||
|
property="og:image"
|
||||||
|
content={`/api/experiments/og-image?experimentLabel=${stats.experimentLabel}&variantsCount=${stats.promptVariantCount}&scenariosCount=${stats.testScenarioCount}`}
|
||||||
|
key="og-image"
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
)}
|
||||||
<AppShell title={experiment.data?.label}>
|
<AppShell title={experiment.data?.label}>
|
||||||
<VStack h="full">
|
<VStack h="full">
|
||||||
<Flex
|
<PageHeaderContainer>
|
||||||
px={4}
|
<Breadcrumb>
|
||||||
py={2}
|
<BreadcrumbItem>
|
||||||
w="full"
|
<ProjectBreadcrumbContents orgName={experiment.data?.organization?.name} />
|
||||||
direction={{ base: "column", sm: "row" }}
|
</BreadcrumbItem>
|
||||||
alignItems={{ base: "flex-start", sm: "center" }}
|
|
||||||
>
|
|
||||||
<Breadcrumb flex={1}>
|
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<Link href="/experiments">
|
<Link href="/experiments">
|
||||||
<Flex alignItems="center" _hover={{ textDecoration: "underline" }}>
|
<Flex alignItems="center" _hover={{ textDecoration: "underline" }}>
|
||||||
@@ -102,13 +141,14 @@ export default function Experiment() {
|
|||||||
)}
|
)}
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
<HeaderButtons />
|
<ExperimentHeaderButtons />
|
||||||
</Flex>
|
</PageHeaderContainer>
|
||||||
<ExperimentSettingsDrawer />
|
<ExperimentSettingsDrawer />
|
||||||
<Box w="100%" overflowX="auto" flex={1}>
|
<Box w="100%" overflowX="auto" flex={1}>
|
||||||
<OutputsTable experimentId={router.query.id as string | undefined} />
|
<OutputsTable experimentId={router.query.id as string | undefined} />
|
||||||
</Box>
|
</Box>
|
||||||
</VStack>
|
</VStack>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +1,33 @@
|
|||||||
import {
|
import { SimpleGrid, Icon, Breadcrumb, BreadcrumbItem, Flex } from "@chakra-ui/react";
|
||||||
SimpleGrid,
|
|
||||||
Icon,
|
|
||||||
VStack,
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
Flex,
|
|
||||||
Center,
|
|
||||||
Text,
|
|
||||||
Link,
|
|
||||||
HStack,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { RiFlaskLine } from "react-icons/ri";
|
import { RiFlaskLine } from "react-icons/ri";
|
||||||
import AppShell from "~/components/nav/AppShell";
|
import AppShell from "~/components/nav/AppShell";
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import {
|
import {
|
||||||
ExperimentCard,
|
ExperimentCard,
|
||||||
ExperimentCardSkeleton,
|
ExperimentCardSkeleton,
|
||||||
NewExperimentCard,
|
NewExperimentCard,
|
||||||
} from "~/components/experiments/ExperimentCard";
|
} from "~/components/experiments/ExperimentCard";
|
||||||
import { signIn, useSession } from "next-auth/react";
|
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
|
||||||
|
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
|
||||||
|
import { useExperiments } from "~/utils/hooks";
|
||||||
|
|
||||||
export default function ExperimentsPage() {
|
export default function ExperimentsPage() {
|
||||||
const experiments = api.experiments.list.useQuery();
|
const experiments = useExperiments();
|
||||||
|
|
||||||
const user = useSession().data;
|
|
||||||
const authLoading = useSession().status === "loading";
|
|
||||||
|
|
||||||
if (user === null || authLoading) {
|
|
||||||
return (
|
|
||||||
<AppShell title="Experiments">
|
|
||||||
<Center h="100%">
|
|
||||||
{!authLoading && (
|
|
||||||
<Text>
|
|
||||||
<Link
|
|
||||||
onClick={() => {
|
|
||||||
signIn("github").catch(console.error);
|
|
||||||
}}
|
|
||||||
textDecor="underline"
|
|
||||||
>
|
|
||||||
Sign in
|
|
||||||
</Link>{" "}
|
|
||||||
to view or create new experiments!
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Center>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell title="Experiments">
|
<AppShell title="Experiments" requireAuth>
|
||||||
<VStack alignItems={"flex-start"} px={4} py={2}>
|
<PageHeaderContainer>
|
||||||
<HStack minH={8} align="center" pt={2}>
|
<Breadcrumb>
|
||||||
<Breadcrumb flex={1}>
|
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
|
<ProjectBreadcrumbContents />
|
||||||
|
</BreadcrumbItem>
|
||||||
|
<BreadcrumbItem minH={8}>
|
||||||
<Flex alignItems="center">
|
<Flex alignItems="center">
|
||||||
<Icon as={RiFlaskLine} boxSize={4} mr={2} /> Experiments
|
<Icon as={RiFlaskLine} boxSize={4} mr={2} /> Experiments
|
||||||
</Flex>
|
</Flex>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</HStack>
|
</PageHeaderContainer>
|
||||||
<SimpleGrid w="full" columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing={8} p="4">
|
<SimpleGrid w="full" columns={{ base: 1, md: 2, lg: 3, xl: 4 }} spacing={8} py="4" px={8}>
|
||||||
<NewExperimentCard />
|
<NewExperimentCard />
|
||||||
{experiments.data && !experiments.isLoading ? (
|
{experiments.data && !experiments.isLoading ? (
|
||||||
experiments?.data?.map((exp) => <ExperimentCard key={exp.id} exp={exp} />)
|
experiments?.data?.map((exp) => <ExperimentCard key={exp.id} exp={exp} />)
|
||||||
@@ -72,7 +39,6 @@ export default function ExperimentsPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</VStack>
|
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
181
app/src/pages/logged-calls/index.tsx
Normal file
181
app/src/pages/logged-calls/index.tsx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user